二进制程序从源文件到可执行程序的过程

在c语言中,我们都知道一个源程序从源码到可执行文件都要经历预处理,编译,汇编和链接的过程,最终形成可执行文件,然后才被操作系统装载进内存运行。

为了复习pwn知识,写个文章来探究一下经常被我忽略的关于程序的中间过程。

预编译

该过程主要处理源代码中以’#’开始的预编译指令
具体处理规则如下:

  • 删除#define并展开所有的宏定义
  • 处理所有条件预编译指令:如 “#if”等
  • 处理”#include”预编译指令,将被包含的文件插入到该预编译指令的位置
  • 删除注释,添加行号和文件名标识

预编译的过程可用如下指令来执行

gcc -E BinaryName.c -o BinaryName.i

源代码如下:

经过预编译后:

编译

编译过程经过一系列词法分析等步骤生成汇编代码文件,是程序构建的核心部分
不同的操作系统有不同的编译器,比如Linux下的gcc,g++,Windows下的MSVCCL等(后文都以Linux平台下来探究)
我们可以使用gcc -S BinaryName.i -o BinaryName.s或者gcc -S BinaryName -o BinaryName.s来查看经过编译后的文件

汇编

汇编过程可以将汇编代码转变为机器可以执行的指令
gcc -c BinaryName.s -o BinaryName.o
gcc -c BinaryName.c -o BinaryName.o

目标文件

我们先看下c语言源代码中的变量和函数

c语言源代码中的变量和函数

  • 声明和定义的区别:
    • 声明是告诉编译器该变量没有在当前文件中定义而是在其他文件中定义
    • 定义变量是告诉编译器在生成的目标文件中留出空间来存放该变量
    • 定义函数是告诉编译器在目标文件中生成该函数的二进制代码

变量类型如下:

  1. 全局变量:非static,表示该变量的生命周期是整个程序的执行期间,可以被其他文件访问
  2. 全局变量:static,生命周期也是整个程序的执行期间,但是不能被其他文件访问
  3. 局部变量:static,生命周期是程序的执行期间,但是作用域仅在该函数体中
  4. 局部变量:非static,仅存在于当前当前函数体中

对于函数也同理,如果定义了static则无法被其他文件所读取

目标文件的格式

部分段名如下

  • .data段保存已经初始化的全局静态变量和局部静态变量
  • .rodata段存放的是只读数据,一般是程序里面的只读变量(const修饰的)和字符串常量
  • .bss段存放的是未初始化的全局变量和局部静态变量
  • .comment存放的是编译器版本信息
  • .debug存放调试信息
  • .dynamic存放动态链接信息
  • .strtab String Table字符串表,用于存放ELF文件中用到的各种字符串
  • .symtab Symbol Table符号表
  • .plt .got 动态链接的跳转表和全局入口表
  • .init 程序初始化段
  • .fini 程序终结代码段

ELF文件头结构体如下

typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf32_Half e_type; /* Object file type */
Elf32_Half e_machine; /* Architecture */
Elf32_Word e_version; /* Object file version */
Elf32_Addr e_entry; /* Entry point virtual address */
Elf32_Off e_phoff; /* Program header table file offset */
Elf32_Off e_shoff; /* Section header table file offset */
Elf32_Word e_flags; /* Processor-specific flags */
Elf32_Half e_ehsize; /* ELF header size in bytes */
Elf32_Half e_phentsize; /* Program header table entry size */
Elf32_Half e_phnum; /* Program header table entry count */
Elf32_Half e_shentsize; /* Section header table entry size */
Elf32_Half e_shnum; /* Section header table entry count */
Elf32_Half e_shstrndx; /* Section header string table index */
} Elf32_Ehdr;

typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf64_Half e_type; /* Object file type */
Elf64_Half e_machine; /* Architecture */
Elf64_Word e_version; /* Object file version */
Elf64_Addr e_entry; /* Entry point virtual address */
Elf64_Off e_phoff; /* Program header table file offset */
Elf64_Off e_shoff; /* Section header table file offset */
Elf64_Word e_flags; /* Processor-specific flags */
Elf64_Half e_ehsize; /* ELF header size in bytes */
Elf64_Half e_phentsize; /* Program header table entry size */
Elf64_Half e_phnum; /* Program header table entry count */
Elf64_Half e_shentsize; /* Section header table entry size */
Elf64_Half e_shnum; /* Section header table entry count */
Elf64_Half e_shstrndx; /* Section header string table index */
} Elf64_Ehdr;

ELF header结构体如下

源代码到目标文件的处理

int globalA = 2333;		
int globalB;
static int globalC = 666; // 全局static
static int globalD;

extern int globalE; // 声明全局变量
extern int Print(); // 声明函数

void test(int a){
return n;
}

int main(void){
static int a = 123;
static int b;
int c = 1;
int d;
test(12);
return 0;
}

通过nm命令来查看目标文件的符号,从左到右分别是变量的相对地址,变量所在段的名字和变量名字。

其中a.1922是编译器修改变量名字的结果,因为a是一个局部静态变量,作用域在他的函数体中,所以当我们在不同的函数体中声明相同名字的局部静态变量(这种操作是被允许的)而且其生命周期为整个程序执行时,编译器为了支持这种功能,所以来加后缀来标识不同的局部静态变量。

我们还可以用objdump来查看目标文件的结构,-h参数可查看各个段的基本信息

Size表示段长度
File off表示在文件中的偏移
每个段第二行中的”CONTENTS” “ALLOC”等表示段的属性,”CONTENTS”表示在文件中存在
比如数据段的大小为0xc字节大小,因为有三个四字节的变量分别为globalA,globalC和a

链接

使用如下命令来完成链接过程
gcc Binary.o -o BinaryName

可以看到之前的一些相对地址全部变为了绝对地址,完成了地址重定位

装载运行

当程序加载进内存程序即可运行,在这个过程中我们不得不提到虚拟内存,动态链接库和函数执行的问题。我们给出程序执行的顺序,具体探究我们等到之后的文章再另行分析。

1.首先 bash 进行 fork 系统调用,生成一个子进程,接着在子进程中运行 execve 函数指定的 elf 二进制程序( Linux中执行二进制程序最终都是通过 execve 这个库函数进行的),execve 会调用系统调用把 elf 文件 load 到内存中的代码段(_text)中。
2.如果有依赖的动态链接库,会调用动态链接器进行库文件的地址映射,动态链接库的内存空间是被多个进程共享的。
3.内核从 elf 文件头得到_start的地址,调度执行流从_start指向的地址开始执行,执行流在_start执行的代码段中跳转到libc中的公共初始化代码段libc_start_main,进行程序运行前的初始化工作。
4.
libc_start_main的执行过程中,会跳转到_init中全局变量的初始化工作,随后调用我们的main函数,进入到主函数的指令流程。

参考文献

文章作者: Alex
文章链接: http://example.com/2021/03/09/binary-programming/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Alex's blog~