运行c_一段C代码,是如何编译运行的?

641b2e9faadea3f9e58d7abb15c73025.png

想一下, 我们想把源文件放到内存中执行,应该怎么做?

直觉上我们需要将源代码翻译成机器语言,以某种结构组织代码和数据。再让CPU去按这种结构读取指令。如果是多个源文件, 我们可能还需要按某种方式将它们组合到一块。 编译运行的原理其实大致类似,下面让我们看下具体流程:

一、 源文件的编译执行流程

链接(linking)是将各种代码和数据片段收集并组合成一个单一文件的过程, 这个文件可被加载到内存并执行。

首先, 来看两段C代码:

code/link/main.c

int sum(int *a, int n);
int array[2] = {1,2};
int main () {
     int val = sum(array , 2);
     return val;
}

code/link/sum.c

int sum (int *a, int n) {
     int i, s = 0;
     for(i = 0; i < n; i++){
           s += a[i];
     }
     return s;
}

在linux 上我们可以通过系统提供的gcc编译器将其编译成可执行文件prog,命令如下:

gcc -o prog main.c sum.c

那执行这个命令时, 系统具体都做了哪些内容呢?如图所示展示了源文件编译链接的整个过程:

d1d45e257541dc460ea2208b26a1445e.png
  1. 首先预处理将C的源文件main.c 翻译成一个ASCII 码的中间 main.i ,这个过程等价于命令:cpp main.c /tmp/main.i
  2. 编译器 ccl 将main.i 翻译成一个ASCII 汇编语言文件 main.s ,等价于: ccl /tmp/main.i -Og -o /tmp/main.s
  3. 然后,汇编器as , 将main.s 翻译成一个可重定位目标文件main.o, 等价于:as -o /tmp/main.o /tmp/main.s
  4. sum 的编译过程和main类似
  5. 链接器把main.o 和 sum.o 以及一些必要的目标文件链接在一起, 生成一个可执行目标文件
  6. 最后, 我们用shell 命令./prog 执行程序。 操作系统会调用一个叫做加载器的函数(loader), 将可执行文件中的代码和数据复制到内存,然后将控制转移到程序开头。

二、可重定位目标文件的结构

在上面的过程中我们提到了两个目标文件: 可重定位目标文件和可执行目标文件。

目标文件, 其实就是子节序列以文件的形势存放在硬盘中。 源代码以某种格式序列化, 在运行的时候系统按找这种格式去找到每个函数,每个执行和每个变量的地址。 linux 以可执行可链接(ELF)格式存储。

为了更好的被CPU 识别, 想一下如何组织这些二进制代码可以更高效呢?

就像书桌上的物品要分类放置才整洁一样,为了便于管理翻译出来的二进制代码也分类存放,把表示代码的放在一起,表示数据的放在一起。这样,二进制代码就分为了不同的块来存放。这样的一个区域就是被称为段(segment)的东西。

首先,让我们来看一下由源文件编译产生的可重定位目标文件长什么样子:

736ad8040c7e387714b0d25cb3d54897.png
  • ELF 头: 记录程序入口地址 , 目标机器型号版本号等信息。
  • .txt : 文本段
  • .data .bss : 数据段(.data 存放已初始化的数据, .bss存放未初始化的数据)
  • .rel.text : 文本重定位段
  • .rel.data: 数据重定位段(被模块引用或定义的全局变量的重定位信息)
  • .symtab: 符号表 (符号表是一个符号信息的数组, 数组的每个条目中包含被分配到哪个节、 距该节起始位置的偏移)。

一般来说,代码中都会存在引用了外部的函数,或者变量的情况。既然是引用,那么这些函数、变量并没存在该目标文件内。在使用他们的时候, 就要给出他们的实际地址(这个过程发生在链接的时候)。而重定位表,提供了寻找这些实际地址的信息。 当汇编器遇到最终位置未知的目标引用时, 就会生成一个重定位条目。

一个可执行文件往往包含多个源文件? 在我们产生了多个可重定位目标文件之后, 链接器又是怎么把他们汇集到一起的呢?

三、 链接过程

每个全局变量和函数都可以看作是一个符号,存放在符号表中。 链接过程可以大致分为两步:

1. 符号解析 :将每个符号引用比如代码main.c 中的 sum , array 和符号表中的的符号定义关联起来 。

2. 重定位: 将可重定位目标文件组合起来,将所有相同类型的节合并。 将运行时的内存地址赋给新的聚合节,把符号定义和一个内存地址相关联。 然后修改这些符号的引用, 使他们指向这个内存位置。通过重定位段 .rel.text 和.rel.data 生成 已重定位的.text节和.data 节。

3. 加载到内存之后, .text 为只读内存段,.data 和.bss为读/写内存段。

四、 加载可执行目标文件

1059a1c131a61b14108af33a9871795c.png

程序复制到内存运行之后的内存布局如上图所示。 刚才我们提到.data和.bss节中保存全局和静态变量, 而局部变量在运行时保存在栈中。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值