C++ 链接符号决议 -- 自以为懂了,结果...

2023-09-19 16:09:34 +08:00
 xuelang

在 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++ 链接符号决议:从符号重定义说起

1920 次点击
所在节点    C++
23 条回复
codehz
2023-09-19 16:28:34 +08:00
没报错的情况是不是可以报告成连接器的 bug 呢,至少应该给个警告(
xuelang
2023-09-19 16:41:45 +08:00
@codehz 不是 bug ,是 feature 的
codehz
2023-09-19 17:23:40 +08:00
@xuelang 怎么就不是 bug 了,根据那啥 ODR 规则,就不能存在同名的符号,但具体到这个情况,倒是有可能被归类为无须诊断的非良构程序
不能因为恰好通过编译链接,能运行,它就突然变成良构的了
zmcity
2023-09-19 17:43:58 +08:00
因为链接是通过函数签名选需要链接的产物(.o ),而没有把每个签名对应的函数单独提取出来。
你先链了 A ,就会把 A.o 链接到产物里,然后 A 中又没有 DemoB(int, int)的签名,所以会把 B.o 链上,就会 multiple definition 。
yolee599
2023-09-19 17:55:18 +08:00
为啥你们写 C/C++ 那么多奇怪的问题,就是骚操作太多,我写 C 写了几年都没这么多屁事
xuelang
2023-09-19 18:00:00 +08:00
@codehz 你说的也有道理,严格来说确实不能有同名符号,至少也得给个 warning 。不然换个链接顺序,结果可能都不一样,这个是不符合预期。
xuelang
2023-09-19 18:00:42 +08:00
@zmcity 这里 a 是静态库归档文件,里面有很多 .o ,如果某个 .o 里面的符号都没用到,就会丢掉。
xuelang
2023-09-19 18:01:14 +08:00
@yolee599 c 和 c++ 半斤八两吧。。 奇怪问题也不少
cnbatch
2023-09-19 19:25:43 +08:00
说实话,既然都用 C++了,那就直接用 namespace 分隔开嘛,何必这样搞成个歧义二进制折磨自己
xuelang
2023-09-19 19:58:00 +08:00
@cnbatch 理想很美好。。 现实庞大代码仓库里的代码可不受金科玉律控制,各种奇葩问题都有。
geelaw
2023-09-19 20:10:33 +08:00
无论是看代码还是问 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 的情况。

至于某个具体的编译器、链接器产生的什么行为,不过是巧合罢了。
xuelang
2023-09-19 20:15:17 +08:00
嗯嗯,十分同意,标准才是硬道理,写代码也都要注意到这些。
就算 sum 这种能编译,链接成功,也是一个很坏的代码习惯。能正常运行的,结果符合预期的,也不一定就是对的实现,可能是编译器的巧合行为,说不定后面就不行了。
allAboutDbmss
2023-09-19 20:16:16 +08:00
rust 有类似问题吗?
geelaw
2023-09-19 20:20:00 +08:00
满足 ODR 的程序,如果同一个名字在多个翻译单元里有定义,那么任意可达的定义都是等价的,因此可以随便选(比如第一个赢)。

随便选最有意义的作用是模板,因为每个使用了某个模板的翻译单元必须包含那个模板的完整定义(需要 SFINAE 和 name lookup ),不能让模板声明在 .hh 里实现在 .cc 里。

更早的时候链接时优化还不流行,因此内联( C++ 意义的 inline )函数可以促进函数在编译时被内联(优化意义的 inline )。

链接并不是“决议”工具。
xuelang
2023-09-19 20:25:59 +08:00
@geelaw 链接并不是“决议”工具?这里怎么理解
tool2d
2023-09-19 21:46:40 +08:00
这算是 gcc 的问题,你换 vc 一开始 sum 就不能链接成功。

符号一样,什么前面的函数体去覆盖后面的函数体,对于微软来说,是完全不存在的事情。

还有一点,linux so 动态链接库里的符号可以是未决的,但是 dll 缺一个函数,都没办法生成。光是这点,微软就已经领先 100 年。
geelaw
2023-09-19 22:09:16 +08:00
@xuelang #15 链接并不能用来挑选(“决议”) A 的这个定义或那个定义,A 自始自终都只有一个定义。
xuelang
2023-09-20 09:27:52 +08:00
@geelaw 链接过程是用来找到符号的定义,英文是 resolution ,翻译成决议也还可以。
xuelang
2023-09-20 09:28:30 +08:00
@tool2d vc 没用过,不过这里微软做法貌似是合理些。
geelaw
2023-09-20 12:46:27 +08:00
@xuelang #18 汉语“决议”是“会议所确定的较重大的事项”的意思,在计算机技术方面应该理解为复杂的选择过程,比如“重载决议”是可接受的说法,因为要根据形参列表和实参列表从多个可能的选项里根据一系列不那么直接的规则选出最合适的。链接里的 symbol resolution 的 resolution 应该译作“解析”,因为这个过程只是简单地查找符号的定义,不存在复杂的规则。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/975233

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX