C/C++ 编译链接与装载深入浅出 其二

论坛 期权论坛 期权     
YongHao写东西的cach   2019-7-7 23:03   2982   0
上接第一篇文章: https://mp.weixin.qq.com/s?__biz=MzIzOTIyNTA4MA==&mid=2649795418&idx=1&sn=75aa50a55333f3fcef791b9961b37536&chksm=f1295acdc65ed3db13333671ee9d627dc6d9593ef46f4cb0c5a1e641225c673383dcbd599a0b&token=1190612621&lang=zh_CN#rd


[h1]模块间通信[/h1][h2]链接器的由来[/h2]C/C++模块之间通信的方式有两种, 一种是模块间函数调用, 另一种是模块之间的变量访问. 在编译成目标文件的时候, 由于没有办法得知所引用的外部函数或者外部变量的地址, 所以会先置0. 所以问题本质上就是, 如何得知目标函数或者目标变量的地址呢?
手动查找修改自然不是我们的方法, 这就是链接器(Link Editor)与它的工作-重定位 的由来.
[h2]重定位表[/h2]链接器在处理目标文件时, 对目标文件中引用外部函数或者变量的位置进行重定位, 即代码段和数据段中那些对绝对地址的引用的位置. 这些重定位的信息都记录在ELF文件的重定位表里面. 其实目标文件中, 除了.data, .text这些段外, 若用到了其他目标文件中的外部变量或者函数, 就会多了对应的重定位表.
假如.data用了外部的全局变量, 就会多了一个.reldata段记录了所引用的外部变量的地址偏移值(offset)以及在符号表中的下标等. 假如.text没有用了外部的全局变量, 就不会有.reltext段.
[h1]符号[/h1]在链接中, 我们将函数和变量统称为符号(Symbol), 函数名或变量名就是符号名(Symbol Name)., 每个目标文件都会有一个相应的符号表(Symbol Table), 这个表记录了目标文件中所有到的所有符号. 每个定义的符号都有一个对应的值, 叫做符号值(Symbol Value), 对于变量和函数而言, 符号值就是他们的地址. 使用
  1. nm hello.o
复制代码
就可以查看hello.o里所有的符号.
[h2]符号修饰与函数签名[/h2]就C++而言, 为了实现重载这简单的一种情况, 编译器和链接器如何区分两个名字一样的函数呢?
为了支持这些情况, 人们发明了符号修饰(NAME Decoration)或符号改编(NAME Mangling). 编译器与链接器在处理符号时, 会把函数名, 命名空间, 参数类型, 所在的类等信息, 全部根据自己定的表翻译成一串符号, 自然就不会有相同.
[h2]extern C[/h2]由此引出extern C的由来. 为什么C++用使用C代码的时候, 需要用
  1. extern "C"
复制代码
来当做C语言处理呢? C++兼容C, 直接用应该也没有问题吧?
原因就是上述的符号改编(NAME Mangling), 试想一下使用C中string.h里而不是C++中string的memset的时候, 如果不限定
  1. extern "C"
复制代码
的时候, 就会进行符号改编从而使用了C++里string的memset, 使用
  1. extern "C"
复制代码
就不会进行符号改编从而链接到正确的C函数. C++编译器会在编译C++的程序时默认定义
  1. _cplusplus
复制代码
宏, 以下就是解决方法:
  1. #ifdef __cplusplus
复制代码
  1. extern "C" {
复制代码
  1. #endif
复制代码
  1. [/code][code]void *memset(void *, int, size_t);
复制代码
  1. [/code][code]#ifdef __cpluplus
复制代码
  1. }
复制代码
  1. #endif
复制代码
[h2]很多错误的根源-弱符号与强符号[/h2]对C/C++来说, 编译器默认函数和初始化了的全局变量为强符号 , 未初始化的全局变量为弱符号. 我们也可以通过GCC的
  1. __attribute__((weak))
复制代码
来定义任何一个强符号为弱符号.
  1. extern int ext;
复制代码
  1. [/code][code]int weak
复制代码
  1. int strong = 1;
复制代码
  1. __attribute__((weak)) weak2 = 2;
复制代码
  1. int main(void)
复制代码
  1. {
复制代码
  1. return 0;
复制代码
  1. }
复制代码
  1. [/code]上面的这段程序中, weak和weak2是弱符号, strong 和main是强符号, ext既非强符号也非弱符号, 因为它是一个外部变量的引用.
  2. 有以下规则:
  3. 1.
  4. 不允许强符号被多次定义(即不同的目标文件中不能有同名的强符号)
  5. [list][*][/list][code]我们常见的报错:符号重定义错误就是链接器因此而报的.
复制代码
2.
如果一个符号在某个目标文件中是强符号, 在其他文件中都是弱符号, 那么选择强符号.


  1. /* bar.c */
复制代码
  1. int x;
复制代码
  1. void f(){ x = 1314; }
复制代码
  1. [/code][code] /* foo.c */
复制代码
  1. #include
复制代码
  1. void f(void);
复制代码
  1. int x = 777;
复制代码
  1. int main(void)
复制代码
  1. {
复制代码
  1. f();
复制代码
  1. printf("x = %d\n", x);
复制代码
  1. return 0;
复制代码
  1. }
复制代码
这里输出的x是1314, 明显main里的x变量由777被改变成1314了. 因为foo.c中的x是强符号, 而bar.c中的x是弱符号, 所以bar.c中就使用了foo.c中的符号, 这会带来难以查找的错误. 在编译链接时,
  1. gcc foo.c bar.c
复制代码
, 链接器不会表明它监测到多个x的定义.
  1. /* bar2.c */
复制代码
  1. double x;
复制代码
  1. void f() { x = -0.0; }
复制代码
  1. [/code][code] /* foo2.c */
复制代码
  1. #include
复制代码
  1. void f(void);
复制代码
  1. int x = 1234;
复制代码
  1. int y = 5678;
复制代码
  1. int main()
复制代码
  1. {
复制代码
  1. f();
复制代码
  1. printf("x = 0x%x y = 0x%x \n", x, y);
复制代码
  1. return 0;
复制代码
  1. }
复制代码
这里, foo2.c中的x跟y都是强符号, 而bar2.c中的x是弱符号, 链接器选择了foo2.c中的x. 而在
  1. bar2.c 中的 f函数中
复制代码
赋值时, 就是给for2.c中的
  1. int x
复制代码
赋值. 在一台IA32机器上, double类型是8个字节, 而int类型是4个字节, 因此bar2.c的
  1. x=-0.0
复制代码
会覆盖了x跟y的位置. (x跟y在foo2.c中占8个字节, 而在bar2.c中的强符号就占了8个字节, 执行f函数赋值为-0.0时就覆盖了x跟y的位置了)
在编译链接时,
  1. gcc foo.c bar.c
复制代码
, 在Mac系统里, gcc和clang编译, 链接器ld会表明double x这个强符号替换了int x这个弱符号.
ld: warning: tentative definition of '_x' with size 8 from '/var/folders/81/0q8j79597dldk23mm2svgpjc0000gn/T//cc79XQz8.o' is being replaced by real definition of smaller size 4 from '/var/folders/81/0q8j79597dldk23mm2svgpjc0000gn/T//ccBHUpu6.o'
1.如果一个符号在所有的目标文件中都是弱符号, 那么选择其中占用空间最大的一个. 其实这个说法有两个, 选择最大空间的是程序员的自我修养的说法, 而CSAPP的说法是, 随机选择一个. 在这里, 我猜测是两者用词用所不同. 请往下看:
  1. /* weak2.c */
复制代码
  1. __attribute__((weak)) long double x;
复制代码
  1. [/code][code] /* weak1.c */
复制代码
  1. double x;
复制代码
  1. [/code][code] /* main.c */
复制代码
  1. #include
复制代码
  1. void f(void);
复制代码
  1. int x;
复制代码
  1. int main()
复制代码
  1. {
复制代码
  1. x = 1314;
复制代码
  1. return 0;
复制代码
  1. }
复制代码
  1. gcc -c weak1.c weak2.c main.c
复制代码
得到目标文件, 用
  1. nm -S weak1.o
复制代码
分别查看, 可以知道, main.c中的x占用4字节, weak1.c中的x占用8字节, weak2.c中x占用16字节.
  1. nm -S w1.o
复制代码
  1. 0000000000000008 0000000000000008 C x
复制代码
  1. [/code][code] nm -S w2.o
复制代码
  1. 0000000000000010 0000000000000010 C x
复制代码
  1. [/code][code] nm -S main.o
复制代码
  1. 0000000000000000 0000000000000040 T main
复制代码
  1.                  U printf
复制代码
  1. 0000000000000004 0000000000000004 C x
复制代码
  1. 0000000000000008 0000000000000008 C y
复制代码
最终链接后, 使用
  1. readelf -s
复制代码
或者
  1. nm -S
复制代码
查看:
  1. nm -S a.out
复制代码
  1. 0000000000601040 B __bss_start
复制代码
  1. 0000000000601040 0000000000000001 b completed.6973
复制代码
  1. 0000000000601030 D __data_start
复制代码
  1. 0000000000601030 W data_start
复制代码
  1. 0000000000400470 t deregister_tm_clones
复制代码
  1. 00000000004004e0 t __do_global_dtors_aux
复制代码
  1. 0000000000600e18 t __do_global_dtors_aux_fini_array_entry
复制代码
  1. 0000000000601038 D __dso_handle
复制代码
  1. 0000000000600e28 d _DYNAMIC
复制代码
  1. 0000000000601040 D _edata
复制代码
  1. 0000000000601060 B _end
复制代码
  1. 00000000004005e4 T _fini
复制代码
  1. 0000000000400500 t frame_dummy
复制代码
  1. 0000000000600e10 t __frame_dummy_init_array_entry
复制代码
  1. 0000000000400730 r __FRAME_END__
复制代码
  1. 0000000000601000 d _GLOBAL_OFFSET_TABLE_
复制代码
  1.                  w __gmon_start__
复制代码
  1. 00000000004003e0 T _init
复制代码
  1. 0000000000600e18 t __init_array_end
复制代码
  1. 0000000000600e10 t __init_array_start
复制代码
  1. 00000000004005f0 0000000000000004 R _IO_stdin_used
复制代码
  1.                  w _ITM_deregisterTMCloneTable
复制代码
  1.                  w _ITM_registerTMCloneTable
复制代码
  1. 0000000000600e20 d __JCR_END__
复制代码
  1. 0000000000600e20 d __JCR_LIST__
复制代码
  1.                  w _Jv_RegisterClasses
复制代码
  1. 00000000004005e0 0000000000000002 T __libc_csu_fini
复制代码
  1. 0000000000400570 0000000000000065 T __libc_csu_init
复制代码
  1.                  U __libc_start_main@@GLIBC_2.2.5
复制代码
  1. 000000000040052d 0000000000000040 T main
复制代码
  1.                  U printf@@GLIBC_2.2.5
复制代码
  1. 00000000004004a0 t register_tm_clones
复制代码
  1. 0000000000400440 T _start
复制代码
  1. 0000000000601040 D __TMC_END__
复制代码
  1. 0000000000601050 0000000000000010 B x
复制代码
  1. 0000000000601048 0000000000000008 B y
复制代码
  1. [/code][code]
复制代码
  1. gcc version 4.8.4 (Ubuntu 4.8.4-2ubuntu1~14.04)
复制代码
可以看到, 最终的x是占用16字节的空间.
[h2]总结[/h2]尽量不要使用多个不同类型的弱符号.


分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:5
帖子:1
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP