从源代码到可执行文件

前言:

  要了解程序的漏洞,那就得先熟悉程序本身。通常我们接触得多的要么是源码,要么是已经编译好的可执行文件,而中间经历了些啥就有点陌生了。这个过程对于后面二进制安全的学习是至关重要的,本文仅讲解基本知识,深入一点的东西后面再单独写。本文以Linux下的ELF文件来学习,至于其它比如exe文件等格式以后再讲。

编译原理

以C语言为例,C语言编译主要分为四个阶段:
1,预处理
  预处理指在编译,链接之前对源文件进行的宏定义、文件包含等操作,主要是处理以#开头的命令,例如#include 等。预处理命令要放在所有函数之外,而且一般都放在源文件的前面。

2,编译
  此阶段完成语法和语义分析,然后生成中间代码,此中间代码是汇编代码

3,汇编
  将汇编代码转换为二进制代码

4,链接
  将有关的目标文件彼此相连接,使得所有的这些目标文件成为一个能够使操作系统装入并执行的统一整体。
file

预处理

  上面已经叙述了预处理的概念,这里细说一下预处理是怎么实现的以及有哪些预处理操作。

预处理命令:预处理操作是通过以#号开头的命令来实现的,如#include,#define,#undef等
例如:

#include <stdio.h>
int main()
{
     printf("Hello World\n")
     return 0;
 }

  这里面的 '#include ' 就是一个预处理操作,意思是编译器会将stdio.h文件嵌入到此文件中。

具体嵌入过程如下:
  编译器以C文件作为一个单元,首先读这个C文件,发现包含.h文件,就会在所有搜索路径中寻找这个文件,之后,就会将相应.h文件中再去处理宏,变量,函数声明,嵌套的.h文件包含等,检测依赖关系,进行宏替换,看是否有重复定义与声明的情况发生,最后将那些文件中所有的东西全部扫描进这个当前的C文件中,形成 .i 中间文件。

可以类比一下php的文件包含,只是这个更为复杂。

C语言中部分预处理命令:
指令  说明
\#  空指令,无任何效果
#include    包含一个源代码文件
#define 定义宏
#undef  取消已定义的宏
#if 如果给定条件为真,则编译下面代码
#ifdef  如果宏已经定义,则编译下面代码
#ifndef 如果宏没有定义,则编译下面代码
#elif   如果前面的#if给定条件不为真,当前条件为真,则编译下面代码
#endif  结束一个#if……#else条件编译块

大多数预处理器指令属于下面3种类型:
●宏定义:#define 指令定义一个宏,#undef指令删除一个宏定义。
●文件包含:#include指令导致一个指定文件的内容被包含到程序中。
●条件编译:#if,#ifdef,#ifndef,#elif,#else和#dendif指令可以根据编译器可以测试的条件来将一段文本包含到程序中或排除在程序之外。

剩下的#error,#line和#pragma指令更特殊的指令,较少用到。

编译

  这里的编译与我们常常说的编译不同,这里是狭义的编译。
这个阶段是词法与语法分析阶段,同时也是源文件的优化阶段,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。

  优化一部分是对中间代码的优化。这种优化不依赖于具体的计算机。另一种优化则主要针对目标代码的生成的优化。

  对于前一种优化,主要的工作是删除公共表达式、循环优化(代码外提、强度削弱、变换循环控制条件、已知量的合并等)、复写传播,以及无用赋值的删除,等等。

  后一种类型的优化同机器的硬件结构密切相关,最主要的是考虑是如何充分利用机器的各个硬件寄存器存放的有关变量的值,以减少对于内存的访问次数。另外,如何根据机器硬件执行指令的特点(如流水线、RISC、CISC、VLIW等)而对指令进行一些调整使目标代码比较短,执行的效率比较高,也是一个重要的研究课题。

  理解起来就是,源文件在这个阶段检查了语法,做了一下瘦身,变成汇编代码[ .s ]文件。

汇编

  上一个阶段把源代码变成了汇编代码,这个阶段就是把汇编代码转变为二进制码也就是目标文件。

  这个目标文件就已经开始由段结构组成了,至少分为两个段:
数据段(.data):主要存放程序中要用到的各种全局变量或静态的数据。
代码段(.text):该段中所包含的主要是程序的指令。
"关于这些段结构,之后再细说"

  UNIX环境下,这里的目标文件通常指的是可重定位文件,其中包含有适合于其它目标文件链接来创建一个可执行的或者共享的目标文件的代码和数据。
  通俗的讲就是,这个目标文件具备了与其它文件进行链接的代码和数据,而链接之后的文件也属于目标文件,只是链接后的目标文件才是可执行的目标文件。关于这里的目标文件,之后在独立文档里再详细说一下。
此阶段 [ .s ] 文件转变为 [ .o ] 文件。

链接

据我们熟悉的可执行文件只差这一步了

  这个阶段需要理解的是,我们写的源文件,既是经过前面三个阶段的处理之后,仍然是不能执行的。其中还有一些问题,如目标文件之间引用彼此定义的函数或变量 (统称 '符号' ),或者是对库文件的调用等。还需要链接程序来完成最后的工作。

  链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够使操作系统装入执行的统一整体。

根据链接方式不同,分为了两种:
  1,静态链接:这种方式是将所链接的目标文件全都打包成集合。

  静态链接,函数的代码将从其所在地静态链接库中被拷贝到最终的可执行程序中。这样该程序在被执行时这些代码将被装入到该进程的虚拟地址空间中。静态链接库实际上是一个目标文件的集合,其中的每个文件含有库中的一个或者一组相关函数的代码。

  2,动态链接:这种方式中,被调用的库或目标文件和我们要执行的目标文件是分开的,仅仅是在执行时才映射到虚拟地址空间。

  动态链接,被调用的函数代码被放到称作是动态链接库或共享对象的某个目标文件中。链接程序此时所作的只是在最终的可执行程序中记录下共享对象的名字以及其它少量的登记信息。在此可执行文件被执行时,动态链接库的全部内容将被映射到运行时相应进程的虚地址空间。动态链接程序将根据可执行程序中记录的信息找到相应的函数代码。

  这两种链接的区别要比较明显,静态链接会导致最终的可执行文件较大,而动态链接的可执行文件本身虽小,且当多个可执行文件同时调用同一个库或目标文件时也比较节省内存。

这里的链接也只是概念,原理部分后面再单独弄一篇文档叙述一下。

到这步完成,整个编译过程就结束了。
  预处理部分建议再取了解一下预处理指令,比如宏定义,文件包含,条件编译等

发表评论

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据