计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业
学 号
班 级
学 生
指 导 教 师 吴锐
计算机科学与技术学院
2024年5月
本文主要通过分析 hello 程序的一生,即预处理、编译、汇编、链接、进程管理等整个生命周期。回顾了这学期计算机系统这门课的几乎所有知识。在分析过程中使用 ubuntu 作为操作系统,并使用了一些工具辅助完成,目的是对于计算机系统的工作与原理有更深的了解。本文不仅理论上探讨了这些工具的原理和方法,还实际演示了它们的操作和结果,并插入大量图片,阐述了计算机系统的工作原理和体系结构,帮助读者更深入地理解和掌握C语言程序的编译和执行过程。
关键词:计算机系统;程序的一生;P2P;020
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第1章 概述
1.1 Hello简介
P2P:即From Program to Process。指从hello.c(Program)变为运行时的进程(Process)。hello.c源程序经过预处理器(cpp)处理生成一个 hello.i 文件,也就是修改了的源程序。由编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,接下来汇编器(as)将hello.s翻译成机器语言指令,将其打包成可重定位目标程序的格式并保存在二进制文件hello.o中。再由链接器生成可执行目标程序 hello,此时在 shell 中调用相关命令将为其创建进程(Process),执行程序。
020:即From Zero-0 to Zero-0。指最初内存并无hello文件的相关内容,在 shell 中输入相关命令后,shell 将调用 fork 函数为这一程序创建进程,之后将通过 exceve 在进程的上下文中加载并运行 hello,将进程映射到虚拟内存空间,并加载需要的物理内存。当执行结束后父进程将回收这一进程,内核将清除hello这一进程的相关信息,这一进程就结束了。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件环境:
处理器:12th Gen Intel(R) Core(TM)i7-12700H 2300Mhz
RAM:16.0G
系统类型:64位操作系统,基于x64的处理器
软件环境:Windows11 64位,VMware,Ubuntu 20.04
开发与调试工具:Visual Studio 2022 64位;vim objump edb gcc readelf等工具
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
中间结果文件名称 | 文件作用 |
hello.i | 预处理后的文件 |
hello.s | 汇编程序 |
hello.o | 可重定位目标文件 |
hello | 可执行目标程序 |
hello.elf | hello.o的elf格式文件 |
hello.asm | 反汇编hello.o得到的反汇编文件 |
hello1.elf | hello的elf格式文件 |
hello1.asm | 反汇编hello得到的反汇编文件 |
1.4 本章小结
本章首先介绍了hello的P2P,020流程,包括流程的设计思路和实现方法;然后,详细说明了本实验所需的硬件配置、软件平台、开发工具以及本实验生成的各个中间结果文件的名称和功能。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
2.1.1预处理的概念
预处理步骤是指预处理器在程序运行前,对源文件进行简单加工的过程。预处理过程主要进行代码文本的替换工作,用于处理以#开头的指令,还会删除程序中的注释和多余的空白字符。预处理指令可以简单理解为#开头的正确指令,它们会被转换为实际代码中的内容(替换)。
2.1.2预处理的作用
预处理过程中并不直接解析程序源代码的内容,而是对源代码进行相应的分割、处理和替换,主要有以下作用:
1.头文件包含:将所包含头文件的指令替代。比如 hello.c 的第一行的#include<stdio.h>命令告诉预处理器读取系统头文件 stdio.h 的内容,并把它直接插入程序文本中。
2.宏定义:对于#define 指令,进行宏替换,对于代码中所有使用宏定义的地方使用符号表示的实际值替换定义的符号。
3.条件编译:根据可能存在的#ifdef 来确定程序需要执行的代码段。
4.处理特殊符号:例如#error 等,预编译程序可以识别一些特殊的符号,并在后续过程中进行合适的替换。
5.其他:删除 c 语言源程序中的注释部分等。
简单来说,预处理是一个文本插入与替换的过程预处理器。
2.2在Ubuntu下预处理的命令
预处理的命令:gcc -E hello.c -o hello.i
图 2.2-1在Ubuntu下预处理的命令
2.3 Hello的预处理结果解析
可以发现,原本的源代码文件只有 24 行,预处理后的文件为 3061 行,原本的源代码部分在 3048 行之后,在这之前是 hello 引用的所有的头文件 stdio.h, unistd.h , stdlib.h 内容的展开。而很显然我们发现插入的部分不止有这三个头文件的内容,还出现了其他的头文件,这是因为这三个头文件中同样使用#include 命令引入了其他的头文件,这些头文件同样出现在了 hello.i 文件中。预处理器不会对头文件中的内容做任何计算或转换,只是简单地复制和替换。
hello.i具体信息如下图所示:
图 2.3-1hello.i具体信息
可以观察如下两张图,我们发现在源代码头部出现的注释在预处理之后的源代码部分已经不可见,因此这一点就印证了我们上面说的在预处理过程中预处理器将删除源代码中的注释部分。由于源代码中不存在宏定义与#ifdef 等部分,因此这一部分无法展示。
图 2.3-2源代码与预处理后对比图
2.4 本章小结
本章讲述了在linux环境中,如何用命令对C语言程序进行预处理,以及预处理的含义和作用。然后用一个简单的hello程序演示了从hello.c到hello.i的过程,并用具体的代码分析了预处理后的结果。通过分析,我们可以发现预处理后的文件hello.i包含了标准输入输出库stdio.h的内容,以及一些宏和常量的定义,还有一些行号信息和条件编译指令。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
3.1.1编译的概念
编译器(ccl)将文本文件 hello.i 翻译成文本文件 hello.s,它包含一个汇编语
言程序。
3.1.2编译的作用
计算机程序编译的作用是使高级语言源程序变为汇编语言,提高编程效率和可移植性。计算机程序编译的基本流程包括词法分析、语法分析、语义分析、中间代码生成、代码优化和目标代码生成等阶段。
1. 扫描(词义分析):将源代码程序输入扫描器,将源代码中的字符序列分割为一系列 c 语言中的符合语法要求的字符单元,这一部分可以分为自上而下的分析和自下而上的分析两种方式。
2. 语法分析:基于词法分析得到的字符单元生成语法分析树。
3. 语义分析:在语法分析完成之后由语义分析妻进行语义分析,主要就是为了判断指令是否是合法的 c 语言指令,这一部分也可以叫做静态语义分析,并不判断一些在执行时可能出现的错误,例如如果不存在 IDE 优化,这一步对于 1/0
这种只有在动态类型检查的时候才会发现的错误,代码将不会报错。
4. 中间代码:中间代码的作用是可使使得编译程序的逻辑更加明确,主要是为了下一步代码优化的时候优化的效果更好。
5. 代码优化:根据用户指定的不同优化等级对代码进行安全的、等价的优化,这一行为的目的主要是为了提升代码在执行时的性能。
6. 生成代码:生成是编译的最后一个阶段。在经过上面的所有过程后,在这一过程中将会生成一个汇编语言代码文件,也就是我们最后得到的 hello.s 文件,这一文件中的源代码将以汇编语言的格式呈现。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
图 3.2-1在Ubuntu下编译的命令
3.3 Hello的编译结果解析
3.3.1汇编初始部分
在main函数前有一部分字段展示了段名称及指令:
图 3.3-1hello.s文件初始部分
.file | 声明源文件名称 |
.text | 代码段:这个段包含程序的可执行指令。程序的主要代码、函数定义等都放在这个段中。每个可执行文件至少有一个 .text 段。 |
.section .rodata | 只读数据段:这个段用于存放只读数据,比如字符串常量、只读数组等。 |
.align | 对齐指令:用于确保后续的内容在内存中的地址是8字节对齐 |
.string | 定义了存储在只读数据段中的字符串。 |
.globl | 声明 `main` 函数是全局的,这样链接器在其他文件中也能看到它。 |
.type | 声明一个符号的类型:指定 `main` 是一个函数,便于链接器和调试器识别符号 |
.LC0 .LC1 | 局部标签,用于标识这些字符串常量的地址。 |
3.3.2 数据
(1)常量
①数字常量:通过观察我们可以发现在源代码中使用的数字常量都是储存在.text 段的,包括在比较的时候使用的数字变量 5,在循环的时候使用的循环比较变量等数字常量都是储存在.text 节的,具体情况可以见如下截图:
图 3.3-2hello.s文件中数字常量示例一
图 3.3-3hello.s文件中数字常量示例二
②字符串常量:有两个字符串存放在只读数据段中
图 3.3-4hello.s文件中字符串常量示例
- 局部变量:可以发现局部变量是储存在栈中的某一个位置的或是直接储存在寄存器中的,对于源代码中的每一个局部变量可以进行逐一分析。局部变量共有三个,一个是循环变量 i,以及 argc 和 argv。
①对于 i,我们发现它储存在栈中地址为-4(%rbp)的位置,对于 i 的操作可见如下截图:
图 3.3-5hello.s文件中局部变量i
②局部变量 argc,标志的是在程序运行的时候输入的变量的个数,可以发现
它储存在栈中地址为-20(%rbp)的位置,对于它的操作主要是与 5 比较之后确定有一部分代码是否执行,具体汇编代码如下截图:
图 3.3-6hello.s文件中局部变量argc
③ 局部变量 argv,是一个保存着输入变量的数组,观察发现它储存在栈中。具体汇编代码段如下:
图 3.3-7hello.s文件中局部变量argv
数组:局部变量argv同时也是hello.c中唯一的数组,数组的每个元素都是一个指向字符类型的指针。由代码知数组起始地址存放在栈中-32(%rbp)的位置,被两次调用作为参数传到printf中。如图,分别将rdi设置为两个字符串的起始地址:
3.3.3全局函数
hello.c中只声明了一个全局函数int main(int arge,.char*argv[]),我们通过汇编代码
可知。
3.3.4赋值
hel1o.c中的赋值操作有for循环开头的i=0,该赋值操作体现在汇编代码上,是用mov指令实现,如图:
。由于int型变量i是一个32位变量,使用movl传递双字实现。
3.3.5算术操作
每次循环结束的时候都对其进行+1 操作,该操作体现在汇编代码则使用指令add实现,同样,由于变量i为32位,使用指令addl。指令如下:
3.3.6关系操作
hello.c中存在两个关系操作,分别为:
- 条件判断语句if(argc!=5):汇编代码将这条代码翻译为:
使用了cmp指令比较立即数5和参数argc大小,并且设置了条件码。根据条件码,如果不相等则执行该指令后面的语句,否则跳转到.L2。
- 在for循环每次循环结束要判断一次i<10,判断循环条件被翻译为:
同(1),设置条件码,并通过条件码判断跳转到什么位置。
3.3.7控制转移指令
设置过条件码后,通过条件码进行控制转移,在本程序中存在两个控制转移:
(1)
判断argc是否为5,如果不为5,则执行if语句,否则执行其他语句,在汇编代码中则表现为如果条件码为1,则跳到.L2,否则执行cmpl指令后的指令。
(2)
在for循环每次结束判断一次i<10,翻译为汇编语言后,通过条件码判断每次循环是否跳转到.L4。而在for循环初始要对i设置为0,如下:
然后直接无条件跳转到.L3循环体。
3.3.8函数操作
- main函数
参数传递:该函数的参数为int argc,,char*argv[]。具体参数传递地址和值都在前面阐述过。
函数调用:通过使用call内部指令调用语句进行函数调用,并且将要调用的函数地址数据写入栈中,然后自动跳转到这个调用函数内部。main函数里调用了printf、exit、sleep函数。
局部变量:使用了局部变量i用于for循环。具体局部变量的地址和值都在前面阐述过。
函数返回:在源代码中最后的返回语句是 return 0,因此在汇编代码中最后是将%eax设置为 0 并返回这一寄存器。汇编代码如下:
图 3.3-8hello.s文件中main函数
- printf函数
参数传递:第一次调用的时候只传入了字符串参数首地址;for 循环中printf函数传入参数argv[1],argv[2],argv[3]。
函数调用:该函数调用了两次。第一次将寄存器%rdi设置为待传递字符串"用法: Hello 学号 姓名 手机号 秒数!\n"的起始地址;第二次将其设置为"Hello %s %s %s\n"的起始地址。具体已在前面讲过。使用寄存器%rsi完成对argv[1]的传递,用%rdx完成对argv[2]的传递,%rcx完成对argv[3]的传递。
具体汇编代码如下:
图 3.3-9hello.s文件中printf函数
- exit函数
参数传递:传入的参数为 1,执行退出命令。
函数调用:当 if 条件满足的时候调用这一函数。
具体汇编代码如下:
- atoi、sleep函数
可见,atoi函数将参数argv[4]放入寄存器%rdi中用作参数传递,简单使用call指令调用。
然后,将转换完成的秒数从%eax传递到%edi中,edi存放sleep的参数,再使用call调用。
- getchar函数
无参数传递,直接使用call调用即可
3.3.9类型转换
atoi函数将字符转换为sleep函数需要的整型参数.
3.4 本章小结
这一章介绍了C编译器如何把hello.i文件转换成hello.s文件的过程,简要说明了编译的含义和功能,演示了编译的指令,并通过分析生成的hello.s文件中的汇编代码,探讨了数据处理,函数调用,赋值、算术、关系等运算以及控制跳转和类型转换等方面,比较了源代码和汇编代码分别是怎样实现这些操作的。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
4.1.1汇编的概念
汇编是指汇编器(as)将包含汇编语言的.s文件翻译为机器语言指令,并把这些指令打包成为一个可重定位目标文件的格式,生成目标文件.o文件。.o文件是一个二进制文件,包含main函数的指令编码。
4.1.2汇编的作用
将汇编代码根据特定的转换规则转换为二进制代码,也就是机器代码,机器代码也是计算机真正能够理解的代码格式。
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
4.2 在Ubuntu下汇编的命令
命令:as hello.s -o hello.o
图 4.2-1在Ubuntu下汇编的命令
4.3 可重定位目标elf格式
在shell中输入readelf -a hello.o > hello.elf 指令获得 hello.o 文件的 ELF 格式:
图 4.3-1在Ubuntu下得到hello.o文件elf格式的命令
4.3.1 ELF头
ELF头(ELF header)以一个l6字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含了帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量。ELF头如下:
图 4.3-2ELF头
4.3.2 节头表
描述了.o 文件中每一个节出现的位置,大小,目标文件中的每一个节都有一个固定大小的条目(entry)。具体内容如下图所示:
图 4.3-3节头表
4.3.3 重定位节
重定位节中包含了在代码中使用的一些外部变量等信息,在链接的时候需要根
据重定位节的信息对这些变量符号进行修改。链接的时候链接器会根据重定位节
的信息对外部变量符号决定选择何种方法计算正确的地址,通过偏移量等信息计
算出正确的地址。 本程序需要重定位的信息有:.rodata 中的模式串,puts,exit,printf,sleep,getchar等。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改,而调用本地函数的指令不需修改。可执行目标文件中不包含重定位信息。具体重定位节的信息如下图所示:
图 4.3-4重定位节
4.3.4 符号表
.symtab 是一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。该符号表不包含局部变量的信息。例如本程序中的 getchar、puts、exit 等函数名都需要在这一部分体现,具体信息如下图所示:
图 4.3-5符号表
4.4 Hello.o的结果解析
4.4.1命令
在shell中输入 objdump -d -r hello.o > hello.asm 指令输出hello.o的反汇编文件,并与第3章的hello.s文件进行对照分析。
图 4.4-1shell下输入命令
4.4.2与hello.s对照分析
可以发现有以下几点不同:
- 增加机器语言
每一条指令增加了一个十六进制的表示,即该指令的机器语言。例如,在hello.s中的一个cmpl指令表示为
而在反汇编文件中表示为
- 操作数进制
反汇编文件中的所有操作数都改为十六进制。如1中的例子,立即数由hello.s中的$5变为了$0x5,地址表示也由-20(%rbp)变为-0x14(%rbp)。可见只是进制表示改变,数值未发生改变。
- 分支转移
对于条件跳转,hello.s 给出的是段名字,例如.L2 等来表示跳转的地址,如下:
而反汇编文件中,所有跳转的位置被表示为主函数+段内偏移量这样确定的地址:
- 函数调用
hello.s 中,call 指令后跟的是需要调用的函数的名称,而 hello.o 反汇编代码中 call 指令使用的是 main 函数的相对偏移地址。同时可以发现在hello.o 反汇编代码中调用函数的操作数都为 0,即函数的相对地址为 0,因为再链接生成可执行文件后才会生成其确定的地址,所以这里的相对地址都用 0 代替。
hello.s hello.o的反汇编文件
4.5 本章小结
这一章介绍了汇编的概念和作用。以Ubuntu系统下的hello.s文件为例,说明了如何把它汇编成hello.o文件,并生成ELF格式的可执行文件hello.elf。将可重定位目标文件改为ELF格式观察文件内容,对文件中的每个节进行简单解析。通过分析hello.o的反汇编代码(保存在hello.asm中)和hello.s的区别和相同点,让人清楚地理解了汇编语言到机器语言的转换过程,以及机器为了链接而做的准备工作。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
5.1.1链接的概念
链接是将各种不同文件的代码和数据片段收集并组合成一个单一文件的过
程,这个文件可被加载到内存并执行。链接可以执行与编译时(compile time),也就是在源代码被翻译为机器代码时;也可以执行与加载时(load time),也就是程序被加载器加载到内存并执行时:甚至执行于运行时,也就是由应用程序来执行。
5.1.2链接的作用
在现代系统中,链接由链接器自动执行。链接器在软件开发中起关键作用,它使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。
注意:这儿的链接是指从 hello.o 到hello生成过程。
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
图 5.2-1在Ubuntu下链接的命令
5.3 可执行目标文件hello的格式
命令: readelf -a hello > hello1.elf
5.3.1ELF头
图 5.3-1ELF头
hello1.elf中的ELF头与hello.elf中的ELF头包含的信息种类基本相同,以描述了生成该文件的系统的字的大小和字节顺序的16字节序列Magic开始,剩下的部分包含帮助链接器语法分析和解释目标文件的信息。与hello.elf相比较,hello1.elf中的基本信息未发生改变(如Magic,类别等),而类型发生改变,程序头大小和节头数量增加,并且获得了入口地址。
5.3.2节头表
描述了各个节的大小、偏移量和其他属性。链接器链接时,会将各个文件的相同段合并成一个大段,并且根据这个大段的大小以及偏移量重新设置各个符号的地址。
图 5.3-2节头表
5.3.3程序头
程序头部分是一个结构数组,描述了系统准备程序执行所需的段或其他信息。
图 5.3-3程序头
5.3.4Dynamic section
图 5.3-4Dynamic section
5.3.5 Symbol table
符号表中保存着定位、重定位程序中符号定义和引用的信息,所有重定位需要引用的符号都在其中声明。
图 5.3-5符号表
5.4 hello的虚拟地址空间
使用edb加载hello,我们在终端中输入edb并回车,打开edb界面,在File-Open中选择hello文件,点击open,界面显示如下:
图 5.4-1edb界面
可以在 edb 的 Data Dump 窗口看到 hello 的虚拟地址空间分配的情况,具体内容截图如下:
图 5.4-2data dump
可以发现这一段程序的地址是从 0x401000 开始的,并且该处有 ELF 的标识,可以判断从可执行文件时加载的信息。接下来可以分析其中的一些具体的内容。
由5.3可知,.init段的起始地址是0x401000.
使用edb查询可得到以下结果:
同样的,可以找到.text节
5.5 链接的重定位过程分析
在Shell中使用命令objdump -d -r hello > hello1.asm生成反汇编文件hello1.asm
图 5.5-1shell下的命令
5.5.1分析hello与hello.o的不同
- 链接后代码中函数数量增加
链接后的反汇编文件hello1.asm中,多出了.plt,puts@plt,printf@plt,getchar@plt,exit@plt,atoi@plt,sleep@plt等函数的代码。同时每一个函数都有了相应的虚拟地址。这是因为动态链接器将共享库中hello.c用到的函数加入可执行文件中。
图 5.5-2链接后代码中增加的函数
- 全局变量的引用
由于 hello.o 中还未对全局变量进行定位,因此 hello.o 中用 0 加上%rip 的值来表示全局变量的位置,而在 hello 中,由于已经进行了定位,因此全局变量的的值使用一个确切的值加上%rip 表示全局变量的位置。
3.hello 中无 hello.o 中的重定位条目,并且跳转和函数调用的地址在 hello 中都变成了虚拟内存地址。这是由于 hello.o 中对于函数还未进行定位,只是在.rel.text 中添加了重定位条目,而 hello 进行定位之后自然不需要重定位条目。
4.地址访问:在链接完成之后,hello 中的所有对于地址的访问或是引用都调用的是虚拟地址。例如下图中116行条件跳转代码所示:
5.5.2链接的过程。
链接主要分为两个过程:符号解析和重定位。
1.符号解析:目标文件定义和引用符号,符号解析将每个符号引用和一个符号定义关联起来。
2.重定位:
(1)重定位节和符号定义。在这一步中,链接器将所有相同类型的节合并为同一类型的聚合节。然后链接器将运行时的内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。至此程序中每条指令和全局变量都有唯一的运行内存地址。
(2)重定位节中的符号引用。这一步中链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。要执行这一步,链接器依赖于可重定位目标模块中称为重定位条目的数据结构。
(3)重定位过程地址计算方法如下:
5.6 hello的执行流程
通过edb的调试,一步一步地记录下call命令进入的函数。
图 5.6-1edb界面
所有过程:
(I)开始执行:_start、_libe_start_main
(2)执行main:_main、printf、_exit、_sleep、getchar
(3)退出:exit
子程序名或程序地址:
程序名 | 程序地址 |
_start | 0x4010f0 |
_libc_start_main | 0x2f12271d |
main | 0x401125 |
_printf | 0x4010a0 |
_sleep | 0x4010e0 |
_getchar | 0x4010b0 |
_exit | 0x4010d0 |
5.7 Hello的动态链接分析
当程序调用一个由共享库定义的函数时,由于编译器无法预测这时候函数的地址是什么,即共享库中的函数地址在编译时未知,因此这时编译系统提供了延迟绑定的方法,将函数地址的解析推迟到程序第一次调用该函数时进行。这不仅减少了程序启动时的开销,还允许共享库在加载时的地址空间布局更加灵活。
延迟绑定是通过GOT(Global Offset Table,全局偏移表)和PLT(Procedure Linkage Table,过程链接表)实现的,用gdb调试hello程序,输入命令info files,得到GOT起始表位置为:0x404000:
图 5.7-1.got
延迟绑定的详细过程:GOT用于存储动态链接过程中需要重定位的全局数据和函数地址。PLT包含指向共享库函数的间接跳转代码,每个共享库函数在PLT中都有一个对应的入口。当程序第一次调用一个由共享库定义的函数时,程序代码调用PLT中的一个入口,初始时,PLT入口会跳转到一个公共的“PLT0”段,而不是直接跳转到调用函数的实际地址。 PLT0段代码会调用动态链接器(ld.so 或 ld-linux.so)的解析函数,并传递函数名和调用的PLT入口索引。动态链接器通过GOT中的数据来确定调用的函数。动态链接器查找共享库符号表,找到该函数的实际地址。动态链接器将实际地址填入GOT中对应的位置。因此,GOT中的条目更新为这个函数的实际地址。PLT入口修改为直接跳转到该函数的实际地址,以便后续调用时无需再次解析。
由于GOT和PLT已经被更新,后续再对这个函数的调用将直接通过PLT跳转到实际地址,无需再次经过动态链接器。
举例:调用dl_init函数的前后变化
GOT表位置在调用dl_init之前0x404008后的16个字节均为0 | 调用了dl_init之后字节改变了 |
5.8 本章小结
本章首先阐述了链接的基本概念和作用,展示了使用命令链接生成hello可执行文件,观察了hello文件ELF格式下的内容,利用edb观察了hello文件的虚拟地址空间使用情况,最后以hello程序为例对重定位过程、执行过程和动态链接进行分析。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
6.1.1进程的概念
进程的经典定义就是一个执行中程序的实例。
6.1.2进程的作用
进程提供给应用程序两个关键抽象:①一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器②一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用内存系统。进程作为一个执行中程序的实例,系统中每个程序都运行在某个进程的上下文中。
6.2 简述壳Shell-bash的作用与处理流程
6.2.1 Shell-bash的作用
Shell 是一种应用程序,这个应用程序提供了一个用户界面,用户通过这个界面访问操作系统内核的服务。它接受用户输入的命令,并调度相应的应用程序。
6.2.2 Shell-bash的处理流程
1.显示提示符:Shell 显示提示符,等待用户输入命令。
2.读取输入:用户输入命令后,Shell 读取输入内容。
3.解析输入:Shell 对输入的命令进行解析,分割成命令和参数。例如,ls -l /home 被解析为命令 ls 和参数 -l 以及 /home。
4.查找命令:Shell 查找命令的可执行文件。它会在 PATH 环境变量指定的目录中搜索。
5.执行命令:如果命令是 Shell 内部命令,Shell 会直接执行。如果命令是外部可执行文件,Shell 创建一个子进程,并在子进程中执行该命令。
6.等待命令完成:如果命令在前台运行,Shell 会等待命令完成。如果命令在后台运行,Shell 不等待命令完成,继续处理其他命令。
7.获取命令结果:Shell 获取命令的返回状态和输出结果,并进行处理,例如显示输出结果或错误信息。
8.显示提示符:处理完当前命令后,Shell 再次显示提示符,等待用户输入下一条命令。
6.3 Hello的fork进程创建过程
父进程通过调用 fork 函数创建一个新的运行的子进程。调用 fork 函数后,新创建的子进程几乎但不完全与父进程相同:子进程得到与父进程虚拟地址空间相同的(但是独立的)一份副本,包括代码、数据段、堆、共享库以及用户栈,子进程获得与父进程任何打开文件描述符相同的副本,这意味着当父进程调用 fork 时,子进程可以读写父进程中打开的任何文件。fork 被调用一次,却返回两次,子进程返回 0,父进程返回子进程的 PID。父进程和新创建的子进程之间最大的区别在于它们有不同的 PID。
6.4 Hello的execve过程
exceve 函数在当前进程的上下文中加载并运行一个新程序。exceve 函数加载
并运行可执行目标文件,并带参数列表和环境变量列表。只有当出现错误时,exceve
才会返回到调用程序。所以,与 fork 一次调用返回两次不同,在 exceve 调用一次并从不返回。当加载可执行目标文件后,exceve 调用启动代码,启动代码设置栈,将可执行目标文件中的代码和数据从磁盘复制到内存中,然后通过跳转到程序的第一条指令或入口点来运行该程序,由此将控制传递给新程序的主函数。
6.5 Hello的进程执行
6.5.1进程调度的过程
相关概念:
- 上下文:上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
- 时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。因此,多任务也叫做时间分片。
- 用户模式和内核模式:处理器通常使用某个控制寄存器中的一个模式位来提供这种功能。当设置了模式位时,进程就运行在内核模式里。一个运行在内核模式的进程可以执行指令集中的所有指令且可以访问系统中的任何内存位置。没有设置模式位时,进程就运行在用户模式中。用户模式中的进程不允许执行特权指令,也不能直接引用地址空间中内核区内的代码和数据。
进程调度:
即使在系统中通常有许多其他程序在运行,进程也可以向每个程序提供一种假象,好像它在独占地使用处理器。如果想用调试器单步执行程序,我们会看到一系列的程序计数器(PC)的值,这些值唯一的对应于包含在运行时动态链接到程序的共享对象中的指令。这个 PC 的序列叫做逻辑控制流,或者简称逻辑流。
进程是轮流使用处理器的,每个进程执行它的流的一部分,然后被抢占,然后轮到其他进程。在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行,我们说内核调度了这个进程。在内核调度了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。
6.5.2用户模式和内核模式的转换
运行应用程序代码的进程初始时是在用户模式中的。进程从用户模式变为内核模式的唯一方法是通过诸如中断、故障或者陷入系统调用这样的异常。当异常发生时,控制传递到异常处理程序,处理器将模式从用户模式变为内核模式。
处理程序运行在肉核模式中,当它返回到应用程序代码时,处理器就把模式从内核模式改回到用户模式。上下文切换如下图所示:
图 6.5-1上下文切换
6.6 hello的异常与信号处理
6.6.1异常的分类
类别 | 原因 | 异步/同步 | 返回行为 |
中断 | 来自 I/O 设备的信号 | 异步 | 总是返回到下一条指令 |
陷阱 | 有意的异常 | 同步 | 总是返回到下一条指令 |
故障 | 潜在可恢复的错误 | 同步 | 可能返回到当前指令 |
终止 | 不可恢复的错误 | 同步 | 不会返回 |
6.6.2异常的处理方式
6.6.3运行结果及相关命令
1.正常执行状态:在程序正常运行时,打印10次提示信息,以输入回车为标志结束程序,并回收进程。
2.不停乱按:将屏幕的输入缓存到缓冲区。乱码被认为是命令,不影响当前进程的执行。由下图,仍然打印10次信息。
3.运行时按下Ctrl + C
按下Ctrl + C,Shell进程收到SIGINT信号,Shell结束并回收hello进程。
4.运行时按下Ctrl + Z
按下Ctrl + Z,Shell进程收到SIGSTP信号,Shell显示屏幕提示信息并挂起hello进程。
5.Ctrl + Z挂起hello进程后运行ps 、jobs命令
ps(process status)命令用于显示当前运行的进程的信息。jobs 命令用于显示当前 shell 会话中所有作业的状态信息。作业是指由当前 shell 启动的程序或进程,包括前台和后台运行的程序。
可以发现hello进程确实被挂起而非被回收。
6.pstree命令,显示所有进程的树状结构。
7.kill命令,用于发送信号给一个或多个进程,以控制这些进程的行为。
8.fg命令,用于将后台运行的作业(job)恢复到前台运行。
可以发现,输入fg命令后,hello再从挂起处继续执行,打印剩下的语句。程序仍然可以正常结束,并完成进程回收。
6.7本章小结
本章主要介绍了 hello 可执行文件的执行过程,包括进程创建、加载和终止,
以及通过键盘输入等过程。从创建进程到进程运行并回收进程,这一整个过程中需要各种各样的异常和中断等信息。最后,本章对hello程序可能出现的异常情况,以及运行结果中的各种输入进行了解释和说明。程序的高效运行离不开异常、信号、进程等概念,正是这些机制支持 hello 能够顺利地在计算机上运行。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
7.1.1逻辑地址
逻辑地址是指程序在源代码中使用的地址,也叫相对地址。它由编译器生成的相对于代码段、数据段或堆栈段的偏移量。在源代码中直接使用的变量和函数名实际上是逻辑地址。逻辑地址在编译时生成,并且在程序的不同部分之间是相对的。
例如,变量 i 在编译过程中会被分配一个逻辑地址,该地址在程序段内是一个偏移量。代码中的函数调用 printf、sleep、getchar 等在编译时也会有相应的逻辑地址。
7.1.2线性地址
线性地址是逻辑地址到物理地址变换之间的中间层。通过将逻辑地址转换为段基地址并加上段内偏移量(逻辑地址)得到的地址。在x86架构中,这个过程称为段地址转换。
例如,如果 main 函数在代码段中的逻辑地址是 0x0000,而代码段的基址是 0x08048000,那么 main 函数的线性地址就是 0x08048000 + 0x0000 = 0x08048000。
7.1.3虚拟地址
虚拟地址是操作系统为每个进程提供的地址空间中的地址。在大多数现代操作系统中,虚拟地址和线性地址通常是相同的。
7.1.4物理地址
物理地址是内存硬件实际使用的地址。它是内存芯片中的真实位置。物理地址由操作系统通过页表将虚拟地址翻译而来。
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址由段标识符和段内偏移量组成,段标识符是由一个16位长的字段组成的,也称为段选择符。其中前13位是一个索引号,后3位为一些硬件细节。索引号即是“段描述符”的索引,段描述符具体地址描述了一个段,很多个段描述符就组成了段描述符表。通过段标识符的前13位直接在段描述符表中找到一个具体的段描述符。线性地址为段首地址与逻辑地址中的偏移量组成。其中,段首地址存放在段描述符中。而段描述符存放在描述符表中,也就是 GDT(全局描述符表)或 LDT(局部描述符表)中。
段式管理是一种内存管理技术,它将内存划分成若干段(Segment)。每个段有一个段基址(Base Address)和段界限(Limit)。段式管理允许程序在不同的段之间独立操作,这些段可以用于存储代码、数据和堆栈等。
IA-32处理器由逻辑地址到线性地址的转化如下图:
图 7.2-1IA-32处理器由逻辑地址到线性地址的转化
7.3 Hello的线性地址到物理地址的变换-页式管理
页式管理是一种内存空间存储管理的技术,将内存划分为固定大小的块,称为页(Page)。每个页对应一个物理内存块,称为页框(Frame)。页表用于管理线性地址到物理地址的映射关系。页式管理采用请求调页或预调页技术实现了内外存存储器的统一管理。
下面为页式管理的图示:
图 7.3-1页式管理
7.4 TLB与四级页表支持下的VA到PA的变换
Core i7采用四级页表的层次结构。CPU产生虚拟地址VA,虚拟地址VA传送给MMU,MMU使用VPN高位作为TLBT和TLBI,向TLB中寻找匹配。如果命中,则得到物理地址PA。如果TLB中没有命中,MMU查询页表,CR3确定第一级页表的起始地址,VPN1确定在第一级页表中的偏移量,查询出PTE,以此类推,最终在第四级页表中找到PPN,与VPO组合成物理地址PA,添加到PLT。工作原理如下:
图 7.4-1Core i7的四级页表的层次结构
7.5 三级Cache支持下的物理内存访问
MMU 将物理地址发给 L1 缓存,缓存从物理地址中取出缓存偏移 CO、缓存组索引 CI 以及缓存标记 CT。若缓存中 CI 所指示的组有标记与 CT 匹配的条目且有效位为 1,则检测到一个命中条目,读出在偏移量 CO 处的数据字节,并把它返回给 MMU,随后 MMU 将它传递给 CPU。若不命中,则在下一级 cache 或是主存中寻找需要的内容,储存到上一级 cache 后再一次请求读取。
7.6 hello进程fork时的内存映射
当 fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的 PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_ struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念.
7.7 hello进程execve时的内存映射
假设运行在当前进程中的程序执行了如下的execve调用:
execve("hello",NULL,NULL);
execve函数在当前进程中加载并运行包含在可执行目标文件hello中的程序,用 hello 程序有效地替代了当前程序。加载并运行 hello需要以下几个步骤:
- 删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
- 映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区。bss 区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。
- 映射共享区域。如果hello程序与共享对象(或目标)链接,比如标准C库1ibc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
- 设置程序计数器(PC)。execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的人口点。
下一次调度这个进程时,它将从这个人口点开始执行。Linux将根据需要换入代码和数据页面。下图概括了私有区域的不同映射:
图 7.7-1私有区域的不同映射
7.8 缺页故障与缺页中断处理
页面命中完全是由硬件完成的,而处理缺页是由硬件和操作系统内核协作完成的:
1. 处理器生成一个虚拟地址,并将它传送给 MMU
2. MMU 生成 PTE 地址,并从高速缓存/主存请求得到它
3. 高速缓存/主存向 MMU 返回 PTE
4. PTE 中的有效位是 0,所以 MMU 出发了一次异常,传递 CPU 中的控制
到操作系统内核中的缺页异常处理程序。
5. 缺页处理程序确认出物理内存中的牺牲页,如果这个页已经被修改了,则
把它换到磁盘。
6. 缺页处理程序页面调入新的页面,并更新内存中的 PTE
7. 缺页处理程序返回到原来的进程,再次执行导致缺页的命令。CPU 将引起
缺页的虚拟地址重新发送给 MMU。因为虚拟页面已经换存在物理内存中,
所以就会命中。
缺页的操作图如下:
图 7.8-1缺页的操作图
7.9动态存储分配管理
定义:动态存储分配管理是一种内存管理方法。对内存空间的分配、回收等操作在进程执行过程中进行,以便更好地适应系统的动态需求,提高内存利用率。
分配器的基本风格:
1. 显示分配器:要求应用显示地释放任何已分配的块。
2. 隐式分配器:要求分配器检测一个已分配的块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器。
基本方法与策略
- malloc和free函数
C标准库提供了一个称为 malloc程序包的显式分配器。程序调用malloc,malloc可以通过使用map和munmap函数,显式地分配和释放堆内存,或者还可以使用sbrk函数。
- 隐式空闲链表
任何实际的分配器都需要一些数据结构,允许它来区别块边界,以及区别已分配块和空闲块。大多数分配器将这些信息嵌入块本身。一个简单的方法如下图所示。
图 7.9-1隐式空闲链表图示
带边界标记的隐式空闲链表的每个块是由一个字的头部、有效载荷、可能的额
外填充以及一个字的尾部组成的。当一个应用请求一个 k 字节的块时,分配器搜索空闲链表,查找一个符合大小的空闲块来放置这个请求块。分配器有三种放置策略:首次适配、下一次适配和最佳适配。在释放一个已分配块的时候需要考虑是否能与前后空闲块合并,减少系统中碎片的出现。
3.显式空闲链表
将空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。
例如,堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个pred(前驱)和 succ(后继)指针,如下图所示。
图 7.9-2显式空闲链表图示
7.10本章小结
本章主要介绍了hello程序的存储管理,辨析了逻辑地址、线性地址、虚拟地址和物理地址的关系;又逐步分析了将地址翻译为最终物理地址;更深层次的理解了页表、Cache、内存映射的概念,对fork、execve有了新的理解视角;同时介绍了在发生缺页异常的时候系统将会如何处理这一异常;最后介绍了动态内存管理的基本方法和策略。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件。所有的 I/O 设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入输出都被当做对相应文件的读和写来执行。
设备管理:unix io接口。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。
8.2 简述Unix IO接口及其函数
8.2.1Unix IO接口
1.打开文件:一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个 I/O 设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
2.Linux shell 创建的每个进程开始时都有三个打开的文件:标准输入(文件描
述符 0)、标准输出(描述符为 1),标准出错(描述符为 2)。头文件<unistd.h>定义了常量 STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO,他们可用来代替显式的描述符值。
3.改变当前的文件位置,文件开始位置为文件偏移量,应用程序通过 seek 操作,可设置文件的当前位置为 k。
4.读写文件,读操作:从文件复制 n 个字节到内存,从当前文件位置 k 开始,然后将 k 增加到 k+n;写操作:从内存复制 n 个字节到文件,当前文件位置为 k,然后更新 k。
5.关闭文件:当应用完成对文件的访问后,通知内核关闭这个文件。内核会释放文件打开时创建的数据结构,将描述符恢复到描述符池中。
8.2.2Unix IO函数
(1)打开文件:int open(char *filename, int flags, mode_t mode);
Open 函数将 filename 转换为一个文件描述符,并且返回描述符数字,返回的
描述符总是在进程当中没有打开的最小描述符。Flags 参数指明了进程打算如何访
问这个文件,同时也可以是一个或者更多为掩码的或,为写提供给一些额外的指示。Mode 参数指定了新文件的访问权限位。
(2)关闭文件:int close(int fd);
调用 close 函数,通知内核结束访问一个文件,关闭打开的一个文件。成功返回 0,出错返回-1。
(3)读文件:ssize_t read(int fd, void *buf, size_t n);
调用 read 函数从描述符为 fd 的当前文件位置复制最多 n 个字节到内存位置buf。返回值-1 表示错误,返回值 0 表示 EOF,否则返回值表示的是实际传送的字节数量。
(4)写文件:ssize_t write(int fd, const void *buf, size_t n);
调用从内存位置 buf 复制至多 n 个字节到描述符 fd 的当前文件位置。返回值- 1表示出错,否则,返回值表示内存向文件 fd 输出的字节的数量。
8.3 printf的实现分析
printf 函数:
int printf(const char *fmt, ...)
{
int i;
va_list arg = (va_list)((char *)(&fmt) + 4);
i = vsprintf(buf, fmt, arg);
write(buf, i);
return i;
}
可以发现,在 printf 函数中,参数的传递是通过可变参数列表(varargs)实现的,即 printf 函数的第一个参数是格式化字符串,后续参数是根据格式化字符串中的格式说明符动态确定的。同时在printf 内部调用了两个函数,一个是 vsprintf,一个是 write。
((char *)(&fmt) + 4)表示的是...中的第一个参数的地址。
下一句 i = vsprintf(buf, fmt, arg);返回的是要打印出来的字符串的长度
int vsprintf(char *buf, const char *fmt, va_list args)
{
char *p;
chartmp[256];
va_listp_next_arg = args;
for (p = buf; *fmt; fmt++)
{
if (*fmt != '%')
{
*p++ = *fmt;
continue;
}
fmt++;
switch (*fmt)
{
case 'x':
itoa(tmp, *((int *)p_next_arg));
strcpy(p, tmp);
p_next_arg += 4;
p += strlen(tmp);
break;
case 's':
break;
default:
break;
}
return (p - buf);
}
}
Printf 执行流程:
vsprintf接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出,之后存入 buf,然后返回格式化数组的长度。write 函数将 buf 中的 i 个元素写到终端。从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等。字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
getchar是读入函数的一种。它从标准输入里读取下一个字符,相当于getc(stdin)。返回类型为int型,为用户输入的ASCII码或EOF。getchar 可用宏实
现:#define getchar() getc(stdin)。getchar 有一个 int 型的返回值。当程序调用 getchar 时.程序就等着用户按键。用户输入的字符被存放在键盘缓冲区中。直到用户按回车为止(回车字符也放在缓冲区中)。当用户键入回车之后,getchar 才开始从 stdin 流中每次读入一个字符。getchar 函数的返回值是用户输入的字符的 ASCII 码,若文件结尾(End-Of-File)则返回-1(EOF),且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完后,才等待用户按键。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章主要介绍了 linux 系统中的 I/O 设备基本概念和管理方法,同时简单介绍了 printf 和 getchar 函数的实现。
(第8章1分)
结论
hello所经历的过程:
- 预处理:hello.c源程序经过预处理器(cpp)处理生成一个 hello.i 文件,也就是修改了的源程序。
- 编译:由编译器(ccl)将文本文件hello.i翻译成文本文件hello.s
- 汇编:汇编器(as)将hello.s翻译成机器语言指令,将其打包成可重定位目标程序的格式并保存在二进制文件hello.o中。
- 链接:链接器(ld)将hello.o文件和可重定位目标文件和动态链接库链接起来,生成可执行目标程序 hello
- 运行:在shell中输入./hello 2022111743 ljm 18704630403 1
- 创建子进程:终端判断输入的指令不是shell内置指令,于是调用fork函数创建一个新的子进程。
- 加载程序:shell调用execve函数,启动加载器,映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入main函数。
- 执行指令:CPU为进程分配时间片,在一个时间片中,hello享有CPU资源,顺序执行自己的控制逻辑流。
- 访问内存:MMU将程序中使用的虚拟内存地址通过页表映射成物理地址。
- 信号管理:当程序在运行的时候我们输入Ctrl+c,内核会发送SIGINT信号给进程并终止前台作业。当输入Ctrl+z时,内核会发送SIGTSTP信号给进程,并将前台作业停止挂起。
- I/O 设备,在 hello 程序中存在输入与输出,这些部分与 printf,getchar 函
数有关,这些函数与 linux 系统的 I/O 设备密切相关。
- 终止:当子进程执行完成时,内核安排父进程回收子进程,将子进程的退出状态传递给父进程。内核删除为这个进程创建的所有数据结构。
这样,hello 就结束了它的一生。
感悟:
通过本次大作业,我对计算机系统这一门课有了更深入的理解,并且将每一章的内容串联起来。从前我只知道写了代码再按‘run’程序就会运行,而现在我明白一个程序是如何一步步从源代码到最终运行结束,这背后复杂又精妙的软硬件配合,不禁让我感慨计算机的奇妙,它内部软硬件结构的有趣!
(结论0分,缺失 -1分,根据内容酌情加分)
附件
列出所有的中间产物的文件名,并予以说明起作用。
中间结果文件名称 | 文件作用 |
hello.c | 源程序 |
hello.i | 预处理后的文件 |
hello.s | 汇编程序 |
hello.o | 可重定位目标文件 |
hello | 可执行目标程序 |
hello.elf | hello.o的elf格式文件 |
hello.asm | 反汇编hello.o得到的反汇编文件 |
hello1.elf | hello的elf格式文件 |
hello1.asm | 反汇编hello得到的反汇编文件 |
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] Randal E.Bryant David R.O'Hallaron.深入理解计算机系统(第三版).机械工业出版社,2016.
[2]CSDN 博客 ELF 可重定位目标文件格式
[3]Shell 教程 | 菜鸟教程 (runoob.com)
[4]逻辑地址、虚拟地址、线性地址、物理地址及其相互关系 - CSDN文库
[5][转]printf 函数实现的深入剖析 - Pianistx - 博客园 (cnblogs.com)
[6]getchar(计算机语言函数)_百度百科 (baidu.com)
(参考文献0分,缺失 -1分)