2018 HIT CSAPP 大作业:Hello的一生

目 录

第1章 概述 - 4 -
1.1 Hello简介 - 4 -
1.2 环境与工具 - 4 -
1.3 中间结果 - 4 -
1.4 本章小结 - 5 -
第2章 预处理 - 6 -
2.1 预处理的概念与作用 - 6 -
2.2在Ubuntu下预处理的命令 - 6 -
2.3 Hello的预处理结果解析 - 6 -
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的结果解析 - 16 -
4.5 本章小结 - 17 -
第5章 链接 - 18 -
5.1 链接的概念与作用 - 18 -
5.2 在Ubuntu下链接的命令 - 18 -
5.3 可执行目标文件hello的格式 - 18 -
5.4 hello的虚拟地址空间 - 19 -
5.5 链接的重定位过程分析 - 20 -
5.6 hello的执行流程 - 25 -
5.7 Hello的动态链接分析 - 25 -
5.8 本章小结 - 25 -
第6章 hello进程管理 - 26 -
6.1 进程的概念与作用 - 26 -
6.2 简述壳Shell-bash的作用与处理流程 - 26 -
6.3 Hello的fork进程创建过程 - 26 -
6.4 Hello的execve过程 - 27 -
6.5 Hello的进程执行 - 28 -
6.6 hello的异常与信号处理 - 29 -
6.7本章小结 - 30 -
第7章 hello的存储管理 - 31 -
7.1 hello的存储器地址空间 - 31 -
7.2 Intel逻辑地址到线性地址的变换-段式管理 - 31 -
7.3 Hello的线性地址到物理地址的变换-页式管理 - 31 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 32 -
7.5 三级Cache支持下的物理内存访问 - 32 -
7.6 hello进程fork时的内存映射 - 33 -
7.7 hello进程execve时的内存映射 - 33 -
7.8 缺页故障与缺页中断处理 - 34 -
7.9动态存储分配管理 - 34 -
7.10本章小结 - 36 -
第8章 hello的IO管理 - 37 -
8.1 Linux的IO设备管理方法 - 37 -
8.2 简述Unix IO接口及其函数 - 37 -
8.3 printf的实现分析 - 38 -
8.4 getchar的实现分析 - 41 -
8.5本章小结 - 41 -
结论 - 41 -
附件 - 43 -
参考文献 - 44 -

第1章 概述
1.1 Hello简介
hello.c经预处理变成了hello.i,hello.i经编译器编译形成了汇编程序hello.s,hello.s经过汇编器形成了可重定位目标文件hello.o,hello.o与libc.so经过链接器链接形成了可执行目标文件hello,这是P2P的过程。
之后 shell 为其fork,形成子程序,再在子程序中 execve,为其映射虚拟内存,然后进入 main 函数执行目标代码,CPU 为运行的 hello 分配时间片执行逻辑控制流。当程序运行结束后,shell 父进程负责回收 hello 进程,内核删除相关数据结构,以上便是 020。
1.2 环境与工具
硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk
软件环境:Window10 64 位 Vmware 14;Ubuntu 16.04 LTS 64 位

开发与调试工具:GCC,GDB,EDB,OBJDUMP
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
1.3.1 hello.i
经预处理后的hello.c。
1.3.2 hello.s
经编译后的hello.i形成的汇编程序。
1.3.3 hello.o
经汇编后的hllo.s形成的可重定位目标文件。
1.3.4 hello
经过链接的hello.o形成的可执行目标文件。
1.4 本章小结
本章介绍了hello的P2P以及020的过程,以及该过程中形成的中间文件。

第2章 预处理
2.1 预处理的概念与作用
概念:预处理就是在编译的第一遍扫描之前所作的工作。
作用:
1:将头文件中的内容(源文件之外的文件)插入到源文件中
2:进行了宏替换的过程,定义和替换了由#define指令定义的符号
3:删除掉注释的过程,注释是不会带入到编译阶段
4:条件编译
2.2在Ubuntu下预处理的命令
gcc -E hello.c -o hello.i
在这里插入图片描述

2.3 Hello的预处理结果解析
预处理器读取hello.c头文件中的内容并把它直接插入程序文本中,并进行了宏替换的过程,定义和替换了由#define指令定义的符号,同时消掉了注释,这样就得到了hello.i。
2.4 本章小结
hello.c经过预处理变成了hello.i,但它仍然是一个文本文件,是不能被执行的

第3章 编译
3.1 编译的概念与作用
概念:编译就是将高级语言翻译成汇编语言的过程。
作用:编译将高级语言翻译成汇编语言,它为不同高级语言的不同编译器提供了通用的输出语言,方便汇编器将其翻译为机器语言指令。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
在这里插入图片描述

3.3 Hello的编译结果解析
3.3.1 数据的存储
main函数的第一个参数int argc存储在寄存器%edi中,第二个参数char *argv[]存储在寄存器%rsi中,编译器为了防止这两个参数在调用函数过程中被篡改,将它们分别存储在-20(%rbp)、-32(%rbp)中,而局部变量int i存储在-4(%rbp)中。

3.3.2 if语句
hello.c中if语句:
在这里插入图片描述

编译器将if语句处理成了这样的格式:
if(argc==3) goto .L2;
printf(“Usagee: Hello 学号 姓名!\n”);
exit(1);
.L2

以方便翻译成汇编语言。
在翻译成汇编语言时,编译器使用了cmp语句来实现argv与3的比较,用je语句实现了goto的功能,翻译后的汇编语言:
在这里插入图片描述

3.3.3 for循环
hello.c中的for循环为:
在这里插入图片描述

扫描二维码关注公众号,回复: 4716286 查看本文章
其中还有sleep函数的调用,函数调用的具体实现在下一小节中详细解释,这里主要讨论for循环的实现。

首先编译器将for循环处理成了跳转到中间的模式,即:
i = 0;
goto .L3;
.L4
body-statement
i++;
.L3
if(i<=9) goto .L4

这样编译器只要使用条件跳转jle和cmp语句即可实现for循环,翻译完成的汇编语句如下:

在这里插入图片描述

3.3.4函数调用
我们可以以for循环中sleep函数的调用为例来分析编译器怎么处理函数调用语句。
sleep函数的调用:
在这里插入图片描述
在汇编语言中,调用函数使用call语句,但在使用call语句前,应该先将要调用的函数的参数准备好,准备参数的汇编语句如下:
在这里插入图片描述
将参数存入%edi寄存器。准备好参数后,使用call语句调用了sleep函数:
在这里插入图片描述

3.4 本章小结
编译将hello.i变成了hello.s,用汇编语言将hello.c表示了出来,但它仍然是个文本文件,还不能被机器执行。
第4章 汇编
4.1 汇编的概念与作用
概念:汇编就是将汇编语言翻译成机器语言的过程。
作用:汇编使得文本文件变为二进制文件,并将其打包成可重定位目标程序的格式,方便进行链接。
4.2 在Ubuntu下汇编的命令
gcc -c hello.s -o hello.o

在这里插入图片描述

4.3 可重定位目标elf格式
hello.o的elf格式应为可重定位的目标文件的格式,有ELF头、各段和节头表。
4.3.1的ELF头
在这里插入图片描述
包括了16字节的标识信息、文件类型、机器类型、节头表的偏移、节头表的表项大小以及表项个数。

4.3.2 节头表
在这里插入图片描述

hello.o中包含了13个节。
4.3.3 .text节
占用空间大小:0字节
相对头文件偏移:0x40字节
存储了hello.c编译后的代码部分:
在这里插入图片描述

4.3.4 .rela.text节
占用空间大小:0x18字节
相对头文件偏移:0x318字节
存储了.text节的重定位信息,用于重新修改.text节代码段中的指令中的地址信息:
在这里插入图片描述

4.3.5 .data节
占用空间大小:0字节
相对头文件偏移:0xc0字节
存储了已初始化的全局变量,即sleepsecs:
在这里插入图片描述

4.3.6.bss节
占用空间大小:0字节
相对头文件偏移:0xc4字节
存储了未初始化的全局变量,仅是占位符,不占据任何磁盘空间,hello.o的.bss应该为空:
在这里插入图片描述

4.3.7 .rodata节
占用空间大小:0字节
相对头文件偏移:0xc4字节
存储了一些只读数据,如printf格式串,跳转表,switch等:
在这里插入图片描述

4.3.8 .comment节
占用空间大小:0x1字节
相对头文件偏移:0xef字节
存储了编译器的版本信息:
在这里插入图片描述

4.3.9 .note.GNU-stack节
占用空间大小:0字节
相对头文件偏移:0x125字节
在这里插入图片描述

4.3.10 .eh_frame节
占用空间大小:0字节
相对头文件偏移:0x128字节
在这里插入图片描述

4.3.11 .rela.eh_frame节
占用空间大小:0x18字节
相对头文件偏移:0x3d8字节
在这里插入图片描述
4.3.12 .shstrtab节
占用空间大小:0字节
相对头文件偏移:0x3f0字节
节名表:
在这里插入图片描述

4.3.13 .symtab节
占用空间大小:0x18字节
相对头文件偏移:0x160字节
存放函数和全局变量(符号表)的信息:
在这里插入图片描述

4.3.14 .strtab节
占用空间大小:0x18字节
相对头文件偏移:0x2e0字节
包含symtab及debug节中的符号和节名:
在这里插入图片描述

4.3.15 重定位表

在这里插入图片描述
(1) 节偏移:0x16
符号:.rodata
类型:R_X86_64_32
偏移调整:0x0
(2) 节偏移:0x1b
符号:puts
类型:R_X86_64_PC32
偏移调整:-0x4
(3) 节偏移:0x25
符号:exit
类型:R_X86_64_PC32
偏移调整:-0x4
(4) 节偏移:0x4c
符号:.rodata
类型:R_X86_64_32
偏移调整:0x1e
(5) 节偏移:0x56
符号:printf
类型:R_X86_64_PC32
偏移调整:-0x4
(6) 节偏移:0x5c
符号:sleepsecs
类型:R_X86_64_PC32
偏移调整:-0x4
(7) 节偏移:0x63
符号:sleep
类型:R_X86_64_PC32
偏移调整:-0x4
(8) 节偏移:0x72
符号:getchar
类型:R_X86_64_PC32
偏移调整:-0x4
(9) 节偏移:0x20
符号:.text
类型:R_X86_64_PC32
偏移调整:0x0

4.4 Hello.o的结果解析
在这里插入图片描述
在这里插入图片描述
机器语言是由0、1构成的序列,一个汇编语言中的指令对应唯一的一个01序列。在第13行中,hello.s中的je是跳转到标记过的.L2,而hello.o的反汇编中可以发先是跳转到地址。第30行和第6f行同理。第15行使用条件跳转语句,出现了hello.o的反汇编中重定位信息,而hello.s中仍是一个.rodata中的地址,第15行的机器语言bf后的地址信息为零,在重定位后,地址信息将换成对应符号的地址信息。使用函数调用的语句处理相同,都是先空下地址信息,等重定位后再补上。
4.5 本章小结
汇编将汇编语言变成了机器语言,但这时的机器语言仍然不能被机器执行,因为其中一些数据以及代码的信息还不完善,等到链接后才能形成可执行文件。
第5章 链接

5.1链接的概念与作用
概念:链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。
作用:链接使得分离编译成为可能,当需要修改一个大型的应用程序时,我们可以单独修改其中某一模块,再重新编译它,链接应用,不必重新编译其他文件。
5.2 在Ubuntu下链接的命令
在这里插入图片描述

5.3 可执行目标文件hello的格式
hello的ELF格式应为可执行目标文件的格式,有ELF头,段头部表,各段(不包括重定位段)、可能有节头表。
5.3.1 ELF头
在这里插入图片描述

5.3.2 段信息
在这里插入图片描述
在这里插入图片描述

5.4 hello的虚拟地址空间
在这里插入图片描述
在这里插入图片描述

5.5 链接的重定位过程分析

hello:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
hello.o:
在这里插入图片描述
对照可以发现,hello.o中只有main一个函数,而hello中多了_init,_start和.plt节中的函数。
在链接的过程中符号解析阶段,链接器先读入hello.o,链接器判断这是一个可重定位的目标文件,于是将它存入一个集合E,解析其中的符号,若出现未定义的符号,则存入一个集合U(存放未定义符号)中,而出现了定义却未使用的符号则放入一个集合D中。然后链接器读入crt*库中的目标文件,同读入hello.o的过程一样,这样E中就有了_start。接着接入了动态链接库libc.so,具体过程将在后文讲述。
在重定位的过程中,链接器将根据重定位条目修改需要被修改的引用。重定位条目有32种不同的类型,但最基本的只有两种:R_X86_64_32、R_X86_PC32,其中前一种重定位一个使用32位绝对地址的引用,后一种重定位一个使用32位PC相对地址的引用。链接器会为每个节和每个符号选择一个运行时地址(分为别ADDR(s)和ADDR(symbol)),下面用puts说明重定位的过程:
首先puts的重定位条目信息:
节偏移:0x1b
符号:puts
类型:R_X86_64_PC32
偏移调整:-0x4
puts重定位条目的类型为R_X86_64_PC32,为相对地址的重定位,则puts
重定位后与PC的偏差为:ADDR(symbol)-(ADDR(s)+ 节偏移 )+偏移调整。只要将引用处改为该偏差即可跳到puts函数
R_X86_64_32类型的重定位地址计算则要简单一点,引用处改为ADDR(symbol)+偏移调整即可。

5.6 hello的执行流程
(以下格式自行编排,编辑时删除)
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。

5.7 Hello的动态链接分析
(以下格式自行编排,编辑时删除)
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
5.8 本章小结
可重定位目标文件在链接后形成了可执行目标文件,本章介绍了链接的概念与作用、hello的ELF格式、分析了 hello 的虚拟地址空间、重定位过程、执行流程、动态链接过程。

第6章 hello进程管理
6.1 进程的概念与作用
概念:进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。进程是一个执行中的程序的实例。
作用:在运行一个程序时,会得到一个假象,就好像这个程序是系统中当前运行的唯一的程序一样。这个程序好像是独占地使用处理器和内存,处理器就好像是无间断地一条接一条地执行我们程序中的指令。最后,这个程序中的代码和数据好像是系统内存中唯一的对象。这都是进程提供给我们的。
6.2 简述壳Shell-bash的作用与处理流程
Shell是一个程序,它是linux系统与用户之间的桥梁。Shell是指一个应用程序,它为用户提供了一个输入指令的界面,使得用户能够通过这些指令访问操作系统内核的服务。
处理流程:
1.读入用户输入的指令
2.切分指令获得所有的参数
3.分析指令,若是内置指令,立即执行
4.否则fork一个子进程,并在子进程中运行
5.Shell应该能够接受从键盘输入的信号
6.3 Hello的fork进程创建过程
Shell从终端读入指令./hello 1170300515 y,辨别出./hello不是shell的内置命令,于是shell调用fork函数创建了一个运行中的子程序,这个子程序几乎但不完全与父进程相同,子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这意味着当父进程调用fork函数时,子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大的区别在于它们有不同的PID。
子进程与父进程是并发运行的独立进程,内核能够以任意方式交替执行它们的逻辑控制流中的指令。但在shell中父进程显式地等待子进程的完成。
6.4 Hello的execve过程
execve函数的原型为:
int execcve(const char *filename,const char argv[],const char envp[])
execve函数加载并运行可执行目标文件filename,argv为参数列表,envp为环境变量。
execve加载hello后,它会先删掉原来子进程的虚拟内存段,创建一组新的数据、代码、用户栈和堆段,将这组段初始化为零,通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为hello中的内容。
加载器创建的虚拟内存段如图:
在这里插入图片描述
接着加载器会跳转到_start函数的位置,_start函数设置栈,并将控制传递给hello的主函数main。除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制,直到 CPU 引用一个被映射的虚拟页时才会进行,这时操作系统利用它的页面调度机制自动将页面从磁盘传送到内存。
6.5 Hello的进程执行
时间片是指从进程开始运行直到被抢占的时间。
操作系统内核使用一种称为上下文切换的较高层形式的异常控制流来实现多任务。内核为每个进程维持一个上下文,上下文就是内核重新启动一个被抢占的进程所需的状态(它由一些对象的值组成,包括通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等)。
在内核决定抢占当前进程,并重新开始一个先前被抢占了的进程时,内核会使用上下文切换的机制来将控制转移到新的进程。上下文切换的过程如下:
1.保存当前进程的上下文
2.恢复某个先前被抢占的进程被保存的上下文
3.将控制传递给这个新恢复的进程
在执行hello时,sleep函数会显式地请求让调用函数休眠,这时内核会决定执行上下文切换,切换过程如下图:
在这里插入图片描述
当hello调用getchar时,实际上是执行输入流是stdin的系统read。hello运行在用户状态中,它通过执行系统调用read陷入内核。内核中的陷阱处理程序请求来自磁盘控制器的DMA传输,并且安排在磁盘控制器完成从磁盘到内存的数据传输后,磁盘中断处理器。磁盘读取数据要用一段相对较长的时间,所以内核决定执行上下文切换,等磁盘发出一个中断信号,表示数据从键盘缓冲区读入内存后,内核再进行上下文切换,将控制返回给hello。

6.6 hello的异常与信号处理
6.6.1 ctrl+z
从键盘输入ctrl+z后可以发现显示hello的运行已停止,这是因为输入ctrl+z后,父进程shell收到信号SIGSTP,信号的处理函数是将子进程挂起并显示出已停止子进程,用ps查看可发现hello没有被回收。

6.6.2 jobs
jobs的功能是列出暂停的进程

6.6.3 fg
fg i的功能是将job[i]恢复成前台进程继续执行,输入fg后,挂起的hello又继续执行。

6.6.4 ctrl+c
ctrl+c的作用是终止子进程,父进程shell收到信号SIGINT,信号处理函数的逻辑是结束hello,并回收hello的进程,用ps查看可发现hello被回收了。

6.7本章小结
本章阐述了进程的概念与作用,说明了hello执行时shell处理它的流程,说明了shell如何fork一个子进程运行hello以及加载器如何加载hello,以及加载完毕后hello执行时可能进行的上下文切换过程以及shell对信号的处理。

第7章 hello的存储管理
7.1 hello的存储器地址空间
物理地址:在存储器里以字节为单位存储信息,为正确地存放或取得信息,每一个字节单元给以一个唯一的存储器地址。又称为绝对地址。
逻辑地址:逻辑地址就是程序经编译后的地址,由程序产生的与段相关的偏移地址部分和选择符组成。
线性地址:逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。
虚拟地址与线性地址相同。
7.2 Intel逻辑地址到线性地址的变换-段式管理
当CPU在16位时,在保护模式下,线性地址需要逻辑地址通过段机制来得到。逻辑地址根据选择符在段选择表中得到目标描述符,从描述符中提取出基地址,基地址加上逻辑地址中的偏移就得到了线性地址。
当CPU在32位时,内存为4GB,寄存器和指令可以寻址整个线性地址空间,所以不需要基地址了,基地址为0,即逻辑地址=描述符=线性地址。在64位时,逻辑地址和线性地址合二为一。
7.3 Hello的线性地址到物理地址的变换-页式管理
从线性地址(虚拟地址)(VA)到物理地址(PA)的变换是通过分页机制来实现的。
VM系统将虚拟内存分割为称为虚拟页(VP)的大小固定块,在linux下为4KB。相应地,物理内存被分割为物理页,大小也为4KB。
如果不考虑TLB和多级页表,虚拟地址由虚拟页号(VPN)和虚拟页偏移量(VPO)组成,由虚拟页号(VPN)可在页表中找到相应的PTE,通过PTE找到的对应的物理页号(PPN),由于物理页面和虚拟页面都是4KB的,所以虚拟页偏移量(VPO)与物理页偏移量(PPO)相等。找到的物理页号(PPN)和物理页偏移量(PPO)就组成了虚拟地址对应的物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
TLB是MMU中的一个关于PTE的小的缓存,有了TLB后,VPN又分为了TLB标记(TLBT)和TLB索引(TLBI),TLB的机制与全关联的cache的机制相同,如果TLB有T = 2t个组,那么TLB索引(TLBI)是由VPN的t个最低位组成的,TLB标记(TLBT)是由VPN中剩余的位组成。
引入多级页表后,VPN被划分成了多个区域,例如使用k级页表,VPN被划分成了k个VPN,每个VPN i都是一个到第i级页表的索引,第k个VPN中存储着VPN对应的PPN。
CPU产生一个VA,MMU在根据VPN在TLB中搜索PTE,若命中,MMU取出相应的PTE,根据PTE将VA翻译成PA;若没命中,则通过多级页表查询PTE是否在页中,若在页中,找到对应的PIE,MMU将VA翻译成PA,若没有在页中,则进行缺页处理。
7.5 三级Cache支持下的物理内存访问
由VA翻译出的PA在cache的机制下由块偏移(CO)、组索引(CI)和标记(CT)组成,块偏移(CO)的位数由块大小决定,B = 2b字节,其中B为块大小,b为块偏移(CO),组索引(CI)由组数决定,S = 2s字节,其中S为组数,s为组索引(CI)。PA的b个最低位为块偏移,由块偏移向高位的s位为组索引,剩余的位为标记(CT)。
首先,在L1-cache中,使用组索引找到对应的组,在组中搜索标记,若标记在组中且当前块的有效为1,则命中(hit),根据块偏移取出相应的数据发送到CPU;否则不命中,则向下一级缓存中查找块(L2-cache -> L3-cache ->主存),找到相应块后,若上一级缓存中有空闲块,则将相应块放入空闲块的位置,若上一级缓存中没有空闲块,则采用最近最少使用策略 LFU 进行替换。
7.6 hello进程fork时的内存映射
当shell调用fork函数时,内核创建了一个子进程,并赋给子进程唯一的PID和一个虚拟内存空间,这个虚拟空间是调用进程的一个副本,包括mm_struct、区域结构和页表,它将这两个进程的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
7.7 hello进程execve时的内存映射
shell调用execve函数时,会启动加载器代码,加载器能够加载并运行hello可执行目标文件中的程序,用其代替当前的程序。以下为加载器使hello中的程序能够替代当前程序的步骤:
1.删除已存在的用户区域。删除当前虚拟空间中已有的用户部分已存在的区域结构。
2.映射私有区域。为hello中的程序的代码、数据、bss和栈区域构建新的区域结构。所有这些区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为0。
3.映射共享区域。将hello与共享对象libc.so链接,libc,so是动态链接到hello中的,然后映射到用户虚拟空间中的共享区域内。
4.设置程序计数器(PC)。execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
内存映射如图:

7.8 缺页故障与缺页中断处理
DRAM不命中称为缺页,缺页是一种异常。当发生缺页异常时,缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,如果牺牲页已经被修改了,那么内核就会将它复制回磁盘。接着内核会修改牺牲页对应的页表条目,反映出该页不再缓存在主存中。内核从磁盘复制需要的页到主存中相应的位置,并更新相应的PTE,随后返回,当异常处理程序返回时,它会重新启动导致缺页的指令。

7.9动态存储分配管理
Printf会调用malloc,下面简述动态内存管理的基本方法与策略:
动态内存分配器维护着一个进程的虚拟内存区域,称为堆(heap)。系统之间细节不同,但不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址),对于每个进程,内核维护着一个变量brk,它指向堆的顶部。
分配器将堆视为一组不同大小的块(block)的集合来维护。每个块就是一个连续的虚拟内存片(chunk),要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它被显式地应用所分配。一个已分配的块保持已分配的状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器有两种基本风格,都要求应用显式地分配块:显式分配器、隐式分配器。
显式分配器:要求应用显式地释放任何已分配的块。
隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。
任何实际的分配器都需要一些数据结构,允许它来区别块边界以及已分配块和空闲块。大多数分配器将这些信息嵌入块本身,下面介绍一种数据结构:带边界标记的隐式空闲链表
结构如图:

相对于普通的隐式空闲链表,带边界标记的隐式空闲链表在块结尾处添加了一个脚部(footer),其中脚部就是头部的一个副本,分配器可以通过检查一个块的脚部,判断前面一个块的起始位置和状态。这个脚部总是在距当前块开始位置的一个字的距离。
下面介绍一些分配器利用隐式空闲链表管理块的策略:
1.放置已分配的块
当一个应用请求一个k字节的块时,分配器搜索空闲链表,查找一个足够大可以放置所请求块的空闲块。常见的放置策略有三种:首次适配、下一次适配、最佳适配。
(1)首次适配:从头开始搜索空闲链表,选择第一个合适的空闲块。
(2)下一次适配:从上一次查询结束的地方开始,搜索空闲链表,选择第一个合适的空闲块
(3)最佳适配:检查每个空闲块,选择合适所需请求大小的最小空闲块。
2.分割空闲块
如果分配器为请求匹配的空闲块不太好,那么分配器通常会选择将这个空闲块分割为两部分。第一部分变成分配块,而剩下的变成一个新的空闲块。
3.合并空闲块
对于合并空闲块,分配器可以选择立即合并或者推迟合并。立即合并就是在每次一个块被释放时,就合并所有的相邻块。推迟合并是等到某个稍晚的时候再合并空闲块。有了脚部后,空闲块的合并变得简单起来。通过脚部和头部来访问当前空闲块的上一个块和下一个块,若有同为空闲块的块,那么修改块的头部和脚部即可合并空闲块。
7.10本章小结
本章介绍了hello的存储器地址空间、intel逻辑地址到线性地址的变换(段机制)、hello的线性地址到物理地址的变换(分页机制)、TLB与四级页表支持下的VA到PA的变换、三级cache支持下的物理内存的访问、hello进程fork时的内存映射、hello进程execve时的内存映射、缺页故障与缺页中断处理以及动态存储分配管理。

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
8.2.1 Unix I/O统一操作

1. 打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个 I/O 设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。

2.Shell 创建的每个进程都有三个打开的文件:标准输入(描述符为0),标准输出(描述符为1),标准错误(描述符为2)。
3.改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置 k,初始为 0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行 seek,显式地将改变当前文件位置 k。
4.读写文件:一个读操作就是从文件复制 n>0 个字节到内存,从当前文件位置 k 开始,然后将 k 增加到 k+n,给定一个大小为 m 字节的而文件,当 k>=m 时,触发 EOF。类似一个写操作就是从内存中复制 n>0个字节到一个文件,从当前文件位置 k 开始,然后更新 k。
5.关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。

8.2.2 Unix I/O函数

1.int open(char *filename,int flags,mode_t mode);
open函数将filename转换为一个文件描述符,并且返回描述符数字。flags参数指明了进程打算如何访问这个文件。mode参数指定了新文件的访问权限位。
2.int close(int fd);
关闭一个已打开的文件。
3.ssize_t read(int fd,void *buf,size_t n);
read 函数从描述符为 fd 的当前文件位置赋值最多 n 个字节到内存位置 buf。返回值-1 表示一个错误,0表示 EOF,否则返回值表示的是实际传送的字节数量。
4.ssize_t write(int fd,const void *buf,size_t n);
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; 
} 

首先 arg 获得第二个不定长参数,即输出的时候格式化串对应的值。
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。用格式字符串对个数变化的参数进行格式化,产生格式化输出,并返回要打印出来的字符串的长度。
write函数:
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL

int INT_VECTOR_SYS_CALL表示要通过系统来调用sys_call这个函数。
Sys_call函数:
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 

syscall 将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的 ASCII 码。字符显示驱动子程序将通过 ASCII 码在字模库中找到点阵信息将点阵信息存储到 vram 中。
显示芯片会按照一定的刷新频率逐行读取 vram,并通过信号线向液晶显示器传输每一个点(RGB 分量)。
于是我们的打印字符串就显示在了屏幕上。
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。大体逻辑是读取字符串的第一个字符然后返回。
8.5本章小结
本章介绍了linux的io设备管理方法、Unix的接口及其函数、printf的实现分析、getchar的实现分析。
结论
1.预处理。Hello.c经过预处理变成了hello.i。
2.编译。Hello.i经过编译变成了hello.s。
3.汇编。Hello.s经过汇编变成了可重定位目标文件hello.o。
4.链接。可重定位目标文件hello.o在链接器与加载器的作用下与动态链接库libc.so链接生成了可执行目标文件hello。
5.创建子进程。shell调用fork函数为hello创建子进程。
6.运行进程。shell在子进程中调用execve函数,加载映射虚拟内存,进入程序入口后开始载入物理内存,进入main函数。
7.访问内存:MMU将程序中使用的虚拟地址翻译为物理地址,通过物理地址访问内存。
8.动态内存申请:printf 会调用 malloc 向动态内存分配器申请堆中的
内存。
9.信号处理:hello程序可接受一些信号并处理这些信号(如ctrl+z、ctrl+c产生的信号等)
10.结束:shell回收hello进程和它的子进程。

附件
1.hello.i经预处理后的hello.c。
2.hello.s 经编译后的hello.i形成的汇编程序。
3.hello.o 经汇编后的hllo.s形成的可重定位目标文件。
4.hello 经过链接的hello.o形成的可执行目标文件。

参考文献
为完成本次大作业你翻阅的书籍与网站等
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.

猜你喜欢

转载自blog.csdn.net/weixin_44077946/article/details/85392514