哈尔滨工业大学hit计算机系统大作业 程序人生-Hello’s P2P

计算机系统

大作业

题 目 程序人生-Hello’s P2P
专 业 计算机
学 号
班 级
学 生
指导教师

计算机科学与技术学院
2021年5月
摘 要
本论文通过从计算机系统的层次上,具体而细致地分析了hello.c程序从产生、预处理、编译等各个过程生成可执行文件,到运行这个可执行文件后系统CPU以及内存中发生的进程创建与回收、虚拟地址的翻译等活动,从而更好地理解计算机系统对代码的每一步处理,对修改代码以调试功能或优化性能指导意义。

关键词:计算机系统;链接;进程;存储管理;系统级I/O。

目 录

第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 本章小结 - 6 -
第3章 编译 - 7 -
3.1 编译的概念与作用 - 7 -
3.2 在UBUNTU下编译的命令 - 7 -
3.3 HELLO的编译结果解析 - 7 -
3.4 本章小结 - 9 -
第4章 汇编 - 10 -
4.1 汇编的概念与作用 - 10 -
4.2 在UBUNTU下汇编的命令 - 10 -
4.3 可重定位目标ELF格式 - 10 -
4.4 HELLO.O的结果解析 - 12 -
4.5 本章小结 - 12 -
第5章 链接 - 13 -
5.1 链接的概念与作用 - 13 -
5.2 在UBUNTU下链接的命令 - 13 -
5.3 可执行目标文件HELLO的格式 - 13 -
5.4 HELLO的虚拟地址空间 - 16 -
5.5 链接的重定位过程分析 - 16 -
5.6 HELLO的执行流程 - 17 -
5.7 HELLO的动态链接分析 - 18 -
5.8 本章小结 - 18 -
第6章 HELLO进程管理 - 19 -
6.1 进程的概念与作用 - 19 -
6.2 简述壳SHELL-BASH的作用与处理流程 - 19 -
6.3 HELLO的FORK进程创建过程 - 19 -
6.4 HELLO的EXECVE过程 - 20 -
6.5 HELLO的进程执行 - 20 -
6.6 HELLO的异常与信号处理 - 21 -
6.7本章小结 - 23 -
第7章 HELLO的存储管理 - 24 -
7.1 HELLO的存储器地址空间 - 24 -
7.2 INTEL逻辑地址到线性地址的变换-段式管理 - 24 -
7.3 HELLO的线性地址到物理地址的变换-页式管理 - 24 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 25 -
7.5 三级CACHE支持下的物理内存访问 - 26 -
7.6 HELLO进程FORK时的内存映射 - 26 -
7.7 HELLO进程EXECVE时的内存映射 - 26 -
7.8 缺页故障与缺页中断处理 - 27 -
7.9动态存储分配管理 - 28 -
7.10本章小结 - 28 -
第8章 HELLO的IO管理 - 29 -
8.1 LINUX的IO设备管理方法 - 29 -
8.2 简述UNIX IO接口及其函数 - 29 -
8.3 PRINTF的实现分析 - 30 -
8.4 GETCHAR的实现分析 - 32 -
8.5本章小结 - 32 -
结论 - 33 -
附件 - 34 -
参考文献 - 35 -

第1章 概述
1.1 Hello简介
Hello的P2P过程:最初Hello是存有代码的文件hello.c(Program),预处理hello.c生成hello.i文件,编译hello.i生成hello.s文件,汇编hello.s生成可重定位目标文件hello.o,最后hello.o和其他可重定位目标文件链接生成可执行文件hello,运行该可执行文件后,操作系统内进程管理调用fork函数,创建子进程,在其中运行(Process),完成了“From Program to Process”。
Hello的020过程:系统进程的上下文中原本没有Hello(Zero-0),通过fork函数创建子进程,在子进程中通过execve函数运行hello,运行期间的异常、信号等会处理后继续运行,VA的翻译过程需要TLB、4级页表、3级Cache的参与以提高运行速度,通过I/O管理实现显示结果或对程序运行进行干预,最后程序执行完成,子进程等待被回收,回收后内核会从系统中删除掉它的所有痕迹,Hello从系统中被清除(Zero-0),完成了“From Zero-0 to Zero-0”。
1.2 环境与工具
硬件环境:X64 CPU;4GHz;16G RAM;512G HD Disk.
软件环境:Windows11 64位;VMware 16;Ubuntu 20.04 LTS 64位。
开发工具:edb; VMvare 16; gdb; readelf; objdump; vi/vim/gedit+gcc.
1.3 中间结果
hello.i:预处理生成的文本文件hello.i
hello.s:编译产生的文本文件hello.s
hello.o:汇编后生成的可重定位目标文件hello.o
helloo_asm.txt:hello.o生成的hello.o的反汇编代码文件
hello:链接生成的可执行文件hello
hello_asm.txt:hello生成的hello的反汇编代码文件
1.4 本章小结
实验概述,简介Hello的两个流程,指明使用环境与工具,列举中间产生结果。

