UE4版本 v4.26

操作系统 CentOS 7

编译器 clang version 10.0.1

问题

项目流水线执行 BuildEngine 构建引擎,时不时出现 undefined reference 的链接错误,重试后可构建成功。

时常出现,重试成功,极大可能是链接时序问题。

复现

本地服务器搭建类似环境,尝试复现该问题。

查阅 UE4 构建工具的代码,在合适地方添加日志,分析日志结合代码,最终确定问题所在。

UE4Editor 可执行文件 依赖于 libUE4Editor-Engine.solibUE4Editor-Engine.so 依赖于 libUE4Editor-Xyz.soUE4Editor 不直接依赖 libUE4Editor-Xyz.so

链接错误出现在链接生成 UE4Editor 的时候,需要去链接 libUE4Editor-Engine.so,此时报错找不到 libUE4Editor-Xyz.so 中的符号, 即 undefined reference

分析

分析链接过程。

链接生成 libUE4Editor-Engine.so

libUE4Editor-Engine.so 包含引擎核心代码,其会引用大量依赖其他模块,也有一些其他模块依赖它,这样就出现了模块循环依赖。

UE4 构建时,考虑到了循环依赖情况,会采取类似下面措施处理。

libUE4Editor-Engine.so 这种出现循环依赖的库,会进行两次链接。

第一次链接仅链接生成库自身,链接脚本 Link-libUE4Editor-Engine.so.link.sh

关键链接参数:

1
-Wl,--start-group -lpthread -lrt -ldl -Wl,--allow-shlib-undefined -Wl,--end-group
  • 没有指定依赖的库 这样生成的 so 中不包含依赖库信息。
  • 指定允许未定义符号参数 -Wl,--allow-shlib-undefined 该参数会在链接时忽略未定义的符号,从而即使找不到依赖库的符号,也不会报错。

第二次重链接会生成完整的库,链接脚本 Relink-libUE4Editor-Engine.so.sh

关键链接参数:

1
-Wl,--start-group -lpthread -lrt -ldl -lUE4Editor-Xyz -Wl,--end-group
  • 指定了依赖的库 参数 -lUE4Editor-Xyz 表示其依赖于 libUE4Editor-Xyz.so
  • 没有指定允许未定义符号参数 默认情况下,链接可执行程序时有符号未定义,就会报错,但链接动态库时,会忽略未定义符号,不会报错。

链接生成 UE4Editor

关键链接参数:

1
--unresolved-symbols=ignore-in-shared-libs --start-group -lUE4Editor-Xyz --unresolved-symbols=ignore-in-shared-libs --end-group
  • 指定了未定义符号处理策略参数 --unresolved-symbols=ignore-in-shared-libs 会忽略来自动态库中的未定义符号,不会报错。

观察上述信息,如果出现符号未定义的链接错误,需要满足两点。

  • 链接参数指定的未定义符号处理策略参数无效,这样链接的时候就还是会去找动态库中未定义的符号。
  • 未定义的符号找不到,意味着其所在的依赖库信息找不到,也意味着链接生成可执行程序 UE4Editor时,去链接的动态库 libUE4Editor-Engine.so,是第一次链接生成的不包含依赖库信息的库,而不是重链接生成的那个完整的库。

定位

UE4的每个编译或链接等操作,都被定义成一个 Action。

此例中,仅考虑链接情况,有4个Action。

  • libUE4Editor-Xyz.so 链接Action
  • libUE4Editor-Engine.so 链接Action
  • libUE4Editor-Engine.so 重链接Action
  • UE4Editor 链接Action

Action彼此之间有依赖关系,被依赖的Action先执行完成后,才会执行依赖Action。

到这里一切看似正常,但 UE4 实际实现时,存在一个问题,重链接Action不会被依赖。

此例中,UE4Editor 链接Action 应该依赖于 libUE4Editor-Engine.so 重链接Action,这样才能保证其链接的是一个完整库。但其实际依赖的仅是 libUE4Editor-Engine.so 链接Action

