计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 医学类1
学 号 2022112310
班 级 2252002
学 生 王炫淳
指 导 教 师 史先俊
计算机科学与技术学院
2024年5月
摘要是论文内容的高度概括,应具有独立性和自含性,即不阅读论文的全文,就能获得必要的信息。摘要应包括本论文的目的、主要内容、方法、成果及其理论与实际意义。摘要中不宜使用公式、结构式、图表和非公知公用的符号与术语,不标注引用文献编号,同时避免将摘要写成目录式的内容介绍。
关键词:关键词1;关键词2;……;
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第1章 概述
1.1 Hello简介
。Hello的P2P:程序员先编写出hello的原始C语言代码,然后经过预处理、编译、汇编、链接得到可执行文件,再由shell为hello创建进程并加载可执行文件,得到一个运行中的hello进程,于是hello便从代码变成了运行着的进程(from program to process)。
Hello的020:shell为hello创建进程并加载hello的可执行文件,为其提供了虚拟地址空间等进程上下文,实现了hello的从无到有的过程。
Hello在运行时会经历诸多的异常与信号,以及对存储器的访问也会涉及诸多机制,以及通过中断和IO端口与外设交互,等等。最终,hello正常退出或收到信号后终止,都会使得操作系统结束hello进程,释放其占用的一切资源,返回shell,这便是hello的从无到有再到无的过程(from zero to zero)。
1.2 环境与工具
硬件环境: CPU:Intel i912900H,16GB内存。
系统环境: 虚拟机:Ubuntu 20.04.4 LTS,VMWare Workstation 16
工具: 文本编辑器gedit,反汇编工具edb 1.3,反汇编工具objdump,编译环境gcc等。
1.3 中间结果
①原始代码hello.c。
②预处理后的代码hello.i。
③编译后的汇编语言代码hello.s。
④可重定位目标文件hello.o。
⑤hello.o的objdump结果hello_o_disasm.txt。
⑥可执行文件hello。
⑦hello的objdump结果hello_disasm.txt。
1.4 本章小结
本章主要介绍了hello.c程序P2P,020的过程。列出了本次实验所需的环境和工具以及过程中所生成的中间结果。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
C语言编译器的预处理是将原始的代码按照带有“#”号的预处理语句进行扩展,例如,把#define的宏进行替换,通过条件编译指令可以根据不同的条件编译不同的代码段,预处理器会在预处理阶段删除所有注释,使得编译器不会看到任何注释内容等。
。
2.2在Ubuntu下预处理的命令
在终端内输入命令gcc -E hello.c,在屏幕上得到hello.c的预处理结果(如图)。为方便起见我们重定向gcc的输出,将结果保存到hello.i文件内。
2.3 Hello的预处理结果解析
查看hello.i文件,在最开头,是hello.c涉及到的所有头文件的信息。
然后,先是这些头文件用typedef定义的诸多类型别名:
然后的内容是被include的头文件们的主体内容,包括大量的函数声明和少部分struct定义等。它们都完全经过预处理器的宏展开。
最后是源文件中的hello.c的核心内容
2.4 本章小结
从预处理步骤中我们可以初步发现,一个看似简单的 "Hello, World!" 程序,其背后隐藏了大量的实现细节和声明。这些隐藏的细节展示了 C 语言在编译过程中进行的大量底层工作,这也使得即使是一个小小的程序,其背后所依赖的库和定义也多得多。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译是将高级编程语言编写的源代码转换为机器代码的过程,使计算机能够执行程序。编译的主要作用包括:
高效执行:将源代码转换为高效的机器码,提高程序的运行速度。
错误检查:在编译过程中检测语法和语义错误,帮助开发者在运行前发现问题。
代码优化:通过优化技术生成更高效的目标代码,提高程序性能。
模块化开发:支持将不同模块组合成一个完整的可执行程序,便于分模块开发。
移植性:通过生成中间代码和目标代码,便于程序在不同平台和架构上移植。
编译的这些作用使得软件开发更加高效、可靠,并能生成性能优异的应用程序。
3.2 在Ubuntu下编译的命令
应截图,展示编译过程!
gcc -S hello.i -o hello.s -fno-PIC -no-pie -m64
3.3 Hello的编译结果解析
.file
.text
.section .rodata
.align 8
.LC0:
.string "\347\224\250\346\263\225: Hello \345\255\246\345\217\267 \345\247\223\345\220\215 \346\211\213\346\234\272\345\217\267 \347\247\222\346\225\260\357\274\201"
.LC1:
.string "Hello %s %s %s\n"
.text
.globl main
.type main, @function
main:
.LFB6:
.cfi_startproc
endbr64
pushq %rbp // 保存调用者的栈帧指针。
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp // 设置新的栈帧指针。
.cfi_def_cfa_register 6
subq $32, %rsp
movl %edi, -20(%rbp) // 将参数 argc(%edi)保存到局部变量 -20(%rbp)。
movq %rsi, -32(%rbp) // 将参数 argv(%rsi)保存到局部变量 -32(%rbp)。
cmpl $5, -20(%rbp) // 比较 argc 是否等于5。
je .L2 // 如果是,跳转到 .L2。
movl $.LC0, %edi // 将 .LC0 的地址放入 %edi。
call puts // 调用 puts 打印出错误信息。
movl $1, %edi // 将 1 放入 %edi。
call exit // 调用 exit 退出程序。
.L2:
movl $0, -4(%rbp) // 初始化局部变量 -4(%rbp) 为0。
jmp .L3 // 跳转到 .L3。
.L4:
movq -32(%rbp), %rax // 将 argv 的地址放入 %rax。
addq $24, %rax // argv + 24 指向第四个参数(假定为第一个字符串)。
movq (%rax), %rcx // 加载第四个参数到 %rcx。
movq -32(%rbp), %rax // 将 argv 的地址放入 %rax。
addq $16, %rax // argv + 16 指向第三个参数(假定为第二个字符串)。
movq (%rax), %rdx // 加载第三个参数到 %rdx。
movq -32(%rbp), %rax // 将 argv 的地址放入 %rax。
addq $8, %rax // argv + 8 指向第二个参数(假定为第三个字符串)。
movq (%rax), %rax // 加载第二个参数到 %rax。
movq %rax, %rsi // 将第二个参数的值放入 %rsi。
movl $.LC1, %edi // 将 .LC1 的地址放入 %edi。
movl $0, %eax // 将 0 放入 %eax。
call printf // 调用 printf 打印出格式化字符串。
movq -32(%rbp), %rax // 将 argv 的地址放入 %rax。
addq $32, %rax // argv + 32 指向第五个参数
movq (%rax), %rax // 加载第五个参数到 %rax。
movq %rax, %rdi // 将第五个参数的值放入 %rdi。
call atoi // 调用 atoi 将字符串转换为整数。
movl %eax, %edi // 将 atoi 的返回值(整数)放入 %edi。
call sleep // 调用 sleep 函数。
addl $1, -4(%rbp) // 将局部变量 -4(%rbp) 加 1。
.L3:
cmpl $9, -4(%rbp) // 比较局部变量 -4(%rbp) 是否小于等于 9。
jle .L4 // 如果是,跳转到 .L4。
call getchar // 调用 getchar 函数等待用户输入。
movl $0, %eax // 将 0 放入 %eax,表示程序返回值为 0。
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE6:
.size main, .-main
.ident "GCC: (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0" .section .note.GNU-stack,"",@progbits
.section .note.gnu.property,"a"
.align 8 。
.long 1f - 0f
.long 4f - 1f .long 5
0:
.string "GNU" 1:
.align 8
.long 0xc0000002
.long 3f - 2f
2:
.long 0x3
3:
.align 8
4:
3.4 本章小结
我们可以看到,编译后生成的汇编代码 hello.s 相比于预处理后的 hello.i 文件大大缩减了。这是因为 hello 程序的核心功能仅在 main 函数中,而 hello.s 中的汇编代码几乎全部用于实现 main 函数。这些汇编代码已经将程序转化为接近机器级的表示形式,能够直接控制CPU的执行。此外,它们依然具有一定的可读性,能够被人类理解,因此可以说汇编代码是程序员与机器之间的重要桥梁。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编语言是一种低级编程语言,直接与计算机的硬件架构相关。它使用助记符(mnemonics)来代替机器码中的二进制指令,这些助记符对应于特定的机器指令。每条汇编指令通常对应一条机器指令,使得汇编语言比高级语言(如C、Python)更接近计算机硬件。
4.2 在Ubuntu下汇编的命令!
gcc -c hello.s -o hello.o -fno-PIC -no-pie -m64
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
使用readelf解析汇编器生成的可重定位目标文件hello.o,结果如下:
在 hello.o 文件中,可以看到它包含了13个节(包括代码节 .text、只读数据节 .rodata 等)。此外,还包含了8个重定位条目和7个全局符号(所有这些全局符号都是函数声明,属于强符号)。在这8个重定位条目中,有两个条目对应的是 .rodata 节中的数据地址,显然是 printf 使用的两个字符串地址。其余6个重定位条目是 call 指令调用的函数地址。由于这些函数地址目前还是空的,它们需要在与对应的库链接时进行填充。
4.4 Hello.o的结果解析
在终端中输入命令 `objdump -d -r hello.o`,可以得到 `hello.o` 中可执行代码的反汇编结果。我们可以看到,这个反汇编结果与之前分析的 `hello.s` 文件内容非常相似,但反汇编结果还显示了汇编指令对应的机器码。
在左侧,我们能看到汇编指令对应的机器码,这些机器码以字节形式呈现。每条汇编指令对应若干字节的机器码,机器码的开头字节描述了指令的类型,后续字节表示操作数。
值得注意的是,在19 1e 28 4f 59 6c 73 82 8处指令中,操作数并不是0。但在 `hello.s` 文件中,这些操作数应该是数据节或代码节中的一个地址。这是因为在可重定位目标文件中,这些地址暂时还不是真正的地址。只有在链接阶段,经过重定位,才能确定这些操作数的实际地址。
因此,在目标文件中,汇编器暂时将这些操作数设为0,并创建一个重定位条目。链接器在链接阶段使用这些重定位条目来设置指令的最终操作数地址,从而完成程序的正确运行
总结来说,反汇编显示的机器码和指令操作数反映了目标文件中的临时状态,最终在链接阶段通过重定位条目进行正确设置,以确保程序能够正确执行。
在汇编过程中,程序的每一条汇编指令被转化为相应的机器码,但其中涉及地址的部分并未确定下来。相反,这些地址会被标记为需要重定位的条目。可重定位的机器码使得这些地址在链接阶段可以根据程序的整体布局进行调整,以确保所有引用都能正确指向实际的地址。这一机制允许代码和数据在内存中灵活放置,为程序的最终执行做好准备。通过这种方式,汇编生成的机器码可以在链接阶段与其他目标文件和库进行整合,确保程序能够正确运行。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
编译过程中的链接是将多个源文件编译生成的目标文件或中间代码文件合并成一个可执行文件或库文件的过程。链接的作用是解决程序中各个部分之间的引用关系,将它们有效地组合在一起,使得程序能够正确地执行。通过链接,程序可以调用其他模块的函数或访问其他模块的变量,实现了模块化设计和代码复用。
5.2 在Ubuntu下链接的命令
ld -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 /usr/lib/gcc/x86_64-linux-gnu/11/crtbegin.o hello.o -lc /usr/lib/gcc/x86_64-linux-gnu/11/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -z relro -o hello
5.3 可执行目标文件hello的格式
使用readelf解析hello的ELF格式,得到hello的节信息和段信息:
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息:
5.5 链接的重定位过程分析
使用objdump运行得到了以下结果:
首先,链接器会组织所有模块的节,为可执行文件的虚拟地址空间定型。然后,根据这个虚拟地址空间,链接器会设置那些在hello.o中存在的.rel.text和.rel.data节中的重定位条目指向的位置的操作数,确保它们都被设置为正确的地址。这样,就会扩充很多函数代码,包括程序加载后执行main前的准备工作,以及hello所需的库函数的定义等。在main函数中,原本在hello.o中等待重定位而暂时置0的地址操作数,现在被设置为虚拟地址空间中真正的地址。
5.6 hello的执行流程
主要函数 | 程序地址 |
_init | 0x401000 |
.plt | 0x401020 |
puts@plt | 0x401090 |
printf@plt | 0x4010a0 |
getchar@plt | 0x4010b0 |
atoi@plt | 0x4010c0 |
exit@plt | 0x4010d0 |
sleep@plt | 0x4010e0 |
_start | 0x4010f0 |
main | 0x4011d6 |
_fini | 0x401270 |
5.7 Hello的动态链接分析
当程序调用一个由共享库定义的函数时,由于编译器无法预测这时候函数的地 址是什么,因此这时,编译系统提供了延迟绑定的方法,将过程地址的绑定推迟到第一次调用该过程时。通过 GOT和过程链接表PLT的协作来解析函数的地址。在 加载时,动态链接器会重定位 GOT 中的每个条目,使它包含正确的绝对地址,而 PLT 中的每个函数负责调用不同函数。那么,通过观察edb,便可发现 dl_init 后.got.plt节发生的变化
使用readelf读取hello文件,寻找其中.got与.got.plt的地址如图所示:
使用edb对于调用_init前后的PLT的数据变化如下所示:
调用前:
调用后:
5.8 本章小结
链接中包含了许多提高程序执行效率、减小空间浪费的措施,如静态链接库、动态链接共享库等,为编写高效的程序提供了思路
经过链接,已经得到了一个可执行文件,接下来只需要在 shell 中调用命令就 可以为这一文件创建进程并执行该文件。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程是正在运行的程序的实例。每个进程都有自己的地址空间、代码、数据和执行环境。进程是操作系统管理和调度的基本单位,它在计算机系统中扮演着至关重要的角色。
6.2 简述壳Shell-bash的作用与处理流程
(以下格式自行编排,编辑时删除)
Shell(壳)是操作系统的用户界面,负责接收用户输入的命令并将其传递给操作系统内核执行。其中,Bash(Bourne Again Shell)是一种常见的 Unix 和 Linux 系统中使用的 Shell。
处理流程:
读取命令行输入: Shell 接收用户在命令行输入的命令和参数。
解析命令: Shell 解析用户输入的命令,分解为命令名称和参数。
查找命令: Shell 查找用户输入的命令名称对应的可执行文件或内置命令。
执行命令: 如果找到了对应的命令,Shell 将调用相应的程序执行命令,或者执行内置命令来完成用户请求的操作。
等待命令完成: Shell 等待执行的命令完成,并显示执行结果或者提示用户输入下一个命令。
循环处理: 重复上述过程,继续等待用户输入命令并执行,直到用户退出 Shell 或者终止 Shell 进程。
6.3 Hello的fork进程创建过程
程序开始执行: Hello 程序开始执行,操作系统创建一个初始的进程(通常称为父进程)来执行程序的代码。
遇到 fork() 调用: 在 Hello 程序中的某个地方,可能会遇到 fork() 调用。fork() 调用会创建一个新的进程(通常称为子进程)。
fork() 调用执行: 当程序执行到 fork() 调用时,操作系统会复制当前进程(父进程)的所有内容(包括代码、数据、堆栈等),创建一个新的子进程。这个子进程与父进程几乎完全相同,但是它们有不同的进程 ID。
父子进程的返回值: 在 fork() 调用后,父进程和子进程都会继续执行接下来的代码。在父进程中,fork() 调用的返回值是子进程的进程 ID(PID),而在子进程中,fork() 调用的返回值是 0。
继续执行: 父子进程在 fork() 调用后,会继续执行各自的代码。通常情况下,父进程和子进程会在 fork() 调用之后执行相同的代码,但可以根据需要在代码中进行条件判断,以区分父进程和子进程。
6.4 Hello的execve过程
程序开始执行: Hello 程序开始执行,操作系统创建一个初始的进程来执行程序的代码。
调用 execve(): 在 Hello 程序中的某个地方,可能会调用 execve() 系统调用,以执行另一个程序。execve() 接收三个参数:要执行的程序的路径、命令行参数的数组、环境变量的数组。
加载并执行新程序: 当程序执行到 execve() 调用时,操作系统会将指定路径的程序加载到当前进程的地址空间,并开始执行该程序。这个新程序会完全替换当前进程的内容,包括代码、数据和堆栈等。
程序执行: 一旦新程序被加载到内存中并开始执行,它就会接管控制权,并按照程序的逻辑执行其代码。
6.5 Hello的进程执行
进程调度是操作系统中至关重要的功能,它通过一系列精密的操作来决定在某个时间点上 CPU 应该被分配给哪个进程。这个过程涉及到多个关键步骤:首先,操作系统维护着一个就绪队列,其中包含了所有处于就绪状态的进程。然后,调度器会根据一定的调度算法(如先来先服务、最短作业优先、轮转调度等)从这个队列中选取下一个要执行的进程。接着,发生了一次上下文切换,当前正在执行的进程的上下文信息(包括寄存器状态、内存映像等)会被保存下来,而被选中的新进程的上下文信息会被加载到 CPU 中,从而执行该进程的代码。在进程执行过程中,每个进程都有一定的时间片(时间片轮转调度算法下),在这个时间片用完之前,该进程会一直执行。如果一个进程在时间片用完之前没有完成,调度器会将它放回就绪队列,并选择下一个要执行的进程。此外,当一个进程从就绪状态切换到运行状态时,它会从用户态切换到核心态,以便访问系统资源和执行特权指令。最后,当所有进程执行完成或者系统关闭时,进程调度结束。综上所述,进程调度通过精心管理进程的状态和资源,确保了多个进程能够在系统中有序地运行,从而提高了系统的效率和性能。
6.6 hello的异常与信号处理
异常可分为中断、陷阱、故障和终止四类。中断是来自I/O设备的信号,是异步的,总是返回到下一条指令;陷阱是有意的异常,是同步的,总是返回到下一条指令;故障是潜在可恢复的错误,是同步的,可能返回到当前指令;终止是不可恢复的错误,是同步的,不会返回。
当按下普通按键或回车时,键盘会发送一个中断信号(异步异常)到操作系统内核,内核会识别并处理该信号,但通常不会对正在运行的进程产生影响。这是因为在这种情况下,内核只是简单地将按键信息传递给用户态的进程,而不会触发任何特殊的处理动作。
当按下 Ctrl+C 时,操作系统内核会接收到一个中断信号,并识别为 SIGINT 信号,然后将其发送给前台进程(例如 shell),通知其终止当前正在运行的任务。前台进程会接收到 SIGINT 信号,并根据其信号处理程序的设置采取相应的行动。对于没有设置特殊信号处理程序的进程(如 hello 程序),默认操作是直接终止进程的执行。
类似地,当按下 Ctrl+Z 时,操作系统内核会接收到一个中断信号,并识别为 SIGTSTP 信号,然后将其发送给前台进程。前台进程会接收到 SIGTSTP 信号,并根据其信号处理程序的设置采取相应的行动。对于大多数前台进程,默认操作是暂停进程的执行,使其进入后台状态,等待继续执行。
在处理 Ctrl+Z 暂停后,用户可以使用 `fg` 命令将暂停的作业移至前台继续执行,或者使用 `bg` 命令将其放在后台继续执行。对于被暂停的作业,可以使用 `jobs` 命令查看当前的作业状态。最后,使用 `kill` 命令可以向指定进程发送信号,终止其执行。
6.7本章小结
学习了hello的各种机制,以及shell的底层逻辑
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
在这个hello.c程序中,我们可以借助它来说明逻辑地址、线性地址、虚拟地址和物理地址的概念。
逻辑地址:
在程序执行过程中,每个变量和指令都有自己的地址,这个地址称为逻辑地址。在这个C程序中,argv数组中的每个元素都有自己的逻辑地址。
虚拟地址:
虚拟地址是由操作系统分配给程序的地址空间中的地址。当程序加载到内存中时,它的所有逻辑地址都会被映射到虚拟地址上。在这个程序中,main()函数中的变量和指令的逻辑地址会被映射到虚拟地址空间中。
线性地址:
线性地址是指程序执行时使用的地址,也就是逻辑地址经过分段机制转换后的地址。在这个程序中,由于采用了64位架构(-m64选项),程序会使用线性地址来寻址内存。
物理地址:
物理地址是指RAM(随机访问存储器)中的实际地址,用于在RAM中存储和访问数据和指令。在虚拟地址被CPU访问之前,操作系统会将虚拟地址转换为物理地址。这个过程通过页表实现,将虚拟地址映射到物理地址上。这样,程序中的变量和指令就能够在RAM中正确地被访问。
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址:
逻辑地址由段选择子和段内偏移量组成。段选择子用于选择一个段,段内偏移量表示相对于这个段起始地址的偏移量。逻辑地址的结构通常是:
段选择子:段内偏移量。
段选择子:
段选择子包含了段描述符的索引和描述符表的选择子。段描述符提供了有关段的信息,如段的起始地址、段的大小和访问权限等。
线性地址:
在段式管理中,逻辑地址首先被转换成线性地址。这个过程称为地址转换。地址转换的第一步是从段选择子中获取段描述符,然后使用段描述符的信息计算出线性地址。
具体地,段选择子的索引部分用于在全局描述符表(GDT)或局部描述符表(LDT)中找到对应的段描述符。然后,使用段描述符中的基地址字段和逻辑地址中的段内偏移量字段来计算线性地址。
计算线性地址的公式通常为:
线性地址=段基址+段内偏移量
线性地址=段基址+段内偏移量。
页式管理:
在得到线性地址后,如果开启了分页机制,则线性地址会被转换成物理地址。这个过程是通过页表完成的,将线性地址映射到物理地址。
总的来说,逻辑地址到线性地址的变换通过段选择子和段描述符来实现,其中包含了对段的选择和定位,然后使用段描述符中的基地址字段和逻辑地址中的段内偏移量字段来计算出线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
在x86 架构中,线性地址到物理地址的变换是通过页式管理实现的。在这种管理方式下,操作系统维护着一个页表,它记录了线性地址和物理地址之间的映射关系。当CPU访问一个线性地址时,操作系统通过页表查找对应的物理地址,并返回给CPU,使CPU能够直接访问内存中的数据和指令。这个过程中,页表的访问速度会影响整个系统的性能,因此CPU通常会使用TLB缓存来加速地址转换过程。
7.4 TLB与四级页表支持下的VA到PA的变换
在支持四级页表的x86 架构系统中,虚拟地址(VA)到物理地址(PA)的变换过程涉及TLB缓存和多级页表。首先,CPU检查TLB缓存以查找最近的虚拟地址到物理地址的映射关系,如果找到,则直接从TLB中获取物理地址。如果TLB中没有找到对应映射,则CPU通过多级页表进行地址转换。在四级页表中,虚拟地址的每个页表索引对应一个级别的页表。CPU依次遍历页表索引,直到找到最终的页表项,其中包含了物理地址或者下一级页表的地址。这样,CPU最终得到虚拟地址对应的物理地址,以便访问内存中的数据或指令。TLB缓存和多级页表的结合使得地址转换更加高效,从而提高了系统的整体性能。
7.5 三级Cache支持下的物理内存访问
在拥有三级Cache支持的体系结构中,物理内存访问通常经过多层Cache的逐级访问。首先,CPU核心会检查L1 Cache,如果数据命中,则直接从这里获取。如果未命中,则继续访问更大容量的L2 Cache。如果数据在L2 Cache中命中,则从这里获取,并可能被复制到L1 Cache中。如果L2 Cache也未命中,则继续访问共享的L3 Cache,其中包含更多CPU核心共享的数据。如果数据在L3 Cache中命中,则从这里获取,并可能被复制到较低级别的Cache中。如果连L3 Cache中也未找到数据,则CPU将访问主内存来获取数据,同时在Cache中进行缓存以提高后续访问的速度。 Cache的层次结构设计旨在最大限度地减少内存访问延迟,提供更快速的数据访问路径,以提高系统性能。
7.6 hello进程fork时的内存映射
当一个进程(比如这里的hello进程)调用fork()函数时,操作系统会创建一个新的进程,称为子进程。这个子进程将会继承父进程的内存映像,包括代码段、数据段、堆和栈等。在hello进程中,当调用fork()函数时,操作系统会复制父进程的地址空间,包括程序的代码段、数据段以及堆栈等,然后为子进程分配一个新的进程标识符(PID)。这意味着子进程将拥有与父进程相同的内存布局和内容。但是,父进程和子进程的内存空间是独立的,它们之间的变化不会相互影响。因此,在hello进程中调用fork()函数后,父进程和子进程将会拥有各自独立的内存空间,但内存映射的内容将保持一致。
7.7 hello进程execve时的内存映射
当hello进程调用execve()函数时,它会加载一个新的程序替换当前的进程映像。在这个过程中,操作系统会清除当前进程的内存映像,并加载新程序的代码段、数据段以及其他必要的资源到内存中。因此,hello进程调用execve()后,原有的内存映像将会被替换为新程序的内存映像,新程序将会占用相同的地址空间,但是内存中的内容将会完全改变为新程序的内容。
7.8 缺页故障与缺页中断处理
当hello进程调用execve()函数时,操作系统会加载一个新的可执行文件,替换当前进程的内存映像。这个新程序的代码段、数据段以及其他相关资源将会被加载到进程的地址空间中。因此,hello进程调用execve()后,原有的内存映射将会被完全替换为新程序的内存映像,而新程序将会占用相同的地址空间,但内存中的内容将会完全改变为新程序的内容。
7.9动态存储分配管理
动态存储分配管理是指在程序运行时动态地分配和释放内存,以满足程序的内存需求。malloc()是一个常用的动态内存分配函数,用于分配指定大小的内存块。基本的动态内存管理方法包括使用malloc()分配内存,然后使用free()释放已分配的内存块。管理动态内存的策略包括首次适应、最佳适应和最差适应等。首次适应会从内存空闲列表中找到第一个能够容纳请求大小的空闲块;最佳适应会选择能够最小程度地浪费内存的空闲块;而最差适应则会选择能够最大程度地利用内存的空闲块。这些策略各有优缺点,选择合适的策略取决于具体的应用场景和性能要求。Printf等函数会调用malloc()来动态分配内存以存储输出的字符串等数据,然后在使用完后调用free()来释放已分配的内存,以避免内存泄漏和提高系统资源利用率。
7.10本章小结
通过上述分析,我们揭示了hello的看似简单的内存访问背后的复杂机制,尤其是极度重要的基于页式管理的虚拟内存机制。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
Linux的IO设备管理方法基于设备的模型化和Unix I/O接口。在Linux中,一切设备都被视为文件,这种模型化使得对设备的管理和访问变得统一和简化。通过Unix I/O接口,应用程序可以像操作文件一样操作设备,包括打开、读取、写入和关闭等操作。这种统一的设备访问方式使得开发人员可以使用相同的系统调用和文件操作函数来处理设备,简化了IO设备的管理和编程。
8.2 简述Unix IO接口及其函数
open():打开文件或设备。
close():关闭文件或设备。
read():从文件或设备中读取数据。
write():向文件或设备中写入数据。
lseek():在文件中定位读写位置。
ioctl():对设备进行控制操作。
fcntl():对文件描述符进行控制操作。
select():等待文件描述符的状态改变。
poll():轮询文件描述符的状态改变。
dup():复制文件描述符。
dup2():复制文件描述符到指定的文件描述符。
pipe():创建管道。
socket():创建套接字。
bind():将套接字绑定到地址。
listen():监听连接请求。
accept():接受连接请求。
connect():连接到远程套接字。
send():发送数据到套接字。
recv():从套接字接收数据。
shutdown():关闭套接字的读写。
getsockopt():获取套接字选项。
setsockopt():设置套接字选项。
8.3 printf的实现分析
printf函数包含在头文件<stdio.h>中
__fortify_function int
printf (const char *__restrict __fmt, ...)
{
return __printf_chk (__USE_FORTIFY_LEVEL -1, __fmt, __va_arg_pack ());
}
# elif !defined __cplusplus
# define printf(...) \
__printf_chk (__USE_FORTIFY_LEVEL - 1, __VA_ARGS__)
# define fprintf(stream, ...) \
__fprintf_chk (stream, __USE_FORTIFY_LEVEL - 1, __VA_ARGS__)
# endif
函数原型如下:
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;
}
(char*)(&fmt) + 4) :表示的是...(可变参数)中的第一个参数的地址
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
getchar的大致函数实现如下:
#include <unistd.h>
int getchar(void) {
char c;
// 从标准输入中读取一个字符
if (read(STDIN_FILENO, &c, 1) != 1) {
// 如果读取失败,返回EOF
return EOF;
} else {
// 返回读取到的字符
return (int)c;
}
}
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章介绍了系统级IO、Unix/IO的基本知识,I/O 是系统操作不可或缺的一部分,学习系统的IO有助于理解其他的系统概念。最后分析了printf、getchar两个标准化的IO函数
(第8章1分)
结论
通过hello的一生,预处理、编译、汇编、链接直至消失,一点痕迹也没有留下,回顾其一生,我了解到了计算机的先进设计及巧妙思想。
Hello的一生已经消逝,我的一生才刚刚开始。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
(附件0分,缺失 -1分)
hello.i | 预处理生成的文本文件 |
hello | .o经过链接生成的可执行目标文件 |
hello.o | .s文件汇编后得到的可重定位目标文件 |
hello.s | .i文件编译后得到的汇编语言文件 |
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 《深入理解计算机系统(原书第三版)》 Randal E.Bryant David R.O’Hallaron 机械工业出版社
[2] [SHELL(bash)脚本编程六:执行流程-腾讯云开发者社区-腾讯云](SHELL(bash)脚本编程六:执行流程-腾讯云开发者社区-腾讯云)
[3][转]printf 函数实现的深入剖析 - Pianistx - 博客园](https://www.cnblogs.com/pianist/p/3315801.html)
(参考文献0分,缺失 -1分)