链接(2)

7.5 符号和符号表

之前一节大改说道了符号表, 这一节展开来分析. 对于一个可重定位目标模块m而言, 有三种不同类型的符号 :

1. 有m定义并且能够被其他模块引用的符号(全局符号), 包括非静态的C函数以及定义的全局变量(限定词不带static)...

2. 被其他模块定义但是被模块m引用的符号(外部符号), 一般是定义在其他模块的C函数和变量(有的时候为了表明这个变量定义不在该文件, 还是用extern限定词)...

3. 只被本模块定义和引用的符号(本地符号), 一般是定义在本模块的C函数和全局变量但是被static修饰, 那么这些符号只能在m内使用, 另外写在函数内的static变量也属于这个范畴...

另外一点到了链接处, 所有函数内的局部变量都已经处理掉了, 使用的是栈和寄存器的管理方式, 并不需要添加到符号表中... 

另外对于书中给出的例子, 可以用上一节中讲解的方法进行分析 :

int f(){
    static int x = 1;
    return x;
}        

int g(){
    static int x = 0;
    return x;      
}

可以发现两个变量的名字分别被改成了 x.1480和x.1483... 这一点也可以通过readelf中的符号表分析得到 :

同时, 由于第二个x被初始化为0, 所以它会出现在.bss中而不是.data中...

然后是小提示 :

我们来仔细看一下ELF文件中的.symtab段中的结构(也就是ELF符号表的结构) :

这两张图结合着看加上上一节的讲解, 我们现在可以知道 :

1. 符号表中每一项都是15个bytes, 但是实际占用16bytes(手动验证发现的)...

2. 第一项name并本身不是字符串, 而是用来表示在.strtab(字符串表)中的字节偏移, 也就是说符号名都存在那个表中...

3. 这里的value也不是符号的值, 这个值在可重定位文件中指的是相对于该符号所属的Section的偏移, 在可执行目标文件中是符号在内存中的虚拟地址...

4. size 是目标的大小

5. type这里可以看到的这几种 :

  a. NOTYPE这我不知道有什么用.

  b. FIEL同上, 虽然它后面名字是文件名.

  c. SECTION代表section, 好像也没什么卵用.

  d. OBJECT代表数据.

  e. FUNC代表函数.

6. bind可以看到LOCAL和GLOBAL两种, 很好理解就不解释了.

7. Vis是保留字, 好像没什么卵用...

8. Ndx是该符号所属的Section编号, 这里要提到三个pseudo Section : ABS表示不该被重定位的符号, UND是undefined的缩写, 表示在本模块引用而在其他模块定义的符号, COM是还未被分配位置的未初始化的数据目标, 对于COM的符号, value字段按对齐要求即可, size给出最小的大小(有点不理解什么叫最小大小), 但是如果你记得上一节提到的swap.c的话, 那个定义在函数外面未被初始化的bufp1就是一个这样的变量...

如果你感兴趣的话, 可以对着符号表的二进制数据参考上面给出的结构体一个一个撸, 撸出来之后就是上面用readelf分析出来的这张图. 到这里对于符号表以及字符串表都应该有一定的了解了...

7.6 符号解析

链接器解析符号引用的方法是将每个引用于它输入的可重定位目标文件的符号表中的一个确定的符号定义联系起来, 这样的话就有两种情况 :

1. 对于那些本地符号(带local的, 只在这个模块中使用), 因为在同一模块下定义和引用, 另外因为编译器只允许这样的变量有一次定义, 所以很容易就可以把符号和引用相联系, 另外对于那些在函数内定义的static的变量, 如果和之外的重名, 类似上面两个函数中static变量重名, 将会被改写成类似x.1483(这里的前提是原名是x).

2. 对于全局符号而言, 当遇到不在该模块中定义的符号时, 编译器会先假设该符号在其他模块中被定义, 仍然为它在符号表中生产一个条目, 同时将它交给链接器处理, 如果链接器在所有的输入模块都找不到该符号的定义, 那么就会输出一条错误信息并终止. 类似这样的 :

