目 录
2.2在Ubuntu下预处理的命令......................................................................... - 5 -
3.2 在Ubuntu下编译的命令............................................................................ - 6 -
4.2 在Ubuntu下汇编的命令............................................................................ - 7 -
5.2 在Ubuntu下链接的命令............................................................................ - 8 -
5.3 可执行目标文件hello的格式.................................................................... - 8 -
6.2 简述壳Shell-bash的作用与处理流程.................................................. - 10 -
6.3 Hello的fork进程创建过程................................................................... - 10 -
6.6 hello的异常与信号处理............................................................................ - 10 -
7.1 hello的存储器地址空间............................................................................ - 11 -
7.2 Intel逻辑地址到线性地址的变换-段式管理............................................ - 11 -
7.3 Hello的线性地址到物理地址的变换-页式管理....................................... - 11 -
7.4 TLB与四级页表支持下的VA到PA的变换................................................ - 11 -
7.5 三级Cache支持下的物理内存访问.......................................................... - 11 -
7.6 hello进程fork时的内存映射............................................................... - 11 -
7.7 hello进程execve时的内存映射.......................................................... - 11 -
7.8 缺页故障与缺页中断处理............................................................................. - 11 -
8.1 Linux的IO设备管理方法.......................................................................... - 13 -
8.2 简述Unix IO接口及其函数........................................................................ - 13 -
第1章 概述
1.1 Hello简介
1.1.1 hello的P2P
P2P是From Program to Process的简写。从字面意思上理解,就是从程序(program)到进程(process)的转化过程。程序hello生命周期开始于存储在磁盘的一段源代码hello.c,hello.c由“程序员”用“高级语言”(C语言)创建,这种形式的程序易于解读但不能被系统直接执行。因此,要将hello.c转化为进程,首先要将其转化为称为“可执行目标程序”的二进制文件并存储在磁盘。现代计算机系统中,该过程分为预处理、编译、汇编和链接四个阶段,得到可执行文件hello。
之后,可以通过Shell为hello创建进程。调用fork函数为hello创建一个子进程,之后调用execve函数加载程序,覆盖刚才创建的子进程,到此完成了hello从程序到进程的转化。
Hello的P2P过程如图(1)所示。

图(1)hello的P2P图示
1.1.2 hello的O2O
020即From Zero-0 to Zero-0。完成P2P中Shell子进程创建之后,会调用execve函数加载程序。hello开始存于磁盘,不占用主存等其他存储,现代计算机系统中,借助虚拟内存机制和高速缓存,将其从磁盘逐层传输到CPU。CPU为hello分配时间片,进行取指、译码、执行等流水线操作。执行过程中,通过L1、L2、L3三级Cache高速缓存、TLB多级页表和Pagefile高效读取数据,I\O管理和信号处理实现输入输出。hello的进程短暂驻留于主存,页表,Cache,寄存器等结构中。
hello运行结束后,由父进程Bash Shell回收其僵死进程,hello的虚拟内存空间被释放,运行过程中的相关数据被删除,hello进程便经历了从无到有再到无的过程。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
1.2.1 硬件环境
机型:ASUS Vivobook Pro 15 N6506MV_N6506MV
CPU:Intel Core Ultra 9 185H 2.50GHz
内存:16GB(15.4GB可用)
1.2.2 软件环境
Windows11 64位;
VMwareWorkstationPro 16.2.4 build-20089737;
Ubuntu 22.04.1.
1.2.3 开发工具
Codeblocks 64位;vi/vim/gedit+gcc;GDB;OBJDUMP;EDB
1.3 中间结果
| 文件名称 | 文件介绍 | ||
| hello.c | 原始C语言文件 | ||
| hello.i | hello.c预处理后的文本文件 | ||
| hello.s | hello.i编译后的汇编语言文件 | ||
| hello.o | hello.s汇编后的可重定位目标文件 | ||
| hello | hello.o经过连接后的可执行目标文件 | ||
| helloo.asm | hello.o的反汇编结果 | ||
| hello.asm | hello的反汇编结果 | ||
表一:论文撰写过程中的中间结果
1.4 本章小结
本章基于hello的自白,分别简要论述了程序hello的P2P和020过程,写明本文软硬件环境以及开发调试工具,列表写明本文涉及到的中间结果的名称及意义。
第2章 预处理
2.1 预处理的概念与作用
2.1.1 预处理的概念
预处理的概念是预处理器(cpp)根据以字符#开头的命令,修改原始C程序,得到更加规整的文本文件hello.i。
预处理阶段主要的操作包括宏定义替换(#define)、文件包含(#include)、条件编译(#ifdef)等。经过预处理的一系列操作,得到新的C语言程序文本文件,以供后续处理。
2.1.2 预处理的作用
(1)宏定义替换
预处理器将宏替换成其定义的内容。原始C程序中可能通过“#define 标识符 字符串”的C语言代码进行了宏定义,预处理器根据这样的C语句,将程序中的宏替换为其定义内容。
除了原始C代码中的宏定义以外还有预先定义的宏,无需另外声明可直接使用,称为预定义宏,表二列举了一些C语言的预定义宏。
表二:C语言中一些预定义宏
| 预定义宏 | 含义 | ||
| __FILE__ | 当前源文件名 | ||
| __LINE__ | 当前源代码行号 | ||
| __DATE__ | 编译日期 | ||
| __TIME__ | 编译时间 | ||
预定义宏同样在预处理阶段完成宏定义替换。
预处理的宏替换允许程序员自定义宏或使用预定义宏。C语言中的宏定义可以减少代码中的冗,提高代码可读性,可维护性。
(2)文件包含
将原始C代码中的#include <stdio.h>替换为标准输入输出头文件的具体内容,使后续编译器无需额外依赖外部文件即可处理头文件中的函数等内容。
预处理的文件包含允许源文件包含其他文件的内容,提升代码的复用性,使得程序代码更加模块化。
(3)条件编译
在预处理阶段,通过条件编译指令决定哪个程序段需要编译,那些不需要编译。为后面的编译工作做铺垫。
条件编译提升了程序的灵活性,便于对程序进行调试等操作。
预处理的不同操作有着其各自的作用,为程序员编写程序提供相应的便利。总而言之,预处理从多个方面优化对高级语言程序的编写、阅读、调试、修改等操作,有利于基于高级语言的程序设计。
2.2在Ubuntu下预处理的命令
Ubuntu中,针对hello.c的预处理指令为
gcc -E hello.c -o hello.i
或
cpp hello.c>hello.i
生成文本文件为hello.i。
借助ls指令,图(2),图(3)分别展示了通过两种与处理指令对原始C代码进行预处理的过程。

图(2)gcc预处理命令

图(3)cpp预处理命令
2.3 Hello的预处理结果解析
执行预处理指令后,生成了文本文件hello.i。对比hello.c和hello.i,解析预处理结果,有以下结论。
1)注释全部被删除。
2)原始程序中存在
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
预处理器进行文本包含操作。预处理器在系统中找到他们各自对应的头文件,并将内容补充到文本hello.i中,为后续编译做准备。除此之外,可以看到一开始多了几行预处理行指示符,他们不是具体执行的代码。如图(4)所示。


图(4)文本包含操作
3)原始程序的主体main函数内容上并未改变,仅对空格个数之类的文本格式进行了微调,仍存在于文本文件hello.i中。如图(5)。


图(5)main函数存在于hello.i文本中
2.4 本章小结
本章论述了预处理的概念和主要操作与作用,写明在Ubuntu下预处理的两个常用命令,最后查看预处理后的hello.i文件,分析了预处理器对hello.c进行预处理结果。
第3章 编译
3.1 编译的概念与作用
3.1.1 编译的概念
编译即指编译器(ccl)将C语言编码转换为汇编语言代码的一个过程,将hello.i文件转换为hello.s文件。它包含一个汇编语言程序,该程序包含main的定义。
3.1.2 编译的作用
编译过程涉及一系列中间操作,以下为主要的操作及作用:
(1) 语法分析:检查代码的语法是否正确;
(2) 语义分析:检查代码的逻辑是否合理;
(3) 代码优化:对代码进行优化以提高运行效率改良代码结构;
(4) 生成目标代码:根据目标架构的指令集类型和特性,生成对应的汇编代码。
3.2 在Ubuntu下编译的命令
在Ubuntu中,hello.i文件进行编译的操作命令为:gcc -S hello.i -o hello.s。
图(6)展示了hello编译生成hello.s的过程。

图(6)编译命令
3.3 Hello的编译结果解析
查看编译阶段结果hello.s,如图(7)。


图(7)hello.s
下面对编译产生hello.s过程中的操作进行解析。
Part1 数据类型分析
3.3.1常量
在汇编语言中,常量通常以立即数(直接值)的形式出现。立即数是指在指令中直接写出的数值,而不是通过变量名或内存地址间接引用的。在编写汇编代码时,程序员可以将常量直接写入指令,使得汇编器在生成机器代码时将这些常量嵌入到指令本身。此外,有些情况下,常量也可以存放在数据段中,通过其内存地址进行访问。常用的汇编指令既可以使用直接出现的常量,也可以通过地址间接引用存储在数据段中的常量。







图(8)字符串常量的处理
图中分别用红色和蓝色的线条标记了hello.c中的字符串常量与对应的操作。一下将红色和蓝色线条对应的字符串常量分别记为字符串1,字符串2。
字符串存储在.sction.rodata段,此段用于存储只读数据,程序运行时无法修改这些数据。hello.s中,string语句将字符串写入相应段中。字符串1中的汉字是UTF-8编码的,语句中\347\224\250\346\263\225这样的语句段是汉字的转义序列。图中可以看出字符串2直接写入。
3.3.2参数
main函数涉及到了参数传递,即int类型的argc和char **类型的argv。hello.s中对他们的操作包括存入,访问及基于对参数的访问的其他操作和结束时的清理。
1.准备阶段开辟栈帧
hello.s中首先通过指令开辟了栈帧,用于存储函数参数和局部变量。
开辟栈帧主要包含以下操作:
(1) pushq %rbp,将调用者的%rbp保存到栈顶,以便main执行完之后恢复原有状态;

(2) movq %rsp , %rbp,用当前的栈顶指针作为新栈帧的基址;

(3) subq $32,%rbp,编译器根据后续参数以及局部变量需要的 ,减小栈顶地址,开辟大小合适的栈空间。hello.s通过开头的指令分配了4个字节给main的参数和局部变量;

2.存入操作
在完成了栈的开辟之后,依次将参数通过寄存器存入栈帧内。int类型的argc通过寄存器edi%传递并存入-20(%rbp),char**类型的argv通过寄存器%rsi传递并存入-32(%rbp)。

3.访问操作以及基于访问的其他操作
基于之前的存入操作,对参数的访问通过栈顶指针基准上加偏移量实现。在这种访问机制基础上,可以方便地对参数(或类似方法存储的局部变量)进行操作。例如将argc与5作比较时,就是访问-20(%rbp)并进行关系运算。

4.清理
最后,函数返回前清理栈帧。返回值0赋值给%eax,同时会把高位置0。随后的leave指令等价于mov %rbp, %rsp pop %rbp 恢复栈顶指针,撤销本函数的局部变量分配并恢复上一级函数的基址指针(回收开头pushq的伏笔),使得栈与调用该函数前状态一致。

3.3.3局部变量
hello的main函数涉及到一个局部变量,即int类型的i。hello.s中,对i的操作包括存入,访问,算术运算以及结束时的清理。
栈开辟后,就已经包含了i的位置,但hello.c定义int i后并未赋予其初始值,故hello.s也没有在此处进行对应的操作。

hello.s中指令将i存储在-4(%rbp)。类似于对参数的操作,访问局部变量只需以栈顶指针为基准,加上偏移量即可访问存在栈上的局部变量。给i赋值为0的操作,就是直接在栈上访问i,并将立即数0赋值到对应位置。

对局部变量的其他操作,如算术运算,基于对局部变量的访问实现。如对i进行加1操作,就直接访问栈顶指针加上偏移,对其进行加1。

最后,函数调用结束,局部变量与参数类似同样随着栈帧被清理。
3.3.4全局变量和静态变量
hello中并不涉及全局变量和静态变量,这里仅在理论上简单讨论。全局变量和静态变量在汇编语言中通常存储在数据段(Data Segment)。数据段通常分为已初始化数据段(.data)和未初始化数据段(.bss)。已初始化的全局/静态变量存储在.data。未初始化的存储在.bss。访问全局或静态变量时,汇编代码通常用绝对地址或基址加偏移的方式。全局变量和静态变量的生命周期是整个程序运行期间,它们在函数调用结束时不会被释放或清除。函数结束时,只会影响栈上的局部变量(即自动变量)。静态变量在第一次执行到声明时初始化,以后就一直存在(与全局变量类似),即使函数多次调用也不会重置。全局变量在程序启动时初始化,直到程序结束才释放。
3.3.5 表达式
hello.s对表达式i++进行了处理,经过逐步操作,得到汇编语言指令。

编译器对表达式的处理可分为以下7个阶段:
1. 词法分析
编译器首先把源代码中的表达式分解成记号(Token),比如标识符、运算符、常量等。
2. 语法分析
接着,编译器根据语言的语法规则把这些记号组合成语法树,明确表达式的结构和优先级。
3. 语义分析
编译器检查表达式中的变量和操作是否合法,比如变量是否已声明、类型是否匹配,运算是否合法等。
4. 中间代码生成
编译器把语法树转换成一种便于优化和机器码生成的中间表示,通常为三地址码、四元式等。
5. 代码优化
对中间代码进行优化,比如消除公共子表达式、常量合并、减少不必要的中间变量等,提高效率。
6. 目标代码生成
把优化后的中间代码翻译成对应平台的汇编代码或机器代码。
7. 寄存器分配与指令调度
编译器决定具体用哪些寄存器和指令来实现表达式的计算,尽量高效地利用硬件资源。
Part2 操作分析
3.3.6 赋值
C语言中的赋值运算符是“=”,把右边的值赋给左边的变量。编译器基于数据传送指令实现这种操作。
(1) 参数
为main传入参数时通过数据传送指令实现了赋值操作。具体操作为将main函数的参数通过寄存器赋值到栈帧中分配的位置,对于int类型的argc,其大小为4字节,用传送4字节的双字的movl指令进行传送,对于char**类型的argv,64位操作系统下其大小为8字节,用传送8字节的四字的movq指令进行传送。

(2) 变量
对于main函数中定义的int类型局部变量i,其在hello.c中定义为“int i;”并没为其赋初值。在hello.s中,栈帧开辟后同样为给i赋初值。在后续,hello.c中给i赋值0,hello.s中则通过操作 movl $0,-4(%rbp)完成对i的赋值操作。

3.3.7 类型转换
hello中没有通过“(type) expression”的方法进行类型转换,也没有源程序没有明写出来,由编译器完成的隐式类型转换,但是存在调用atoi函数显示地将字符串转换为整数的操作。
atoi函数的作用为跳过前导空白字符(如空格、制表符),处理可选符号,并读取连续的数字字符(0-9),直到遇到非数字字符。

3.3.8 算术操作
hello.c中的i++,经过编译器对表达式进行处理,在hello.s中通过addl指令实现,访问i在栈上的位置,加上立即数1并赋值到i在栈中的位置。

3.3.9 加载有效地址
程序打印字符串常量,需要从3.3.1中所说的段中加载字符串,涉及到加载有效地址操作指令。该操作通过leaq指令实现,leaq指令是movq指令的变形。其指令形式是从内存读取数据到寄存器,但实际上并未引用内存,而是将有效地址写入目的操作数,为后面的内存引用提供指针。后面将改地址通过%rax传给%rdi准备作为puts的参数。



3.3.10 关系操作
hello.c中包含对参数argc和常数的比较关系运算以及对int变量i和常数的比较关系运算,对于依据“argc!=5”的if条件分支,根据之前的分析,我们知道argc存储于-20(%rbp)处,i, hello.s中处理为先用指令cmpl $5,-20(%rbp)进行比较,再用指令je .L2实现根据关系运算结果跳转完成逻辑链条,若argc=5则跳转到.L2处;对于“i<10”, hello.s中的处理为先用指令cmpl $9,-4(%rbp)进行i和9的比较比较,再用指令jle .L4实现根据关系运算结果跳转完成逻辑链条,若i<=9,则跳转到.L4,对于int类型的i而言,<=9和<10是等价的。


3.3.11 数组/指针/结构操作
hello.c中,存在对数组argv的访问,这个访问过程中设计了数组和指针操作。hello.s中,访问了argv[1],argv[2],argv[3],argv[4]元素。在hello.s中,如图(9),每个红色方框的指令段对应一个数组元素的访问。以访问argv[i]为例:
(1) 将数组的起始地址赋值给%rax;
(2) 加上i*8,%rax存储了目的数据的地址后;
(3) 将%rax解引用得到 (%rax),这样就获取了argv[i]指向的字符串地址(指针操作);
(4) 将(%rax)赋值给当前的备用的寄存器。
访问多个元素则重复这一系列操作。如图(9),用这样的指令结构,访问了argv[3],argv[2]。但是注意到一个问题,访问argv[1]时,与之前将(%rax)赋值给其他寄存器不同,先将(%rax)赋值给了%rax,之后才赋值给%rsi。尝试分析这个问题,首先,直接对%rsi赋值的话,是地址->寄存器赋值,并不违反ISA约束。查阅资料,这可能是为保护寄存器内容、简化变量管理而生成的多余中转。指令集本身是允许直接movq (%rax), %rsi的。或者说,是在当前的编译指令下,编译器的临时策略。
访问三个参数之后,指令加载字符串常量,再调用了printf函数,第一轮访问结束。之后访问argv[4],只访问一个元素,%rax解引用后的值(%rax)传送到%rax中再送到%rdi中,这里也出现了类似之前%rsi遇到的情况。





图(9)数组/指针操作
3.3.12 控制转移
hello涉及到了循环结构和循环结构,条件分支。
条件分支:
图(10)展示了条件分支的指令结构。
hello的条件分支基于比较指令和跳转指令实现,如图(10)。je基于比较结果决定是否跳转完成了条件分支的逻辑。


图(10)条件分支
循环结构:
图(11)展示了循环结构的指令结构。
hello的循环结构基于比较指令、算术指令和跳转指令实现,如图(11)。每一轮循环主体指令执行完毕,进入收尾阶段时,i加1,i再与9进行比较,jle基于比较结果(i是否小于等于9)决定是否跳转回循环的指令段的开头,完成完整的循环结构和逻辑。



图(11)循环结构
3.3.13 函数操作
编译器首先识别出函数名、参数列表、返回类型以及函数体等信息并维护一个符号表,用于记录所有标识符(变量、函数名、参数名等)的类型、作用域、存储位置等信息。当遇到函数定义时,编译器会将函数名、参数类型、返回类型添加到符号表中。基于符号表,编译器会检查每个函数定义,声明,调用出参数与返回类型的一致性,并检查作用域是否唯一,有无重载。
对于函数调用行为,编译器一方面需要生成栈帧分配,参数传递方案(寄存器还是地址),局部变量管理以及函数本体代码等函数定义机器代码,另一方面,要生成参数准备,保存上下文,用call指令跳转至函数入口的函数调用代码。
hello程序写明涉及到的函数包括main printf exit sleep getchar atoi等,而写明的两处printf中有一处只打印一个字符串常量,编译器将调用printf处理为调用puts函数。这些函数一些有参数一些无参数,如果有的话调用这些函数的参数传递,空间分配等操作由编译器自动处理好。例如图(12)所示的printf调用。
|


|

图(12)printf的调用
3.4 本章小结
本章论述了编译的概念和作用,介绍了在Ubuntu下编译的命令,最后基于实际编译后的汇编代码对hello的编译结果从数据类型,操作两方面进行了详细的分析论述,包括常量,变量(全局/局部/静态),表达式,赋值,类型转换,算术操作,关系操作,数组/指针操作,控制转移,函数操作(参数传递、函数调用、局部变量、函数返回),几乎覆盖了hello编译过程中涉及的主要操作。
第4章 汇编
4.1 汇编的概念与作用
4.1.1 汇编的概念
汇编是通过汇编器(as)将包含汇编语言程序的文本文件hello.s翻译成机器语言指令,并把这些指令打包成可重定位目标程序格式,保存于目标文件hello.o(二进制文件)中。
4.1.2 汇编的作用
(1)将汇编指令转换为对应的机器码字节;
(2)生成重定位目标信息以为后续链接操作进行准备;
(3)生成的机器语言指令是最低级,可以直接被硬件电路执行的指令,效率最高。
4.2 在Ubuntu下汇编的命令
在Ubuntu中,hello.s文件进行编译的操作命令为:gcc hello.s -c -o hello.o

4.3 可重定位目标elf格式
通常来讲,可重定位文件的ELF格式可以概括为如图(14)的表,通过readelf可列出其各节的基本信息。

图(14)ELF格式概括图示
4.3.1 ELF头
执行Linux命令:readelf -h hello.o ,输出ELF头结果如图(15)。

图(15)hello.o的ELF头
ELF文件的开头是16字节的Magic数,其作用是标识文件格式,并描述文件所用系统的位数(如32位或64位)及字节序(大端或小端)。如果Magic数不符合规范,操作系统会拒绝加载该文件。
ELF头的其余部分,主要为链接器和操作系统提供目标文件的关键信息,包括:ELF头本身的大小、文件类型(如可重定位文件、可执行文件等)、目标平台的机器类型(大端还是小端)、节区头表在文件中的偏移位置,以及节区头表中每个条目的大小和总数。这些信息为后续的语法分析、链接和加载过程提供了必要的依据。
4.3.2 节头部表
执行Linux命令:readelf -S hello.o,输出节头部表如图(16)。

图(16)hello.o的节头部表
节头部表的信息包含了每个节的大小,起始位置等信息。能确定每个节在文件中的位置。hello.o的ELF文件共0~13个节。
4.3.3 重定位节
执行Linux命令:readelf -r hello.o,输出重定位节如图(17)。

图(17)hello.o的重定位节
节.rela.text是重定位节,他是一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。
一般而言,任何外部函数的调用,全局变量的引用的指令都是需要修改的,本地函数的调用不需要修改。
对当前ELF文件的重定位节进行分析,共有8条重定位信息,对应着hello.s中puts,printf,atoi,sleep,exit,getchar函数调用和对两个字符串常量的访问取值。
重定位项目可以分为两类:
第一类:R_X86_64_PC32:PC 相对寻址修正(用于数据引用)。
第二类:R_X86_64_PLT32:过程链接表(PLT)修正(用于函数调用)。
具体下来:
第一类:
00000000001c 000300000002 R_X86_64_PC32 0000000000000000 .rodata – 4
对应读取字符串 Hello 2023111438 王泽华 13212884263 3!\n
000000000062 000300000002 R_X86_64_PC32 0000000000000000 .rodata + 30
对应读取字符串Hello %s %s %s\n
第二类
000000000024 000500000004 R_X86_64_PLT32 0000000000000000 puts – 4
对应调用函数puts
00000000002e 000600000004 R_X86_64_PLT32 0000000000000000 exit – 4
对应调用函数exit
00000000006f 000700000004 R_X86_64_PLT32 0000000000000000 printf – 4
=对应调用函数printf
000000000082 000800000004 R_X86_64_PLT32 0000000000000000 atoi – 4
对应调用函数atoi
000000000089 000900000004 R_X86_64_PLT32 0000000000000000 sleep – 4
对应调用函数sleep
000000000098 000a00000004 R_X86_64_PLT32 0000000000000000 getchar – 4
对应调用函数getchar
4.3.4 符号表
执行Linux命令:readelf -s hello.o ,输出符号表如图(18)。

图(18)hello.o的符号表
节.symtab是一个符号表,它存放程序中定义和引用的函数和全局变量的信息。
4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编,结果如图(19)。


