一个C源文件到可执行文件 [反汇编-函数栈帧 编译 链接]

来源:互联网 发布:学霸 知乎 编辑:程序博客网 时间:2024/05/01 02:10

《Linux C 编程一站式学习》第18,19章练习。

平台:x86/Debian GNU/Linux gcc

1 C源文件代码对应的指令

计算机是由数字电路组成的运算机器,只能对数字做运算。加载到内存中运行的文件被称之为可执行文件,可执行文件中的二进制对应着C源代码的标识符和数据。由一个C源文件到可执行文件可分为两个阶段:编译和链接。对可执行文件进行反汇编可以看到C代码中的每个语句所对应的机器指令bytes(left)及反汇编代码(right)。如以下C语言程序可用gcc编译器来描述其生成可执行文件的过程:

[cpp] view plain copy

/* Filename:    c_to_elf.c  * Brife:   To see c source file to be elf file by compiling and link  * Date:    2014.7.20 - Sunday  * Author:  One fish  */  #include <stdio.h>  //------Global varible-----------  static int i;  //------Functions decaration  int add_ij(int i, int j);  //---Main the entry of this application  int main(void)  {      int j, k = 1;      i   = k;      j   = 2;      add_ij(i, j);      return 0;  }  int add_ij(int i, int j)  {      int k;      k   = i + j;      return k;  }  

(1) gcc命令选项
Figure1.gcc 命令选项
图中的各个名字是gcc默认生成的(不用-o选项让生成文件以其它名字生成)。以.s为后缀的为C源文件对应的汇编文件;以.o为后缀的为C源文件对应的目标文件;.out为C源文件对应的可执行文件。

(2) 目标文件

选择由C源文件汇编文件得到目标文件:

gcc -g -S c_to_elf.c [生成c_to_elf.s,是c_to_elf.c对应的汇编代码]

gcc -g -c c_to_elf.s[生成c_to_elf.o,即为目标文件]

-g选项是使生成的文件能够关联上C源代码。

(3) 反汇编–可执行文件

选择由(2)中生成的目标文件得到可执行文件c_to_elf:gcc -g c_to_elf.o -o c_to_elf。目标文件也可以进行反汇编。
用objdump命令可对可执行文件进行反汇编:objdump –dS c_to_elf > exe_disassembly.txt。对c_to_elf的反汇编信息都在exe_disassembly.txt文件中,只看C代码对应指令部分[第一列为指令地址,中间的几列为机器指令,最后的是汇编代码]:
[cpp] view plain copy

080483dc <main>:  int add_ij(int i, int j);  //---Main the entry of this application  int main(void)  {   80483dc:   55                      push   %ebp   80483dd:   89 e5                   mov    %esp,%ebp   80483df:   83 e4 f0                and    $0xfffffff0,%esp   80483e2:   83 ec 20                sub    $0x20,%esp      int j, k = 1;   80483e5:   c7 44 24 1c 01 00 00    movl   $0x1,0x1c(%esp)   80483ec:   00       i   = k;   80483ed:   8b 44 24 1c             mov    0x1c(%esp),%eax   80483f1:   a3 84 96 04 08          mov    %eax,0x8049684      j   = 2;   80483f6:   c7 44 24 18 02 00 00    movl   $0x2,0x18(%esp)   80483fd:   00       add_ij(i, j);   80483fe:   a1 84 96 04 08          mov    0x8049684,%eax   8048403:   8b 54 24 18             mov    0x18(%esp),%edx   8048407:   89 54 24 04             mov    %edx,0x4(%esp)   804840b:   89 04 24                mov    %eax,(%esp)   804840e:   e8 07 00 00 00          call   804841a <add_ij>      return 0;   8048413:   b8 00 00 00 00          mov    $0x0,%eax  }   8048418:   c9                      leave     8048419:   c3                      ret      0804841a <add_ij>:  int add_ij(int i, int j)  {   804841a:   55                      push   %ebp   804841b:   89 e5                   mov    %esp,%ebp   804841d:   83 ec 10                sub    $0x10,%esp      int k;      k   = i + j;   8048420:   8b 45 0c                mov    0xc(%ebp),%eax   8048423:   8b 55 08                mov    0x8(%ebp),%edx   8048426:   01 d0                   add    %edx,%eax   8048428:   89 45 fc                mov    %eax,-0x4(%ebp)      return k;   804842b:   8b 45 fc                mov    -0x4(%ebp),%eax  }   804842e:   c9                      leave     804842f:   c3                      ret     

(4) 由反汇编分析程序运行时堆栈内存的变化

虽然程序中的内存地址都是虚拟地址,但可根据虚拟内存与物理内存的映射关系用虚拟内存地址来模拟堆栈内存上物理地址的变化。

[1]进入main函数main栈帧的开始
Figure2.程序进入main函数运行
esp寄存器指向栈底,ebp寄存器指向栈顶。进入main函数时假设esp指向栈内存的内容为……。然后栈内存会随着接下来的指令而得到分配。

[2]main函数内的局部变量的栈内存
Figure3.main函数局部变量栈内存
用栈内存地址esp + 0x1c 和 esp + 0x18来存储k和j(这个关系会被记住,当程序中用到k或j时,关联的就是这两个地址或者地址中的内容)。

