在谈编译链接过程之前我们需要了解一下虚拟地址空间以及程序在编译链接过程时经过了什么步骤。虚拟地址空间之前在进程空间的博客中详细介绍过了,详见:https://blog.csdn.net/wry_sunny_/article/details/86651871
一、虚拟地址空间
上图就是32位系统中4G虚拟地址空间的分布情况
.text 段:指令段,存放的是指令代码,在程序中,我们把局部变量定义(局部变量的 定义是指令而不是数据)还有一些操作指令都存放在.text 中。它的属性是可执行可读不可 写。 .
data 段:数据段,里面存放的是初始化不为 0 的静态局部变量和全局变量。属性是可读可 写不可执行。注意:在 data 段上面有一个 rodata 段(rodata 段的位置在.o 文件时去观察它 是在.bss 段的下方),它的属性是只可读。所以当我们这样定义字符串时:char *p = “hello world”; *p = ‘a’;会报错的原因就是这里的”hello world”在 rodata 段存放,它只可读, 不可修改。 .
bss 段:数据段,里面存放的是未初始化或者初始化为 0 的全局变量和静态局部变量。属性 是可读可写不可执行,在 bss 段中的数据默认都会被修改为 0。
堆:堆内存是在 c 语言中用 malloc 申请或者在 c++中用 new 来申请的一端可能不连续的内存, 堆是由低地址向高地址申请的。
栈:栈中存放的是局部变量,特性是先进后出,并且它是由系统自动开辟以及释放。并且内存是连续的。
命令行参数:main 函数的参数列表。argc 参数个数 argv 参数内容 envp 环境变量
二、源文件生成二进制可执行文件的流程
1、预编译:预编译又称为预处理,是做些代码文本的替换工作。是整个编译过程的最先做的工作。
预编译一些基本要做的事情:
(1)删除所有的注释
(2)展开所有的宏命令(#define)
(3)处理所有的预编译指令(#if,#endif)
(4)展开所有的头文件,将头文件中的所以东西展开(#include)
在经过预编译之后产生main.i文件。
2、编译:编译程序所要作得工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。
编译时要做的一些事情:
(1)生成和汇总符号
(2)生成汇编指令
产生main.s文件。
3、汇编:汇编实际上指把汇编语言代码翻译成目标机器指令的过程。对于被翻译系统处理的每一个C语言源程序,都将最终经过这一处理而得到相应的目标文件。
汇编时期要做的事情就是转换为二进制可重定位文件,生成main.o文件。
4、链接:应该叫静态链接。多个目标文件、库拼合为最终的可执行文件的过程。生成二进制可执行文件。
三、二进制可重定位文件组成
我们在linux系统下利用file命令来查看main.o的文件编码格式,可以看出main.o的文件类型是可重定位文件(LSB relocatable)。
file main.o:file命令用来识别文件类型,也可用来辨别一些文件的编码格式。
readelf 命令:一般般用于查看ELF格式的文件信息,常见的文件如在Linux上的可执行文件,动态库(*.so)或者静态库(*.a) 等包含ELF格式的文件。
readelf -h main.o 显示elf文件开始的文件头信息。
我们利用readelf -h命令查main.o文件头的信息,可以看出文件里面存在program header,起始位置为0,大小为52bytes;还存在section headers表,起始位置为208,里面一共有9张表的表头信息,每个表头信息大小为40bytes,也就是section headers表总大小为40*9bytes。
readelf -S main.o:sections 显示节头信息(如果有数据的话)。
从图中我们可以看到的是Section Header table表中的信息,其中虚拟地址空间的地址均为0,二进制可重定位文件,并不能读入到我们最终执行该程序的虚拟地址空间,所以没有办法确定该段在虚拟地址空间运行时最终真正的地址。所以我们之前说的低地址不可以访问并不冲突。
这些段中的具体存放的是什么东西,在刚开始的虚拟地址空间中已经介绍了。不过需要注意的是,因为.bss段存放的都是初始化为0或者没有初始化的数据(默认值也为0),也就是值都为0的数据,那么我完全不用存储他们的值,也知道存在.bss段的数据值都是0。既然不存,就需要.systable段了,也就是我们的符号表段来记录符号。
以下即为我们的程序
#include<iostream>
using namespace std;
int data1 = 10;
int data2 = 0;
int data3;
static int data4 = 20;
static int data5 = 0;
static int data6;
short a = 10;
short b = 20;
void Fun();
int main()
{
int a = 30;
int b = 0;
int c;
static int data7 = 40;
static int data8 = 0;
static int data9;
int *p = (int *)malloc(4*1000);
Fun();
getchar();
return 0;
}
objdump命令:objdump命令是Linux下的反汇编目标文件或者可执行文件的命令,它以一种可阅读的格式让你更多地了解二进制文件可能带有的附加信息。
objdump -t main.o:显示文件的符号表入口。
利用objdump命令来查看文件的符号表, 在符号表中,有每个数据的信息,可以看到data1,data4,data7都在.data段,data2,段,.data2,data5,data6,data8,data9都在.bss段,data3有*COM*标记。由此我们可以看出,所有的数据会生成符号,存储在符号表中,这样的话就算.bss段没有在文件中占据真正的内存,我们也可以通过符号表得知.bss段中数据的存在,而且他们的值都为0。我们可以看到data3比较特殊,它只给了一个*COM*标记,这就是强符号与弱符号的原因了。
强符号和弱符号:
在C语言中有强符号和弱符号的概念。
弱符号:没有初始化的全局非静态变量。
强符号:有初始化的全局非静态变量。
一个文件中的弱符号有可能在链接时候会被其他文件的同名强符号替代,所以在编译过程中并不给他分配实际的段,只有在链接完成后发现他没有被替代才会真正给他分配。