编译链接的过程

来源:互联网 发布:超级优化基因液txt免费 编辑:程序博客网 时间:2024/06/06 17:34

开篇:计算机的必要元素为CPU、I/O、内存,我们可以选择不同厂商的品牌,但是操作系统为了屏蔽底层的差异,使用户在应用层,感受不到差异,So,用户在应用层编写程序的时候,操作系统为用户提供了统一的接口,就比如,我们可以调用open打开一个文件,也可以打开一个socket描述符,也可以打开一个设备文件。操作系统为了屏蔽IO层,提供了一个VFS(虚拟文件系统);为了屏蔽IO和内存,提供了一个虚拟内存;为了屏蔽CPU、IO、内存之间的差异,提供了进程;所以,我们编写的文件是不可能直接加载到物理内存上的,是在虚拟内存上的;
CPU主要是用来运算数据的,CPU的位数是指一次性能加以运算的最长的整数的宽度;CPU在ALU中运算数据;CPU的位数是ALU的宽度,是数据总线的长度;
虚拟内存(IBM的定义):它存在,你能看的见,它是物理的;
它存在,你却看不见,它是透明的;
它不存在,你却看的见,它是虚拟的;
可执行文件运行的时候,操作系统都会给它一个虚拟内存;

补充
Linux中的静态链接库为libc.a;动态链接库为libc.so
Windows中的静态链接库为libc.lib; 动态链接库为libc.dll
动态链接库和静态链接库的区别:<1>装载时间不同;静态链接库的装载时间为编译的时候,动态链接库的装载时间为运行的时候;<2>静态库在编译的时候,把包含调用函数的库是一次性全部加载进去的,动态库是在运行的时候,把用到的函数的定义加载进去;所以,链接器在链接静态库的时候,是以目标文件为单位的,每个函数独立放在一个目标文件中可以尽量减少空间的浪费;
这里写图片描述

这里写图片描述

为什么需要高级语言(C语言、C++语言)???
使用机器指令或者汇编语言编写程序是一件费事乏味的事情,程序员需要考虑字长、内存大小、通信方式、存储方式等,这使得程序的开发和低效;并且使用机器语言或者汇编语言编写的程序依赖于特定的机器,一个为某种CPU编写的程序在另外一种CPU下完全无法运行,需要重新编写,这样是很难让人接受的,所以后来就有了跨平台,开发效率高的高级语言出现了(C,C++)。

编译链接的过程,开始了!!!linux

gcc hello.c
./a.out
上面的步骤分为四个步骤:预处理、编译、汇编、链接;

预编译
将.c文件预编译生成.i文件;
- 将所有的“#define”宏定义进行替换;
- 处理所有预编译指令,比如“#if”,“#ifdef”等;
- 处理“#include”,将被包含的文件插入到该预编译指令的位置;
- 删除所有的注释;
- 添加行号和文件名标识,比如#2 “hello.c” 2,以便于调试用的行号信息;

编译
将.i文件编译为.s文件;
- 编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后生产相应的汇编文件;代码的优化,汇总所有的符号;

汇编
将.s文件汇编后生成.o或者.obj文件
- 汇编器是将汇编代码转换成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令;
- 汇编完成以后生成二进制可重定位目标文件;

链接

  • 合并所有obj文件的段,并调整段偏移和段长度,合并符号表,进行符号解析;在符号解析完成以后,需要给符号分配内存地址(分配的是虚拟地址);
  • 链接的核心,符号的重定位(链接完成以后,数据符号填充的是绝对地址,而函数符号填充的是相对于下一行指令地址的偏移量);
  • 链接器在链接的时候,只对global符号进行处理,local的符号不做处理

全文以下面这段代码为例:

  1   2 #include <stdio.h>  3 #include <stdlib.h>  4   5   6   7 int gdata1 = 10;  8 int gdata2 = 0;  9 int gdata3; 10  11 static int gdata4 = 11; 12 static int gdata5 = 0; 13 static int gdata6; 14  15 int main() 16 { 17     int a = 10; 18     int b = 0; 19     int c; 20     static int d = 12; 21     static int e = 0; 22     static int f; 23  24     return 0; 25 }~       

obj文件的组成格式是什么?为什么它不能运行?
通过gcc -c main.c,我们可以得到main.o文件,又通过命令
readelf -h main.o,可以得到main.o文件的头信息;由下图可知,main.o文件的头信息为ELF Header:其中有运行在什么体系上、对应的体系版本号、入口地址等字段信息;通过图片,我们可以看到,入口地址为0x0,这个地址是禁止访问的;所以说,obj文件是不能运行的;从图中还可以看出,ELF Header的大小为52字节,十六进制为0x34;又由下面的file main.o命令,可以看出obj文件是可重定向的,所以不能运行;
图1:
这里写图片描述
图2:
这里写图片描述

obj文件的组成格式
这里写图片描述
执行命令objdump -h main.o,来查看main.o文件中各种重要的段;由下图可以得出,bss段的起始地址和comment段的起始地址是一样的,所以,bss(better save space)节省的是文件的空间,而不是虚拟地址空间;
图3:
这里写图片描述

