摘 要
本报告以老师提供的非常具有代表性的hello.c程序为例,分析了一个C语言程序从源代码到进程执行的完整生命周期。通过深入分析程序的预处理、编译、汇编、链接、进程管理和存储管理等关键环节,探究了计算机系统底层的工作机制。撰写本报告的过程中采用Ubuntu 20.04作为实验环境,结合GCC工具链和多种调试分析工具,详细记录了程序在各阶段的转换过程与中间结果。特别探讨了ELF文件格式、虚拟地址空间管理、动态链接机制、进程调度策略以及内存管理技术等核心概念。实践探究结果印证了现代计算机系统通过精妙的分层抽象和高效的资源管理机制,来实现程序的可靠执行和系统资源的高效利用,不仅加深了对计算机系统工作原理的理解,也为后续的系统级编程和性能优化提供了理论基础和实践指导。
关键词:计算机系统;程序生命周期;ELF格式;进程管理;存储管理;动态链接
第1章 概述
1.1 Hello简介
Hello程序是一个简单的C语言程序,它展示了程序从源代码到进程执行的完整生命周期,即”From Program to Process”(P2P)的过程,以及从零开始到最终结束的”From Zero-0 to Zero-0″(020)过程。
P2P过程分析:
Program阶段:
程序员创建hello.c源文件,包含main函数和必要的头文件。
预处理阶段:处理#include和#define等预处理指令,生成hello.i文件。
编译阶段:将预处理后的C代码转换为汇编代码(hello.s)。
汇编阶段:将汇编代码转换为机器码,生成可重定位目标文件(hello.o)。
链接阶段:将目标文件与系统库链接,生成可执行文件(hello)。
Process阶段:
Shell通过fork()创建新进程。
execve()加载并执行hello程序。
CPU执行程序指令,输出问候信息。
程序终止,进程被操作系统回收。
020过程分析:
From Zero:程序从无到有,经过编译链接过程生成可执行文件。
执行阶段:程序运行时创建进程,在屏幕上输出指定格式的信息。
To Zero:程序执行完毕,进程终止,所有资源被系统回收,回归初始状态。
1.2 环境与工具
硬件环境:
处理器:AMD Ryzen 7 7745HX with Radeon Graphics-3.60 GHz
内存:16.0GB(机器的全部内存);4GB RAM(Ubuntu虚拟机可用的内存空间)
存储:2TB(机器的全部硬盘空间)20GB SSD(分配给Ubuntu虚拟机的硬盘空间)
软件环境:
操作系统:Ubuntu 20.04.6 LTS
内核版本:Linux 5.15.0-134-generic
开发与调试工具:
GCC工具链:
gcc 11.3.0:用于编译和链接程序
编译命令:gcc -m64 -Og -no-pie -fno-stack-protector -fno-PIC hello.c -o hello
调试工具:
GDB 12.1:用于程序调试
EDB调试器:用于分析程序执行过程
分析工具:
objdump:用于反汇编和目标文件分析
readelf:用于分析ELF格式文件
strace:用于跟踪系统调用
1.3 中间结果
在Hello程序的生成和执行过程中,会产生以下中间文件:
hello.i:预处理后的源代码文件
作用:展示宏展开和头文件包含后的完整代码
生成命令:gcc -E hello.c -o hello.i
hello.s:汇编代码文件
作用:展示C代码转换后的汇编语言表示
生成命令:gcc -S hello.i -o hello.s
hello.o:可重定位目标文件
作用:包含机器码但未完成最终地址重定位
生成命令:gcc -c hello.s -o hello.o
hello:最终可执行文件
作用:可直接在Linux系统上运行的程序
生成命令:gcc hello.o -o hello
1.4 本章小结
本章概述了Hello程序的生命周期(P2P和020过程),介绍了完成本大作业所使用的软硬件环境和工具链,并列举了在程序构建过程中生成的中间文件及其作用。通过本章的介绍,我们对Hello程序有了整体认识,为后续章节深入分析程序的预处理、编译、汇编、链接、进程管理和存储管理等细节奠定了基础。接下来的章节将逐步深入分析Hello程序在计算机系统中的完整执行过程。
第2章 预处理
2.1 预处理的概念与作用
预处理(Preprocessing)是C程序编译的第一个阶段,由预处理器(cpp)负责处理源代码中的预处理指令(以#开头的指令)。其主要作用包括:
宏展开:替换#define定义的宏。
头文件包含:将#include指定的头文件内容插入到源文件中。
条件编译:根据#if、#ifdef、#ifndef等条件编译指令选择性地包含或排除代码。
删除注释:移除所有注释(/* … */ 和 // …)。
添加行号和文件名标记:便于后续编译阶段的错误定位。
预处理后的文件(.i后缀的文件)仍然是纯文本文件,但已经去除了预处理指令,并完成了宏替换和头文件展开。
2.2在Ubuntu下预处理的命令
在Ubuntu中,可以使用GCC的-E选项进行预处理:
gcc -E hello.c -o hello.i
其中,-E表示仅执行预处理,不进行编译、汇编和链接;-o hello.i表示指定输出文件名为hello.i。
在Ubuntu终端执行此命令的结果如下图所示:

图2.1:Ubuntu下执行预处理命令前后截图
可以看到,在没有执行命令行时,该目录下只有hello.c一个文件;执行过预处理命令后,便多了生成的预处理文件:hello.i。同时也可以看到在当前目录的文件夹里出现了所预期的hello.i文件。
2.3 Hello的预处理结果解析
打开生成的hello.i文件,可见原本的c程序被预处理成为了3060行代码:

图2.2:查看hello.i的结果
可以观察到,c程序里的代码部分在预处理后并没有变化,只占了3060行预处理代码中的14行。前面的大量代码,都源于头文件<stdio.h> <unistd.h> <stdlib.h> 的依次展开。
以 stdio.h 的展开为例:预处理过程中,#include指令的作用是把指定的头文件的内容包含到源文件中。stdio.h是标准输入输出库的头文件,它包含了用于读写文件、标准输入输出的函数原型和宏定义等内容。
当预处理器遇到#include<stdio.h>时,它会在系统的头文件路径下查找stdio.h文件,一般在/usr/include目录下,然后把stdio.h文件中的内容复制到源文件中。stdio.h文件中可能还有其他的#include指令,比如#include<stddef.h>或#include<features.h>等,这些头文件也会被递归地展开到源文件中。
预处理器不会对头文件中的内容做任何计算或转换,只是简单地复制和替换。
在阅读预处理得来的hello.i的内容时候,也可以发现并印证一些预处理过程中会对原始代码做出的改变。下面以截图举例说明:
首先就是头文件的展开:

图2.3:头文件展开截图示例
可以看到,一些库函数里的功能函数,比如fflush和相关的FILE操作函数,都被展开成了如上图所示的以extern为开头的声明格式。
然后就是注释删除:

图2.4:注释删除截图示例
可以看到,预处理文件在头文件展开代码结束后紧跟着hello.c中的代码部分,原有的一些注释被全部删除了,这符合我们对预处理过程的认知。
2.4 本章小结
本章讲述了在Linux环境中,如何用命令对c程序进行预处理,以及预处理的含义和作用。然后用一个简单的hello程序演示了从hello.c到hello.i的过程,并对所生成的hello.i文件进行了查看和分析。通过对比观察,可以分析得到预处理过程会将引用的头文件全部展开为所引用的库文件内容,并且预处理文件会包含一些宏和常量的定义,还有一些行号信息和条件编译指令,同时也会把c程序中的注释部分全部删除。
第3章 编译
3.1 编译的概念与作用
编译(Compilation)是将预处理后的C代码(.i文件)转换为汇编代码(.s文件)的过程。编译器(如GCC)在此阶段执行以下关键操作:
词法分析:将源代码分解为词法单元(tokens)。
语法分析:构建抽象语法树(AST),检查语法正确性。
语义分析:验证类型、作用域等语义规则。
中间代码生成:生成与机器无关的中间表示(如RTL)。
代码优化:对中间代码进行优化(如常量传播、死代码消除)。
目标代码生成:将优化后的中间代码转换为目标机器的汇编指令。
3.2 在Ubuntu下编译的命令
在Ubuntu中使用GCC生成汇编代码的命令如下:
gcc –S hello.i –o hello.s
在Ubuntu终端执行此命令的结果如下图所示:

图3.1:Ubuntu下执行编译命令前后截图
可以看到,在没有执行命令行时,该目录下只有hello.c和hello.i两个文件;执行过编译命令后,便多了生成的汇编文件:hello.s。同时也可以看到在当前目录的文件夹里出现了所预期的hello.s文件。
3.3 Hello的编译结果解析
3.3.1 程序元信息和字符串常量

图3.2:汇编代码中的程序元信息和字符串常量部分
首先开头标识了源文件为hello.c,之后定义了两个字符串常量,存储在.rodata只读数据段:.LC0是参数错误提示信息,.LC1是printf的格式字符串。.LC0应为”用法: Hello 学号 姓名 手机号 秒数!”,此处是汉字,呈现为UTF-8编码的中文字符串的八进制转义序列表示,因为在汇编文件(.s)中,GCC编译器会将非ASCII字符(如中文)转换为八进制转义序列(后跟3位八进制数字),以确保汇编器能正确解析。
3.3.2 函数框架

图3.3:汇编代码中的函数框架部分
此部分设置了函数栈帧,分配32字节栈空间;endbr64是安全特性指令,防止控制流劫持。
3.3.3 参数检查

图3.4:汇编代码中的参数检查部分
此部分使用cmpl指令比较参数数量,并通过PLT(过程链接表)调用库函数。
3.3.4 主循环结构
此部分使用jmp和条件跳转实现for循环,通过指针算术运算访问argv数组元素,并在每次循环调用printf和sleep函数。对于具体代码的分析见下图:

图3.5:汇编代码中的主循环结构部分
3.3.5 函数返回

图3.6:汇编代码中的函数返回部分
此部分调用getchar等待输入,设置返回值0,然后恢复栈帧并返回。
以上我们是从汇编代码的整体结构入手,分析每一段汇编代码对应什么作用,下面我们将进一步把汇编代码按着数据处理,函数调用,赋值、算术、关系等运算以及控制跳转和类型转换等具体操作进行分析。
3.3.6 数据与类型处理
(1)常量处理

图3.7:汇编代码中的常量处理部分
字符串常量使用.string指令定义,存储在只读数据段(.rodata),编译器将中文字符转换为UTF-8编码的八进制转义序列。
(2)变量处理

图3.8:汇编代码中的变量处理部分
这两行汇编代码分别将argc和argv存储到栈。
(3)类型转换

图3.9:汇编代码中的类型转换部分
atoi实现显式的字符串到整数转换,movl指令处理32位到64位的隐式扩展。
3.3.7 表达式与操作符
(1)算数运算

图3.10:汇编代码中的算数运算(其一)
此条语句是通过addq实现指针运算;

图3.11:汇编代码中的算数运算(其二)
而此条语句是使用addl实现整数自增,他们都是算术运算的体现。
(2)关系运算

图3.12:汇编代码中的关系运算
这两句是将argc与5比较,若不相等,则跳转到.L2。
(3)位运算

图3.13:汇编代码中的位运算
虽然没有显式位操作,但寄存器清零隐含位运算。
3.3.8 控制流实现
(1)if条件判断

图3.14:汇编代码中的if条件判断
此语句块通过cmpl+je实现if条件判断,并使用标签(如.L2)实现基本块划分。
(2)for循环

图3.15:汇编代码中的for循环
3.3.9 函数操作
(1)参数传递

图3.16:汇编代码中的参数传递
这段遵循System V AMD64 ABI调用约定,其中前6个参数通过寄存器传递(rdi, rsi, rdx, rcx, r8, r9)。
(2)函数调用

图3.17:汇编代码中的函数调用
这段使用call指令实现函数调用,其中库函数通过PLT(过程链接表)实现延迟绑定。
(3)返回值处理

图3.18:汇编代码中的返回值处理
这段返回值通过eax寄存器传递,并用leave指令恢复栈帧。
3.3.10 内存访问
(1)数组访问

图3.19:汇编代码中的数组访问
这段通过基址+偏移量实现数组访问,且由于是64位系统,每个指针占8字节。
3.3.11 其他特性
(1)sizeof处理
编译器静态计算sizeof结果,直接使用常量:

图3.20:汇编代码中的sizeof处理
(2)复合赋值
虽然没有显式复合赋值,但有类似的操作:

图3.21:汇编代码中的复合赋值
3.4 本章小结
这一章介绍了C编译器如何把hello.i文件转换成hello.s文件的过程,简要说明了编译的含义和功能,演示了编译的指令,并通过分析生成的hello.s文件中的汇编代码,先从汇编代码整体结构上分块分析了每一段汇编代码对应什么作用,然后进一步地将汇编代码按着数据处理,函数调用,赋值、算术、关系运算等具体操作进行了分析,明确了我们所编写的c程序是如何转化为更贴近硬件的汇编代码,以及汇编代码是如何通过一些更底层的操作实现我们编写的高级语言程序中的功能的。
第4章 汇编
4.1 汇编的概念与作用
汇编是将汇编语言代码(.s文件)转换为机器语言指令(二进制目标文件.o)的过程。汇编器(如as)在这一阶段主要完成以下工作:
指令转换:将汇编指令逐行转换为对应的机器码。
符号解析:处理标签和符号引用,生成符号表。
重定位信息生成:标记需要链接阶段处理的地址引用。
节区划分:将代码、数据等分配到ELF格式的不同节区。
汇编阶段生成的可重定位目标文件(.o)包含机器指令,但尚未解决外部引用和最终地址分配问题,需要通过链接器进一步处理。
4.2 在Ubuntu下汇编的命令
通常而言,在Ubuntu中使用GCC生成目标文件的命令如下:
gcc –c hello.s –o hello.o
由于hello.c的注释中要求了特定的汇编指令,于是用以下指令来完成对hello.s的汇编:
gcc -m64 -Og -no-pie -fno-stack-protector -fno-PIC hello.c -o hello.o
在Ubuntu终端执行此命令的结果如下图所示:

图4.1:Ubuntu下执行编译命令前后截图
可以看到,在没有执行命令行时,该目录下只有hello.c,hello.i和hello.s三个文件;执行过编译命令后,便多了生成的二进制目标文件:hello.o。同时也可以看到在当前目录的文件夹里出现了所预期的hello.o文件(显示为绿色)。
4.3 可重定位目标elf格式
可以使用readelf工具分析hello.o的ELF格式:
readelf –a hello.o
关键信息如下:
(1) ELF头:
ELF头(ELF header)以一个l6字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含了帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是有节头部表描述的,其中目标文件中每个节都有一个固定大小的条目(entry)。ELF头展示如下:

图4.2:ELF头截图
(2)程序头:
程序头定义了文件在内存中的加载布局,它指定了代码段、数据段等各个部分在内存中的位置、大小以及权限(如可读、可写、可执行)。通过程序头,操作系统能够将 ELF 文件的各个部分正确地映射到内存中,从而为程序的执行提供必要的空间和访问权限。

图4.3:程序头截图
(3)关键节区:
关键节区主要包含有代码节区(.text 和 .plt)和数据节区(.rodata 和 .data):
代码节区(.text 和 .plt)是程序的核心部分之一。.text 区包含了程序的指令代码,这些指令是程序运行时 CPU 执行的具体操作序列。而 .plt(过程链接表)区则用于动态链接,它包含了动态链接桩,这些桩用于在运行时加载和解析动态链接库中的函数地址,从而实现程序与外部共享库的交互。
数据节区(.rodata 和 .data)则用于存储程序的常量和变量。.rodata 区存储只读数据,如字符串常量、全局常量等,这些数据在程序运行过程中不会被修改,因此通常被放置在只读内存区域以提高安全性和效率。.data 区则存储可读写的数据,如全局变量、静态变量等,这些数据在程序运行时可以被修改,因此需要分配在可读写的内存区域。

图4.4:关键节区截图
(4)重定位信息:
重定位信息是 ELF 文件中用于修正外部符号引用的关键部分。在程序编译时,某些符号(如函数名、变量名)可能尚未确定其最终地址,重定位信息记录了这些符号的位置和需要修正的偏移量。在程序加载或链接时,链接器会根据重定位信息将这些符号正确地映射到内存中的实际地址,从而确保程序能够正确地访问外部资源。

图4.5:重定位信息截图
(5)符号表:
符号表记录了程序中所有函数和变量的名称、地址、类型等信息。它为调试器、链接器等工具提供了对程序内部结构的详细描述,使得这些工具能够正确地解析和操作程序中的各个符号。例如,调试器可以通过符号表获取函数的名称和地址,从而在调试过程中方便地设置断点和查看变量值。

图4.6:动态符号表截图

图4.7:静态符号表截图
4.4 Hello.o的结果解析
输入 objdump -d -r hello.o > hello_o.asm 指令输出hello.o的反汇编文件,并与第3章的hello.s文件进行对照分析。

图4.8:输出hello.o的反汇编文件
经过hello.s(汇编代码)与 hello.asm(反汇编机器码)的对照分析,下面将重点说明机器语言构成、与汇编语言的映射关系,以及分支转移和函数调用的差异:
1.机器语言与汇编语言的基本映射
示例1:函数序言(Stack Frame Setup)
对比下面两图所示的代码:

图4.9:函数序言的汇编代码

图4.10:函数序言的机器码
可以看到如下差异:
汇编代码中分配了 32 字节栈空间,而机器码实际分配 8 字节(0x8)。这是因为编译器优化了栈布局,合并了局部变量的存储。
机器码多了一条 push %rbx,用于保存被调用者保存的寄存器(%rbx),而汇编代码中未显式体现。
示例2:字符串加载
对比下面两图所示的代码:

图4.11:字符串加载的汇编代码

图4.12:字符串加载的机器码
可以看到如下差异:
汇编代码使用 RIP 相对寻址(.LC0(%rip)),而机器码直接使用绝对地址 0x402008(链接时确定的 .rodata 段地址)。
链接器将 RIP 相对地址解析为固定虚拟地址。
2.分支转移的差异
示例1:条件跳转(if语句)
对比下面两图所示的代码:

图4.13:条件跳转的汇编代码

图4.14:条件跳转的机器码
可以看到如下差异:
汇编代码中的 je(相等跳转)在机器码中变为 jne(不等跳转),目标地址从标签 .L2 变为绝对地址 0x4011ef。
跳转偏移量 0x0a 表示跳转到当前指令地址 +10 字节处(4011e3 + 0x0a = 4011ed,实际为 4011ef,因指令长度差异)。
示例2:循环(for语句)
对比下面两图所示的代码:

图4.15:循环的汇编代码

图4.16:循环的机器码
可以看到如下差异:
标签 .L4 被替换为绝对地址 0x401203。
偏移量 0xbe 是补码表示的 -66,跳转到 401243 – 66 = 401203。
3. 函数调用的差异
示例1:库函数调用(如 printf)
对比下面两图所示的代码:

图4.17:printf函数调用的汇编代码

图4.18:printf函数调用的机器码
可以看到如下差异:
汇编代码中的 printf@PLT 在机器码中实际调用 __printf_chk@plt(编译器优化后的安全版本)。
调用地址 0x4010b0 是 PLT 表中的条目,偏移量 0xfffffe8d 表示跳转到 40121e + 0xfffffe8d = 4010b0(补码计算)。
示例2:动态链接的 PLT 条目
在机器码中,存在着动态链接的 PLT 条目:

图4.19:动态链接的 PLT 条目
这里第一跳转到 PLT 条目,第二跳通过 GOT 表(0x404028)解析实际函数地址,即延迟绑定。
4. 操作数处理的差异
示例1:立即数与内存地址
对比下面两图所示的代码:

图4.20:立即数的汇编代码

图4.21:立即数的机器码
在这里,立即数 $1 在机器码中被扩展为 4 字节,也就是0x00000001。
示例2:全局变量访问(如 stdin)
对比下面两图所示的代码:

图4.22:全局变量访问的汇编代码

图4.23:全局变量访问的机器码
可以看到,getchar 被优化为 getc(stdin),stdin 的地址 0x404060 在 GOT 中。
5. 其他关键差异
(1)指令编码:
汇编代码中的 endbr64(安全特性指令)直接对应机器码 f3 0f 1e fa。
nop 指令可能被优化为 0x90 或其他占位符。
(2)数据对齐:
汇编代码中的 .align 8 在机器码中通过插入填充字节实现。
(3)符号解析:
汇编代码中的标签(如 .LC0)在机器码中被替换为绝对地址(如 0x402008)。
进一步分析,产生这些差异的根本原因有以下几点:
(1)链接阶段:汇编代码中的符号(标签、函数名)被解析为虚拟地址。
(2)编译器优化:指令可能被重排序或替换(如 printf → __printf_chk)。
(3)ABI 要求:补充寄存器保存(如 push %rbx)未在汇编代码中显式写出。
4.5 本章小结
本章详细分析了从汇编代码(hello.s)到可重定位目标文件(hello.o)的转换过程。通过使用readelf和objdump工具,我们深入研究了hello.o的ELF格式结构,包括节区划分、重定位信息和符号表等关键组成部分。通过对比汇编代码和反汇编结果,我们清晰地看到了汇编语言指令与机器码之间的映射关系,以及链接阶段需要解决的重定位问题。这一阶段为后续的链接过程奠定了基础,使多个目标文件能够合并为一个完整的可执行程序。
第5章 链接
5.1 链接的概念与作用
链接(Linking)是将多个可重定位目标文件(.o)合并生成可执行文件(如 hello)的过程。其主要作用包括:
符号解析:将符号引用(如 printf)与定义(如 libc.so 中的实现)绑定。
地址重定位:将目标文件中的相对地址转换为最终执行的绝对地址。
合并节区:将不同目标文件的 .text、.data 等节区合并。
处理库依赖:动态链接共享库(如 libc.so)。
5.2 在Ubuntu下链接的命令
gcc hello.o -o hello
实际链接过程隐含调用了 ld,并链接了默认的启动文件(crt1.o)和库(libc.so)。
在Ubuntu终端执行此命令的结果如下图所示:

图5.1:Ubuntu下执行链接命令前后截图
可以看到,在没有执行命令行时,该目录下只有hello.c,hello.i,hello.s,hello.o,
hello.asm五个文件;执行过编译命令后,便多了生成的可执行文件:hello。同时也可以看到在当前目录的文件夹里出现了所预期的hello文件(显示为绿色)。
5.3 可执行目标文件hello的格式
使用readelf解析hello的ELF格式,得到hello的节信息和段信息,输入指令:
readelf -a hello
便可查看hello的ELF格式,下面进行详细说明:
1. ELF头:
ELF头(ELF header)以一个l6字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含了帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是有节头部表描述的,其中目标文件中每个节都有一个固定大小的条目(entry)。ELF头展示如下:

图5.2:ELF头截图
2. 程序头:
程序头定义了文件在内存中的加载布局,它指定了代码段、数据段等各个部分在内存中的位置、大小以及权限(如可读、可写、可执行)。通过程序头,操作系统能够将 ELF 文件的各个部分正确地映射到内存中,从而为程序的执行提供必要的空间和访问权限。

图5.3:程序头截图
3. 关键节区:
关键节区主要包含有代码节区(.text 和 .plt)和数据节区(.rodata 和 .data):
代码节区(.text 和 .plt)是程序的核心部分之一。.text 区包含了程序的指令代码,这些指令是程序运行时 CPU 执行的具体操作序列。而 .plt(过程链接表)区则用于动态链接,它包含了动态链接桩,这些桩用于在运行时加载和解析动态链接库中的函数地址,从而实现程序与外部共享库的交互。
数据节区(.rodata 和 .data)则用于存储程序的常量和变量。.rodata 区存储只读数据,如字符串常量、全局常量等,这些数据在程序运行过程中不会被修改,因此通常被放置在只读内存区域以提高安全性和效率。.data 区则存储可读写的数据,如全局变量、静态变量等,这些数据在程序运行时可以被修改,因此需要分配在可读写的内存区域。

图5.4:关键节区截图
4. 重定位信息:
重定位信息是 ELF 文件中用于修正外部符号引用的关键部分。在程序编译时,某些符号(如函数名、变量名)可能尚未确定其最终地址,重定位信息记录了这些符号的位置和需要修正的偏移量。在程序加载或链接时,链接器会根据重定位信息将这些符号正确地映射到内存中的实际地址,从而确保程序能够正确地访问外部资源。

图5.5:重定位信息截图
5. 符号表
符号表记录了程序中所有函数和变量的名称、地址、类型等信息。它为调试器、链接器等工具提供了对程序内部结构的详细描述,使得这些工具能够正确地解析和操作程序中的各个符号。例如,调试器可以通过符号表获取函数的名称和地址,从而在调试过程中方便地设置断点和查看变量值。

图5.6:动态符号表截图

图5.7:静态符号表截图
5.4 hello的虚拟地址空间
使用edb打开hello从Data Dump窗口观察hello加载到虚拟地址的情况,查看各段信息。如下图所示:

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

图5.9:.interp节截图
在edb中找到对应的信息:

图5.10:edb中.interp节的对应信息
同样的,我们可以找到如.text节的信息:

图5.11:.text节截图

图5.12:edb中.text节对应的信息
5.5 链接的重定位过程分析
5.5.1分析hello与hello.o区别
在终端使用命令objdump -d -r hello > hello.asm生成反汇编文件hello.asm,如下图所示:

图5.13:输出hello的反汇编文件
与第四章中生成的hello_o.asm文件进行比较,其不同之处如下:
(1)链接后函数数量增加:
链接后的反汇编文件hello2.asm中,多出了.plt,puts@plt,printf@plt,getchar@plt,exit@plt,sleep@plt等函数的代码。这是因为动态链接器将共享库中hello.c用到的函数加入可执行文件中。
图5.14:链接后增加的函数
(2)函数调用指令call的参数发生变化:
在链接过程中,链接器解析了重定位条目,call之后的字节代码被链接器直接修改为目标地址与下一条指令的地址之差,指向相应的代码段,从而得到完整的反汇编代码。

图5.15:call的参数变化体现
(3)跳转指令参数发生变化:
在链接过程中,链接器解析了重定位条目,并计算相对距离,修改了对应位置的字节代码为PLT 中相应函数与下条指令的相对地址,得到完整的反汇编代码。

图5.16:跳转指令参数变化体现
5.5.2 重定位过程
重定位由两步组成:
(1)重定位节和符号定义
在这一步中,链接器将所有相同类型的节合并为同一类型的聚合节。然后链接器将运行时的内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。至此程序中每条指令和全局变量都有唯一的运行内存地址。
(2)重定位节中的符号引用
这一步中链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。要执行这一步,链接器依赖于可重定位目标模块中称为重定位条目的数据结构。
重定位过程地址计算方法如下:

图5.17:重定位过程地址计算方法
5.6 hello的执行流程
5.6.1 过程:
通过edb的调试,一步一步地记录下call命令进入的函数。

图5.18:edb调试界面
开始执行:_start、_libe_start_main
执行main:_main、printf、_exit、_sleep、getchar
退出:exit
5.6.2 子程序名和地址:
|
程序名 |
程序地址 |
|
_start |
0x4010f0 |
|
_libc_start_main |
0x2f12271d |
|
main |
0x401125 |
|
_printf |
0x4010a0 |
|
_sleep |
0x4010e0 |
|
_getchar |
0x4010b0 |
|
_exit |
0x4010d0 |
表5.1:子程序名和地址
5.7 Hello的动态链接分析
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。延迟绑定是通过GOT和PLT实现的,根据hello.elf文件可知,GOT起始表位置为:0x404000:

图5.19:GOT起始表位置
GOT表位置在调用dl_init之前0x404008后的16个字节均为0:
图5.20:Data Dump中的GOT表(其一)
调用了dl_init之后字节改变了:

图5.21:Data Dump中的GOT表(其二)
对于变量而言,利用代码段和数据段的相对位置不变的原则去计算正确地址;
对于库函数而言,需要plt、got合作。plt初始存的是一批代码,它们跳转到got所指示的位置,然后调用链接器。初始时got里面存的都是plt的第二条指令,随后链接器修改got,下一次再调用plt时,指向的就是正确的内存地址。接下来执行程序的过程中,就可以使用过程链接表plt和全局偏移量表got进行动态链接。
5.8 本章小结
本章首先阐述了链接的基本概念和作用,展示了使用命令链接生成hello可执行文件,观察了hello文件ELF格式下的内容,利用edb观察hello文件的虚拟地址空间使用情况,最后以hello程序为例对重定位过程、执行过程和动态链接进行分析。
第6章 hello进程管理
6.1 进程的概念与作用
进程是计算机系统中程序执行的基本单位,是操作系统资源分配和调度的基本实体。进程具有以下特点:
独立性:每个进程拥有独立的地址空间、数据栈和其他系统资源。
动态性:进程是程序的一次执行过程,具有创建、执行、暂停和终止等生命周期。
并发性:多个进程可以并发执行,通过时间片轮转等方式共享CPU资源。
在hello程序的执行过程中,进程的作用包括:
为程序提供独立的执行环境;
管理程序使用的系统资源(如内存、文件描述符等);
实现程序与其他进程的隔离和保护;
通过进程调度实现并发执行。
6.2 简述壳Shell-bash的作用与处理流程
Shell是用户与操作系统内核交互的接口,它为用户提供一个操作界面,接受用户输入的命令,并调度相应的应用程序。bash(Bourne Again Shell)是Linux系统中常用的Shell实现。首先从终端读入输入的命令,对输入的命令进行解析,如果该命令为内置命令,则立即执行命令,否则调用fork创建一个新的子进程,在该子进程的上下文中执行指定的程序。判断该程序为前台程序还是后台程序,如果为前台程序则等待程序执行结束,若为后台程序则将其放回后台并返回。在过程中shell可以接受从键盘输入的信号并对其进行处理。
对于hello程序,bash的主要作用和处理流程如下:
1.命令解析:
(1)读取用户输入的”./hello”命令。
(2)解析命令和参数(如学号、姓名等)。
2. 路径查找:
在PATH环境变量指定的目录中查找hello可执行文件;若未找到则报错。
3. 进程创建:
(1)调用fork()系统调用创建子进程。
(2)在子进程中调用execve()加载并执行hello程序。
4. 环境设置:
(1)为hello程序设置环境变量。
(2)建立标准输入/输出/错误流。
5. 进程管理:
(1)支持后台运行(&)、作业控制等。
(2)处理信号(如Ctrl-C、Ctrl-Z等)。
6.3 Hello的fork进程创建过程
首先用户再shel1界面输入指令:./hel1o 2023111767 张鸣文
Shell判断该指令不是内置命令,于是父进程调用fork函数创建一个新的子进程,该子进程得到与父进程用户级虚拟地址空间相同的一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程与父进程最大的区别就是具有不同的PID。在父进程中,fork返回子进程的PID,而在子进程中fork返回0,返回值提供一个明确的方法来分辨程序是父进程还是在子进程中执行。
6.4 Hello的execve过程
execve函数在当前进程的上下文中加载并运行一个程序。函数声明如下:
int execve(const char *filename, const char *argv[], const char *envp[]);
execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境变量envp。只有当出现错误时,例如找不到filename,execve才会返回到调用程序。所以,与fork一次调用返回两次不同,execve调用一次并不返回。main函数运行时,用户栈的结构如图所示:

图6.1:用户栈的结构图
6.5 Hello的进程执行
hello程序在运行时,进程提供给应用程序的抽象有:
(1)一个独立的逻辑控制流,它提供一个假象好像我们的进程独占地使用处理器;
(2)一个私有的地址空问,它提供一个假象好像我们的程序独占地使用CPU内存。
操作系统提供的抽象有:
(1)逻辑控制流。如果想用调试器单步执行程序,我们会看到一系列的程序计数器(PC)的值,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或是包含在运行时动态链接到程序的共享对象中的指令。这个PC值的序列叫做逻辑控制流,或者简称为逻辑流。一个逻辑流的执行在时间上与另一个流重叠,称为并发流,这两个流被称为并发地运行。
(2)上下文切换。操作系统内核使用一种称为上下文切换的叫高层形式的异常控制流来实现多任务。内核为每一个进程维持一个上下文。上下文就是内核重新启动一个被抢占的进程所需状态。
(3)时间片。一个进程执行它的控制流的一部分的每一时间段叫做时间片。因此,多任务也叫做时间分片。
(4)用户模式和内核模式。处理器通常使用某个控制寄存器中的一个模式位来提供这种功能。当设置了模式位时,进程就运行在内核模式里。一个运行在内核模式的进程可以执行指令集中的所有指令且可以访问系统中的任何内存位置。没有设置模式位时,进程就运行在用户模式中。用户模式中的进程不允许执行特权指令,也不能直接引用地址空间中内核区内的代码和数据
(5)上下文信息。上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。hello程序执行过程中,在进程调用execve函数后,进程就为hello程序分配新的虚拟地址空间,开始时程序运行在用户模式中,调用printf函数输出“Hello 2023111767 张鸣文”,之后调用sleep函数,进程进入内核模式,运行信号处理程序,再返回用户模式,运行过程中,cpu不断切换上下文,使运行过程被切分成时间片,与其他进程交替占用cpu,实现进程的调度。
6.6 hello的异常与信号处理
6.6.1 异常的分类:
|
类别 |
原因 |
异步/同步 |
返回行为 |
|
中断 |
来自I/O设备的信号 |
异步 |
总是返回下一条指令 |
|
陷阱 |
有意的异常 |
同步 |
总是返回下一条指令 |
|
故障 |
潜在可恢复的错误 |
同步 |
可能返回当前指令 |
|
终止 |
不可恢复的错误 |
同步 |
不会返回 |
表6.1:异常的分类
6.6.2 异常的处理方式:

图6.2:异常的处理方式示意图
6.6.3 运行结果及相关命令:
(1)正常运行状态
在程序正常运行时,打印10次提示信息,以输入回车为标志结束程序,并回收进程。

图6.3:程序正常运行结果截图
(2)运行时按下Ctrl + C
按下Ctrl + C,Shell进程收到SIGINT信号,Shell结束并回收hello进程。

图6.4:运行时按Ctrl+C的结果截图
(3)运行时按下Ctrl + Z
按下Ctrl + Z,Shell进程收到SIGSTP信号,Shell显示屏幕提示信息并挂起hello进程。

图6.5:运行时按Ctrl + Z的结果截图
(4)对hello进程的挂起可由ps和jobs命令查看,可以发现hello进程确实被挂起而非被回收,且其job代号为1。

图6.6:对hello进程的挂起的ps和jobs查看
(5)在Shell中输入pstree命令,可以将所有进程以树状图显示:

图6.7:进程的树状图表示(前半部分)
![图片[1] - 哈尔滨工业大学计算机系统HIT-ICS大作业:程序人生-Hello’s P2P - 宋马](https://pic.songma.com/blogimg/20250510/2e4a910acc7b40da8dde220bac85ba8f.png)
图6.8:进程的树状图表示(后半部分)
(6)输入kill命令,则可以杀死指定(进程组的)进程:

图6.9:杀死进程截图
(7)输入kill -CONT PID(这里的PID值要根据需要再次启动的进程的PID值调整,下例中为4560,所以输入的指令为kill -CONT 4560)则命令将hello进程再次调到前台执行,可以发现Shell首先打印hello的命令行命令,hello再从挂起处继续运行,打印剩下的语句。程序仍然可以正常结束,并完成进程回收。

图6.10:继续调出已经暂停的进程截图
(8)不停乱按
在程序执行过程中乱按所造成的输入均缓存到stdin,当getchar的时候读出一个’
’结尾的字串(作为一次输入),hello结束后,stdin中的其他字串会当做Shell的命令行输入。会发现,不停乱按并没有阻碍进程的执行,最终还是完整的输出了预期中的10次提示信息。

图6.11:不停乱按的结果演示
6.7本章小结
本章的主要内容是探讨计算机系统中的进程和shell,首先通过一个简单的hello程序,简要介绍了进程的概念和作用、shell的作用和处理流程,还详细分析了hello程序的进程创建、启动和执行过程,最后,本章对hello程序可能出现的异常情况,以及运行结果中的各种输入进行了解释和说明。
第7章 hello的存储管理
7.1 hello的存储器地址空间
(1)逻辑地址:
在有地址变换功能的计算机中,访问指令给出的地址(操作数)叫逻辑地址,也叫相对地址。要经过寻址方式的计算或变换才得到内存储器中的物理地址。逻辑地址是由一个段标识符加上一个指定段内相对地址的偏移量,由程序hello产生的与段相关的偏移地址部分。
(2)线性地址:
线性地址是逻辑地址到物理地址变换之间的一步,程序hello的代码会产生逻辑地址,在分段部件中逻辑地址是段中的偏移地址,加上基地址就是线性地址。
(3)虚拟地址:
程序访问存储器所使用的逻辑地址称为虚拟地址。虚拟地址经过地址翻译得到物理地址。与实际物理内存容量无关,是hello中的虚拟地址。
(4)物理地址:
在存储器里以字节为单位存储信息,每一个字节单元给一个唯一的存储器地址,这个地址称为物理地址,是hello的实际地址或绝对地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段式管理是指把一个程序分成若干个段进行存储,每个段都是一个逻辑实体。段式管理是通过段表进行的,包括段号(段名)、段起点、装入位、段的长度等。程序通过分段划分为多个块,如代码段、数据段、共享段等。
一个逻辑地址是两部分组成的,包括段标识符和段内偏移量。段标识符是由一个16位长的字段组成的,称为段选择符。其中前13位是一个索引号,后3位为一些硬件细节。索引号即是“段描述符”的索引,段描述符具体地址描述了一个段,很多个段描述符就组成了段描述符表。通过段标识符的前13位直接在段描述符表中找到一个具体的段描述符。
全局描述符表(GDT)整个系统只有一个,它包含:(1)操作系统使用的代码段、数据段、堆栈段的描述符(2)各任务、程序的LDT(局部描述符表)段。
每个任务程序有一个独立的LDT,包含:(1)对应任务/程序私有的代码段、数据段、堆栈段的描述符(2)对应任务/程序使用的门描述符:任务门、调用门等。
段式管理图示如下:

图7.1:内存分段寻址
7.3 Hello的线性地址到物理地址的变换-页式管理
虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。VM系统将虚拟内存分割,称为虚拟页,类似地,物理内存也被分割成物理页。利用页表来管理虚拟页,页表就是一个页表条目(PTE)的数组,每个PTE由一个有效位和一个位地址字段组成,有效位表明了该虚拟页当前是否被缓存在DRAM中,如果设置了有效位,那么地址字段就表示DRAM中相应的物理页的起始位置,如果发生缺页,则从磁盘读取。
MMU利用页表来实现从虚拟地址到物理地址的翻译。
下面为页式管理的图示:

图7.2:内存分页寻址
7.4 TLB与四级页表支持下的VA到PA的变换
Core i7采用四级页表的层次结构。CPU产生虚拟地址VA,虚拟地址VA传送给MU,MMU使用VPN高位作为TLBT和TLBI,向TLB中寻找匹配。如果命中,则得到物理地址PA。如果TLB中没有命中,MMU查询页表,CR3确定第一级页表的起始地址,VPN1确定在第一级页表中的偏移量,查询出PTE,以此类推,最终在第四级页表中找到PPN,与VPO组合成物理地址PA,添加到PLT。工作原理如下:

图7.3:多级页表工作原理(其一)
多级页表的工作原理展示如下:

图7.4:多级页表工作原理(其二)
7.5 三级Cache支持下的物理内存访问
如图为高速缓存存储器组织结构:

图7.5:高速缓存存储器的结构
高速缓存的结构将m个地址位划分成了t个标记位,s个组索引位和b个块偏移位:
![图片[2] - 哈尔滨工业大学计算机系统HIT-ICS大作业:程序人生-Hello’s P2P - 宋马](https://pic.songma.com/blogimg/20250510/957833566f4b40bab62c161e2e02153b.png)
图7.6:高速缓存的地址位划分
如果选中的组存在一行有效位为1,且标记位与地址中的标记位相匹配,我们就得到了一个缓存命中,否则就称为缓存不命中。如果缓存不命中,那么它需要从存储器层次结构的下一层中取出被请求的块,然后将新的块存储在组索引位指示组中的一个高速缓存行中,具体替换哪一行取决于替换策略,例如LRU策略会替换最后一次访问时间最久远的那一行。
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_ struct、.区域结构和页表的原样副本。当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任何一个。后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。

图7.7:进程的地址空间示意图
7.7 hello进程execve时的内存映射
execve函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下几个步骤:
删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
映射私有区域。为新程序的代码、数据、.bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区,.bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零。
映射共享区域。hello程序与共享对象1ibc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
设置程序计数器。execve做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。如图所示:

图7.8:execve设置程序计数器示意图
7.8 缺页故障与缺页中断处理
如果程序执行过程中发生了缺页故障,则内核调用缺页处理程序。处理程序执行如下步骤:
检查虚拟地址是否合法,如果不合法则触发一个段错误,终止这个进程。
检查进程是否有读、写或执行该区域页面的权限,如果不具有则触发保护异常,程序终止。
两步检查都无误后,内核选择一个牺牲页面,如果该页面被修改过则将其交换出去,换入新的页面并更新页表。然后将控制转移给hello进程,再次执行触发缺页故障的指令。

图7.9:缺页故障处理示意图
7.9动态存储分配管理
动态内存管理是程序在运行时按需分配和释放内存的机制,如printf在格式化输出(如处理长字符串或可变参数)时可能隐式调用malloc分配缓冲区。以下是其核心方法与策略:
7.9.1 基本方法:
(1)显式分配:通过malloc、calloc等函数主动申请堆内存。
(2)隐式分配:库函数(如printf)内部可能调用malloc管理缓冲区,对用户透明。
(3)依靠释放机制:需手动free释放,否则导致内存泄漏(隐式分配的内存通常由库函数自行释放)。
7.9.2 管理策略:
(1)首次适应(First-Fit):从堆中找到第一个足够大的空闲块分配,速度快但易产生碎片。
(2)最佳适应(Best-Fit):选择最小的合适空闲块,减少浪费但搜索开销大。
(3)最差适应(Worst-Fit):选择最大的空闲块,减少碎片但易导致大块内存耗尽。
在碎片处理上,内部碎片有着分配块大于实际需求(如对齐填充),无法利用的问题;而外部碎片空闲内存分散,无法合并满足大请求的问题。对应的解决方案是通过brk/sbrk扩展堆,或使用内存池预分配大块内存。合并与分割的运用,可以释放内存时合并相邻空闲块,减少碎片,同时在大空闲块被请求时,分割为所需大小+剩余小块。除此之外还有一些高效实现,比如维护空闲块链表,加速搜索(如glibc的ptmalloc);预分配固定大小块(如printf可能复用缓冲区避免频繁分配),构成内存池。
7.9.3 常见问题及优化:
(1)内存泄漏:printf内部malloc的缓冲区未释放(依赖库实现)。
(2)性能瓶颈:频繁分配小内存可通过对象池优化。
(3)线程安全:多线程下使用malloc需加锁(如glibc的arena机制)。
总而言之,printf调用malloc体现了动态内存的按需分配和透明管理特性。合理策略需平衡速度(快速分配),空间利用率(减少碎片)和通用性(适应不同大小的请求)这几个问题。现代内存管理器(如ptmalloc、jemalloc)通过混合策略优化这些目标。
7.10本章小结
本章主要介绍了hello的存储器地址空间、intel的段式管理、hello的页式管理,以intel Core i7在指定环境下介绍了虚拟地址VA到物理地址PA的转换、物理内存访问,分析了hello进程fork时的内存映射、hello进程、execve时的内存映射、缺页故障与缺页中断处理,也对动态存储分配管理做了一些介绍。
结论
通过对Hello程序的完整分析,我更深入的体会到了计算机系统设计的精妙之处。从源代码到可执行文件的转换过程展现了编译器技术的严谨与高效,预处理阶段的宏展开和头文件包含为后续编译奠定了基础,词法分析和语法分析确保了程序的正确性,而代码优化则体现了对执行效率的极致追求。链接过程中的符号解析和地址重定位揭示了模块化编程的实现机制,动态链接技术更是展现了现代操作系统在资源共享方面的智慧。
在进程管理层面,fork和execve的系统调用展示了操作系统如何通过写时复制等优化技术实现高效的进程创建。进程调度算法和上下文切换机制保证了多任务环境下的公平性和响应性,而信号处理机制则体现了系统对异常情况的优雅应对。存储管理部分的研究让我们理解了虚拟内存如何通过页表映射和TLB加速实现地址转换,多级页表的设计在空间效率和时间效率之间取得了巧妙平衡,缺页处理机制则展现了系统对存储资源的动态管理能力。
现代计算机系统通过多层次的抽象和精密的协作机制,将复杂的硬件细节隐藏在对程序员友好的接口之后。从CPU的指令执行到操作系统的资源管理,从编译器的代码生成到链接器的符号解析,每一层都各司其职又紧密配合,共同构建了一个高效可靠的程序执行环境。这种分层设计不仅提高了系统的可维护性和可扩展性,也为不同领域的开发者提供了适当的抽象级别。
同时,系统性能优化也有多个关键点。缓存机制显著减少了处理器访问内存的延迟,写时复制技术降低了进程创建的开销,动态链接实现了库代码的共享,而现代内存管理器则通过智能的分配策略减少了碎片。这些优化措施共同作用,使得看似简单的Hello程序能够在复杂的计算机系统中高效运行。
通过对Hello程序的系统级分析,我更加深刻地认识到,计算机科学不仅是一门实践性很强的学科,更是一门充满智慧的,当之无愧的艺术。每一个简单的程序背后,都凝聚着一代又一代人的探索与智慧。如此高效的,从软件到硬件的“系统级设计”绝非一日之功。正是一次又一次的迭代,计算机科学才能够有人不断地提出更高效更新颖的解决方案,才能不断地向前发展。这种从微观到宏观、从理论到实践的研究方法,让我受益匪浅。未来,在计算机系统这门课上学到的知识和系统级的思维方式将继续指导我设计出更高效、更可靠的软硬件系统。
附件
hello.c:老师提供的c语言源程序文件;
hello.i:将hello.c预处理得到的预处理文件;
hello.s:将hello.i编译后得到的汇编文件;
hello.o:将hello.s进行汇编后得到的二进制目标文件;
hello:将hello.o链接后得到的可执行文件;
hello_o.asm: 将hello.o反汇编得到的反汇编文件;
hello.asm:将hello反汇编得到的反汇编文件
参考文献
[1] Bryant, R. E., & O'Hallaron, D. R. (2016). Computer Systems: A Programmer's
Perspective (3rd ed.). Pearson.
[2] Kernighan, B. W., & Ritchie, D. M. (1988). The C Programming Language (2nd
ed.). Prentice Hall.
[3] 谭浩强. (2010). C 程序设计(第4版). 北京: 清华大学出版社.
[4] Kerrisk, M. (2010). The Linux Programming Interface: A Linux and UNIX System
Programming Handbook. No Starch Press.
[5] Linux 常用命令:https://www.runoob.com/w3cnote/linux-command-full-fight.html
[6] Linux 下C编程:Linux 操作系统和C语言(详解)_linux c语言-CSDN博客
[7] Ubuntu 相关: http://forum.ubuntu.org.cn/
[8] C汇编Linux手册: http://docs.huihoo.com/c/linux-c-programming/
[9] gcc 使用: https://blog.csdn.net/weixin_50697073/article/details/123759516

















暂无评论内容