在 C++ 中使用 Protobuf 诡异的字段丢失问题排查这篇文章中,分析过因为两个一样的 proto 文件,导致链接错了 pb ,最终反序列化的时候丢失了部分字段。当时也提到过符号决议的过程,不管是动态链接还是静态链接,实际用的都是靠前面的库的符号定义。本来以为对这里的理解很深入了,直到最近又遇见一个奇怪的“符号重定义”问题。
➜ tree
.
├── demoA
│ ├── libDemoA.a
│ ├── sum.cpp
│ ├── sum.h
│ └── sum.o
├── demoB
│ ├── libDemoB.a
│ ├── sum.cpp
│ ├── sum.h
│ └── sum.o
└── main.cpp
问题: demoA/sum.h 和 demoB/sum.h 如果都是只有 sum 函数,那么无论哪个先链接,都不会有问题。 但是一旦里面有 class ,定义不一样,那么就会出错。
更多可以看这篇: 深入理解 C++ 链接符号决议:从符号重定义说起
1
codehz 2023-09-19 16:28:34 +08:00
没报错的情况是不是可以报告成连接器的 bug 呢,至少应该给个警告(
|
3
codehz 2023-09-19 17:23:40 +08:00
@xuelang 怎么就不是 bug 了,根据那啥 ODR 规则,就不能存在同名的符号,但具体到这个情况,倒是有可能被归类为无须诊断的非良构程序
不能因为恰好通过编译链接,能运行,它就突然变成良构的了 |
4
zmcity 2023-09-19 17:43:58 +08:00
因为链接是通过函数签名选需要链接的产物(.o ),而没有把每个签名对应的函数单独提取出来。
你先链了 A ,就会把 A.o 链接到产物里,然后 A 中又没有 DemoB(int, int)的签名,所以会把 B.o 链上,就会 multiple definition 。 |
5
yolee599 2023-09-19 17:55:18 +08:00
为啥你们写 C/C++ 那么多奇怪的问题,就是骚操作太多,我写 C 写了几年都没这么多屁事
|
6
xuelang OP @codehz 你说的也有道理,严格来说确实不能有同名符号,至少也得给个 warning 。不然换个链接顺序,结果可能都不一样,这个是不符合预期。
|
9
cnbatch 2023-09-19 19:25:43 +08:00
说实话,既然都用 C++了,那就直接用 namespace 分隔开嘛,何必这样搞成个歧义二进制折磨自己
|
11
geelaw 2023-09-19 20:10:33 +08:00 via iPhone 2
无论是看代码还是问 ChatGPT 却不查证都是非常糟糕的学习方法,第一步应该是理解 C++ 标准是如何规定的。
文章里无论是 int sum(int, int) 还是 class Demo 都是非常严重的 ODR violation 。 在 [basic.def.odr]/14 里规定了 (14.1) 非内联非模板函数在多个翻译单元中有定义时 (14) 程序不良,且在非模块中无需报错,这适用于 sum 的情况。 在 [basic.def.odr]/14 里规定了 (14.2) 多个翻译单元中有定义的 class 如果不满足 (14.4) 在所有可达的翻译单元中定义是相同的记号( token )序列,则 (14) 程序不良,且在非模块中无需报错,这适用于 class Demo 的情况。 至于某个具体的编译器、链接器产生的什么行为,不过是巧合罢了。 |
12
xuelang OP 嗯嗯,十分同意,标准才是硬道理,写代码也都要注意到这些。
就算 sum 这种能编译,链接成功,也是一个很坏的代码习惯。能正常运行的,结果符合预期的,也不一定就是对的实现,可能是编译器的巧合行为,说不定后面就不行了。 |
13
allAboutDbmss 2023-09-19 20:16:16 +08:00
rust 有类似问题吗?
|
14
geelaw 2023-09-19 20:20:00 +08:00 via iPhone 1
满足 ODR 的程序,如果同一个名字在多个翻译单元里有定义,那么任意可达的定义都是等价的,因此可以随便选(比如第一个赢)。
随便选最有意义的作用是模板,因为每个使用了某个模板的翻译单元必须包含那个模板的完整定义(需要 SFINAE 和 name lookup ),不能让模板声明在 .hh 里实现在 .cc 里。 更早的时候链接时优化还不流行,因此内联( C++ 意义的 inline )函数可以促进函数在编译时被内联(优化意义的 inline )。 链接并不是“决议”工具。 |
16
tool2d 2023-09-19 21:46:40 +08:00 2
这算是 gcc 的问题,你换 vc 一开始 sum 就不能链接成功。
符号一样,什么前面的函数体去覆盖后面的函数体,对于微软来说,是完全不存在的事情。 还有一点,linux so 动态链接库里的符号可以是未决的,但是 dll 缺一个函数,都没办法生成。光是这点,微软就已经领先 100 年。 |
17
geelaw 2023-09-19 22:09:16 +08:00 via iPhone 1
@xuelang #15 链接并不能用来挑选(“决议”) A 的这个定义或那个定义,A 自始自终都只有一个定义。
|
20
geelaw 2023-09-20 12:46:27 +08:00 1
@xuelang #18 汉语“决议”是“会议所确定的较重大的事项”的意思,在计算机技术方面应该理解为复杂的选择过程,比如“重载决议”是可接受的说法,因为要根据形参列表和实参列表从多个可能的选项里根据一系列不那么直接的规则选出最合适的。链接里的 symbol resolution 的 resolution 应该译作“解析”,因为这个过程只是简单地查找符号的定义,不存在复杂的规则。
|
22
MaskRay 2023-11-11 15:51:18 +08:00 1
ODR violation 参见 https://maskray.me/blog/2022-11-13-odr-violation-detection 我打算把这个检测工具写完……
> 还有一点,linux so 动态链接库里的符号可以是未决的,但是 dll 缺一个函数,都没办法生成。光是这点,微软就已经领先 100 年。 这个可以怪 ELF linkers 默认选得不好(-z undefs)。如果链接 DSO 时用-z defs 就不会未决了 https://maskray.me/blog/2021-06-13-dependency-related-linker-options#z-defs |