计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业
学 号
班 级
学 生
指 导 教 师
计算机科学与技术学院
2022年5月
本文以一个简单的hello.c程序为例,在Linux操作系统下逐步拆解其生命周期,解释hello从程序到进程(P2P)的整个过程,包括预处理、编译、汇编、链接、进程、虚拟内存、逻辑内存等,结合CSAPP知识进行详细阐述,利用Linux命令行实现全过程可视化展示,加深对计算机系统工作原理的理解
关键词:P2P;预处理;编译;汇编;链接;进程;内存。
目 录
2.2在Ubuntu下预处理的命令.......................................................................... - 5 -
3.2 在Ubuntu下编译的命令............................................................................. - 6 -
4.2 在Ubuntu下汇编的命令............................................................................. - 7 -
5.2 在Ubuntu下链接的命令............................................................................. - 8 -
5.3 可执行目标文件hello的格式.................................................................... - 8 -
6.2 简述壳Shell-bash的作用与处理流程..................................................... - 10 -
6.3 Hello的fork进程创建过程..................................................................... - 10 -
6.6 hello的异常与信号处理............................................................................ - 10 -
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 -
8.1 Linux的IO设备管理方法.......................................................................... - 13 -
8.2 简述Unix IO接口及其函数....................................................................... - 13 -
第1章 概述
1.1 Hello简介
P2P,From Program to Process,指的是以某一用C语言编写的.c文件为原程序,采用一系列处理办法得到可执行文件的过程,包括预处理(.c->.i),编译(.i->.s),汇编(.s->.o),链接(.o->可执行)等步骤,最终得到的是一个hello二进制可执行文件。执行该文件时,OS(Operation System)在shell中利用fork新建一个子进程来执行该文件。
020,From Zero to Zero,指的是shell中程序被映射到虚拟内存,执行,开始进程后被载入物理内存,执行完毕后,内核通过信号管理相关子进程,将虚拟内存恢复到程序执行前的状态。
1.2 环境与工具
硬件环境
Intel(R) Core(TM) i5-10200H CPU @ 2.40GHz; 16.0GB RAM;256GHD Disk;
软件环境
Windows10 64位;VMware 15.5;Ubuntu 20.04 LTS 64位;
开发工具
Visual Studio 2022 64位;CodeBlocks 64位;vi/vim/gedit+gcc;
作用 | |
hello.c | 源代码,文本文件 |
hello.i | 预处理得到,有一定的代码优化 |
hello.s | 编译得到,为汇编语言文本文件 |
hello.o | 汇编得到,可重定位 |
hello.elf | hello.o的ELF格式,用于分析hello.o。 |
hello_objdump.txt | hello.o的反汇编文件,用于分析hello.o |
hello | 链接得到,可执行文件 |
hello_1.elf | hello的ELF格式,用于分析hello |
hello_objdump_1.txt | hello的反汇编文件,用于分析hello |
1.3 中间结果
1.4 本章小结
简要介绍了hello的一生,实验环境以及过程中得到的各个文件的作用。
第2章 预处理
2.1 预处理的概念与作用
概念:预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。典型地,由预处理器对程序源代码文本进行处理,得到的结果再由编译器核心进一步编译。这个过程并不对程序的源代码进行解析,但它把源代码分割或处理成为特定的单位——预处理记号用来支持语言特性。
对C语言而言,指预处理器cpp将以字符#开头的命令,如宏定义、条件编译、头文件等展开,将展开内容插入到原程序中,形成一个.i文本文件。
作用:
(1)将所有宏定义展开,进行字符替换
(2)将注释删除
(3)将所有引用头文件插入程序
(4)添加行号和文件标识
(5)处理条件编译指令
2.2在Ubuntu下预处理的命令
2.3 Hello的预处理结果解析
预处理结果部分如上图所示,使用nano工具打开hello.i文件,发现文件内容大幅增加,#开头的内容被大段代码替代。其余程序内容仍保持不变。在展开#内容时,gcc到默认的环境变量下寻找函数原型,不断展开#开头的内容,最后得到一个完全展开的文本文件。
2.4 本章小结
本章解释了预处理的相关概念,阐述了预处理器对程序进行预处理的具体措施,解析了预处理的输出结果。此时的代码仍是C语言代码,与原程序同等级别。预处理为后续程序处理提供了便利。
第3章 编译
3.1 编译的概念与作用
概念:编译,指利用编译程序从源语言编写的源程序产生目标程序的过程。
作用:编译把高级语言变成计算机可以识别的2进制语言。编译程序把一个源程序翻译成目标程序的工作过程分为五个阶段:词法分析;语法分析;语义检查和中间代码生成;代码优化;目标代码生成。编译过程最主要的工作是进行词法分析和语法分析,又称为源程序分析,分析过程中发现有语法错误,给出提示信息。
3.2 在Ubuntu下编译的命令
3.3 Hello的编译结果解析
3.3.1 伪指令
注意到hello.s文件中有许多代码以“.”开头,称这些代码为伪指令,具体如下图所示。
对其中出现的所有伪指令做如下阐述:
.file 声明源文件 .text 代码段 .section 只读数据
.align 声明对指令或数据存放地址的对齐方式 .string 声明string类型
.globl 声明全局变量 .type 指定类型 .size 声明大小
3.3.2 数据类型
本hello.c文件体现了C语言中的整型数、字符串和数组三种数据类型,下面对他们逐个分析。
一、字符串
程序中在两处使用了字符串数据类型,如下图所示
在hello.s文件中,可以看到对应的声明如下图所示
由于字符串默认采取UTF-8格式编码,汉字无法直接显示,采用对应的三个编码字节代替表示。
二、整型数
程序中在几处使用了整型数据类型,如下图所示
分别为argc(作为main的第一个参数直接传入),i(作为局部变量),以及一些立即数。
argc:采用如下代码,保存在-0x20(%rbp)栈空间内,大小为4字节。
i:作为局部变量,被保存在编译器或栈中,实际在hello.s中被保存在-4(%rbp)栈空间中,大小为4字节。
立即数:直接编码在汇编代码中,不被保存在栈中。
三、数组
程序中中向main函数传递了char *argv[]数组,
由汇编代码可知,数组在栈中保存时在地址空间上连续,调用数据时通过首地址加偏移量的方式定位具体元素。
3.3.3赋值操作
程序中只涉及到一处赋值操作,如下图所示
由汇编代码可知,该局部变量被放置在栈中,采用下面的汇编代码实现赋值。
此代码将i对应的数据储存在-4(%rbp)中。
3.3.4算术操作
程序中到两处算术操作,如下图所示
可以在hello.s中查找到相关汇编代码如下图所示
i++操作在循环体最后执行,由于i对应的数据此时储存在-4(%rbp)中,故直接采用addl将此数据自加1;由于printf中调用了argc数组的两个元素,汇编代码计算对应元素的地址并传给%rdi以备printf函数调用。
3.3.5关系操作
程序中涉及到两处关系操作,如下图所示
可以在hello.s中查找到相关汇编代码如下图所示
对于argc!=4的比较操作,汇编程序首先将argc的数据存入-20(%rbp),再利用cmpl将其与4进行比较。
对于i<8的比较操作,由于对整型数据而言<8等价于≤7,汇编程序首先将i的值保留在-4(%rdp)中,再用cmpl将其与7进行比较。
3.3.6控制转移
程序中涉及到两处控制转移,如下图所示
可以在hello.s中查找到相关汇编代码如下图所示
对于if条件判断操作,汇编程序比较-20(%rbp)与4是否相等,若相等则表明条件不满足,跳到.L2执行之后的语句,若不相等则表明条件满足,继续执行if内的语句。
对于for循环操作,汇编程序先在.L2中对循环变量i赋初值0,然后跳到.L3判断i是否满足循环条件,若满足则跳到.L4执行循环体,否则执行循环体之后的语句,在循环体.L4的最后每次对循环变量i执行加1操作。
3.3.7函数操作
程序中涉及到两处函数操作,如下图所示。
可以在hello.s中查找到相关汇编代码如下图所示
对于main函数,系统启动函数__libc_start_main调用main函数,实现了控制传递,系统又将参数argc和argv传递给main,使用寄存器存储,再为该程序申请一个栈空间。
对于printf函数:第一次printf将%rdi设置为“用法: Hello 学号 姓名 秒数!\n”的首地址。第二次printf设置%rdi为“Hello %s %s\n”的首地址,设置%rsi为argv[1],%rdx为argv[2]。第一次printf只有一个字符串参数,使用call puts@PLT直接输出结果;第二次printf使用call printf@PLT输出。
sleep函数:使用call sleep@PLT代码调用sleep函数。
3.4 本章小结
本章以一个简单的程序hello.s为例,解释了编译器处理C语言各种操作的方式, 宏观上表现为编译器将.i程序编译为.s的汇编程序。编译将原本的C语言程序处理为更低级的汇编语言,为后面处理为可执行程序做了一步准备。
第4章 汇编
4.1 汇编的概念与作用
概念:把汇编语言翻译成机器语言的过程称为汇编。
作用:使程序向机器级程序更进一步,变为机器可以理解的二进制程序,使计算机可以执行该程序。
4.2 在Ubuntu下汇编的命令
4.3 可重定位目标elf格式
采用readelf -a hello.o>hello.elf指令将hello.o文件转换为hello.elf文件
下面对hello.elf的内容进行解析:
4.3.1 ELF头
hello.elf的头部如下图所示
Type(文件类型) | 可重定位文件(REL) |
Machine(机器类型) | Advanced Micro Devices X86-64 |
Entry point address(入口地址) | 0x0 |
Size of this header(ELF头大小) | 64bytes |
Size of section headers(节头部表的大小) | 64bytes |
Number of section headers(节的个数) | 13 |
ELF头以一个16字节的序列Magic开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。此外,ELF头还给出了帮助链接器语法分析和解释目标文件的信息,具体如下所示
4.3.2节头
hello.elf的节头如下图所示
节头部表非常简明地给出了各节的相关信息,包括名称、类型、地址、偏移量、大小、全体大小、旗标、连接、信息、对齐。
4.3.3重定位节
hello.elf的重定位节如下图所示
重定位节包含了.text 节中需要进行重定位的信息,为链接器操作文件时修改提供方便。
重定位节.rela.text中提供了如下信息:
偏移量,指需要被修改的引用节的偏移。
信息,包括symbol和type两个部分,前四个字节为symbol,后四个字节为type。
Symbol,标识被修改引用应该指向的符号。
Type,重定位的类型。
类型,给链接器提供修改文件的标准。
符号值,一个有符号常数,这里均为0。
符号名称:重定向到的目标的名称。
4.3.4符号表
hello.elf的符号表如下图所示
符号表中存放了程序中定义和引用的函数和全局变量的信息,其中声明了重定位需要的所有符号。
4.4 Hello.o的结果解析
使用objdump -d -r hello.o > hello_objdump.txt命令生成hello.o的反汇编文件
在x86-64架构中,每条汇编语言对应了一条机器语言。
对比hello_objdump.txt和hello.s,可以看出除格式不同外,二者没有十分显著的差别。反汇编代码除汇编代码外,同时提供了汇编代码对应的机器代码即二进制代码。二者不同之处主要体现在以下几点:
一、在反汇编的代码中,操作数均以十六进制表示,而hello.s中以十进制表示。
二、在反汇编的代码中,一些指令不带有表示操作数大小的后缀,而hello.s中均有具体写明。
三、在反汇编的代码中,调用函数采用直接计算首地址的表达方式,而hello.s中采用直接调用函数名的方式。
四、在反汇编的代码中,没有伪指令,而hello.s中含有大量伪指令,以指导编译器进行后续操作。
五、在反汇编的代码中,跳转通过直接给出指令地址的方式表达,而在hello.s中,跳转采取的是Label表达。
4.5 本章小结
本章以hello.s的汇编为例,阐述了汇编对程序的操作。由汇编过程得到的二进制代码较hello.s文件级别更低,是机器可以理解的文件,机器可以执行该程序。人更难以理解该程序,但执行效率更高。
第5章 链接
5.1 链接的概念与作用
概念:链接是将各种代码和数据片段收集并组合称为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。
作用:链接使代码更加简洁,具有鲜明的模块化特征。链接允许我们将代码封装为各个模块。在编程时,在已有模块的前提下,我们直接利用这些模块的函数原型调用它们,这对主函数的编写提供了巨大的便利。
作为编译的最后一步,链接操作根据原汇编文件的信息,向相应的可执行文件中插入对应模块下的具体代码。
5.2 在Ubuntu下链接的命令
5.3 可执行目标文件hello的格式
采用readelf -a hello.o>hello_1.elf指令将hello.o文件转换为hello_1.elf文件
下面对hello.elf的内容进行解析:
5.3.1 ELF头
hello.elf的头部如下图所示
同hello.elf一样,ELF头以一个16字节的序列Magic开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。此外,ELF头还给出了帮助链接器语法分析和解释目标文件的信息。这里重点比较二者的不同之处,具体如下表所示。
Type(文件类型) | 可执行文件(EXEC) |
Entry point address(入口地址) | 0x4010f0 |
Size of program header(程序大小) | 56bytes |
Number of section headers(程序个数) | 12 |
Size of section headers(节头部表的大小) | 64bytes |
Number of section headers(节的个数) | 27 |
5.3.2节头
hello.elf的节头如下图所示
同hello.elf相同,节头部表非常简明地给出了各节的相关信息,包括名称、类型、地址、偏移量、大小、全体大小、旗标、连接、信息、对齐。这里重点比较二者的不同之处:
在hello_1.elf文件中,节的个数有明显增加,地址不再全为0,偏移量有所改变,这是因为链接器会根据代码之间的关系对代码进行修改,通过修改首地址和偏移量的方式将程序相互链接,各个符号的地址和偏移量因此改变。
5.3.4程序节
这部分在hello.elf中没有出现,是链接后特有的部分。具体如下图所示。
5.3.5段节
这部分在hello.elf中没有出现,是链接后特有的部分。具体如下图所示。
5.3.6动态区域
这部分在hello.elf中没有出现,是链接后特有的部分。具体如下图所示。
以上内容的具体含义将在下一节中详细阐述。
5.4 hello的虚拟地址空间
在edb中运行hello,查看Data dump如下图所示。
与程序头中的信息相对比,可以在虚拟地址0x401000到0x402000之间找到程序节中每一项内容的地址,该地址下存放的指令也与程序节保持一一对应。
5.5 链接的重定位过程分析
使用如下命令得到hello的反汇编文件hello_objdump_1.txt
部分内容截图如下:
比较hello_objdump.txt与hello_objdump_1.txt,注意到hello的反编译文件具有明显的模块化特征,整体结构清晰明了,且比hello.o多出了一些段,具体说明如下:
一、函数:在使用ld命令链接的时候,指定了动态链接器为64的/lib64/ld-linux-x86-64.so.2,crt1.o、crti.o、crtn.o中主要定义了程序入口_start、初始化函数_init,_start程序调用hello.c中的main函数,libc.so是动态链接共享库,其中定义了hello.c中用到的printf、sleep、getchar、exit函数和_start中调用的__libc_csu_init,__libc_csu_fini。链接器将上述函数加入。
二、函数调用:链接器解析重定条目时发现对外部函数调用的类型为R_X86_64_PLT32的重定位,此时动态链接库中的函数已经加入到了PLT中,.text与.plt节相对距离已经确定,链接器计算相对距离,将对动态链接库中函数的调用值改为PLT中相应函数与下条指令的相对地址,指向对应函数。对于此类重定位链接器为其构造.plt
三、.rodata引用:链接器解析重定条目时发现两个类型为R_X86_64_PC32的对.rodata的重定位(printf中的两个字符串),.rodata与.text节之间的相对距离确定,因此链接器直接修改call之后的值为目标地址与下一条指令的地址之差,指向相应的字符串。
此外,hello.o反编译文件中给出的起始地址为main起始地址0,而在hello反编译文件中则为_init起始地址0X401000.这与链接器的处理有关。
5.6 hello的执行流程
ld-2.31.so!_dl_start
ld-2.31.so!_dl_init
LinkAddress!_start
ld-2.31.so!_libc_start_main
ld-2.31.so!_cxa_atexit
LinkAddress!_libc_csu.init
ld-2.31.so!_setjmp
LinkAddress!main
ld-2.31.so!exi
5.7 Hello的动态链接分析
首先查看.got和.got.plt在节头中的地址
在edb中查看对应地址的具体内容
运行,再次查看对应地址下的内容
注意到该地址下的内容发生变化。分析其变化过程大致如下所述:
对于动态共享链接库中PIC函数,编译器没有办法预测函数的运行时地址,所以需要添加重定位记录,等待动态链接器处理,为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。
5.8 本章小结
本章以一个简单的hello程序为载体,阐述了链接的概念和作用,通过分析可执行文件的ELF格式及其虚拟地址空间,重定位过程、加载以及运行时函数调用顺序以及动态链接过程,直观地解释了链接和重定位的详细过程。
第6章 hello进程管理
6.1 进程的概念与作用
进程是一个执行中的程序的实例,每一个进程都有它自己的地址空间,一般情况下,包括文本区域、数据区域、和堆栈。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储区着活动过程调用的指令和本地变量。
进程为用户提供了以下假象:我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存,处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
Linux系统中,Shell是一个交互型应用级程序,代表用户运行其他程序(是命令行解释器,以用户态方式运行的终端进程)。
其基本功能是解释并运行用户的指令,重复如下处理过程:
(1)终端进程读取用户由键盘输入的命令行。
(2)分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量
(3)检查第一个(首个、第0个)命令行参数是否是一个内置的shell命令
(4)如果不是内部命令,调用fork( )创建新进程/子进程
(5)在子进程中,用步骤2获取的参数,调用execve( )执行指定程序。
(6)如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid(或wait…等待作业终止后返回。
(7)如果用户要求后台运行(如果命令末尾有&号),则shell返回;
6.3 Hello的fork进程创建过程
进程通过调用fork函数创建一个新的子进程。新创建的子进程几乎但不完全与子进程相同。在创建子进程的过程中,内核会将父进程的代码、数据段、堆、共享库以及用户栈这些信息全部复制给子进程,同时子进程还可以读父进程打开的副本。唯一的不同就是他们的PID,这说明,虽然父进程与子进程所用到的信息几乎是完全相同的,但是这两个程序却是相互独立的,各自有自己独有的用户栈等信息。
fork函数虽然只会被调用一次,但是在返回的时候却有两次。在父进程中,fork函数返回子进程的PID;在子进程中,fork函数返回0。这就提供了一种用fork函数的返回值来区分父进程和子进程的方法。
6.4 Hello的execve过程
fork之后,子进程调用execve函数(传入命令行参数)在当前进程的上下文中加载并运行一个新程序即hello程序,execve调用驻留在内存中的被称为启动加载器的操作系统代码来执行hello程序,加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零,通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容。最后加载器设置PC指向_start地址,_start最终调用hello中的main函数。除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制。直到CPU引用一个被映射的虚拟页时才会进行复制,这时,操作系统利用它的页面调度机制自动将页面从磁盘传送到内存。
6.5 Hello的进程执行
shell为hello fork了一个子进程,这个子进程和shell进程有独立的逻辑控制流,它们是并发进行的,但是若hello是以前台任务进行的,那么shell将会挂起等待hello运行结束,否则它们将会“同时运行”。
内核调度hello的进程开始进行,输出,然后执行sleep函数,这个函数是系统调用,它让调用进程休眠。内核转而执行其他进程,这时就会发生一个上下文转换。此后又会发生一次进程转换,恢复hello进程的上下文,继续执行hello进程。
6.6 hello的异常与信号处理
正常运行,程序可以正常执行,结束,被父进程回收。
按下ctrl+c,程序被强制结束。
按下ctrl+z,程序被暂时挂起。
此时使用ps命令查看进程。
hello 被暂时挂起,pid为9469. 使用jobs命令可以查看详细信息。
使用pstree可以查看该进程在进程表中的具体位置。
使用fg 1命令将其调到前台运行。
再次使用ctrl+z将其挂起,查看pid,使用kill命令将其终止
其他情况随意按键,不会对程序产生影响,但输入内容会显示在屏幕上。
6.7本章小结
本章从进程的角度分别描述了hello子进程fork和execve过程,并针对execve过程中虚拟内存映像以及栈组织结构等作出说明。同时了解了逻辑控制流中内核的调度及上下文切换等机制。阐述了Shell和Bash运行的处理流程以及hello执行过程中可能引发的异常和信号处理。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:程序经过编译后出现在汇编代码中的地址。逻辑地址用来指定一个操作数或者是一条指令的地址。是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 [段标识符:段内偏移量]。
线性地址:也叫虚拟地址,和逻辑地址类似,也是一个不真实的地址,如果逻辑地址是对应的硬件平台段式管理转换前地址的话,那么线性地址则对应了硬件也是内存的转换前地址。
虚拟地址:就是线性地址。
物理地址:用于内存芯片级的单元寻址,与处理器和CPU链接的地址总线相对应。可以直接把物理地址理解成插在机器上那根内存本身,把内存看成一个从0字节一直到最大空量逐字节编号的大数组,然后把这个数组叫做物理地址,但是事实上,这只是一个硬件提供给软件的抽象,内存的寻址方式并不是这样。所以,说它是“与地址总线相对应”更贴切一些,不过抛开对物理内存寻址方式的考虑,直接把物理地址与物理的内存一一对应,更便于人们理解。
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址的实际上是一对<选择符,偏移>,从左开始,第13位是索引,通过这个索引,可以定位到段描述符,而段描述符记载了有关一个段的位置和大小信息,以及访问控制的状态信息。段描述符一般由8个字节组成。由于8B较大,而Intel为了保持向后兼容,将段寄存器仍然规定为16-bit,我们无法通过16-bit长度的段寄存器来直接引用64-bit的段描述符。因此在逻辑地址中,只用13bit记录其索引。而真正的段描述符,被放于数组之中。
这个内存中的数组就叫做GDT,Intel的设计者提供了一个寄存器GDTR用来存放GDT的入口地址。程序员将GDT设定在内存中某个位置之后,可以通过LGDT指令将GDT的入口地址装入此寄存器,此后,CPU根据此寄存器中的内容作为GDT的入口来访问GDT了。除了GDT之外,还有LDT,但与GDT不同的是,LDT在系统中可以存在多个,每个进程可以拥有自己的LDT。LDT的内存地址在LDTR寄存器中。
TI位就是用来表示此索引所指向的段描述符是存于全局描述表中,还是本地描述表中。=0,表示用GDT,=1表示用LDT。
RPL位,占2bit,是保护信息位。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址(也就是虚拟地址 VA)到物理地址(PA)之间的转换通过分页机制完成。而分页机制是对虚拟地址内存空间进行分页。
使用虚拟寻址,CPU通过生成一个虚拟地址来访问主存,这个虚拟地址被送到内存之前首先转换为适当的物理地址。将一个虚拟地址转换为物理地址的过程叫做地址翻译,需要CPU硬件和操作系统之间的紧密合作。CPU芯片上的内存管理单元利用主存中的查询表来动态翻译虚拟地址。
虚拟地址作为到磁盘上存放字节的数组的索引,磁盘上的数组内容被缓存在主存中。同时,磁盘上的数据被分割成块,这些块作为磁盘和主存之间的传送单元。虚拟内存分割被成为虚拟页。物理内存被分割为物理页,物理页和虚拟页的大小相同。
任意时刻虚拟页都被分为三个不相交的子集:
未分配的:VM系统还未分配的页
缓存的:当前已经缓存在物理内存的已分配页
未缓存的:当前未缓存在物理内存的已分配页
每次将虚拟地址转换为物理地址,都会查询页表来判断一个虚拟页是否缓存在DRAM的某个地方,如果不在DRAM的某个地方,通过查询页表条目可以知道虚拟页在磁盘的位置。页表将虚拟页映射到物理页。有效位表明虚拟页是否缓存在DRAM中,n位地址字段是物理页的起始地址或者虚拟页的起始地址。
7.4 TLB与四级页表支持下的VA到PA的变换
TLB是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块。用于组选择和行匹配的索引和标记字段是从虚拟地址的虚拟页号中提取出来的。TLB中所有地址翻译步骤都是在MMU上执行的。
具体步骤为:
CPU产生一个虚拟地址
MMU从TLB中取出相应的PTE
MMU将这个虚拟地址翻译成物理地址,发送给高速缓存
高速缓存返回数据子给CPU
使用四级页表的地址翻译步骤:
如果TLB未命中,MMU会向页表中查询,CR3确定第一级页表的起始地址,VPN1确定第一级页表的偏移量,查询出PTE,如果在物理内存中,且权限符合,则确定第二级页表的起始地址,以此类推,最终在第四级页表中查询到PPN与VPO组合成PA。
7.5 三级Cache支持下的物理内存访问
针对物理内存访问,主要对各类高速缓存存储器的读写策略做出说明:
当CPU执行一条读内存字w的指令,它向L1高速缓存请求这个字。如果L1高速缓存由w的一个缓存的副本,那么就得到L1的高速缓存命中,高速缓存会很快抽取出w并返回给CPU。否则就是缓存不命中,当L1高速缓存向主存请求包含w的块的一个副本时,CPU必须等待。当被请求的块最终从内存到达时,L1高速缓存将这个快存放在它的一个高速缓存行里,从被缓存的块中抽取字w,然后返回给CPU。总体来看,高速缓存确定一个请求是否命中,然后抽取出被请求的字的过程,分为三步,(1)组选择、(2)行匹配、(3)字抽取。
7.6 hello进程fork时的内存映射
当fork函数被shell进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID,为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将这两个进程的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的、写时复制的。
7.7 hello进程execve时的内存映射
execve函数在shell中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效的替代了当前程序。加载并运行hello需要以下几个步骤:
删除已存在的用户区域。删除shell虚拟地址的用户部分中的已存在的区域结构。
映射私有区域。为hello的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello 文件中的.text和.data 区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello 中。栈和堆区域也是请求二进制零的,初始长度为零。
映射共享区域。如果hello程序与共享对象(或目标)链接,比如标准C 库libc. so, 那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
设置程序计数器(PC), execve 做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
缺页故障:当指令引用一个相应的虚拟地址,而与改地址相应的物理页面不在内存中,会触发缺页故障。通过查询页表PTE可以知道虚拟页在磁盘的位置。缺页处理程序从指定的位置加载页面 到物理内存中,并更新PTE。然后控制返回给引起缺页故障的指令。当指令再次执行时,相应的物理页面已经驻留在内存中,因此指令可以没有故障的运行完成。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址)。对于每个进程,内核维护着一个变量brk。它指向堆的顶部。
分配器将堆视为一组不同大小的块(block)的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显示地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显示地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器有两种分格:
显式分配器:要求应用显式地释放任何已分配的块。
隐式分配器:要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器。
7.10本章小结
本章主要介绍了hello的存储器地址空间、intel的段式管理、hello的页式管理,以及VA到PA的变换、物理内存访问和hello进程fork时的内存映射、execve时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
一个Linux文件就是一个m字节的序列。所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。这使得所有输入和输出都能以一种统一且一致的方式来执行。
8.2 简述Unix IO接口及其函数
Unix I/O接口功能:
打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。
Shell创建的每个进程都有三个打开的文件:标准输入,标准输出,标准错误。
改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。
读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。
Unix I/O函数:
int open(char* filename,int flags,mode_t mode) ,进程通过调用open函数来打开一个存在的文件或是创建一个新文件。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
int close(fd),fd是需要关闭的文件的描述符,close返回操作结果。
ssize_t read(int fd,void *buf,size_t n),read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
ssize_t wirte(int fd,const void *buf,size_t n),write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。
8.3 printf的实现分析
printf接受一fmt的格式,然后将匹配到的参数按照fmt格式输出。printf的代码如下图所示,它调用了两个外部函数vsprintf和write
下面查看vsprintf和write
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
getchar的实现具体如下图所示:
8.5本章小结
本章系统阐释了Unix I/O,通过LinuxI/O设备管理方法以及Unix I/O接口及函数,解释系统级I/O的底层实现机制。对printf和getchar函数的底层实现进行解析,对Unix I/O以及异常中断等进行了简单介绍。
结论
hello程序从.c文件开始,经过操作系统的一系列加工,最后总算走完了它的一生。它这一生,是艰苦的,因为饱受折磨(被软件处理的);又是幸福的,因为有人相助(也是软件处理的)。它这一生意义非凡,作为一个简单甚至可以说是简陋的程序,它的实现却是几十上百年无数计算机工程师努力的结果。
下面让我们回顾它光辉灿烂的一生:
编写,通过随便什么编辑器将代码键入hello.c
预处理,将hello.c调用的所有外部的库展开合并到一个hello.i文件中
编译,将hello.i编译成为汇编文件hello.s
汇编,将hello.s汇编成为可重定位目标文件hello.o
链接,将hello.o与可重定位目标文件和动态链接库链接成为可执行目标程序hello
运行,在shell中输入./hello 7203610717 徐嘉辰
创建子进程,shell进程调用fork为其创建子进程
运行程序,shell调用execve,execve调用启动加载器,加映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入 main函数。
执行指令,CPU为其分配时间片,在一个时间片中,hello享有CPU资源,顺序执行自己的控制逻辑流。
访问内存,MMU将程序中使用的虚拟内存地址通过页表映射成物理地址。
动态申请内存,printf会调用malloc向动态内存分配器申请堆中的内存。
信号,如果运行途中键入ctrl-c或ctrl-z则调用shell的信号处理函数分别停止、挂起。
结束,shell父进程回收子进程,内核删除为这个进程创建的所有数据结构。
以上,向hello的一生致敬。向所有计算机工程师致敬。
附件
文件名称 | 作用 |
hello.c | 源代码,文本文件 |
hello.i | 预处理得到,有一定的代码优化 |
hello.s | 编译得到,为汇编语言文本文件 |
hello.o | 汇编得到,可重定位 |
hello.elf | hello.o的ELF格式,用于分析hello.o。 |
hello_objdump.txt | hello.o的反汇编文件,用于分析hello.o |
hello | 链接得到,可执行文件 |
hello_1.elf | hello的ELF格式,用于分析hello |
hello_objdump_1.txt | hello的反汇编文件,用于分析hello |
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 林来兴. 空间控制技术[M]. 北京:中国宇航出版社,1992:25-42.
[2] 辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.
[3] 赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).
[4] 谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.
[5] KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.
[6] CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.
[7]https://blog.youkuaiyun.com/weixin_45406155/article/details/103775420?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522165287367816782391815065%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=165287367816782391815065&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_ecpm_v1~rank_v31_ecpm-11-103775420-null-null.142^v10^pc_search_result_control_group,157^v4^control&utm_term=hello%E4%BA%BA%E7%94%9F&spm=1018.2226.3001.4187
[8]https://blog.youkuaiyun.com/weixin_42017042/article/details/85468924?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522165287367816782391815065%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=165287367816782391815065&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_ecpm_v1~rank_v31_ecpm-1-85468924-null-null.142^v10^pc_search_result_control_group,157^v4^control&utm_term=hello%E4%BA%BA%E7%94%9F&spm=1018.2226.3001.4187
[9]https://blog.youkuaiyun.com/weixin_42867636/article/details/85497861?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522165293796516781667815264%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=165293796516781667815264&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_ecpm_v1~rank_v31_ecpm-10-85497861-null-null.142^v10^pc_search_result_control_group,157^v4^control&utm_term=hello%E4%BA%BA%E7%94%9F&spm=1018.2226.3001.4187
[10]https://blog.youkuaiyun.com/HelloTheWholeWorld/article/details/85339220?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522165293796516781667815264%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=165293796516781667815264&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_ecpm_v1~rank_v31_ecpm-4-85339220-null-null.142^v10^pc_search_result_control_group,157^v4^control&utm_term=hello%E4%BA%BA%E7%94%9F&spm=1018.2226.3001.4187
[11]https://blog.youkuaiyun.com/hahalidaxin/article/details/85144974?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522165293789016781432987015%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=165293789016781432987015&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_ecpm_v1~rank_v31_ecpm-15-85144974-null-null.142^v10^pc_search_result_control_group,157^v4^control&utm_term=hello%E4%BA%BA%E7%94%9F&spm=1018.2226.3001.4187