哈工大CSAPP2023大作业-程序人生-Hello’s P2P

计算机系统

大作业

题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术
学   号 2022113595
班   级 2203103
学 生 郑欢洋    
指 导 教 师 史先俊

计算机科学与技术学院
2023年4月
摘 要
Hello程序的一生中经历了完整的预处理、编译、汇编、链接、加载、运行、回收过程。本文以Hello的生命历程为线索,分析了Hello是如何从源代码转换为二进制程序、被加载到内存中执行、最后被回收的。在分析的同时,本文介绍了计算机中编译器套件、CPU、Cache、内存等各个软硬件系统是如何工作的,对于计算机系统这门课程的学习有积极意义。

关键词:计算机系统;编译过程;储存器层次结构;进程管理;

(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)

目 录

第1章 概述 - 4 -
1.1 Hello简介 - 4 -
1.2 环境与工具 - 4 -
1.3 中间结果 - 4 -
1.4 本章小结 - 4 -
第2章 预处理 - 5 -
2.1 预处理的概念与作用 - 5 -
2.2在Ubuntu下预处理的命令 - 5 -
2.3 Hello的预处理结果解析 - 5 -
2.4 本章小结 - 5 -
第3章 编译 - 6 -
3.1 编译的概念与作用 - 6 -
3.2 在Ubuntu下编译的命令 - 6 -
3.3 Hello的编译结果解析 - 6 -
3.4 本章小结 - 6 -
第4章 汇编 - 7 -
4.1 汇编的概念与作用 - 7 -
4.2 在Ubuntu下汇编的命令 - 7 -
4.3 可重定位目标elf格式 - 7 -
4.4 Hello.o的结果解析 - 7 -
4.5 本章小结 - 7 -
第5章 链接 - 8 -
5.1 链接的概念与作用 - 8 -
5.2 在Ubuntu下链接的命令 - 8 -
5.3 可执行目标文件hello的格式 - 8 -
5.4 hello的虚拟地址空间 - 8 -
5.5 链接的重定位过程分析 - 8 -
5.6 hello的执行流程 - 8 -
5.7 Hello的动态链接分析 - 8 -
5.8 本章小结 - 9 -
第6章 hello进程管理 - 10 -
6.1 进程的概念与作用 - 10 -
6.2 简述壳Shell-bash的作用与处理流程 - 10 -
6.3 Hello的fork进程创建过程 - 10 -
6.4 Hello的execve过程 - 10 -
6.5 Hello的进程执行 - 10 -
6.6 hello的异常与信号处理 - 10 -
6.7本章小结 - 10 -
第7章 hello的存储管理 - 11 -
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 -
7.9动态存储分配管理 - 11 -
7.10本章小结 - 12 -
第8章 hello的IO管理 - 13 -
8.1 Linux的IO设备管理方法 - 13 -
8.2 简述Unix IO接口及其函数 - 13 -
8.3 printf的实现分析 - 13 -
8.4 getchar的实现分析 - 13 -
8.5本章小结 - 13 -
结论 - 14 -
附件 - 15 -
参考文献 - 16 -

第1章 概述
1.1 Hello简介
在P2P(From Program to Process)过程中,源代码文件Hello.c(Program)被转换成一个运行中的进程(Process)。P2P过程可以简略分为“编译过程”与“执行过程”。在编译过程中,文本文件Hello.c经过预处理、编译、汇编、链接这四个过程成为elf格式的可执行文件Hello(Linux下)。在执行过程中,父进程(如Shell进程)通过fork函数创建子进程,并使用execve函数来将Hello加载到当前子进程中,从而完成了程序到进程的转换。
020(From Zero to Zero)过程着重强调了Hello从加载到被回收的整个执行过程。Hello的数据最初不存在于内存中,在调用execve函数后,内核删除原有的虚拟内存段,创建新的代码段、数据段、堆栈,然后跳至Hello的入口点开始执行。OS的TLB、4级页表、3级Cache等机制为虚拟内存到物理内存的转换提供了支持。Hello的代码、数据等信息在缺页中断时被从硬盘加载到内存、Cache中。CPU从存储器中读取指令、数据,并进行译码和执行,从而实现了Hello的功能。系统IO的标准输入/输出让Hello的运行结果能够在屏幕上显示。当Hello执行结束时,父进程将对其进行回收,删除其痕迹。Hello从此不在存在于内存/Cache中,实现了“赤条条来去无牵挂”。
1.2 环境与工具
硬件环境:12th Gen Intel® Core™ i7-12700H 2.30 GHz; 16 GB RAM
操作系统:Windows11;Ubuntu22.04(WSL2)
开发工具:g++,gdb,objdump,edb
代码编辑器:VSCode
1.3 中间结果
名称 作用
hello.i 预处理器处理hello.c生成的结果代码
hello.s 编译器处理hello.i生成的汇编代码
hello.o 汇编器处理hello.s生成的elf可重定位目标文件
hello 链接器链接hello和其他库产生的elf可执行目标文件
hellorel.s 从hello.o中反汇编得到的汇编代码
helloexec.s 从hello中反汇编得到的汇编代码
hellorel.txt 用readelf工具从hello.o中读取的信息
helloexec.txt 用readelf工具从hello.o中读取的信息

1.4 本章小结
本章介绍了hello的P2P与020过程,并列举了本次大作业过程中的软硬件环境以及产生的中间文件。
(第1章0.5分)

第2章 预处理
2.1 预处理的概念与作用
预处理是在编译前对源代码进行的一系列宏替换和条件编译操作,预处理之后的源代码一般以“.i”为后缀名。
预处理是由C预处理器cpp完成的。
预处理器负责解析文件中的以“#”开头的预处理器指令(#include、#define等),并完成对应文本替换。例如,hello.c中的“#include <stdio.h>”指示预处理器将头文件stdio.h的内容插入程序中;“#define MAXN 2000”指示预处理器将文件中单独出现的“MAXN”词语全部替换为“2000”。除了解析预处理指令外,与预处理器还会进行删除注释、将物理行替换为逻辑行等操作。
预处理是编译的准备工作,经过预处理,源代码中适合程序编写的格式(如#include、#define等)被转换为适合机器读取的格式,为之后的编译流程做好了准备。
2.2在Ubuntu下预处理的命令
cpp hello.c hello.i (直接调用预处理器)
或 gcc -E hello.c hello.i (使用gcc命令)

2.3 Hello的预处理结果解析
预处理之后,hello.i文件的长度为3093行,其中3079~3093行为main函数的内容,main之前的代码为三个头文件“stdio.h”、“unistd.h”、“stdlib.h”的内容。

在main函数部分,预处理之后的文件与预处理之前并无显著区别,但是注释被删除,且缩进也被修改。

在头文件部分,容易发现预处理器的处理是递归进行的,即头文件内部的#include与#define命令也得到了处理。

2.4 本章小结
本章介绍了预处理的概念和作用,并且以hello.c为例演示了预处理的结果。
(第2章0.5分)

第3章 编译
3.1 编译的概念与作用
狭义的编译指将预处理之后的.i文件转换为.s汇编语言程序的过程。该过程由C编译器ccl完成,包括词法分析、语法分析、语义分析、源代码优化等步骤。
编译之后的代码已经从高级语言转变成了十分接近机器语言的汇编指令。

3.2 在Ubuntu下编译的命令
gcc -m64 -Og -no-pie -fno-stack-protector -fno-PIC -S hello.c -o hello.s

3.3 Hello的编译结果解析
3.3.1 常量
hello中的常量包括数字与字符串。
3.3.3.1 数字
数字常量被转换为汇编指令中的直接值。
如在比较if(argc != 4)时,汇编指令为cmpl $4, %edi,直接将%edi寄存器的值和直接值4进行比较。
3.3.3.2 字符串
字符串被储存在.rodata区中,该区用于储存只读变量和字符串常量。

标签.LC0代表字符串"用法: Hello 学号 姓名 秒数!\n",标签.LC1代表字符串"Hello %s %s\n"。在汇编代码可以通过标签调用这些字符串,如“movl $.LC0, %edi”。
中文字符以UTF-8编码储存,每个汉字占三个字节。比如“\347\224\250”表示汉字“用”。ASCII范围内的字符则以明文表示。

3.1.2 局部变量
hello.c中的局部变量包括整型变量、指针变量等。
在64位模式下,函数的第一个参数存放在寄存器%rdi中,第二个参数存放在寄存器%rsi中。因此,int型局部变量(main的第一个参数)argc储存在寄存器%edi中,指针型局部变量(main的第二个参数)argv储存在寄存器%rsi中。
此外,在函数体内定义的局部变量i储存在寄存器%ebp中。
3.1.3 赋值
hello.c中出现了赋值语句i = 1,由于i是一个局部变量,保存在寄存器%ebp中,所以只需要简单的将寄存器%ebp的值赋为0即可。所对应的汇编语句为“movl $0, %ebp”。
3.1.4 算数操作
hello.c中出现了自增运算符“++”,由于i是一个局部变量,保存在寄存器%ebp中,因此只需将寄存器%ebp的值+1即可,所对应的汇编语句为“addl $1, %ebp”。
3.1.5 关系操作
hello.c中出现了两个判断“argc!=4”与“i<8”。在汇编语言中, cmp命令比较会将两个操作数相减,但不会改变操作数的值,只会修改标志位。在执行完cmp命令后,通过判断标志位的值,即可得知两个操作数的关系。
因为cmp指令常常与跳转相关,因此存在大量条件跳转指令,如je,jl,jg,jz等,用于在有符号或者无符号的条件下,通过标志位判断两个操作数的关系,然后执行跳转。
例如,判断“argc!=4”时,先通过“cmpl $4, %edi”来比较直接值4与寄存器%edi的值(由上文知argc的值储存在edi中)。然后通过jne命令,在ZF标志位为0,即argc-4非0时发起跳转,从而跳过if语句块内代码的执行。
在判断“i<8”时,先通过“cmpl $7, %ebp”计算7 – i,然后通过jle命令,在7-i≤0(ZF=1或SF≠OF)的时候,跳转到循环体的开头。
3.1.6 数组操作
hello.c中,argv为一个字符指针数组的头指针。不放称这个数组为s,易知s数组的元素大小为8字节(x64系统的指针大小)。
在调用“movq %rsi, %rbx”后,s数组的首指针储存在%rbx寄存器中。在调用argv[1],argv[2],argv[3]时,采用的是相对寻址方式,三个元素的地址分别为8(%rbx)、16(%rbx)、24(%rbx)。
3.1.7 控制转移
hello.c内有if与for两个流程控制语句。if语句块内的代码从标签.L6开始,如果“argc!=4”为真,那么就跳转.L6开始执行,该语句块以exit函数结束,因此执行完毕后会直接退出程序。
for语句块内的代码从.L3开始,当执行到“cmpl $7, %ebp”时,如果%ebp仍然小于等于7,就跳转回.L3再执行一遍循环体;如果%ebp的值大于7,则不会往回跳转,而是按顺序继续执行接下来的代码。
3.1.8 函数操作
在hello.c中,没有自定义函数,但存在大量对于系统函数的调用。
在x86-64系统中,函数的第1~6个入参分别储存在寄存器%rdi、%rsi、%rdx、%rcx、%r8、%r9中。在调用函数前,需要先将参数储存到这些寄存器中,然后通过“call”指令来进行函数调用。
在被调用的函数中,需要保存%rbp、%rbx等Callee Save的寄存器(通常把这些寄存器的值压入堆栈中),并且改变%rbp、%rsp寄存器以指向新的栈底和栈顶(“movl $0, %ebp”、“subq $8, %rsp”)。而当被调用的函数返回时。需要将返回值保存在%rax寄存器中,并恢复%rbp、%rbx、%rsp等寄存器的值(一般是将%rbp赋值给%rsp,然后从堆栈中弹出其他寄存器的值),最后调用ret来返回。
3.4 本章小结
本章讨论了hello.c的汇编过程,并且着重分析了hello的汇编结果代码。对于C语言中的各个要素,本章简要介绍了它们在汇编语言中是如何表示的。
(第3章2分)

第4章 汇编
4.1 汇编的概念与作用
汇编过程将上一步的汇编代码文件(.s)转换为二进制机器码,产生.o目标文件,这个过程是通过汇编器as完成的。
经过汇编之后,代码已经从可读的汇编语言转换为最底层的机器语言。
4.2 在Ubuntu下汇编的命令
gcc -m64 -Og -no-pie -fno-stack-protector -fno-PIC -c hello.c -o hello.o

4.3 可重定位目标elf格式
hello.o文件包括以下几个部分:
(1)ELF头(ELF Header)
ELF头描述了文件的类型、系统、架构等基本信息,并给出了各个部分的开始地址、节头部的大小、节头数量等信息。

(2)节头表(Section header table)
节头表描述了ELF文件中每个节的名称、类型、地址、文件偏移、大小、标志等信息。典型的节如下表所示:

节 内容
.text 程序的可执行代码
.data 已初始化的全局变量
.rodata 只读数据和跳转表
.bss 未初始化的全局变量
.strtab ELF文件使用的字符串表
.rela.text .text节的重定向表

(3)重定位表
重定位表是按节储存了,例如,“.text节”有专属的重定位表“.rela.text”。
汇编器对于目标位置未知的引用会生成一个重定位条目,指导链接过程中如何修改这个位置的引用。例如,printf函数的目标位置对于汇编器就是未知的(实际存在于C标准库中)。因此,汇编器在.rela.text中创建了条目“000000000045  000800000004 R_X86_64_PLT32    0000000000000000 __printf_chk - 4”,来指导链接过程。

(4)符号表
符号表存放了函数中定义和调用的函数和全局变量。在链接过程中,不同的目标文件可能相互引用变量和函数,链接器将检查这些调用是否冲突,并实现这些调用。

分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。

4.4 Hello.o的结果解析
通过objdump对于hello.o进行反汇编得到helloelf.s,与hello.s进行比较。
容易发现,helloelf.s中的机器指令能够与汇编指令一一对应。helloelf.s中的机器指令通过反汇编转换为汇编指令之后,与hello.s大致上是相同的,但是也存在以下区别:
(1)helloelf.s中不含.cfi_开头的汇编指示符
(2)helloelf.s中直接值以16进制表示,例如“mov $0x1,%edi”
(3)hello.s中跳转通过标签(如.L2、.L3)标识,而helloelf.s中通过与PC的相对位置给出。例如“17: eb 4d jmp 66 <main+0x66>”,在当前语句时,PC指向下一条语句,PC=19,19+4d=66,刚好为跳转的目标位置。
(4)hello.s中对于符号的引用直接通过函数名或变量名表示,helloelf.s中这些符号的地址暂时以0代替,并且给出重定向条目
4.5 本章小结
本章讨论了hello的汇编过程。在汇编过程后,hello.s转换为可重定位目标文件hello.o。这种二进制文件适合于与其他目标文件链接,并创建最终的可执行文件。通过对于hello.o的分析,可以得到elf文件的基本结构,并且可以探究汇编过后的机器代码与汇编指令之间的联系。
(第4章1分)

第5章 链接
5.1 链接的概念与作用
注意:这儿的链接是指从 hello.o 到hello生成过程。
链接是将多个文件中单独的代码节和数据节合并成单个节、解析这些文件中的符号、重定向符号并更新对这些符号的引用、最终形成可执行文件的过程。
使用链接机制后,在修改项目中的部分源代码时,只需重新编译被修改的文件,而无需重新编译所有源代码,提高了修改项目的效率。
5.2 在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 -a hello > helloelf2.txt命令列出可执行目标文件hello的信息。
(1)ELF头
在ELF头中,列出了hello的基本信息。
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。与hello.o的elf头相比,可以发现其类型从可重定向目标文件变成了可执行目标文件。同时,程序主入口点、程序头部等原本为空的数值也已经被填入。

(2)节头部表
hello的节数达到了27个(hello.o只有14个节),原本留空的address字段也被填入了。
新增的段.interp、.dynamic、.dynsym等用于与动态链接相关的内容。

(3)程序头表
hello.o中不包含程序头表中的内容。hello的elf文件中,程序头表描述了磁盘上可执行文件的布局,以及可执行文件是如何被映射到内存中的。

(4)动态段,与动态链接相关

(5).rela开头的重定向段
尽管hello已经被链接器处理过。但是仍然存在.rela段,因为程序在启动时才会与动态链接库链接,此时无法确定这些引用的具体位置,仍然需要重定位节。

(6)符号表

5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。

当前程序的虚拟地址空间从00400000开始,到00405000结束。
根据helloelf2.txt的内容,. interp段从4002e0开始,可以发现此处存放了动态链接器的路径。

.text段从4010f0开始。可以发现从此处开始的机器码与helloelf.s里面的机器码相同。

.rodata段从402000开始,可以发现从此处开始存放着两个字符串常量。

5.5 链接的重定位过程分析
通过命令objdump -d -r hello > helloexec.s对hello进行反汇编。
hello与hello.o的汇编代码有以下区别:
(1)函数数量不同
hello.o中只有main函数的源代码,在链接过程中,puts、printf、exit、sleep等函数的代码也被加入文件中,使得最后得到的代码多出了大量的指令。
(2)符号的地址不同
在hello.o中,符号的地址均以0代替。而在hello中,符号的地址均已经被修改为虚拟地址空间的地址,而调用符号时使用的地址也已经相应地更新了。
重定向的过程如下:

在此处因为.rodata.str1.8的地址未知,因此需要添加一条重定向条目。
在重定向时,通过5.4的分析可以知道该字符串从0x402008开始,因此在小端位下该指令应该被修改为bf 08 20 40 00 。这与链接完的实际结果相同。
其他条目的链接与此同理。

5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
在加载hello的过程中,加载器根据hello的程序头部表将hello的各段映射到内存当中的指定位置。然后将PC指向Hello的入口点,开始执行hello。hello的执行过程中线调用_init、_start进行初始化,然后进入hello的主流程。
hello的主流程中先检查命令函参数是否正确,然后重复输出八遍Hello 学号 姓名,输出之间等待指定的秒数。
这些工作完成后,hello通过一个getchar()等待输入,接收到输入之后退出。
子程序名 程序地址
hello!_init 0x0000000000401000
hello!_start 0x00000000004010f0
hello!main 0x00000000004011d6
hello!__printf_chk@plt 0x0000000000401050
hello!puts@plt 0x0000000000401030
hello!sleep@plt 0x0000000000401070
hello!getc@plt 0x0000000000401080
hello!exit@plt 0x0000000000401060
5.7 Hello的动态链接分析
在调用共享库函数时,链接器会对这个函数生成一个重定位条目。动态链接器将在程序加载时对于该条目进行解析。GNU编译系统使用延迟绑定,将绑定地址过程推迟到第一次调用该过程时。绑定是通过GOT和PLT实现的。

执行_init之前的GOT与PLT:

执行_init之后的GOT与PLT:

5.8 本章小结
本章展示了链接前后可重定位目标文件与可执行目标文件的区别,还分析了加载器是如何把可执行目标文件的各段加载到内存中的,以及动态链接器是如何把符号动态加载到hello中的。
(第5章1分)

第6章 hello进程管理
6.1 进程的概念与作用
进程是一个正在运行的程序的实例。进程的代码、数据以及支持进程运行的环境合称为进程的上下文。
进程给应用程序提供了两个关键抽象:
(1)每个应用程序似乎独立地使用CPU
(2)每个应用程序似乎独占地使用内存系统
6.2 简述壳Shell-bash的作用与处理流程
shell是一个交互型应用级程序,代表用户运行其他程序。
shell的工作包括创建进程、加载运行程序、前后台控制、作业调用、信号发送与管理等。
shell的处理流程如下:
(1)从键盘读取并解析指令;
(2)如果指令是内部命令,则立即处理它;
(3)如果指令是可执行文件,则在子进程的上下文中加载和运行它;
(4)如果指令以“&”结尾,则让作业在后台执行,继续接收下一个命令;否则在前台执行作业,等待作业结束再接收下一个命令。
6.3 Hello的fork进程创建过程
在shell中执行Hello时,父进程(shell进程)通过调用fork函数创建一个新的运行的子进程。新创建的子进程拥有与父进程用户级虚拟空间相同但独立的一份副本,包括代码和数据段、堆、共享库以及用户栈;另外,子进程还会继承父进程打开的所有文件;不过,子进程与父进程将具有不同的PID。
6.4 Hello的execve过程
在通过fork创建子进程之后,子进程会调用execve函数。该函数在当前上下文中加载一个新程序,加载过程包括以下几步:
(1)删除子进程现有的虚拟内存段;
(2)将新程序的代码、数据、bss段映射到虚拟内存中,将栈与堆初始化为0;
(3)让PC指向新程序的入口点
在这个过程完成之后,子进程会开始执行新加载的程序。
值得一提的是,在加载过程中,只创建了从虚拟内存到硬盘的映射,除了一些头部信息实际上并没有读文件。新程序的代码与数据是在遇到缺页中断时才从硬盘读取到内存的。
6.5 Hello的进程执行
hello进程具有独立的逻辑控制流,该控制流不可能一直在CPU上运行。系统通过进程控制块中的滴答计时器来限制hello进程的运行时间。当计时器减为0,控制从进程切换到内核,内核将进行上下文切换,将控制转移给其他进程。此时hello进程暂停执行。当控制切换回hello进程时,hello进程就可以继续执行了。在这种调度方式下,尽管同时只有一个进程在cpu上运行(单核处理器),但可以近似地认为进程是并行执行的。
此外,在hello进程遇到陷阱时,控制权也会转移到内核,由内核进行系统调用操作,如从缓冲区读取字符等。
6.6 hello的异常与信号处理
hello执行过程中可能出现的异常包括中断、陷阱、故障、终止等。
(1)乱按键盘:输入的字符会储存到缓冲区,没有被hello处理的输入会被当做是shell的输入,作为命令处理。

(2)输入Ctrl-Z,给shell发送SIGTSTP信号,shell将此信号转发给前台进程组的每个进程,hello的默认信号处理程序会停止hello进程的执行。

(3)输入Ctrl-C,给shell发送SIGINT信号,shell将此信号转发给前台进程组的每个进程,hello的默认信号处理程序会终止hello进程的执行。

(4)Ctrl-Z之后输入ps -au:可以看到已经停止的hello进程

(5)Ctrl-Z之后输入jobs:可以看到已经停止的hello作业

(6)Ctrl-Z之后输入pstree:可以查看进程之间的父子关系。

(7)Ctrl-Z之后输入fg %1,可以将hello进程切换到前台执行。

(8)Ctrl-Z 之后输入kill %1,可以终止hello进程。

6.7本章小结
本章主要讨论了进程的创建与调度,以及shell的工作流程。着重分析了hello的加载、执行,以及hello的异常与信号处理。
(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:逻辑地址由段标志符与段内偏移量组成,是一种相对位置。例如hello.s中的相对偏移地址。
线性地址:逻辑地址的段地址+偏移地址得到线性地址;线性地址空间是非负整数地址的有序集合。
虚拟地址:虚拟地址与线性地址是相似的;虚拟地址空间是一个拥有N=2^n个地址的有序集合。
物理地址:每个物理地址与系统物理内存的一个字节相对应。
7.2 Intel逻辑地址到线性地址的变换-段式管理
在保护模式下,以段描述符作为下标,到GDT或者LDT中查表获得段地址。段地址+偏移地址=线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
从线性地址/虚拟地址到物理地址的转化通过对于虚拟内存空间的分页机制完成。
将虚拟地址分为两个部分:VPN(虚拟页号)和VPO(虚拟页偏移量)。因为虚拟内存与物理内存页大小相同,所以VPO = PPO(虚拟页偏移量等于物理页偏移量)。为了得到PPN(物理页号),需要以VPN为索引查询当前进程的页表。PTE(页表条目)由有效位与PPN组成。
若PTE有效位为1,那么说明所需的虚拟内存页已经加载到物理内存中,可以直接获取PPN,并与PPO组装成PA。
若PTE有效位为0,那么将会触发一次缺页异常。在发生缺页异常时,会调用操作系统的缺页处理程序,确定物理内存中的牺牲页,更新物理内存与页表条目。这时可以从更新之后的页表获取PPN,并与PPO组装成PA。
7.4 TLB与四级页表支持下的VA到PA的变换
TLB(翻译后备缓冲器)的作用是加快页表的翻译。
在MMU进行地址翻译时,先将VPN传给TLB。如果TLB中已经缓存了对应的PTE,就可以直接获取PTE并进行接下来的翻译;如果TLB不命中,则需要Cache或内存中取出对应的PTE到TLB中,然后再翻译出PA。
TLB位于MMU中,如果TLB命中的话无需访问高速缓存或内存,可以大大加快地址翻译的错误。
而四级页表的机制可以减小页表的大小,节省内存空间。
在四级页表中,虚拟地址的VPN被分为4个,第i个VPN是第i级页表的索引。前三级页表的条目是一个指向低一级的页表的索引,而第四级页表的条目包含了有效位、PPN等信息。由于某个页表条目不存在时,他所指向的更低级的页表也无需存在,因此可以节省储存页表的内存空间。
7.5 三级Cache支持下的物理内存访问
在完成了从VA到PA的转化之后,就可以通过PA访问物理内存了。
计算机通过高速缓存来加快对于内存的访问。高速缓存一般有L1、L2、L3三级。这里以L1 Cache为例介绍高速缓存的机制。
Core i7中的L1 Cache为组相联高速缓存,总共有64组、每组8路、块大小64kb。
物理地址PA被划分为标记、组索引、块偏移。
首先通过组索引来定位到L1 Cache中的某组,然后将标记位与L1 Cache中每路的标记位比较。如果匹配成功且对应路的有效位为1,即为命中,可以通过块偏移从高速缓冲块中取出相应的数据。
若没有匹配成功或者匹配成功但是标志位为1,即为缓存不命中。需要向下一级缓存请求数据(L2 Cache、L3 Cache、主存)。取出数据后新的行会替换当前缓存组中的某一行。
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核给新进程创建虚拟内存,创建当前进程的mm_struct、区域结构和页表的原样副本,将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当两个进程中的任意一个对虚拟内存的某个页面进行写操作时,会触发保护故障,此时写时复制机制会在内存中创建一个对应页面的副本,并更新页表条目指向副本。此时进程对于内存的修改就会反应在副本页面上。
写时复制机制为每个进程保持了私有地址空间的抽象概念。同时延迟私有对象中的副本直到最后可能的时刻,充分利用了稀有的物理内存。
7.7 hello进程execve时的内存映射
在通过execve加载新进程时,需要以下几个步骤:
①删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
②映射私有区域。为新程序(即hello)的代码、数据、bss和栈区域等创建新的区域结构。所有这些区域都是私有的、写时复制的。
③映射共享区域。如果hello程序与共享对象(或目标)链接(如标准C库),那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
④设置程序计数器。最后设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
缺页故障的产生:在对于虚拟地址进行翻译时,如果发现对应的PTE的有效位未设置,说明虚拟地址对应的内容还没有缓存在物理内存中,此时会触发一个缺页故障。
在发生了缺页故障时,会调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,用当前请求的内容替换物理内存中的牺牲页,并修改牺牲页对应的页表条目。
当缺页异常处理程序完成之后,它会重新启动导致缺页的指令,现在该指令就可以正常运行了。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。对每个进程,内核维护一个全局变量brk指向堆顶。
分配器将堆视为一组不同大小的块的集合来维护。每个块是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留,供应用程序使用;空闲块可用来分配。空闲块保持空闲,直到空闲块显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的(即显式分配器),要么是内存分配器自身隐式执行的(即隐式分配器)。显式分配器和隐式分配器是动态内存分配器的两种基本风格。两种风格都要求应用显式地分配块,不同之处在于由哪个实体来负责释放已分配的块。显式分配器要求应用显式地释放任何已分配的块。隐式分配器要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器,而自动释放未使用的已分配的块的过程叫做垃圾收集。
分配器的具体操作过程以及相应策略:
①放置已分配块:当一个应用请求一个k字节的块时,分配器搜索空闲链表。查找一个足够大可以放置所请求的空闲块。执行这种搜索的常见策略包括首次适配、下一次适配和最佳适配等。
②分割空闲块:一旦分配器找到了匹配的空闲块,需要决定分配这个空闲块中多少空间。可以选择用整个块,但会造成额外的内部碎片;也可以选择将空闲块分割为两部分,第一部分变成已分配块,剩下的变成新的空闲块。
③获取额外的堆内存:如果分配器不能为请求块找到空闲块,分配器通过调用sbrk函数,向内核请求额外的堆内存。分配器将额外的内存转化成一个大的空闲块,将这个块插到空闲链表中,然后被请求的块放在这个新的空闲块中。
④合并空闲块:分配器释放一个已分配块时,要合并相邻的空闲块。分配器决定何时执行合并,可以选择立即合并或者推迟合并。合并时需要合并当前块和前面以及后面的空闲块。
7.10本章小结
本章主要介绍了Hello的虚拟内存空间是如何转换成物理内存空间的。同时也介绍了这种储存机制在fork、execve时的应用,以及动态分配器的原理。
(第7章 2分)

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
一个Linux文件就是一个m个字节的序列:B_0,B_1,B_2……B_(m-1)
所有的 IO 设备(例如网络、磁盘和终端)都被模型化为文件,所有的输入和输出都被当做对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许 Linux内核引出一个简单、低级的应用接口,称为 Unix I/O,使得所有的输入和输出都能以一种统一且一致的方式来执行。
8.2 简述Unix IO接口及其函数
Unix IO 接口:
使得所有的输入和输出都能以一种统一且一致的方式来执行。
①打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个 I/O 设备。内核返回一个小的非负整数,即描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
②Linux shell创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。头文件中的常量可以代替显式的描述符值。
③改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置 k,初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek操作,显式地设置文件的当前位置为k。
④读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的文件,当k≥m时执行读操作会触发EOF条件,应用程序能检测到这个条件。类似地,写操作就是从内存复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
⑤关闭文件:当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
Unix I/O函数:

①进程通过调用open函数打开一个存在的文件或者创建一个新文件。
int open(char* filename,int flags,mode_t mode);
open函数将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags参数指明了进程打算如何访问这个文件;mode参数指定了新文件的访问权限位。
②进程通过调用close函数关闭一个打开的文件。
int close(int fd);
fd是需要关闭的文件描述符,成功返回0,错误返回-1。关闭一个已关闭的描述符会出错。
③应用程序通过分别调用read和write函数来执行输入和输出。
ssize_t read(int fd,void *buf,size_t n);
ssize_t wirte(int fd,const void *buf,size_t n);
read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,而返回值0表示EOF。否则返回值表示的是实际传送的字节数量。write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。
8.3 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;
}
printf提供了比Unix IO中write函数更强大的输入输入功能。
首先,printf创建一个缓冲区,然后调用vsprintf函数对于格式串进行解析,将待输出的内容存进buf变量中,将待输出的字节数存入i中。
然后调用write函数,该函数将触发一个系统调用,将控制转移到内核并将buf在标准输出上输出。
在UNIX IO中,标注输出stdout也被作为文件处理,当需要将stdout输出到屏幕上时,需要字符显示驱动子程序读取stdout中的数据(UTF-8编码),并从字模库中取出对应的字模加载到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
这样就完成了字符串到屏幕的输出过程。
8.4 getchar的实现分析
当键盘按键按下时触发一个系统中断,键盘中断处理子程序接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
当getchar被调用时,它会试图缓冲区中读取一个字符。如果缓冲区为空,getchar将调用read系统函数来从键盘缓冲区中读取数据到自身的缓冲区(直到接收到回车键才返回)。当缓冲区填充完成之后,getchar函数从缓冲区中读取一个字符后返回。
8.5本章小结
本章介绍了Unix IO的设计理念以及其提供的接口和函数,并通过printf和getchar 两个函数分析了高级的IO函数是如何与Unix IO协同工作的。
(第8章1分)
结论
Hello的历程:
①编码:编写hello.c文件;
②预处理:C预处理器对hello.c进行宏替换得到hello.i;
③编译:C编译器将hello.i编译为汇编语言文件hello.s
④汇编:汇编器将汇编语言文件hello.s文件汇编为二进制的可重定位目标文件hello.o。
⑤链接:链接器将hello.o与其他库链接得到可执行目标文件hello
⑥运行:父进程(Shell等)通过fork创建子进程,通过execve加载hello程序。
⑦运行中:hello的代码和数据在适当的时机被从磁盘加载到主存和Cache;内核对hello进行调度,hello的逻辑控制流在数个时间片中运行;hello在运行过程中对接受到的信号作出响应。
⑧终止后:hello终止后,由hello的父进程负责回收hello,清除hello的痕迹。
感悟:
计算机是一个复杂和精密的系统,各个模块的分工协作使其能迅速处理复杂的过程。在学习计算机系统的过程中,需要善于使用抽象建模的方法,排除无需关心的部分,将关键内容以容易理解的模型表述。同时也不能忽略了各个系统之间的关联。
通过这门课程的学习,我对于计算机的底层工作原理有了更深入的了解,学会了根据计算机系统的特性编写更高效、更安全的代码,收获颇丰。
(结论0分,缺失 -1分,根据内容酌情加分)

附件
名称 作用
hello.i 预处理器处理hello.c生成的结果代码
hello.s 编译器处理hello.i生成的汇编代码
hello.o 汇编器处理hello.s生成的elf可重定位目标文件
hello 链接器链接hello和其他库产生的elf可执行目标文件
hellorel.s 从hello.o中反汇编得到的汇编代码
helloexec.s 从hello中反汇编得到的汇编代码
hellorel.txt 用readelf工具从hello.o中读取的信息
helloexec.txt 用readelf工具从hello.o中读取的信息

(附件0分,缺失 -1分)

参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 物理地址和逻辑地址
https://blog.csdn.net/wrx1721267632/article/details/52056910
[2] GDT(全居描述符表)和LDT(局部描述符表)
https://blog.csdn.net/genghaihua/article/details/89450057
[3] x86-64 汇编:寄存器和过程调用约定
https://blog.csdn.net/qq_34908601/article/details/123772569
[4] [转]printf 函数实现的深入剖析
https://www.cnblogs.com/pianist/p/3315801.html
[5] 常用系统IO:open、close、lseek、read、write 学习、使用
https://blog.csdn.net/qq_52858062/article/details/115532586
(参考文献0分,缺失 -1分)

  • 9
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值