C++库菱形依赖下的二进制重复问题及CMake处理问询
本文针对嵌入式开发中常见的菱形依赖(C依赖A和B,A依赖B)场景,分情况解析二进制冗余问题及CMake的链接行为。
1. A暴露B的符号(A的接口中直接引用B的符号)
1.1 不使用CMake的情况
编译规则:编译A时依赖B(静态库仅需头文件,动态库需链接B);编译C时使用-lA -lB。以下分四种库组合说明:
A静态、B静态
链接C时,链接器先从libA.a提取A的目标文件(.o),发现其引用B的符号后,从libB.a提取对应.o;接着处理libB.a时,会跳过已提取过的、被C直接引用的B的.o。最终C的二进制中仅包含一份B的所有被A或C引用的代码,无冗余。A静态、B动态
链接C时,libA.a的.o引用B的符号,链接器会绑定动态库libB.so;重复指定的-lB会被链接器忽略。最终C的二进制中不包含B的代码,运行时仅加载一份libB.so实例,无冗余。A动态、B静态
编译libA.so时,链接器会将libB.a中被A引用的.o打包进动态库(动态库需独立解析符号)。链接C时,-lB会将libB.a中被C直接引用的.o打包进C。此时B的代码被重复包含:libA.so中有A用到的部分,C的可执行文件中有C用到的部分,无法去重。A动态、B动态
编译libA.so时仅记录对libB.so的符号引用;链接C时,-lA -lB会合并动态库依赖,最终C仅绑定libB.so。运行时仅加载一份libB.so实例,无冗余。
1.2 使用CMake的情况
配置规则:A通过target_link_libraries(A PUBLIC B)链接B,C通过target_link_libraries(C PRIVATE A B)链接A和B。
由于CMake会自动处理依赖传递和去重,最终生成的链接命令与手动写-lA -lB完全一致,因此结果与1.1中的四种组合完全相同:
- 静态A+静态B:C仅含一份B的必要代码
- 静态A+动态B:C链接动态B,无冗余
- 动态A+静态B:A的动态库与C中各含部分B的代码,存在冗余
- 动态A+动态B:运行时加载一份B,无冗余
2. A不暴露B的符号(A仅内部使用B,接口无B的符号)
2.1 不使用CMake的情况
编译规则:编译A时依赖B;编译C时使用-lA -lB。以下分四种库组合说明:
A静态、B静态
链接逻辑与1.1的静态组合一致:链接器会从libB.a中提取所有被A或C引用的.o,重复的.o会被跳过。最终C的二进制中仅包含一份B的所有被引用的代码,无冗余。A静态、B动态
与1.1的静态A+动态B结果一致:C绑定动态库libB.so,运行时仅加载一份实例,无冗余。A动态、B静态
编译libA.so时,会将A引用的B的.o打包进动态库,且由于A不暴露B的符号,这些符号会被标记为局部符号。链接C时,-lB会将C引用的B的.o打包进C,链接器无法识别局部符号与外部符号的关联性,因此B的代码被重复包含。A动态、B动态
与1.1的动态A+动态B结果一致:运行时仅加载一份libB.so实例,无冗余。
2.2 使用CMake的情况
配置规则:A通过target_link_libraries(A PRIVATE B)链接B,C通过target_link_libraries(C PRIVATE A B)链接A和B。
由于A的PRIVATE链接不会将B传递给C,用户手动让C链接B后,CMake生成的链接命令仍为-lA -lB,因此结果与2.1中的四种组合完全相同。
嵌入式场景选型建议
为避免菱形依赖导致的二进制体积冗余,优先选择以下方案:
- 使用动态库B:无论A是静态还是动态库,都不会出现代码冗余,运行时仅加载一份B的实例。
- A和B均为静态库:链接器可全局去重,确保C中仅包含一份B的必要代码。
- 避免A为动态库、B为静态库的组合:这种场景必然会导致B的代码被重复打包,无法通过链接器去重。
内容的提问来源于stack exchange,提问作者Oersted