libUE4Editor-Engine.so 重链接ActionUE4Editor 链接Action 都依赖于 libUE4Editor-Engine.so 链接Action,但彼此间没有依赖关系。所以在实际构建时,时序不确定。

  • 如果Engine重链接Action执行完成后,Editor才开始链接,Editor链接的就是完整的Engine.so,此时正常。

  • 如果Engine重链接Action执行中,Editor已经开始链接,Editor链接的就是Engine第一次链接后的那个不完整的Engine.so,这个so中不包含依赖库信息,所以Editor也就找不到 libUE4Editor-Xyz.so

问题就出在第二种情况下。找不到Xyz.so,当然其中的符号就是未定义了,此时报错 undefined reference

但前面已经分析过,链接过程中指定了 --unresolved-symbols=ignore-in-shared-libs,会忽略未定义符号,不应该报错才对。

经过对比测试,在使用 LLVM ld.lld 链接器时才会出现该问题,使用 GUN ld 链接器没有问题。LLVM ld.lld 链接器中这个参数没有效果(至少是当前使用的这个版本 LLD 10.0.1 (compatible with GNU linkers))。

解决

方案1

换一个 LLVM ld.lld 链接器支持的参数。

修改文件 LinuxToolChain.cs 中的代码,将忽略未定义符号链接参数 --unresolved-symbols=ignore-in-shared-libs 替换为 --allow-shlib-undefined

方案2

UE4Editor 链接Action 依赖于 libUE4Editor-Engine.so 重链接Action

文件 Actions.cs 中的 class Action 中添加一个字段,保存其关联的Action,即用来在 链接Action 中保存其对应的 重链接Action。

1
2
3
4
/// <summary>
/// The actions that this action related(e.g. relink)
/// </summary>
public List<Action> RelatedActions = new List<Action>();

文件 LinuxToolChain.cs 中 添加代码。

1
RelinkAction.bProducesImportLibrary = LinkAction.bProducesImportLibrary;

bProducesImportLibrary 这个字段后面会用来区分构建的是可执行程序还是动态库,因为只有可执行程序链接时,才会出现问
题。这里让 重链接Action 跟 第一次链接Action 保持一致。

1
LinkAction.RelatedActions.Add(RelinkAction);

记录下 第一次链接Action 对应的 重链接Action。

文件 ActionsGraph.csActionGraph::Link 中添加代码。

1
2
3
4
5
6
7
if (!Action.bProducesImportLibrary)
{
foreach (Action RelatedAction in PrerequisiteAction.RelatedActions)
{
Action.PrerequisiteActions.Add(RelatedAction);
}
}

把 重链接Action 添加到依赖于 第一次链接Action 的可执行文件链接Action的依赖列表,这样可执行文件一定会在 重链接Action 执行完成即完整so生成后,才会开始链接。

修改后观察,链接成功,问题已解决。

扩展

假设 c.exe 依赖于 libb.solibb.so 依赖于 liba.so

不指定依赖库

构建 libb.so 时不指定依赖库

1
clang -shared -fPIC -o libb.so b.c

ldd libb.so 结果

1
2
3
4
5
linux-vdso.so.1 (0x00007fff96b3a000)
/$LIB/libonion.so => /lib64/libonion.so (0x00007f52dc3bc000)
libc.so.6 => /lib64/libc.so.6 (0x00007f52dc1ec000)
libdl.so.2 => /lib64/libdl.so.2 (0x00007f52dc1e5000)
/lib64/ld-linux-x86-64.so.2 (0x00007f52dc3cb000)

使用 LLVM ld


构建 c.exe 时不指定依赖库,不指定链接参数

1
clang -fuse-ld=lld -o c.exe c.c -Wl,--start-group -Wl,--end-group
1
2
3
4
ld.lld: error: undefined symbol: cal
>>> referenced by c.c
>>> /tmp/c-e8630d.o:(main)
clang: error: linker command failed with exit code 1 (use -v to see invocation)

构建 c.exe 时指定依赖库,不指定链接参数

1
clang -fuse-ld=lld -o c.exe c.c -L. -Wl,--start-group -lb -Wl,--end-group
1
2
ld.lld: error: ./libb.so: undefined reference to sum
clang: error: linker command failed with exit code 1 (use -v to see invocation)

构建 c.exe 时指定依赖库,指定链接参数 --unresolved-symbols=ignore-in-shared-libs

1
clang -fuse-ld=lld -o c.exe c.c -L. -Wl,--start-group -lb -Wl,--unresolved-symbols=ignore-in-shared-libs -Wl,--end-group
1
2
ld.lld: error: ./libb.so: undefined reference to sum
clang: error: linker command failed with exit code 1 (use -v to see invocation)

构建 c.exe 时指定依赖库,指定链接参数 --allow-shlib-undefined

1
clang -fuse-ld=lld -o c.exe c.c -L. -Wl,--start-group -lb -Wl,--allow-shlib-undefined -Wl,--end-group
1
构建成功

使用 GNU ld


构建 c.exe 时不指定依赖库,不指定链接参数,使用 GNU ld

1
clang -o c.exe c.c -Wl,--start-group -Wl,--end-group
1
2
3
x86_64-unknown-linux-gnu-ld: /tmp/c-d47523.o: in function `main':
c.c:(.text+0x1a): undefined reference to `cal'
clang: error: linker command failed with exit code 1 (use -v to see invocation)

构建 c.exe 时指定依赖库,不指定链接参数,使用 GNU ld

1
clang -o c.exe c.c -L. -Wl,--start-group -lb -Wl,--end-group
1
2
x86_64-unknown-linux-gnu-ld: ./libb.so: undefined reference to `sum'
clang: error: linker command failed with exit code 1 (use -v to see invocation)

构建 c.exe 时指定依赖库,指定链接参数 --unresolved-symbols=ignore-in-shared-libs,使用 GNU ld

1
clang -o c.exe c.c -L. -Wl,--start-group -lb -Wl,--unresolved-symbols=ignore-in-shared-libs -Wl,--end-group
1
构建成功

构建 c.exe 时指定依赖库,指定链接参数 --allow-shlib-undefined,使用 GNU ld

1
clang -o c.exe c.c -L. -Wl,--start-group -lb -Wl,--allow-shlib-undefined -Wl,--end-group
1
构建成功

指定依赖库

构建 libb.so 时指定依赖库

1
clang -shared -fPIC -o libb.so b.c -L. -la

ldd libb.so 结果

1
2
3
4
5
6
linux-vdso.so.1 (0x00007ffee9716000)
/$LIB/libonion.so => /lib64/libonion.so (0x00007f6f815bb000)
liba.so => not found
libc.so.6 => /lib64/libc.so.6 (0x00007f6f813eb000)
libdl.so.2 => /lib64/libdl.so.2 (0x00007f6f813e4000)
/lib64/ld-linux-x86-64.so.2 (0x00007f6f815ca000)

使用 LLVM ld


构建 c.exe 时不指定依赖库,不指定链接参数

1
clang -fuse-ld=lld -o c.exe c.c -Wl,--start-group -Wl,--end-group
1
2
3
4
ld.lld: error: undefined symbol: cal
>>> referenced by c.c
>>> /tmp/c-cb2dc3.o:(main)
clang: error: linker command failed with exit code 1 (use -v to see invocation)

构建 c.exe 时指定依赖库,不指定链接参数

1
clang -fuse-ld=lld -o c.exe c.c -L. -Wl,--start-group -lb -Wl,--end-group
1
构建成功

构建 c.exe 时指定依赖库,指定链接参数 --unresolved-symbols=ignore-in-shared-libs

1
clang -fuse-ld=lld -o c.exe c.c -L. -Wl,--start-group -lb -Wl,--unresolved-symbols=ignore-in-shared-libs -Wl,--end-group
1
构建成功

构建 c.exe 时指定依赖库,指定链接参数 --allow-shlib-undefined

1
clang -fuse-ld=lld -o c.exe c.c -L. -Wl,--start-group -lb -Wl,--allow-shlib-undefined -Wl,--end-group
1
构建成功

使用 GNU ld


构建 c.exe 时不指定依赖库,不指定链接参数,使用 GNU ld

1
clang -o c.exe c.c -Wl,--start-group -Wl,--end-group
1
2
3
x86_64-unknown-linux-gnu-ld: /tmp/c-d74279.o: in function `main':
c.c:(.text+0x1a): undefined reference to `cal'
clang: error: linker command failed with exit code 1 (use -v to see invocation)

构建 c.exe 时指定依赖库,不指定链接参数,使用 GNU ld

1
clang -o c.exe c.c -L. -Wl,--start-group -lb -Wl,--end-group
1
2
3
bin/x86_64-unknown-linux-gnu-ld: warning: liba.so, needed by ./libb.so, not found (try using -rpath or -rpath-link)
x86_64-unknown-linux-gnu-ld: ./libb.so: undefined reference to `sum'
clang: error: linker command failed with exit code 1 (use -v to see invocation)

构建 c.exe 时指定依赖库,指定链接参数 --unresolved-symbols=ignore-in-shared-libs,使用 GNU ld

1
clang -o c.exe c.c -L. -Wl,--start-group -lb -Wl,--unresolved-symbols=ignore-in-shared-libs -Wl,--end-group
1
构建成功

构建 c.exe 时指定依赖库,指定链接参数 --allow-shlib-undefined,使用 GNU ld

1
clang -o c.exe c.c -L. -Wl,--start-group -lb -Wl,--allow-shlib-undefined -Wl,--end-group
1
构建成功

参考信息

GNU ld

Manual

1
2
3
4
5
6
7
8
9
10
11
12
--unresolved-symbols=method
Determine how to handle unresolved symbols. There are four possible values for method:
ignore-all
Do not report any unresolved symbols.
report-all
Report all unresolved symbols. This is the default.
ignore-in-object-files
Report unresolved symbols that are contained in shared libraries, but ignore them if they come from regular object files.
ignore-in-shared-libs
Report unresolved symbols that come from regular object files, but ignore them if they come from shared libraries. This can be useful when creating a dynamic binary and it is known that all the shared libraries that it should be referencing are included on the linker's command line.
The behaviour for shared libraries on their own can also be controlled by the --[no-]allow-shlib-undefined option.
Normally the linker will generate an error message for each reported unresolved symbol but the option --warn-unresolved-symbols can change this to a warning.
1
2
3
4
5
6
7
8
9
10
11
--allow-shlib-undefined
--no-allow-shlib-undefined
Allows or disallows undefined symbols in shared libraries. This switch is similar to --no-undefined except that it determines the behaviour when the undefined symbols are in a shared library rather than a regular object file. It does not affect how undefined symbols in regular object files are handled.
The default behaviour is to report errors for any undefined symbols referenced in shared libraries if the linker is being used to create an executable, but to allow them if the linker is being used to create a shared library.

The reasons for allowing undefined symbol references in shared libraries specified at link time are that:

• A shared library specified at link time may not be the same as the one that is available at load time, so the symbol might actually be resolvable at load time.
• There are some operating systems, eg BeOS and HPPA , where undefined symbols in shared libraries are normal.

The BeOS kernel for example patches shared libraries at load time to select whichever function is most appropriate for the current architecture. This is used, for example, to dynamically select an appropriate memset function.

LLVM ld.lld

Manual

1
2
--allow-shlib-undefined
Allow unresolved references in shared libraries. This option is enabled by default when linking a shared library.
1
2
--unresolved-symbols -= value
Determine how to handle unresolved symbols.