摘 要
本篇文章聚焦于阐述 C 语言程序从源代码到可执行文件的转变过程。以 hello.c 程序为案例,深入剖析了计算机生成 hello 可执行文件所经历的预处理、编译、汇编、链接以及进程管理等全流程。论文不仅从理论层面深入探究了相关工具的原理与方法,还通过实际操作演示了工具的运用及相应结果,以此剖析计算机系统的工作原理与体系架构,致力于助力读者更为深入且透彻地理解与掌握 C 语言程序的编译和执行机制。
**关键词:**生命周期;计算机系统;体系结构
目 录
[第1章 概述................................................................................... - 4 -](#第1章 概述................................................................................... - 4 -)
[1.1 Hello简介............................................................................ - 4 -](#1.1 Hello简介............................................................................ - 4 -)
[1.2 环境与工具........................................................................... - 5 -](#1.2 环境与工具........................................................................... - 5 -)
[1.3 中间结果............................................................................... - 5 -](#1.3 中间结果............................................................................... - 5 -)
[1.4 本章小结............................................................................... - 5 -](#1.4 本章小结............................................................................... - 5 -)
[第2章 预处理............................................................................... - 7 -](#第2章 预处理............................................................................... - 7 -)
[2.1 预处理的概念与作用........................................................... - 7 -](#2.1 预处理的概念与作用........................................................... - 7 -)
[2.2在Ubuntu下预处理的命令................................................ - 7 -](#2.2在Ubuntu下预处理的命令................................................ - 7 -)
[2.3 Hello的预处理结果解析.................................................... - 8 -](#2.3 Hello的预处理结果解析.................................................... - 8 -)
[2.4 本章小结............................................................................... - 8 -](#2.4 本章小结............................................................................... - 8 -)
[第3章 编译................................................................................... - 9 -](#第3章 编译................................................................................... - 9 -)
[3.1 编译的概念与作用............................................................... - 9 -](#3.1 编译的概念与作用............................................................... - 9 -)
[3.2 在Ubuntu下编译的命令.................................................... - 9 -](#3.2 在Ubuntu下编译的命令.................................................... - 9 -)
[3.3 Hello的编译结果解析........................................................ - 9 -](#3.3 Hello的编译结果解析........................................................ - 9 -)
[3.4 本章小结............................................................................. - 14 -](#3.4 本章小结............................................................................. - 14 -)
[第4章 汇编................................................................................. - 16 -](#第4章 汇编................................................................................. - 16 -)
[4.1 汇编的概念与作用............................................................. - 16 -](#4.1 汇编的概念与作用............................................................. - 16 -)
[4.2 在Ubuntu下汇编的命令.................................................. - 16 -](#4.2 在Ubuntu下汇编的命令.................................................. - 16 -)
[4.3 可重定位目标elf格式...................................................... - 17 -](#4.3 可重定位目标elf格式...................................................... - 17 -)
[4.4 Hello.o的结果解析........................................................... - 21 -](#4.4 Hello.o的结果解析........................................................... - 21 -)
[4.5 本章小结............................................................................. - 24 -](#4.5 本章小结............................................................................. - 24 -)
[第5章 链接................................................................................. - 25 -](#第5章 链接................................................................................. - 25 -)
[5.1 链接的概念与作用............................................................. - 25 -](#5.1 链接的概念与作用............................................................. - 25 -)
[5.2 在Ubuntu下链接的命令.................................................. - 25 -](#5.2 在Ubuntu下链接的命令.................................................. - 25 -)
[5.3 可执行目标文件hello的格式......................................... - 26 -](#5.3 可执行目标文件hello的格式......................................... - 26 -)
[5.4 hello的虚拟地址空间....................................................... - 32 -](#5.4 hello的虚拟地址空间....................................................... - 32 -)
[5.5 链接的重定位过程分析..................................................... - 34 -](#5.5 链接的重定位过程分析..................................................... - 34 -)
[5.6 hello的执行流程............................................................... - 38 -](#5.6 hello的执行流程............................................................... - 38 -)
[5.7 Hello的动态链接分析...................................................... - 39 -](#5.7 Hello的动态链接分析...................................................... - 39 -)
[5.8 本章小结............................................................................. - 40 -](#5.8 本章小结............................................................................. - 40 -)
[第6章 hello进程管理.......................................................... - 41 -](#第6章 hello进程管理.......................................................... - 41 -)
[6.1 进程的概念与作用............................................................. - 41 -](#6.1 进程的概念与作用............................................................. - 41 -)
[6.2 简述壳Shell-bash的作用与处理流程........................... - 42 -](#6.2 简述壳Shell-bash的作用与处理流程........................... - 42 -)
[6.3 Hello的fork进程创建过程............................................ - 42 -](#6.3 Hello的fork进程创建过程............................................ - 42 -)
[6.4 Hello的execve过程........................................................ - 42 -](#6.4 Hello的execve过程........................................................ - 42 -)
[6.5 Hello的进程执行.............................................................. - 43 -](#6.5 Hello的进程执行.............................................................. - 43 -)
[6.6 hello的异常与信号处理................................................... - 44 -](#6.6 hello的异常与信号处理................................................... - 44 -)
[6.7本章小结.............................................................................. - 50 -](#6.7本章小结.............................................................................. - 50 -)
[第7章 hello的存储管理...................................................... - 51 -](#第7章 hello的存储管理...................................................... - 51 -)
[7.1 hello的存储器地址空间................................................... - 51 -](#7.1 hello的存储器地址空间................................................... - 51 -)
[7.2 Intel逻辑地址到线性地址的变换-段式管理.................. - 51 -](#7.2 Intel逻辑地址到线性地址的变换-段式管理.................. - 51 -)
[7.3 Hello的线性地址到物理地址的变换-页式管理............. - 52 -](#7.3 Hello的线性地址到物理地址的变换-页式管理............. - 52 -)
[7.4 TLB与四级页表支持下的VA到PA的变换................... - 53 -](#7.4 TLB与四级页表支持下的VA到PA的变换................... - 53 -)
[7.5 三级Cache支持下的物理内存访问................................ - 55 -](#7.5 三级Cache支持下的物理内存访问................................ - 55 -)
[7.6 hello进程fork时的内存映射......................................... - 56 -](#7.6 hello进程fork时的内存映射......................................... - 56 -)
[7.7 hello进程execve时的内存映射..................................... - 57 -](#7.7 hello进程execve时的内存映射..................................... - 57 -)
[7.8 缺页故障与缺页中断处理................................................. - 58 -](#7.8 缺页故障与缺页中断处理................................................. - 58 -)
[7.9本章小结.............................................................................. - 59 -](#7.9本章小结.............................................................................. - 59 -)
[结论............................................................................................... - 60 -](#结论............................................................................................... - 60 -)
[附件............................................................................................... - 62 -](#附件............................................................................................... - 62 -)
[参考文献....................................................................................... - 63 -](#参考文献....................................................................................... - 63 -)
第1章 概述
1.1 Hello简介
1.1.1 P2P
P2P ( Program to Process **)**强调程序从静态源代码到动态运行进程的转化过程,其中包含以下中间步骤与文件形态:
Program (程序源代码)
.c文件:源代码文件,如hello.c,此时程序仅是文本形式。
预处理阶段( Preprocessing )
.i 文件:使用预处理器(如gcc -E)对hello.c进行宏展开、头文件展开后生成的文件,如hello.i。
编译阶段( Compilation )
.s 文件:编译器(如gcc -S)将.i文件转换成汇编语言形式的文件,如hello.s。
汇编阶段( Assembly )
.o 文件:汇编器(如gcc -c或as)将汇编语言代码转换成机器语言(二进制指令)的目标文件,如hello.o。
链接阶段( Linking )
可执行文件(.out或可直接执行的二进制文件,如hello):链接器(如ld或gcc hello.o -o hello)将多个目标文件与系统库链接后生成最终的可执行文件(如a.out或hello)。
Process (进程)
当用户运行程序(如执行./hello或./a.out)后,操作系统将可执行文件加载到内存中,创建一个动态运行的实例(进程),使得静态的可执行文件变为动态运行的进程。
P2P 完整阶段的具体文件序列为:
hello.c → hello.i → hello.s → hello.o → hello/a.out(可执行文件)→ 运行中的进程
1.1.2 020
"020"强调程序生命周期内从资源空闲(零状态)到占用资源(非零状态),再回到资源释放(零状态)的完整过程,结合文件状态说明如下:
第一个" 0 "(零状态)
初始时程序(源代码)处于静态磁盘存储状态,没有实际资源被占用,仅仅是文本文件(.c)。
中间的" 2 "(从零到非零,资源分配)
程序经历以下中间文件的生成:
.i文件(预处理后的中间文件,占用磁盘)
.s文件(编译后汇编文件,占用磁盘)
.o文件(汇编后目标文件,占用磁盘)
.out或可执行文件(占用磁盘,准备运行)
运行程序时,操作系统通过系统调用加载可执行文件到内存中创建进 程,分配内存、CPU、I/O资源,正式进入非零(占用资源)状态。
最后一个" 0 "(从非零回到零,资源回收)
当程序运行结束后,进程终止,操作系统释放所有被占用的资源(内存、文件资源、CPU资源等),磁盘中中间文件(如.i、.s、.o、.out)也可由用户手动删除,系统重新回到初始的零状态。
020 完整阶段序列:
初始状态(0资源) → hello.c→ hello.i (预处理)→ hello.s (编译)→ hello.o (汇编)→ hello/a.out (链接)→ 创建进程(分配内存和CPU资源,非0状态运行)→ 进程终止,资源释放(回到0状态)
1.2 环境与工具
PU i7-12700H,16G RAM,1T SSD;Win11,VMware,Ubuntu20.04
1.3 中间结果
表格 1 hello的中间结果
|------------|-----------------------------|
| hello.i | 预处理后得到的文本文件 |
| hello.s | 编译后得到的汇编语言文件 |
| hello.o | 汇编后得到的可重定位目标文件 |
| hello.asm | 反汇编hello.o得到的反汇编文件 |
| hello1.asm | 反汇编hello可执行文件得到的反汇编文件 |
| hello.elf | 用readelf读取hello.o得到的ELF格式信息 |
1.4 本章小结
本章主要介绍了程序从源代码到运行过程中的各个中间阶段及其对应的文件形态,并结合资源生命周期阐明了程序在磁盘与内存中从"零"到"非零"再回到"零"的完整过程。此外,还说明了实验所使用的软硬件环境及各阶段的具体中间产物。

图 1编译系统
第2章 预处理
2.1 预处理的概念与作用
2.1.1 预处理的概念
预处理是C语言程序编译过程中的第一阶段,由预处理器(CPP)完成。预处理主要执行文本替换任务,包括:宏定义的展开、头文件的包含(如stdio.h、stdlib.h)、条件编译以及注释的删除。预处理不会进行语法检查,其输出结果依旧是可读的C语言代码。
2.1.2 预处理的作用
预处理的作用在于提高程序的模块化和可读性,简化代码管理,提高程序移植性。经过预处理后的文件扩展名通常为".i",该文件将成为后续编译阶段的输入。
2.2在Ubuntu下预处理的命令
预处理的命令:gcc -E hello.c -o hello.i

图 2在Ubuntu下预处理的命令
- gcc:调用GNU C编译器。
- -E:指定只进行预处理,不进行编译、汇编和链接。
- hello.c:源代码文件名。
- -o hello.i:指定输出的预处理文件名为hello.i。

图 3 gcc -E hello.c -o hello.i的结果
2.3 Hello的预处理结果解析

图 4 hello.i文件内容展示
对生成的hello.i文件进行查看,我们可以发现以下内容变化:
1、原hello.c程序中的宏定义被完全展开。
2、所有的注释(如// 大作业的 hello.c 程序)被移除。
3、所有包含的头文件(如stdio.h、unistd.h、stdlib.h)被插入到源文件内,从而大幅增加了代码长度。例如,源程序中包含的#include <stdio.h>预处理后会展开成stdio.h头文件的完整内容,其中包括各种函数声明、类型定义及宏定义。
2.4 本章小结
本章主要阐述了C语言程序预处理的基本概念和作用,以及在Ubuntu系统中如何执行预处理命令。通过对hello.c的预处理实例,我们清晰地看到了宏定义展开、头文件插入和注释删除等预处理操作的具体效果,理解了预处理在整个编译流程中的重要地位,为后续的编译阶段提供了清晰的C语言源代码基础。
第3章 编译
3.1 编译的概念与作用
3.1.1 编译的概念
编译是将高级语言源代码(此处为预处理后的文件 .i)转换为汇编语言程序(扩展名 .s)的过程。这一阶段由编译器负责实现,它对代码进行词法分析、语法分析、语义检查、以及一定程度的优化,最终输出汇编语言形式的中间代码。
3.1.2 编译的作用
编译的作用主要是使高级语言程序能够被计算机进一步处理,并优化代码结构,提高程序的运行效率
3.2 在Ubuntu下编译的命令
编译的命令:gcc -S hello.i -o hello.s

图 5在Ubuntu下编译的命令
gcc:调用GNU C编译器。-S:指定只编译到汇编代码阶段。hello.i:预处理后的文件。-o hello.s:指定输出汇编语言文件名。

图 6 gcc -S hello.i -o hello.s的结果
3.3 Hello的编译结果解析
3.3.1 汇编初始部分
在main函数前有一部分字段展示了节名称:

图 7 hello.s汇编初始部分
以下是汇编初始部分的节名称及其含义:
表格 2汇编初始部分的节名称及其含义
|------------------|-----------------------|
| .file | 声明出源文件 |
| .text | 表示代码节 |
| .section和.rodata | 表示只读数据段 |
| .align | 声明对指令或者数据的存放地址进行对齐的方式 |
| .string | 声明一个字符串 |
| .globl | 声明全局变量 |
| .type | 声明一个符号的类型 |
3.3.2 数据部分
(1)字符串程序有两个字符串存放在只读数据段中,如图:

图 8两个字符串存放在只读数据段
在hello.c 中,唯一的字符串数组是在 main函数中传入的第二个参数(即 char **argv)。每个数组元素是一个指向字符串的指针。由于数组的起始地址存放在栈中,并且该地址被两次传递作为参数给 printf,可以看到如下代码将 %rax 寄存器设置为这两个字符串的起始地址:


图 9 图 10两个字符串的起始地址
(2)参数argc
参数argc 是 main 函数的第一个参数,它被存储在 %edi 寄存器中。

图 11寄存器压入栈
从第22行,可以看出寄存器%edi地址被压入栈中。
从第24行,可以看出该地址上的数值与立即数5进行比较,从而得知argc的值是否大于或者小于5;如果小于5,则被存放在寄存器并被压入栈中。
(3)局部变量的存储
程序中的局部变量只有一个,通过第32行语句(下图)可以看到局部变量 i 存储在栈的 -4(%rbp) 地址位置。

图 12局部变量的存储
3.3.3 全局函数
hello.c 中只声明了一个全局函数 int main(int argc, char* argv[]),我们通过汇编代码可以看到它是如何被定义和实现的。
在汇编代码中,相关的声明如下:

图 13全局函数
这两行代码表明 main 是一个全局函数。.global 指令声明 main 为全局符号,意味着它可以被其他模块或文件所引用。.type 指令则指定 main 的类型为函数类型,即表明它是一个函数。
在汇编中,main 函数被定义在 .text 段中,标识着程序的执行代码部分。通过这段汇编代码,我们可以看到 main 函数的入口和相关实现细节。
3.3.4 赋值操作
1 、赋值给栈中的位置:

图 14赋值给栈中的位置
在该程序中,使用了 movl 和 movq 指令进行赋值操作。特别注意,movl 用于将 32 位数据移动到栈中,而 movq 用于将 64 位数据移动到栈中。栈中数据的位置通常由基指针 %rbp 指定。
2 、寄存器之间的赋值:
程序中也有将一个寄存器的值赋给另一个寄存器的操作。例如:

图 15寄存器之间的赋值
这条指令将栈指针 %rsp 的值赋给基指针 %rbp,为接下来的栈操作提供基准。
3 、常数赋值给栈上的位置:
程序中使用 movl 将常数值赋给栈上的特定位置。例如:

图 16常数赋值给栈上的位置
这条指令将常数 0 赋值给栈上 -4(%rbp) 的位置。
4 、调用函数时传递参数:
通过将值赋给特定寄存器,程序将参数传递给外部函数。在这段代码中,传递给函数的参数值通过以下几种赋值操作来设置:


图 17图 18调用函数时传递参数
这里 movq -32(%rbp), %rax 将栈上数据赋给 %rax 寄存器,接着使用 leaq 将字符串地址赋给 %rdi 寄存器,这些寄存器的值随后会作为参数传递给函数(如 printf 和 puts)。
3.3.5 算术操作
1 、减法操作(subq ):
这条指令将常数 32 从栈指针 %rsp 中减去,意味着栈指针向下移动了 32 字节,通常是为局部变量分配栈空间。

图 19减法操作
2 、加法操作(addq ):
这条指令将常数 24 加到 %rax 寄存器的值上,更新 %rax 寄存器的内容。

图 20加法操作
3.3.6 关系操作
1 、比较操作(cmp ):

图 21比较操作
这条指令将栈上 -20(%rbp) 地址处的数据与常数 5 进行比较。cmp 指令会根据两个操作数的差值设置标志寄存器,但不会保存结果。它只是用于为条件跳转指令提供信息。
2 、条件跳转(je ):

图 22条件跳转
这条指令会在比较结果为"相等"时跳转到标签 .L2。也就是说,如果 cmpq 操作中的两个数相等,程序就会跳转到 .L2 处执行。
3.3.7 控制转移指令
利用je、jmp、jle进行跳转,控制转移指令

图 23 je控制转移指令
这个指令是 "Jump if Equal"(若相等则跳转)。它会根据上一条比较指令(cmpl)的结果来决定是否跳转到标签 .L2。

图 24 jmp控制转移指令
这个指令是无条件跳转。它会跳转到标签 .L3。

图 25 jle控制转移指令
这个指令是 "Jump if Less or Equal"(若小于或等于则跳转)。它会根据上一条比较指令(cmpl)的结果来决定是否跳转到标签 .L4。
3.3.8 函数操作
1 、main 函数
(1 )参数传递:
main 函数的参数为 int argc, char* argv[],其中 argc 表示命令行参数的数量,argv 是一个指针数组,每个元素指向一个参数的字符串。参数 argv[0] 是程序名称,argv[1] 是第一个命令行参数,依此类推。
这些参数的地址和值都在函数调用前通过栈进行传递。在汇编代码中,%rdi、%rsi、%rdx 等寄存器用于传递参数。
(2 )函数调用:
在 main 函数中,使用了 call 指令调用其他函数。主要的调用有:
- printf **:**用于打印输出,输出格式是 "Hello %s %s %s\n",对应的参数是 argv[1]、argv[2] 等。
- exit **:**程序退出,传递了一个退出状态(通常是1)。
- sleep **:**让程序暂停指定的秒数,秒数由 atoi 函数转换的结果提供。
(3 )局部变量:
main 函数使用了局部变量 i,用于循环迭代等操作。在汇编代码中,局部变量通过栈分配,通常在栈中有一个偏移量。
2 、printf 函数
(1 )参数传递:
printf 函数在汇编中调用时,参数通过寄存器传递。具体来说,格式字符串(例如 "Hello %s %s %s\n")的地址传递给 %rdi,argv[1] 和 argv[2] 等通过 %rsi 和 %rdx 等寄存器传递。
第一次调用时,格式字符串的地址传递给 %rdi,第二次传递 argv[1] 和 argv[2]。
(2 )函数调用:
在汇编中,通过 call printf@PLT 调用 printf 函数。这种调用会跳转到 printf 的地址,执行其功能。

图 26调用printf函数
3 、exit 函数
参数传递与函数调用:
exit 函数接收一个退出状态作为参数,通常将退出状态设置为1或0。汇编代码中,将退出状态(1)传递给 %rdi 寄存器,然后通过 call exit@PLT 调用 exit 函数,退出程序。

图 27调用exit函数
4 、atoi 和 sleep 函数
(1 )atoi 函数:
atoi 函数将字符串参数转换为整数。在汇编中,将 argv[3] 的地址传递给 %rdi,然后调用 atoi 函数来进行转换。
atoi 函数执行后,返回的整数存储在 %eax 寄存器中。

图 28调用atoi函数
(2 )sleep 函数:
sleep 函数接收一个整数(秒数)作为参数。在汇编中,将 atoi 函数返回的值传递给 %edi,然后调用 sleep 函数。程序会暂停指定的秒数。

图 29调用sleep函数
5 、getchar 函数
无参数传递:
getchar 函数从标准输入读取一个字符。它没有参数,因此在汇编中直接使用 call getchar@PLT 来调用该函数。

图 30调用getchar函数
总结来说,代码中的每个函数通过寄存器进行参数传递,并使用 call 指令进行调用。具体的参数通过栈和寄存器传递,汇编代码中详细描述了这些操作。
3.3.9 类型转换
1 、 atoi 函数调用(字符串转整数)
atoi(ASCII to Integer)函数将字符串表示的数字转换为整数。在代码中,我们看到 atoi 的调用位于 第 51 行:
call atoi@PLT
该函数的输入是 argv[3] 的地址,表示一个字符串(数字的字符表示)。在汇编中,argv[3] 的值通过寄存器 %rdi 传递给 atoi。atoi 函数会将该字符串解析为一个整数,并将结果存储在 %eax 寄存器中。其汇编语言如下:
movq argv[3], %rdi ; 将 argv[3](字符串)加载到 %rdi
call atoi@PLT ; 调用 atoi 将字符串转换为整数
这种类型转换是从字符串(字符数组)到整数类型(int)。
2 、将 atoi 的返回值传递给 sleep (整数转秒数)
sleep 函数用于使程序暂停一段时间,参数为秒数。在汇编中,atoi 返回的整数存储在 %eax 寄存器中。这个值通过以下步骤传递给 sleep:
movl %eax, %edi ; 将 %eax 中的值(整数秒数)传递给 %edi
call sleep@PLT ; 调用 sleep 函数,暂停指定的秒数
这里,atoi 的返回值(整数)通过寄存器 %eax 传递到 %edi,然后传递给 sleep 函数。sleep 函数会根据整数值来控制暂停的时间。
3 、其它隐性类型转换
代码中的类型转换主要集中在 atoi 和 sleep 函数调用中:
从字符串(char*)到整数(int)的转换是通过 atoi 完成的。将整数(int)传递给 sleep 函数作为秒数。
总结来说,在这个汇编代码中,类型转换 主要发生在:(1)字符串到整数(通过 atoi)。(2)整数作为秒数传递(通过 sleep)。
这些转换确保了输入数据(字符串)能够正确地被解析并用于需要整数的操作。
3.4 本章小结
在本章中,我们详细探讨了编译的概念、作用以及在Ubuntu下的编译命令。通过对hello.c程序进行编译操作,我们深入理解了从高级语言源代码到汇编语言的转换过程。编译阶段对程序进行词法分析、语法分析和优化,最终输出汇编代码作为中间文件(.s)。此外,本章还详细介绍了编译过程中涉及的各个步骤和常用命令,并结合实际案例解析了编译结果。
在Ubuntu下使用gcc -S命令对hello.i进行编译时,程序的汇编输出通过不同的汇编指令来展示程序的结构和各部分功能。通过对汇编初始部分和数据部分的分析,进一步加深了对编译后代码结构和寄存器使用的理解。汇编语言不仅仅是高级语言的转化结果,还涉及到了硬件资源的管理和指令的优化。
本章的内容为后续的汇编、链接等阶段打下了坚实的基础,通过对编译结果的逐步分析和理解,我们获得了更深刻的计算机系统内部工作机制的认知。
第4章 汇编
4.1 汇编的概念与作用
4.1.1 汇编的概念
1 、汇编语言: 汇编语言是一种低级语言,设计用于与特定计算机硬件(尤其是CPU架构)直接交互。它通过一组助记符表示机器指令,例如 MOV, ADD,SUB 等。
2 、目标文件(.o **文件):**目标文件是由汇编器(Assembler)将汇编语言源文件(.s 文件)转换而来的中间文件。目标文件包含机器语言的二进制代码,但它不能直接执行。它通常包含机器指令、符号表、重定位信息等。
4.1.2 汇编的作用
1 、机器指令的生成
汇编就是将高级语言转化为机器可直接识别执行的代码文件的过程,汇编器将.s 汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式。 .o 文件是一个二进制文件,它包含程序的指令编码。
2 、优化性能
汇编语言允许程序员在硬件层面进行精细的控制,因此它能够生成比高级语言更高效的代码。在对性能要求极高的场合,程序员会使用汇编语言进行关键部分的优化,确保程序以最低的资源消耗高效运行。
3 、硬件控制
汇编语言与计算机硬件指令集紧密相关,因此它能够直接控制CPU寄存器、内存等硬件资源。这对于开发操作系统、嵌入式系统或设备驱动等需要与硬件紧密交互的程序非常重要。
4.2 在Ubuntu下汇编的命令
在Ubuntu系统下,对hello.s进行汇编的命令为:
gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o
运行截图如下:

图 31在Ubuntu下汇编的命令
gcc:GNU 编译器集合(GNU Compiler Collection)的命令,用于编译 C 语言、C++、汇编语言等程序。
-m64:指定生成 64 位代码。它告诉编译器使用 64 位架构的指令集(如 x86-64)。该选项影响编译过程中寄存器的大小和数据模型(指针大小为 64 位)。
-no-pie:禁用位置独立执行(Position Independent Executable, PIE)。PIE 是一种可执行文件的特性,可以在内存的任意位置加载。这个选项表示生成的目标文件不支持这一特性,通常是为了生成更加传统的可执行文件。
-fno-pic:禁用位置独立代码(Position Independent Code, PIC)。在生成共享库或需要在任意内存地址执行的代码时,编译器会生成位置独立代码,允许程序在内存的任何位置加载。这个选项指示编译器不生成位置独立代码。
-c:只进行编译,不进行链接。该选项意味着 gcc 会将汇编文件 hello.s 编译成目标文件 hello.o,但不会生成最终的可执行文件(即不链接)。
-o hello.o:指定输出文件的名称。此选项表示将编译后的目标文件命名为 hello.o。如果没有这个选项,默认的输出文件名为 a.out。
hello.s:这是输入的汇编源文件,包含用汇编语言编写的程序代码。该文件的扩展名 .s 表示这是汇编语言源代码。
以下是gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o的结果:

图 32 gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o的结果
4.3 可重定位目标elf格式
4.3.1 生成ELF格式的可重定位目标文件
在shell中输入readelf -a hello.o > hello.elf 指令获得 hello.o 文件的 ELF 格式,运行截图如下:

图 33readelf -a hello.o > hello.elf后的结果
readelf 是一个用于查看 ELF 文件内容的工具,能够显示 ELF 文件的各类信息。它可以展示如 ELF 头、节头、段头、符号表等各种细节内容。
-a是 readelf 命令的一个选项,表示输出 ELF 文件的所有信息
hello.o是一个目标文件(Object File)。这个文件通常是编译阶段的产物,包含机器语言指令和其他信息,但尚未经过链接。
>是 Linux/Unix 命令中的输出重定向符号。它将命令的标准输出(stdout)保存到指定的文件中,而不是在终端显示。
4.3.2 查看ELF格式文件的内容
ELF(Executable and Linkable Format)是一种广泛使用的文件格式,用于存储可执行文件、目标文件、共享库和核心转储等。ELF格式定义了文件的结构,包括文件头、节头、段头等。
1 、ELF 头
ELF头部存储了ELF文件的基本信息,例如文件类型、系统架构、入口点、节头的位置等。
(1)Magic:7f 45 4c 46 是ELF文件的标识符。它用来确保文件是ELF格式。接下来的部分表示该文件的类型信息,如02 01 01表示该文件为64位的ELF文件,采用小端序(little-endian)。
(2)类别:ELF64,表示该文件是64位格式。
(3)数据:2 补码,小端序,表明文件使用小端字节序,符合x86-64架构的常规设置。
(4)版本:版本1表示这是当前ELF格式的标准版本。
(5)操作系统/ABI:UNIX - System V表示该文件符合UNIX系统V的ABI(应用程序二进制接口)标准。
(6)类型:REL表示这是一个可重定位文件(Relocatable),一般用于编译阶段生成目标文件。
(7)系统架构:Advanced Micro Devices X86-64,表明该文件针对x86-64架构编译。
(8)入口点地址:0x0,说明该ELF文件没有设置有效的入口点(通常用于共享库或中间文件)。程序头起点:0,没有程序头,因此该文件没有程序段(只包含节)。
(9)节头起点:1088 (bytes into file),从文件的1088字节处开始存放节头信息。
(10)节头表的数量和大小:文件中有14个节头,每个节头大小为64字节。
(11)节头字符串表索引:节头的名称表索引为13,这个表包含了文件节的名称信息。

图 34 ELF头的信息
2 、节头
节头表存储了ELF文件各个节的信息。每个节通常包含代码、数据、符号等信息。
1\] .text 类型:PROGBITS,表示包含程序代码。 权限:AX,表示此节具有执行(X)和分配(A)权限,通常存放程序的指令。 地址和偏移:文件偏移为0x40,长度为163字节。 \[2\] .rela.text 类型:RELA,这是一个重定位节,记录了需要在链接时修改的地址。 偏移:0x2f0,大小为192字节,包含了符号重定位信息。 \[3\] .data 类型:PROGBITS,存放程序的数据段。 权限:WA,表示该节可写(W)且分配(A)。 \[4\] .bss 类型:NOBITS,未初始化的全局变量和静态变量。 权限:WA,该节可写,且没有分配初始值。 \[5\] .rodata 类型:PROGBITS,存放只读数据。 权限:A,表示只读(A)且已分配。 \[6\] .comment 类型:PROGBITS,存储编译器注释信息。 权限:MS,表示可以合并(M)并且包含字符串(S)。 \[7\] .note.GNU-stack 存放栈信息,通常由GNU工具链生成,用来指示是否使用栈保护。 \[8\] .note.gnu.pr\[...
存储有关GNU工具的注释。
9\] .eh_frame
存放异常处理信息,通常用于C++程序中的异常机制。
\[10\] .rela.eh_frame
重定位信息,特别是与异常处理相关的部分。
\[11\] .symtab
符号表,包含程序中的所有符号信息。符号表中包含了函数、变量、段等符号。
\[12\] .strtab
字符串表,存储符号名称。
\[13\] .shstrtab
节头名称字符串表,存储所有节头的名称。

图 35节头表储存的信息
**3** **、重定位节**
重定位节存储了需要在链接时调整的符号和地址信息。.rela.text这个节包含了多个重定位条目,指示在链接过程中哪些地址需要被修正。例如:R_X86_64_PC32:这是一种重定位类型,表示需要修正一个32位的地址值,通常是相对地址。R_X86_64_PLT32:这表示需要修正一个指向PLT(过程链接表)中函数的指针。

图 36重定位节的信息
**4** **、符号表**
符号表(.symtab)存储了程序中所有符号(如函数、变量等)的信息。每个符号条目包含符号的值、类型、大小、绑定方式(是否为全局符号)等信息。例如:
(1)main:这是程序的入口点(main函数),类型是FUNC,并且是全局符号。
(2)puts、exit、printf、atoi、sleep、getchar:这些是外部函数,标记为UND(未定义),表示它们是从其他文件(如标准库)中引入的。

图 37符号表的信息
### 4.4 Hello.o的结果解析
### 4.4.1 命令
在shell中输入 objdump -d -r hello.o \> hello.asm 指令输出hello.o的反汇编文件,运行截图如下:

图 38 objdump -d -r hello.o \> hello.asm的结果
objdump 是一个常用的工具,用于显示目标文件(如 .o 文件)的内容,它支持反汇编、符号表查看、节头信息查看等功能。objdump 是 GNU 二进制工具集的一部分。
-d 选项表示反汇编操作,具体来说,它会将目标文件中的机器代码转换为人类可读的汇编代码。这个选项用于查看目标文件中的代码段(例如 .text 段)如何映射为汇编指令。这意味着,objdump -d 会将文件中的二进制机器指令(如 CPU 执行的指令)反汇编为相应的汇编指令(如 mov, add, jmp 等),便于理解和分析。
-r 选项表示显示重定位信息。重定位是链接过程中调整程序地址的过程,目标文件通常包含重定位信息,这些信息帮助链接器在程序的最终地址空间中修正指令和数据的地址。这个选项会输出重定位节(如 .rela.text 等)的内容,显示与文件中每个地址相关的符号和地址修正。
hello.o 是一个目标文件,通常是在编译阶段生成的二进制文件,包含了汇编语言代码转换后的机器代码。目标文件还可能包含其他节,如 .text(代码段)、.data(数据段)等。
\> 是 Linux/Unix 中的输出重定向符号。它会将命令的输出结果写入指定的文件,而不是直接在终端显示。
### 4.4.2 与hello.s的对照分析
**1** **、增加机器语言**
在hello.asm中,每一条指令增加了一个十六进制的表示,即该指令的机器语言。例如:在hello.s中,我们第21行语句如下:

图 39 hello.s中第21行语句
而在hello.asm中,增加了一个十六进制的表示,表示如下:

图 40hello.asm中第11行语句
**2** **、操作数的进制**
在1、增加机器语言的例子中,操作数也进行了改变。原在hello.s中的操作数$32,在hello.asm中,操作数变成了十六进制,变成了$0x20。可见只是进制表示改变,数值未发生改变。
**3** **、分支转移**
反汇编的跳转指令中,所有跳转的位置被表示为主函数+段内偏移量这样确定的地址,见下图,而不再是hello.s中的段名称。

图 41 hello.asm中主函数+段内偏移量表示转移地址
而在hello.s中,地址用.L2、.L3、.L4这样的段地址进行跳转,见下图:

图 42 hello.s中段地址表示转移地址
**4** **、函数调用**
反汇编文件中对函数的调用与重定位条目相对应。观察下面两个call指令调用函数,在hello.s中为

图 43 hello.s中调用函数表示
在汇编语言中,函数调用则是通过类似 call puts@PLT 的汇编指令表示。这会将函数地址加载到寄存器中并跳转执行。例如,call puts@PLT 会调用 puts 函数,call exit@PLT 会调用 exit 函数,call printf@PLT 调用 printf,call atoi@PLT 调用 atoi,等等。
而在hello.asm中,函数调用在反汇编中显示为机器指令。e8 00 00 00 00 是 call 指令(调用函数),并且后面跟随的是函数地址或相对地址。

图 44 hello.asm中调用函数表示
**5** **、符号表与重定位**
反汇编的 hello.asm 直接显示了重定位条目和符号名。例如:.rela.text 中的重定位条目(如 R_X86_64_PLT32 puts)表示需要将某些符号地址(如 puts)修正为正确的地址。

图 45 hello.asm的地址修正
hello.s在汇编源代码中,符号的名称直接作为汇编指令的操作数。例如:movq %rdi, -32(%rbp) 和 call puts@PLT 都是直接使用了符号(puts)和寄存器。
### 4.5 本章小结
本章主要介绍了汇编语言的基本概念和作用,详细阐述了汇编阶段在程序从源代码到目标文件转化过程中的关键作用。首先,本文探讨了汇编语言与机器指令的关系,强调了汇编语言如何通过助记符指令直接与计算机硬件交互,从而生成可执行的机器代码。接着,本文分析了在Ubuntu系统下进行汇编操作的过程,并详细解释了可重定位目标文件(.o 文件)的结构。
在汇编过程中,目标文件采用ELF格式进行存储,通过使用readelf工具查看了该格式的详细信息,包括ELF头、节头及符号表等。此外,我们还利用objdump命令对目标文件进行了反汇编分析,进一步理解了目标文件与汇编源文件之间的差异,尤其是在机器语言表示和符号表重定位方面。
## **第5章 链接**
### 5.1 链接的概念与作用
### 5.1.1 链接的概念
链接是编译过程的最后一步,在这个阶段,编译器生成的中间代码文件(目标文件)被组合成一个完整的可执行文件或共享库。具体来说,链接过程会做以下几件事:
**(1** **)符号解析:**链接器会根据目标文件中的符号表,解析程序中的所有函数和变量的引用,确保所有的外部函数调用、变量引用都能指向正确的内存地址。
**(2** **)地址分配:**链接器根据程序的虚拟内存布局为不同的代码和数据段分配地址,确保程序的每一部分(如代码段、数据段)都能正确地加载到内存中。
**(3** **)重定位:**如果目标文件中的代码和数据没有直接指向最终地址(因为它们是相对地址),链接器会在链接过程中修改这些地址,使其指向正确的地址。
### 5.1.2 链接的作用
**合并多个目标文件:**编译器通常将源代码文件分成多个目标文件(.o 文件),每个目标文件包含源代码的一部分。在链接过程中,链接器将这些目标文件合并,生成一个单一的可执行文件或库。
**符号解析与重定位:**程序中的某些函数和变量可能在其他文件中定义,链接器会将这些符号解析并重新定位,确保程序中每个符号都指向正确的位置。比如,函数调用或全局变量的引用会在链接阶段被替换为实际的内存地址。
**库的链接------静态链接:**在静态链接中,所有需要的库函数和对象文件在编译时就已经集成到目标文件中,生成最终的可执行文件。这个文件不依赖于外部库,因而可以独立运行。
**库的链接------动态链接:**在动态链接中,库文件(例如 .so 文件)不会在编译时被完全链接到程序中,而是等到程序运行时动态加载。这样可以节省内存,因为多个程序可以共享同一个库文件。
**优化与内存管理:**通过链接,程序可以实现更好的内存布局,减少内存占用。通过优化链接过程,链接器能够去除无用的代码或函数(例如,未被调用的函数),从而减小生成文件的大小。
### 5.2 在Ubuntu下链接的命令
在Ubuntu系统下,链接的命令为:ld -o hello -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o hello.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o
运行截图如下:

图 46链接后的结果展示
ld 是 Linux 下的链接器,用于将目标文件(.o 文件)和库文件链接成一个可执行文件或共享库。它是 GCC 编译工具链的一部分。
-o hello选项指定了输出的文件名为 hello,即链接后的可执行文件将被命名为 hello。
-dynamic-linker /lib64/ld-linux-x86-64.so.2选项指定了动态链接器(dynamic linker)的路径,动态链接器是用来加载和链接动态库(共享库)的工具。在这个例子中,/lib64/ld-linux-x86-64.so.2 是 64 位 Linux 系统上的动态链接器路径。这个动态链接器将在运行时负责加载程序依赖的共享库。
/usr/lib/x86_64-linux-gnu/crt1.o是 C 运行时(CRT,C Runtime)的启动文件,包含了程序的入口点初始化代码。它主要负责设置程序运行的环境,执行一些初始化任务(比如设置堆栈、调用程序的 main 函数)。
/usr/lib/x86_64-linux-gnu/crti.o 是 C 运行时初始化文件,它包含了程序启动之前的一些基本设置,用于保证程序在启动时能够正确运行。在这个文件中,包含了一些初始化程序所必需的内容,如对 C 运行时的初始化。
hello.o是由编译器生成的目标文件,包含了 hello.c(源代码)编译后生成的机器代码。这个文件包含了程序的主要逻辑。
/usr/lib/x86_64-linux-gnu/libc.so是 C 标准库(libc)的共享库文件。在大多数程序中,都会使用 C 标准库提供的函数(如 printf、malloc、free 等)。通过指定这个文件,程序可以在运行时动态链接到 C 标准库中的函数。
/usr/lib/x86_64-linux-gnu/crtn.o是 C 运行时的结束文件,它负责在程序运行结束时清理资源。程序在退出时会调用这个文件中的代码,来处理清理和资源释放的工作。
### 5.3 可执行目标文件hello的格式
使用readelf -a hello命令,解析hello的ELF格式,得到hello的节信息和段信息:
### 5.3.1 ELF头
在这张 readelf -a hello 的输出中,ELF 头同样以 "7f 45 4c 46" 的 Magic 字节开头,表明这是一个符合 ELF 规范的文件;接着标示为 ELF64 格式,采用小端(little endian)二补码存储,版本号为 1,遵循 UNIX -- System V 的 ABI(ABI 版本 0),机器架构为 AMD x86-64。与可重定位目标文件不同,这里 类型 为 EXEC(可执行文件),因此它具有入口点 0x4010f0,并且在文件第 64 字节处开始包含 程序头表(每项 56 字节,共 12 项);而节头表位于偏移 13560 字节处,包含 27 个节头(每个 64 字节),节头名称字符串表索引为 26。除了 Type、入口地址、程序头表及节头数量等与目标文件差异明显外,Magic、Class、Data、Version、OS/ABI、Machine 等基础信息与之前的 ELF 文件保持一致。

图 47 ELF头
### 5.3.2 节头
以下展示了可执行文件的节头表(Section Headers),每一行都列出了节的序号、名称(如 .interp、.text、.data 等)、类型(PROGBITS、NOTE、RELA、SYMTAB 等)、在内存中的虚拟地址、在文件中的偏移量、节的大小、标志位(可写 W、可分配 A、可执行 X、合并 M、字符串 S、调试信息 I 等)、以及该节所关联的符号表索引(Link)和附加信息(Info)、以及对齐要求(Align)。例如,.text 段被标记为 PROGBITS、可分配且可执行(AX),它从文件偏移 0x101f0 处开始,占 0xd8 字节,加载到虚拟地址 0x4010f0,按 16 字节对齐;.data 段存放可写数据(WA),偏移 0x3048,大小 0x4,地址 0x404048,对齐 8;.rodata 段存放只读常量(A),偏移 0x2048,大小 0xa0,地址 0x402048。像 .rela.plt 和 .rela.dyn 这样的 RELA 段则包含重定位条目,供链接器在装载时修正符号引用;.symtab 是符号表,.strtab 和 .shstrtab 分别存放符号名称和节名称字符串。链接器在生成最终可执行文件时,会根据这些节头信息将各目标文件的同名节合并、重新分配内存地址和文件偏移,并利用重定位和符号表来完成符号解析和地址修正,从而得到布局合理、能够正确运行的程序映像。


图 48节头
### 5.3.3 程序头
以下是可执行文件的程序头表(Program Headers),它决定了运行时内核和加载器如何把文件映像装入内存。第一行 PHDR 段描述了程序头表自身在文件中(Offset 0x40)和内存中(VirtAddr 0x400040)的存放位置与大小,用来让动态链接器定位后续各段。接下来 INTERP 段(Offset 0x2e0, VirtAddr 0x4002e0)指定了运行时要调用的动态链接器 /lib64/ld-linux-x86-64.so.2。随后是一系列 LOAD 段:第一个 LOAD(Offset 0x0, VirtAddr 0x400000)映射了只读执行的代码段(R),第二个 LOAD(Offset 0x5f0, VirtAddr 0x4005f0)继续代码区并带可执行权限(R E),第三个 LOAD(Offset 0x1000, VirtAddr 0x401000)映射了.hint、.plt 等只读可执行内容,第四个 LOAD(Offset 0x2e50, VirtAddr 0x403e50)则映射了可读写的数据段(RW),所有这些 LOAD 段都有 0x1000 或 0x8 的对齐。DYNAMIC 段紧接数据段,包含动态链接所需的符号表、重定位表等信息;NOTE 和 GNU_PROPERTY 段则存放 ELF 注释与属性;GNU_STACK 段用来标记栈空间的读写权限;最后的 GNU_RELRO 段将某些已重定位的数据设为只读,防止运行时被意外篡改。整个程序头表通过这些条目告诉内核如何按权限和对齐要求把文件各部分正确映射到进程的虚拟内存中。

图 49程序头
### 5.3.4 Dynamic section
在偏移 0x2e50 处的动态节包含 21 条记录,它首先通过 (NEEDED) libc.so.6 指明运行时所依赖的共享库,然后 (INIT)=0x401000 与 (FINI)=0x4011c8 分别给出程序初始化和清理例程的入口地址。紧接着,(HASH)=0x400350 与 (GNU_HASH)=0x400388 指向两种哈希表以加速符号查找,(STRTAB)=0x400480、(SYMTAB)=0x4003a8、(STRSZ)=103 及 (SYMENT)=24 则定位并描述了符号名称字符串表和符号表条目的大小。(PLTGOT)=0x404000、(PLTRELSZ)=144、(PLTREL)=RELA 与 (JMPREL)=0x400560 定义了针对过程链接表(PLT)的重定位记录,而 (RELA)=0x400530、(RELASZ)=48、(RELAENT)=24 又给出了全局数据和其他符号的重定位表信息。最后,(VERNEED)=0x400500、(VERNEEDNUM)=1 与 (VERSYM)=0x4004e8 提供了符号版本需求和版本索引表,整个动态表以 (NULL)=0x0 结尾,这些条目共同指导动态链接器如何查找库、解析符号、应用重定位并绑定正确版本,从而完成程序的动态加载。

图 50 Dynamic section
### 5.3.5 Symbol table
在这张符号表输出中,首先看到的是 动态符号表 (.dynsym),它包含 9 条全局或弱符号,仅用于运行时动态链接。第 0 项保留为空;第 1 至 4 项都是外部函数(puts@GLIBC_2.2.5、atoi@GLIBC_2.2.5、exit@GLIBC_2.2.5、sleep@GLIBC_2.2.5,以及一个版本为 GLIBC_2.34 的函数)标记为 UND,表示它们的定义来自 libc.so.6;第 5 项 __gmon_start__ 为弱符号,用于性能检测;剩下几项也是各类外部函数调用。动态链接器正是通过这些 .dynsym 条目来解析并绑定库函数。紧接其后的 完整符号表 (.symtab) 共 26 条条目,既包含本地符号也包含全局符号。前几项是编译生成时的文件级符号(如 crt1.o、hello.c、.dynamic、GLOBAL_OFFSET_TABLE_ 等);中间部分列出了运行时用到的各种地址标记(_edata、.init、.fini、__data_start、__bss_start、_IO_stdin_used、_end);还包括启动例程 _start(值 0x4010f0)、运行时重定位函数 _dl_relocate_static_pie(隐藏符号)、以及应用程序的 main 函数(值 0x401125);最后几项则是与运行时库函数相连的未定义符号(puts@GLIBC_2.2.5、getchar@GLIBC_2.2.5、printf@GLIBC_2.2.5、atoi@GLIBC_2.2.5、exit@GLIBC_2.2.5、sleep@GLIBC_2.2.5),供链接阶段或运行时解析。通过 .symtab,链接器能够完成更细粒度的符号解析、重定位和调试信息查询,而 .dynsym 则专注于运行时库函数的动态绑定。

图 51 Symbol table
### 5.4 hello的虚拟地址空间
观察程序头的LOAD可加载的程序段的地址为0x400000。如图:

图 52程序头
使用edb打开hello从Data Dump窗口观察hello加载到虚拟地址的情况,查
看各段信息。如图:



图 53从Data Dump窗口观察hello加载到虚拟地址的情况
程序从地址0x400000开始到0x401000被载入,虚拟地址从0x400000到0x400f0结束,根据5.3中的节头部表,可以通过edb找到各段的信息。
如.interp节,在hello.elf文件中能看到开始的虚拟地址:

图 54 hello.elf中.interp节的虚拟地址
在edb中找到对应的信息:

图 55 edb找到对应的.interp节的虚拟地址
如.dynstr节,在hello.elf文件中能看到开始的虚拟地址:

图 56 hello.elf中.dynstr节的虚拟地址
在edb中找到对应的信息:

图 57 edb找到对应的.dynstr节的虚拟地址
### 5.5 链接的重定位过程分析
### 5.5.1 ELF分析hello与hello.o的区别
在Shell中使用命令objdump -d -r hello \> hello1.asm生成反汇编文件hello1.asm

图 58 objdump -d -r hello \> hello1.asm后的结果
第四章中生成的hello.asm文件和hello1.asm文件,这两份汇编其实反映了程序在「未链接」和「已链接」两种不同阶段的样子,以下我们来分析不同之处:
**(** **1** **)链接后函数数量增加**
在 hello.o 中,反汇编只展示了一个符号化的 main 函数(及其局部符号 .L2/.L3 等),所有代码都紧凑地位于 .text 节中。

图 59所有代码都紧凑位于.text节中
链接生成的可执行文件里,除了 main 以外,还多出了 _start、__libc_start_main@GLIBC_2.34、_dl_relocate_static_pie、.init 中的钩子、.fini 中的清理例程,和整块用来跳转到共享库函数的 PLT 代码(puts@plt、printf@plt、getchar@plt、atoi@plt、exit@plt、sleep@plt)------函数总数大幅增加,且分布在多个段里。

图 60除了.text节还有很多其他段
**(2** **)调用指令(call** **)的参数**
在目标文件反汇编里,call 指令通常写作 call 32 \