第2章 预处理
2.1 预处理的概念与作用
概念:预处理器对hello.c代码中以#开头的的命令(如头文件、宏定义等)进行处理,进行代码的增删,得到hello.i文本文件中不含这些命令、注释,取而代之的是具体详细的能实现这些功能的必要的代码。
作用:
(1)删除所有以#开头的命令、删除所有注释;
(2)将#include指向的文件插入;
(3)将#define定义的字符替换;
(4)通过条件编译指令,如#endif等,来决定要编译哪些代码;
(5)添加行号和文件标示,这样的在调试和编译出错的时候才知道是是哪个文件的哪一行。
2.2在Ubuntu下预处理的命令
预处理命令:gcc -E hello.c -o hello.i

图2-2-1
2.3 Hello的预处理结果解析
hello.c只有23行,而hello.i文件有3060行,hello.i中多出的行中包括了<stdio.h>、<unistd.h>和<stdlib.h>文件的展开,减少了的行是前几行#开头的行、注释行被删除。

图2-3-1
2.4 本章小结
介绍预处理的概念与作用,在Ubuntu下将hello.c进行了预处理得到hello.i文件,经过比对代码,观察到了预处理作用。

第3章 编译
3.1 编译的概念与作用
概念:编译器将hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。
作用:对高级语言进行词法分析、语法分析、语义分析,根据需要进行优化,最后转化为计算机可识别的汇编语言。
3.2 在Ubuntu下编译的命令
编译命令:gcc -S hello.i -o hello.s

图3-2-1
3.3 Hello的编译结果解析
3.3.1数据
(1)常量:字符串常量(图3-3-1行6、8)“用法: Hello 学号 姓名 秒数!\n”、“Hello %s %s\n”,被保存在数据段;数字常量如4(图3-3-2行24)、0、8等和整型main存储在.text节中;整型argc、字符串数组argv均为传入main函数的参数,分别存储在%edi、%rsi中(图3-3-2行22、23),使用时在栈中。

图3-3-1

图3-3-2

(2)局部变量:整型i(图3-3-3行52、53),存储在栈上,记录循环次数。

图3-3-3

3.3.2赋值
i赋初值为0:将立即数0放入栈中i的位置上(图3-3-4行31)。

图3-3-4

3.3.3类型转换
调用atoi函数,字符串argv[3]转换为整型,然后将结果作为sleep函数的参数(图3-3-5行48)。

图3-3-5

3.3.4算术操作
i++:将栈中存i的位置内的数值加上立即数1(图3-3-3行51)。

3.3.5关系操作
判断传入参数argc是否等于4(图3-3-2行24、25)。

3.3.6数组/指针/结构操作
字符串数组argv[]储存命令行各个参数,指向数组argv的指针用于访问到字符串数组argv[],数组存储于栈上的一段连续的空间内,访问时只需要起始地址和偏移量(图3-3-6)。

图3-3-6

3.3.7控制转移
(1)if:判断argc是否等于4,根据判断结果进行跳转(图3-3-2行24、25)。
(2)for:判断i是否小于等于7,根据判断结果进行跳转(图3-3-3行53、54)。

3.3.8函数操作
(1)参数传递:main函数传入的两个参数argc和argv分别存在%edi和%rsi中;printf函数用%edi、%rsi、%rdx传入字符串参数首地址、栈上数据的地址;exit函数用%edi传入参数1,sleep函数用%edi传入atoi(argv[3])。
(2)函数调用:汇编程序中“call 函数名”,如图3-3-6行43。
(3)局部变量:均存储在栈上,如main函数中的i,见图3-3-3行52、53。
(4)函数返回:main函数结束时返回0(图3-3-7)。

图3-3-7
3.4 本章小结
介绍编译的概念与作用,在Ubuntu下将hello.i进行了预处理得到hello.s文件,经过编译结果详细的解析,观察到了编译的过程与作用。

第4章 汇编
4.1 汇编的概念与作用
概念:汇编器将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中。
作用:将汇编语言转换为计算机能够理解并执行的二进制机器语言。
4.2 在Ubuntu下汇编的命令
汇编命令:gcc -c hello.s -o hello.o

图4-2-1
4.3 可重定位目标elf格式
4.3.1 ELF头
开始为一个16字节的序列,描述了生成该文件的系统的字的大小(64bytes)和字节顺序(小端);其余部分包含帮助链接器语法分析和解释目标文件的信息。

图4-3-1

4.3.2节头部表
记录了每个节的名称、类型、属性(读写权限)、在ELF文件中占的度、对齐方式和偏移量。

图4-3-2

4.3.3重定位节
每一行表示一条重定位条目,用于链接时,链接器找到并修改这些位置。
每个重定位条目分为:Offset——需要重定位的位置在.text(或.data)中的偏移;Info——高四字节为要重定位到的符号,低四字节标识出Type;Name——外部符号的名称(如sleep、atoi等);Addend——某些类型的重定位要用其对被修改引用的值做偏移调整。
从重定位条目中的信息,可以计算出重定位后的地址

图4-3-3

4.3.4符号表
存放在程序中定义和引用的函数和全局变量的信息,重定位需要引用的符号都在其中声明。

图4-3-4

4.4 Hello.o的结果解析
(1)机器语言是由0和1构成的,为了方便阅读,以16进制数的形式呈现在文档中,一条机器指令由操作码和操作数组成。
(2)与汇编语言的映射关系:一一对应。
(3)机器语言与汇编语言不一致:
操作数,机器语言是十六进制(图4-4-1行25);汇编语言是十进制;
分支转移,机器语言跳转是相对寻址(图4-4-1行24);汇编语言是绝对寻址;
函数调用,机器语言call使用被调用函数的相对偏移地址(图4-4-1行21),汇编语言call使用函数名称;
访问要重定位的地方,机器语言置0代表占位符(图4-4-1行16),等链接时需要重定位。

图4-4-1

4.5 本章小结
介绍汇编的概念与作用,在Ubuntu下将hello.s进行了预处理得到hello.o文件,经过查看elf格式、反汇编hello.o并与hello.s对比,观察到了汇编的过程与作用。

第5章 链接
5.1 链接的概念与作用
概念:链接器将多个.o可重定位的目标文件链接成为一个可加载可执行的目标文件hello。
作用:将多个如printf.o的单独的预编译好了的目标文件合并到我们的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-2-1
5.3 可执行目标文件hello的格式
用readelf -a hello命令,查看到ELF头、节头部表,可以查看各个段的信息(图5-3-2),与4.3中不同的是:address列不再是0而是具体地址,还可以看出每个节的大小,类型。

图5-3-1

图5-3-2
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,根据图5-3-1,可以知道hello的每个段被映射到了哪段虚拟内存中。以代码段和PHDR段为例进行对比分析,由图5-3-1可知,代码段偏移为0,被映射到虚拟地址0x0000000000400040,如图5-4-1,可知位于hello开头的代码被加载到虚拟地址为0x400000的地方,同理PFDR段位于偏移0x40处,被加载到了0x400040处。

