一文读懂--程序的编译汇编和链接

发布于:2025-05-16 ⋅ 阅读:(12) ⋅ 点赞:(0)

在Linux下,我们在使用GCC来编译一个程序时,我们只需要使用最简单的命令

$gcc hello.c
$./a.out

事实上,这个过程包含了4个步骤,即预处理(Prepressing),编译(Compilation),汇编(Assembly)和链接(LinKing)

预编译

首先,源文件hello.c和相关的头文件会被预编译期cpp预编译成为一个.i文件;当然对于C++来说,源文件可能是cpp或者cxx格式的,预编译后的文件扩展名为.ii文件。

在GCC中我们可以使用如下的命令完成预编译:

$gcc -E hello.c -o hello.i

预编译的处理规则:

  • 将所有的 #define 删除掉,并且展开所有的宏定义

  • 处理所有的条件预编译指令,比如 #if #ifdef #elif #else #endif

  • 处理 #include 预编译命令,将被包含的文件插入到该预编译指令的位置,该过程是递归进行的

  • 删除所有的注释

  • 添加行号和文件名标识,便于编译器产生调试用的行号信息以及用于编译时产生编译错误或者告警时能够显示行号

  • 保留所有的#pragma 编译器指令,因为编译器需要使用他们。

经过预编译后的.i文件不包含任何宏定义,并且包含的文件也已经被插入到.i文件中。

图片

# 1 "hello.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 31 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 32 "<command-line>" 2
# 1 "hello.c"
# 1 "/usr/include/stdio.h" 1 3 4
# 27 "/usr/include/stdio.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/bits/libc-header-start.h" 1 3 4
# 33 "/usr/include/x86_64-linux-gnu/bits/libc-header-start.h" 3 4
# 1 "/usr/include/features.h" 1 3 4
...
extern int __overflow (FILE *, int);
# 873 "/usr/include/stdio.h" 3 4

# 2 "hello.c" 2


# 3 "hello.c"
int main()
{
 printf("Hello World!");
 return 0;
}

可以看到,这里<stdio.h>头文件被展开产生了很多信息。

编译

编译过程就是将预处理完的文件进行一系列的词法分析,语法分析,语义分析以及优化后生成相应的汇编文件,是程序构建中一个比较核心的部分。

上面说的过程采用的是如下命令:

$gcc -S hello.i -o hello.s

新版本的GCC把预编译和编译两个步骤合成了一个步骤,使用了一个叫cc1的程序来完成这个步骤,它的位置在 /usr/lib/gcc/i486-linux-gnu/4.1/里面

归根到底gcc命令只是这些后台程序的包装,他会根据不同的参数要求去调用预编译编译程序 cc1,汇编器 as,以及链接器 ld 我们通过命令将预处理文件转换成了汇编文件:

        .file   "hello.c"
        .text
        .section        .rodata
.LC0:
        .string "Hello World!"
        .text
        .globl  main
        .type   main, @function
main:
.LFB0:
        .cfi_startproc
        endbr64
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        leaq    .LC0(%rip), %rdi
        movl    $0, %eax
        call    printf@PLT
        movl    $0, %eax
        popq    %rbp
        .cfi_def_cfa 7, 8
        ret
        .cfi_endproc
.LFE0:
        .size   main, .-main
        .ident  "GCC: (Ubuntu 9.3.0-10ubuntu2) 9.3.0"
        .section        .note.GNU-stack,"",@progbits
        .section        .note.gnu.property,"a"
        .align 8
        .long    1f - 0f
        .long    4f - 1f
        .long    5
0:
        .string  "GNU"
1:
        .align 8
        .long    0xc0000002
        .long    3f - 2f
2:
        .long    0x3
3:
        .align 8
4:

这其中 LC0 指示了程序的全局符号表的位置,用于指定程序中全局变量和函数的地址;LFB0 用于指定程序局部符号表的位置,用于指定局部变量和函数的地址。LFE0通常被用作函数内的分支目标,也就代表着程序的出口点,当代码执行到这里就表示控制权将返回函数被调用的位置。这种标签的使用可以提到代码编译的准确性和效率。

汇编

汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。所以汇编过程只需要根据机器指令对照表一一翻译过来即可,以上的汇编过程我们可以使用汇编器 as 来完成:

$ as hello.s -o hello.o

或者

$ gcc -c hello.s -o hello.o

我们使用objdump查看目标文件内部的结构,参数 -h 是将目标文件的各个段的基本信息都打印出来

hello.o:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00000020  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000000  0000000000000000  0000000000000000  00000060  2**0
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000000  0000000000000000  0000000000000000  00000060  2**0
                  ALLOC
  3 .rodata       0000000d  0000000000000000  0000000000000000  00000060  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .comment      00000025  0000000000000000  0000000000000000  0000006d  2**0
                  CONTENTS, READONLY
  5 .note.GNU-stack 00000000  0000000000000000  0000000000000000  00000092  2**0
                  CONTENTS, READONLY
  6 .note.gnu.property 00000020  0000000000000000  0000000000000000  00000098  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  7 .eh_frame     00000038  0000000000000000  0000000000000000  000000b8  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA

可以看见除了最基本的代码段,数据段和BSS段以外,还有只读数据段(.rodata),注释信息段(.comment)和堆栈提示段(.note.GNU-stack)

链接

链接的主要内容就是把各个模块之间相互引用的部分都处理好,使得各个模块之间能够正确的衔接。链接的过程主要包括地址和空间分配,符号决议和重定位

最基本的静态链接过程就是将目标文件和库一起链接成可执行文件,最常见的库就是运行时库,它是支持程序运行的基本函数的集合。

ELF文件头

我们可以使用readelf指令查看文件头的信息:

$ readelf -h hello.o

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:          792 (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:         14
  Section header string table index: 13

elf文件头结构及相关常数被定义在"/usr/include/elf.h",这个文件头中定义了ELF魔数,文件机器字节长度,数据存储方式,版本,运行平台,硬件平台,硬件平台版本等等一系列信息。

段表

段表表示了各个段的基本属性结构。例如段名,段长度,在文件中的偏移,读写权限等。编译器,连接器和装载器都是依据段表来定位和访问各个段的属性的,段表在ELF文件中的位置由ELF文件头中的 e_shoff 来决定,也就是 “Start of section headers” 来决定的,也就是是上面表中的 792

我们同样使用readelf -S来查看elf文件的段表信息:

$ readelf -S hello.o

There are 14 section headers, starting at offset 0x318:

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
       0000000000000020  0000000000000000  AX       0     0     1
  [ 2] .rela.text        RELA             0000000000000000  00000258
       0000000000000030  0000000000000018   I      11     1     8
  [ 3] .data             PROGBITS         0000000000000000  00000060
       0000000000000000  0000000000000000  WA       0     0     1
  [ 4] .bss              NOBITS           0000000000000000  00000060
       0000000000000000  0000000000000000  WA       0     0     1
  [ 5] .rodata           PROGBITS         0000000000000000  00000060
       000000000000000d  0000000000000000   A       0     0     1
  [ 6] .comment          PROGBITS         0000000000000000  0000006d
       0000000000000025  0000000000000001  MS       0     0     1
  [ 7] .note.GNU-stack   PROGBITS         0000000000000000  00000092
       0000000000000000  0000000000000000           0     0     1
  [ 8] .note.gnu.propert NOTE             0000000000000000  00000098
       0000000000000020  0000000000000000   A       0     0     8
  [ 9] .eh_frame         PROGBITS         0000000000000000  000000b8
       0000000000000038  0000000000000000   A       0     0     8
  [10] .rela.eh_frame    RELA             0000000000000000  00000288
       0000000000000018  0000000000000018   I      11     9     8
  [11] .symtab           SYMTAB           0000000000000000  000000f0
       0000000000000138  0000000000000018          12    10     8
  [12] .strtab           STRTAB           0000000000000000  00000228
       000000000000002b  0000000000000000           0     0     1
  [13] .shstrtab         STRTAB           0000000000000000  000002a0
       0000000000000074  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)

其中第【2】个段 .rela.text 这个段被称为重定位表,他存在的意义是程序在链接的时候,由于内存地址和物理地址不同,需要将程序中的地址进行转换,以便于程序可以正常的在内存中运行。重定位表就是用来记录这些转换关系的。

其中第【11】段的"symtab"也就是我们常说的符号表,那么符号表的结构又是怎么样的呢?

我们可以使用指令 readelf -s hello.o来查看符号表

Symbol table '.symtab' contains 13 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS hello.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    7
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    8
     8: 0000000000000000     0 SECTION LOCAL  DEFAULT    9
     9: 0000000000000000     0 SECTION LOCAL  DEFAULT    6
    10: 0000000000000000    32 FUNC    GLOBAL DEFAULT    1 main
    11: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _GLOBAL_OFFSET_TABLE_
    12: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND printf

它是一个至关重要的部分,他主要管理和存储程序中的符号信息,这些符号通常包括变量名,函数名等,可以被视为地址的引用,例如函数和变量的地址;他们在编译和链接的过程中,进行定位或者重定位。符号表可以视为一个数组,数组中的每一个元素都是一个结构体。此外,符号表还能提供局部变量和全局变量以及原代码行号等信息。

日常开发过程中我们也可以通过符号表信息来判断某些功能或者接口有没有编译进版本当中。

其中第【12】和第【13】段被称为字符串表和段表字符串表。字符串表主要存储程序中若干个以 ‘\0’结尾的字符串,这些字符串通常包含符号的名字或者节的名字,通过将所有的字符串集中放到一个表中,ELF文件实现了对文件的加载,链接和调试等功能。


网站公告

今日签到

点亮在社区的每一天
去签到