目录

c++代码变成可执行文件

(1)预处理过程

(2)编译过程

(3)汇编过程

(4)链接过程

(5)目标文件

(6)静态链接和动态链接

可执行文件变成进程

正文

image-20191128103820967

一个c++程序,从.c文件,变成二进制可执行文件,会经历上述四个过程。

预处理阶段:对源代码文件中文件包含关系(头文件)、预编译语句(宏定义)进行分析和替换,生成预编译文件。

编译阶段:将经过预处理后的预编译文件转换成特定汇编代码,生成汇编文件

汇编阶段:将编译阶段生成的汇编文件转化成机器码,生成可重定位目标文件

链接阶段:将多个目标文件及所需要的库连接成最终的可执行目标文件


预处理过程

1
gcc -E hello.c -o hello.i

预处理最终生成.i文件。预处理主要处理“#”开头的代码指令。主要内容如下:

(1)展开所有的宏定义(#define)

(2)处理“#if”,”#endif”

(3)递归的处理“#include”文件

(4)删除所有的注释

(5)给代码,添加行号,一边编译和调试输出信息


编译过程

gcc -S hello.i -O hello.s

编译过程最终生成.s文件。编译过程就是吧预处理完的文件,进行词法分析,语法分析,语义分析以及优化后产生相应的汇编代码。

关于词法分析,语法分析等这些更具体的过程,可以回顾一下编译原理这门课程,或者相关书籍。


汇编过程

1
gcc -C hello.s -O hello.o

汇编过程最终生成.o文件(目标文件)。这个过程相对简单,它就是将汇编代码,根据具体的对照表进行翻译,生成一句一句的机器指令的过程。


链接过程

链接器比编译器出现的还要早,在纸带打孔的时代就已经出现。链接器的作用其实就是 将不同的模块分别进行编译,生成目标文件。这样的好处是一个模块的修改,不会影响到整个系统,更便于修改和重复使用。


目标代码

目标文件主要分为两种:Windows的PE(Portable Executable)和linux下的ELF(Executable Linkable Format),以ELF为例,它的格式如下:

Other Data
.comment 存放编译器版本信
.rodata 存放只读数据,如常量,const
.data 数据段
.text 代码段
ELF head 文件头


目标文件的头中包含一个长度为 32 个字节的 exec 数据结构,通常称为文件头结构或执行头结构,其具体定义如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
struct exec{
    unsigned long a_magic,    //执行文件魔数,使用 N_MAGIC 等宏访问
    unsigned a_text ,   //代码段长度 byte
    unsigned a_data,  //数据区长度 byte
    unsigned a_bss,  //文件中未初始化数据区长度
    unsigned a_syms,  //文件中符号表长度
    unsigned a_entry,  //执行地址入口
    unsigned a_trsize,  //代码重定位信息长度
    unsigned a_drsize   //数据重定位信息长度
};

其中一个有意思的就是 魔数,这个是头文件的最开始的两个字节。它的作用就是用来判断文件的类型。操作系统在加载可执行文件的时候,首先检查的就是这个魔数,如果魔数不正确,就不会加载。

注意上面的是ELF文件结构,这和进程的虚拟空间结构不同,这个没有.bss。进程的空间结构如下。

image-20191128112903472

在C++中,虚拟内存分为代码段、数据段、BSS段、堆区、文件映射区以及栈区六部分。

代码段:包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。

数据段:存储程序中已初始化的全局变量和静态变量

bss :存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量。

堆区:调用new/malloc函数时在堆区动态分配内存,同时需要调用delete/free来手动释放申请的内存。

映射区:存储动态链接库以及调用mmap函数进行的文件映射. 处理大文件。

:使用栈空间存储函数的返回地址、参数、局部变量、返回值


链接分为两种,一种是动态链接,一种是静态链接

静态链接

image-20191128114231593

静态链接做的事情如上图所示,它将不同的目标文件合并,生成一个大的目标文件。

静态链接具体做的事情如下:

(1)空间与地址的分配

扫描所有的输入目标文件,并且获得它们的各个段的长度,属性和位置,并且将输入目标文件中的符号表中所有的符号和符号引用收集起来,统一放在一个全局符号表中。这一步中,链接器将能获得所有输入目标文件的段长度,并且将它们合并,计算出输出文件中各个段合并后的长度与位置,并建立映射关系

(2)符号解析与重定位 使用上面第一步中收集的信息,读取输入文件中段的数据,重定位数据,并且进行符号解析与重定位,调整代码中的地址等。事实上第二步是链接过程的核心,特别是重定位的过程。


动态链接

可以看出来静态链接有一个很不好的地方在于,它浪费了内存空间。如上图所示,假设object C是某个公共库的文件,例如printf的二进制文件,假设占1M。那么假设电脑中有100个使用了printf的程序。那么printf最终会占用100M。所以这个时候动态链接出现了,

动态链接的核心就是,运行的时候加载需要的目标文件。这样带来的挑战就是如何装载时重定位。

具体的过程大概是这样。 在形成可执行程序时,发现引用了一个外部的函数,此时会检查动态链接库,发现这个函数名是一个动态链接符号,此时可执行程序就不对这个符号进行重定位,而把这个过程留到装载时再进行。


可执行文件变成一个进程

一般的操作系统都包括shell。shell是壳, 它是一个软件, 它提供了一个用户操作系统的一个接口, 通过shell我们可以调用其他软件,比如chmod chown firefox man 等, 它可以调用内核来工作. shell其实就是一个接口;

在我们通过shell,运行一个编译链接好的二进制文件后。发生了什么事情呢?

首先,shell是一个进程,它有一个主循环,能通过getcmd 读取命令行的输入。然后它调用fork生成一个shell进程的副本。父shell调用wait ,而子进程执行用户命令。

需要留意的是父子进程拥有不同的内存空间和寄存器,改变一个进程中的变量不会影响另一个进程。 但是刚开始子进程和父shell使用的都是同一份代码。

接下来shell在调用 exec,将从某个文件(通常是可执行文件)里读取内存镜像,并将其替换到调用它的进程的内存空间。这份文件必 须符合特定的格式,规定文件的哪一部分是指令,哪一部分是数据,哪里是指令的开始等等。而这里的文件就是我们上面提到的ELF文件。

子进程执行完后exit.然后返回到父进程 shell.