图5-4-1
5.5 链接的重定位过程分析
5.5.1 hello与hello.o的不同
(1)函数地址发生变化:hello中main的起始地址为401125(图5-5-1行102);而hello.o中main的起始地址是从0开始,这也就导致了其他函数变量等地址的不同。
(2)call指令发生了变化:hello中, call指令后接的是绝对地址(有函数名)(图5-5-1行112);hello.o中call后接的是相对偏移地址。
(3)hello增加了hello.o中是不存在的节.init、.plt、.fini。
(4).链接增加新的函数:在hello中链接加入了在hello.c中用到的库函数,如exit、printf、sleep、getchar等函数。

图5-5-1

5.5.2链接的过程
(1)符号解析:目标文件定义和引用符号,每个符号对应于一个函数,一个全局变量或者一个静态变量,通过符号解析将每个符号引用正好和一个符号定义关联起来。
(2)重定位:编译器和汇编器生成从地址0开始的代码和数据节。链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使得它们指向这个内存位置。

5.5.3 hello中重定位的方法
(1)重定位和符号定义。在这一步中,链接器将hello.o所有类型相同的节合并成同一类型的新的聚合节。然后,链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。当这一步完成时,hello中的每条指令和全局变量都有唯一的运行时内存地址了。
(2)重定位节中的符号引用。链接器依赖hello.o中的可重定位条目,修改代码节和数据节中每个符号的引用,使得它们指向正确的运行时地址。
(3)计算需要被重定位的位置——refptr = .text + r.offset;计算运行时需要重定位的位置——refaddr = ADDR(.text) + r.offset;更新该位置——*refptr = (unsigned) (ADDR(r.symbol) + r.addend-refaddr)。
5.6 hello的执行流程
子程序名 程序地址
ld-2.31.so!_dl_start 0x7f94a9d4bdf0
ld-2.31.so!_dl_init 0x7f94a9d5bc20
hello!_start 0x4010f0
libc-2.31.so!__libc_start_main 0x7f94a9d5f560
hello!main 0x401125
hello!puts@plt 0x401030
hello!exit@plt 0x401070
hello!printf@plt 0x401040
hello!atoi@plt 0x401060
hello!sleep@plt 0x401080
hello!getchar@plt 0x401050
libc-2.31.so!exit 0x7f94a9d560d0

5.7 Hello的动态链接分析
对于变量而言,利用代码段和数据段的相对位置不变的原则计算正确地址。
对于库函数而言,GNU系统使用被称为延迟绑定的技术来动态链接共享库中的函数。这个过程需要两个数据结构来实现:GOT和PLT。GOT是数据段的一部分,其中每个被这个目标模块引用的全局数据目标都有一个8字节条目,编译器还为GOT中每个条目生成一个重定位记录,在加载时,动态链接器会重定位GOT中的每个条目,使得它包含目标正确的绝对地址;PLT是代码段中的一部分,初始存的是一批代码,它们跳转到GOT所指示的位置。
对于Hello,执行dl_init前,部分地址是空的(图5-7-1),执行dl_init后,这些地址不再为空(图5-7-2)。

图5-7-1

图5-7-2
5.8 本章小结
介绍链接的概念与作用,在Ubuntu下将hello.o进行了预处理得到hello文件,经过查看elf格式、反汇编hello并与hello.o的反汇编结果对比、详细具体地分析了重定位的过程与结果、比较动态链接前后内存中相关项目的变化,观察到了链接的过程与作用。