[3] main函数栈帧的形成
Figure4.main函数栈帧的形成
到main函数的return语句处就标志着main函数栈帧形成。调用子函数给子函数传参数是将参数的值压入(复制了参数)本函数栈中(这个地址会被记住,进入子函数后到这个地址来取值)。全局变量i的地址是0x8049684这个地址是在程序链接时被分配的。

[4] 子函数add_ij()栈帧的分配与回收
Figure5.add_ij子函数栈帧分配及回收
进入子函数add_ij后,为add_ij及其内部的变量分配栈内存形成add_ij的函数栈帧。当退出add_ij函数执行完毕后,add_ij函数栈帧被操作系统回收。依据对ebp和esp的操作,此时会返回到调用子函数add_ij的函数中,去继续执行调用函数的下一个语句。

[5]main函数栈帧的回收

Figure6.main函数栈帧的回收

main()是一个具入口性质的函数(首先被调用),当main函数执行完毕后。它的函数栈帧以相同的方式被回收,ebp与esp会返回到到上一层栈帧(如果有)。

对可执行文件进行的反汇编中可以看出,可执行文件中的有的地址(如全局变量i)已经被分配,可程序还没有运行,无法保证这个地址的可用性。其实这个地址是虚拟地址,可执行程序被加载到内存中运行时,操作系统会检查内存中的页表看这个虚拟地址可以映射到物理地址中的哪一个地址,这个地址才是变量真正的地址。MMU的存在可以解决链接时为程序分配的地址是否可用问题。

2 编译—-C源文件到目标文件

编译是指将C代码翻译成汇编或机器指令的过程。

(1) C程序对应的汇编代码
C源文件可被gcc编译为汇编文件。即此笔记前提到的c_to_elf.s文件,摘main和add_ij()函数对应的汇编代码如下:

Figure7.main和add_ij()对应的汇编代码

(2) C程序的目标文件

笔记前提到的c_to_elf.o为c_to_elf.c对应的目标文件,在linux下用readelf –a c_to_elf.o > obj.txt命令将目标文件中的内容读到obj.txt文件中,截取一部分出来瞧瞧:

[1] ELF Header

Figure8.目标文件中的ELF Header
从ELF Header中可以看出,计算机内的数字采用的2’s complement方式,小端方式存储。操作系统为UNIX,文件本身类型为REL,文件中段头(section headers)的文件起始地址为1264,一共有21个段头。

[2] Section headers

截图看以下section headers的信息:
Figure9.Section headers
与ELF Header描述相对应,一共有0 – 20个section header,除了与汇编文件中对应的.text、.data、.bss段(section)之外,其它的都是编译器自动添加的。Addr是这些段加载到内存中的地址,加载地址要在链接时设定(设定的虚拟地址),所以这里全是0。Off和size列支出了各段(section)的起始文件地址和长度。C源文件中的全局变量static int i;变量被存在了.bss section(还没有内存地址)。局部变量要在运行到局部变量处时方为其分配空间。

3 链接—-C源文件到可执行文件
链接主要有两个作用,一是修改目标文件中的信息,对地址做重定位;二是把多个目标文件合并成一个可执行文件。目标文件需要经过链接才可以成为可执行文件,才能被操作系统加载到内存中运行。可执行文件是指前笔记中生成的c_to_elf。

[1] ELF Header
Figure10.可执行文件的ELF Header
在可执行文件中,文件类型变为EXEC,Entry point address为_start符号的地址。同时,program headers也诞生了,section headers的数目也发生了变化。

[2] Section Headers

截取一部分如下图:
Figure11.可执行文件中的section headers
可执行文件是由目标文件经过链接而来的,为各个段都分配了虚拟地址。原来的.text、.data、.bss段都被分配了虚拟地址。笔记前面特别关注了全局变量i的虚拟地址为0x8049684,这里的.bss段的虚拟地址为0x8049680,如果全局静态变量位于.bss段的话,为什么相差4个字节同求解释。在可执行文件中找到了i的信息:

Figure12.全局变量i的信息
从此表中可以看出i的地址为0x8049684。

[3] Program Headers

Program Headers用来描述Segment的信息。Segment由多个Section组成。一般是将具有共同属性如.data和.bss汇聚为一个Segement加入到内存。个人理解目标文件以Section的形式存在,可执行文件以Segement的形式存在以方便加载到内存中运行。

4 小结

一个C源文件经过编译和链接可形成可执行文件。编译的过程C源码翻译成机器指令或汇编代码。对目标文件进行反汇编可以看到,程序中地址如函数的地址是用的相对地址。经过链接的目标文件为程序分配了虚拟地址,程序中使用的是绝对地址(反汇编查看)。

虚拟内存可解决链接时为可执行文件加载到内存中的地址冲突问题。如果直接使用物理地址,怎么敢保证链接时为程序分配的地址没有被用到。而有了MMU后,跟程序有关的地址都是虚拟地址,操作系统会根据内存中的页表将程序加载到可用的内存中去。

《Linux C 编程一站式学习》P.299:程序的函数调用规则(1中的函数栈帧)并不是体系结构所强加的,ebp寄存器并不是必须这么用,函数的参数和返回值也不是必须这么传,只是操作系统和编译器选择了以这样的方式实现C代码中的函数调用,这称为Calling Convention,Calling Convention是操作系统二进制接口规范。

CNote Over.

原创粉丝点击