本论文通过详细分析一个hello程序在Ubuntu系统下的从代码到执行结束的整个过程,从处理、编译、汇编、链接、进程管理、存储管理和IO管理等方面,结合CSAPP课本相关章节,分析hello过程出现的各种现象,理解计算机的各种行为,运用相关知识分析hello程序的“一生”经历。
关键词: 预处理;编译;汇编;链接;运行;子进程;虚拟内存;信号
目 录
6.2 简述壳Shell-bash的作用与处理流程... - 22 -
6.3 Hello的fork进程创建过程... - 22 -
7.2 Intel逻辑地址到线性地址的变换-段式管理... - 27 -
7.3 Hello的线性地址到物理地址的变换-页式管理... - 27 -
7.4 TLB与四级页表支持下的VA到PA的变换... - 28 -
7.5 三级Cache支持下的物理内存访问... - 28 -
7.6 hello进程fork时的内存映射... - 29 -
7.7 hello进程execve时的内存映射... - 29 -
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P过程:From Program to Progress。源代码文件hello.c经过预处理、编译、汇编和链接等过程,生成可执行目标文件hello。
O2O过程:From Zero-O to Zero-O。shell运行可执行目标文件hello,为其创建并管理进程,对其进行存储管理,映射虚拟内存,分配物理内存,CPU分配时间片并执行逻辑控制流。程序运行结束后,hello进程终止,被父进程回收,释放内存,回收虚拟内存。
1.2 环境与工具
硬件环境:X64CPU;8GHz;8GRAM;1TB HD
软件环境:Windows10 64位;VirtualBox;Ubuntu 18.04 LTS 64位
使用工具:VS Code;Objdump;Gdb;Hex Editor Neo
1.3 中间结果
hello.i 预处理hello.c得到的文件,是将引用的头文件展开后得到的C语言文件。
hello.s 编译hello.i得到的汇编语言文件。
hello.o 对hello.s进行汇编,得到的可重定位目标文件。
hello 对hello.o进行链接,生成的可执行文件。
hello_o_elf.txt 对hello.o进行elf分析得到的文本文件。
hello_out_elf.txt 对hello.out进行elf分析得到的文本文件。
1.4 本章小结
本章介绍了hello程序P2P、020的过程,以及实验的基本信息。
第2章 预处理
2.1 预处理的概念与作用
概念:预处理器cpp根据以字符#开头的命令(宏定义、条件编译),修改原始的C程序,将引用的所有库展开合并成为一个完整的文本文件。
作用:将源文件中用#include形式声明的文件复制到新的程序中。比如hello.c第6-8行中的#include 等命令告诉预处理器读取系统头文件stdio.h unistd.h stdlib.h 的内容,并把它直接插入到程序文本中。用实际值替换用#define定义的字符串,根据#if后面的条件决定需要编译的代码。
2.2在Ubuntu下预处理的命令
命令:cpp hello.c > hello.i
图2-1 执行预处理
2.3 Hello的预处理结果解析
图2-2 hello.i中定位main函数
在hello.i中,main函数从第3047行开始,前面的内容是所引用的头文件的展开。
2.4 本章小结
本章简要介绍了预处理操作,并对hello.c文件进行了预处理,并分析所得文件hello.i。
第3章 编译
3.1 编译的概念与作用
编译:将C程序转化为汇编代码。
作用:编译器将hello.i文件转化为汇编指令写成的程序文件hello.c。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
图3-1 编译
3.3 Hello的编译结果解析
3.3.1 数据
hello.s中涉及的C数据类型有:整数、字符串、数组。
整数:
int i:局部变量存放在寄存器或栈中,在hello.s中,i存放在-4(%rbp)中。
int argc:存放在%rdi中。
字符串:
图3-2 hello.s中存放的字符串常量
数组:
hello程序中的数组是char *argv[],作为main函数的参数,指针存放在%rsi中。
3.3.2 赋值
程序中涉及赋值的操作:i = 0。
对应的汇编代码为:
图3-3 赋值操作的汇编代码
3.3.3 算术运算
程序中的算术运算操作:i++。
对应汇编指令:
图3-4 算术运算的汇编代码
3.3.4 关系运算
程序中的关系运算操作:
argc != 4、i < 8: 用cmpl语句实现:
图3-5 数值比较
3.3.5 控制转移
程序中的控制转移语句有:
- if (argc != 4),如果argc等于4(条件不成立),则跳转至指定的语句;
图3-6 if语句的实现
- for(i=0;i<8;i++),汇编代码如下图:
图3-7 for循环的实现
其中,-4(%rbp)存储i的值,若i不小于7,则跳转回.L4处。
3.3.6 函数操作
程序中涉及的函数操作有:
- main函数:
传递控制,函数被调用(call)时才能执行,因此被系统启动函数__libc_start_main调用。call指令将下一条指令的地址压栈,然后跳转到main函数。
传递数据,外部调用过程向main函数传递参数argc和argv,依次使用%rdi和%rsi存储,函数正常返回值是0,将%eax设置为0。
分配和释放内存,使用%rbp记录栈帧的底,函数分配栈帧空间在%rbp的上方,程序结束时,调用leave指令,leave相当于mov %rbp,%rsp;pop %rbp,恢复栈空间为调用之前的状态,然后ret返回,ret相当pop IP,将下一条要执行指令的地址设置为dest。
printf函数:
传递数据:第一次printf将%rdi设置为“用法: Hello 学号 姓名!\n”字符串的首地址。第二次printf设置%rdi为“Hello %s %s\n”的首地址,设置%rsi为argv[1],%rdx为argv[2]。
控制传递:第一次printf因为只有一个字符串参数,所以call puts@PLT;第二次printf使用call printf@PLT。
exit函数:
传递数据:将%edi设置为1。
控制传递:call exit@PLT。
sleep函数:
传递数据:将%edi设置为sleepsecs。
控制传递:call sleep@PLT。
getchar函数:
控制传递:call gethcar@PLT
3.4 本章小结
本章主要阐述编译器对C语言的各个数据类型以及各类操作的处理方法,先给出原理,再结合hello.c的 C程序到hello.s汇编代码之间的映射关系作出解释。
编译器将.i的拓展程序编译为.s的汇编代码。经过编译之后,我们的hello从C语言转化成更加底层的汇编语言。
第4章 汇编
4.1 汇编的概念与作用
汇编器(as)将.s汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在.o目标文件中,.o文件是一个二进制文件,它包含程序的指令编码。
4.2 在Ubuntu下汇编的命令
as hello.s -o hello.o
图4-1 执行汇编命令
4.3 可重定位目标elf格式
命令:readelf -a hello.o > hello_o_elf.txt。功能:用readelf读取hello.o,存放到文本文件中。ELF格式的组成如下:
ELF 头:以16B的序列Magic开始,Magic描述了生成该文件的系统的字的大小和字节顺序,ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型、机器类型、字节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量等信息。
图4-2 ELF头
节头部表:包含了文件中出现的各个节的语义,包括节的类型、位置和大小等信息。
图4-3 节头部表
重定位节’.rela.text’ ,一个.text节中位置的列表,包含.text节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。如图4-4,图中8条重定位信息分别是对.L0(第一个printf的字符串参数)、puts函数、exit函数、.L1(第二个printf的字符串参数)、printf函数、sleepsecs、sleep函数、getchar函数进行的重定位声明。
图4-4 重定位节.rela.text
4.4 Hello.o的结果解析
图4-5 用objdump反汇编
总体观察图4-5后发现,除去显示格式之外两者差别不大,主要差别如下:
分支转移:反汇编代码跳转指令的操作数使用的不是段名称如.L3,因为段名称只是在汇编语言中便于编写的助记符,所以在汇编成机器语言之后显然不存在,而是确定的地址。
函数调用:在.s文件中,函数调用之后直接跟着函数名称,而在反汇编程序中,call的目标地址是当前下一条指令。这是因为hello.c中调用的函数都是共享库中的函数,最终需要通过动态链接器才能确定函数的运行时执行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其call指令后的相对地址设置为全0(目标地址正是下一条指令),然后在.rela.text节中为其添加重定位条目,等待静态链接的进一步确定。
全局变量访问:在.s文件中,访问rodata(printf中的字符串),使用段名称+%rip,在反汇编代码中0+%rip,因为rodata中数据地址也是在运行时确定,故访问也需要重定位。所以在汇编成为机器语言时,将操作数设置为全0并添加重定位条目。
4.5 本章小结
本章阐述了从hello.s到hello.o的汇编过程,通过查看hello.o的elf格式文本和对objdump得到的反汇编代码与hello.s进行比较,展示了从汇编语言映射到机器语言汇编器需要实现的转换。
第5章 链接
5.1 链接的概念与作用
概念:经过汇编后,链接器(as)将可重定位目标程序与其他需要的各种代码和数据片段收集并组合称为一个单一文件,即可执行目标文件(二进制文件),它可被加载(复制)到内存并执行。
作用:链接可以执行于编译时,也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被加载到内存并执行时;甚至执行于运行时,也就是由应用程序来执行。早期计算机系统中链接时手动执行的,在现代系统中,链接器由链接器自动执行。链接器使得分离编译成为可能。开发过程中无需将大型的应用程序组织委员一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。
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-1 执行链接指令
5.3 可执行目标文件hello的格式
图5-2 用readelf读取hello的ELF格式
段头部表如下:
图5-3 段头部表
可以看出,段头部表概括了各段的基本信息,包括各段的起始地址,大小等信息。
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息:
图5-4 查看虚拟地址空间
同时查看elf文件中的程序头部分:
图5-5 程序头
可以发现大致的对应关系,例如,对于LOAD代码段,可以看出开始于内存地址0x400000处,总共内存大小是0x5c0个字节,并且被初始化为可执行目标文件的头0x5c0字节,并且可以在edb中找到对应的位置:
图5-5 LOAD代码段
5.5 链接的重定位过程分析
图5-6 执行objdump
可以看出,hello程序比hello.c多了一些内容。
l. 函数个数:在使用ld命令链接的时候,指定了动态链接器为64的/lib64/ld-linux-x86-64.so.2,crt1.o、crti.o、crtn.o中主要定义了程序入口_start、初始化函数_init,_start程序调用hello.c中的main函数,libc.so是动态链接共享库,其中定义了hello.c中用到的printf、sleep、getchar、exit函数和_start中调用的__libc_csu_init,__libc_csu_fini,__libc_start_main。链接器将上述函数加入。2. 函数调用:链接器解析重定条目时发现对外部函数调用的类型为R_X86_64_PLT32的重定位,此时动态链接库中的函数已经加入到了PLT中,.text与.plt节相对距离已经确定,链接器计算相对距离,将对动态链接库中函数的调用值改为PLT中相应函数与下条指令的相对地址,指向对应函数。对于此类重定位链接器为其构造.plt与.got.plt。
3. rodata引用:链接器解析重定条目时发现两个类型为R_X86_64_PC32的对.rodata的重定位(printf中的两个字符串),.rodata与.text节之间的相对距离确定,因此链接器直接修改call之后的值为目标地址与下一条指令的地址之差,指向相应的字符串。
5.6 hello的执行流程
使用edb执行hello,观察函数执行流程,过程中执行的主要函数如下:
ld-2.27.so!_dl_start
ld-2.27.so!_dl_init
hello!_start
libc-2.27.so!__libc_start_main
-libc-2.27.so!__cxa_atexit
-libc-2.27.so!__libc_csu_init
hello!_init
libc-2.27.so!_setjmp
-libc-2.27.so!_sigsetjmp
--libc-2.27.so!__sigjmp_save
hello!main
hello!puts@plt
hello!exit@plt
*hello!printf@plt
*hello!sleep@plt
*hello!getchar@plt
ld-2.27.so!_dl_runtime_resolve_xsave
-ld-2.27.so!_dl_fixup
--ld-2.27.so!_dl_lookup_symbol_x
libc-2.27.so!exit
5.7 Hello的动态链接分析
在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。GNU编译系统使用延迟绑定(lazybinding),将过程地址的绑定推迟到第一次调用该过程时。延迟绑定是通过GOT和PLT实现的。GOT是数据段的一部分,而PLT是代码段的一部分。两表内容分别为:
PLT:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。
GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。
根据hello ELF文件可知,GOT起始表位置为0x601000,如图:
图5-7 got起始表
同时观察虚拟地址空间,发现了以下不同:
调用dl_init之前,0x601008后的16个字节均为0:
图5-8 调用dl_init之前
调用dl_init之后,调用_start之后发生改变,0x601008后的两个8个字节分别变为:0x7fb06087e168、0x7fb06066e870,其中GOT[O](对应0x600e28)和GOT[1](对应0x7fb06087e168)包含动态链接器在解析函数地址时会使用的信息。GOT[2](对应0x7fb06066e870)是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,改变后的GOT表如下:
图5-9 改变后的GOT表
GOT[2]对应部分是共享库模块的入口点,如下:
图5-10 got[2]
举例puts函数在调用puts函数前对应GOT条目指向其对应的PLT条目的第二条指令,如图puts@plt指令跳转的地址:
图5-11 puts@plt指令跳转的地址
在之后的函数调用时,首先跳转到PLT执行.plt中逻辑,第一次访问跳转时GOT地址为下一条指令,将函数序号压栈,然后跳转到PLT[0],在PLT[0]中将重定位表地址压栈,然后访问动态链接器,在动态链接器中使用函数序号和重定位表确定函数运行时地址,重写GOT,再将控制传递给目标函数。之后如果对同样函数调用,第一次访问跳转直接跳转到目标函数。
因为在PLT中使用的jmp,所以执行完目标函数之后的返回地址为最近call指令下一条指令地址,即在main中的调用完成地址。
5.8 本章小结
在本章中主要介绍了链接的概念与作用、hello的ELF格式,分析了hello的虚拟地址空间、重定位过程、执行流程、动态链接过程。
第6章 hello进程管理
6.1 进程的概念与作用
进程是一个执行中的程序的实例,每一个进程都有它自己的地址空间,一般情况下,包括文本区域、数据区域、和堆栈。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。
进程为用户提供了以下假象:我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存,处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
Shell的作用:Shell是一个用C语言编写的程序,他是用户使用Linux的桥梁。Shell 是指一种应用程序,Shell应用程序提供了一个界面,用户通过这个界面访问操作系统内核的服务。
处理流程:
1)从终端读入输入的命令。
2)将输入字符串切分获得所有的参数
3)如果是内置命令则立即执行
4)否则调用相应的程序为其分配子进程并运行
5)shell应该接受键盘输入信号,并对这些信号进行相应处理
6.3 Hello的fork进程创建过程
当在shell中运行hello后,父进程通过调用fork函数创建一个新的运行的子进程。新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时。子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程最大的区别在于他们有不同的id。
fork函数调用一次返回两次,在父进程中fork会返回子进程的PID,在子进程中fork会返回0;父进程与子进程是并发运行的独立进程。内核能够以任何方式交替执行他们逻辑控制流中的指令。系统进程创建hello子进程然后调用waitpid()函数知道hello子进程结束,程序进程图如下:
图6-1 fork进程图
6.4 Hello的execve过程
在终端中键入 ./hello 1190201913 alex,运行的终端程序会对输入的命令行进行解析,因为hello不是一个内置的shell命令所以解析之后终端程序判断./hello的语义为执行当前目录下的可执行目标文件hello,之后终端程序首先会调用fork函数创建一个新的运行的子进程,新创建的子进程几乎但不完全与父进程相同,子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,这就意味着,当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程与子进程之间最大的区别在于它们拥有不同的PID。
父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的逻辑控制流的指令。在子进程执行期间,父进程默认选项是显示等待子进程的完成。
6.5 Hello的进程执行
逻辑控制流:一系列程序计数器PC的值的序列叫做逻辑控制流,进程是轮流使用处理器的,在同一个处理器核心中,每个进程执行它的流的一部分后被抢占(暂时挂起),然后轮到其他进程。
时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
用户模式和内核模式:处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。
简单看hello sleep进程调度的过程:当调用sleep之前,如果hello程序不被抢占则顺序执行,假如发生被抢占的情况,则进行上下文切换,上下文切换是由内核中调度器完成的,当内核调度新的进程运行后,它就会抢占当前进程,并进行1)保存以前进程的上下文2)恢复新恢复进程被保存的上下文,3)将控制传递给这个新恢复的进程 ,来完成上下文切换。
hello初始运行在用户模式,在hello进程调用sleep之后陷入内核模式,内核处理休眠请求主动释放当前进程,并将hello进程从运行队列中移出加入等待队列,定时器开始计时,内核进行上下文切换将当前进程的控制权交给其他进程,当定时器到时时(2.5secs)发送一个中断信号,此时进入内核状态执行中断处理,将hello进程从等待队列中移出重新加入到运行队列,成为就绪状态,hello进程就可以继续进行自己的控制逻辑流了。
之后的9个sleep进程调度如上。
当hello调用getchar的时候,实际落脚到执行输入流是stdin的系统调用read,hello之前运行在用户模式,在进行read调用之后陷入内核,内核中的陷阱处理程序请求来自键盘缓冲区的DMA传输,并且安排在完成从键盘缓冲区到内存的数据传输后,中断处理器。此时进入内核模式,内核执行上下文切换,切换到其他进程。当完成键盘缓冲区到内存的数据传输时,引发一个中断信号,此时内核从其他进程进行上下文切换回hello进程。进程切换过程同上,省略。
6.6 hello的异常与信号处理
如图6-2,是正常执行hello程序的结果,当程序执行完成之后,进程被回收。
如图6-3,是在程序输出2条info之后按下ctrl-z的结果,当按下ctrl-z之后,shell父进程收到SIGSTP信号,信号处理函数的逻辑是打印屏幕回显、将hello进程挂起,通过ps命令我们可以看出hello进程没有被回收,此时他的后台job号是1,调用fg 1将其调到前台,此时shell程序首先打印hello的命令行命令,hello继续运行打印剩下的6条info,之后输入字串,程序结束,同时进程被回收。
如图6-4是在程序输出3条info之后按下ctrl-c的结果,当按下ctrl-c之后,shell父进程收到SIGINT信号,信号处理函数的逻辑是结束hello,并回收hello进程。
如图6-5是在程序运行中途乱按的结果,可以发现,乱按只是将屏幕的输入缓存到stdin,当getchar的时候读出一个’\n’结尾的字串(作为一次输入),其他字串会当做shell命令行输入。
图6-2 正常执行hello程序
图6-3 运行中按下ctrl-z
图6-4 运行中按下ctrl-c
图6-5 运行时乱按
6.7本章小结
本章主要探讨了hello的进程管理,首先介绍了进程的概念与作用,并对shell做了介绍,然后研究了hello的fork、exceve过程,简单概括就是调用fork 函数创建一个新的运行的子进程,然后调用exceve加载hello。最后对hello程序执行时的进程管理和异常进行了研究。
第7章 hello的存储管理
7.1 hello的存储器地址空间
物理地址:CPU通过地址总线的寻址,找到真实的物理内存对应地址。 CPU对内存的访问是通过连接着CPU和北桥芯片的前端总线来完成的。在前端总线上传输的内存地址都是物理内存地址。
逻辑地址:程序代码经过编译后出现在 汇编程序中地址。逻辑地址由选择符(在实模式下是描述符,在保护模式下是用来选择描述符的选择符)和偏移量(偏移部分)组成。
线性地址:逻辑地址经过段机制后转化为线性地址,为描述符:偏移量的组合形式。分页机制中线性地址作为输入。
至于虚拟地址,只关注课本中提到的虚拟地址,实际上就是这里的线性地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
在x86保护模式下,段的信息(段基线性地址、长度、权限等)即段描述符占8个字节,段信息无法直接存放在段寄存器中(段寄存器只有2字节)。Intel的设计是段描述符集中存放在GDT或LDT中,而段寄存器存放的是段描述符在GDT或LDT内的索引值(index)。
一个逻辑地址由两部份组成,段标识符: 段内偏移量。
首先,给定一个完整的逻辑地址[段选择符:段内偏移地址],
1、看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再依据对应寄存器,得到其地址和大小。我们就有了一个数组了。
2、拿出段选择符中前13位,能够在这个数组中。查找到相应的段描写叙述符,这样。它了Base。即基地址就知道了。
3、把Base + offset,就是要转换的线性地址了。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址即虚拟地址,用VA来表示。由图,VA被分为虚拟页号(VPN)与虚拟页偏移量(VPO),CPU取出虚拟页号,通过页表基址寄存器(PTBR)来定位页表条目,在有效位为1时,从页表条目中取出信息物理页号(PPN),通过将物理页号与虚拟页偏移量(VPO)结合,得到由物理地址(PPN)和物理页偏移量(PPO)组合的物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
在Intel Core i7环境下研究VA到PA的地址翻译问题。前提如下:
虚拟地址空间48位,物理地址空间52位,页表大小4KB,4级页表。TLB 4路16组相联。CR3指向第一级页表的起始位置(上下文一部分)。
解析前提条件:由一个页表大小4KB,一个PTE条目8B,共512个条目,使用9位二进制索引,一共4个页表共使用36位二进制索引,所以VPN共36位,因为VA 48位,所以VPO 12位;因为TLB共16组,所以TLBI需4位,因为VPN 36位,所以TLBT 32位。
CPU产生虚拟地址VA,VA传送给MMU,MMU使用前36位VPN作为TLBT(前32位)+TLBI(后4位)向TLB中匹配,如果命中,则得到PPN(40bit)与VPO(12bit)组合成PA(52bit)。
如果TLB中没有命中,MMU向页表中查询,CR3确定第一级页表的起始地址,VPN1(9bit)确定在第一级页表中的偏移量,查询出PTE,如果在物理内存中且权限符合,确定第二级页表的起始地址,以此类推,最终在第四级页表中查询到PPN,与VPO组合成PA,并且向TLB中添加条目。
如果查询PTE的时候发现不在物理内存中,则引发缺页故障。如果发现权限不够,则引发段错误。
7.5 三级Cache支持下的物理内存访问
前提:只讨论L1 Cache的寻址细节,L2与L3Cache原理相同。L1 Cache是8路64组相联。块大小为64B。
解析前提条件:因为共64组,所以需要6bit CI进行组寻址,因为共有8路,因为块大小为64B所以需要6bit CO表示数据偏移位置,因为VA共52bit,所以CT共40bit。
在上一步中我们已经获得了物理地址VA,使用CI(后六位再后六位)进行组索引,每组8路,对8路的块分别匹配CT(前40位)如果匹配成功且块的valid标志位为1,则命中(hit),根据数据偏移量CO(后六位)取出数据返回。
如果没有匹配成功或者匹配成功但是标志位是1,则不命中(miss),向下一级缓存中查询数据(L2 Cache->L3 Cache->主存)。查询到数据之后,一种简单的放置策略如下:如果映射到的组内有空闲块,则直接放置,否则组内都是有效块,产生冲突(evict),则采用最近最少使用策略LFU进行替换。
7.6 hello进程fork时的内存映射
当fork 函数被shell调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID 。为了给hello进程创建虚拟内存,它创建了hello进程的mm_struct 、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。当fork 在hello进程中返回时,hello进程现在的虚拟内存刚好和调用fork 时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve 函数在shell中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下几个步骤:
1删除已存在的用户区域。删除当前进程(shell)虚拟地址的用户部分中的已存在的区域结构。
2映射私有区域。为hello的代码、数据、bss 和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello 文件中的.text和.data 区。bss 区域是请求二进制零的,映射到匿名文件,其大小包含在hello 中。栈和堆区域也是请求二进制零的,初始长度为零。下图概括了私有区域的不同映射。
3映射共享区域。如果hello程序与共享对象(或目标)链接,比如标准C 库libc. so, 那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
4设置程序计数器(PC) 。execve 做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
在虚拟内存的习惯说法中, DRAM 缓存不命中称为缺页(page fault) 。下图 展示了在缺页之前我们的示例页表的状态。CPU 引用了VP 3 中的一个字, VP 3 并未缓存在DRAM 中。地址翻译硬件从内存中读取PTE 3, 从有效位推断出VP 3 未被缓存,并且触发一个缺页异常。缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,在此例中就是存放在PP3中的VP4 。如果VP4已经被修改了,那么内核就会将它复制回磁盘。无论哪种情况,内核都会修改VP4 的页表条目,反映出VP 4 不再缓存在主存中这一事实。
7.9动态存储分配管理
动态储存分配管理使用动态内存分配器来进行。动态内存分配器维护着一个进程的虚拟内存区域,称为堆。
分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可以用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配的状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器有两种基本风格,都要求应用显式地分配块,它们的不同之处在于由哪个实体来负责释放已分配的块。
显式分配器:要求应用显式地释放任何已分配的块。
隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。而自动释放未使用的已分配的块的过程叫做垃圾收集。
动态内存分配主要有两种基本方法与策略:
带边界标签的隐式空闲链表分配器管理
带边界标记的隐式空闲链表的每个块是由一个字的头部、有效载荷、可能的额外填充以及一个字的尾部组成的。
在隐式空闲链表中,因为空闲块是通过头部中的大小字段隐含地连接着的。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。其中,一个设置了已分配的位而大小为零的终止头部将作为特殊标记的结束块。
当一个应用请求一个k字节的块时,分配器搜索空闲链表,查找一个足够大的可以放置所请求块的空闲块。分配器有三种放置策略:首次适配、下一次适配合最佳适配。分配完后可以分割空闲块减少内部碎片。同时分配器在面对释放一个已分配块时,可以合并空闲块,其中便利用隐式空闲链表的边界标记来进行合并。
显式空间链表管理
显式空闲链表是将空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。如,堆可以组织成一个双向链表,在每个空闲块中,都包含一个前驱与一个后继指针。
在显式空闲链表中。可以采用后进先出的顺序维护链表,将最新释放的块放置在链表的开始处,也可以采用按照地址顺序来维护链表,其中链表中每个块的地址都小于它的后继地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。
7.10本章小结
本章主要对hello的存储管理进行了研究,主要包括有逻辑地址、线性地址、虚拟地址、物理地址以及他们之间的变换,同时对虚拟内存的原理并结合hello探究了虚拟地址转化为物理地址的过程。最后,对动态内存的分配也进行了探讨,主要介绍了动态内存分配的两种方法与策略。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:所有的IO设备都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单低级的应用接口,称为Unix I/O。
8.2 简述Unix IO接口及其函数
Unix I/O接口统一操作:
打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。
Shell创建的每个进程都有三个打开的文件:标准输入,标准输出,标准错误。
改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。
读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。
Unix I/O函数:
int open(char* filename,int flags,mode_t mode) ,进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
int close(fd),fd是需要关闭的文件的描述符,close返回操作结果。
ssize_t read(int fd,void *buf,size_t n),read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
ssize_t wirte(int fd,const void *buf,size_t n),write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。
8.3 printf的实现分析
首先查看pringf函数:
static int printf(const char *fmt, ...)
{
va_list args;
int i;
va_start(args, fmt);
write(1,printbuf,i=vsprintf(printbuf, fmt, args));
va_end(args);
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': //只处理%x一种情况
itoa(tmp, *((int*)p_next_arg)); //将输入参数值转化为字符串保存在tmp
strcpy(p, tmp); //将tmp字符串复制到p处
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
根据代码可知内核向寄存器传递几个参数后,中断调用syscall函数。对应ebx打印输出的buf数组中第一个元素的地址,ecx是要打印输出的个数。
再看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
其中ecx中是要打印出的元素个数 ,ebx中的是要打印的buf字符数组中的第一个元素,sys_call的功能就是不断的打印出字符,直到遇到:’\0’ 。其中call是访问字库模板并且获取每一个点的RGB信息最后放入到eax也就是输出返回的应该是显示vram的值,然后系统显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
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有一个int型的返回值。当程序调用getchar时,程序就等着用户按键,用户输入的字符被存放在键盘缓冲区中直到用户按回车为止(回车字符也放在缓冲区中)。当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的第一个字符的ASCII码,如出错返回-1,且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完为后,才等待用户按键。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接收到回车键才返回。
8.5本章小结
本章主要研究了hello的IO管理。首先对Linux系统下的IO设备管理方法以及Unix IO接口及其函数进行了分析,然后着重分析了printf函数与getchar函数的实现,对hello的IO管理有了一定的了解。
结论
hello所经历的过程:
hello最初是一个C语言源程序hello.c,里面包含了hello程序所有的C语言代码。
hello.c经过预处理后,得到hello.i,hello的预处理文本文件。
hello.i经历编译后,得到hello.s,里面包含一个汇编程序。
hello.s经历汇编后,得到hello.o,hello的可重定位目标文件。
hello.o经历链接后,得到最终的可执行目标文件——hello。
在shell中运行hello,shell为hello程序fork子进程,调用execve,将hello程序加载并运行,得到子进程hello
通过虚拟内存映射,为hello分配虚拟内存空间,内核通过为hello划分时间片,让hello能够执行自己的逻辑流。
运行hello时,内存管理单元MMU、翻译后备缓冲器TLB、多级页表机制、三级cache共同完成hello对地址的请求。
异常处理机制保证了hello对异常信号的处理,使程序平稳运行。
Unix I/O让程序能够与文件进行交互。
当hello运行完毕,shell父进程回收hello,hello的一生结束。
初学C语言时,以为hello.c能一步变为可执行文件,过程很简单。通过本次大作业,我体会到原来中间有这么多环节,涉及计算机系统软硬件的方方面面,我加深了对程序运行过程和计算机系统组成的认识。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
hello.c C程序源文件
hello.i 预处理hello.c得到的文件,是将引用的头文件展开后得到的C语言文件。
hello.s 编译hello.i得到的汇编语言文件。
hello.o 对hello.s进行汇编,得到的可重定位目标文件。
hello 对hello.o进行链接,生成的可执行文件。
hello_o_elf.txt 对hello.o进行elf分析得到的文本文件。
hello_out_elf.txt 对hello.out进行elf分析得到的文本文件。
参考文献
为完成本次大作业你翻阅的书籍与网站等
[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.