深入理解编译连接和运行

  • 对于每一个程序经过编译连接生成可执行文件的过程中操作系统会为每一个程序分配一个虚拟地址空间,其大小和操作系统的位数有关(32位操作系统为4G)。这4G的空间被分为3G用户空间和1G内核空间,其中内核空间是共享的,用户空间时独立的,用户空间又有如下分布:

    • 保留区:不可访问
    • text段:存放程序编写后的指令
    • data段:存放初始化了的且初始值不为零的全局变量
    • bss段:存放未初始化或初始化为零的全局变量
    • 保留堆区:这部分空间只有在遇到malloc或new指令时才被定义为堆区,并且可用于加载程序加载需要的共享库。
    • stack区:函数运行时的局部变量存储区
    • 下面就是主函数的命名行参数和环境变量存放区。

这里写图片描述

//main.c

int gdata1 = 10;
int gdata2 = 0;
int gdata3;
static int gdata4 = 20;
static int gdata5 = 0;
static int gdata6;
int main()
{
   int a = 30;
   inr b = 0;
   int c;
   static int d = 40;
   static int e = 0;
   static int f;

   return 0;
}

对于上面的代码,在生成可执行文件的过程中经过了如下步骤:
这里写图片描述

  • 预编译:删除所有 #define 展开宏定义、处理条件预编译指令、将 #include 包含的文件在该指令处展开、删除注释、添加行号和文件名标识、保留 #pragma 指令。

  • 编译:词法、语法、语义的分析及代码的优化、汇总所有符号

  • 汇编:根据特定平台将汇编指令转化为机器码,构建.o/.obj文件的组成格式。

生成的 .o 文件的段表信息为(只列出了一部分段,每个文件都有很多的段,这里就不列举了,感兴趣的可以下去自己查看):

在这个地方我们发现根据数据分配规则.bss应该有6个整型数据,分配的空间应为24字节,但.bss段的大小只为0x14=20,少了四字节,这是为什么呢?

  • 原来是因为编译时函数和数据会产生相应的符号,在C语言的编译阶段数据会有强符号和弱符号之分,弱符号在编译时不分配到对应的段,仅为强符号分配段内存。

    • 强符号:初始化的数据生成的符号

    • 弱符号:未初始化的数据生成的符号

所以可知gdata3为弱符号,不存在bss中。

生成的符号表为:
这里写图片描述

根据上面的符号表中的data3可知COM块中的存放弱符号,在连接时链接器会在该块中选用内存占用较大的弱符号,或者选用同名的强符号(其他文件中的符号)。

打印各个段的内容:
这里写图片描述

  • 我们能够找到定义的各个变量存储的位置

在这里我们看到上面的例子中没有 .bss 段存在,这是因为.bss段节省了文件的空间,但虚拟内存的空间是占据的。

为了进一步理解bss段的处理过程,我们来打印一下文件头:
这里写图片描述

由文件头部可知,二进制可从定位文件中有一个section段,section段保存文件中所有段的详细信息,而bss段中的数据无初始值,默认为0,对于使用时的值不确定,所以没有必要存储,但得记录其存在,所以就保存在section段中。而data段中的数据有初始值,不仅要记录其存在还要保存他的初始值,目的是为了在使用他的时候它的值不会被改变,所以得在文件中保存。

根据以上的例子,我们可以画出 .o/.obj 文件的格式:
这里写图片描述

链接过程:(生成二进制可执行文件a.out

  • 合并所有obj文件的段,并调整短偏移和段长度,合并符号表进行符号解析并分配内存地址

  • 合并符号表:根据相同属性的段进行合并,组织在一个页面上(这是由于内存是以页面方式对齐的)。

  • 符号解析:所有obj符号表中对符号引用的地方都要找到符号定义的地址。

  • 符号解析完成,符号表合并完成

  • 连接的核心 ——> 符号重定位

    • 编译过程中不分配内存地址,强符号和local符号给出在各个段中的地址,对于一些弱符号就给出默认的地址0x00000000。符号表合并成后就知道要使用哪些弱符号,就将要使用的弱符号的真实地址放在分配给该符号。

    • static定义的变量或函数只在本文件可见,生成的符号为local符号,多文件可见的变量生成gloable符号,链接器在连接的时候只处理gloable符号,不处理local符号。

在链接完成后生成可执行a.out文件,可执行文件运行起来就成为进程(linux下执行./a.out命令可将生成的可执行文件运行起来),程序到进程内核做了如下工作:

  • 创建虚拟地址空间到物理内存的映射(创建内核地址映射结构体),创建页目录和页表

  • 加载代码段和数据段

  • 把可执行文件的入口地址写入到CPU的pc寄存器里面。
    这里写图片描述

下面我们就来分析一下可执行文件的格式,首先打印一下文件头:
这里写图片描述
则可以知道可执行文件的格式和obj文件的格式很相似,下面来看一下各个段信息:

这里写图片描述

根据以上的文件各个部分的分析我们可得到可执行文件的组成格式大致为:

这里写图片描述

我们再来看看program header段的内容:

其中的两个LOAD页面是程序运行的关键,他是保存要加载到内存上的段的信息的并决定那些段加载到一个页面上,并且内存是按页面方式对齐(32位操作系统一个页面位4K)。从文件信息中可以看出只有代码段和数据段被加在了。这些段保存了程序运行的指令和数据。

程序运行过程大致为如下过程:
这里写图片描述

磁盘上的可执行文件的两个LOAD页面首先加载到虚拟地址空间,程序运行时要用到这些数据时通过地址映射加载到物理内存上,程序通过加载指令与数据开始运行到结束。

猜你喜欢

转载自blog.csdn.net/magic_world_wow/article/details/80708323