常量字符串存在哪呢?
.rodata段;
图4:
这里写图片描述
图5:
这里写图片描述

显示ELF文件段表的内容
图6:
这里写图片描述

bss段既然节省obj文件的信息,那么在文件中都不存,那么gdata2,gdata3,gdata5,gdata6,e,f都存在哪里呢?
通过图1中的段start of section headers的偏移208可以找到,段表的起始地址,208字节对应的十六进制为0xd0,在图6中的最上面我们就可以看到段表的起始地址就是0xd0,在段表中保存着各种段的详细信息,其中就有bss段,说明bss段是保存在了段表中,又因为bss中保存的数据要么是初始化为0的,要么未初始化的,所以在obj文件中,没必要存bss段,因为bss段中的值都为0,不像data中的数据,需要把用户初始化的值保存起来,不至于下次访问的时候,操作系统给个0值,那用户就不开心了;

强符号和弱符号的区别
强符号和弱符号是出现在C语言中的;初始化了的都叫强符号,没有初始化的都叫弱符号;在C语言工程中,如果定义了多个同名强符号,那么就会出错;如果出现了同名的强符号和弱符号,选择强符号;如果出现了同名的弱符号,选择内存占用量最大的符号;

链接的时候,对obj文件的global进行处理
由图6知,bss段的大小为0x14,也就是20字节,应该放在bss段的应该有gdata2,gdata3,gdata5,gdata6,e,f,总共6个变量,应该是占24个字节,可是现在只有20字节;看下图,得知,gdata3放在了comment段,这个段表示里面的数据都是未决定的符号,因为gdata3是一个弱符号,等到链接的时候,才选择,把gdata3放在正确的段中;
图7:
这里写图片描述

为了更好地看链接的过程,上面的示例代码,变为:
main.c文件的内容为:

 1   2 #include <stdio.h>  3 #include <stdlib.h>  4   5 extern int gdata10;  6   7 extern int sum(int a,int b);  8   9 int gdata1 = 10; 10 int gdata2 = 0; 11 int gdata3; 12  13 static int gdata4 = 11; 14 static int gdata5 = 0; 15 static int gdata6; 16  17  18 int main() 19 { 20     int a = 10; 21     int b = 0; 22     int c = gdata10; 23     static int d = 12; 24     static int e = 0; 25     static int f; 26  27     sum(a,b); 28     return 0; 29 }

sum.c文件的内容:

  1 #include <stdio.h>  2 #include <stdlib.h>  3   4   5   6 int gdata10 = 12;  7   8 int sum(int a,int b)  9 { 10     return a+b; 11 }

编译以后,生成sum.o和main.o;
sum.o
图8:
这里写图片描述
main.o
图9:
这里写图片描述

由图9可知,gdata10和sum符号是“UND”,未定义的符号,这样叫做符号的引用,那么当链接的时候,就要找到符号定义的地方;链接的时候要进行符号解析,符号解析:所有obj文件符号引用的地方,都要找到该符号定义的地方;

未定义的数据所生成的符号,填充的是0地址;
未定义的函数生成的符号,填充的是fffffffc,表示的意思是相当于下一行指令的地址的偏移量;,由图可知,下一行指令的地址为00000000,所以-4,就等于fffffffc;
图10:
这里写图片描述

在链接的时候,相同属性的段进行合并,组织在同一个页面上,生成的可执行文件是按照页面对齐的;

对sum.o和main.o进行链接:
图11:
这里写图片描述

查看生成的可执行文件中对应的符号,都已经分配了正确的虚拟内存地址;每个符号都得到了一个正确的虚拟地址空间上的地址;
图12:
这里写图片描述
图13:
这里写图片描述
数据符号对应的是绝对地址,函数符号对应的是相对于下一行指令的偏移;(怎么知道当前的call的地址是多少???用当前PC寄存器中的值加上偏移量,就是call指令跳转的地址)

进入到程序的运行
./a.out
1、创建虚拟地址空间到物理内存的映射(创建内核地址映射结构体,创建页目录,页表);
2、加载代码段和数据段;
3、把可执行文件的入口地址写到CPU的PC寄存器里面;
图14:
这里写图片描述
图15:
这里写图片描述
由上可以知道,.text段的偏移是0x94,又由图15可知,run的ELF Header的大小为52字节(0x34),.text段的偏移应该是0x34的,怎么就变成了0x94呢???由图16可知,有三个program header,起始偏移为0x34;
图16:
这里写图片描述

通过图16可知,有两个LOAD页,第一个LOAD页保存的是.text段的内容,第二个LOAD页保存的是.data段和.bss段的内容;两个LOAD项指示了,当前程序的哪些段应该放在同一个页面上;

可执行文件的布局
图17:
这里写图片描述
两个LOAD页使用mmap将页面映射到虚拟地址空间上,然后虚拟地址空间上的页面又是通过多级页表映射到物理内存上的;
这里写图片描述

参考资料:程序员的自我修养

原创粉丝点击