C/C++——编译过程及相关文件分析

1. 编译过程

每种高级语言都有对应的编译器,而且针对不同指令集架构的CPU会提供不同的编译器。本文以C语言为例,CPU指令集架构不做前提约束,实际上同一种语言也只有在狭义的编译阶段有所区别,其他阶段的处理,如二进制文件处理等待均类似。

C语言的编译有两种方式,一种是本地编译(在一个平台上编译该平台运行的程序),另一种是交叉编译(在一个平台上编译供另一个平台运行的程序)

  • 编译的功能
  1. 将高级语言转换为处理器能够执行的二进制代码。
  2. 对编程语言进行语法检查核逻辑检查
  3. 分配寄存器和内存地址(存储器)
  4. 代码优化
  • 编译过程
    C语言的翻译可以分为四个阶段:预编译阶段编译阶段,汇编阶段,链接阶段
    C/C++——编译过程及相关文件分析
  1. 预编译阶段
    主要处理文件中宏定义,注释,以及将头文件插入到主代码中,生成扩展名为“.i”的文件,替换原来的.c文件(原文件保留)。
  2. 编译阶段
    将代码转化成汇编语言或某种中间代码。编译器首先会检查代码的规范性,是否有语法错误等,以确定代码实际要做的工作;检查完毕后转换为底层机器可处理的汇编语言,即生成.s文件替换.i文件。

    该阶段会进行优化处理,除了对中间代码的结构进行优化外,还会充分利用硬件寄存器中存储的变量值,减少内存访问次数;根据指令执行特点(RSIC,CSIC,流水线等)对指令进行调整
  3. 汇编阶段
    将汇编语言直接映射为二进制码,以二进制码格式的指令将会被打包封存成可重定位的目标程序的格式,生成.o文件替代原来的.s文件。.o文件由段组成,且至少有两个段:代码段和数据段。代码段主要包含程序的指令,数据段主要是各种全局变量或静态数据。

    UNIX环境下会生成三个目标文件:
    1) 可重定位文件:其中包含有适合于其它目标文件链接来创建一个可执行的或者共享的目标文件的代码和数据。
    2) 共享的目标文件:这种文件存放了适合于在上下文里链接的代码和数据。
    3) 可执行文件:包含了一个可以被操作系统创建一个进程来执行之的文件。
     
  4. 链接阶段
    将多个.o文件合并整合成一个可执行文件。上图表现的是一个动态链接过程。

    该阶段的三个步骤:
    1)将代码和数据模块象征性地放入内存
    2)决定数据和指令标签的地址
    3)修补内部和外部引用
  • 二进制文件的内存加载
    链接后,可执行文件已经在磁盘中,操作系统或无操作系统的机器可以通过装载器(loader)将其读入内存并启动执行它
    C/C++——编译过程及相关文件分析
  1. 对于静态链接的内存加载:
    对于以静态链接形式编译的程序,由于其已经是一个完整的不依赖于任何其他东西的可执行程序,可以直接被执行。
  2. 对于动态链接的内存加载工作步骤:
    loader程序将elf文件读入内存,然后启动链接器程序根据elf文件头的说明,将其所需要的其他程序段找到读入内存并装配在一起(相似段融合,然后重新编排符号表和重定位表,并根据重定位表来做重定位)。装配完后返回loader程序继续执行,然后loader直接跳转到装配完成的程序中的入口地址处执行。
  • 函数调用时资源分配和进程调度
  1.  发生函数调用时,在执行该函数前,先将这次调用中需要用到的参数保存,方便取用
  2. 将控制权移交给这次调用的功能函数
  3. 根据情况为函数申请一定的本地存储空间,满足函数存储过程中的存储需求
  4. 执行该函数的功能调用
  5. 执行完成后,将结果数据保存好,便于主进程获取,同时还原函数执行过程中使用过的寄存器值,恢复先前分配的存储空间。
  6. 将控制权转移给主进程
  • 堆栈

假设一个过程,编译器需要使用多于4个参数寄存器和两个返回值寄存器。由于任务完成后必须消除踪迹,因此调用者使用的任何寄存器都必须恢复到过程调用前所存储的值。这种情况可以看成是需要将寄存器换出到存储器的一个例子。

换出寄存器的最理想的数据结构是栈——一种后进先出的队列,栈需要一个指针指向栈中最新分配的地址,以指示下一个过程放置换出寄存器的位置,或是寄存器旧值的存放位置。在每次寄存器进行保存或恢复时,栈指针以字为单位进行调整,由此将数据放入栈中称为压栈,从栈中移除数据称为出栈

栈:被组织成后进先出队列形式并用于寄存器换出的数据结构
栈指针:指示栈中最近分配的地址的值,它指示寄存器被换出的位置,或寄存器旧值的存放位置
压栈:向栈中增加元素
出栈:向栈中移除元素

2. elf文件格式

C/C++——编译过程及相关文件分析

  • ELF Header
    链接后的elf文件有三种类型,分别是可重定位文件rel(包含适合于与其他目标文件链接来创建可执行文件,或者共享目标文件的代码和数据);可执行文件exec(包含适合于执行的的一个程序,此文件规定了exec()如何创建一个程序的进程映像);共享目标文件dyn(包含可在两种上下文链接的代码和数据,如linux中已.so结尾的文件)

简写为Ehdr,  包含以下信息:
e_ident[**]                   //elf标识
e_type;                       //elf类型
e_ machine;               //目标文件体系类型,即运行架构
e_version;                  //目标文件版本
e_entry;                      //elf入口地址
e_ phoff;                     //程序头部偏移
e_shoff;                      //节区头部偏移
e_flags;                     
e_ehsize;                  //ELF格式头部大小
e_phentsize;             //程序头部表项大小
e_phnum;                  //程序头表项个数,即segment数
e_shentsize;              //节区头部表项大小
e_shnum;                  //节区表项个数,即section数
e_shstrndx;

  • Program Header Table

简写为Phdr,包含以下信息:
p_type;                    //segment类型
p_offset;                  //segment在文件中的偏移
p_vaddr;                 //segment虚地址
p_paddr;                 //物理地址
p_filesz;                  //文件中segment字节数
p_memsz;              //内存中segment字节数
p_flags;
p_align;

  • segment
    内容包括text segment,data segment等,segment包含多个section,
  1. .text
    已编译程序的指令代码段
  2. .rodata
    ro代表read only,表示只读数据
  3. .data
    已初始化的C程序全局变量和静态局部变量。C程序普通变量在运行是被保存在堆栈中,既不在.data中,也不在.bss中,此外,如果变量初始化值为0,也可能会放到bss段。
  4. .bss
    未初始化的C程序变量和静态局部变量。目标文件格式区分初始化和未初始化变量是为了空间效率,在ELF文件中.bss段不占据实际的存储器空间,仅仅是一个占位符。
  5. .debug
    调试符号表,调试器用此段的信息帮助调试

简称sym,包含以下信息:
st_name;        
st_value;
st_size;
st_info;
st_other;
st_shndx;

  • section header table

简称Shdr,包含信息:
sh_name;                 //节区名称,字符串表索引值
sh_type;                   //节区种类,如rel*
sh_flags;                 //
sh_addr;                  /地址
sh_offset;                //输出节区第一个字节偏移
sh_size;                   //节区大小
sh_link;                    //给出字节头部表索引链接
sh_info;                   //给出节区附加信息
sh_addralign;          //对齐约束
sh_entsize;             //给出对于某些有固定项目的大小,如符号表