编译

2019-10-03  本文已影响0人  delta1037

编译


一、系统环境

二、GCC编译过程

2.1 GCC编译过程

编译过程-简单.png
  1. 预处理

    • 删除所有的#define,展开所有的宏定义
    • 处理所有的条件预编译指令<#if,#endif,#ifdef,#ifndef,#elif,#else>
    • 处理#include预编译指令,将包含的文件插入到include的位置(递归进行)
    • 删除所有的注释
    • 添加行号和文件名标识(调试时使用)
    • 保留所有的#pragma编译器指令(编译器需要使用这些指令)
    # 单独产生预处理后的文件(本模块假设hello.c是源代码程序,hello.i是hello.c预处理后的文件,hello.s是hello.c编译后的文件,hello.o是hello.c汇编后的文件,hello是hello.c最终的可执行程序)
    
    # 使用gcc命令产生预处理文件
    $ gcc -E hello.c -o hello.i
    
    # 使用cpp命令产生预处理文件
    $ cpp hello.c > hello.i
    
  2. 编译:将预处理完的文件进行一系列的词法分析、语法分析、语义分析、中间代码生成、目标代码生成与优化之后产生相应的汇编代码文件

    • 词法分析:扫描器运行类似于有限状态机的算法将代码的字符序列分割成一系列的记号
    • 语法分析:语法分析器对扫描器产生的记号进行语法分析,从而产生语法树(以表达式为节点的树)
    • 语义分析:语义分析器确定语句的意义(比如两个指针做乘法是没有意义的),编译器只能分析静态语义(在编译时能够确定的语义,通常包括声明和类型的匹配,类型的转换;与之相对的动态语义是在运行时才能确定的语义,例如将0作为除数是一个运行期语义错误)
    # 编译预处理后的文件产生汇编代码文件
    $ gcc -S hello.i -o hello.s
    
    # 编译源文件产生汇编代码文件
    $ gcc -S hello.c -o hello.s
    
    # 现在的gcc编译器将预处理和编译两个步骤合成了一个步骤,使用一个叫cc1的程序来完成这个过程
    $ /usr/lib/gcc/x86_64-linux-gnu/7/cc1 hello.c -o hello.s
    
  3. 汇编:将汇编代码转变成机器可以执行的指令(根据汇编指令和机器指令的对照表一一翻译)

    # 使用as处理汇编文件产生目标文件
    $ as hello.s -o hello.o
    
    # 使用gcc处理汇编文件产生目标文件
    $ gcc -c hello.s -o hello.o
    
    # 使用gcc处理源文件产生目标文件
    $ gcc -c hello.c -o hello.o
    
  4. 链接:将目标文件链接到一起形成可执行文件,主要包括地址和空间分配,符号决议,和重定位等步骤

    • 符号决议:也叫做符号绑定、名称绑定、名称决议等等。从细节上来讲,决议更倾向于静态链接,绑定更倾向与动态链接

    • 重定位:编译一个文件时不知道一个要调用的函数或者需要操作的一个变量的地址,就会把这些调用函数或者操作变量的指令目标地址搁置,等到最后链接的时候由链接器去将这些指令的目标地址修正,这个地址修正的过程也被叫做重定位,每一个需要修正的地方叫做重定位入口

2.2 实际编译过程

  1. 使用如下样例,包含hello.c和func.c两个源文件(之后也是用这两个文件进行分析)

    /* hello.c:主测试程序,包括全局静态变量,局部静态变量,全局变量,局部变量,基本的函数调用 */
    // export var
    extern int export_func_var;
    
    // global var
    int global_uninit_var;
    int global_init_var_0 = 0;
    int global_init_var_1 = 1;
    
    // const var
    const char *const_string_var = "const string";
    
    // static global var
    static int static_global_uninit_var;
    static int static_global_init_var_0 = 0;
    static int static_global_init_var_1 = 1;
    
    // func header
    void func_call_test(int num);
    
    int main(void){
        // local var
        int local_uninit_var;
        int local_init_var_0 = 0;
        int local_init_var_1 = 1;
    
        // static local var
        static int static_local_uninit_var;
        static int static_local_init_var_0 = 0;
        static int static_local_init_var_1 = 1;
    
        // call func
        func_call_test(8);
    
        // export var op
        export_func_var = export_func_var * 2;
    
        return 0;
    }
    
    /* func.c:包含一个简单的被调用函数和一个全局变量 */
    int export_func_var = 666;
    
    void func_call_test(int num){
        int double_num = num * 2;
    }
    
  2. 使用gcc -v hello.c func.c编译生成可执行文件a.out,产生如下输出(简化版本)

    [delta@delta: code ]$ gcc -v func.c hello.c
    
    # 对func.c的预处理和编译过程
    /usr/lib/gcc/x86_64-linux-gnu/7/cc1 func.c -o /tmp/ccfC6J5E.s
    # 对func.c产生的.s文件汇编产生二进制文件
    as -v --64 -o /tmp/ccF4Bar0.o /tmp/ccfC6J5E.s
    
    # 对hello.c的预处理和编译过程
    /usr/lib/gcc/x86_64-linux-gnu/7/cc1 hello.c -o /tmp/ccfC6J5E.s
    # 对hello.c产生的.s文件汇编产生二进制文件
    as -v --64 -o /tmp/cc7UmhQl.o /tmp/ccfC6J5E.s
    
    # 链接过程
    /usr/lib/gcc/x86_64-linux-gnu/7/collect2 -dynamic-linker ld-linux-x86-64.so.2 Scrt1.o crti.o crtbeginS.o /tmp/ccF4Bar0.o /tmp/cc7UmhQl.o crtendS.o crtn.o
    

三、链接过程解析

Q:

3.1 目标文件

3.1.1目标文件类型

注:

  1. PE和ELF格式都是COFF(Common file format)格式的变种
  2. 目标文件与可执行文件的内容和结构类似,所以一般采用相同的格式存储。广义上来可以将目标文件和可执行文件看做是同一种类型的文件,在window下统称它们为PE-COFF文件格式,在Linux下统称它们为ELF文件。
  3. 不止是可执行文件按照可执行文件格式存储,动态链接库(DLL,Dynamic Linking Library)(Window的.dll和Linux的.so)以及静态链接库(Static Linking Library)(Window的.lib和Linux的.a)文件都按照可执行文件的格式存储。(静态链接库稍有不同,它是把很多的目标文件捆绑在一起形成一个文件,再加上一些索引。可以理解为一个包含很多目标文件的文件包)

3.1.2 ELF文件类型

ELF文件类型 说明 实例
可重定位文件(Relocatable File) 包含代码和数据,可以被用来链接成可执行文件或者共享目标文件,静态链接库可以归为这一类 Linux的.o,Window下的.obj
可执行文件(Executable File) 包含可以直接执行的程序,一般没有扩展名 Linux的/bin/bash文件,Window的.exe
共享目标文件(Shared Object File) 包含代码和数据,链接器可以上映这种文件与其他可重定位文件和共享目标文件进行链接产生新的目标文件;动态链接器可以将几个共享目标文件与可执行文件结合,作为进程映像的一部分来运行 Linux的.so,Window的.dll
核心转储文件(Core Dump File) 进程意外终止时,系统将该进程的地址空间的内容以及终止时的其它信息转储到核心转储文件 Linux下的core dump

3.1.3目标文件结构

目标文件中包含编译后的指令代码、数据,还包括了链接时需要的一些信息(符号表,调试信息和字符串等),一般目标文件将这些信息按照不同的属性,以节(Section)的形式存储(有时也称为段(Segment))。如下图所示

ELF结构.png
3.1.3.1常见的段
段名 说明
.text/.code 代码段,编译后的机器指令
.data 数据段,全局变量和局部静态变量
.bss 未初始化的全局变量和局部静态变量(.bss段只是为未初始化的全局变量和局部静态变量预留位置)
.rodata 只读信息段
.rodata1 存放只读数据,字符串常量,全局const变量。与.rodata一样
.comment 编译器版本信息
.debug 调试信息
.dynamic 动态链接信息
.hash 符号哈希表
.line 调试时的行号表,即源代码行号与编译后的指令的对应表
.note 额外的编译器信息。程序的公司名,发布版本号
.strtab String Table,字符串表,用来存储ELF文件中用到的各种字符串
.symtab Symbol Table,符号表
.shstrtab Section String Table,段名表
.plt/.got 动态链接的跳转表和全局入口表
.init/.fini 程序初始化与终结代码段
3.1.3.2目标文件结构分析
  1. ELF文件头:

    • 使用gcc -c hello.c -o hello.o生成目标文件hello.o,并使用readelf -h hello.o读取目标文件的ELF文件头,可以看出ELF文件头定义了ELF魔数、文件机器字节长度、数据存储方式、版本,运行平台、ABI版本、ELF重定位类型、硬件平台、硬件平台版本、入口地址、程序入口和长度、段表的位置和长度及段的数量等,如下图所示

      ELF Header:
        Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
        Class:                             ELF64
        Data:                              2's complement, little endian
        Version:                           1 (current)
        OS/ABI:                            UNIX - System V
        ABI Version:                       0
        Type:                              REL (Relocatable file)
        Machine:                           Advanced Micro Devices X86-64
        Version:                           0x1
        Entry point address:               0x0
        Start of program headers:          0 (bytes into file)
        Start of section headers:          1328 (bytes into file)
        Flags:                             0x0
        Size of this header:               64 (bytes)
        Size of program headers:           0 (bytes)
        Number of program headers:         0
        Size of section headers:           64 (bytes)
        Number of section headers:         15
        Section header string table index: 14
      
    • ELF文件头结构体定义在/usr/include/elf.h中,目标文件hello.o的文件头中机器字节长度为ELF64,找到64位版本文件头结构体Elf64_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;
      
    • 除结构体中的e_ident对应到readelf输出的从Magic到ABI Version部分,其它都是一一对应关系

    • e_shstrndx变量表示.shstrtab在段表中的下标

  1. 段表

    • 使用gcc -c hello.c -o hello.o生成目标文件hello.o,并使用readelf -S hello.o读取目标文件的段表部分

      Section Headers:
        [Nr] Name              Type             Address           Offset
             Size              EntSize          Flags  Link  Info  Align
        [ 0]                   NULL             0000000000000000  00000000
             0000000000000000  0000000000000000           0     0     0
        [ 1] .text             PROGBITS         0000000000000000  00000040
             0000000000000035  0000000000000000  AX       0     0     1
        [ 2] .rela.text        RELA             0000000000000000  00000440
             0000000000000048  0000000000000018   I      12     1     8
        [ 3] .data             PROGBITS         0000000000000000  00000078
             000000000000000c  0000000000000000  WA       0     0     4
        [ 4] .bss              NOBITS           0000000000000000  00000084
             0000000000000014  0000000000000000  WA       0     0     4
        [ 5] .rodata           PROGBITS         0000000000000000  00000084
             000000000000000d  0000000000000000   A       0     0     1
        [ 6] .data.rel.local   PROGBITS         0000000000000000  00000098
             0000000000000008  0000000000000000  WA       0     0     8
        [ 7] .rela.data.rel.lo RELA             0000000000000000  00000488
             0000000000000018  0000000000000018   I      12     6     8
        [ 8] .comment          PROGBITS         0000000000000000  000000a0
             000000000000002c  0000000000000001  MS       0     0     1
        [ 9] .note.GNU-stack   PROGBITS         0000000000000000  000000cc
             0000000000000000  0000000000000000           0     0     1
        [10] .eh_frame         PROGBITS         0000000000000000  000000d0
             0000000000000038  0000000000000000   A       0     0     8
        [11] .rela.eh_frame    RELA             0000000000000000  000004a0
             0000000000000018  0000000000000018   I      12    10     8
        [12] .symtab           SYMTAB           0000000000000000  00000108
             0000000000000240  0000000000000018          13    16     8
        [13] .strtab           STRTAB           0000000000000000  00000348
             00000000000000f6  0000000000000000           0     0     1
        [14] .shstrtab         STRTAB           0000000000000000  000004b8
             0000000000000076  0000000000000000           0     0     1
      Key to Flags:
        W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
        L (link order), O (extra OS processing required), G (group), T (TLS),
        C (compressed), x (unknown), o (OS specific), E (exclude),
        l (large), p (processor specific)
      
    • 段表结构体定义在/usr/include/elf.h中,目标文件hello.o的文件头中机器字节长度为ELF64,找到64位版本段表结构体定义Elf64_Shdr(每个Elf64_Shdr对应一个段,Elf64_Shdr又称为段描述符<Section Descriptor>),如下所示

      typedef struct
      {
        Elf64_Word   sh_name;        /* Section name (string tbl index) */
        Elf64_Word   sh_type;        /* Section type */
        Elf64_Xword  sh_flags;       /* Section flags */
        Elf64_Addr   sh_addr;        /* Section virtual addr at execution */
        Elf64_Off        sh_offset;      /* Section file offset */
        Elf64_Xword  sh_size;        /* Section size in bytes */
        Elf64_Word   sh_link;        /* Link to another section */
        Elf64_Word   sh_info;        /* Additional section information */
        Elf64_Xword  sh_addralign;   /* Section alignment */
        Elf64_Xword  sh_entsize;     /* Entry size if section holds table */
      } Elf64_Shdr;
      
    • Elf64_Shdr部分成员解释

      变量名 说明
      sh_name 段名是一个字符串,位于一个叫.shstrtab的字符串表中,sh_name是段名字符串在.shstrtab中的偏移
      sh_addr 段虚拟地址,如果该段可以加载,sh_addr为该段被加载后在进程地址空间的虚拟地址,否则为0
      sh_offset 段偏移,如果该段存在于文件中则表示该段在文件中的偏移,否则无意义
      sh_link、sh_info 段链接信息,如果该段的类型是与链接相关的,则该字段有意义
      sh_addralign 段地址对齐,sh_addralign表示是地址对齐数量的指数,如果sh_addralign为0或者1则该段没有字节对齐要求
      sh_entsize 对于一些段包含了一些固定大小的项,比如符号表,则sh_entsize表示每个项的大小
  1. 重定位表:hello.o中包含一个.rela.text的段,类型为RELA,它是一个重定位表。链接器在处理目标文件时必须对文件中的某些部位进行重定位,这些重定位信息都记录在重定位表中。对于每个需要重定位的代码段或者数据段,都会有一个相应的重定位表。
  1. 字符串表

    • .strtab:字符串表,保存普通的字符串,比如符号的名字

    • .shstrtab:段表字符串表,保存段表中用到的字符串,比如段名

结论:ELF文件头中的e_shstrndx变量表示.shstrtab在段表中的下标,e_shoff表示段表在文件中的偏移,只有解析ELF文件头,就可以得到段表和段表字符串表的位置,从而解析整个ELF文件

3.1.4 链接的接口——符号

3.1.4.1 符号定义
  1. 定义:在链接中,目标文件之间相互拼合实际上是目标文件之间对地址的引用,即对函数和变量地址的引用。在链接中,将函数和变量统称为符号(Symbol),函数名或变量名称为符号名(Symbol Name)

  2. 每个目标文件都有一个符号表记录了目标文件中用到的所有符号(每个定义的符号都有一个符号值,对于函数和变量来说,符号值就是它们的地址),常见分类如下

    符号类型 说明
    定义在本目标文件中的全局符号 可以被其它目标文件引用的符号
    在本目标文件中引用的符号,却没有定义在本目标文件中 外部符号(External Symbol)
    段名,由编译器产生 它的值就是该段的起始地址
    局部符号 只在编译单元内部可见,链接器往往忽略它们
    行号信息 目标文件指令与代码行的对应关系,可选
3.1.4.2 符号结构分析
3.1.4.3 符号修饰与函数签名

​ 符号修饰与函数签名:在符号名前或者后面加上_修饰符号,防止与库文件和其它目标文件冲突。现在的linux下的GCC编译器中,默认情况下去掉了加上_这种方式,可以通过参数选项打开

3.1.4.4 弱符号与强符号

​ 在编程中经常遇到符号重定义的问题,例如hello.c和func.c都定义了一个_global并将它们都初始化,在编译时就会报错。对于C/C++来说,编译器默认函数和初始化的全局变量为强符号,未初始化的全局变量为弱符号。

3.2 静态链接

3.2.1 空间和地址分配

链接器在合并多个目标文件的段时,采用相似段合并的方式,并分配地址和空间(虚拟地址空间的分配)

两步链接法:

  1. 空间和地址分配:扫描所有的目标文件,获得它们的各个段的长度、属性和位置,并且将输入目标文件中的符号表中所有的符号定义和符号引用收集起来,统一放到一个全局符号表,这一步中,链接器将能够获得所有输入目标文件的段长度,并将它们合并,计算输出文件中各个合并之后的段的长度,建立映射关系。
  2. 符号解析与重定位:使用空间和地址分配中收集到的所有信息,读取输入文件中段的数据、重定位信息,并且进行符号解析与重定位、调整代码中的地址等。

当进行了空间和地址分配之后,各个段的虚拟地址也就确定了,由于各个符号在段内的位置是相对的,所以各个符号的地址也就确定了。

3.2.2 符号解析与重定位

3.2.3 静态库链接

3.2.4 BFD库简介

现代GCC(具体来讲是GNU 汇编器GAS)、链接器、调试器和GDB及binutils的其他工具都是通过BFD库来处理目标文件,而不是直接操作目标文件。

3.3 装载与动态链接

3.3.1可执行文件的装载

3.3.2 动态链接

四、参考文献

[0] 程序员的自我修养 :链接、装载与库 / 俞甲子,石凡,潘爱民著.—北京:电子工业出版社

[1] GNU ONLINE DOC - collect2 https://gcc.gnu.org/onlinedocs/gccint/Collect2.html

上一篇下一篇

猜你喜欢

热点阅读