(4.3)符号表和符号解析

符号和符号表

连接器需要使用符号表进行符号解析然后生成可执行文件,目标文件中通常有一个符号表,表中包含了在该文件中定义的所有符号的信息。C 文件包含以下 3 种符号:

  1. 全局符号: 包括非静态的函数名和非静态的全局变量。
  2. 外部符号: 包括在其他模块定义的外部函数名和外部变量名。
  3. 本地符号: 包括带 static 的函数名和全局变量名。

static 属性的本地变量在 .data 和 .bss 中分配空间。如果要链接的两个可重定位文件中包含了同名的 static 变量,则需要分别为他们分配空间。
例如:

int func1(){
    
    
	static int x = 0;
	return x;
}
int func2(){
    
    
	static int x = 1;
	return x;
}

两个函数中包含静态变量 x,且都初始化,编译器则会在 .data 节为两者分配空间,并在符号表中创建 func1.x 和 func2.x 两个符号。
在这里插入图片描述
上图中的全局符号有:

  • main.c 中的变量 buf,函数 main
  • swap.c 中的变量 bufp0,函数 swap

外部符号有:

  • swap.c 中的 buf
  • main.c 中的 swap

本地符号有:

  • swap.c 中的 bufp1

swap.c 中的 temp 是运行时动态分配到栈中的,不是符号。

ELF 文件符号表中每个表项具有以下数据结构:
在这里插入图片描述

  • st_name: 给出符号在字符串表中的索引,指向在字符串表中的一个以 null 结尾的字符串。
  • st_value: 给出符号的值。在可重定位文件中,是符号所在位置相对于所在节起始位置的偏移量;在可执行文件中是符号所在的虚拟地址。
  • st_size: 给出符号表示对象的字节个数。若符号是函数名,则是函数所占字节的大小。
  • st_info: 给出符号的类型和绑定属性,从以下定义的宏可以看出,符号类型占低 4 位,绑定类型占高 4 位。
    符号类型可以是未指定(NOTYPE)、变量(OBJECT)、函数(FUNC)、节(SECTION)。绑定属性可以是本地(LOCAL)、全局(GLOBAL)、弱(WEAK)等。
#define ELF_32_ST_INFO(bind, type) (((bind << 4) + ((type) & 0xf))
  • st_other: 指出该符号的可见性,通常在可重定位文件中指出。定义了符号成为可执行文件或共享库文件一部分后该符号的访问形式。
  • st_shndx: 指出符号所在节在节头表中的索引。

使用命令 readelf -s main.o 查看 main.o 中的符号表:
在这里插入图片描述
main 模块中三个全局符号中,buf 是 变量(Type = OBJECT),它位于节头表中的第三个表项(Ndx = 3)对应的 .data 节中偏移量为 0 (Value = 0),占 8 个字节;main 是函数(Type = FUNC),它位于节头表中第一个表项(Ndx = 1)对应的 .text 节中偏移量为 0 处,占 20 字节;swap 是 未指定(NOTYPE)且未定义(UND)的符号,说明 swap 是 main 中被引用的外部符号。

符号解析

符号解析的目的是将模块中符号的引用与某个模块中符号的定义建立关联,用来在重定位时将引用符号的地址重定位为相关联的定义符号的地址。

扫描二维码关注公众号,回复: 15385482 查看本文章

全局符号的强弱性

已初始化的全局符号为强符号,未初始化的为弱符号。上面的 main.c 和 swap.c 模块中,所有全局变量都是强符号。
链接器处理多重定义符号主要有以下几个规则:

  • 强符号只能定义一次,多次会出现链接错误
  • 若一个符号有一次强符号定义和多次弱符号定义,则以强符号为准
  • 若一个符号有多次弱符号定义,则任选一个。

例一:

在这里插入图片描述

上图中强符号 x 被定义了两次,所以会抛出链接错误:
在这里插入图片描述
例二:

在这里插入图片描述
y 在 main.c 中定义为强符号,则 p1.c 中的 y 为引用符号。这两个 y 就是一个变量,y 被初始化为 100 ,调用 p1 后 y 的值被改为 200。z 在两个文件中都为弱符号,则按顺序来 main.c 中的 z 为定义,p1.c 中的 z 为符号引用,调用 p1 后 z 的值被修改为 2000。

