带图片的原文章地址:博客园
计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 航院自动化英才
学 号 7203610404
班 级 2036017
学 生 彭癸龙
指 导 教 师 史先俊
计算机科学与技术学院
2022年5月
本文以hello这个最简单的程序回顾了计算机系统如何实现一个程序的运行,从hello这个程序运行的“一生”回顾计算机系统里的重要概念。本文主要涉及编译链接、进程控制、内存管理和IO。
关键词:计算机系统;P2P;编译;链接;进程;虚拟内存;IO
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
6.2 简述壳Shell-bash的作用与处理流程... - 30 -
6.3 Hello的fork进程创建过程... - 31 -
7.2 Intel逻辑地址到线性地址的变换-段式管理... - 37 -
7.3 Hello的线性地址到物理地址的变换-页式管理... - 39 -
7.4 TLB与四级页表支持下的VA到PA的变换... - 40 -
7.5 三级Cache支持下的物理内存访问... - 42 -
7.6 hello进程fork时的内存映射... - 43 -
7.7 hello进程execve时的内存映射... - 43 -
第1章 概述
1.1 Hello简介
Hello的P2P是指program to process, 由程序到进程。也就是从我们编写的c语言程序到进程的过程。Hello.c 文件经过cpp预处理得到hello.i,而后经过ccl生成hello.s, 在之后经过汇编器as得到hello.o,之后链接器ld将其转换为hello可执行目标文件,最后shell为hello进行fork创建子进程,hello就成了process。
Hello的020即 from zero-0 to zero-0,说的是内存数据的从无到有再到无。初始时内存里没有hello的相关数据,因此是from zero-0,通过shell中的execute函数将hello载入内存为其分配空间,当程序结束后,hello进程被回收,内核删除内存里关于hello的数据,完成to zero-0.
1.2 环境与工具
本机:cpu:ryzen5800H RAM:3200MHz 16GB Samsung
虚拟环境:vmware16pro Ubuntu20.04
1.3 中间结果
Halo.asm hello的反汇编
Halo.elf hello的elf格式
Hello hello程序的可执行程序
Hello.asm hello.o的反汇编
Hello.c 源程序
Hello.elf hello.o的elf格式
Hello.i hello的预处理文件
Hello.o hello的可重定位目标文件,为汇编后的结果
Hello.s hello的汇编文件
1.4 本章小结
本章介绍hello从诞生到执行结束的过程,阐述了p2p和020的含义。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理是指预处理器(cpp,C Pre-Processor)根据以字符#开头的命令,修改原始的C程序的过程。
预处理的作用是由预处理器来进行注释删除、包含其他文件、以及执行宏替代,方便后续的编译。
2.2在Ubuntu下预处理的命令
Figure 1 预处理操作
2.3 Hello的预处理结果解析
打开经过预处理后的hello.i文件,发现文件有3060行,远远多余之前的程序,仔细观察发现#后include的头文件已经被插入其中,所有注释都已经删除。在hello.i
文件开始的几行内也记录了被插入的头文件的位置。
Figure 2 插入头文件的位置
Figure 3 预处理后产生的文件
2.4 本章小结
本章对预处理的概念和作用进行了说明,并对hello.c进行了预处理操作以验证。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译是指通过编译器将程序从便于人编写、阅读、维护的高级计算机语言所写作的源代码程序,翻译为计算机能解读、运行的低阶机器语言的过程。
编译的作用是产生目标语言(target language),即汇编语言,为后续的汇编程序准备。
3.2 在Ubuntu下编译的命令
键入命令:gcc -m64 -Og -no-pie -fno-PIC hello.i -o hello.s -S,可生成hello.s文件。
Figure 4 编译命令
3.3 Hello的编译结果解析
3.3.1 数据
程序中包含了字符串常量,在.s文件内如下
Figure 5 字符串常量
文件内也有进行for循环时使用的局部变量i,存在ebp寄存器内。
Figure 6 寄存器内的i变量
程序内的argc和argv作为main函数的参数,一般在edi和rdi内,在调用函数之前有寄存器传递参数。
Figure 7 edi存argc
Figure 8 rbx存argv地址,调用atoi时传递
3.3.2 赋值
在c语言程序中没有出现赋值 = 操作。
3.3.3 类型转换
C程序中未出现类型转换操作
3.3.4 sizeof
C语言程序中未出现sizeof
3.3.5 算数操作
For循环里每个循环对i都进行加1操作,在.s文件内如下
Figure 9 算数操作
3.3.6 逻辑/位操作
C语言源程序内没有逻辑/位操作。
3.3.7 关系操作
For循环每次进行判断应用了==关系操作,在文件内如下
Figure 10 关系操作
每个循环开始前与7比较大小进行==关系操作。
3.3.8 数据/指针/结构操作
在for循环的每个循环内需要对数据内元素进行访存操作,在文件内如下
Figure 11 通过寄存器内元素地址进行数组元素访存
3.3.9 控制转移
源程序中运用了for循环,在文件内如下
Figure 12 控制转换-for
同时程序内还含有if,在文件内如下
Figure 13 控制转移-if
3.3.10 函数操作
源程序调用了printf、atio、sleep、getchar、exit函数,在文件内如下
Figure 14 printf函数
寄存器Rdx,rsi,edi存储的都为printf函数的参数,eax为printf函数的返回值,在调用前置零。
Figure 15 atio函数
寄存器rdi存储着atio函数的参数,eax寄存器存储着该函数的返回值。
Figure 16 sleep函数
该函数的参数储存在edi寄存器内
Figure 17 调用getchar函数
Figure 18 调用exit函数
3.4 本章小结
本章介绍了编译的概念和作用,编译为后续生成二进制机器码做准备。以生成的hello.s为例,分析了编译的结果。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
使用汇编语言编写的源代码,然后通过相应的汇编程序(assembler)将它们转换成可执行的机器代码。这一过程被称为汇编过程。
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
汇编的作用将汇编语言等价翻译为机器语言,生成可重定位目标程序。
4.2 在Ubuntu下汇编的命令
输入的汇编命令为:gcc -m64 -Og -no-pie -fno-PIC hello.s -o hello.o -c
Figure 19 汇编过程
4.3 可重定位目标elf格式
在终端内输入readelf -a hello.o > hello.elf指令以获得.elf文件,内容为hello.o的ELF格式:
Figure 20 hello.o的ELF格式
- ELF头
含有Magic、类别、数据、版本等系统基本信息
Figure 21 ELF头
- 节头
包含节的名称、大小、地址、偏移量信息
Figure 22 节头
- 程序头
Figure 23 无程序头
- 重定位节
包含了c语言中的两个字符串,puts、exit、printf、atoi、sleep、getchar函数。偏移量:本数据成员给出重定位所作用的位置。对于重定位文件来说,此值是受重定位作用的存储单元在节中的字节偏移量;对于可执行文件或共享ELF文件来说,此值是受重定位作用的存储单元的虚拟地址。
信息:本数据成员既给出了重定位所作用的符号表索引,也给出了重定位的类型。
类型:重定位到的目标类型。
加数:计算重定位位置的辅助信息。
Figure 24 重定位节
- 符号表
包含了偏移量、大小、类型、全局或者局部的信息,例如main函数就是偏移量为0的109字节全局函数。
Figure 25 符号表
4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
Figure 26 生成反汇编文件
打开反汇编的文件,首先发现文件很小,只有main函数部分,与之前lab中拆炸弹的反汇编文件不一样,bomb的反汇编文件有许多系统的库函数的反汇编。通过与hello.s进行比较差异如下:
- 分支转移
Hello.s内分支转移通过段.L1来转移,而反汇编内通过偏移量计算地址来进行转移
Figure 27 分支转移
- 函数调用
Hello.s内函数调用直接call+函数名,而在反汇编里采用call+地址的方式进行函数调用,并且此时未经过链接器链接,文件内也没有库函数的反汇编,call之后的偏移量全为0,待链接器链接能够确定函数位置时call的偏移量便不再为0。
Figure 28 函数调用
- 常数的表示
在反汇编文件内常数表达为16进制,而在Hello.s内则为十进制。
Figure 29 24表示为16进制的0x18
4.5 本章小结
本章回顾了编译的概念和作用,以hello.o为例分析验证编译的结果,比较反汇编文件和Hello.s文件的区别。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
链接是指通过链接器将多个可重定位目标文件与库链接为一个可执行文件的过程。
链接将一个程序的整体氛围几个模块,让整个工程分散化不那么复杂,也方便修改和查看。
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
Figure 30 链接过程
5.3 可执行目标文件hello的格式
使用❯ readelf -a hello > halo.elf生成halo.elf
Figure 31 生成ELF文件过程
分析文件的内容为
- ELF头
与hello.elf的基本相同,但是类型、入口点地址、程序头起点等存在不同。
Figure 32 ELF头
- 节头
与hello.elf相比,节头内容更多,并且地址和对齐的信息发生改变。节头包含了名称、类型、地址、偏移量、大小、全体大小、旗帜、链接、信息、对齐的内容。
Figure 33 新的节头
- 程序头
相比于hello..elf没有程序头,halo.elf内包含了程序头。程序头表 (program header table) 是一个结构体数组,数组中的每个结构体元素是一个程序头 (program header),每个程序头描述一个段 (segment)。
- dynamic section
相较于hello.elf多出了一个dynamic section,其中包含了动态库,以及需要立即加载与否和符号表。
Figure 34 dynamic section
- 重定位节
重定位节与Hello.elf相比名称进行了变更,信息进行了更新。
Figure 35 重定位节
- symbol table
符号表更新了变得更长,保存着定位、重定位程序中符号定义和引用的信息,所有重定位需要引用的符号都在其中有声明。
Figure 36 符号表
5.4 hello的虚拟地址空间
使用symbols查看,发现hello加载时内存里各段的地址与elf表内地址相同,例如.interp段。
Figure 37 使用symbols查看
5.5 链接的重定位过程分析
经过链接后的hello多了许多内容,具体如下。
Figure 38 objdump查看反汇编
- 多了许多后缀.plt的函数,这是应为链接后将动态库内的函数反汇编加入到可执行文件之中,例如printf, getchar, atoi, exit等等。
Figure 39 新增的函数反汇编
- 函数调用,call之后是明确的内存地址,这是链接之后的结果。而不再是未链接之前偏移量为0的call。
Figure 40 函数调用的不同
- 跳转后接上内存的虚拟地址,而不是未经过链接的非地址的小数字。
Figure 41 跳转的不同
5.6 hello的执行流程
Figure 42 程序名称
5.7 Hello的动态链接分析
编译器没有办法预测函数的运行时地址,所以需要添加重定位记录,等待动态链接器处理,为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,在GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数,在加载时,动态链接器会重定位GOT中的每个条目,使得它包含目标的正确的绝对地址。
在elf节头内可知.got.plt在地址00404000.
Figure 43 函数调用前
Figure 44 函数调用后
可以看见.got.plt节内容发生了更新。
5.8 本章小结
本章回顾了链接的概念和作用,并且得到了可执行文件,然后将可执行文件的elf格式以及反汇编同hello.o的elf格式和反汇编进行比较,进而验证了链接的作用加深了理解。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程是一个正在运行的程序的实例。(In computing, a process is the instance of a computer program that is being executed by one or many threads.)
进程为程序提供了两个抽象:
- 逻辑控制流,每个进程好像都在独立的占用cpu,其背后的机制是由内核完成的
- 私有地址空间,每个程序好像独立的占用内存系统。
6.2 简述壳Shell-bash的作用与处理流程
Shell(也称为壳层)在计算机科学中指“为用户提供用户界面”的软件,通常指的是命令行界面的解析器。一般来说,这个词是指操作系统中提供访问内核所提供之服务的程序。Shell也用于泛指所有为用户提供操作界面的程序,也就是程序和用户交互的层面。因此与之相对的是内核(英语:Kernel),内核不提供和用户的交互功能。
Shell的处理流程为读取命令,分析命令参数,识别是否为内置命令,判断正误,执行。
Figure 45 shell的流程
6.3 Hello的fork进程创建过程
打开shell输入./hello 7203610404 彭癸龙 1, hello后是该执行该文件的参数。Shell会识别该命令行为非内置命令,之后在当前目录下用输入的参数执行程序,这是用fork函数创建子进程实现的,子进程和父进程有相同的进程上下文,但是pid不同,父进程可以回收子进程。当父进程不存在而子进程需要回收时则通过init进程回收僵尸子进程。
Figure 46 shell内运行hello
6.4 Hello的execve过程
在创建子进程后会通过execve函数载入我们编写的hello程序。加载器删除子进程原始的上下文(上下文这时和shell相同),并初始化新的进程上下文。通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容。最后设置PC指向程序入口点,完成对可执行程序的execve加载过程,这时进程上下文就是hello程序的进程上下文。
6.5 Hello的进程执行
在程序运行时,Shell为hello fork了一个子进程,这个子进程与Shell有独立的逻辑控制流。在执行sleep函数之前一直是hello的时间片,hello的进程也不会被抢占。当hello调用sleep函数时,其效果是进入内核模式,内核处理之后释放当前hello进程切换为其他进程。同时将 hello 进程从运行队列加入等待队列,并开始由计时器进行计时。当计时达到参数的要求时触发异常控制流,进入内核模式。系统从当前进程切换为等待序列中的hello进程,进行了进程上下文切换,又由内核模式转为用户模式。此时 hello 进程执行逻辑控制流。
6.6 hello的异常与信号处理
正常运行的结果:
Figure 47 不输入时正常运行
在程序执行时键入回车,程序仍然正常执行,只是中间有空行。
Figure 48 键入回车
当键入ctrl+c时程序提前结束了,这是因为shell进程收到了SIGSTP信号,信号处理程序将hello进程回收。
Figure 49 键入ctrl+c提起停止
当输入ctrl+z时程序被挂起。
Figure 50 键入ctrl+z挂起
然后键入ps jobs pstree fg kill命令,结果如下。
Figure 51 键入ps
Figure 52 键入jobs
Figure 53 键入pstree展示进程树
Figure 54 键入kill杀死进程
Figure 55 不停乱按
6.7本章小结
本章回顾了进程的概念和作用,和shell的概念和作用。以hello为例分析进程,在shell中运行hello。并且在执行进程中通过尝试键入不同的输入达到从逻辑控制流到异常控制流的效果,验证了信号和异常控制流的知识。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
1. 逻辑地址
逻辑地址是指由程序产生的与段相关的偏移地址,逻辑地址由选择符和偏移量两部分组成。具体而言,其为hello.asm中的相对偏移地址。
2. 线性地址
逻辑地址经过段机制转化后为线性地址,方便处理器进行寻址,用于描述程序分页信息的地址。具体以hello而言,线性地址标志着 hello 应在内存上哪些具体数据块上运行。
3. 虚拟地址
即为上述线性地址。
4. 物理地址
CPU通过地址总线的寻址,找到真实的物理内存对应地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
英特尔为了解决巨大的内存空间问题,引入了一个中间结构体,段描述符。并增设两个寄存器GDTR和LDTR。段描述符占8个字节定义如下。
通过段描述符,我们能够得到如下信息:
段的基址,由B31-B24/B23-B16/B15-B0构成,一共32位,基址可以是4GB空间中任意地址;
段的长度,由L19-L16/L15-L0构成,一共20位。如果G位为0,表示段的长度单位为字节,则段的最大长度是1M,如果G位为1,表示段的长度单位为4kb,则段的最大长度为1M*4K=4G。假定我们把段的基地址设置为0,而将段的长度设置为4G,这样便构成了一个从0地址开始,覆盖整个4G空间的段。访存指令中给出的“逻辑地址”,就是放到地址总线上的“物理地址”,这有别于“段基址加偏移”构成的“层次式”地址。
段的类型,代码段还是数据段,可读还是可写
Figure 56 段描述符定义
十六位段寄存器的称之为段选择符,其定义如下
Figure 57 段选择符定义
通过一个索引,可以定位到段描述符,进而通过段描述符得到段基址。段基址与偏移量结合就得到了线性地址,虚拟地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
概念上而言, 虚拟内存被组织为一个由存放在磁盘上的 N 个连续的字节大小的单元组成的数组。 每字节都有一个唯一的虚拟地址, 作为到数组的索引。 磁盘上数组的内容被缓存在主存中。 和存储器层次结构中其他缓存一样, 磁盘( 较低层) 上的数据被分割成块,这些块作为磁盘和主存( 较高层) 之间的传输单元。 VM 系统通过将虚拟内存分割为称为虚拟页( Virtual Page, VP)的大小固定的块来处理这个问题。每个虚拟页的大小为 P= 2〃 字节。 类似地, 物 理 内 存 被 分 割 为 物 理 页( Physical Page, PP ), 大小也为 P 字节( 物理页也被称为页帧( page frame))。同任何缓存一样, 虚拟内存系统必须有某种方法来判定一个虚拟页是否缓存在DRAM 中的某个地方。这些功能是由软硬件联合提供的, 包括操作系统软件、 MMU(内存管理单元) 中的地址翻译硬件和一个存放在物理内存中叫做页表(page table)的数据结构 ,页表将虚拟页映射到物理页。每次地址翻译硬件将一个虚拟地址转换为物理地址时,都会读取页表。操作系统负责维护页表的内容,以及在磁盘与 DRAM 之间来回传送页。页表就是一个页表条目( Page Table Entry,PTE)的数组。虚拟地址空间中的每个页在页表中一个固定偏移量处都有一个 PTE。有效位表明了该虚拟页当前是否被缓存在 DRAM 中。 如果设置了有效位,那么地址字段就表示 DRAM 中相应的物理页的起始位置, 这个物理页中缓存了该虚拟页。如果没有设置有效位,那么一个空地址表示这个虚拟页还未被分配。
Figure 58 页表
7.4 TLB与四级页表支持下的VA到PA的变换
TLB 是一个小的、 虚拟寻址的缓存, 其中每一行都保存着一个由单个 PTE 组成的块。TLB 通常有高度的相联度。
图56展示了当 TLB 命中时(通常情况)所包括的步骤。 这里的关键点是, 所有的地址翻译步骤都是在芯片上的 MMU 中执行的,因此非常快。
Figure 59 TLB命中
当 TLB 不命中时,MMU必须从L1缓存中取出相应的PTE, 如图57所示。 新取出的PTE 存放在TLB 中,可能会覆盖一个已经存在的条目。
Figure 60 TLB不命中
图58描述了使用k级页表层次结构的地址翻译。 虚拟地址被划分成为k个 VPN 和1 个 VPO。 每个 VPN i都是一个到第i级页表的索引其中1<=i<=k,第j级页表中的每个PTE,1<=j<=k-1,都指向第 j+1 级的某个页表的基址。第k级页表中的每个 PTE 包含某个物理页面的 PPN, 或者一个磁盘块的地址。为了构造物理地址,在能够确定 PPN之前,MMU 必须访问k个 PTE。对于只有一级的页表结构, PPO 和 VPO 是相同的。访问k个 PTE, 第一眼看上去昂贵而不切实际。 然而, 这里 TLB 能够起作用,正是通过将不同层次上页表的 PTE 缓存起来。实际上,带多级页表的地址翻译并不比单级页表慢很多。
Figure 61 多级页表加速地址翻译
7.5 三级Cache支持下的物理内存访问
使用物理寻址,多个进程同时在高速缓存中有存储块和共享来自相同虚拟页面的块成为很简单的事情。而且高速缓存无需处理保护问题,因为访问权限的检查是地址翻译过程的一部分。图 59展示了一个物理寻址的高速缓存如何和虚拟内存结合起来。主要的思路是地址翻译发生在高速缓存查找之前。如果在一级cache中命中PA对应的块,就根据CO将要找的数据传给cpu。如果在一级cache中找不到对应的数据,就得根据相同的地址去二级,三级甚至主存中寻找数据。在二,三级cache中找到数据还需要在一级cache中根据具体情况选择是否要进行驱逐和替换。
Figure 62
7.6 hello进程fork时的内存映射
当fork函数被当前进程hello调用时,内核为新进程hello创建各种数据结构,并分配给它一个唯一的PID。为了给这个新的hello创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当着两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve函数加载并运行hello需要以下几个步骤:
1. 删除已存在的用户区域
删除当前进程hello虚拟地址的用户部分中的已存在的区域结构。
2. 映射私有区域
为新程序的代码、数据、bss和栈区域创建新的私有的、写时复制的区域结构。其中,代码和数据区域被映射为hello文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。
3. 映射共享区域
若hello程序与共享对象或目标链接,则将这些对象动态链接到hello程序,然后再映射到用户虚拟地址空间中的共享区域内。
4. 设置程序计数器
最后,execve设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
图 60 展示了在缺页之前我们的示例页表的状态。CPU 引用了VP 3中的一个字VP 3并未缓存在DRAM中.地址翻译硬件从内存中读取 PTE 3 , 从有效位推断岀 VP 3 未被缓存,并且触发一个缺页异常。缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,在此例中就是存放在 PP 3 中的 VP 4。如果 VP 4 已经被修改了,那么内核就会将它复制回磁盘。无论哪种情况,内核都会修改VP 4的页表条目,反映出 VP 4 不再缓存在主存中这一事实。
Figure 63 缺页之前
接下来,内核从磁盘复制 VP 3 到内存中的 PP 3, 更新 PTE 3, 随后返回。 当异常处理程序返回时,它会重新启动导致缺页的指令,该指令会把导致缺页的虚拟地址重发送到地址翻译硬件。但是现在,VP 3已经缓存在主存中了,那么页命中也能由地址翻译硬件正常处理了。图 61展示了在缺页之后我们的示例页表的状态。
Figure 64 缺页之后
7.9动态存储分配管理
动态内存管理一般由动态内存分配器维护,分配器有两种风格,显示和隐式,两者的区别在于前者要求应用显式的释放任何已分配块,而后者则自动检测一个已分配块何时不再被程序使用。不过两者都要求显式地分配空闲块。
C 标准库提供了一个称为 malloc 程序包的显式分配器。程序通过调用 malloc 函数来从堆中分配块。动态内存分配器, 例如 malloc, 可以通过使用 mmap 和 munmap 函数, 显式地分配和释放堆内存,或者还可以使用 sbrk 函数。
我们可以将堆组织为一个连续的已分配块和空闲块的序列的结构成为隐式空闲链表。分配器可以通过遍历堆中所有的块, 从而间接地遍历整个空闲块的集合。隐式空闲链表的优点是简单。显著的缺点是任何操作的开销,例如放置分配的块, 要求对空闲链表进行搜索,该搜索所需时间与堆中已分配块和空闲块的总数呈线性关系。很重要的一点就是意识到系统对齐要求和分配器对块格式的选择会对分配器上的最小块大小有强制的要求。
7.10本章小结
本章回顾了虚拟内存管理,如何用段式管理实现虚拟地址到物理地址的翻译,如何使用缓存、多级页表、TLB加速这个翻译过程。之后讨论了hello在fork和execve后的内存映射。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
一个 Linux 文件就是一个 771 个字节的序列。所有的 I/O 设备(例如网络、 磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单、低级的应用接口,称为UnixI/O, 这使得所有的输人和输出都能以一种统一且一致的方式来执行。
8.2 简述Unix IO接口及其函数
1. 接口
1. 打开文件。 一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O 设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。 应用程序只需记住这个描述符。
2. Linux shell 创建的每个进程开始时都有三个打开的文件: 标准输入(描述符为 0)、 标准输出(描述符为 1)和标准错误(描述符为 2)。头文件< unistd.h> 定义了常量 STDIN_FILENO、STDOUT FILENO和STDERR FILENO,它们可用来代替显式的描述符值。
3. 改变当前的文件位置。 对于每个打开的文件,内核保持着一个文件位置 k, 初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行 seek 操作,显式地设置文件的当前位置为k。
4. 读写文件。一个读操作就是从文件复制n>0 个字节到内存,从当前文件位置k开始,然后将k增 加 到k+n,给定一个大小为m字节的文件,当k>=m时执行读操作会触发一个称为 end-of-file(EOF)的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的 “EOF 符号”。
类似地,写操作就是从内存复制n>0 个字节到一个文件, 从当前文件位置k开始,然后更新k。
- 关闭文件。 当应用完成了对文件的访问之后, 它就通知内核关闭这个文件。 作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
2.函数
int open(char *filename, int flags, mode_t mode);
open 函数将 filename 转换为一个文件描述符, 并且返回描述符数字。 返回的描述符总是在进程中当前没有打开的最小描述符。
int close(int fd);
ssize_t read(int fd, void *buf, size_t n);
ssize_t write(int fd, const void *buf, size .t n);
read 函数从描述符为 fd 的当前文件位置复制最多 n 个字节到内存位置 buf。返回值-1表示一个错误,而返回0表示EOF。否则,返回值表示的是实际传送的字节数量。
write 函数从内存位置 buf 复制至多n个字节到描述符 fd 的当前文件位置。
8.3 printf的实现分析
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;
}
在形参列表里有这么一个token:...,这个是可变形参的一种写法。当传递参数的个数不确定时,就可以用这种方式来表示。va_list的定义:typedef char *va_list这说明它是一个字符指针。其中的: (char*)(&fmt) + 4) 表示的是...中的第一个参数。
再看Windows下的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。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
再看printf里的write
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
给几个寄存器传递了参数,然后以一个int INT_VECTOR_SYS_CALL结束。INT_VECTOR_SYS_CALL代表通过系统调用syscall,而syscall通过如下实现
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
通过直接写显存的方式输出字符串。
8.4 getchar的实现分析
Getchar调用系统函数read,发送中断信号,内核进而抢占进程,等待用户输入字符串,输入字符和回车后,这些都保存到缓存区内,然后再次发送信号,进行上下文切换重新回到进程的逻辑控制流,getchar从缓存区读取字符串。
8.5本章小结
本章介绍了linux系统关于IO的接口和函数,之后对printf和getchar进行解析。
(第8章1分)
结论
Hello的程序进程基本过程同大作业的章节顺序一样。
从program到process,经历了预处理、编译、汇编、链接、加载运行的过程。而从存储空间的角度,execve将进程对应的代码和数据加载如虚拟内存空间,程序以此为基础执行,经历执行指令和访存的过程,在printf函数出还会调用malloc,同时逻辑流会被异常流打断,这是为了处理信号,最后hello被父进程回收,内核会回收内存里hello的数据,这便是020.
我认为计算机系统是个相对抽象的系统,他的设计和实现运用了挺多抽象和创造概念,也运用了类似流水线这种现实中非常使用的概念,因而计算机系统是个综合而抽象的系统。同时作为一个系统它非常具有体系和结构,从最基础的数据的存储和处理,再到cpu流水线和访存、存储结构,再到系统与人的交互,每个部分都考虑周全。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
Halo.asm hello的反汇编
Halo.elf hello的elf格式
Hello hello程序的可执行程序
Hello.asm hello.o的反汇编
Hello.c 源程序
Hello.elf hello.o的elf格式
Hello.i hello的预处理文件
Hello.o hello的可重定位目标文件,为汇编后的结果
Hello.s hello的汇编文件
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[转]printf 函数实现的深入剖析 - Pianistx - 博客园
(参考文献0分,缺失 -1分)