摘 要
本文主要讲述了hello.c程序在编写完成后运行在linux中的生命历程,借助相关工具分析预处理、编译、汇编、链接等各个过程在linux下实现机制及原因,介绍了shell的动态链接、进程运行、内存管理、IO管理、进程管理等相关知识,虚拟内存、异常信号处理及UnixI/O等。
关键词:Hello程序;预处理;编译;汇编;链接;进程;存储;虚拟内存;I/O
目 录
第1章 概述
1.1 Hello简介
P2P:在Linux中,hello.c经过cpp的预处理、ccl的编译、as的汇编、ld的链接最终成为可执行目标程序hello,在shell中键入启动命令后,shell为其fork产生一个子进程,然后hello便从程序变为了进程。
020: shell为此子进程execve,映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入 main函数执行目标代码,CPU为运行的hello分配时间片执行逻辑控制流。当程序运行结束后,shell父进程负责回收hello进程,内核删除相关数据结构。
1.2 环境与工具
硬件环境:处理器:Intel® Core™ i7-8550U CPU @ 1.80GHz 1.99GHz
RAM:8.00GB 系统类型:64位操作系统,基于x64的处理器
软件环境:Windows10 64位;Ubuntu 19.04
开发与调试工具:gcc,as,ld,vim,edb,readelf,VScode
1.3 中间结果
hello.s:hello.i编译后的汇编文件。
hello.o:hello.s汇编之后的可重定位目标文件。
hello:链接之后的可执行目标文件。
hello.out:hello反汇编之后的可重定位文件。
1.4 本章小结
本章介绍了Hello的P2P,020,此次实验的环境与工具,以及综述中间结果。
第2章 预处理
2.1 预处理的概念与作用
(一)预处理的概念:预处理器(cpp)根据以字符#为开头的命令,修改原始的C语言程序。比如hello.c中的#include 命令告诉预处理器读取系统头文件stdio.的内容,并把它直接插入程序文本中,结果就得到了另一个C程序,通常是以.i作为文件扩展名。
(二)预处理的作用:主要是对源程序的替代作用。
- 处理宏定义指令,如#define Name TokenString,#undef等。对于前一个伪指令,预编译所要做的是将程序中的所有Name用TokenString替换,但作为字符串常量的Name则不被替换。对于后者,则将取消对某个宏的定义,使以后该串的出现不再被替换。
- 处理条件编译指令,伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉。
- 头文件包含指令,如#include。在头文件中一般用伪指令#define定义了大量的宏(最常见的是字符常量),同时包含有各种外部符号的声明。采用头文件的目的主要是为了使某些定义可以供多个不同的C源程序使用。只要加上#include就可以实现引用。
处理特殊符号,预编译程序可以识别一些特殊的符号。预编译程序会对在源程序中出现的这些串用合适的值进行替换。
2.2在Ubuntu下预处理的命令
预处理命令为:cpp hello.c > hello.i
会发现文件夹中出现了hello.i文件
hello.i文件前部分截图:
2.3 Hello的预处理结果解析
经过预处理指令,我们发现原本很短的hello.c变成了3060行,原因是预处理对原文件中的宏进行了宏展开,头文件中的内容被包含进该文件中。
其中源代码都在后面几行。
2.4 本章小结
本章主要介绍了预处理的概念与作用,预处理在Ubuntu下预处理的命令,以及对Hello的预处理结果解析。
第3章 编译
3.1 编译的概念与作用
(一)编译的概念
编译过程输入的是用汇编语言书写的源程序,输出的是用机器语言表示的目标程序,编译结束会生成汇编程序,汇编程序是指把汇编语言书写的程序翻译成与之等价的机器语言程序的翻译程序。
- 编译的作用
生成与机器语言程序等价的的翻译程序。由于该文件是文本形式不是二进制代码,所以更方便理解。
3.2 在Ubuntu下编译的命令
Ubuntu下编译的命令:gcc -S -o hello.s hello.i
发现文件夹中出现了hello.s
3.3 Hello的编译结果解析
(一)数据
1.常量
这里涉及到了一个常量4,保存在.text节中,作为指令的一部分。
- 局部变量
局部变量被保存在栈中。
其中有变量int i和变量int argc。
图3.3.2.1
通过22行将-20(%rbp)的值传入第一个参数传递寄存器,以及第24行的比较指令就可以判断-20(%rbp)中保存的是变量argc。
令-4(%rbp)中的值初始化为0(31行),以及循环加一(51行),判断其值与7的大小关系(53行),可以判断i存储在-4(%rbp)中,
我们发现不管是i还是argc都是存储在栈中的。
- 字符串
两个printf函数中包含两个字符串字面量,为只读数据存储在.rodata段。
4.全局变量:
初始化的全局变量储存在.data节,它的初始化不需要汇编语句,而是直接完成的。
(二)函数操作
1.main函数
int main(int argc,char *argv[])
参数:传入参数argc和argv[],分别用寄存器%rdi和%rsi存储。
函数调用:被系统启动函数调用。
函数返回:设置%eax为0并且返回,对应return 0 。
2.printf函数:
参数传递:call puts时只传入了字符串参数首地址;for循环中call printf时传入了 argv[1]和argc[2]的地址。
函数调用:if判断满足条件后调用,与for循环中被调用。
3.atoi函数:atoi(argv[3])
参数:argv[3],存储在rax中,通过%rdi传参。
4.sleep函数:sleep(atoi(argv[3]))
参数:sleep函数的参数为atoi函数的返回值,存储在%eax中,通过%edi传参。
5.exit函数:
参数:传入的参数为1,再执行退出命令
函数调用:if判断条件满足后被调用.
6.getchar函数:getchar();
函数调用:在main中被调用,call getchar
(三)数组argv[]
组argv[]是main函数的第二个参数,所以通过%rsi寄存器传递参数,如图3.3.2.1可知,最后存储在-32(%rbp)中(23行)。
(四)赋值
for(i=0;i<8;i++) 中的i=0为赋值语句,对应的汇编代码是
movl $0, -4(%rbp)
因为i是int类型的,所以用movl。
(五)算数操作
for(i=0;i<8;i++) 中的i++
在每次循环执行的内容结束后,对i进行一次自加,栈上存储变量i的值加1
addl $1, -4(%rbp)
(六)关系操作
1.用于判断argc是否为4,源代码为
if(argc!=4){
printf("用法: Hello 学号 姓名 秒数!\n");
exit(1);
}
汇编代码为
cmpl $4, -20(%rbp)
je .L2
如果相等跳转到L2
2.for(i=0;i<8;i++) 中的i<8
汇编代码为
cmpl $7, -4(%rbp)
jle .L4
jle用于判断cmpl产生的条件码,如果i的值小于等于7,则跳转到L4继续循环。
(七)控制转移
if(argc!=4){
printf("用法: Hello 学号 姓名 秒数!\n");
exit(1);
}
汇编代码为
cmpl $4, -20(%rbp)
je .L2
如果相等跳转到L2
(八)类型转换
hello.c中涉及的类型转换是:atoi(argv[3]),将字符串类型转换为整数类型。
3.4 本章小结
通过本章的学习我了解了编译的概念与作用,编译在Ubuntu下编译的命令以及Hello的编译结果解析。
第4章 汇编
4.1 汇编的概念与作用
汇编的概念:将汇编语言(这里是hello.s)翻译成机器语言(hello.o)的过程称为汇编。
汇编的作用:汇编器将hello.s文件翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在目标文件hello.o中,hello.o文件是一个二进制文件。
4.2 在Ubuntu下汇编的命令
命令:as hello.s -o hello.o
生成hello.o文件
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
ELF头文件以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节的顺序。ELF头剩下的部分包含帮助连接器语法分析和解释目标文件的信息。其中包含ELF头的大小、目标文件的类型,机器类型,节头部表的文件偏移,以及节头部表中条目的大小和数量。
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 1240 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 14
Section header string table index: 13
节头部表:描述目标文件的节
重定位节:
表述了各个段引用的外部符号等,在链接时,需要通过重定位节对这些位置的地址进行修改。链接器会通过重定位条目的类型判断该使用什么养的方法计算正确的地址值,通过偏移量等信息计算出正确的地址。
本程序需要重定位的信息有:.rodata中的模式串,puts,exit,printf,slepsecs,sleep,getchar这些符号。
符号表:
.symtab是一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。
4.4 Hello.o的结果解析
终端输入objdump -d -r hello.o > hello4.txt指令获得反汇编代码。
反汇编代码与hello.s中的代码指令并没有太大的区别,但是反汇编代码会显示机器代码,且部分指令的具体内容不同。
1.分支指令:在汇编代码中,分支跳转是直接以.L0等符号表示,但是在反汇编代码中,分支转移表示为主函数+段内偏移量。
2.函数调用:汇编代码在call后直接指出调用函数名,而反汇编call后的目标地址是下一条指令。
3.数据:反汇编是十六进制,汇编代码是十进制。
4.反汇编代码中有重定位条目形如2b: R_X86_64_PLT32 exit-0x4,然而汇编代码中没有。
机器语言:机器语言程序的是二进制机器指令的集合电脑可以识别,机器指令由操作码和操作数构成。
每一条汇编语言操作码都可以用机器二进制数据来表示,进而可以将所有的汇编语言(操作码和操作数)和二进制机器语言建立一一映射的关系,因此可以将汇编语言转化为机器语言,通过对机器代码的分析可以看出一下不同的地方。
4.5 本章小结
通过本章的学习,我了解了汇编的概念与作用,其在Ubuntu下汇编的命令,可重定位目标elf格式以及 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.3 可执行目标文件hello的格式
命令:readelf -a hello > ./hello1.elf
ELF文件头:有27个节
节头部表:描述目标文件的节,对 hello中所有的节信息进行了声明,其 中包括大小 Size 以及在程序中的偏移量 Offset,因此根据 Section Headers 中的信息我们就可以用 HexEdit 定位各个节所占的区间(起始位置,大小)。其中 Address 是程序被载入到虚拟地址的起始地址。
描述了各个节的大小、偏移量和其他属性。链接器链接时,会将各个文件的相同段合并成一个大段,并且根据这个大段的大小以及偏移量重新设置各个符号的地址。
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
根据5.3中的节头部表,我们可以在edb中找到相应节的位置以及其详细内容。
5.5 链接的重定位过程分析
使用命令:objdump -d -r hello >hello.out,获得hello的反汇编代码.
分析hello和hello.o的不同:
1.链接增加新的函数:
在hello中链接加入了在hello.c中用到的库函数。
2.增加的节:
hello中增加了.init和.plt节,和一些节中定义的函数。
3.函数调用:
跳转和函数调用的地址在hello中都变成了虚拟内存地址。对于hello.o的反汇编代码,函数只有在链接之后才能确定运行执行的地址,因此在.rela.text节中为其添加了重定位条目。
4.地址访问:
hello.o中的相对偏移地址变成了hello中的虚拟内存地址。而hello.o文件中对于某些地址的定位是不明确的,其地址也是在运行时确定的,因此访问也需要重定位,在汇编成机器语言时,将操作数全部置为0,并且添加重定位条目。
链接的过程:
根据hello和hello.o的不同,分析出链接的过程为:
链接就是链接器(ld)将各个目标文件(各种.o文件)组装在一起,文件中的各个函数段按照一定规则累积在一起。
5.6 hello的执行流程
通过edb的调试,一步一步地记录下call命令进入的函数
进入start
call main
进入函数
函数名和地址
hello!_init 401000
hello!puts@plt 401030
hello!printf@plt 401040
hello!getchar@plt 401050
hello!atoi@plt 401060
hello!exit@plt 401070
hello!sleep@plt 401080
hello!_start 4010f0
hello!_dl_relocate_static_pie 401120
hello!_main 401125
hello!_libc_csu_init 4011c0
hello!_libc_csu_fini 401230
5.7 Hello的动态链接分析
节头部表中可以看到:
对于变量而言,我们利用代码段和数据段的相对位置不变的原则计算正确地址。对于库函数而言,需要plt、got合作,plt初始存的是一批代码,它们跳转到got所指示的位置,然后调用链接器。初始时got里面存的都是plt的第二条指令,随后链接器修改got,下一次再调用plt时,指向的就是正确的内存地址。plt就能跳转到正确的区域。
5.8 本章小结
本章主要复习了链接的操作,通过查看虚拟地址空间,对比了hello与hello.o的区别,分析链接的重定位过程,重温了hello的执行过程以及hello的动态链接分析。至此,hello程序才被完全创造了出来。
第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进程创建过程
根据shell的处理流程,可以推断,输入命令执行hello后,父进程如果判断不是内部指令,即会通过fork函数创建子进程。子进程与父进程近似,并得到一份与父进程用户级虚拟空间相同且独立的副本——包括数据段、代码、共享库、堆和用户栈。父进程打开的文件,子进程也可读写。二者之间最大的不同或许在于PID的不同。Fork函数只会被调用一次,但会返回两次,在父进程中,fork返回子进程的PID,在子进程中,fork返回0。
6.4 Hello的execve过程
execve函数在加载并运行可执行目标文件Hello,且带列表argv和环境变量列表envp。该函数的作用就是在当前进程的上下文中加载并运行一个新的程序。
只有当出现错误时,例如找不到Hello时,execve才会返回到调用程序,这里与一次调用两次返回的fork不同。
在execve加载了Hello之后,它调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数,该主函数有如下的原型:
int main(intargc , char \*\*argv , char \*envp);
结合虚拟内存和内存映射过程,可以更详细地说明exceve函数实际上是如何加载和执行程序Hello:
删除已存在的用户区域(自父进程独立)。
映射私有区:为Hello的代码、数据、.bss和栈区域创建新的区域结构,所有这些区域都是私有的、写时才复制的。
映射共享区:比如Hello程序与标准C库libc.so链接,这些对象都是动态链接到Hello的,然后再用户虚拟地址空间中的共享区域内。
设置PC:exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。
6.5 Hello的进程执行
逻辑控制流:
一系列程序计数器 PC 的值的序列叫做逻辑控制流。由于进程是轮流使用处理器的,同一个处理器每个进程执行它的流的一部分后被抢占,然后轮到其他进程。
用户模式和内核模式:
处理器使用一个寄存器提供两种模式的区分。用户模式的进程不允许执行特殊指令,不允许直接引用地址空间中内核区的代码和数据;内核模式进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
上下文:
上下文就是内核重新启动一个被抢占的进程所需要恢复的原来的状态,由寄存器、程序计数器、用户栈、内核栈和内核数据结构等对象的值构成。
示例:sleep进程的调度过程
初始时,控制流再hello内,处于用户模式
调用系统函数sleep后,进入内核态,此时间片停止。
2s后,发送中断信号,转回用户模式,继续执行指令。
调度的过程:
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行,我们说内核调度了这个进程。在内核调度了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。
用户态与核心态转换:
为了能让处理器安全运行,不至于损坏操作系统,必然需要先知应用程序可执行指令所能访问的地址空间范围。因此,就存在了用户态与核心态的划分,核心态可以说是“创世模式”,拥有最高的访问权限,处理器以一个寄存器当做模式位来描述当前进程的特权。进程只有故障、中断或陷入系统调用时才会得到内核访问权限,其他情况下始终处于用户权限之中,保证了系统的安全性。
6.6 hello的异常与信号处理
正常运行状态:
hello运行会出现的异常:
- 中断 来自I/O设备的信号 异步 总是返回到下一条指令
处理方式
2.陷阱 有意的异常 同步 总是返回到下一条指令
处理方式
3.故障 潜在可恢复的错误 同步 可能返回到当前指令
处理方式:
4.终止 不可恢复的错误 同步 不会返回
处理方式:
按下Ctrl-C,程序终止,进程收到 SIGINT 信号,查询不到jobs等,程序已经结束
按下Ctrl-Z,程序停止,进程收到 SIGSTP 信号,可以查询到pid和jobs等信息。
按下回车
中途乱按,乱按只是将屏幕的输入缓存stdin
6.7本章小结
通过本章的学习,我了解了进程的概念与作用,Shell-bash的作用与处理流程,Hello的fork进程创建过程,Hello的execve过程,Hello的进程执行以及hello的异常与信号处理。通过一次次尝试,对程序执行中产生信号和信号的处理过程有了更多的认识。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:格式为“段地址:偏移地址”,是CPU生成的地址,在内部和编程使用,并不唯一。
物理地址:加载到内存地址寄存器中的地址,内存单元的真正地址。CPU通过地址总线的寻址,找到真实的物理内存对应地址。在前端总线上传输的内存地址都是物理内存地址。
线性地址:逻辑地址向物理地址转化过程中的一步,逻辑地址经过段机制后转化为线性地址。
虚拟地址:保护模式下程序访问存储器所用的逻辑地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
分段功能在实模式和保护模式下有所不同。
实模式:逻辑地址=线性地址=实际的物理地址。段寄存器存放真实段基址,同时给出32位地址偏移量,则可以访问真实物理内存。
保护模式:线性地址还需要经过分页机制才能够得到物理地址,线性地址也需要逻辑地址通过段机制来得到。Base:基地址,32位线性地址指向段的开始。Limit:段界限,段的大小。 DPL:描述符的特权级0(内核模式)-3(用户模式)。
段寄存器用于存放段选择符,通过段选择符可以得到对应段的首地址。处理器在通过段式管理寻址时,首先通过段描述符得到段基址,然后与偏移量结合得到线性地址,从而得到了虚拟地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
系统将虚拟页作为数据传输的单位。Linux下每个虚拟页大小为4kB。物理内存也被分割为物理页,MMU负责地址翻译,MMU使页表将虚拟页到物理页的映射,即虚拟地址到物理地址的映射。
7.4 TLB与四级页表支持下的VA到PA的变换
Core i7采用四级页表的层次结构。CPU产生VA,VA传输给MMU,MMU使用VPN高位作为TLBT和TLBI向TLB中寻找匹配。如果命中,则得到PA。如果TLB没有命中,MMU查询页表,CR3确定第一级页表的起始地址,VPN1确定在第一级页表中的偏移量,查询出PTE,以此类推,最终在第四级页表中找到PPN,与VPO组合成PA,添加到PLT。
7.5 三级Cache支持下的物理内存访问
首先取组索引对应位,向L1cache中寻找对应组。如果存在,则比较标志位,并检查对应行的有效位是否为1。如果上述条件均满足则命中。否则按顺序对L2cache、L3cache、内存进行相同操作,直到出现命中。然后向上级cache返回直到L1cache。如果有空闲块则将目标块放置到空闲块中,否则将缓存中的某个块驱逐,将目标块放到被驱逐块的原位置。
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中,栈和堆地址也是请求二进制零的,初始长 度为零。
- 映射共享区域, hello程序与共享对象 libc.so 链接,libc.so 是动态链 接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
4)设置程序计数器(PC),execve做的最后一件事情就是设置当前进程 上下文的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
缺页故障是一种常见的故障,要访问的主页不在主存,需要操作系统调入才能访问。缺页中断处理函数为do_page_fault函数,选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令再次发送VA到MMU,这次MMU就能正常翻译VA了。
7.9动态存储分配管理
printf函数会调用malloc,下面简述动态内存管理的基本方法与策略:
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器分为两种基本风格:显式分配器、隐式分配器。
显式分配器:要求应用显式地释放任何已分配的块。
隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。
堆中的块主要组织为两种形式:
- 隐式空闲链表(带边界标记)
在块的首尾的四个字节分别添加header和footer,负责维护当前块的信息(大小和是否分配)。由于每个块是对齐的,所以每个块的地址低位总是0,可以用该位标注当前块是否已经分配。可以利用header和footer中存放的块大小寻找当前块两侧的邻接块,方便进行空闲块的合并操作。
- 显式空闲链表
在未分配的块中添加两个指针,分别指向前一个空闲块和后一个空闲块。采用该策略,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。
7.10本章小结
通过此次实验,我了解了hello的存储器地址空间,Intel逻辑地址到线性地址的变换-段式管理,Hello的线性地址到物理地址的变换-页式管理,TLB与四级页表支持下的VA到PA的变换,三级Cache支持下的物理内存访问,hello进程fork时的内存映射,hello进程execve时的内存映射,缺页故障与缺页中断处理,动态存储分配管理。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
一个Linux文件就是一个m字节的序列:
B0,B1,B2……Bm
所有的 IO 设备(如网路、磁盘、终端)都被模型化为文件,而所有的输入和输出都被 当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单低级的应用接口,称为 Unix I/O,这使得所有的输入和输出都被当做相应文件的读和写来执行:
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
Unix I/O 接口:
(1)打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想 要访问一个 I/O 设备,内核返回一个小的非负整数,叫做描述符,它在 后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文 件的所有信息。
(2)Shell 创建的每个进程都有三个打开的文件:标准输入,标准输出,标准错误。
(3)改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位 置 k,初始为 0,这个文件位置是从文件开头起始的字节偏移量,应用 程序能够通过执行 seek,显式地将改变当前文件位置 k。
(4)读写文件:一个读操作就是从文件复制 n>0 个字节到内存,从当前文 件位置 k 开始,然后将 k 增加到 k+n,给定一个大小为 m 字节的而文 件,当 k>=m 时,触发 EOF。类似一个写操作就是从内存中复制 n>0 个字节到一个文件,从当前文件位置 k 开始,然后更新 k。
(5)关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢 复到可用的描述符池中去。
8.3 printf的实现分析
前提:printf和vsprintf代码是windows下的。
查看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;
}
其中va_list即char *
va_list arg = (va_list)((char*)(&fmt) + 4); 即将arg指向了第一个const参数。
vsprintf(buf, fmt, arg)接受确定输出格式的格式字符串fmt,用格式字符串对个数变化的参数进行格式化,产生格式化输出。它最终会返回一个长度,即要打印出来的字符串的长度。 vsprintf生成显示信息,到write系统函数,陷阱系统调用int 0x80或syscall,字符显示驱动子程序实现从ASCII到字模库到显示vram,可以存储每一个点的RGB颜色信息,显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。如此之后,屏幕上就会显示出我们输入的信息了。
8.4 getchar的实现分析
getchar 的源代码为:
1. int getchar(void)
2. {
3. static char buf[BUFSIZ];
4. static char *bb = buf;
5. static int n = 0;
6. if(n == 0)
7. {
8. n = read(0, buf, BUFSIZ);
9. bb = buf;
10. }
11. return(--n >= 0)?(unsigned char) *bb++ : EOF;
12. }
异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键 的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子 程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码 转换成 ASCII 码,保存到系统的键盘缓冲区之中。
getchar 函数落实到底层调用了系统函数 read,通过系统调用 read 读取存储在 键盘缓冲区中的 ASCII 码直到读到回车符然后返回整个字串,getchar 进行封装, 大体逻辑是读取字符串的第一个字符然后返回。
8.5本章小结
了解了Linux的IO设备管理方法,Unix IO接口及其函数,printf的实现,getchar的实现。
结论
Hello的一生可谓变化多端:
hello.c经过预编译,拓展得到hello.i文本文件
hello.i经过编译,得到汇编代码hello.s汇编文件
hello.s经过汇编,得到二进制可重定位目标文件hello.o
hello.o经过链接,生成了可执行文件hello
bash进程调用fork函数,生成子进程;并由execve函数加载运行当前进程的上下文中加载并运行新程序hello
hello的变化过程中,会有各种地址,但最终我们真正期待的是PA物理地址。
hello再运行时会调用一些函数,比如printf函数,这些函数与linux I/O的设备模拟化密切相关
hello最终被shell父进程回收,内核会收回为其创建的所有信息
附件
hello.i:hello.c预处理之后文本文件。
hello.s:hello.i编译后的汇编文件。
hello.o:hello.s汇编之后的可重定位目标文件。
hello:链接之后的可执行目标文件。
hello.out: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] file:///C:/Users/m1777/Desktop/深入理解计算机系统原书第3版-文字版.pdf