图(19)反汇编结果
4.3.4 hello.o反汇编结果与hello.s对比分析:
相同点:
指令操作流程不变,对寄存器等的调用一致,从汇编语言到机器指令并未改变程序的功能;
不同点:
(1) 指令不同。汇编语言指令中,以下数据传送之类指令都带有对操作数字长的标注,如movq,leaq,addl,机器语言指令中,这些指令全部被简化为不标注字长的指令,如mov,lea,add;
(2) 数字进制不同。汇编语言指令中立即数等以10进制表述,机器语言指令中以16制表述;
(3) 分支跳转不同。汇编语言指令中存在如.L3,.L2这样的标签标记跳转目标,机器语言指令中没有这样的标签,所有跳转基于相对偏移指明跳转的目的行;
(4) 函数调用不同。汇编语言指令中存在函数名指明调用信息,在机器语言指令中全部被替换为编码;
(5) 数据(字符串常量)引用不同。汇编语言中存在.LC0这样的数据段标签,机器语言指令中同样替换为编码;
(6) 处理程度不同。机器语言指令比汇编语言指令多了插入的重定位信息。
4.4.2 机器语言的构成
机器语言是计算机能直接识别和执行的二进制代码。
每一条机器指令通常包括如下几个部分:
操作码(Opcode):指定要执行的操作(如加法、跳转等);
操作数(Operands):指定操作涉及的数据或地址,可能是寄存器、内存地址、立即数等;
寻址方式(Addressing Mode):描述操作数如何获取(直接、间接、寄存器寻址等)。
4.4.3 与汇编语言的映射关系
汇编语言生成机器语言的过程就是汇编语言到机器语言的一个映射,这个过程中汇编指令规定的操作流程不改变,但是形式上,一方面格式上发生改变,转变为机器能执行的二进制文件,另一方面机器语言在一定程度上可以看做是对汇编语言的针对计算机的进一步简化解析。这一点体现在操作数的数字进制从简单易读的10进制转换为更适合机器的16进制,十分重要地,也体现在汇编语言中通过标签写明的一些引用数据段,跳转,函数调用等操作解析成了基于可直接硬件计算的编码(绝对地址和相对偏移)实现。
4.5 本章小结
本章论述了汇编的概念和作用,借助readelf指令查看了hello.o文件,分析ELF头、节头部表、重定位节和符号表,重点分析了重定位节的项目类型和具体到操作的对应关系,之后对比hello.o的反汇编文件和hello.s,指出他们的异同,分析了机器语言的组成以及他和汇编语言的映射关系。
第5章 链接
5.1 链接的概念与作用
5.1.1 链接的概念
汇编是通过链接器(ld)将包含汇编语言程序的文本文件hello.s翻译成机器语言指令,并把这些指令打包成可重定位目标程序格式,保存于目标文件hello.o(二进制文件)中。
5.1.2 链接的作用
把预编译好的若干目标文件合并成为一个可执行目标文件。增强程序的复用性,大幅度简化了程序员对代码的编写,同时使程序的生成变得更模块化,更加灵活,具有更强的可移植性。
5.2 在Ubuntu下链接的命令
在Ubuntu中,hello.s文件进行编译的操作命令为:
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

图(20)链接命令
5.3 可执行目标文件hello的格式
5.3.1 ELF头
执行Linux命令:readelf -h hello ,输出ELF头结果如图(21)。

图(21)hello的ELF头
hello的ELF头给出的基本信息与hello.o的ELF头给出的基本信息种类基本一致,具体内容有不同。开头Magic数描述了文件格式类型,ELF文件目标机器类型(64位或32位),字节序(大端或小端),版本号。之后,又通过文字列出了包括文件类型,机器类型,系统架构,节头部表文件偏移量,节区头表中每个条目的大小和总数。相比hello.o,入口点地址,程序头起点不再是0而是0x4010f0,程序头的大小数量也不再是0,节头数量从14增加到27(下面对应的最大的节头指标号从13变为26)。
5.3.2 节头部表
执行Linux命令:readelf -S hello ,输出节头部表结果如图(22)。






图(22)hello的头节部表
这部分给出了各个节的名称,大小,类型,地址,文本中的偏移量等信息。
5.3.3 符号表
执行Linux命令:readelf -s hello ,输出符号表结果如图(23)。






图(23)hello的符号表
在符号表中保存着定位、重定位程序中符号定义和引用的信息。与hello.o中有所不同,符号表分为了两个部分.dynsym和.symtab,即动态符号表和静态符号表。
动态符号表专为动态链接(运行时链接)而设计,只包含需要被动态链接器(如 ld.so)解析的符号(即外部可见的符号:导出的函数、变量等),不包含本地符号,体积较小,只保留必要的外部符号。所有需要支持动态链接的 ELF 文件(如共享库 .so、动态可执行文件)都必须有 .dynsym。
静态符号表主要用于调试和静态链接。包含了目标文件或可执行文件中所有符号的信息,包括本地符号、全局符号、未导出的符号等。也会包含未被导出的局部符号。体积通常较大,信息最全。不是所有 ELF 文件都包含 .symtab,如 strip 过的可执行文件可能被移除,动态库也可以没有。
5.3.4 程序头表
执行Linux命令:readelf -l hello ,输出程序头表结果如图(24)。


图(24)hello的程序头表
程序头表描述可执行文件或共享库在运行时应该如何被加载,指导操作系统从哪些文件加载哪些内容、加载到什么位置即这些内容的性质。
5.3.5 重定位节
执行Linux命令:readelf -r hello ,输出程序头表结果如图(25)。





图(25)hello的重定位节
重定位节这里分成了两部分.rela.dyn和.rela.plt,项目类型分别为R_X86_64_GLOB_DAT和R_X86_64_JUMP_SLO,分别对应引用数据段(字符串常量)和函数调用。
5.4 hello的虚拟地址空间
执行命令edb –run hello,edb打开hello,查看本进程虚拟地址空间各段信息。如图(26)。

图(25)edb加载hello

图(26)Data Dump
从Data Dump可以看出,虚拟地址从0x401000开始,之后每一个节对应一个节头表的声明,0x401000在ELF中对应.init。




图(27)节与虚拟地址对照
5.5 链接的重定位过程分析
使用指令objdump -d -r hello > hello.asm生成反汇编文件。

图(28)反汇编命令
结果如图(29)



图(29)反汇编结果
结合重定位项目,并将hello.o与hello的反汇编代码进行对比分析重定位操作:
(1) 动态链接器将共享库中hello.c所涉及到的函数添加到了可执行文件中,使得hello的反汇编文件相比hello.o多出了puts,printf,getchar,atoi,sleep,exit函数所涉及到的指令,这些体现在.plt.sec段中,如图(30)。

图(30).plt.sec的函数代码
(2) 链接器解析重定位条目,将之前在hello.o中用于指引函数调用操作的字节代码被进一步替换为目标函数的地址。如对printf的调用,如图(31)所示,call的操作地址4010a0就是printf的地址。




图(31)call操作地址实例
(3) 链接器解析重定位条目,将指引跳转指令的字节代码进一步替换为更直接的地址并标明相对main的偏移量。如图(32)。










图(32)跳转操作地址实例
5.6 hello的执行流程
用edb单步执行hello,通过plugin中的symbols查看执行流程,如图(33)所示。


图(32)执行流程
基于此结果,在gdb中在主要过程处设置断点,并运行hello,得出执行流程为:
(1)用户不添加参数或用户添加不等于4个参数:
_start—>_init—>main—>puts—>exit
(2)用户添加4个参数:
_start—>_init—>main—>
printf—>atoi—>sleep—>
printf—> atoi—>sleep—>
printf—> atoi—>sleep—>
printf—> atoi—>sleep—>
printf—> atoi—>sleep—>
printf—> atoi—>sleep—>
printf—> atoi—>sleep—>
printf—> atoi—>sleep—>
printf—> atoi—>sleep—>
printf—> atoi—>sleep—>
getchar
此时给与任意字符输入
—>exit
5.7 Hello的动态链接分析
动态链接的本质在于将程序划分为若干独立的目标模块(如共享库),并推迟符号解析与地址绑定的时机至程序加载和运行阶段,由操作系统的动态链接器(如ld-linux.so)在进程启动时负责统一管理和处理。这一机制不仅简化了库的升级和部署,也显著提升了多进程间对共享代码的复用效率。
在编译和链接阶段,编译器会为源文件中未定义的外部符号(例如共享库函数)生成必要的重定位信息。由于这些符号的实际地址只有在运行时才可确定,静态链接器不会为其分配绝对地址,而是留待动态链接器在加载可执行文件和所依赖的动态库时,依据系统实际的内存布局进行符号解析和重定位。
为了高效处理外部函数调用,现代ELF系统采用了延迟绑定(lazy binding)技术。该技术利用全局偏移表(GOT, Global Offset Table)与过程链接表(PLT, Procedure Linkage Table)协同工作。具体而言,PLT为每个外部函数分配一个跳板入口,初始状态下该跳板会间接跳转到动态链接器的解析例程,而GOT则保存着用于解析的辅助信息。当程序首次调用某个外部函数(如printf)时,执行流会经PLT跳转至GOT,触发动态链接器解析该符号并将其真实地址写回GOT。此后再调用该函数时,便能直接通过GOT定位至实际实现,无需再次解析,大大降低了运行时开销。
以hello程序为例,通过分析其ELF文件可知,.got.plt段从虚拟地址0x404000开始。对比动态链接初始化(如_dl_init)前后该段内存内容会发现,初始化完成后,GOT相关条目已经被重定位为目标函数的实际地址,标志着符号解析的完成。
此外,对于全局变量的访问,编译器通常会利用代码段和数据段之间的相对偏移关系,在编译时即可生成有效的地址计算方式,无需依赖运行时重定位。而函数调用则需依赖PLT与GOT机制,以实现动态分配与链接。整体而言,动态链接不仅有效减少了程序启动时的链接负担,还增强了库的灵活装载能力和内存空间复用率,从而提升了系统的整体运行效率与可维护性。

图(33)调用前

图(33)调用后
5.8 本章小结
本章首先论述了链接的概念和作用,对hello的ELF格式进行查看并分析其中信息,查看本进程的虚拟地址空间各段信息,并找到了其与ELF中的节的对应关系,重点分析了hello的重定位,结合hello.o的重定位项目,对照重定位前后分析hello重定位的方法,通过gdb结合edb调试的实验分析了hello的执行流程,最后分析了hello的动态链接。
第6章 hello进程管理
6.1 进程的概念与作用
6.1.1 进程的概念
进程是一个执行中的程序的实例。一方面,是计算机操作系统进行资源分配和调度的基本单位,另一方面,是基本的执行单元。
6.1.2 进程的作用
教材中描述:进程提供一个假象,好像程序独占地使用处理器/内存。
进程是的程序都有独立的虚拟地址空间和资源,不同进程间相互隔离,不会彼此干扰,为并发执行奠定了基础,每个进程具有独立PID,便于调度和控制。
6.2 简述壳Shell-bash的作用与处理流程
Shell(如bash)是命令行解释器,它的主要作用包括:
1.接收用户输入的命令;
2.解析命令,找到对应的程序;
3.创建子进程执行命令;
4.等待命令执行结束后返回结果。
处理流程简述:**
1. 用户在Shell中输入命令(如`./hello`)。
2. Shell自动对输入字符串进行切分处理并解析,获取并识别所有的参数
3. 基于解析结果进行决策:
若输入参数为内置命令,则立即执行;
若非内置命令,则Shell 调用`fork()`创建一个子进程并运行;
若输入参数非法,则返回并报告错误信息;
4. 子进程调用`execve()`加载并执行目标程序(如hello)。
5. Shell 等待子进程结束,返回控制权给用户。
6.3 Hello的fork进程创建过程
通过终端打开Shell,输入命令运行hello(argc=5),Shell会为程序hello创建一个进程。
创建过程是Shell调用fork函数创建一个子进程,他在子进程和父进程中各返回一次,子进程中返回0,父进程中返回子进程的PID。进程与父进程上下文一致(栈、通用寄存器、当前PC、环境变量、文件)。子进程与父进程PID不同,子进程结束父进程存在的情况下,父进程会回收僵死的子进程。
6.4 Hello的execve过程
上文创建完子进程后,子进程除了PID以外,与父进程相同,虽然是为hello而准备的进程,但内容上与hello无关,还不算是hello的进程。
为了加载hello的内容,子进程会调用execve函数,在当前上下文中加载并运行hello程序。execve函数调用后不返回,他的功能是删除调用进程现有的代码和地址空间并初始化,将文件,代码,数据段等映射到私有区域,再将共享库等映射到公有区域。设置当前上下文的程序计数器(PC),使之指向hello的入口_start地址,_start会调用hello的main函数,完成hello的加载。
6.5 Hello的进程执行
经过Shell调用fork创建子进程,子进程调用execve加载程序之后,hello的进程创建完毕。进程hello的逻辑控制流于Shell相互独立,在理想情况下,不被抢占,会正常执行至sleep。若被抢占,则会进入内核模式,保存当前上下文并切换,转入用户模式调度其他进程的执行。
当sleep函数被调用,hello在这个期间不需要CPU。因此,为了充分调用CPU,sleep函数自觉地向内核发送信号请求挂起hello进程, hello加入等待队列,进行上下文切换使其进入内核模式并开始计时,同时用户模式运行其他进程。计时结束,sleep函数结束返回,通过中断异常恢复hello进程,hello离开等待队列,并恢复用户模式,基于保存的上下文继续其逻辑控制流。
6.6 hello的异常与信号处理
6.6.1异常的分类
| 类型 | 原因 | 同步/异步 | 返回 | ||
| 中断 | IO设备传入信号 | 异步 | 下一条指令 | ||
| 陷阱 | 故意产生的异常,实现系统调用 | 同步 | syscall之后的指令 | ||
| 故障 | 出现错误,能够被修正 | 同步 | 当前指令 | ||
| 终止 | 不可恢复的致命错误 | 同步 | 无法返回 | ||
6.6.2信号处理
1.中断
如图(34),当指令执行过程中,中断引脚电压变高,表明出现中断异常,当前指令处理完,控制权转交给处理程序,进行中断处理,完毕后控制权交还,继续处理下一条指令。

图(34) 中断处理
2.陷阱
如图(35),当指令执行过程中,应用系统执行一次系统调用,控制权转交给处理程序,进行陷阱处理,之后处理下syscall的下一条指令。

图(35) 陷阱处理
3.故障
如图(36),当指令执行过程中,导致了一个故障,当前指令处理完,控制权转交给处理程序,进行故障处理,完毕后控制权交还,重新执行引起故障的指令。

图(36) 故障处理
4.终止
如图(37),当指令执行过程中,发生致命的硬件错误,控制权转交给处理程序,错误无法修正,终止程序。

图(37) 中断处理
6.6.2 hello运行中的异常与信号处理
1.Crtl+Z
进程会收到SIGSTP信号,将进程hello暂时挂起,终端显示程序 [1]+ 已停止,hello的作业编号存在编号,并且仍能查到hello进程的PID,说明其仍然存在,只是暂时停止了。执行fg命令可以恢复hello的运行。执行kill可以终止hello,之后fg无法恢复。

图(38) Ctrl+Z

图(38) Ctrl+Z后ps查看PID

图(38) Ctrl+Z后fg恢复

图(38) Ctrl+Z后kill进程hello
2.Ctrl+C
进程会收到SIGINT信号,终止进程hello。查不到hello的PID,jobs也不显示。hello进程已经彻底终止。

图(39) Ctrl+C
3.随机输入(乱按)
在不发生巧合的情况下,会输入到缓冲区,可能会引起中断,hello的运行好像没有发生异常一样。

图(39) 随机输入(乱按)
6.7本章小结
本章论述了进程的概念与作用,论述了Shell-bash的基本概念。论述了hello调用fork创建进程,execve加载进程的过程,进程的执行。谈论了异常的分类以及处理方式,基于实研给出了hello在argc=5情况下各种系统对各种异常和信号的处理。
第7章 hello的存储管理
7.1 hello的存储器地址空间
7.1.1 逻辑地址
逻辑地址是由程序生成的、用于访问操作数或指令的地址。每个逻辑地址由段选择子(段寄存器)和段内偏移量组成,偏移量表示从段起始位置到目标单元的距离。在hello程序中,每个函数的逻辑地址均由所属段的段基址和该函数在段内的偏移量两部分构成。
7.1.2 线性地址
线性地址是逻辑地址到物理地址转换过程中的中间结果。在x86等采用分段机制的体系结构中,程序生成的逻辑地址由段选择子和段内偏移组成,逻辑地址经段机制转换后,即将段基址与偏移量相加,得到线性地址。如果系统启用了分页机制,线性地址会作为分页单元的输入,通过页表映射最终转换为物理地址;若未采用分页机制,则线性地址直接对应物理地址。
7.1.3 虚拟地址
程序访问存储器时所使用的虚拟地址,是由程序生成的、在进程虚拟地址空间中的地址。虚拟地址通过地址转换机制(如分页机制)被映射为物理地址。虚拟地址空间的大小通常独立于实际物理内存容量。在hello程序中,所有内存访问均采用虚拟地址完成。
7.1.4 物理地址
物理地址是内存芯片级的存储单元寻址方式,与CPU的地址总线直接对应。物理地址通常由固定宽度的无符号整数(如32位或36位)表示。在x86处理器实模式下,物理地址等于段基址与段内偏移量之和,CPU可通过该地址直接访问主存。对于hello程序而言,物理地址即为其代码和数据在实际物理内存中的存放位置。
7.2 Intel逻辑地址到线性地址的变换-段式管理
在Intel x86架构下,内存寻址采用了“段式管理”机制,将内存空间划分为若干逻辑段,每次内存访问都需要经过“逻辑地址到线性地址”的转换过程。逻辑地址由“段选择子(Segment Selector)”和“段内偏移(Offset)”两部分组成。
具体流程如下:首先,CPU通过段寄存器(如CS、DS、SS等)获取段选择子,进而索引到全局描述符表(GDT)或局部描述符表(LDT)中的对应段描述符。段描述符记录了该段的基址(Base Address)、界限和访问权限等信息。随后,CPU将段基址与段内偏移相加,得到一个唯一的线性地址(Linear Address)。这个线性地址接下来会经过分页机制,最终转换为物理地址。
段式管理允许系统为不同类型的数据或代码分配不同的段,实现访问隔离和内存保护。在现代x86-64操作系统中,虽然分段功能被极大简化,分页成为内存管理的主流,但段式管理作为地址变换的第一步依然存在,为系统的安全性和兼容性提供了基础支撑。
7.3 Hello的线性地址到物理地址的变换-页式管理
在现代操作系统中,线性地址(又称虚拟地址)到物理地址的映射主要通过页式管理机制实现。以x86-64为例,CPU生成的线性地址被划分成若干页(通常4KB),每个进程拥有独立的虚拟地址空间。操作系统为每个进程维护一份页表,记录虚拟页与物理页帧的映射关系。
当Hello程序访问某个虚拟地址时,CPU首先查找其页表,确定该虚拟页对应的物理页帧号,然后用物理页帧号加上页内偏移量得到最终物理地址。页式管理不仅简化了内存分配,还为进程隔离、虚拟内存与内存保护提供了基础。
7.4 TLB与四级页表支持下的VA到PA的变换
在x86-64架构下,虚拟地址到物理地址的转换涉及四级页表:PML4、PDPT、PD、PT。每一级页表使用虚拟地址的一部分作为索引逐级查找,最终获得物理页帧号。
为加速地址转换,CPU内置TLB(Translation Lookaside Buffer),作为虚拟地址到物理地址映射的高速缓存。当虚拟地址命中TLB时,可直接得到物理地址;若未命中,则需访问四级页表,进行多次内存读取完成转换,结果再写回TLB以备后用。