第6章 hello进程管理
6.1 进程的概念与作用
概念:进程的经典定义就是一个执行中程序的实例。在现代系统上运行一个程序时,我们会得到一个假象,就好像我们的程序是系统中当前运行的唯一的程序一样。我们的程序好像是独占地使用处理器和内存。处理器就好像是无间断地一条接一条地执行 我们程序中的指令。最后,我们程序中的代码和数据好像是系统内存中唯一的对象。这些假象都是通过进程的概念提供给我们的。
作用:每次运行程序时,shell创建一新进程,在这个进程的上下文切换中运行这个可执行目标文件。应用程序也能够创建新进程,并且在新进程的上下文中运行它们自己的代码或其他应用程序。进程提供给应用程序的关键抽象:一个独立的逻辑控制流,如同程序独占处理器;一个私有的地址空间,如同程序独占内存系统。
6.2 简述壳Shell-bash的作用与处理流程
作用:是一个交互型应用程序,作为命令解析器,代表用户运行其他程序。
处理流程:
(1)读取用户输入命令,对用户输入命令进行解析;
(2)判断是否为内置命令若为内置命令,是则调用内置命令处理函数,不是则在硬盘中查找该命令并将其调入内存,再将其解释为系统功能调用并转交给内核执行;
(3)shell 应该接受键盘输入信号,并对这些信号进行相应处理。
6.3 Hello的fork进程创建过程
在终端中输入“./hello 120L021328 苑沛科 1”后,shell判断这不是内部命令,就会调用fork函数,产生一个子进程,有与父进程相同的副本(包括数据段、代码、共享库、堆和用户栈),相同且独立的虚拟地址空间等,子进程读写父进程任意的文件。
一次fork会有两次返回值,通过返回值区分子进程(0)与父进程(子进程的PID)。
6.4 Hello的execve过程
在fork创建的子进程中调用execve函数,在当前进程的上下文中加载并运行可执行目标文件hello,且带参数列表argv和环境变量列表envp。只有当出现错误时,例如找不到hello这个filename,execve才会返回到调用程序。所以,正常情况下execve不返回。运行时,程序创建一个内存映像,在程序头部表的引导下,加载器将可执行文件的片复制到代码段和数据段,接下来,加载器跳转到程序的入口_start函数的地址,这个函数是在系统目标文件ctrl.o中定义的,对所有的c程序都一样。_start函数调用系统启动函数,_libc_start_main,该函数定义在libc.so里,初始化环境,调用用户层的main函数,处理main函数返回值,并且在需要的时候返回给内核。
6.5 Hello的进程执行
进程上下文信息:上下文就是内核重新启动一个被抢占的进程所需的状态。它由一些对象的值组成,这些对象包括通用目的寄存器,浮点寄存器,程序计数器,用户栈,状态寄存器,内核栈和各种内核数据结构。
进程时间片:一个进程执行它的控制流的每一时间段就成为时间片。如图6-5-1中进程A就由两个时间片组成。

图6-5-1

执行hello进程中,CPU不断切换上下文,使运行过程被切分成时间片,与其他进程交替占用CPU(如hello运行到sleep时),实现进程的调度。开始时是运行在用户模式,收到信号后进入内核模式,运行信号处理程序,之后再返回用户模式,实现用户态与核心态转换。
6.6 hello的异常与信号处理
6.6.1异常及处理
(1)中断 总是返回到下一条指令
(2)陷阱 总是返回到下一条指令
(3)故障 可能返回到当前指令
(4)终止 不会返回

6.6.2 键盘输入及处理
(1)不停乱按:进程并没有接收到信号,不会影响程序,程序继续进行(图6-6-1)。

图6-6-1

(2)回车:类似(1),但是回车会当作进程结束后的命令行,由终端读入(图6-6-2)。

图6-6-2

(3)Ctrl-Z:内核发送一个SIGTSTP信号给到前台进程组中的每个进程,进程收到停止信号,进程停止(图6-6-3)。输入ps命令,发现hello进程依旧存在(图6-6-4)。输入jobs命令,列出已启动的任务状态(图6-6-5)。输入pstree,列出进程间的联系(图6-6-6)。输入fg命令,使被挂起的进程调回前台继续运行(图6-6-7)。Ctrl-Z后输入kill命令,发送SIGKILL信号,杀死进程(图6-6-8)。

图6-6-3

图6-6-4

图6-6-5

图6-6-6

图6-6-7

图6-6-8

(4)Ctrl-C:内核发送一个SIGINT信号给到前台进程组中的每个进程,进程收到终止信号,进程终止(图6-6-9)。

图6-6-9

6.7本章小结
介绍进程的概念与作用,在Ubuntu下使用shell,用命令将进程显示出来观察到了hello进程的执行过程、键盘输入信号的处理过程,加深对进程与信号的理解。

