目录
第1章 概述
1.1 Hello简介
P2P:From Program to Process
图1.1.1 P2P流程图
020:From Zero-0 to Zero-0
操作系统调用execve后映射虚拟内存,先删除当前虚拟地址的数据结构并为hello创建新的区域结构,进入程序入口后载入物理内存,再进入main函数执行代码。执行完成后,父进程回收hello进程,内核删除相关数据结构。
1.2 环境与工具
Intel(R) Core(TM) i5-8265U CPU @ 1.60GHz
Win10教育版 64位
虚拟机Vmware Workstation Pro
Ubuntu20.04
Gcc ld readelf gedit objdump edb hexedit
1.3 中间结果
hello.i: 预处理生成的文本文件
helllo.s: .i文件编译后得到的汇编语言文件
hello.o: .s文件汇编后得到的可重定位目标文件
hello: .o文件经过链接生成的可执行目标文件
1.4 本章小结
本章主要介绍了P2P,020的定义,以及实验的一些基本信息。
第2章 预处理
2.1 预处理的概念与作用
概念:
预处理执行以#开头的命令(宏定义、条件编译、读取头文件)、删除注释等来修改c程序生成.i文件。
作用:
- 用实际值替换宏定义的字符串
- 文件包含:将头文件中的代码插入到新程序中
- 条件编译:根据if后面的条件决定需要编译的代码
2.2在Ubuntu下预处理的命令
在Ubuntu中输入:
gcc -E hello.c -o hello.i
图2.2.1 预处理截图
2.3 Hello的预处理结果解析
结果:
通过预处理得到的hello.i文件相较于hello.c文件发生了从23行到3060行的扩展。
解析:
hello.c的宏进行了宏展开,头文件的内容被引入。
2.4 本章小结
本章了解了预处理的概念与操作,同时对预处理得到的文件进行了解析。
第3章 编译
3.1 编译的概念与作用
概念:
利用编译程序处理.i文件,生成.s文件的过程。
作用:
进行词法分析、语法分析、目标代码的生成,检查无误后生成汇编语言。
3.2 在Ubuntu下编译的命令
在Ubuntu中输入:
gcc -S hello.i -o hello.s
图3.2.1 编译截图
3.3 Hello的编译结果解析
3.3.1 编译文件指令
图3.3.1 编译文件指令
.file: 声明源文件
.text: 声明代码段
.section .rodata: 只读数据段
.align: 声明对指令或数据的存放地址进行对齐的方式
.data: 声明数据段
.global: 声明全局变量
.size: 声明大小
.type: 声明类型
3.3.2 数据
1. 字符串
输出字符串作为全局变量保存,存储于.rodata节中,.s文件中两个字符串均为printf参数。
图3.3.2 程序中的字符串
图3.3.3 两字符串作为printf参数
2. 数组
有两个参数int argc,char *argv[], argv作为存放char指针的数组为第二个参数。数组的起始地址存放在栈中-32(%rbp)的位置,被两次调用找参数传给printf。
图3.3.4 数组argv
如图,我们可以发现:数组argv的存放位置为-32(%rbp),同时图中还展示了两次获取argv[1], argv[2]地址的过程。
3. 局部变量
main函数声明了一个局部变量i,编译器进行编译的时候将局部变量i放在堆栈中。如图所示,局部变量i放在栈上-4(%rbp)的位置。
图3.3.5 局部变量
3.3.3 全局函数
hello.c声明了一个全局函数main(int argc, char *argv[] ),经过编译后,这段汇编代码也说明main函数是全局函数。
图3.3.6 全局函数main
3.3.4 类型转换
hello.c中涉及的类型转换是atoi(argv[3])。
3.3.5 算术操作
Hello.c中的算术操作是i++,同时因为i是int类型的(四个字节),所以汇编代码通过addl实现
3.3.6 关系操作 && 控制转移指令
1. argc != 4: 这个条关系语句在汇编代码中的实现为cmpl $4, -20(%rbp),同时与je语句构成条件语句,表示若argc等于4则跳转,否则就继续向下执行。
图3.3.7 argc != 4
2. i < 7: 这个关系语句在汇编代码中的实现为cmpl $7,-4(%rbp),同时与jle构成条件语句,表示若i<7则跳转,否则就继续向下执行。
图3.3.8 i < 7
3.3.7 函数操作
hello.c涉及的函数操作有:main,printf,exit,sleep,getchar函数,函数的返回值存储在%eax中。
main函数的参数是argc和argv;
printf函数的参数是上述的两个字符串;
exit参数是1;
sleep参数是atoi(argv[3]);
3.3.8 赋值操作
汇编代码中赋值操作主要通过mov指令来实现,而mov的后缀取决于数据得类型:
movb: 一个字节
movw: 两个字节
movq: 四个字节
movl: 八个字节
3.4 本章小结
本章主要介绍了编译器的编译机制,涵盖编译器在编译阶段处理各种数据的操作以及C语言中各种类型与操作对应的汇编代码。
第4章 汇编
4.1 汇编的概念与作用
概念:
汇编语言翻译成机器语言的过程。
作用:
汇编器(as)将汇编程序翻译成机器语言指令并将其打包成重定位目标程序的格式,然后保存于.o目标文件中。
4.2 在Ubuntu下汇编的命令
输入指令:
gcc hello.s -c -o hello.o
图4.2.1 汇编命令
4.3 可重定位目标elf格式
- ELF Header
ELF Header中包含了Magic,类别,数据,版本,OS/ABI,ABI版本,类型,系统架构,版本,入口点地址,程序头起点,节头部起始位置,标志,头部大小,节头部大小、数量以及字节头部表的索引等信息。
图4.3.1 ELF Header
- Section Header
Section Header中包含了各节的名称、类型/全体大小、地址/标志/链接/信息、偏移/对齐等信息。
由于是可重定位目标文件,所以各节的地址都为0,然后利用偏移量的信息得到各节在文件中的起始位置。
同时观察标志信息,我们可以发现.text代码节是AX(可执行,但不可写),.data数据节是WA(可写,但不可执行),.rodata只读数据节是A(既不可写也不可执行)。
图4.3.2 Section Header
- 符号表.symtab
符号表.symtab中包含了18个entries(程序中定义与引用的函数与全局变量信息)的value(相对于目标节的起始位置偏移),大小,类型(数据/函数),局部/全局类型,vis,Ndx,名称等信息。
图4.3.3 符号表.symtab
- 重定位节.rela.text
重定位节.rela.text表示.text节中位置列表,包含了.text节进行重定位的信息。
重定位节中具体包含了偏移量(前四个字节是symbol(被修改引用应该指向的符号),后四个字节是type(重定位的类型))、信息、类型(指引链接器修改新的应用的信息)、符号值、符号名称(重定位到的目标的名称)、加数(一个有符号常数,用于对被修改引用的值做偏移调整)。
图4.3.4 重定位节.rela.text
4.4 Hello.o的结果解析
反汇编hello.o:objdump -d -r hello.o
图4.4.1 hello.o的反汇编
图4.4.2 hello.s
hello.o与hello.s比较:
1. 分支跳转:反汇编语言的jmp指令后面用的是具体的地址,而汇编语言的jmp指令后面用的是段名称。如:
图4.4.3 反汇编语言中的jmp指令
图4.4.4 汇编语言中的jmp指令
2. 函数调用:hello.o中call指令后面是下一条指令的地址,而hello.s中call指令后面直接使用的是调用函数的名称。如:
图4.4.5 反汇编语言中的call指令
图4.4.6 汇编语言中的call指令
而这是因为hello.c中调用的函数需要通过动态链接器才能确定调用函数运行时的执行地址,在其汇编为机器语言后,这些不确定地址函数的相对地址被设置为0,因此目标地址正好就是下一条指令地址。
4.5 本章小结
本章对hello.s进行汇编得到hello.o,并逐个分析了ELF Header、Section Header、.symtab、.rela.text中包含的信息,最后重点比较了hello.s与hello.o中的分支转移与函数调用的差别。
第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
图5.2.1 链接命令
5.3 可执行目标文件hello的格式
- ELF Header
类型为EXEC表明hello为可执行文件。
图5.3.1 ELF Header
- Section Headers
根据其中的地址、偏移量与大小信息,我们可以用Hexedit定位各个节所占区间的起始位置与大小。
图5.3.2 Section Headers
- 重定位节.rela.text
图5.3.3 .rela.text
- 符号表.symtab
图5.3.4 .symtab
5.4 hello的虚拟地址空间
使用edb加载hello,发现虚拟地址从0x401000开始,结束于0x401ff0(如下图)。通过5.3中的Section Headers,我们可以找到各个节的信息。
图5.4.1 hello虚拟地址起始
5.5 链接的重定位过程分析
输入objdump -d -r hello.o命令,得到hello.o的反汇编代码;输入objdump -d -r hello命令,得到hello的反汇编代码:
图5.5.1 得到hello的反汇编代码
以下为hello.out与hello.o文件中的代码:
图5.5.2 hello
图5.5.3 hello.o
比较hello与hello.o的反汇编代码,我们可以发现:
(1)hello的函数调用操作是直接通过地址进行调用,而hello.o则是通过地址偏移量进行调用。
(2)hello的lea指令后有具体的地址,而hello.o的lea指令后面的地址还是相对地址,说明hello已经完成了重定位,而hello.o尚未完成。
结合hello.o的重定位项目,分析hello中对其怎么重定位的:
(1)重定位节和符号定义链接器将所有类型相同的节合并为可执行目标文件的节,此后链接器就依次赋给新的节以及模块定义中的所有节、符号以运行时的内存地址。此时,程序中全局变量与所有指令都有唯一的运行时地址。
(2)当编译器遇到目标引用的位置未知的情况时就会生成一个重定位条目,当重定位符号引用时,链接器利用可重定位条目使代码节与数据节指向正确的运行时地址。
5.6 hello的执行流程
hello的执行流程:
- ld-2.31.so!_dl_start
- ld-2.31.so!_dl_init
- libc-2.31.so!_libc_start_main
- libc-2.31.so!_cxa_atexit
- hello!_libc_csu_init
- libc-2.31.so!_setjmp
- Libc-2.31.so!exit
图5.6.1 hello中main函数执行
5.7 Hello的动态链接分析
动态链接过程分析:
动态链接是将程序划分为不同模块,在程序运行时才将这些模块链接在一起形成一个完整程序。同时在形成可执行程序时,若发现一个外部函数,则会检查动态链接库。在调用共享函数库时,编译器会为这个不知道地址的函数生成一条重定位条目,在程序加载时再去解析它。GNU编译系统通过GOT和PLT来实现延迟绑定,将过程地址的绑定推迟到第一次调用时。其中,GOT是是数据段的一部分,PLT是代码段的一部分:
GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT [0]和GOT [1]包含动态链接器在解析函数地址时会使用的信息。GOT [2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。
PLT:PLT是一个数组,其中每个条目是16字节代码。PLT [0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。
根据hello的ELF Header中的信息,我们可知GOT起始表位置为0x403ff0。
图5.7.1 GOT起始表位置
通过edb查看GOT表内容,我们可以发现,在调用_start前,GOT表的16个字节全为0.
图5.7.2 调用_start前GOT表内容
调用_start之后发生改变,0x403ff0后两个8个字节分别变为0x7f564118afc0、0x0, 其中GOT [1](对应0x7f564118afc0、)包含动态链接器在解析函数地址时会使用的信息。GOT [2](对应0x00)是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数。
图5.7.3 调用_start后GOT表内容
5.8 本章小结
本章我们首先进行了通过对hello以及hello.o的反汇编进行了链接的重定位分析,然后使用edb对hello的执行过程进行了跟踪分析,最后我们分别查看了hello中调用_start前后,GOT表内容的变化进行了针对hello的动态链接的分析。
第6章 hello进程管理
6.1 进程的概念与作用
概念:
进程是一个执行中的程序实例。
作用:
进程为用户提供以下的假象:
- 我们的程序是系统当前运行的唯一程序一样,我们的程序独占处理器与内存。
- 处理器无间断的执行我们程序中的指令,我们程序中的代码和数据是系统内存中唯一对象。
6.2 简述壳Shell-bash的作用与处理流程
作用:
Shell是一个用C语言编写的程序,他是用户使用Linux的桥梁。Shell 是指一种应用程序,Shell应用程序提供了一个界面,用户通过这个界面访问操作系统内核的服务。
处理流程:
- 读取用户输入
- 分析输入的内容,获取命令行参数,并构造传递给execve的argv向量
- 检查第一个命令行参数是否是内置的shell命令,如果是则直接执行,否则调用相应的程序执行命令。
- 在程序运行期间,shell需要监视键盘的输入内容,并且做出相应的反应。
6.3 Hello的fork进程创建过程
fork()函数会创建一个与父进程完全相同但独立的子进程(包括代码段、段、数据段、共享库和用户栈),同时子进程还获得与父进程任何打开文件描述相同的副本,父进程与子进程的最大不同时它们的PID不相同。
父进程与子进程是并发运行的,在子进程执行期间,父进程默认是等待子进程完成。
6.4 Hello的execve过程
execve函数的功能室在当前子进程的上下文加载并运行一个新的程序,所需步骤如下:
首先删除已存在的用户区域,然后为新程序的代码、数据等创建新的区域结构(私有且写时复制)。若程序与共享对象链接,则都是动态链接,然后再映射到用户虚拟地址空间的共享区域。最后,execve会设置当前进程点的上下文中的PC,是指指向代码区域的入口点。
进程的地址空间如图所示:
图6.4.1 进程的地址空间
6.5 Hello的进程执行
上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的的状态,他有通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈与各种内核数据结构等对象构成。
时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
用户模式与内核模式:处理器通常使用一个寄存器提供两种模式的区分,该寄 存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中, 用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的 代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任 何命令,并且可以访问系统中的任何内存位置。
hello程序执行:
开始时hello程序运行在用户模式下,先输出hello 1190501809 熊皓,然后hello调用sleep函数,此时进程为内核模式。内核会请求主动释放当前进程,并将hello进程移出运行队列加入等待队列。定时器开始计时,内核进行上下文切换移交进程控制权,直到定时器发送一个中断信号时,进程恢复为内核模式执行中断处理,将hello程序重新加入运行队列,此时hello进程就可以继续自己的控制逻辑流了。
图6.5.1 hello程序执行
6.6 hello的异常与信号处理
- 异常
图6.6.1 异常类型
- 异常时产生的信号
图6.6.2 异常时产生的信号
- 键盘上各种操作导致的异常
- hello程序正常运行
图6.6.3 正常运行
- 乱按(包括回车)
发现乱按后按了回车只是将屏幕输入缓存到stdin,当getchar读到’/n’结尾的字符串时,其他字符串会被当做shell命令行输入。
图6.6.4 乱按(包括回车)
- ctrl-c
按了ctrl+c时,父进程收到SIGINT信号,终止并回收hello进程。
图6.6.5 ctrl-c
用ps查看前台进程,发现没有hello进程:
图6.6.6 ps
- ctrl-z
按了ctrl+z时,父进程收到SIGTSTP信号,将当前hello进程挂起:
图6.6.7 ctrl-z
用ps查看前台进程,发现hello进程并没有被回收:
图6.6.8 ps
输入fg 1将hello程序调到前台,hello程序继续执行,最后输入字符串,程序结束并被回收:
图6.6.9 fg
6.7本章小结
本章我们主要探究了hello程序的进程管理,首先我们探究了fork()函数创建子进程的问题,然后研究了execve()函数。最后,我们逐步探讨了hello进程的执行并对hello异常与信号,及不同输入导致的结果进行了分析。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:
程序经过编译后出现在汇编代码中的地址,用来指定一个操作数或者一条指令的地址,表示为[段标识符,段内偏移量]。
线性地址:
一个虚拟地址,对应硬件也是内存的转换前的地址。
虚拟地址:
同线性地址概念相同。
物理地址:
CPU通过地址总线的寻址,找到真实的物理内存对应地址。 CPU对内存的访问是通过连接着CPU和北桥芯片的前端总线来完成的。在前端总线上传输的内存地址都是物理内存地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部分组成,段标识符,段内偏移量。段标识符是一个16位长的字段组成,称为段选择符,其中前13位是一个索引号。后面三位包含一些硬件细节。
索引号,这里可以直接理解成数组下标,它对应的“数组”就是段描述符表,段描述符具体描述了一个段地址,这样,很多段描述符就组成段描述符表。可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。
Base字段表示的是包含段的首字节的线性地址,就是一个段的开始位置的线性地址。一些全局的段描述符就放在“全局段描述符表”(GDT,存在gdtr控制寄存器中)中,一些局部的段描述就放在“局部段描述符表”(LDT,存在ldtr寄存器中)中。
首先,给定一个完整的逻辑地址[段选择符:段内偏移地址],看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小,我们就有了一个数组了。拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样就得到了基地址。把Base + offset,就是要转换的线性地址了。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址(VA)到物理地址(PA)的变换通过MMU完成。CPU中的一个控制器寄存器,页表基址寄存器(PTBR)指向当前页表。n位的虚拟地址包含两部分:一个p位的虚拟页面偏移(VPO)和一个(n-p)位的虚拟页号(VPN),MMU利用VPN来选择适当的PTE,将页表条目中的物理页号(PPN)和虚拟地址中的VPO串联起来,就得到相应的物理地址。
图7.3.1 使用页表的地址翻译
7.4 TLB与四级页表支持下的VA到PA的变换
每次CPU产生一个虚拟地址,MMU(内存管理单元)就必须查阅一个PTE(页表条目),以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会从内存多取一次数据,代价是几十到几百个周期。如果PTE碰巧缓存在L1中,那么开销就会下降1或2个周期。然而,许多系统都试图消除即使是这样的开销,它们在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓存器(TLB)。TLB是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单一PTE组成的块。TLB通常有高的相联度,从虚拟地址中的页号提取出组选择和行匹配的索引和标记字段。
因为所有的地址翻译都是在芯片上的MMU中进行的,因此非常快。
压缩页表的常用方法是使用层次结构的页表,而使用多级页表层次结构进行地址翻译时,虚拟地址先被划分为k个VPN和1个VPO。每个VPN i都是一个到第i级页表的索引,第j级页表汇总的每个PTE包含某个物理页面的PPN,或者一个磁盘块的地址。为了能构造物理地址,在能够确定PPN之前, MMU必须访问k个PTE。
Core i7是四级页表进行的虚拟地址转物理地址。48位的虚拟地址的前36位被分为四级VPN区。结合存放在CR3的基址寄存器,由前面多级页表的知识,可以确定最终的PPN,与VPO结合得到物理地址。
图7.4.1 使用k级页表的地址翻译
7.5 三级Cache支持下的物理内存访问
对于一个虚拟地址请求,首先将去TLB寻找,看是否已经在TLB中缓存。如果命中的话就直接MMU获取,没有命中的话就先在结合多级页表,得到物理地址,去cache中找,到了L1里面以后,寻找物理地址又要检测是否命中,不命中则紧接着寻找下一级cache L2,接着L3。这里就是使用到CPU的高速缓存机制了,一级一级往下找,直到找到对应的内容。
7.6 hello进程fork时的内存映射
当fork()函数被shell进程调用时,内核为新进程创建各种数据结构,并分配给它一个pid。同时内核创建了当前进程的mm_struct、区域结构和页表的原样副本来给新进程创建虚拟内存,并将这个进程的每个页面都标记为只读和每个区域结构都标记为私有的写时复制。
图7.6.1 一个私有的写时复制对象
7.7 hello进程execve时的内存映射
execve函数在当前进程中加载并运行包含在可执行目标文件hello.out中的程序,用hello.out程序有效地替代了当前程序,加载并运行hello.out需要以下几个步骤:
- 删除已存在的用户区域,删除当前进程虚拟地址的用户部分中已存在的区域结构。
- 映射私有区域,为新程序的代码、数据、bss和栈区域创建新的区域结构(所有这些新的区域都是私有的、写时复制的)。
- 映射共享区域,如果hello.out程序与共享对象(或目标)链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间汇总的共享区域中。
- 设置程序计数器,execve做的最后一件事就是设置当前进程上下文中的程序计数器,是指指向代码区域的入口点。
下一次调度这个进程时,它将从这个入口点开始执行,Linux将根据需要换入代码和数据页面。
图7.7.1 加载器是如何映射用户地址空间的区域的
7.8 缺页故障与缺页中断处理
当指令引用一个相应的虚拟地址而改地址相应的物理页面不在内存中,这会出发缺页故障。通过查询页表PTE可以知道虚拟页在磁盘的位置。缺页处理程序从指定的位置加载页面 到物理内存中,并更新PTE。然后控制返回给引起缺页故障的指令。当指令再次执行时,相应的物理页面已经驻留在内存中,因此指令可以没有故障的运行完成。
7.9动态存储分配管理
动态内存分配维护着一个进程的虚拟内存区域,称为堆。分配器将堆视作一组不同大小的块的集合来维护,每个块就是一个连续的虚拟内存片(要么是已分配的,要么是空闲的)。空闲块保持空闲直到它显示地被应用所分配,一个已分配的块保持已分配状态,直到被释放。
分配器有两种风格,两种风格都要求应用显示地分配块,它们的不同处在于由哪个实体来负责释放已经分配的块:
- 显示分配器,要求应用显示地释放任何已分配的块。(例如C语言中的malloc与free函数)
- 隐式分配器(垃圾收集器),要求分配器检测一个已分配块何时不再被程序使用,那么就释放这个块。
图7.9.1 堆
动态内存分配主要有两种基本方法与策略:
- 隐式空闲链表
隐式空闲链表中一个块是由一个字的头部、有效荷载以及可能的填充组成:
图7.9.2 隐式空闲链表一个堆块的格式
空闲块通过头部块的大小字段隐含的连接着,因此分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。其中,一个设置了已分配的位而大小为零的终止头部将作为特殊标记的结束块。
图7.9.3 用隐式空闲链表来组织堆
- 显示空闲链表
显示空闲链表是将空闲块组织为某种形式的显示数据结构。堆被组织为一个双向空闲链表,在每个空闲块中,都包含一个前驱和后继的指针。在显式空闲链表中可以采用后进先出的顺序维护链表,将最新释放的块放置在链表的开始处,也可以采用按照地址顺序来维护链表,其中链表中每个块的地址都小于它的后继地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。
图7.9.4 显示空闲链表一个堆块的格式
7.10本章小结
本章我们主要探究hello的存储空间管理,首先我们分析了hello的存储地址空间,然后我们研究了地址的翻译问题。同时结合TLB与多级页表,探究了更快的VA向PA的转换过程。最后,我们就动态存储管理从显式分配器与隐式分配器(包含隐式空闲链表和显式空闲链表)进行了探讨。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单低级的应用接口,称为Unix I/O。
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
Unix I/O接口:
- 打开文件,一个应用程序通过要求内核打开相应的文件来宣告其想要访问一个I/O设备,内核返回描述符(一个小的非负整数),其将在后续对此文件的所有操作中标识这个文件。
- Shell创建的每个进程都有三个打开文件:标准输入,标准输出,标准错误。
- 改变当前文件位置,应用程序可通过执行seek显示地改变当前文件的位置k。
- 读写文件,一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的而文件,当k>=m时执行读操作会触发一个成为end-of-file(EOF)的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF符号”。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
- 关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。
Unix I/O函数:
(1)int open(char *filename, int flags, mode_t mode);
open函数将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags参数指明了进程打算如何访问这个文件。mode参数指定了新文件的访问权限位。作为上下文的一部分,每个进程都有一个umask,它是通过调用umask函数来设置的。当进程通过带某个mode参数的open函数调用来创建一个新文件时,文件的访问权限位被设置成mode&~umask。
(2)int close(int fd);
成功返回0错误返回EOF
(3)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的当前文件位置。
- off_t lseek(int handle, off_t offset, int fromwhere);
应用程序显式地修改当前文件的位置。
8.3 printf的实现分析
printf需要现接受一个fmt格式,然后将匹配到的参数按照fmt格式输出。同时printf函数中包含两个外部函数:
vsprintf:接受确定输出格式的格式字符串fmt,用格式字符串对个数变化的参数进行格式化,产生格式化输出。
write:将元素写到终端。
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
图8.3.1 printf函数
8.4 getchar的实现分析
程序调用getchar函数时,先是用户通过键盘输入字符(存于缓冲区中),直到用户按下回车。此时getchar函数才开始从缓冲区中读入字符(若有多的字符,则会存于缓冲区,直到下一次getchar进行读取),getchar的返回值为用户输入的第一个字符的ascill码,但有错误时会返回-1.
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
图8.4.1 getchar函数
8.5本章小结
本章我们主要探究hello的I/O管理的内容,首先我们分析了Unix I/O的接口内容以及Unix I/O涉及的函数,最后我们依次实现了printf函数与getchar函数。
结论
用计算机系统的语言,逐条总结hello所经历的过程:
hello.c | 源程序 |
hello.i | 预处理 |
hello.s | 编译 |
hello.o | 汇编,将汇编语言转换为机器语言 |
hello | 链接,生成可执行文件hello |
创建进程 | 在终端输入./hello,运行hello程序,shell用fork函数创建一个子进程 |
加载程序 | 通过加载器,调用execve函数删除原来进程内容,加载当前进程。运行hello时,MMU、TLB、多级页表、三级cache共同完成地址的请求。同时异常处理机制完成了对异常信号的处理。Unix I/O使程序能与外部文件进行读入、写出等交互操作。 |
终止 | 当hello运行完毕时,shell安排父进程回收子进程且删除为这个进程创建的所有数据结构。 |
对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法:
从hello.c源程序到最后hello运行完毕,我完整地操作并分析了操作系统、硬件与软件对hello.c操作的全过程。其中经历了很多阶段,且这些流程都是优化过的最适合的计算机流程,通过对这些阶段的分析探讨,我熟悉了计算机系统操作的大致流程,并且对操作系统以及硬件软件的了解更加深入了。
附件
列出所有的中间产物的文件名,并予以说明起作用。
Hello.c | 源程序 |
Hello.i | 预处理后的文本文件 |
Hello.s | 编译后的汇编程序文本文件 |
Hello.o | 汇编后的可重定位目标文件 |
hello | 链接后可执行目标文件 |
Hello1.txt | Hello.o的反汇编文件 |
Hello2.txt | hello的反汇编文件 |
Hello1elf.txt | ELF格式下的hello.o |
Hello2elf.txt | ELF格式下的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.