找不到符号定义是一方面, 另一方面, 还有可能出现多个定义, 这时候链接器要么选择报错, 要么选择根据某些规则选择其中某个定义而放弃其他定义...

这是关于C++和Java中重载的处理, 可以作为背景知识了解一下 :

7.6.1 链接器如何解析多重定义的全局符号

编译器在编译过程中, 将全局符号分成两种 :

1. 函数和已经初始化的变量称之为强符号...

2. 未初始化的变量称之为弱符号...

然后将这些信息输出给汇编器, 汇编器再将这些信息隐含地编码在可重定位目标文件的符号表里...  之后链接器以如下规则处理多重定义的符号 :

1. 不允许出现多个强符号.

2. 强符号和弱符号同时出现时, 选择强符号.

3. 如果存在多个弱符号, 随机选择一个.

对于第一种情况, 链接器将会报错, 报错信息大概是这样的 :

第二种情况类似下面这样 :

这种情况, 链接器一般不会有任何提示, 相当于默认全局变量x就初始化为了15213...

第三种情况其实第二种类似, 一般也得不到任何提示, 同时随机选择一个定义.  有时候链接器的这种特征(针对2, 3), 极容易带来一些不易察觉的运行时错误, 例如下面这种情况 :

这时候显然链接可以通过, 那么此时如果执行这个程序会发生什么呢 ? 首先是 x, y 分别被初始化为15213和15212, 之后main()执行调用f(), 然后因为在f中针对第6行生成的指令其实是针对double的, 而double是64位的, 此时给它赋值-0.0, 回忆第二章中, 浮点数的表示, 这里-0.0的十六进制表示应该是0x1000000000000000, 由于是小端, 如果内存从低到高的话, 那么这个值在内存中是这样的 : 00 00 00 00 00 00 00 10, 所以强行分配给只占32位的x. 此时因为x和y作为两个全局变量连续地放在.data段中, 所以结果就是x变成了0x0, 而y变成了0x10000000...

这种错误细微而难以察觉, 如果怀疑程序中有类似这样的错误, 可以使用给GCC加上 -fno-common 的flag, 这时候只要链接器遇到多重定义的全局符号, 就会输出警告信息.

7.6.2 与静态库链接

我们知道平时在C语言里面常用到的比如printf, scanf之类的标准IO是属于标准库的, 那么你有没有想过C语言到底是如何实现对这些函数的支持的呢? 下面介绍如下几种方法 :

1. 将标准库与编译器绑定在一起, 让编译器来识别标准库并生成对应的代码, Pascal就是这么做的, 这是因为Pascal只提供了少量的标准函数, 而对于C来说这样做是不合适的, 因为C标准定义了大量标准函数, 这样做会显著增加编译器的复杂性, 另外每次添加, 删除或者修改一个标准函数时, 就需要一个新的编译器版本. 当然这样做对于程序员来说是相当方便的...

2. 将所有的标准库函数都放在一个单独的可重定位目标模块中, 然后程序员可以手动将这个模块链接到他们的可执行文件中(相当于直接当成了一个普通的可重定位文件), 这样做当然实现了标准库与编译器的分离, 并且也不是很麻烦, 但是问题就在于你随便写一个程序(就算只有一行return 0), 编译完成之后的可执行目标文件都链接上了一大堆的标准库函数, 浪费磁盘空间, 同时在运行的时候这些内容又要再次拷贝的内存中, 所以也浪费了内存. 同时一旦标准库中的发生改变(甚至只是很小的改变), 有需要重新编译整个库文件, 十分耗时...

3. 针对第二种方式, 有人提出了将所有标准库函数分开放, 那么确实这些问题都解决了, 但是这对于程序员来说又是一大噩梦, 因为我们必须在链接时显式地声明自己使用过的标准库函数, 结果链接过程就类似这样 :

在这样的背景下, 静态库的概念被提出来了 :

7.6.3 链接器如何使用静态库来解析引用(这一块书中讲的也是相当清晰, 直接照搬了)

原文地址:https://www.cnblogs.com/nzhl/p/5679467.html