深入程序编译链接和运行

来源:互联网 发布:y2电机技术数据大全 编辑:程序博客网 时间:2024/05/17 00:55
   作为C/C++后台开发人员,但是对程序的编译链接过程似懂非懂,只能说是门外汉!此文章旨在深入程序编译链接过程,主要阐述程序编译、链接过程中具体做了哪些事。如果你能正确回答下面的问题,那么请点击右上角的X,退出阅读。

1.程序编译的过程是什么?编译的每个阶段都要做哪些事情?
2.*.o/.obj文件的组成格式是什么?为什么不能运行?
3.链接过程都做了哪些事情?符号的重定位是什么意思?
4.可执行文件的组成格式是什么?它为什么可以运行?CPU怎么知道它从哪儿开始执行?
5.程序运行要经过哪些步骤?

以下讲述以32位linux操作系统为例,参考《程序员的自我修养》相关章节。话不多说!进入正题!

编译

程序编译要经过3个阶段:预编译->编译->汇编
main.o经过预编译生成main.i
main.i经过编译生成main.s
main.s经过汇编生成main.o
预编译阶段:
1.删除注释
2.处理以”#”开头的预编译指令
编译阶段:
1.进行语法分析、语义分析、词法分析
2.编译代码
3.进行代码优化
4.汇总所有的符号,生成符号表
汇编:
1.将mov lea sub等汇编指令转换为特定平台(一般为Intel X86或AT&T)的机器码
2.构建.o/.obj文件的格式

以下面代码为例,看看*.o文件的组成格式

main.c#include <stdio.h>int gdata1 = 10;int gdata2 = 0;int gdata3;static int gdata4 = 11;static int gdata5 = 0;static int gdata6;int main(){    int a = 10;    int b = 0;    int c;    static int d = 12;    static int e = 0;    static int f;    return 0;}

请问:以上代码经过编译后处于什么位置?
如果你看过我的博客《Linux虚拟地址空间分布》,应该能很快回答上来。
所有程序经过编译后只有两种表现形式:指令或者代码
a、b、c 经过编译后是指令,保存在.text段
gdata1、gdata4、d 这三个变量在.data段
gdata2、gdata5、gdata6、e、f 这五个变量在.bss段
gata3存在.comment段

通过objdump -h main.o,打印出main.o的各段信息。如下图示:

这里写图片描述

由上图可知,.text段的大小是0x1b,段地址是偏移0x34。
问题来了,前52字节放的是什么东西? elf header!
elf header稍后会说到,现在请先记住,它的大小是52字节。

.data段的大小是0x0c,段地址是偏移0x50。根据前面分析,.data段放了三个int类型的变量,刚好占12字节。.text段的偏移(0x34)+.text段的大(0x1b)=0x4f-->按4字节对齐-->0x50-->.data段的偏移。因此,**.data段是紧挨着.text段存放的**。.bss段的大小是0x14,段地址的偏移是0x5c。根据前面的分析,.bss段存放了五个int类型的变量,占20字节(0x14)。.data段的偏移(0x50)+.data段的大小(0x0c)=0x5c-->.bss段的偏移。.bss段是挨着.data段存放的。

那么问题来了,.comment段的偏移为什么也是0x5c呢?
你是否想到《Linux虚拟地址空间分布》中说的:.bss段可以理解为better save space。节省的这个空间,就是*.o文件的空间!因此。.bss段根本不占main.o文件的空间,紧挨着.data段存放的是.comment段。

为什么不用保存bss段的数据信息?
因为.bss段存放的是未初始化或者初始化为0的值,不需要记录它们的值,只需要记录它们的存在就可以。需要使用时,全部初始化为0。
你是否又有疑惑?不保存bss段的数据信息,怎么找到数据呢?客官别着急,看了elf header的信息,你就懂了~

通过readelf -h main.o,得到elf header保存的信息,如下图:

这里写图片描述

elf header记录了程序的信息,如适合用在什么平台上,适合用在什么体系上等等。其中,主要的几项有:
1.size of this header:elf header的大小,52byte。现在知道为什么.text的偏移是0x34了吧。
2.Entry point address:程序的入口地址
3.start of section header:208(0xd0),段表地址的偏移量

