当前位置: 首页 > news >正文

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

在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_startprocendbr64pushq   %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq    %rsp, %rbp.cfi_def_cfa_register 6leaq    .LC0(%rip), %rdimovl    $0, %eaxcall    printf@PLTmovl    $0, %eaxpopq    %rbp.cfi_def_cfa 7, 8ret.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-64Sections:
Idx Name          Size      VMA               LMA               File off  Algn0 .text         00000020  0000000000000000  0000000000000000  00000040  2**0CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE1 .data         00000000  0000000000000000  0000000000000000  00000060  2**0CONTENTS, ALLOC, LOAD, DATA2 .bss          00000000  0000000000000000  0000000000000000  00000060  2**0ALLOC3 .rodata       0000000d  0000000000000000  0000000000000000  00000060  2**0CONTENTS, ALLOC, LOAD, READONLY, DATA4 .comment      00000025  0000000000000000  0000000000000000  0000006d  2**0CONTENTS, READONLY5 .note.GNU-stack 00000000  0000000000000000  0000000000000000  00000092  2**0CONTENTS, READONLY6 .note.gnu.property 00000020  0000000000000000  0000000000000000  00000098  2**3CONTENTS, ALLOC, LOAD, READONLY, DATA7 .eh_frame     00000038  0000000000000000  0000000000000000  000000b8  2**3CONTENTS, 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 00Class:                             ELF64Data:                              2's complement, little endianVersion:                           1 (current)OS/ABI:                            UNIX - System VABI Version:                       0Type:                              REL (Relocatable file)Machine:                           Advanced Micro Devices X86-64Version:                           0x1Entry point address:               0x0Start of program headers:          0 (bytes into file)Start of section headers:          792 (bytes into file)Flags:                             0x0Size of this header:               64 (bytes)Size of program headers:           0 (bytes)Number of program headers:         0Size of section headers:           64 (bytes)Number of section headers:         14Section 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           OffsetSize              EntSize          Flags  Link  Info  Align[ 0]                   NULL             0000000000000000  000000000000000000000000  0000000000000000           0     0     0[ 1] .text             PROGBITS         0000000000000000  000000400000000000000020  0000000000000000  AX       0     0     1[ 2] .rela.text        RELA             0000000000000000  000002580000000000000030  0000000000000018   I      11     1     8[ 3] .data             PROGBITS         0000000000000000  000000600000000000000000  0000000000000000  WA       0     0     1[ 4] .bss              NOBITS           0000000000000000  000000600000000000000000  0000000000000000  WA       0     0     1[ 5] .rodata           PROGBITS         0000000000000000  00000060000000000000000d  0000000000000000   A       0     0     1[ 6] .comment          PROGBITS         0000000000000000  0000006d0000000000000025  0000000000000001  MS       0     0     1[ 7] .note.GNU-stack   PROGBITS         0000000000000000  000000920000000000000000  0000000000000000           0     0     1[ 8] .note.gnu.propert NOTE             0000000000000000  000000980000000000000020  0000000000000000   A       0     0     8[ 9] .eh_frame         PROGBITS         0000000000000000  000000b80000000000000038  0000000000000000   A       0     0     8[10] .rela.eh_frame    RELA             0000000000000000  000002880000000000000018  0000000000000018   I      11     9     8[11] .symtab           SYMTAB           0000000000000000  000000f00000000000000138  0000000000000018          12    10     8[12] .strtab           STRTAB           0000000000000000  00000228000000000000002b  0000000000000000           0     0     1[13] .shstrtab         STRTAB           0000000000000000  000002a00000000000000074  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 Name0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS hello.c2: 0000000000000000     0 SECTION LOCAL  DEFAULT    13: 0000000000000000     0 SECTION LOCAL  DEFAULT    34: 0000000000000000     0 SECTION LOCAL  DEFAULT    45: 0000000000000000     0 SECTION LOCAL  DEFAULT    56: 0000000000000000     0 SECTION LOCAL  DEFAULT    77: 0000000000000000     0 SECTION LOCAL  DEFAULT    88: 0000000000000000     0 SECTION LOCAL  DEFAULT    99: 0000000000000000     0 SECTION LOCAL  DEFAULT    610: 0000000000000000    32 FUNC    GLOBAL DEFAULT    1 main11: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _GLOBAL_OFFSET_TABLE_12: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND printf

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

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

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

相关文章:

  • Datawhale 5月llm-universe 第2次笔记
  • Vue 3中ref
  • css画图形
  • BUUCTF——web刷题第一页题解
  • 漂亮的收款打赏要饭网HTML页面源码
  • leetcode-hot-100 (子串)
  • Apple Vision Pro空间视频创作革命:从180度叙事到沉浸式语法的重构——《Adventure》系列幕后技术深度解析
  • [c++项目]云备份项目测试
  • 抢购Python代码示例与技术解析
  • Java中的设计模式
  • C++:字符数组与字符串指针变量的大小
  • 35页AI应用PPT《DeepSeek如何赋能职场应用》DeepSeek本地化部署与应用案例合集
  • 【论文阅读】BEVFormer
  • P8803 [蓝桥杯 2022 国 B] 费用报销
  • TypeScript中文文档
  • 【Java项目脚手架系列】第七篇:Spring Boot + Redis项目脚手架
  • 配置别名路径 @
  • ArcGIS切片方案记录bundle文件
  • 机器学习笔记3
  • 【iOS】alloc的实际流程
  • 自然资源部:不动产登记累计化解遗留问题房屋2000多万套
  • 年在沪纳税350亿人民币,这些全球头部企业表示“对上海承诺不会变”
  • 丹麦外交大臣拉斯穆森将访华
  • 4月新增社融1.16万亿,还原地方债务置换影响后信贷增速超过8%
  • 七旬男子驾“老头乐”酒驾被查,曾有两次酒驾两次肇事记录
  • 多家外资看好中国市场!野村建议“战术超配”,花旗上调恒指目标价