图()4级页表
7.5 三级Cache支持下的物理内存访问
计算机存储采用多级缓存结构,类似地,现代CPU采用多级Cache结构(L1,L2,L3),以缩短CPU访问主存的延迟。每次物理地址发出后,CPU依次在L1、L2、L3 Cache中查找数据。若数据命中Cache,CPU可直接读取,否则需访问更慢的主存。
以三级Cache为例,L1 Cache容量最小、速度最快,L3容量最大、速度较慢。三级Cache协同工作,大幅提升了内存访问效率,减轻了主存带宽压力

图()计算机存储结构
7.6 hello进程fork时的内存映射
当hello进程执行fork()时,操作系统通过“写时复制(Copy-On-Write, COW)”机制,为子进程创建父进程虚拟地址空间的副本。初始时父子进程共享物理内存,且页表项被标记为只读。只有当父或子进程尝试写入某页时,操作系统才会为该页分配新的物理页帧,实现真正的数据分离。这种策略兼顾了效率与内存利用率。

图()写时复制
7.7 hello进程execve时的内存映射
当hello进程调用execve()加载新程序时,原有用户空间的虚拟内存映射会被彻底清空。操作系统重新分配代码段、数据段、堆、栈等必要内存区域,并以新程序的可执行文件内容填充相应物理页。此时,原先的页表和物理内存(若无其它进程共享)会被回收,进程映像实现彻底替换。主要步骤为:
1. 删除已存在的用户区域;
2. 为所加载的新程序的代码、数据创建相应的结构,映射到用户虚拟地址空间的私有区域;
3. 将hello与共享对象(libc.so,包含数据和代码)链接(动态链接),映射到用户虚拟地址空间的共享区域;
4. 设置当前上下文的程序计数器(PC),使之指向被当前调用程序的起点。

图()内存映射
7.8 缺页故障与缺页中断处理
当进程访问一个未映射到物理内存的虚拟地址时,CPU会触发缺页异常(Page Fault)。操作系统捕获该异常后,判断该虚拟地址是否合法(如属于已分配的栈、堆等),若合法则为其分配物理页帧并建立映射,恢复进程执行。若非法,则通常导致进程被终止(如Segmentation Fault)。缺页异常是实现虚拟内存、按需分配和文件映射的关键机制。
7.10本章小结
本章论述了hello 的存储器地址空间、intel 的逻辑地址到线性地址的变换-段式管理、hello 的线性地址到物理地址的变换-页式管理, VA 到PA 的变换、物理内存访问,hello进程fork、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
(以下格式自行编排,编辑时删除)
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
(以下格式自行编排,编辑时删除)
8.3 printf的实现分析
(以下格式自行编排,编辑时删除)
[转]printf 函数实现的深入剖析 - Pianistx - 博客园
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
(以下格式自行编排,编辑时删除)
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
(以下格式自行编排,编辑时删除)
(第8章 选做 0分)
结论
到了这里,我们已经可以给hello的生命周期一个比较完整的总结了。
一开始,hello以高级语言文本hello.c形式存储于磁盘中。
要执行hello,首先要将hello转化为进程,转化为进程要先转化为机器语言。这边是P2P(Program to Process)的过程。
1. hello.c由cpp预处理,进行宏替换,文本包括等操作得到hello.i文本;
2. hello.i由ccl编译,得到汇编语言文件hello.s;
3. hello.s经过as汇编,得到二进制可重定位目标文件hello.o;
4. hello.s经过ld链接,得到可执行文件hello,存于磁盘中;
5. 用户给Shell运行hello的命令,Shell解析命令,获取参数,调用fork为hello创建子进程,子进程调用execve加载hello并为其建立数据结构等进程执行所必要的因素;
6. hello在变化,执行的过程中,涉及到计算机系统中定义的各种地址,地址的根本是物理地址;
7. hello进程运行中可以接收各种信号,系统会做出相应的反应;
8. hello运行结束,Shell将进程回收,内核回收为期创建的结构和信息。
综上所述,如此简单的hello的短暂的生命周期中经历了如此多的时间,变化,有如此多值得研究的地方。撰写本论文的过程到了由浅入深,由粗到细的对CSAPP的复习过程,也是一次充满实验探索的研究过程,不禁感叹这样一个小小的程序竟让我花费了这么大力气。这种感受本质上来源于现代计算机系统的复杂,精细,可以说是很大程度上的现代科技集大成者,包含的知识既有深度又有广度。
撰写本文过程中,也感受到一些成就感。在第5章调试过程中,处于严谨需要自主设计实验验证过程的调用顺序。“君子善假于物”,这个过程我也深刻体会了如何将各种各样的调试工具结合起来,形成一套对自己而言最熟练,障碍最小的实验操作方案。
虽然马上就要期末考试了,即便是学完了CSAPP,对计算机系统的很多组成部分都只是概念上的认识了解,实验上除了简易Shell的设计,也基本限于应用,验证的层面。在我正在操作的这台计算机面前,明显比较菜的是我不是它。
附件
列出所有的中间产物的文件名,并予以说明起作用。
| 文件名称 | 文件介绍 | ||
| hello.c | 原始C语言文件 | ||
| hello.i | hello.c预处理后的文本文件 | ||
| hello.s | hello.i编译后的汇编语言文件 | ||
| hello.o | hello.s汇编后的可重定位目标文件 | ||
| hello | hello.o经过连接后的可执行目标文件 | ||
| helloo.asm | hello.o的反汇编结果 | ||
| hello.asm | hello的反汇编结果 | ||
参考文献
[1] 大卫R.奥哈拉伦,兰德尔E.布莱恩特. 深入理解计算机系统[M]. 机械工业出版社,2016.
[2] https://github.com
[3] https://www.deepseek.com/
[4] https://blog.csdn.net/ling12abc/article/details/102993550
[5] 逻辑地址、物理地址、虚拟地址_虚拟地址 逻辑地址-CSDN博客
[6] https://blog.csdn.net/dianqicyuyan/article/details/121006913
转载自CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2401_84354932/article/details/148030929



