从底层剖析程序从编译到运行的整个过程
三个阶段
- 一、编译阶段
- 二、链接阶段
- 三、运行阶段
为了方便解释,给出两端示例代码,下面围绕代码进行实验:
//sum.cpp
int gdata = 10;
int sum(int a,int b)
{return a+b;
}
//main.cpp
extern int gdata;
int sum(int ,int);static int stat;int data = 20;int main()
{int a = gdata;int b = data;int ret = sum(a,b);return 0;
}
前两个阶段:
一、编译阶段
三件重要的事情:
编译阶段只关注自己模块内的事情
编译阶段不分配虚拟空间地址,无法运行
目标文件由各个段组成
编译阶段的产物是可重定位的二进制目标文件,由各个段组成
一、符号表(.symtab段)
查看符号表命令:objdump -t main.o
1.符号表中存储程序产生的符号,如
静态全局变量stat
的符号为_ZL4stat
,定义在.bss
区域
全局变量data
的符号为data
,定义在.data
区域
主函数main()
的符号为main
,定义在.text
区域
外部变量gdata
的符号为gdata
,定义为UND
,表示符号的引用,不知道在哪里定义
外部函数sum(int,int)
的符号为_Z3sumii
,定义为UND
,表示符号的引用,不知道在哪里定义
2.符号表中可以看到变量的链接属性
l
:表示lcoal
,符号只能在当前文件可见,内部链接属性
g
:表示global
,符号可以在所有文件可见,外部链接属性
所以链接的时候链接器只能看见
gloal
的符号 看不到lcoal
的符号
这就解释了静态全局变量/函数 和普通全局变量/函数同名的问题
在多个文件中可以定义名字相同的静态全局变量/函数,因为local
属性链接器不可见,但若多个文件中普通的全局变量/函数重名,因为具有global
属性,链接的时候符号解析就会冲突
3.编译过程中变量不分配虚拟空间地址
我们查看以下.text
段,注意需要带有-g
输出调试信息
g++ -c main.cpp -g
objdump -S main.o
观察,编译阶段产生了二进制机器码,但是不分配虚拟空间地址,所以地址先用0替代,即编译阶段指令没法用,需要等链接阶段分配虚拟地址补上地址才有用,这就是目标文件无法运行的原因之一
4.查看目标文件的各个段
命令:readelf -S main.o
二、链接阶段
链接所有的编译完成的目标文件(.o)和静态库文件(.a)
链接步骤:
步骤一:
将所有的目标文件的各个段进行合并
main.o的.text
段和sum.o的.text
段合并
main.o的.data
段和sum.o的.data
段合并
main.o的.bss
和sum.o的.bss
段合并
合并后进行符号解析
如链接阶段符号为UND(符号引用)的,都需要找到该符号定义的地方,如果没有找到=符号未定义,找到多个定义=符号重定义
UND 找到定义解析成具体 .text .data ..
段
步骤二:
符号的重定位(重定向)
符号解析之后,给所有的符号分配虚拟地址空间,成为了可执行文件
验证
使用链接器自己链接::ld -e main sum.o main.o
查看符号表:objdump -t a.out
可以看到所有符号均有定义的段,无UND
符号引用的情况
所有符号均分配了地址(看第一列)
再看看代码段.text
之前机器码缺少地址的,现在也都补充上了,所以变成了可以运行的二进制机器码(指令)
补充1:
看一下可执行文件的文件头信息
.text段的信息
可以发现,可执行文件头记录了程序入口指令地址,所以CPU知道从哪个指令开始执行(这里是main函数作为入口)
补充2:
可执行文件所有的段都和二进制目标文件相同,多了一个programa headers
段,用来告诉操作系统,运行这个程序的时候,把哪些内容加载进内存(数据段 指令段),注意:不是所有的段都需要加载进内存的
查看programa headers段:readelf -l a.out