第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:由程序产生的与段相关的偏移地址部分,也称相对地址,要经过寻址方式的计算或变换才得到内存储器中的物理地址。即hello中的偏移地址。
线性地址:地址空间中的地址是连续的。
虚拟地址:为了简化讨论,假设是线性地址空间。CPU从一个有N = 2n个地址的地址空间中生成虚拟地址,存在磁盘上,这个地址空间称为虚拟地址空间。即hello的虚拟内存。
物理地址:计算机系统的主存被组织成一个个由M个连续的字节大小的单元组成的数组。每字节都有一个唯一的物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部分组成:段标识符,段内偏移量offset。
段标识符:一个16位长的字段组成(图7-2-1)。高13位是索引号。低三位是TI和RPL。

图7-2-1
在GDT或LDT中的段,根据相应寄存器,得到其地址和大小,得到一个数组了。用索引号在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段,得到Base字段,它描述了一个段的开始位置的线性地址。全局的段描述符,存放在全局段描述符表(GDT)中,一些局部的段描述符,存放在局部段描述符表(LDT)中。TI = 0,选择全局描述符表(GDT);TI = 1,选择局部描述符表(LDT);GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。RPL = 00,为第0级,位于最高级的内核态,RPL = 11,为第3级,位于最低级的用户态,第0级高于第3级。
Base与offset结合得到对应的线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
将各进程的虚拟空间划分成若干个长度相等的页,页式管理把内存空间按页的大小分成片或者页面,然后把页式虚拟地址与内存地址建立一一对应页表,并用相应的硬件地址变换机构,来解决离散地址变换问题。页式管理采用请求调页或预调页技术实现了内外存存储器的统一管理。
地址翻译的过程见图7-3-1。

图7-3-1
7.4 TLB与四级页表支持下的VA到PA的变换
CPU计算出一个虚拟地址,将其传到MMU。MMU首先拿出虚拟地址的VPN查询TLB,若命中则直接将对应PTE中的物理页号与虚拟页偏移量组合起来形成物理地址,若没有命中。则去查询四级结构。然后构造出物理地址(图7-4-1)。

图7-4-1
7.5 三级Cache支持下的物理内存访问
L1为64组,8行,每行为64字节。所以把一个物理地址分为:组索引(CI)应该为log2(64)=6位;块内偏移(CO)也为6位;其余位为标记位(CT)。首先根据组索引进行组选择,然后根据标记位来选择对应的行,若该行有效位为0,则表示不命中,若有效位为1,则表示命中。不命中时,继续在L2查找,查找方法相同(可能组索引、行标记、块偏移的位数不同,依cache具体定),若找到了则要将其放进L1中。若L1中对于的组内有空闲的行,则直接放到该行内,否则要驱逐掉一行,有一种简单的策略是最不常用策略(LFU),它将会选择最近使用次数最少的一行替换掉。还有一种策略是最近最少使用策略(LRU),将会替换最后一次访问时间最久远的那一行。
L2、L3同理。
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID,为了给这个新进程创建虚拟内存,他创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork函数在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的概念。
7.7 hello进程execve时的内存映射
execve函数在当前进程加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下几个步骤:
(1)删除已存在的用户区域:删除当前进程虚拟地址的用户部分中已存在的区域结构。
(2)映射私有区域:为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆区域也是请求二进制零的,初始长度为零。图7-7-1概括了私有区域的不同映射。
(3)映射共享区域:hello程序与共享对象链接,这些共享对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
(4)设置程序计数器(PC)。execve做的最后一件事就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。下一次调度这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
execve只是建立了映射关系,并未加载程序到物理内存中,当开始调度这个进程开始运行的时候,产生缺页故障,然后才会所需要的页面加载进去。

图7-7-1
7.8 缺页故障与缺页中断处理
缺页故障:当指令引用一个虚拟地址,在 MMU 中查找页表时发现与该地址相对应的物理地址DRAM缓存不命中。
缺页异常调用内核中的缺页异常处理程序。若导致缺页异常的虚拟地址时合法的,则缺页异常处理程序会选择一个牺牲页,比如存放在PP3中的VP4。如果VP4已经被修改了,那么内核会将它复制回磁盘。内核会修改VP4的页表条目,反映出VP4不再缓存在主存中这一事实。接下来,内核从磁盘复制VP3到内存中的PP3,更新PTE3,然后返回。当异常处理程序返回时,它会重启导致缺页的指令,该指令会把导致缺页的虚拟地址重发送到地址翻译硬件。此时,再次执行导致缺页的指令。CPU将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面现在缓存在物理内存中,所以就会命中,地址翻译硬件正常处理。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。对每个进程,内核维护着一个变量brk,它指向堆的顶部。分配器将堆视为一组不同大小的块(block)的集合来维护。每个块是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式的保留为供应程序使用。空闲块保持空闲,直到它被应用所分配。已分配的块保持已分配,直到它被释放。
分配器分为:显示分配器,要求应用显示地释放任何已分配的块;隐式分配器,要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个快。
malloc函数返回一个指针,指向大小为至少size字节的内存块,这个块会为可能包含在这个块内的任何数据对象类型做对齐。若malloc遇到问题,就返回NULL,并设置errno。malloc不初始化它返回的内存。
记录空闲块的方法:隐式空闲链表、显式空闲链表、分离的空闲列表……
隐式空闲链表:空闲块通过头部中的大小字段隐含的链接着,分配器可以可以通过遍历堆中所有的块,从而间接的遍历整个空闲块的集合。当一个应用请求一个k字节的块时,分配器搜索空闲链表,查找一个大小足够放置所请求块的空闲块。分配器执行这种搜索的方式是由放置策略确定的。一些常见的策略是首次适配、下一次适配和最佳适配。一旦分配器找到一个匹配的块,他就必须做另外一个决定,那就是分配这个空闲块中多少的空间。若找不到合适的空闲块,一个选择是合并那些在内存中物理上相邻的空闲块来创建一些更大的空闲块。,如果还不够,就会通过sbrk函数,向内核请求额外的堆内存。
显式空闲链表:将空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。
分离的空闲链表:维护多个空闲链表,其中每个链表中的块有大致相等的大小。基本的分离存储方法有简单分离存储、分离适配等。
7.10本章小结
引入虚拟地址的概念,分析了如何通过虚拟地址,高效准确地访问到内存,加深了对fork和execve的认识,最后介绍了动态存储分配管理。

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:一个Linux文件就是一个m个字节的序列:B0, B1, ……, Bk, ……, B(m - 1). 所有的 IO 设备(例如网络、磁盘和终端)都被模型化为文件,所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式。
设备管理:允许Linux 内核引出一个简单低级的应用接口,称为 Unix I/O。
8.2 简述Unix IO接口及其函数
8.2.1 Unix IO接口
这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。
(1)打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需要记住这个描述符。
(2)Linux shell创建的每个进程开始时都有三个打开的文件:标准输入,标准输出和标准错误。
(3)改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k,初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek操作,显示地设置文件的当前位置为k。
(4)读写文件。一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。
(5)关闭文件。当应用完成了对文件地访问后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。
8.2.1 Unix IO函数
(1)open函数。将filename转换为文件描述符,并且返回描述符数字。返回地描述符总是在进程中当前没有打开地最小描述符。flag参数指明了进程打算如何访问这个文件。
(2)close函数。调用该函数关闭一个打开的文件。
(3)read函数。从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,而返回值0表示EOF。否则,返回值表示的是实际传送的字节数量。
(4)write函数。从内存位置buf复制至多n个字节到描述符fd的当前文件位置。
(5)lseek函数。调用该函数,应用程序能够显示地修改当前文件的位置。
8.3 printf的实现分析
printf函数的函数体如图8-3-1。参数列表中的“…”表示不定长参数。代码中arg就是获取不定长阐述列表的首地址,因为C语言参数压栈的方式是从右往左,并且栈是从高地址向低地址生长的,而fmt为字符指针,在32位系统中,其大小为4字节,所以(&fmt)+4就为…中的第一个参数地址。

图8-3-1
接下来的vsprintf函数如图8-3-2。传入格式控制串fmt和参数列表args,vsprintf函数会构造出最终要输出的字符串buf,并返回的一个整型,该整型就是要打印的字符串的长度。所以说:vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
接下来的write函数如图8-3-3。这段汇编是intel格式的,栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址,int INT_VECTOR_SYS_CALLA是通过系统调用sys_call。
接下来的sys_call的实现如图8-3-4。完成一个功能:显示格式化了的字符串

图8-3-2

图8-3-3

图8-3-4
从vsprintf生成显示信息,到write系统函数,到sys_call,然后字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。最后显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量),printf成功实现。
8.4 getchar的实现分析
getchar函数的实现代码如下:
int getchar(void)
{
static char buf[BUFSIZ];
static char* bb=buf;
static int n=0;
if(n==0)
{
n=read(0,buf,BUFSIZ);
bb=buf;
}
return (–n>=0)?(unsigned char)*bb++:EOF;
}
当程序调用getchar时,用户输入的字符被存放在键盘缓冲区中直到用户按回车为止。当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的第一个字符的ascii码,且将用户输入的字符回显到屏幕。其他字符会保留在键盘缓存区中,等待后续getchar调用读取。
异步异常-键盘中断的处理:用户从键盘输入,触发了一个中断信号,当前进程被抢占,进而开始执行键盘中断处理子程序。键盘中断处理子程将输入的字符序通过按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar底层实现使用了read系统函数,通过系统调用来触发一个中断信号,执行一次上下文切换,当前进程被挂起CPU去执其他进程,read读取按键ascii码,直到接受到回车键才返回,当read函数返回时,当前进程重新被调度,getchar获得输入的第一个字符。
8.5本章小结
介绍了Linux的IO设备管理办法、IO接口实现与相应的函数实现,分析了getchar()和printf()函数的实现。

结论
hello所经历的过程:
(1)源程序:编写C语言代码,得到最初的hello.c源程序;
(2)预处理:预处理器解析宏定义、文件包含、条件编译等,生成ASCII码的中间文件hello.i;
(3)编译:编译器生成一个ASCII汇编语言文件hello.s;
(4)汇编:汇编器将汇编指令翻译成机器语言,并生成重定位信息,生成可重定位目标文件hello.o;
(5)链接:链接器进行符号解析、重定位、动态链接等创建一个可执行目标文件hello;
(6)fork创建进程:在shell中运行hello程序时,shell会调用fork函数创建子进程,为之后hello程序的运行提供环境;
(7)execve加载程序:子进程中调用execve函数,加载hello程序,进入hello的程序入口点,hello开始运行了;
(8)运行阶段:内核负责调度进程,并对可能产生的异常及信号进行处理,MMU、TLB、多级页表、cache、DRAM内存、动态内存分配器相互协作,共同完成内存的管理,Unix I/O使得程序与文件进行交互;
(9)终止:hello进程运行结束,shell负责回收终止的hello进程,内核删除为hello进程创建的所有数据结构。

感悟:
虽然hello程序仅有短短23行,运行的功能也十分简单,但是要想实现它,需要计算机系统从硬件到软件的紧密配合,了解到这么细致的地步是枯燥乏味的,但是这对于理解计算机系统有很大的帮助,进而对今后从计算机系统的实现原理来完善代码意义重大。在探索hello程序一生过程中,不仅感叹于计算机系统的复杂与神奇,更希望自己能在此基础上,完成更具有创造性的开发,感受计算机系统更深层次的魅力。

附件
hello.i:预处理生成的文本文件hello.i
hello.s:编译产生的文本文件hello.s
hello.o:汇编后生成的可重定位目标文件hello.o
helloo_asm.txt:hello.o生成的hello.o的反汇编代码文件
hello:链接生成的可执行文件hello
hello_asm.txt:hello生成的hello的反汇编代码文件

参考文献
[1] RANDALE.BRYANT, DAVIDR.O’HALLARON. 深入理解计算机系统[M]. 机械工业出版社, 2016.7.
[2] https://blog.csdn.net/m0_45800663/article/details/103756290
[3] https://blog.csdn.net/Forival/article/details/124437732
[4] https://blog.csdn.net/weixin_42017042/article/details/85468924
[5] https://blog.csdn.net/m0_45170353/article/details/103784883
[6] https://www.cnblogs.com/pianist/p/3315801.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值