通过readelf -S main.o查看段表信息,如图:

这里写图片描述

段表里记录着各个段的信息(地址、偏移量、大小、权限等)。通过main.o文件的elf header可以找到段表,段表里保存着各个段的信息。因此,通过段表可以找到没有保存在.o文件中的.bss段的数据。 到这儿,你应该对.o文件的组成格式有大概的了解。我以main.o为例,画出.o文件的组成格式。如下图:

这里写图片描述

通过命令objump -t main.o显示符号表,如下图:
这里写图片描述
因为gdata3是未初始化的全局变量,编译后是弱符号,处于COM块。

编译过程中不分配地址,指令中所有用到数据的地方都是0,所有用到函数的地方存的都是-4(FFFFFFFC),相对于下一行指令的偏移量。

链接

链接阶段做两件事:
1.合并所有obj文件的段,并调整段偏移和段长度,合并符号表,进行符号解析,给符号分配内存地址;
2.链接的核心:符号的重定位

合并所有obj文件段时,所有相同属性的段进行合并,并组织在同一个页面上。因为段时按照页面对齐,这样组织能节省内存。

编译过程中,如果引用了外部的符号,这些符号的属性是“UND”。这些符号称为对符号的引用。在链接阶段,所有符号引用的地方都要找到符号定义的地方。如果找不到符号定义的地方,则得到"未定义的符号"的链接错误,如果有同名的符号,则发生"符号重定义"的链接错误。

符号解析完成后,每个符号都得到了一个合法的虚拟地址空间上的地址。将指令中所有用到符号地址的地方都换成正确的符号地址,这个过程叫做符号的重定位。数据存的是绝对地址,函数存的是相对于下一行指令的偏移量(偏移量+下一行指令的地址=函数的地址)

通过命令readelf -h main得到main文件的elf header,如下图:
这里写图片描述
main.c的elf header大小也是52字节。

通过objdump -h main查看各段信息,如下图:
这里写图片描述

通过上图可知,.text段是从0x94开始偏移的!那elf header和.text之间保存的是什么?
program headers!

elf header中保存了program headers的信息,如下:
Start of program headers:52
Size of program headers:32
Number of program headers:3
52+32*3=148(0x94)

通过命令readelf -l main,查看program headers的内容,如下图:
这里写图片描述

program header中有两个LOAD项,观察上图知,该文件的两个LOAD页面中,第一个LOAD页面存放的是.text段的数据;第二个LOAD页面存放的是.data和.bss的数据。

两个LOAD页面指示操作系统loader加载器要将哪些页面加载到内存上LOAD页面指明当前exe文件的哪些段在同一页面

再复杂的代码编译后也只有两个LOAD页面,代码编译后要么产生指令,要么产生数据。

.o文件和.exe文件最大的区别就是这个program headers。.exe文件可执行,.o文件不可执行的原因也是因为这个program headers。**.o文件中没有LOAD页面,无法将数据段加载到内存中!**

链接完成后,每个符号都分配到了合法的地址。通过objdump -t main查看符号表,如下图:
这里写图片描述
通过和链接后的符号表对比,发现所有的符号都分配到了地址,gdata3也位于bss段了。

再画一下main可执行文件的组成格式,如下图:
这里写图片描述

再观察一下main文件elf header的内容
这里写图片描述
Entry point address存放的是main函数的地址!
程序运行时,从可执行文件的elf header拿到Entry point address(即main函数的入口地址),将该地址放入PC寄存器中,程序开始运行!

运行

程序运行后就是一个进程,OS会为每个进程分配4G的虚拟地址空间
1.创建虚拟地址空间到物理内存的映射(创建内核地址映射结构体),创建页目录和页表。(运行的过程中,将LOAD页面中的数据(.data/.bss/.text等)通过mmap函数映射到虚拟地址空间中。当真正用的时候,通过多级页表映射,将虚拟地址空间中的页面映射到物理内存中。可以参考我的另一篇文章《32位Linux系统虚拟地址映射》);
2.加载代码段和数据段到内存中;
3.把可执行文件的入口地址写到CPU的PC寄存器里面。

自此,程序的编译-链接-运行过程全部介绍完毕!