目录
· 示例代码
· sub.o 文件内容分析
· 段信息
· 符号表信息
· main.o 文件分析
· 段信息
· 符号表信息
· 绝对寻址
· 相对寻址
· 重定位表信息
· 可执行程序 main
· 段信息
· 符号表信息
· 绝对地址重定位
· 相对地址重定位
· 总结
别人的经验,我们的阶梯!
最近因为项目上的需要,利用动态链接库来实现一个插件系统,顺便就复习了一下关于Linux中一些编译、链接相关的内容。
在链接的过程中,符号重定位是比较麻烦的事情,特别是在动态链接的过程中,因为需要考虑到很多不同的情况。
这篇文章作为第一篇,先来聊一聊静态链接中的重定位过程。
按照惯例,还是以一个简短的示例代码作为载体,看一看GCC在链接的过程中,是如何根据目标文件(.o文件)来进行重定位,生成最终的可执行文件的。
示例代码
示例代码很简单,一共有2个源文件main.c和 sub.c。
在sub.c中定义了一个全局变量和一个全局函数,然后在main.c中使用这个全局变量和全局函数。代码如下:
sub.c
main.c
在一般的开发过程中,都是使用GCC工具,直接把这2个源文件编译得到可执行文件。
但是,为了探究编译、链接过程中的一些内部情况,我们需要把编译、链接的过程拆开,从中间过程中产生的目标文件(.o 文件)中,来查看一些详细信息。
先把这2个源文件编译成目标文件sub.o和main.o:
$ gcc -m32 -c sub.c
$ gcc -m32 -c main.c
这样就得到了两个目标文件,先来初步看一下这2个目标文件中的一些信息。
以上这两个编译过程是各自独立的,虽然main.o中使用了两个符号(全局变量和全局函数),但是此时main.o并不知道这2个符号是在哪个文件中定义的。
当链接器把所有的.o文件链接成可执行文件的过程中,才能确定这2个符号是在哪里。
在Linux系统中,目标文件(.o) 和可执行文件都是ELF格式的,因此如何查看ELF格式文件的一些工具指令就非常有帮助。
很久之前总结过这篇文章:《Linux系统中编译、链接的基石-ELF文件:扒开它的层层外衣,从字节码的粒度来探索》,里面详细总结了ELF文件的内部结构,以及一些相关的工具。
sub.o 文件内容分析段信息
首先来简单瞄一眼一下sub.o中的一些信息。
sub.o中的段信息如下(指令:$ readelf -S sub.o):
我们主要关心黄色的代码段和数据段就可以了,可以看出:
1. 代码段(.text):地址Addr是 0x0000_0000(因为这是目标文件,不是可执行文件,所以不会安排地址),它在 sub.o 文件中的偏移量(Off)是 0x34,长度是 0x0C 字节;
2. 数据段(.data):地址Addr是 0x0000_0000,它在 sub.o 文件中的偏移量(Off)是 0x40,长度是 0x04 字节;
简单算一下:sub.o的开始部分是ELF的 header,通过 readelf -h sub.o 指令可以看出来header部分是52个字节(即:0x34),如下:
因此可以得到:
1. 代码段(.text)是紧接在 header 之后,长度是 0x0C 个字节,在文件中占据着 0x34 ~ 0x3F 这部分空间(0x3F = 0x34 + 0x0C - 1);
2. 数据段(.data)是进阶在代码段之后,在文件中占据着 0x40 ~ 0x43 这部分空间;
符号表信息
下面再来说说符号表的事情。
简单来说,符号表就是一个文件中定义的所有符号、引用的外部符号(在其它文件中定义),包括:变量名、函数名、段名等等,都属于符号。
当然了,在ELF文件中会详细的说明每一个符号的类型、大小、可见性等信息。如果对ELF文件格式有过了解的话,一定知道每一条符号信息,都是通过一个结构体来描述具体含义的,描述符号表的结构体如下:
// Symbol table entries for ELF32.
struct Elf32_Sym {
Elf32_Word st_name; // Symbol name (index into string table)
Elf32_Addr st_value; // Value or address associated with the symbol
Elf32_Word st_size; // Size of the symbol
unsigned char st_info; // Symbol's type and binding attributes
unsigned char st_other; // Must be zero; reserved
Elf32_Half st_shndx; // Which section (header table index) it's defined in
};
再来看一下sub.o中的符号表,下面这张图(指令:readelf -s sub.o):
关注上图中黄色矩形中的两个符号:SubData和SubFunc,很明显它们就是sub.c中定义的两个符号:全局变量和全局函数。
对于SubData符号来说:
1. Size=4: 长度是 4 个字节;
2. Type=OBJECT:说明这是一个数据对象;
3. Bind=GLOBAL:说明这个符号是全局可见的,也就是在其他文件中可以使用;
4. Ndx=2:说明这个符号是属于第 2 个 段中,就是数据段(.data);
同样的道理,对于SubFunc符号来说:
1. Size=12: 长度是 12 个字节;
2. Type=FUNC:说明这是一个函数;
3. Bind=GLOBAL:说明这个符号是全局可见的,也就是在其他文件中可以调用;
4. Ndx=1:说明这个符号是属于第 1 个 段中,就是代码段(.text);
main.o 文件分析
按照上面的步骤,把main.o中的这几个信息也查看一下。
段信息
指令:readelf -S main.o
可以看出:
1. 代码段(.text):地址Addr是 0x0000_0000(因为这是目标文件,不是可执行文件,所以不会安排地址),它在 sub.o 文件中的偏移量(Off)是 0x34,长度是 0x32 字节;
2. 数据段(.data):地址Addr是 0x0000_0000,它在 sub.o 文件中的偏移量(Off)是 0x66,长度是 0 个字节,因为它没有定义变量;
在文件中的布局如下所示: