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

网友投稿 927 2022-09-04

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

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

文章目录

​​一、引入虚拟地址空间​​​​二、虚拟地址空间​​​​三、编译链接过程​​

​​1. 预编译​​​​2. 编译​​​​3. 汇编​​​​4. 链接​​

​​四、解析ELF文件​​

​​1. 查看ELF文件头​​​​2. 查看段表​​​​3. ELF文件不存储.bss段,最后如何给.bss段分配虚拟地址空间?​​​​4. 关于强弱符号​​​​5. global弱符号链接前暂时记录在\*COM* 块​​

一、引入虚拟地址空间

C/C++代码经过编译器编译链接后,需要把指令和数据加载到内存执行

计算机由CPU(运算器、控制器)、内存(存储器)、IO(输入设备和输出设备)组成,为了屏蔽硬件的差异,使应用层能够忽略这些差异,操作系统提供了统一调用的接口(比如系统调用open,不仅可以用来打开文件,也可以打开socket,还可以打开字符设备)

为了屏蔽I/O的差异,OS提供了VFS(Virtual File System)为了屏蔽内存和I/O的差异,OS提供了虚拟存储器(虚拟内存)为了屏蔽CPU、内存、I/O,OS提供了进程

cpu的位数代表着cpu一次性能够处理的数据的位数(ALU的宽度或者数据总线的条数),32位代表cpu能够处理32位的数据,就是4个字节的大小。64位cpu代表cpu一次性能够处理64位的数据,也就是8个字节的大小的数据。

32位CPU:数据总线为32条,地址总线32条16位CPU:数据总线为16条,地址总线20条8位CPU:数据总线为8条,地址总线16条

CPU的位数就是地址总线的条数,这是错误的

二、虚拟地址空间

IBM对虚拟地址空间的解释:

如果它存在,而且你能看见它---它是真实的(real)如果它不存在,但你能看见它---它是虚拟的(virtual)如果它存在,但你看不见它---它是透明的(transparent)如果它不存在,而且你也看不见它---那肯定是你把它擦掉了

生成ELF文件后,​​.text​​​,​​.data​​​,​​.bss​​​段的大小在程序运行时都是不变的,这些段是用来存放指令和数据的,指令和数据被生成后都是固定不变的,不可能写的变量一会有一会没有。而这个时候是没有​​.heap​​​的,只有我们malloc申请内存时,OS才会分配​​.heap​​​。此外程序刚运行起来还需要有​​.stack​​,因为函数就是在栈上运行的

​​.text​​​:代码段,存放代码,指令​​​.rodata​​​:只读数据区,存放常量​​​.data​​​:存放初始化且初始化值不为0的数据​​​.bss​​​:存放未初始化和初始化为0的数据(包括全局变量,static修饰的变量)​​​.heap​​​:堆区​​​.stack​​:栈区(可存放函数形参和局部变量)

内核空间是共享的,用户空间是独立的

内核也分三部分:ZONE_DMA(0 ~ 16M)、ZONE_NORMAL(16M ~ 896M)、ZONE_HIGHMEM(896M ~ 结束)

ZONE_DMA(Direct Memory Access):加快磁盘和内存交换数据。没有DMA的时候,磁盘和内存之间交换数据时,数据必须通过总线经过CPU寄存器才能到达内存,这非常浪费CPU资源。有了DMA以后,某进程进行I/O的时候CPU就会空闲下来去调度其他的进程,增大了CPU的使用效率ZONE_NORMAL:该区域的物理页面是内核能够直接使用的ZONE_HIGHMEM:32位OS在内核里映射高于1G的物理内存时会用到高端内存。64位系统是没有高端内存的,因为64位系统的内核空间高达512G,不需要使用这一区域

#includeint gdata1 = 10; // .dataint gdata2 = 0; // .bssint gdata3; // .bssstatic int gdata4 = 11; // .datastatic int gdata5 = 0; // .bssstatic int gdata6; // .bssint main() { // 生成汇编指令,而不是产生数据 // 运行到该段代码时,才在函数栈帧上开辟4字节的空间存放数据 int a = 12; int b = 0; int c; // 放在数据段,程序启动的时候不会初始化,运行到该代码后再初始化 // 程序运行起来后,.bss段被清0 static int d = 13; // .data static int e = 0; // .bss static int f; // .bss return 0;}

三、编译链接过程

1. 预编译

gcc -E main.c -o main.i

生成预编译文件,进行头文件引入以及宏替换,同时清理注释。不做任何有效的检查

2. 编译

gcc -S main.i -o main.s

词法分析、语法分析、语义分析、代码优化,生成符号(我们编译链接的错误基本上都和符号表有关)

3. 汇编

gcc -c main.s -o main.o

​​.s​​文件中有很多汇编指令,汇编这一过程就是把汇编指令转换为特定平台的机器码

​​*.o​​​称为二进制可重定位目标文件,在​​*.o​​文件中有符号表,只有数据(包括全局变量,static修饰的变量)才产生符号,局部变量生成的是指令

4. 链接

gcc main.o -o main

合并所有​​.o​​文件的段,并调整段偏移的长度,合并符号表,进行符号解析,然后再给符号分配内存地址(虚拟地址)链接的核心:符号重定位

四、解析ELF文件

1. 查看ELF文件头

readelf -h main.o

2. 查看段表

readelf -S main.o

ELF文件头的大小为52字节,0x34​​.text​​​:偏移地址为0x34,段大小为0x1b,0x34 + 0x1b = 0x4f,无法被4字节整除(对齐方式),所以补了一个字节,​​.data​​从0x50开始​​.data​​:偏移地址为0x50,段大小为0x0c​​.bss​​​:偏移地址为0x5c,段大小为0x14,表示占20个字节,可是我们推测的是有6个int变量存储在​​.bss​​段,不对??(我们后面解释)​​.comment​​​:偏移地址为0x5c,段大小为0x2d,偏移地址和​​.bss​​​的偏移地址重合,并把​​.bss​​​覆盖,说明该ELF没有存储​​.bss​​​段,​​.bss​​省的是ELF文件的空间ELF文件就是一个文件头,加上各种段,最后一个段的偏移地址 + 该段的大小,就是整个ELF文件的大小

故ELF文件组成格式为:

查看ELF文件的内容

objdump -s main.o

3. ELF文件不存储.bss段,最后如何给.bss段分配虚拟地址空间?

由于​​.bss​​​存储的都是没有初始化或者初始化为0的数据,ELF文件中不会存储​​.bss​​​段,那程序运行起来的时候,怎么知道存储在​​.bss​​段的信息?

先查看文件头,找到段表的位置,然后查看​​.bss​​段占了多少空间,运行的时候就开辟多少虚拟内存,全部初始化为0

4. 关于强弱符号

C语言里面有强符号(初始化)和弱符号(未初始化)的概念,如果在C语言工程里面:

出现多个强符号,编译肯定出错出现同名的强、弱符号,我们选择强符号出现同名的弱符号,我们选择内存占用最大的弱符号

比如:出现多个强符号,编译出错

编译的时候,每个源文件独自编译,链接的时候再整合符号

test.c

// 在这里就应该能看出来,程序运行起来使用的x不一定是当前这个弱符号x// 因为其他文件中可能还存在同名强符号或者内存占用更大的弱符号int x;void func() { // 编译阶段:20写入x的内存,写4个字节 x = 20;}

main.c

#includeshort x = 10;short y = 10;void func();int main() { func(); printf("%d %d\n", x, y); // 20 0 return 0;}

在func函数中,编译阶段生成 把立即数20写入x的内存,写4个字节的指令(mov dword ptr [x], 14h ),链接阶段同时发现弱符号​​int x​​​和强符号​​short x = 10​​​。于是确定x的地址,就是这个​​short x​​​的地址,汇编指令中把20写入x的地址,实际上是写入了强符号​​short x​​的地址

虽然编译阶段就确定​​short x​​​和​​short y​​​存放在​​.data​​段,并且确认了初始值都为10。程序执行起来的时候,执行了func函数相关的汇编指令,在x的内存上写入4个字节,写入的是14 00 00 00,所以起始地址为&x,往后的4个字节被赋值为14 00 00 00,于是修改了​​short x​​​和​​short y​​的值

5. global弱符号链接前暂时记录在*COM* 块

在查看段表的部分,我们知道​​.bss​​​的偏移地址为0x5c,段大小为0x14,表示占20个字节,可是我们推测的是有6个int变量存储在​​.bss​​段,我们解释一下:

链接的时候处理所有的obj文件中的global符号,而不处理local符号(本文件可见)。而​​int gdata3​​​是global的,且是弱符号,可能被其他我文件中同名的强符号覆盖。​​static int gdata6​​虽然也是弱符号,但它是local的,只是本文件可见,链接的时候也无法被覆盖。

查看符号表(main.o是最早的那个代码)

objdump -t main.o

shen@NONOR-shen:/mnt/c/Users/shen/Desktop$ objdump -t main.omain.o: file format elf64-x86-64SYMBOL TABLE:0000000000000000 l df

我们可以看到所有的变量都是存放正常的,除了弱符号gdata3存放在​​*COM*​​​块(表示gdata3当前是一个未决定的符号,需要等到链接完成才能决定具体存放在ELF文件的哪个部分),这就是为什么我们之前分析的6个未初始化和初始化为0的数据应该全部存放在​​.bss​​段,实际上只存了5个

我们链接一下,然后查看符号表:

shen@NONOR-shen:/mnt/c/Users/shen/Desktop$ gcc main.o -o mainshen@NONOR-shen:/mnt/c/Users/shen/Desktop$ objdump -t main1: file format elf64-x86-64SYMBOL TABLE:0000000000002000 l d .rodata 0000000000000000 .rodata0000000000003df0 l d .init_array 0000000000000000 .init_array0000000000003e00 l d .dynamic 0000000000000000 .dynamic0000000000004000 l d .data 0000000000000000 .data000000000000401c l d .bss 0000000000000000 .bss0000000000000000 l d .comment 0000000000000000 .comment000000000000401c l O .bss 0000000000000001 completed.80610000000000000000 l df

可以看到链接完成后,gdata3已经确定存放在​​.bss​​段了

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:MyISAM主键索引树和二级索引树
下一篇:数据库并发控制,选择乐观锁还是悲观锁?(mysql乐观锁解决并发)
相关文章

 发表评论

暂时没有评论,来抢沙发吧~