所以输出结果为 y=200, z=2000。

例三:
在这里插入图片描述

main.c 中 d 被定义为强符号,类型为 int,p1.c 中引用 d,因为 AMD 64 为小端存储,调用 p1 后 d 的地址 &d 被修改成了 1.0 对应机器数的低 32 位,由于 x 在 d 后面定义,所以 x 在 d 的高两个字节处,则 x 的地址 &x 被修改成了 1.0 对应机器数的高 32 位 0xF30F0000,对应的十进制数为 1072693248。

最后输出结果(编译会由警告:将8字节的数赋值给只有4字节长度的符号d):
在这里插入图片描述

符号解析过程

链接器按从左到右的顺序扫描出现在命令行的可重定位文件和静态库文件,同时维护三个集合:

  • E: 要被合并到一起组合成可执行文件的所有目标文件的集合。
  • U: 未解析的符号集合,未解析的符号即为未与定义符号建立关联的引用符号。
  • D: 是指当前为止已被加入到 E 的所有目标文件中定义符号的集合。

符号解析前,三个集合都是空的。

符号解析的三个过程:

  1. 对命令行中的每一个输入文件 f ,如果是目标文件,就加入 E,根据 f 中未解析符号和定义符号分别对 U、D 进行修改,然后处理下一个文件。
  2. 如果 f 是库文件,链接器会尝试把 U 中所有为解析的符号与 f 中各目标模块定义的符号进行匹配。如果某个模块 m 定义了 U 中的一个未解析符号 x,那么就把 m 加入到 E 中,并把符号 x 从 U 移入 D 中。不断对 f 中所有目标模块重复这个步骤,直到 U、D 不改变。未加入到 E 中的 f 里的目标模块就被丢弃,继续处理下一输入文件。
  3. 如果处理过程中往 D 中加入一个已存在的符号或者扫描完所有文件时 U 非空,链接器报错。否则 E 中所有目标文件重定位生成可执行文件。

与静态库的链接

类 UNIX 系统中,静态库文件的格式为存档文件(archive),后缀为 .a。如,标准 C 函数库文件名为 libc.a,包含了 atoi、printf、scanf、strcpy 等广泛使用的函数。它是默认的用于静态链接的库文件,无需在链接命令中指出。还有其他的库函数,如浮点数运算库:libm.a。

咱们也可以自己定义静态库,让我们以下面的例子说明怎样生成自己的静态库文件。
例如有两个源文件 myproc1.c 和 myproc2.c :
muproc1.c

#include<stdio.h>
void myfunc1(){
    
    
	printf("%s", "This is myfunc1 from mylib!\n");
}

myproc2.c

#include<stdio.h>
void myfunc2(){
    
    
	printf("%s", "This is myfunc2 from mylib!\n");
}

并有一个 main.c 程序调用了 mylib.a 中的 myfunc1:

void myfunc1(void);
int main(){
    
    
    myfunc1();
    return 0;
}

生成静态库前先将两个文件编译成可重定位文件,然后使用 AR 工具生成静态库:
在这里插入图片描述
使用:
在这里插入图片描述
将 main.c 汇编为可重定位文件 main.o 然后再将 main.o 和 mylib.a 以及 libc.a链接,生成可执行文件 myproc。

-static 选项只是链接器生成一个能直接加载到存储器执行的可执行文件。

执行 myfunc:
在这里插入图片描述

链接器中符号解析的过程如下图所示:

在这里插入图片描述
符号解析的结果也命令行指定的文件顺序有关,如果使用:

gcc -static -o myproc ./mylib.a main.o

则会发生链接错误,先从静态库 mylib.a 中查找 U 中未定义的符号,但此时 U 中为空,则 mylib.a 中没有目标模块被加入到 E 中,当扫描到 main.o 时,其引用符号 myfunc1 不能解析,最后导致 U 非空,就会出错。

静态库链接顺序的准则: 将静态库文件放在可执行文件后。如果静态库文件的目标模块中的符号没有引用关系,则顺序可独立;如果有引用关系,则引用符号的静态库在前,定义符号的静态库在后。

猜你喜欢

转载自blog.csdn.net/weixin_45773137/article/details/124855968