[音乐] 上一讲提到一个可执行目标文件,是由
多个可重定位目标文件,链接合并后生成的
那么可重定位目标文件和可执行目标文件,到底有什么差别呢?
本讲先简要介绍目标文件的概念 以及几种目标文件格式标准
因为本课程主要基于IA-32加Linux平台进行讲解
所以我们将主要介绍Linux系统采用的目标文件格式
也就是elf格式 我们先看一个C语言程序的例子
这个C语言程序包含了两个模块,一个是main.c
一个是swap.c,在这个点c文件里面,定义了一个主函数main
在主函数main里面,调用了swap函数
而这个swap函数,是在另外一个模块,swap.c里面来定义的
在main.c里面有一个全局变量buf
然后在swap.c里面有它的全局变量 bufp0,还有一个静态的局部变量
bufp1,在swap这个函数当中,定义了一个temp,局部变量
实际上每一个点c模块,它都有自己的 代码,比如说main.c里面,它的代码就是main函数的代码
那么在swap.c里面,它的代码呢就是swap 这个函数的代码,然后每一个模块里面还有自己的数据
比如说main.c里面,定义了buf这个全局变量,在这个模块当中
定义了它自己的一些全局变量和静态的变量
这些模块最后要在机器里面执行,它必须
先合并成一个程序,合并的时候,我们前面讲过 它是代码和代码合并,数据是和数据合并的
那么要说明的是这个地方的局部变量,像temp这种局部变量
实际上呢,它是分配在栈里面的,我们前面过程调用的时候讲过了 它所以不能够在过程外被引用
所以它不是一种定义的符号 如果我们要对刚才的两个模块main.c和swap.c
进行编译,汇编,链接生成一个可执行文件
p,那么我们可以用这样的一个gcc的编译命令 在这个里面我们通过杠o2
来指定编译的时候,采用二级优化 然后杠g呢,表示要生成调试信息
杠o,这个小o,表示杠小o后面的字符串
比如说这儿的p,实际上就是可执行程序的名字,也就是目标文件的名字
这样的话,通过这个命令,就可以生成一个可执行程序p
然后在命令行提示符下面,我们输入 p这个命令,就可以执行这个程序了
通过这个命令,生成的这个p,是在当前目录下的 所以在这个p前面,要加个点斜杠
这个表示执行的是当前目录下的这个p 这样启动以后呢,就可以执行这个p程序
实际上这个命令,它的整个处理的过程,是下面的这个过程
就是对main.c和swap.c分别进行转换
生成相应的main.o和swap.o
那这个转换的过程,当然先要通过预处理 再编译,最后汇编,生成二进制的代码
这个二进制的代码,就是可重定位的目标文件
这些点o文件里面的一些符号 实际上它还没有值,那些定义的符号还没有值
地址还没确定,然后呢通过链接 确定那里面的符号的应用关系,并且进行重定位以后
就可以生成可执行文件,可执行文件它也是一种目标文件,二进制的目标文件
这个二进制目标文件,就可以在机器上直接执行了,这个显示的是一个静态链接的过程
所以链接的过程,我们可以 来看它的本质,实际上是把
每一个模块当中,它不同的这个节
因为每一个模块里面包含代码部分,比如说text节这个节叫section
text节里面放的实际上就是main函数的代码,然后呢在main.o
里面还有数据节,data节 它里面放的实际上就是,刚才我们看到main.c里面定义的这个全局变量
另外在swap.o里面它也有它的代码节 text节就是swap函数对应的那些指令
就是2进制的这个机器指令,然后也有它的 data节,就是它定义的那些全局变量
还有呢,它的一些比如说没有 初始化的一些变量,就在这个bss节里面
这些当然都属于数据,这个呢属于代码,然后这个可重定位文件
最终要链接生成可执行目标文件,实际上 是把text节和text节合并起来
把data节和data节合并起来,bss节相应的合并起来
这样在可执行文件里面生成新的合并以后的代码节,已经初始化的数据节
和未初始化的数据节,这样的话就得到一个一个的新的
section新的一些节,这些节是已经进行链接了
就是前面我们讲过的已经进行了两个操作,一个叫符号
解析,符号解析以后呢再进行重定位,在这个里面的这些代码
实际上也就是一条一条指令,这些指令当中的 引用的那些符号已经被进行了重定位
已经有具体的地址了,这个就是可执行文件 对于可重定位目标文件里面每个节的构成
可执行文件里面每个节的构成,我们后面会详细地展开,这儿我们大概了解一下链接实际上
实现的就是这样的一个过程。
变成可执行 程序以后,实际上也就是链接以后生成的这个可执行文件
它已经存放在磁盘里面了,可执行文件我们刚才看到它是把所有的.o文件的text 节合并、
data节合并、 bss节合并,如果有只读数据节的话,那么只读数据节这个rodata
也会合并,合并成一个一个节,这个在磁盘当中实际上也就是一串一串的数据
一串一串的信息,可执行文件当中有一个非常重要的
一块数据,叫程序头表或者叫段头表 这个程序头表它是描述如何进行映射的
也就是描述这边在磁盘上存放的这个文件当中的这一个一个节
如何和存储器 进行映射的,链接的时候是不知道这个程序
将来会装入到内存哪个地方去执行,这些代码段合并的时候
怎么样确定这个位置呢?因为这个代码段将来是会装到内存
去执行的,现在链接的时候还不知道这个程序将来会
装到内存什么地方,而我们现在呢又到把它合并到一块存储区里面去
这时候怎么办,上一讲刚刚讲到,我们是把这些程序
相应的这些节合并到虚拟地址空间当中去的 所以我们最终把程序
放到一个虚拟地址空间里面,比如说在Linux里面 它的虚拟存储空间就是这样的一个空间,从0开始一直到
最大的是FFFFF,也就是32个1
那个地址,它是一个32位
地址表示的一个虚拟地址空间,所以 它的这个地址空间的大小是4个G
Byte,4个GB的空间,所以最大的这个地址就是32位的1
就是FFFFFFFF这样的一个地址
那在这个地址空间当中是划分成两大区域,一块是内核区 一块从这儿开始往下就是用户区
用户区最上面的是用户栈,我们在前面讲过程调用的时候实际上讲到
大家应该还记得所谓栈和栈帧的概念
我们那时候讲过这个栈是从高地址,我们可以看到是从这个高地址往低地址长的
每一次函数调用的时候,都会长出一个栈帧,这个栈帧的顶部是用
这个寄存器来指出来的,这个我们在前面讲过程调用的时候讲过了。
这个是用户栈 还有我们知道用这样的一个函数,可以动态申请
一块空间,这块空间实际上就是堆,堆区
这个是堆和栈的区域,可执行文件,这里面的代码
和数据要装入运行的时候,它有一个映射
它的这个只读数据和代码区都是 只读的,可以映射到相应的一块存储区域
然后数据呢,包括data节和bss节 会映射到另外一块区域。
只读数据和代码映射的区域,我们称为叫只读代码段
数据就是这些可以读写的这个数据 就是这两个节映射的,我们叫读写数据段
所以,在虚拟地址空间当中 这两个段都是从可执行文件装入的
也就是从磁盘当中的可执行文件里面装入 到这一块。
这个装入的时候,映射的这个地址 是通过程序头表来描述的,所以我们可以看出
我们反汇编出来的这些代码当中的地址 都是从这个地址开始的,在Linux平台里面
可以看到这些指令的地址,都比080480 00要大。
这些细节的内容,我们后面会一一展开 这儿只是让大家有一个大概的了解,就是我们从.o文件
变成这个可执行文件,而这个可执行文件装入到计算机系统运行
它先要把它落实到一个空间里面去 先要落实这些指令的地址,还有数据的地址
而这些指令和数据的地址,实际上是在虚存空间当中的地址 因此我们也称为是虚拟
地址,也就是给出来的这些地址实际上是一个虚拟地址,不是真正装入到内存的这个内存地址
这个概念大家要有,后面我们很多内容都会围绕着这些概念,大家会越来越清晰
我们可以看一个,以前我们在讲过程调用的时候讲过的一个例子
如果我们把text.o,也就是这个.c文件 生成的可重定位目标文件
用objdump显示出来的时候,我们可以看到这个里面的一个一个 的指令。
前面我们讲过了总序push指令、 mov指令,这一个一个的指令你看它的地址
都是从0开始的,每个函数都是从0开始的 所以它还没有进行链接,还没有进行重定位
当我们把这个模块和这个模块链接在一起
生成一个可执行文件test,再把它dump出来的时候我们可以看到
同样的这个函数,add这个函数的每一条指令,它的
地址就是一个新的地址了,我们可以看到这个地址是080483d4 也就是比刚才我们看到的那个
08048000这个地址要大 我们刚才看到这边的这个地址
08048000是它的首地址 是只读代码段的首地址,所以这个
区域里面实际上是放了一条一条指令,或者是只读数据 这一条一条指令的地址一定比这个地址大,所以我们可以
看到这个里面每一条指令的地址 都比刚才那个大,所以它是按序存放的指令
所以我们可以看到这个实际上是一个虚拟地址 它是在一个虚拟地址空间里面的一个位置值。
这是有关 目标文件,这是可重定位目标文件,下面这个 呢是可执行目标文件,它们的地址是不一样的
[音乐]
[音乐]