哈尔滨工业大学计算机系统大作业-程序人生
即从静态的程序文件转变为在计算机中运行的活动实体。这个转变包括了操作系统为程序分配资源、加载程序到内存中、执行程序指令、处理输入输出等一系列过程。对于hello.c,它需要经历预处理,编译,汇编,链接,最后得到可执行文件。020:即。以hello.c为例子,最初内存没有hello文件相关内容,这表明它尚未启动或执行;之后shell用execve函数启动hello程序,将虚拟内存对应到物理内存中。之
计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 未来技术学院
学 号 2022112415
班 级 22WL022
学 生 张浩天
指 导 教 师 刘宏伟
计算机科学与技术学院
2024年5月
本文主要从hello.c的C语言程序出发,解释了C语言程序如何从源代码,经历预处理、编译、汇编、执行等操作,历经整个“生命”周期,最后在终端上展示出我们想要的文本的过程。本文详细分析了计算机在预处理、编译、汇编、链接、进程管理、内存管理等方面做了哪些操作,保证程序可以正常、稳定且高效的执行。本文充分将理论与实践相结合,在理论分析的基础上,基于Ubuntu系统演示了hello.c的实际表现,并与理论结果对比评价,从而更加深入且充分的剖析了计算机系统的工作流程和原理,深入分析计算机系统架构的优点和其设计理念,实现了走出课本,结合时间,融会贯通。
关键词:计算机系统;生命周期;计算机系统架构;Ubuntu
目 录
第1章 概述
1.1 Hello简介
P2P:即From Program to Process。Hello.c就是一个Program,通过P2P这一过程,变成了运行时进程Process。这一过程强调了由程序到进程的转变,即从静态的程序文件转变为在计算机中运行的活动实体。这个转变包括了操作系统为程序分配资源、加载程序到内存中、执行程序指令、处理输入输出等一系列过程。对于hello.c,它需要经历预处理,编译,汇编,链接,最后得到可执行文件。
020:即From Zero-0 to Zero-0。以hello.c为例子,最初内存没有hello文件相关内容,这表明它尚未启动或执行;之后shell用execve函数启动hello程序,将虚拟内存对应到物理内存中。之后计算机从程序入口开始加载与运行。对hello而言,就是从main函数开始运行。之后控制流就进入main函数,程序就开始执行。在程序运行结束后,shell父进程就会回收hell进程,内核删除hello文件相关的数据结构,释放其占用的资源,包括内存、文件描述符等。
1.2 环境与工具
硬件环境
Amd Ryzen 5 5600 4.16Ghz RAM 16.0GB 系统:x86 64位操作系统
软件环境
Windo11 vmware ubuntu20.04LTS
开发工具与调试环境
Vscode Visual Studio2022 64位;vim objump edb gcc等
1.3 中间结果
|
C语言源文件 |
|
|
hello.i |
预处理后的文件 |
|
hello.s |
编译产生的文件 |
|
hello.o |
汇编产生的可重定位文件 |
|
hello.elf |
ELF格式信息 |
|
hello.asm |
反汇编hello.o得到的反汇编文件 |
|
hello1.asm |
反汇编hello得到的反汇编文件 |
|
hello |
可执行文件 |
1.4 本章小结
本章主要介绍了hello.c的P2P和020流程,围绕hello.c的程序流程进行了一次简单的分析。然后详细的介绍了开发环境以及各个中间结果及其功能。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理是编译过程中的第一个阶段,其主要作用是对源代码进行预处理,生成经过处理后的源代码,以供编译器进一步处理。预处理器是编译器的一部分,它根据预处理指令来操作源代码。
其主要作用包括:宏定义、包含头文件、进行条件编译、移除注释、字符串链接等,以生成最终的编译代码,从而使得源代码更具可读性、可维护性,并且能够根据需要进行灵活地控制编译过程。
2.2在Ubuntu下预处理的命令
在ubuntu环境下,利用gcc工具进行预处理。命令为gcc -E hello.c -o hello,i。

2.3 Hello的预处理结果解析

在上一步中我们通过gcc工具得到了hello.i。用文本编辑器打开它,得到下面这一个图
这个文件还有上千行,图片展示的是其中最前面的一页
仔细翻查文件,可以看到类似宏定义的内容:

包含头文件的语句:

回去翻看hello.c的原程序,可以看到我们确实引用了stdio.h这一头文件。这一头文件是标准输入输出库的头文件。文件在预处理的时候,不断地去“/usr/include/...”这一路径下找各种各样的头文件进行包含,但可以看出,预处理没有对头文件进行修改,而是简单的进行复制替换(typedef __gnuc_va_list va_list;等语句)。同时一些行号和条件编译语句被解释了出来。
同时可以发现我们的注释消失了。这说明预处理会将我们的注释删除掉。

2.4 本章小结
本章主要总结了linux环境下,利用gcc工具对hello.c程序进行预处理,并且对预处理的程序进行了简单的分析。通过分析,我们可以发现预处理修改了.c的内容,将其大规模的进行了扩展,进行了大量的宏定义和头文件引入,同时我们写的注释也被删除了,而部分条件编译指令被添加进来。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译是指将高级语言源代码翻译为目标机器能够执行的低级语言的过程。在编译的过程中,源代码经过预处理、词法分析、语法分析、语义分析、优化和代码生成等多个阶段的处理,最终生成与特定机器体系结构兼容的目标代码。这个目标代码可以是汇编语言程序(.s 文件)或者机器语言程序(二进制文件),可以由汇编器进一步转换为可执行的二进制文件。
编译有以下的几个作用:
1)将高级语言转换为低级语言:编译器将高级语言源代码转换为低级语言(通常是汇编语言或机器语言),使得计算机能够理解和执行这些代码。
2)优化程序性能:编译器在生成目标代码的过程中进行各种优化,包括但不限于代码优化、内存优化、寄存器分配优化等,以提高程序的性能和效率。
3)检查语法和语义错误:编译器在编译过程中会对源代码进行语法和语义分析,检测其中的错误和不一致之处,并给出相应的错误提示。
3.2 在Ubuntu下编译的命令
利用gcc工具,命令为:gcc -S hello.i -o hello.s

3.3 Hello的编译结果解析
3.3.1 文件头部
这一部分主要把整个编译文件的一些基本信息交代了一下。
.file "hello.c":指定源文件名为"hello.c"。
.text:指示后续部分是程序代码段。
.section .rodata:定义只读数据段(只读常量)。
.align 8:将后续数据对齐到8字节边界。
3.3.2 字符串常量
.LC0:包含一个多字节字符序列的字符串常量(可能是UTF-8编码的汉字)。
.LC1:包含一个格式化字符串,用于printf调用。
3.3.3主程序入口

.globl main:声明main是一个全局函数。
.type main, @function:声明main的类型为函数。
.LFB6和.cfi_startproc:调试信息和栈帧布局的开始标记。
3.3.4 初始化与检查

endbr64:用于控制流保护(CFI),新CPU的指令。
pushq %rbp:保存旧的基址指针。
movq %rsp, %rbp:设置新的基址指针。
subq $32, %rsp:为局部变量分配32字节的栈空间。
movl %edi, -20(%rbp) 和 movq %rsi, -32(%rbp):保存传入的参数(argc 和 argv)。

cmpl $5, -20(%rbp):检查传入参数是否为5(即argc == 5)。
je .L2:如果argc == 5,跳转到.L2。
否则,加载.LC0字符串并调用puts打印它,然后调用exit终止程序。
3.3.5 主循环

这一步主要进行的是初始化循环计数器,并且进入循环

-32(%rbp):argv基地址。
addq $24, %rax:取argv[3](字符串)。
将字符串保存到%rcx。

取argv[2](字符串)。保存到%rdx。

movq -32(%rbp), %rax:再次将argv数组的基地址加载到%rax。
addq $8, %rax:将基地址加上8字节,指向argv[1]。
movq (%rax), %rax:将argv[1]的值加载到%rax。
movq %rax, %rsi:将argv[1]的值存储到%rsi,rsi是printf的第二个参数。

leaq .LC1(%rip), %rdi:将格式化字符串的地址加载到%rdi,rdi是printf的第一个参数。
movl $0, %eax:将%eax清零,表示没有浮点数参数传递给printf。
call printf@PLT:调用printf函数,输出格式化字符串。

movq -32(%rbp), %rax:再次将argv数组的基地址加载到%rax。
addq $32, %rax:将基地址加上32字节,指向argv[4]。
movq (%rax), %rax:将argv[4]的值加载到%rax。
movq %rax, %rdi:将argv[4]的值存储到%rdi,rdi是atoi的第一个参数。
call atoi@PLT:调用atoi函数,将字符串转换为整数。
movl %eax, %edi:将转换后的整数结果存储到%edi,edi是sleep的参数。
call sleep@PLT:调用sleep函数,暂停程序执行指定的秒数。

addl $1, -4(%rbp):将局部变量-4(%rbp)的值加1,更新循环计数器。

这一步主要是为了检查是否小于等于9,若是就要继续循环
3.3.6 函数尾部:结束程序

调用getchar等待用户输入。
返回0,清理栈帧并返回。
3.3.7 调试信息及其他

.size main, .-main:计算main函数的大小。
.ident:编译器版本信息。
.section .note.GNU-stack和其他:用于段属性和GNU堆栈保护。
3.4 本章小结
这一章通过在Linux下编译C语言程序hello.c,获取到汇编文件。简要说明了编译的含义和功能,详细的分析了hello.s这一汇编文件,研究了汇编语言是如何组织数据,编排函数调用,解释一系列算术、赋值等操作的,并感受了跳转和循环在汇编语言中的实现方式。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
4.1.1汇编的概念
汇编是指汇编器(as)将包含汇编语言的 .s 文件翻译为机器语言指令,并将这些指令打包成一个可重定位目标文件的格式,从而生成目标文件 .o 文件。.o 文件是一个二进制文件,包含程序的指令编码和相关的元数据,例如符号表和重定位信息。
4.1.2 汇编的作用
汇编的作用是将高级语言转化为机器能够直接执行的代码文件。具体来说,汇编器将 .s 文件中的汇编语言程序翻译成机器语言指令,并将这些指令打包成可重定位目标文件的格式。生成的 .o 文件是一个二进制文件,包含程序的指令编码以及重定位和调试信息,为链接器进一步处理做准备。
4.2 在Ubuntu下汇编的命令
gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o

4.3 可重定位目标elf格式
在shell中输入readelf -a hello.o > hello.elf 指令获得 hello.o 文件的 ELF 格式:

4.3.1 ELF格式文件
ELF(Executable and Linkable Format)是Unix系统中的标准文件格式,用于可执行文件、目标代码和共享库文件。一个典型的ELF可重定位目标文件的结构包括以下部分
- ELF头:文件的第一个部分,描述了文件的基本属性,如文件类型、机器架构、入口点地址等
- 程序头表:描述了如何创建进程映像的表格,但在可重定位文件中通常不存在
- 节头表:描述文件中所有节(section)的表格
- 各个节(Sections):
.text:代码段,包含程序的指令。
.data:数据段,包含初始化的数据。
.bss:未初始化数据段,包含未初始化的全局变量。
.rodata:只读数据段,包含只读数据。
.symtab:符号表段,包含程序中所有符号的信息。
.strtab:字符串表段,包含符号名和其他字符串。
.rel.text/.rela.text:重定位表段,包含对.text段的重定位信息。
.shstrtab:节名称字符串表段,包含节的名称。
4.3.2 hello.elf分析
1.ELF头

该ELF文件是一个64位的小端序的可重定位目标文件(REL),适用于x86-64架构。文件的节头表在文件偏移1264字节处,文件中没有程序头。
2.节头表
节头表描述了每个节的名称、类型、大小、地址、偏移等信息。主要节如下:
.text:包含程序的机器指令,大小为0x9d字节。
.data:数据段,当前为空。
.bss:未初始化数据段。
.rodata:只读数据段,大小为0x40字节。
.symtab:符号表,包含符号信息。
.strtab:字符串表,包含符号名。
.shstrtab:节名称字符串表。
重定位节

.rela.text:包含8个重定位条目,类型包括R_X86_64_PC32和R_X86_64_PLT32,分别用于PC相对地址和PLT(过程链接表)重定位。
.rela.eh_frame:包含1个重定位条目。
符号表
包含18个符号条目,包括文件名、各节和全局符号如main函数和外部库函数(如puts、exit、printf等)。
- 其他信息
在这个ELF文件中,除了这些主要组成部分,还有一些其他的信息,比如所有者、x86特性信息(.note.gnu.property中给出,IBT,SHSTK)等
4.4 Hello.o的结果解析
4.4.1 运行指令
(以下格式自行编排,编辑时删除)
在LINUX终端中输入命令:objdump -d -r hello.o > hello.asm,将objdump得到的文件保存到hello.asm中。

4.4.2 汇编语言和机器语言的映射关系分析
汇编语言和机器语言之间存在直接的映射关系。汇编语言是一种人类可读的编程语言,它使用助记符和标签来表示机器指令和操作数。而机器语言则是计算机实际执行的二进制代码。我们可以通过比较汇编代码和反汇编代码来理解这种映射关系。以下是对提供的文件的详细分析。
- 函数的入口与退出
在汇编代码中,我们可以看到main函数的入口和退出是:

而在objdump生成的反汇编代码中,main是这样进入的:

可以发现:反汇编文件中对函数的调用与重定位条目相对应。在可重定位文件中,call指令明确的指向了一个重定位条目指向的信息。
- 变量存取
在汇编代码中有这样一段:


两者表示的含义是一样的——都是表示:
movl %edi, -20(%rbp): 将传入的第一个参数(通常为整数类型)存储在栈帧的 -20(%rbp) 处。
movq %rsi, -32(%rbp): 将传入的第二个参数(通常为指针类型)存储在栈帧的 -32(%rbp) 处。由此可见,两个文件虽然写法不同,但只是进制表示改变,数值未发生改变。
- 循环与分支
汇编代码中,实现循环与分支的方法是通过直接跳转到代码段,比如.L3:


而在反汇编的跳转指令中,实现方法有所不同。所有的跳转指令,都指向了一个由主函数与段内偏移量之和的定位方式,来确定跳转到的具体代码位置,比如:
这里的跳转是跳转到了主函数地址+0x2f的位置,与上面的.L3,.L2的代码段表示不同,但跳转到的实际代码位置相同。
4.5 本章小结
这一章介绍了汇编的含义和作用,尤其是汇编器(as)在编译链中的作用。我们用Ubuntu系统下,将hello.s汇编成hello.o文件,并生成相应的ELF文件(可重定位目标文件),并对其中的每一个节进行了详细的分析和解析。之后我们利用Ubuntu的反汇编工具objdump,获取到了hello.asm反汇编文件,并于hello.s文件进行了对比,分析彼此的异同点,使我更加清晰了了解了汇编语言是如何转换成机器语言的过程,对汇编也有了更进一步的理解。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
5.1.1 链接的概念
链接是指将编译生成的目标文件(如.o文件)与所需的库文件和其他目标文件进行合并,生成一个可执行文件的过程。这个过程主要由链接器(Linker)完成。链接可以分为两种类型:静态链接和动态链接。
静态链接:在生成可执行文件时,将所有用到的库函数和目标文件的代码直接包含在最终的可执行文件中。
动态链接:可执行文件在运行时才加载所需的库文件。这样可执行文件较小,但依赖于运行时环境中的共享库。
5.1.2 链接的作用
链接的作用主要包括以下几个方面:
1.符号解析和重定位:
链接器将不同目标文件中的符号(变量、函数等)进行解析和重定位。它会确定每个符号的最终地址,并修正代码中的符号引用。
比如,函数调用或变量引用在汇编时可能只是一个相对地址,链接器会将其转换为绝对地址或合适的相对地址。
2.合并代码和数据段:
链接器将所有目标文件中的代码段(.text)、数据段(.data)和只读数据段(.rodata)等合并到一个可执行文件中。这种合并有助于内存管理和提高程序的执行效率。
3.解决外部符号:
链接器处理那些在一个目标文件中引用但定义在另一个目标文件中的外部符号。比如,一个目标文件可能引用了另一个目标文件中的函数,链接器会找到这个函数的定义并解析这种引用。
4.生成可执行文件:
链接器将所有处理后的代码和数据写入一个可执行文件(如 ELF 文件),这个文件可以直接被操作系统加载和执行。
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.3 可执行目标文件hello的格式
用readelf等列出其各段的基本信息,得到各段的起始地址,大小等。

5.3.1 ELF头
这次生成的elf文件的elf头与hello.elf的信息基本一致。不同点在于,程序头大小变大,并且获得了入口点地址:0x4010f0
5.3.2 节头
这一部分展示了ELF 文件中各个节(section)的详细信息,包括节的名称、类型、地址、偏移量、大小等。
其中[Nr]: 节编号
Name: 节的名称
Type: 节的类型(如 PROGBITS、NOTE 等)
Address: 节的虚拟地址
Offset: 节在文件中的偏移量
Size: 节的大小
EntSize: 如果节包含固定大小的条目,此值为每个条目的大小
Flags: 节的标志(如 A 表示分配,X 表示可执行)
Link: 相关的节表索引
Info: 额外信息

Align: 节的对齐要求
5.3.3 程序头
与上次不一样的是,这次多了程序头这一部分。在这一部分中描述了如何创建进程映像。每个程序头表项(Program Header Entry)定义了一个段如何在内存中被加载、映射以及相关的属性。

5.3.4 Dynamic Section

在 ELF 文件的动态段(Dynamic Section)中,包含了很多重要的信息,用于支持动态链接和加载。动态段提供了关于如何在运行时解析符号、加载共享库、初始化和终结程序等方面的信息。比如:
依赖库 (NEEDED):指定需要加载的共享库,动态链接器会根据这些信息找到并加载共享库。
初始化和终结函数 (INIT 和 FINI):在程序开始和结束时执行的特殊函数,用于初始化和清理资源。
哈希表和符号表 (HASH, GNU_HASH, STRTAB, SYMTAB):用于符号解析和查找,符号是指函数和全局变量。
重定位表 (PLTRELSZ, PLTREL, JMPREL, RELA, RELASZ, RELAENT):包含重定位信息,用于在加载时和运行时修正地址。
版本需求 (VERNEED, VERNEEDNUM, VERSYM):描述依赖的共享库的版本信息,确保程序使用正确的库版本。
5.3.5 Symbol table

这一部分是符号表,主要储存关于定位、重定位的符号定义、被引用信息等。所有重定位所需要用到的符号都会在这里进行声明。

5.4 hello的虚拟地址空间
在程序头部分,我们可以看到LOAD加载的程序段的地址为0x0000000000400000:

使用edb加载hello,如图所示:

通过调用edb的data dump栏,直接查看分配的虚拟地址。另外调用edb的Memory Regions,从而调出其他内存空间的内容。通过ELF文件的描述,程序从0x400000开始加载到0x401000。在edb中我们可以看到.interp节的内容:

同样的也可以找到.text节的信息如图:

可以发现:我们在ELF中查到的虚拟内存地址和在edb中实际看见的地址是一致的。
5.5 链接的重定位过程分析
5.5.1 分析hello与hello.o区别
在Ubuntu系统下,执行指令objdump -d -r hello > hello1.asm,得到一个新的,链接过的反汇编文件hello1.asm。执行截图如图所示:

打开hello.asm和hello1.asm,可以看出有以下几个不同点:

新增大量新的函数
在链接后的反汇编文件hello1.asm中,新增了许多新的函数,如puts@plt,printf@plt,getchar@plt和atoi@plt等。这些函数是包含在hello.c一开始所引入的头文件中的,出现在hello1.asm的原因是链接器将hello和其他库文件的函数进行了合并。
- 修改了跳转相关的地址
在新生成的反汇编文件中,跳转指令和call指令的指向更加明确。这是因为链接器解析了之前生成的重定位条目,并将其直接修改为目标地址与下一条指令的地址值之差。

5.5.2 重定位过程
重定位(relocation)是计算机程序在运行时将其地址从一个位置调整到另一个位置的过程。在之前的编译阶段,源代码被编译成目标代码(object code),这包括生成机器指令和数据。在这个阶段,编译器并不知道这些指令和数据在内存中的具体位置,因此它使用相对地址或符号来表示这些位置。
在现在的链接阶段中,链接器将多个目标文件和库文件合并成一个可执行文件。在这个过程中,链接器将符号地址解析成相对地址,并且可能对代码和数据进行一些重定位工作。不过,在链接完成后,生成的可执行文件中的地址仍然是相对地址或逻辑地址(逻辑地址是相对于程序的某个起始地址而言)。在这个过程中,链接器完成了以下几个关键任务:
1.符号解析:
链接器需要解析每个目标文件中的符号,这些符号可以是变量名、函数名或其他标识符。外部符号(在一个目标文件中引用,但在另一个目标文件中定义)需要被解析,以确保它们在最终可执行文件中有正确的定义。
2.地址分配:
链接器为每个目标文件中的代码段和数据段分配地址。这些地址是相对于整个程序的起始地址而言的(逻辑地址),而不是最终的物理地址。
3.重定位:
链接器调整目标文件中的相对地址或符号地址,使它们在整个程序的地址空间中正确定位。具体包括修改指令中的地址、数据段中的地址引用等。
重定位的具体步骤又可以分成:
1.输入文件处理:链接器读取所有输入的目标文件和库文件,构建一个符号表(symbol table),记录每个符号的定义和引用。
2.段(Section)合并:链接器将来自不同目标文件的同类型段合并。例如,所有代码段(通常称为 .text 段)会被合并在一起,所有数据段(通常称为 .data 段)会被合并在一起。合并后的段会被分配一个逻辑地址。
3.符号解析和地址分配:链接器解析符号表中的每个符号,将它们的相对地址解析为段内的具体地址。链接器为每个段分配一个起始地址,并更新段内符号的地址。
4.生成重定位项(Relocation Entries):链接器在目标文件中找到所有需要重定位的地方,并生成重定位项。重定位项记录了需要调整的地址、调整的类型(如绝对地址、相对地址等)、和调整所需的信息(如符号名、偏移量等)。
5.执行重定位:根据重定位项,链接器调整代码段和数据段中的地址。例如,如果一个指令引用了一个外部变量,链接器会将这个指令中的地址更新为变量在整个程序中的地址。
5.6 hello的执行流程
5.6.1 运行过程
在edb中,加载hello,并一步一步运行,记录下call命令进入的函数。

记录结果如下:
1)_start --> _libe_start_main
2) main --> printf -->exit -->_sleep -->getchar
3) exit
5.6.2 子程序名或地址
|
子程序名称 |
程序地址 |
|
_start |
0x4010f0 |
|
main |
0x401125 |
|
_printf |
0x4010a0 |
|
_sleep |
0x4010e0 |
|
_getchar |
0x4010b0 |
|
_exit |
0x4010d0 |
5.7 Hello的动态链接分析
在分析程序的动态链接项目时,我们将着眼于它的动态链接库(Dynamic Link Libraries,DLLs)依赖关系。在一个应用了动态链接的程序编译时,编译器无法确定这个被调用的函数实际的运行时地址,于是就会为这个调用生成一条重定位记录,然后在动态链接器对重定位进行解析时,对其再进行分析。
首先我们要分析hello.elf文件中GOT的位置,如图所示地址为0x404000:


而在edb中,我们观察相应的位置GOT,发现在程序还未开始运行时GOT地址的后16个字节均为0:
当程序调用了dl_init后,这些字节发生了改变
在这个过程中,plt存放了一批初始代码,他们跳转到got所示的位置,调用链接器来进行动态链接。当第二次调用plt时,所指向的就是正确的地址。
5.8 本章小结
本章首先介绍了链接的基本概念和作用,尤其着重介绍了链接器的功能和工作步骤。在Ubuntu系统下,利用hello程序,使用指令对其进行了链接,生成可执行文件后,查看了hello文件的ELF格式内容,并与之前没有链接过的elf格式文件寻找了异同点。最后再次利用hello程序,深入了解了重定向和链接环节的紧密关系。通过本章的一系列分析,我对链接器和链接环节的理解更加透彻。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
6.1.1 进程的概念
进程是操作系统中的一个基本概念,它是正在执行的程序的实例。每个进程都有自己的地址空间、代码、数据和运行时状态,同时进程也是操作系统资源分配的基本单位。进程承担着资源管理、并发执行、通信协作、安全隔离和多任务处理等重要角色,是计算机系统中非常重要的组成部分
6.1.2 进程的作用
1.程序的执行实例: 进程是程序在计算机上的执行实例。当一个程序被加载到内存中并开始执行时,操作系统为它创建一个进程。
2.资源分配的基本单位: 操作系统通过进程来管理系统资源的分配,包括内存、CPU 时间、文件和设备等。每个进程都有自己的内存空间,操作系统负责分配和管理这些内存资源,以便进程可以正常运行。
3.并发执行: 现代操作系统可以同时执行多个进程,通过时间片轮转等调度算法,操作系统能够在单个处理器上轮流执行多个进程,实现并发执行。这样可以提高系统的吞吐量和响应性能。
4.进程间通信: 进程之间可以通过各种通信机制进行数据交换和协作。常见的进程间通信方式包括管道、消息队列、共享内存和信号量等。这些通信机制允许不同的进程之间进行数据共享和同步,从而实现更复杂的任务和应用。
5.安全隔离: 每个进程都有自己独立的内存空间和运行环境,这种隔离性可以保证进程之间互不干扰。如果一个进程崩溃或出现错误,通常不会影响其他进程的运行,从而提高了系统的稳定性和安全性。
6.多任务处理: 操作系统可以在同一台计算机上同时运行多个进程,每个进程都可以执行不同的任务。这种多任务处理能力使得计算机可以同时处理多个用户请求或运行多个应用程序,提高了系统的利用率和效率。
6.2 简述壳Shell-bash的作用与处理流程
Shell(壳)是用户与操作系统内核之间的接口,它提供了一种交互式的方式让用户与操作系统进行通信和控制。Bash(Bourne Again Shell)是一种常见的Unix Shell,它是许多Linux发行版和其他Unix系统的默认Shell。
6.2.1 Shell-Bash的作用
1.命令解释与执行:Bash 接受用户输入的命令,并解释执行这些命令。它负责调用合适的系统程序来执行用户输入的命令,例如执行其他程序、处理文件、管理进程等。
2.环境配置:Bash 允许用户设置环境变量、别名、函数等,以定制Shell的行为和环境。
3.脚本编写:Bash 提供了编写Shell脚本的功能,用户可以用Shell脚本来自动化任务、批处理文件、处理数据等。
6.2.2 处理流程
1.提示符显示:Bash 显示一个提示符,等待用户输入命令。
2.命令解析:用户输入的命令被 Bash 解析,分解成命令名、选项、参数等部分。
3.命令执行:Bash 根据解析后的命令调用相应的程序进行执行。如果命令是内置命令(如 cd、echo 等),则Bash会直接执行相应的操作;如果命令是外部命令(如 ls、grep 等),则Bash会在系统的 PATH 路径中查找可执行文件并执行之。
4.输出显示:执行完命令后,Bash 将输出显示在屏幕上供用户查看。
5.等待下一个命令:Bash 再次显示提示符,等待用户输入下一个命令,整个循环过程重新开始。
6.脚本执行:如果用户输入的是一个脚本文件,Bash 会按照脚本文件中的内容逐行解释和执行,直到脚本文件执行完毕。
6.3 Hello的fork进程创建过程
当我想要执行hello这一可执行文件时,我在shell界面键入了如下指令:

此时shell判断该指令不是内置指令,于是默认为可执行文件。父进程调用fork函数,创建了一个新的子进程。该子进程获得的是与父进程虚拟空间相同的副本,包括了堆栈等信息。但是父进程和子进程的PID不同。
6.4 Hello的execve过程
该函数的函数一般原型为:
int execve(const char *filename, char *const argv[], char *const envp[]);
其功能是:
1.将当前进程的内存映像替换为新程序的内存映像,使得当前进程运行新程序。
2.执行新程序,并将其命令行参数传递给新程序。
3.将新程序的环境变量传递给新程序。
当hello运行到execve函数时,execve函数会根据filename中的路径找到需要执行的程序文件。若成功找到文件,函数就会将当前进程的内存映像替换成新程序的内存映像。这意味着新程序的堆栈等信息将会覆盖当前进程的信息。之后新程序将会开始执行,并利用上argv[]和envp[]中的环境变量。
6.5 Hello的进程执行
在执行 hello 程序时,操作系统会创建一个新的进程来运行该程序。这个进程具有自己的地址空间、代码、数据和运行时状态。
6.5.1进程的创建和上下文切换
当用户在终端执行hello程序时,系统首先调用fork函数,创建了一个新的进程。同时,新进程的内存空间、寄存器状态等信息都被初始化为hello程序的初始状态。进程切换回用户模式,开始执行hello代码
6.5.2 时间片与进程调度
当进程被调度执行时,操作系统为期分配了一个时间片,也即一段时间内可以占用CPU的时间。如果在时间片用完之前进程没有执行完毕,操作系统会进行进程切换,将 CPU 分配给其他进程执行。其中,进程调度算法决定了哪个进程能够获得 CPU 的使用权。
6.5.3 用户态和内核态转换
在执行过程中,进程会在用户态和核心态之间切换,一般来说内核的权限比用户要高。当进程执行系统调用或发生异常时,会发生从用户态到核心态的转换。在核心态下,进程可以执行特权指令并访问系统资源。例如,当进程调用 printf() 输出信息时,会发生一次用户态到核心态的转换,操作系统会处理输出操作,然后将控制权返回给用户态的进程
6.5.4 系统调用执行
当 hello 程序调用 printf() 函数输出信息时,会触发系统调用。操作系统会根据系统调用号识别调用的系统调用,并执行相应的内核代码来完成请求的操作。在执行完系统调用后,操作系统将结果返回给用户进程,然后进程继续执行。
6.5.5 进程执行完毕
当 hello 程序执行完毕时,进程将终止。操作系统会回收进程占用的资源,并向父进程发送相应的信号,通知其子进程的终止状态。
6.6 hello的异常与信号处理
6.6.1 异常以及处理方式
|
种类 |
产生原因 |
同异步 |
返回状态 |
|
中断 |
处理器外部IO信号 |
异步 |
总是返回到下一条指令 |
|
陷阱 |
有意的异常 |
同步 |
总是返回到下一条指令 |
|
故障 |
潜在可恢复的错误 |
同步 |
可能返回到当前指令 |
|
终止 |
不可恢复的错误 |
同步 |
不会返回 |
- 中断处理方式
中断是由于处理器外部硬件产生的。一般会安排指定的中断处理程序。如图进行处理,处理后继续执行下一条指令,就像是没发生过一样。
- 陷阱处理方式
陷阱是有意留下的异常,最常见的应用就是在用户程序和内核之间留一个接口,即系统调用。
- 故障处理方式
故障由错误引起,它有可能可以被故障处理程序所修正,从而能够继续执行当前这一条指令。否则,处理程序就会返回到内核模式的abort函数,abort函数会终止引起故障的应用程序。
- 终止处理方式
终止是由于不可恢复的致命错误导致的,通常是一些硬件错误。当终止发生时,系统将直接返回到一个abort;例程中,不会再返回到当前或者下一条指令。
6.6.2 hello程序运行结果
1.正常状态
在程序正常运行时,打印10次提示信息,以输入回车为标志结束程序,并回收进程。

2.运行时键入Ctrl+C
键入Ctrl+C后,shell收到SIGINT信号,终止并回收hello进程

3.运行时键入Ctrl+Z

Shell收到SIGSTP信号,打印提示信息并挂起hello进程
用ps,pstree或jobs命令,确定hello进程确实是被挂起,而非被回收。

可以看到job代号为1,状态确实是已停止状态。


4.kill杀死命令

键入kill -9 %1,终止进程,用ps查看确认已被杀死

5.输入fg 1则命令将hello进程再次调到前台执行
发现键入fg 1命令后,已经被停止的任务成功被调回前台继续运行,并且可以正常运行结束,收回进程。
6.随意键入
随意键入任意键的话,shell会将这些文本缓存到stdin中。并且会出现在该进程结束后,作为新的一行命令出现。
(注:后面的命令是因为在键入时按下回车键。当getchar读取到回车时就是认为是一个待执行的命令,当本次进程结束并被成功收回以后就是直接执行这些命令,即便他们没有什么具体意义)
6.7本章小结
本章主要是通过理论和实践相结合的方式,对计算机系统中的shell有了一个更加深刻的认识。首先通过运行hello程序,引出了进程的概念和作用,分析了shell-bash的作用和处理流程,并且从头分析了hello程序从创建到运行到回收整个生命周期,系统是如何分配空间和资源的。最后,利用hello程序分析了可能出现的异常情况,并用实际操作进行了模拟。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
7.1.1 逻辑地址
逻辑地址是程序员在编写代码时使用的地址。它是相对于程序的某个基地址的偏移量。逻辑地址由编译器和链接器生成,独立于实际的物理内存。
例如hello程序中的循环体有变量i,编译器会生成相应的逻辑地址,用于指向和表示i的位置。但在程序运行时,这个地址会被转换成实际的内存地址。
7.1.2 线性地址
线性地址是逻辑地址到物理地址变换之间的一步,是由 CPU 生成的地址,用于访问内存。它是逻辑地址经过段选择器转换后的地址。在分段机制中,逻辑地址被段寄存器和段偏移量组合形成线性地址。
例如,当 hello 程序访问变量i时,CPU 会根据段寄存器和逻辑地址计算出线性地址。这种机制允许程序拥有自己的地址空间而不与其他程序冲突。
7.1.3 虚拟地址
虚拟地址是在现代操作系统中使用的地址,它是线性地址经过页表转换后的地址。虚拟地址空间是进程独享的,操作系统通过虚拟内存机制将虚拟地址映射到物理内存地址。
例如:hello 程序中的变量i所对应的虚拟地址可能是 0x8048000,这个地址是程序独享的,不会与其他进程冲突。操作系统将该虚拟地址映射到实际的物理内存。虚拟地址在之前ELF格式文件中可以看到
7.1.4 物理地址
物理地址是内存芯片上的实际地址,表示数据在物理内存中的位置。操作系统和内存管理单元(MMU)负责将虚拟地址转换为物理地址。
例如:当 hello 程序访问变量i时,虚拟地址 0x8048000 可能被映射到物理内存中的地址 0x1A2B3000。这个映射过程由操作系统和硬件共同完成。

在前几章中出现过这种映射关系,比如下图中就明确指出了物理地址和虚拟地址之间的映射关系:
7.2 Intel逻辑地址到线性地址的变换-段式管理
在Intel架构的计算机系统中,段式管理(Segmentation)是一种内存管理机制,用于将逻辑地址转换为线性地址。逻辑地址由段选择子和段内偏移量组成,而线性地址是单一的地址空间中的一个地址。在段式管理中,段选择子和段描述符共同决定了段的基地址,然后通过基地址和段内偏移量计算出线性地址。
- 段选择子:段选择子指示使用哪个段
- 段内偏移量:段内偏移量是相对于段基地址的地址
7.2.1 段选择子结构
1.段选择子包含以下几个部分:
2.索引(Index):段描述符表中的一个索引,用于定位具体的段描述符。
3.TI(Table Indicator):指示使用的是GDT(全局描述符表)还是LDT(局部描述符表)。
4.RPL(Requestor Privilege Level):请求者特权级,指示当前选择子的特权级。
7.2.2 段描述符
段描述符是一个包含段基地址、段限制、段类型和其他属性的结构。段描述符通常存储在GDT或LDT中。
1.段基地址(Base Address):段在内存中的起始地址。
2.段限制(Limit):段的大小。
3.段类型和属性:段的类型(代码段、数据段等)和其他属性(可用位、粒度等)。
7.2.3逻辑地址到线性地址的转换过程
1.获取段选择子:从逻辑地址中提取段选择子。
2.确定段描述符表:根据段选择子中的TI位,确定使用GDT还是LDT。
3.查找段描述符:在相应的描述符表中,根据段选择子中的索引查找段描述符。
4.获取段基地址:从段描述符中提取段的基地址。
5.计算线性地址:将段基地址与段内偏移量相加,得到线性地址。
段式管理示例图:

7.3 Hello的线性地址到物理地址的变换-页式管理
页式管理(Paging)是一种内存管理机制,用于将线性地址转换为物理地址。页式管理通过页表(Page Table)实现这种映射,使得每个进程能够拥有独立的虚拟地址空间。页表相当于一个映射数组,名为页表条目(PTE),每个PTE都由一个有效位和一个位地址字段组成。如下图所示:

页式管理有许多优点:
1.内存保护:每个进程有独立的页表,进程之间的地址空间彼此隔离。
2.内存共享:多个进程可以共享相同的物理内存页,例如共享库。
3.内存利用率:可以通过页交换机制将不常用的页换出到磁盘,腾出内存供其他进程使用。
在 hello 程序的执行过程中,线性地址经过页目录表和页表的多级映射,最终转换为实际的物理地址。这种机制确保了每个进程的地址空间独立,提高了系统的稳定性和安全性。
7.4 TLB与四级页表支持下的VA到PA的变换
在Intel x86-64架构的计算机系统中,使用四级页表和转换后备缓冲(TLB,Translation Lookaside Buffer)进行虚拟地址(VA,Virtual Address)到物理地址(PA,Physical Address)的转换。
首先CPU会产生虚拟内存地址VA,MMU使用VPN高位向TLB中寻找匹配。若命中,则得到物理地址PA。如果PLB没有命中,MMU会查询页表,由CR3确定一级页表起始地址,VPN1确定偏移量,从而锁定PTE。以此类推,在第四级页表中找到PPN,与VPO组合成物理地址PA。
7.5 三级Cache支持下的物理内存访问
当CPU要访问某个数据时,数据可能需要在缓存层级中或需要从主存加载。访问过程如下:
- CPU生成物理地址
- L1查找
- 若L1命中,则直接返回数据,若未命中,则进入L2查找
- 以此类推,接下来是L3,之后是主存。
考虑到实际高速缓存的物理特性,常常被划分成组、行和块,高速缓存的结构将m个地址位划分成了t个标记位,s个组索引位和b个块偏移位。
访问速度很大关系上取决于缓存命中率。如果选中的组中存在一行有效位为1,则有一个缓存命中,则可以直接返回数据,否则需要去下一层存储空间中寻找。
7.6 hello进程fork时的内存映射
如前几章所说,fork函数被调用,一个新的进程就被内核所创建。这个新进程(或者称为子进程)和父进程在堆栈等数据上是一样的。当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任何一个,进行写操作时,系统就会创建新页面在另一个进程的虚拟内存中进行复制操作。
7.7 hello进程execve时的内存映射
execve 系统调用用于加载并执行一个新程序,替换当前进程的内存映射。execve 的执行过程会涉及到新的可执行文件的加载、旧内存空间的释放和新的内存空间的创建。
(1)删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
(2)映射私有区域。为新程序的代码、数据、.bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区,.bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零。
(3)映射共享区域。hello程序与共享对象1ibc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
(4)设置程序计数器。execve做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
处理缺页需要硬件和操作系统内核协作完成:
- 检查VA合法性
- 触发缺页异常处理程序
- 确定物理内存中的牺牲页,若已经被修改过,则把他换出到磁盘
- 缺页处理程序调入新的页面,并更新内存中的PTE
- 返回原进程,继续执行。
7.9动态存储分配管理
动态内存分配器维护这一个进程的虚拟内存区域,称为堆(heap),而内核维护着一个指向堆顶部的变量brk。分配器会将堆视作大小不同的块,每个块都是连续的虚拟内存片。分配器根据风格会有两种分配方法,一种是显式分配器,即要求应用显式地释放任何已分配的块,例如C中的malloc程序。另一种是隐式分配器,会检测一个已分配块何时不再被调用。
一般实现动态存储分配的基本管理方法有:空闲链表,边界标记法等。同时还要兼顾内存碎片管理,可以通过合并空闲块、内存压缩、垃圾收集、分级内存分配等方式。
7.10本章小结
本章中,我们重点关注了hello程序在运行中的存储管理和分配问题。首先我们划分了hello程序运行中所涉及到的几种存储器地址空间,然后重点讨论了段式管理、页式管理、TLB与四级页表支持下的VA到PA的变换和多级缓存命中等存储问题。之后我们又把目光转回进程,重新审视了fork函数和execve函数在使用时,对存储空间的利用和影响。最后我们讨论了缺页故障和动态存储分配管理的内容。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
Linux 将设备抽象为文件,称为设备文件,位于 /dev 目录下。设备文件分为两类:
1.字符设备:按字节进行数据传输的设备,如串口、键盘等。字符设备文件的主次设备号标识设备类型和实例。
2.块设备:按块进行数据传输的设备,如硬盘、光驱等。块设备文件的主次设备号也标识设备类型和实例
Linux将所有输入和输出都当做对相应文件进行读和写。Linux内核有一个应用接口,允许所有的输入和输出都能以一种统一且一致的方式来执行,大大简化了I/O接口的复杂度和处理难度
8.2 简述Unix IO接口及其函数
8.2.1 UNIX IO
Unix 提供了丰富的 I/O 接口,这些接口主要以系统调用的形式提供,应用程序可以通过这些系统调用与操作系统内核进行交互,以实现文件和设备的读写操作。
- 打开文件
一个应用程序通过要求内核打开相应的文件。内核会返回一个小的非负整数,称为描述符,它将在后续的操作中标识这个文件。
- 改变当前文件位置
对于每个打开的文件,内核都保留一个文件位置k,初始值为0,代表从文件开头起始字节的偏移量。应用程序能够执行seek操作,设置k的值
- 读写文件
对文件进行读取和写入。文件末尾没有明确的EOF符号
- 关闭文件
当应用完成对文件的访问后,就会通知内核关闭文件。内核将会释放文件打开时创建的数据结构,并将描述符恢复。
8.2.2 UNIX IO函数
1.int open(const char *pathname, int flags, mode_t mode);
参数:
pathname:文件路径。
flags:文件打开方式(如 O_RDONLY、O_WRONLY、O_RDWR 等)。
mode:文件权限(创建文件时使用,如 0644),可选。
返回值:文件描述符(非负整数),失败时返回 -1。
2.int close(int fd);
参数:
fd:文件描述符。
返回值:成功返回 0,失败时返回 -1。
3.ssize_t read(int fd, void *buf, size_t count);
参数:
fd:文件描述符。
buf:读取数据的缓冲区。
count:要读取的字节数。
返回值:读取的字节数,失败时返回 -1,文件结尾返回 0。
4.ssize_t write(int fd, const void *buf, size_t count);
参数:
fd:文件描述符。
buf:写入数据的缓冲区。
count:要写入的字节数。
返回值:写入的字节数,失败时返回 -1。
5.off_t lseek(int fd, off_t offset, int whence);
参数:
fd:文件描述符。
offset:偏移量。
whence:基准位置(SEEK_SET、SEEK_CUR、SEEK_END)。
返回值:新的文件偏移量,失败时返回 -1。
8.3 printf的实现分析
Windows下printf的写法:
int printf(const char *fmt, ...)
{
int i;
char buf[256];
va_list arg = (va_list)((char*)(&fmt) + 4);
i = vsprintf(buf, fmt, arg);
write(buf, i);
return i;
}
首先,va_list是一个字符指针。在代码中,arg等于的(char*)(&fmt) + 4) 表示的是...中的第一个参数。这是根据栈的逻辑得来的。参数压栈是从右往左,fmt是一个指针,(&fmt) + 4)所指向的正是压入栈的第一个参数。
之后我们来看下一句: i = vsprintf(buf, fmt, arg);
找到vsprintf函数,定义如下:
int vsprintf(char *buf, const char *fmt, va_list args)
{
char* p;
char tmp[256];
va_list p_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);
}
分析代码不难看出,vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
最后一句是write(buf, i),但是他是和底层硬件操作相关联的,比较复杂。在syscall.asm反汇编代码中,我们追踪到了write()

它通过INT_VECTOR_SYS_CALL来调用sys_call这个函数,这是一个陷阱式的系统调用,继续查看sys_call函数:
sys_call:
call save
push dword [p_proc_ready]
sti
push ecx
push ebx
call [sys_call_table + eax * 4]
add esp, 4 * 3
mov [esi + EAXREG - P_STACKBASE], eax
cli
ret
这个文件来源于kernel.asm反汇编文件。分析上面的代码,得知这个函数的功能是将字符串的字节从寄存器复制到显存,然后执行字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
getchar调用系统函数read,产生中断异常,进入异常处理程序,模式由原进程的用户模式切换回内核模式。当用户输入字符串(ascii码)和回车后(在收到回车之前,先保存在系统的键盘缓冲区中),getchar再次发送信号,内核再次调度,实现从缓冲去读入字符。
8.5本章小结
本章主要介绍了Linux的IO设备管理方法,介绍了Unix IO接口和函数。重点分析了printf函数的实现方法,构建起了一套分析库函数的思路,同时也对getchar的实现进行了分析。
(第8章1分)
结论
Hello所经历的过程:
- 预处理
将hello.c中调用的外部库内容插入,展开,生成一个经过修改的hello.i文件
- 编译
通过词法、语法分析,将合法指令转化为汇编代码,这个过程将把hello.i文件翻译成汇编语言文件hello.s
- 汇编
将hello.s的汇编语言翻译成机器语言,并整理成可重定位目标程序格式,存储在hello.o中
- 链接
将hello.o文件和可重定位目标文件和动态链接库链接起来,生成一个可执行目标文件
- 创建进程
在shell中输入命令,终端判断指令是不是内置指令,调用fork函数创建新的子进程
- 加载程序
Shell调用execve函数,启动加载器,将代码和数据加载入虚拟内存空间,程序开始执行
- 执行指令
CPU在进程被调度是分配时间片,独立执行逻辑流。
- 访问内存
内存管理单元MMU将逻辑地址映射成物理地址。通过三级高速缓存系统访问物理内存数据。
- 信号处理
针对进程中出现的异常控制流,系统进入异常处理函数进行响应。比如:当程序在运行的时候我们输入Ctrl+c,内核会发送SIGINT信号给进程并终止前台作业。当输入Ctrl+z时,内核会发送SIGTSTP信号给进程,并将前台作业停止挂起。
10.终止回收
进程结束后,内核安排父进程回收子进程,并将子进程的退出状态返回给父进程。由内核删除这个进程的所有数据结构
感悟:
一个简单的hello函数,能够在机器上正常的运转,有稳定的输出,是计算机系统各个结构和逻辑精巧的配合和设计。每一个环节都有条不紊,逻辑清晰,体现了严谨的工业精神。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
|
文件名 |
功能 |
|
hello.c |
C语言源文件 |
|
hello.i |
预处理后的文件 |
|
hello.s |
编译产生的文件 |
|
hello.o |
汇编产生的可重定位文件 |
|
hello.elf |
ELF格式信息 |
|
hello.asm |
反汇编hello.o得到的反汇编文件 |
|
hello1.asm |
反汇编hello得到的反汇编文件 |
|
hello |
可执行文件 |
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] https://www.cnblogs.com/pianist/p/3315801.html
[2] Randal E.Bryant David R.O'Hallaron.深入理解计算机系统(第三版).机械工业出版社,2016.
[3] https://blog.csdn.net/weixin_46199479/article/details/123438544
[4] https://blog.csdn.net/JachinYang/article/details/117398337
[5] https://www.cnblogs.com/fanzhidongyzby/p/3519838.html
(参考文献0分,缺失 -1分)
更多推荐






所有评论(0)