深入编译链接和运行(二)

一. 编译阶段

main.c

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

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

static int gdata4 = 11;  
static int gdata5 = 0;   
static int gdata6;       

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

    static int d = 13; 
    static int e = 0;  
    static int f;     
    sum(a, b);  
    return 0;
}

test.c

int gdata10 = 13;

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

编译main.c和test.c,生成main.o和test.o
查看main.o的符号表

objdump -t main.o
在这里插入图片描述

*UND**COM*块都叫做符号的引用,在链接的时候,所有obj符号表中对符号引用的地方都要找到该符号定义的地方

  • 如果没找到符号定义的地方就会得到链接的错误unresolve external simple
  • 如果该符号被多次定义,就会得到链接的错误redefination simple(重定义符号)

链接的时候基本上90%的问题都是符号表的问题。
在这里插入图片描述

  • 函数调用的时候涉及函数的跳转,CPU运行哪行指令是由CPU里面的PC寄存器决定的,PC寄存器永远放的是下一行指令的地址。所以CPU在执行当前指令的时候,当前指令执行完,只要检查自己的PC寄存器放的地址是什么,就认为PC寄存器里面的地址是要运行的下一行地址

  • 在编译过程中,使用数据的地方存的都是0,使用函数的地方存的都是偏移量(就是函数和下一行指令地址的偏移量)

  • 编译的过程不给符号分配内存地址,用到的数据地址都是0x0,用到的函数地址都是与下一行指令地址相差-4的一个偏移量。但不管怎么样,编译阶段的地址都是无效的。

二. 链接

链接最终形成的是可执行文件,一种策略是把所有段拿过去汇总,但是这样不好,因为每个obj文件都是按4B的方式对齐,但是可执行文件按页面对齐,32位系统常用的页面是4096B(4k),如果按这种简单的方式合并,比如三个.text合并就要占用三个页面的内存,但是每个页面除了一个字节存的是有效的指令,其余的4095B全都是空的,内存开辟了但是没有存任何东西。

改进:如果obj文件在进行链接的时候,合并obj文件的段是按.text,.data和.bss相同的段分别合并到一个页面,这样合并比纯粹的堆积起来合并效率要好,编译出来会小一些,但是这样合并也不是很好,因为比如三个obj文件合并,三个.data段合并总共是3B占一个页面,.bss和.text也是这样。

所以obj合并的规则:
所有相同属性的段(这些段都是可读可写的或者可读可执行的或只读不能写的等等)进行合并,组织在一个页面,这样就能把.text段和.rodata段就能放在一个页面上,.data段和.bss段放到一个页面,这样可执行文件会更小一些。
原来的段很小,现在变得非常大,就要重新在段表里面调整每个段的起始偏移量以及段的大小,合并符号表,合并符号表的目的是为了进行符号解析,让所有对符号引用的地方都找到该符号定义的地方,且每个符号只有一个定义。

接下来就需要给符号分配内存地址,符号的重定位

每个符号都有内存地址,符号解析完成以后给符号分配的内存地址,但是在编译的时候对符号有引用的地方地址都不正确,现在符号有正确的地址了,就需要把得到的正确符号的地址填写在指令段里,这样符号解析完成后每个符号都得到了一个合法的虚拟地址空间地址。
在这里插入图片描述
在这里插入图片描述

数据符号填的是绝对地址,函数符号因为要设计指令跳转,填的都是偏移量。

我们看到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后面这个偏移量相加,得到跳转地址。

三. 可执行文件的组成格式

在这里插入图片描述

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

可执行文件的组成格式
在这里插入图片描述

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

查看program headers:
在这里插入图片描述

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

程序的运行过程:

  1. 创建虚拟地址空间到物理内存的映射(创建内核地址映射结构体),创建页目录和页表
  2. 加载代码段和数据段
  3. 把可执行文件的入口地址写到CPU的PC寄存器里面

obj文件为什么不能运行,因为他没有program headers段,因为program headers有两个很重要的load页面(不管什么程序编译链接都是两个,因为只有指令和数据加载到内存),这两个load页面指示了操作系统的load加载器,要把当前程序里面的哪些东西加载到内存,哪些段放到一个段上
在这里插入图片描述
我们可以看见load页面只是加载了.data,.bss和.text,实际上不可能这么简单,因为这是我们手动链接的,没有链接C++库,如下图如果让编译器链接
在这里插入图片描述
装载过程:在这里插入图片描述
mmap:专门在虚拟地址空间开辟内存,负责磁盘往虚拟地址空间的映射
可执行文件为什么要两个load项记录哪些段在同一个页面上?因为虚拟地址空间和物理地址空间管理内存的方式是按页面对齐,为了更好地映射。

跟踪可执行程序的执行过程:
在这里插入图片描述
mmap不仅仅要映射当前可执行文件的代码段和数据段,如果使用到了库函数,还需要把libc.so等库映射到虚拟地址空间heap和stack的中间

在这里插入图片描述

五. 总结

现在有两个程序main.c和sum.c,预编译->编译->汇编->链接->运行:

  1. 编译汇编过程分为四步:obj文件怎么组成;编译的过程中符号表怎么汇总;符号表里的符号是对符号的引用还是定义,它们分别在那个段上;符号是global属性还是local属性,这就决定了链接器在链接阶段是否需要去处理,链接器只处理global属性
  2. 链接:对所有obj文件的段进行合并(按属性合并段);调整各个段起始偏移量和段的大小;然后合并符号表,合并符号表的目的是为了让所有对符号的引用找到符号的定义,这个过程不能出错,要不然会出现各种链接错误;接下来给符号分配内存地址;最后进行符号重定位,因为我们代码指令的编译是在编译过程中进行的,但是编译过程中并不给符号分配内存地址,所以在我们指令中,所有对数据符号引用的地方全部是零地址,对函数符号引用的地方都是偏移量-4。
    链接过程给符号分配了合理的内存地址以后,链接器要在代码段里面把哪些没有填写正确地址的替换成符号的正确地址,数据符号替换成符号的绝对地址,函数符号替换成和下一行地址的偏移量,就得到了可执行文件
  3. 可执行文件:比obj文件多了一个program headers,这里面有两个非常重要的load项,这两个load项指示的是把我们这个文件中的某些段分配在一个页面上
  4. 运行:运行程序的时候操作系统会把这两个load项映射到虚拟地址空间上,这个映射是通过mmap进行的,当我们真正用它的时候,会把虚拟地址空间上的页面最终映射到物理内存上的页面通过多级页表映射
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值