0
点赞
收藏
分享

微信扫一扫

详细解析编译链接原理(下篇)


文章目录

  • ​​一、编译阶段​​
  • ​​1. 符号解析​​
  • ​​2. 编译阶段不分配地址​​
  • ​​二、链接​​
  • ​​1. 简单的链接方式​​
  • ​​2. 真实的链接方式​​
  • ​​3. 符号重定位​​
  • ​​三、可执行文件格式​​
  • ​​四、可执行文件装载到内存​​
  • ​​五、总结​​

一、编译阶段

1. 符号解析

main.c

extern int gdata10;  // 声明外部变量
extern int sum(int, int);

int gdata1 = 10; // .data
int gdata2 = 0; // .bss
int gdata3; // .bss

static int gdata4 = 11; // .data
static int gdata5 = 0; // .bss
static int gdata6; // .bss

int main() {
int a = 12;
int b = 0;
int c;

static int d = 13; // .data
static int e = 0; // .bss
static int f; // .bss

sum(a, b);

return 0;
}

sum.c

int gdata10 = 13;

int sum(int a, int b){
return a + b;
}

我们编译生成main.o和sum.o,查看main.o的符号表

详细解析编译链接原理(下篇)_虚拟地址


符号解析:obj文件的符号表中对符号进行引用的都要找到符号定义的地方。比如像​​*UND*​​段中的符号一定要找到定义的地方,否则链接时就会报错。如果重复定义也会报错。

详细解析编译链接原理(下篇)_符号表_02

详细解析编译链接原理(下篇)_可执行文件_03


详细解析编译链接原理(下篇)_c语言_04

详细解析编译链接原理(下篇)_c语言_05

2. 编译阶段不分配地址

详细解析编译链接原理(下篇)_可执行文件_06

  • gdata10的地址:我们看到第19行指令中gdata10的地址是0x0,但这只是完成了汇编,并没有进行符号解析,给符号分配虚拟地址,所以main.o文件中gdata10的地址是0x0
  • sum函数的地址:我们可以看到31行指令中sum函数的地址为ff ff ff fc,这是一个内核空间的地址,同理这也不是一个用户能合法调用的地址,编译过程中不分配地址
  • 总结:编译过程中,用到的数据地址都是0x0,用到的函数地址都是与下一行指令地址相差-4的一个偏移量。但不管怎么样,编译阶段的地址都是无效的

二、链接

1. 简单的链接方式

详细解析编译链接原理(下篇)_可执行文件_07

obj或可执行文件每个段按照4字节对齐,而可执行文件中的每个段装载到虚拟地址空间后的对齐方式是页面(4096字节)对齐,也就是说可执行文件中的每个段装在到虚拟地址空间后至少需要占用一个页面,这非常浪费空间。

所以可执行文件中的段应该尽可能少

那我们把​​.o​​文件中相同的段链接称为目标文件中的一个段不就好了?

还不行,我们有更节省内存空间的方式,那就是按照每个段的属性进行合并。

2. 真实的链接方式

所有相同的段合并后,再按照属性进行合并,加载到虚拟地址空间后按照页面对齐

段合并:所有​​.o​​​文件中的​​.rodata​​​和​​.text​​​能链接到一起,​​.data​​​和​​.bss​​链接到一起,相同属性的段在虚拟地址空间中以页面对齐

段表合并:最终可执行文件的段表也需要调整段偏移和段长度

符号表合并:每个obj文件里都有一个符号表,符号表也需要合并,即进行符号解析,所有的符号都要找到符号定义的地方。然后给符号分配虚拟地址

3. 符号重定位

针对于需要指令跳转的部分

我们先手动连接main.o和sum.o

详细解析编译链接原理(下篇)_符号表_08

查看符号表

详细解析编译链接原理(下篇)_符号表_09

数据符号填的是虚拟地址,而函数符号由于涉及指令跳转,填的是相对于下一行指令的一个偏移量

我们看到call指令后面存的地址是0x 00 00 00 0a,下一行指令的地址是0x 08 04 80 ca,两个相加结果为0x 08 04 80 d4,而这个地址就是sum函数的起始地址

指令跳转:CPU在执行0x 08 04 80 c5这一行指令的时候,PC寄存器存放的是下一行指令的地址,也就是0x 08 04 80 ca。由于此时碰到了call指令,不能按照PC寄存器所存放的地址去执行,需要进行指令跳转,于是CPU拿出PC寄存器中的值与call后面这个偏移量相加,得到跳转地址

三、可执行文件格式

详细解析编译链接原理(下篇)_符号表_10

详细解析编译链接原理(下篇)_符号表_11

从符号表和文件头可以得知,我们的ELF文件头大小只有0x34,而.text段的起始地址却是0x94,也就是可执行文件和obj文件不一样,可执行文件中的.text并不是紧挨着ELF文件头的

详细解析编译链接原理(下篇)_虚拟地址_12

.text段和ELF文件头之间还有program headers,查看ELF文件头我们可以看到每个program headers为32字节,一共有3个,也就是96字节。ELF文件头和program headers的大小分别为52字节和96字节,相加转换为16进制就是0x94,这就是.text的偏移地址了。

查看program headers:

详细解析编译链接原理(下篇)_c语言_13

四、可执行文件装载到内存

程序的运行过程:

  1. 创建虚拟地址空间到物理内存的映射(创建内核地址映射结构体),创建页目录和页表
  2. 加载数据段和代码段(对于运行来说只需要代码和数据,不需要符号表、段表等等 )
  3. 把可执行文件的入口地址写入CPU的PC寄存器

为什么obj文件不能执行?

因为obj文件没有program headers这一段,而program headers有两个LOAD页面(不管什么程序编译链接后都是两个,因为只有指令和数据加载到内存),这俩LOAD页面就告诉加载器,需要把可执行文件中的哪些段加载到内存上,哪些段放到一个页面

查看一下program headers

详细解析编译链接原理(下篇)_可执行文件_14


可以看到第一个LOAD页面,虚拟地址为0x 08 04 80 00,在虚拟地址空间中从这个地址开始存放。第一个页面只有0xe2个字节,剩余的0x1000 - 0xe2个字节全部浪费了,没办法

我们看到两个LOAD页只是加载了.text,.data,.bss段,实际上不可能这么简单,因为这是我们手动链接的,没有链接C++的库,我们让编译器帮我们链接看看

详细解析编译链接原理(下篇)_c语言_15


装载过程如下:

详细解析编译链接原理(下篇)_可执行文件_16

mmap:专门在虚拟地址空间开辟内存,负责磁盘往虚拟地址空间的映射(《深入理解计算机系统》第9章)

我们跟踪一下可执行程序的执行过程

详细解析编译链接原理(下篇)_可执行文件_17


mmap不仅仅要映射当前可执行文件的代码段和数据段,如果使用到了库函数,还需要把libc.so等库映射到虚拟地址空间heap和stack的中间在程序中假如getchar后,我们编译运行,然后查看一下可执行文件如何映射到虚拟地址空间

详细解析编译链接原理(下篇)_符号表_18

五、总结

详细解析编译链接原理(下篇)_虚拟地址_19


举报

相关推荐

0 条评论