一个弱声明问题

最近在适配newlib代码时遇到一个关于弱声明的问题, 研究了一下才发现自己对weak属性与链接时符号选择理解有误.

问题背景

在一个库(liba.a)中重新定义了一个weak属性的函数(func_a), 在同一库中调用该函数时链接了弱声明的函数版本, 但是当通过另一个库(libb.a)的库函数(func_b)调用(func_a)时最终链接的是强声明的函数版本.
精简后的case如下:

[00:51:42] hansy@hansy:~/testcase$ cat weak.c 
int __attribute__((weak)) test() {
  return 0xFF;
}
[00:51:52] hansy@hansy:~/testcase$ cat strong.c 
int test() {
  return 0;
}
[00:51:56] hansy@hansy:~/testcase$ cat wrapper.c 
int test();
int wrapper() {
  return test();
}
[00:52:01] hansy@hansy:~/testcase$ cat main.c 
extern int test();
extern int wrapper();
int main() {
  return wrapper() + test();
}

一共4个文件, 其中weak.c与strong.c分别定义了test函数的两个版本. wrapper.c与main.c都调用了test函数.
精简后的编译命令如下:

[00:57:49] hansy@hansy:~/testcase$ cat build.sh 
gcc weak.c -c -o weak.o
gcc strong.c -c -o strong.o
if [ -e test.a ]; then rm test.a; fi
ar rcs test.a weak.o strong.o
ranlib test.a

gcc wrapper.c -c -o wrapper.o
if [ -e wrapper.a ]; then rm wrapper.a; fi
ar rcs wrapper.a wrapper.o
ranlib wrapper.a

gcc main.c -c -o main.o
gcc main.o test.a wrapper.a -static -o indirect.out
objdump -D indirect.out > indirect.s
echo "indirect output"
./indirect.out && echo $?

gcc main.c strong.o weak.o wrapper.a -static -o direct.out
objdump -D direct.out > direct.s
echo "direct output"
./direct.out && echo $?

indirect.out是通过将weak.c与strong.c打包成test.a后链接生成的程序, direct.out是直接链接weak.o与strong.o生成的程序.
执行shell结果如下:

[01:01:43] hansy@hansy:~/testcase$ sh build.sh 
indirect output
direct output
0

链接库的版本返回值是非0(即使用了weak.c的定义), 而直接链接的版本返回值是0(即使用了strong.c的定义).

问题定位

一开始怀疑是我对weak声明的理解有误, 所以从gcc官网查找了一下说明.

The weak attribute causes a declaration of an external symbol to be emitted as a weak symbol rather than a global. This is primarily useful in defining library functions that can be overridden in user code, though it can also be used with non-function declarations. The overriding symbol must have the same type as the weak symbol. In addition, if it designates a variable it must also have the same size and alignment as the weak symbol. Weak symbols are supported for ELF targets, and also for a.out targets when using the GNU assembler and linker. 

从官网的说明来看强定义默认覆盖弱定义, 那么为什么通过库链接的版本使用的是弱声明呢?
尝试通过nm命令查看各个编译组件的符号定义:

test.a:
weak.o:
0000000000000000 W test
strong.o:
0000000000000000 T test

wrapper.a:
                 U _GLOBAL_OFFSET_TABLE_
                 U test
0000000000000000 T wrapper

main.o
                 U _GLOBAL_OFFSET_TABLE_
0000000000000000 T main
                 U test
                 U wrapper

可以看到test.a包含了两部分, 其中weak.o包含了test的weak定义, strong.o包含了test的strong定义, 而wrapper.a与main.o都包含了undefined的test引用.
即在编译到静态库后test.a仍包含了test的两种实现, 因此问题出在链接时符号选择. 在编译时添加-Wl,-trace-symbol=test查看符号选择:

// indirect
main.o: reference to test
test.a(weak.o): definition of test
wrapper.a(wrapper.o): reference to test

//direct
/tmp/ccqKb5fZ.o: reference to test
strong.o: definition of test
wrapper.a(wrapper.o): reference to test

即编译indirect时选择链接test.a中的weak.o, 而编译direct时恰恰相反.

  1. 注意到nm test.a时首先打印weak.o再打印strong.o, 是否是ar文件顺序先后导致了链接时查找符号结果不同? 尝试调整ar顺序.
main.o: reference to test
test.a(strong.o): definition of test
wrapper.a(wrapper.o): reference to test
indirect output
0

/tmp/ccJCNbJC.o: reference to test
strong.o: definition of test
wrapper.a(wrapper.o): reference to test
direct output
0

替换顺序后strong.o排在weak.o前, 修改后indirect结果与direct结果保持一致(使用strong.o的定义).

  1. 再尝试修改direct的链接顺序(strong.o与weak.o的顺序).
main.o: reference to test
weak.o: definition of test
strong.o: definition of test
wrapper.a(wrapper.o): reference to test
direct output
0

替换顺序后尽管weak.o先被查找到, 但由于strong.o中是强定义, 结果仍然使用strong.o的定义.

  1. 那如果将main.c中test的声明改成weak的呢?
wrapper.o:
                 U _GLOBAL_OFFSET_TABLE_
                 w test
0000000000000000 T wrapper

main.o
                 U _GLOBAL_OFFSET_TABLE_
0000000000000000 T main
                 w test
                 U wrapper

main.o: reference to test
wrapper.a(wrapper.o): reference to test
indirect output
Segmentation fault (core dumped)

/tmp/ccSpseMq.o: reference to test
strong.o: definition of test
wrapper.a(wrapper.o): reference to test
direct output
0

仍然使用最初的脚本, 注意到修改声明后main.o中对test的引用由U(undefined)变为w(weak).
direct仍然使用strong.o的定义, 而indirect.o却报段错误(链接时没有找到test定义), 这是由于main中使用的是弱声明, 默认不会去库中查找符号定义.

结论

更多的实验就不展开了, 这里写下实验结论:

  1. 使用弱声明可以避免链接过程中由于多个同名符号定义导致linker报错的问题.
  2. 当多个object定义了同一个符号时, linker会默认选择强定义覆盖弱定义, 而如果找不到强定义时默认使用第一个查找到的弱定义.
  3. 当该符号的定义不是以object而是以静态库形式参与链接时情况又会稍稍发生变化, 具体而言:
    3.1. 如果参与链接的object与静态库同时包含该符号定义时, 默认选择object中的定义, 即使该定义为weak. 具体原因暂时没空看linker代码, 我的理解是object一定是参与链接的, 而库文件只有在找不到符号的时候才会用来查找.
    3.2. 如果只有静态库包含该符号的定义, 优先选择第一个查找到的定义, 即使该定义为weak. 关于原因我的理解同上, 即linker不会遍历静态库, 而是只有需要时候才会去查找, 只要找到就会返回.
  4. 如果将函数声明为weak会导致该文件中包含一个weak声明的符号, 该符号在链接过程中不会被linker主动解释(即如果该符号已被linker查找到定义则会引用之前的定义, 否则linker将忽略对这个符号的解释).
  5. 关于动态库的实验待补充.
posted @ 2020-06-13 22:28  Five100Miles  阅读(661)  评论(0编辑  收藏  举报