UE4构建时未定义的引用的链接问题
UE4版本 v4.26
操作系统 CentOS 7
编译器 clang version 10.0.1
问题
项目流水线执行 BuildEngine
构建引擎,时不时出现 undefined reference
的链接错误,重试后可构建成功。
时常出现,重试成功,极大可能是链接时序问题。
复现
本地服务器搭建类似环境,尝试复现该问题。
查阅 UE4 构建工具的代码,在合适地方添加日志,分析日志结合代码,最终确定问题所在。
UE4Editor
可执行文件 依赖于 libUE4Editor-Engine.so
,libUE4Editor-Engine.so
依赖于 libUE4Editor-Xyz.so
,UE4Editor
不直接依赖 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 重链接Action
和 UE4Editor 链接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 | /// <summary> |
文件 LinuxToolChain.cs
中 添加代码。
1 | RelinkAction.bProducesImportLibrary = LinkAction.bProducesImportLibrary; |
bProducesImportLibrary
这个字段后面会用来区分构建的是可执行程序还是动态库,因为只有可执行程序链接时,才会出现问
题。这里让 重链接Action 跟 第一次链接Action 保持一致。
1 | LinkAction.RelatedActions.Add(RelinkAction); |
记录下 第一次链接Action 对应的 重链接Action。
文件 ActionsGraph.cs
的 ActionGraph::Link
中添加代码。
1 | if (!Action.bProducesImportLibrary) |
把 重链接Action 添加到依赖于 第一次链接Action 的可执行文件链接Action的依赖列表,这样可执行文件一定会在 重链接Action 执行完成即完整so生成后,才会开始链接。
修改后观察,链接成功,问题已解决。
扩展
假设 c.exe
依赖于 libb.so
,libb.so
依赖于 liba.so
。
不指定依赖库
构建 libb.so
时不指定依赖库
1 | clang -shared -fPIC -o libb.so b.c |
ldd libb.so
结果
1 | linux-vdso.so.1 (0x00007fff96b3a000) |
使用 LLVM ld
构建 c.exe
时不指定依赖库,不指定链接参数
1 | clang -fuse-ld=lld -o c.exe c.c -Wl,--start-group -Wl,--end-group |
1 | ld.lld: error: undefined symbol: cal |
构建 c.exe
时指定依赖库,不指定链接参数
1 | clang -fuse-ld=lld -o c.exe c.c -L. -Wl,--start-group -lb -Wl,--end-group |
1 | ld.lld: error: ./libb.so: undefined reference to sum |
构建 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 | ld.lld: error: ./libb.so: undefined reference to sum |
构建 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 | x86_64-unknown-linux-gnu-ld: /tmp/c-d47523.o: in function `main': |
构建 c.exe
时指定依赖库,不指定链接参数,使用 GNU ld
1 | clang -o c.exe c.c -L. -Wl,--start-group -lb -Wl,--end-group |
1 | x86_64-unknown-linux-gnu-ld: ./libb.so: undefined reference to `sum' |
构建 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 | linux-vdso.so.1 (0x00007ffee9716000) |
使用 LLVM ld
构建 c.exe
时不指定依赖库,不指定链接参数
1 | clang -fuse-ld=lld -o c.exe c.c -Wl,--start-group -Wl,--end-group |
1 | ld.lld: error: undefined symbol: cal |
构建 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 | x86_64-unknown-linux-gnu-ld: /tmp/c-d74279.o: in function `main': |
构建 c.exe
时指定依赖库,不指定链接参数,使用 GNU ld
1 | clang -o c.exe c.c -L. -Wl,--start-group -lb -Wl,--end-group |
1 | bin/x86_64-unknown-linux-gnu-ld: warning: liba.so, needed by ./libb.so, not found (try using -rpath or -rpath-link) |
构建 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
1 | --unresolved-symbols=method |
1 | --allow-shlib-undefined |
LLVM ld.lld
1 | --allow-shlib-undefined |
1 | --unresolved-symbols -= value |