计算机系统大作业——程序人生

摘 要
本文介绍了hello的程序人生。通过hello.c里面的简单代码,并在ubuntu环境下使用命令对它进行一系列操作,依次介绍了预处理、编译、汇编、链接直到最后运行背后的原理,以及说明了系统是如何进行进程管理、存储管理和I\O管理的

关键词:汇编;hello.c;命令;计算机系统;存储

目 录

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

第1章 概述
1.1 Hello简介
P2P(from program to process):其中,program指的是在editor中键入代码得到hello.c程序,process是在linux中,hello.c经过cpp预处理,ccl编译,as汇编,ld链接之后,最终可以作为目标程序执行。在shell中键入启动命令后,shell会使用fork为其创建并生成子进程,这样hello从Program转换为Process.
020(From Zero-0 to Zero-0): shell为此子进程execve,以此来将程序计数器设置为程序入口点。接着映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入 main函数执行目标代码,CPU为运行的hello分配时间片执行逻辑控制流。当程序运行结束后,shell父进程负责回收hello进程,释放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.c 源代码
hello.i hello.c预处理生成的文本文件
hello.s hello.i经过编译器翻译成的文本文件
hello.o hello.s经汇编器翻译成机器语言指令打包成的可重定位目标文件
hello 经过hello.o链接生成的可执行目标文件hello.s经汇编器翻译成机器语言指令打包成的可重定位目标文件
hello.elf hello.o的elf格式文件
hello1elf.txt hello的elf格式文件

1.4 本章小结
介绍了hello的P2P(from program to process)和020(From Zero-0 to Zero-0),标明了此次报告使用的环境与工具,列出了为编写本论文,生成的中间结果文件的名字,文件的作用等。

第2章 预处理
2.1 预处理的概念与作用
预处理的概念:
预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。典型地,由预处理器对程序源代码文本进行处理,得到的结果再由编译器核心进一步编译。这个过程并不对程序的源代码进行解析,但它把源代码分割或处理成为特定的单位。
预处理的作用:
预处理中会展开以#起始的行,称为预处理指令。预处理指令一般被用来使源代码在不同的执行环境中被方便的修改或者编译。
2.2在Ubuntu下预处理的命令
命令:cpp hello.c>hello.i
截图:

图2.2 预处理命令

图2.3 命令结果截图
2.3 Hello的预处理结果解析
结果截图:

图2.3 预处理结果截图
结果解析:在输入命令生成的hello.i文件中,原来hello.c的代码少了三个#include开头的行,但前面多了非常多的内容。其中,在这些多出来的内容中,包括了很多文件的路径,一些用到的数据结构定义,很多extern关键字即声明与定义相对。经过分析,我们可以直到这些多出来的代码是少了的三行#include代码转换得来的,是原文件中的宏的宏展开。
2.4 本章小结
本章说明了预处理的概念与作用,并用给出的hello.c文件作为实例,通过将其在虚拟机中转换为hello.i文件,接着观察生成的文件并加以分析,进一步说明了预处理的作用。

第3章 编译
3.1 编译的概念与作用
编译的概念:
总体来说,源程序会通过翻译程序加工以后生成计算机可以理解的语言——机器语言程序,其中把源程序转化为目标程序的操作就叫做编译。
具体来说,编译是利用编译程序从源语言编写的源程序产生目标程序的过程,用编译程序产生目标程序的动作。 编译就是把高级语言变成计算机可以识别的汇编语言。 编译程序把一个源程序翻译成目标程序的工作过程分为五个阶段:词法分析;语法分析;语义检查和中间代码生成;代码优化;目标代码生成。主要是进行词法分析和语法分析,又称为源程序分析,分析过程中发现有语法错误,给出提示信息。
编译的作用:
计算机是识别不了上一章中我们生成的预处理生成的文件hello.i的,因而我们需要先对它进行编译,把高级语言(hello.c中的c语言)变成计算机可以理解的汇编程序语言。
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.i -o hello.s
截图:

图3.2 命令截图

图3.2 编译命令结果截图
3.3 Hello的编译结果解析
结果截图:

图3.3 编译结果截图
解析:
3.3.1数据
3.3.1.1常量
常量的值保存在.text中,也被保存在.rodata等位置中。
例如hello.c文件中的这一块

这里这个3就会保存在.text节中。
在例如hello.c文件中的这一块

对应的则是hello.s文件中的这一块

说明printf后面的的字符串常量“Hello 1190200826 xhy”便是储存在.rodata节中。
3.3.1.2变量(全局变量/局部/静态)
全局变量:
例如hello.c文件中的这一块

对应的则是hello.s文件中的这一块

说明代码中的全局变量sleepsecs是存储在.data节中,并且它的初始化不需要汇编语句。
局部变量:
例如hello.c文件中的这一块

对应的则是hello.s文件中的这一块

说明代码中的main函数中的这个局部变量i是存储在栈中的(其他局部变量也可能存储在寄存器中),这里的被保存在栈的%rsp-4的位置。
3.3.1.3数组
例如传入main函数的第二个参数char *argv[],在hello.c中的表达形式如下:

对应的则是hello.s文件中的这一块

这说明数组char *argv[]是存放在栈中的,且存档的位置是(%rbp-32)。
3.3.1.4立即数
例如这里的立即数3

对应的则是hello.s文件中的这一块

这说明立即数是直接体现在汇编代码中的。
3.3.2函数
总体来说,函数满足传递参数规则,也就是说前面六个参数依次存储在寄存器%rdi、%rsi、%rdx、%rcx、%r8、%r9中,之后在出现的参数,回依次压入栈中。
例如main函数,在hello.c文件中是这一块

对应的则是hello.s文件中的这一块

说明代码中,传入main函数中两个参数int argc以及int argv分别存储在寄存器%edi和寄存器%rsi中。
再例如printf函数,在hello.c文件中是这两块

对应的则分别是hello.s文件中的这三个部分

再例如exit函数,在hello.c文件中是这一块

对应的则分别是hello.s文件中的这一个部分

其中在exit函数传入的参数1便是用寄存器edi存储的。
再例如sleep函数,在hello.c文件中是这一块

对应的则分别是hello.s文件中的这一个部分

说明代码中,传入sleep函数的参数sleepsecs存储在栈中了。
再例如getchar函数,在hello.c文件中是这一块

对应的则是hello.s文件中的这一个部分

3.3.3算术操作

例如这里每一次循环给i加上1,在hello.c文件中的这一块

对应的则是hello.s文件中的这一个部分

这是汇编代码中对于一元操作数的代码,addl表示让dst加上1。
3.3.4赋值操作
例如在hello.c文件中的这一块,给i赋值为0

对应的则是hello.s文件中的这一个部分

其中mov后面的l表示是四个字节,mov后面一共会有四种对于字节的表达方式,b对应一个字节,w对应两个字节,l对应四个字节,q对应八个字节。
3.3.5控制转移
这一段代码中,有很多控制转移,全都是用jmp命令实现的,其中jmp命令也可以在后面加上条件,来让汇编代码变得更精简,效率更高。
例如这里的循环条件,让i和10进行比较,如果小于10才继续进行循环,大于10就跳出循环。在hello.c文件中的代码如下

在汇编代码中,对应的则是hello.s文件中的这一个部分

其中,我们可以看到汇编代码首先比较了保存在栈中的变量i与立即数9大小,如果小于等于9便会跳到.L4部分,也就是这个部分:

for循环里面的部分。
如果大于9,它就会跳过代码的第56行,执行第57行的call函数,也就是for循环后面的这个getchar函数。

3.4 本章小结
本章说明了汇编的概念,以及作用,与前一章预处理之间的联系。同时第二节中,举了大量给定文件转换而来的hello.s文件中与给定文件hello.c中的对照翻译,依次来说明编译器面对各种数据和操作的机制以及代码中个数据存储的位置,并解释了汇编语言如何可以翻译成高级语言。

第4章 汇编
4.1 汇编的概念与作用
汇编的概念:
简单来说,把汇编语言翻译成机器语言的过程称为汇编。在汇编语言中,用助记符代替操作码,用地址符号或标代替地址码。这样用符号代替机器语言的二进制码,就把机器语言变成了汇编语言,于是汇编语言亦称为符号语言。用汇编语言编写的程序,机器不能直接识别,要由一种程序将汇编语言翻译成机器语言,这种起翻译作用的程序叫汇编程序,汇编程序是系统软件中语言处理的系统软件。
汇编的作用:
汇编把汇编语言翻译成了机器能够完全理解的机器代码,并把这些代码打包成了可重定位目标程序的格式。
4.2 在Ubuntu下汇编的命令
命令:gcc hello.s -o hello.o
截图:

图4.2 命令截图

图4.2 汇编命令结果截图
4.3 可重定位目标elf格式
命令:readelf -a hello.o > hello.elf
截图:

图4.3 命令截图

图4.3 汇编命令结果截图
首先,先查看elf目标文件格式

图4.3elf目标文件格式
4.3.1ELF头
ELF头以一个十六字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助连接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是由节头部表描述的,其中目标文件中的每个节都有一个固定大小的条目。

图4.3.2 hello.elf中ELF头的部分
4.3.2节头表

图4.3.2 hello.elf中节头部表的部分
加载ELF头和节头部表之间的都是节。一个典型的ELF可重定位目标文件包含下面几个节:
4.3.3重定位节
在重定位的步骤中,链接器将所有相同类型的节合并为同一类型的新的聚合节。例如,来自所有输入模块的.data节全被合并成一个接,这个节称为输出的可执行目标文件的.data节。然后,链接器将运行内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。但这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。

图4.3.2 hello.elf中重定位节的部分
其中,offset是需要被修改的引用节的偏移;Info包括symbol和type两个部分,symbol在前面四个字节,type在后面四个字节;symbol是标识被修改引用应该指向的符号;type是重定位的类型;Type告知链接器应该如何修改新的应用;Attend是一个有符号常数,一些重定位要使用它对被修改引用的值做偏移调整;Name是重定向到的目标的名称。
4.3.3.symtab节
一个符号表,它存放在程序中定义和引用的函数的全局变量的信息。每个可重定位目标文件在.symtab中都有一张符号表。然而,和编译器中的符号表不同,.symtab符号表不包含局部变量的条目。

图4.3.3 hello.elf中.symtab节的部分
4.4 Hello.o的结果解析
命令:objdump -d -r hello.o
截图:

图4.4-1 命令截图

图4.4-2 汇编命令结果截图
分析hello.o的反汇编,并与第3章的 hello.s进行对照分析:

图4.4-3 反汇编与hello.s里面的内容对照截图
通过分析机器语言的构成,与汇编语言的映射关系,我们可以知道:
在操作数方面:立即数在hello.s中仍然是十进制表示的,然而到了hello.o的反汇编全变成十六进制数表示了。
在分支转移方面:在hello.s中分支转移用的是助记符(.L2)存储在.rodata段中,然而到了hello.o的反汇编的分支转移用的则是确定的相对偏移地址。
在函数调用方面:在hello.s中函数调用是跟着函数名称的,然而到了hello.o的反汇编的函数调用全都直接是接下来一条指令的地址。
4.5 本章小结
本章首先列出来汇编的概念与作用,与前两张预处理和编译的区别联系。后面通过分析汇编的到hello.o文件,分析其中对应的各种段等,并将其反汇编,和第三章生成的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.2-2 命令运行结果截图5.3 可执行目标文件hello的格式
5.3 可执行目标文件hello的格式
命令:readelf -a hello > hello1elf.txt
截图:

图5.3 命令截图

图5.3 命令运行结果截图
5.3.1ELF头
ELF头以一个十六字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助连接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是由节头部表描述的,其中目标文件中的每个节都有一个固定大小的条目。

图5.3.1 hello1elf.txt中ELF头的部分
5.3.2节头部表

图5.3.2 hello1elf.txt中节头部表的部分
加载ELF头和节头部表之间的都是节。
5.3.3.symtab段

符号表,它存放在程序中定义和引用的函数的全局变量的信息。每个可重定位目标文件在.symtab中都有一张符号表。然而,和编译器中的符号表不同,.symtab符号表不包含局部变量的条目。
5.3.4.程序头表

5.3.5 Segment mapping

5.3.6 .dynsym节

5.3.7 重定位

5.4 hello的虚拟地址空间
使用edb打开hello,查看本进程的虚拟地址空间各段信息。

图5.4-1 用edb查看本进程的虚拟地址空间各段信息。
分析Data Dump窗口显示的虚拟地址空间各段信息,我们可以看到虚拟空间开始于0x400000,以及其他各节的信息(开始的地址与该节的大小)。其中,对应的表如下:

图5.4-2 虚拟地址空间的结构图示
5.5 链接的重定位过程分析
命令:objdump -d -r hello
截图:

图5.5 命令截图

图5.5-1 命令运行结果截图
hello与hello.o的不同:
前者比后者少了重定位条目。

图5.5-2 后者多出来的重定位条目
对应的,hello中函数调用和jmp跳转用的也都是虚拟内存地址。这意味着完成了重定位。
前者比后者多了.dynsym节、.rela节和.plt节,符号节也多出来了一段

图5.5-3 后者多出来的…rela节和.plt节

图5.5-3 后者多出来的.dynsym节

图5.5-3 .symtab节的比较
前者比后者多了一些基本函数的库函数
链接的过程:
1 对各个目标模块中没有定义的变量,在其它目标文件中找到相关的定义。
2 把不同目标文件中生成的相同类型的段进行合并。
3 把不同目标文件中的变量进行地质重定位。
其中,重定位的具体算法如下图所示:

图5.5-4 重定位算法
5.6 hello的执行流程
ld-2.27.so!_dl_start
ld-2.27.so!_dl_init
hello!main
ld-2.27.so! libc_start_main
libc-2.27.so!_cxa_atexit
ld-2.27.so! libc_csu_mit
ld-2.27.so!setjmp
libc-2.27.so!
exit
5.7 Hello的动态链接分析
动态链接的基本思想就是把程序按照模块拆分成各个相对独立部分,在程序运行时才将他们链接在一起形成一个完成的程序,而不是像静态链接把所有的模块都链接成一个单独的可执行文件。
在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。GNU编译系统使用延迟绑定,将过程地址的绑定推迟到第一次调用该过程时。
延迟绑定是通过GOT和PLT实现的。GOT是数据段的一部分,而PLT是代码段的一部分。两表内容分别为:
PLT:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。
GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。
在edb中追踪init执行前后的地址,我们可以发现对于变量,可以正常计算出地址,而对于库函数,就要使用上面的方法。
5.8 本章小结
本章首先介绍了连接的概念与作用,其次在linux中用指令进行连接。将得到的可执行目标文件用命令转成elf格式文件,并将它和第七章给的elf标准格式表进行各节的比对。用edb分析helo的虚拟地址空间,并分析链接的重定位以及hello的动态链接。

第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:
从用户角度来说进程就是一个正在运行中的程序;从操作系统角度来说,操作系统运行一个程序,需要描述这个程序的运行过程,这个描述通过一个结构体task_struct{}来描述,统称为PCB,因此对操作系统来说进程就是PCB程序控制块。
进程的作用:
帮助操作系统管理程序的运行。制造当前的程序是当前唯一运行的程序的假象,制造程序独占处理器和内存、代码和数据是系统中唯一对象的假象。
6.2 简述壳Shell-bash的作用与处理流程
Shell:
Shell是一个交互型应用级程序,代表用户运行其他程序。Shell既是一种命令语言,又是一种程序设计语言。作为命令语言,它交互式地解释和执行用户输入的命令;作为程序设计语言,它定义了各种变量、参数、函数、流程控制等等。它调用了系统核心的大部分功能来执行程序、建立文件并以并行的方式协调各个程序的运行。因此,对于用户来说,Shell是最重要的实用程序。
bash:
Bash是缺省的Linux shell(Bourne Again SHell,bash) 是基于GNU的增强版本sh(最早的shell) 。Bash是一个命令处理器,通常运行于文本窗口中,并能执行用户直接输入的命令。Bash还能从文件中读取命令,这样的文件称为脚本。和其他Unix shell 一样,它支持文件名替换(通配符匹配)、管道、here文档、命令替换、变量,以及条件判断和循环遍历的结构控制语句。包括关键字、语法在内的基本特性全部是从sh借鉴过来的。其他特性,例如历史命令,是从csh和ksh借鉴而来。总的来说,Bash虽然是一个满足POSIX规范的shell,但有很多扩展。
Shell和bash的关系:
在Linux系统中,bash是shell的一种。
处理流程:

  1. 将命令行分成由元字符(meta character) 分隔的记号(token):元字符包括 SPACE, TAB, NEWLINE, ; , (, ), <, >, |, &。记号的类型包括单词,关键字,I/O重定向符和分号。
    2. 检测每个命令的第一个记号,看是否为不带引号或反斜线的关键字。如果是一个 开放的关键字,如if和其他控制结构起始字符串,function,{或(,则命令实际上为一复合命令。shell在内部对复合命令进行处理,读取下一个命 令,并重复这一过程。如果关键字不是复合命令起始字符串,而是如then等一个控制结构中间出现的关键字,则给出语法错误信号。
    3. 依据别名列表检查每个命令的第一个关键字。如果找到相应匹配,则替换其别名定义,并退回第一步;否则进入第4步。
    4. 执行大括号扩展,例如a{b,c}变成ab ac
    5. 如果~位于单词开头,用HOME替换 。使用usr的主目录替换 user。6.对任何以符号HOME替换~。使用usr的主目录替换~user。 6. 对任何以符号HOME 使usr user6.开头的表达式执行参数(变量)替换
    7. 对形如(string)或者‘string‘的表达式进行命令替换,这里是嵌套的命令行处理。8.计算形式为(string)或者`string` 的表达式进行命令替换,这里是嵌套的命令行处理。 8. 计算形式为(string)string8.((string))的算术表达式。
    9. 把行的参数替换,命令替换和算术替换 的结果部分再次分成单词,这次它使用IFS中的字符做分割符而不是步骤1的元字符集。10.对出现∗,?,[]对执行路径名扩展,也称为通配符扩展。11.按命令优先级表(跳过别名),进行命令查寻。先作为一个特殊的内建命令,接着是作为函数,然后作为一般的内建命令,最后作为查找IFS中的字符做分割符而不是步骤1的元字符集。 10. 对出现*, ?, [ ]对执行路径名扩展,也称为通配符扩展。 11. 按命令优先级表(跳过别名),进行命令查寻。 先作为一个特殊的内建命令,接着是作为函数,然后作为一般的内建命令,最后作为查找IFS110.,?,[]11.()PATH找到的第一个文件。
    12. 设置完I/O重定向和其他操作后执行该命令。
    6.3 Hello的fork进程创建过程
    但fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时赋值。
    对于hello来说,输入./hello,shell会对其解析,由于./hello不是shell命令,shell会调用fork函数来创建子进程。
    6.4 Hello的execve过程
    Execve函数在当前进程中加载并运行包含在可执行目标文件a.out中的程序,用a.out程序有效地替代了当前程序。加载并运行a.out需要以下几个步骤:
    1、删除已存在的用户区域。删除当前进程虚拟地址的用户部分中已存在的区域结构。
    2、映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些区域结构都是私有的,写时复制的。虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区。bss区域是请求二进制零的,映射匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零。如图6.4
    3、映射共享区域。如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域。
    4、设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。
    下一次调用这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。

图6.4 加载器是如何映射用户地址空间的区域的
6.5 Hello的进程执行
Hello中存储着时间分片,与OS的其他进程并发执行,其中OS的内核采取上下文交换策略,如下图所示,内核为每个进程维持上下文。在

图6.5 上下文进程切换的剖析
其中,在执行阶段,内核可以调度,也就是抢占当前进程并用一个之前被抢占的进程替换它。同时,hello与OS通过调度,切换上下文,拥有各自的时间片,也就实现了并发运行。
6.6 hello的异常与信号处理
异常的类别分为:

图6.6-1 异常的类别
各种异常的处理:
中断:

图6.6-2 中断处理
陷阱:

图6.6-3 陷阱处理
故障:

图6.6-4 故障处理
终止:终止处理程序从不将控制返回给应用程序。
6.6.1正常运行:

一共运行十遍
6.6.2不停乱按:
6.6.2.1无回车

可以看出,乱按的内容直接显示在打印出的内容之后
6.6.2.2有回车

可以看出乱按的内容显示在打印的两行内容之间。
6.6.3 Ctrl-Z

可以看出此时进程收到内核发送的SIGSTP,前台作业会停止,也就是hello被挂起。
6.6.4 Ctrl-C

可以看出此时进程收到内核发送的SIGINT,前台作业会终止,也就是hello被彻底结束。
6.6.5 Ctrl-z后可以运行ps jobs pstree fg kill 等命令
6.6.5.1 ps

可以看出hello的pid号,说明hello只是暂时的被挂起来了。
6.6.5.2 jobs

6.6.5.3 pstree
此命令将所有行程以树状图显示。树状图将会以 pid (如果有指定) 或是以 init 这个基本行程为根 (root),如果有指定使用者 id,则树状图会只显示该使用者所拥有的行程。其中,命令的使用权限是所有使用者。

命令结果截图:

6.6.5.4fg
该命令使第一个后台作业变成前台的

我们可以看到fg命令后,打印出来按Ctrl-Z之前剩下的七条信息。
6.7本章小结
本章说明了hello的进程管理。首先,介绍了进程的概念与作用,接着讲述了shell、nash 的作用与处理流程,以hello为例子,说明了fork进程创建过程,execve过程,以及进程的执行。最后通过在hello执行时输入各种命令来说明hello的异常与信号处理。

第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:
CPU所生成的地址。逻辑地址是内部和编程使用的、并不唯一。例如,你在进行C语言指针编程中,可以读取指针变量本身值(&操作),实际上这个值就是逻辑地址,它是相对于你当前进程数据段的地址(偏移地址),不和绝对物理地址相干。
线性地址:
逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。是一个32位无符号整数,可以用来表示高达4GB的地址。线性地址通常用十六进制数字表示。
虚拟地址;
线性地址的别称。
物理地址:
计算器系统的主存被组织成一个由M个连续的字节大小的单元组成的数组。每字节都有一个唯一的物理地址。物理地址是加载到内存地址寄存器中的地址,内存单元的真正地址。在前端总线上传输的内存地址都是物理内存地址,编号从0开始一直到可用物理内存的最高端。这些数字被映射到实际的内存条上。物理地址是明确的、最终用在总线上的编号,不必转换,不必分页。
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
7.2 Intel逻辑地址到线性地址的变换-段式管理
基本思想:
程序按内容或过程(函数)关系分成段,每段有自己的名字。一个用户作业或进程所包含的段对应于一个二维线性虚拟空间,也就是一个二维虚拟存储器。
段式管理程序以段为单位分配内存,然后通过地址映射机构把段式虚拟地址转换成实际的内存物理地址。
和页式管理一样,段式管理只允许经常访问的段驻留内存,将来一段时间内不被访问的段放入外存,待需要时自动调入。
步骤:
首先,段式管理程序为一个进入内存准备执行的进程或作业分配部分内存,作为该进程的工作区用于放置即将执行的程序段。接着,随着进程执行,进程根据需要随时申请调入新段和释放在内存中的段。其中,如果内存中没有足够的空闲区满足该段的内存要求。段式管理程序根据给定的置换算法淘汰内存中在今后一段时间内不再被CPU访问的段,也就是淘汰那些访问概率最低的段。
7.3 Hello的线性地址到物理地址的变换-页式管理
如图所示,MMU利用页表来完成线性地址到物理地址的变换。CPU从虚拟地址的前n-p位中,提出虚拟页号,页表基址寄存器通过这个虚拟页号定位到相应的页表条目,但这个页表条目的有效位是1时,就从该页表条目中提出对应的物理页号,将它和刚刚的虚拟地址的后p位也就是过去的虚拟地址偏移量,现在的物理页偏移量相结合,这样,我们就得到了物理地址。

图7.3 使用页表的地址翻译
7.4 TLB与四级页表支持下的VA到PA的变换
下面两张图分别展示了二级页表和k-级页表的地址翻译。

图7.4-2 使用k-级页表的地址翻译

图7.4-2 使用k-级页表的地址翻译
结合两张图我们可以知道,TLB与四级页表支持下的VA到PA的变换步骤如下。虚拟地址被划分成为4个VPN和1个VPO。每个VPNi都是一个到第i级页表的索引,其中1≤i≤3。第j级页表中的每个PTE,1≤j≤3,都是指向第j+1的某个页表的基址。为了构造物理地址,在能够确定PPN之前,MMU必须访问4个PTE,同时,和只有一级的页表结构一样,PPO和VPO是相同的。

图7.4-3 Corei7的地址翻译
Corei7采用的便是四级页表层次结构。36位VPN被划分成四个9位的片,每个片被用作一个页表的偏移量。CR3寄存器包含L1页表的物理地址。VPN1提供到一个L1 PTE的偏移量,这个PTE包含L2页表的基地址。VPN2提供到一个L2 PTE的偏移量,以此类推。
7.5 三级Cache支持下的物理内存访问
存储在每个级别缓存中的所有数据都是下一个级别缓存的一部分。这三种缓存的技术难度和制造成本递减,容量递增。当CPU读取数据时,先在一级缓存中找到,如果找不到则在二级缓存中找到,如果仍然找不到,就到在三级缓存或内存中。
7.6 hello进程fork时的内存映射
首先,内核会为fork出来的进程创建对应的数据结构,并为它分配pid;接着,创建hello进程的mm_struct、区域结构和页表的原样副本来给其创建虚拟内存。将fork后的两个进程的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。

图7.6 私有的写时复制
7.7 hello进程execve时的内存映射
首先,删除已存在的用户区域;其次创建新的区域结构,将私有区域映射到这一块地方。接着映射共享区域以及一部分动态链接库;最后设置PC,让它指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
如下图所示,缺页故障与缺页中断会这么处理:
第1步:处理器生成一个虚拟地址,并把它传送给MMU。
第2步:MMU生成PTE地址,并从高速缓存/主存请求得到它.
第3步:高速缓存/主存向MMU返回PTE。
第4步:PTE中的有效位是0,所以MMU触发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序。。
第5步:缺页处理程序确认出物理内存中的牺牲页,如果这个页已经被修改了,则把它换出到磁盘。
第6步:缺页处理程序页面调入新的页面,并更新内存中的PTE。
第7步:缺页处理程序返回到原来的进程,再次执行导致缺页的命令。CPU将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面现在缓存在物理内存中,所以就会命中。
第8步:主存将所请求字返回给处理器。

图7.8 页面缺页的操作图
7.9动态存储分配管理
创建和删除虚拟内存的区域需要用到动态内存分配器。
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长。对于每个进程,内核维护者一个变量brk,它指向堆的顶部。

图7.9-1 堆
分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可以用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配的状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器有两种基本风格,它们都要求应用显示地分配块。
显式分配器:要求应用显式地释放任何已分配的块。例如,C标准库提供一种叫做malloc程序包的显式分配器。C程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块。C++中的new和delete操作符与C中的malloc和free相当。
隐式分配器:另一方面,要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器,而自动释放未使用的巳分配的块的过程叫做垃圾收集。例如,诸如Lisp、ML以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。
而动态内存分配主要有以下两种策略:
带边界标签的隐式空闲链表:

图7.9-2 一个简单堆块的形式

图7.9-3 用隐式空闲链表来组织堆。
显示空闲链表:

图7.9-使用双向空闲链表的堆块的形式
7.10本章小结
本章首先介绍了四种地址的概念,其次介绍了这四种地址如何用不同的方法转化;接着,描述了四级页表的实现和三级cache的实现;介绍了缺页的解决办法;最后介绍了两种动态存储分配管理。

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应的文件的读和写来执行。
设备管理:向上面所说的将设备优雅地映射为文件的方式,允许Linux内核引用一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。
8.2 简述Unix IO接口及其函数
8.2.1 Unix I/O接口
Linux内核引用的一个简单、低级的应用接口,让所有的输入和输出都得以一种统一且一致的方式来执行。
打开文件:一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
Linux shell创建的每个进程开始时都有三个打开的文件: 标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。头文件<unistd.h>定义了常量STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO,它们可以用来代替显式的描述符值。
改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。
读写文件: 一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的而文件,当k>=m时执行读操作会触发一个成为end-of-file(EOF)的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF符号”。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
关闭文件。:当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
8.2.2 Unix I/O接口的函数
8.2.2.1 open函数

打开一个已存在的文件或者创建一个新的文件
8.2.2.2 close函数

关闭一个打开的文件
8.2.2.3 read函数

执行输入,从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。
8.2.2.4 write函数

从内存位置buf复制至多n个字节到描述符fd的当前文件位置。
8.2.2.5 lseek函数
显示地修改当前文件的位置。
8.3 printf的实现分析

图8.3-1 printf函数的内容
从上面的图,我们可以知道printf函数实现的原理是用vsprintf和write函数进行打印。

图8.3-2 vsprintf函数的内容
从上面的图,我们可以知道vsprintf函数先在在buf中存入格式化后的参数,接着它返回得到的数组的大小。
write函数则会将参数buf中的第i个元素写道终端。
陷阱-系统调用int 0x80或syscall。
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析

图8.4 getchar函数的内容
该函数调用了read函数,会将用户接下来输入的字符存放到键盘缓冲区直到遇到回车才返回输入的字符串。
其中,用户输入字符时,键盘接口会得到该按键对应的键盘扫描码,产生中断请求。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章说明了linux的IO设备管理方法,讲述了Unix IO接口并介绍了它的五个函数。接着,对照代码分析了printf和getchar的实现。

结论
预处理使源代码在不同的执行环境中被方便的修改或者编译。
编译通过语法、句法分析将上一步的结果翻译成了等价的中间代码或汇编代码,生成.i格式的文件。
汇编将上一步的汇编代码结果翻译成机器语言,将上一步得到的.i格式的文件转换成可重定位的目标文件.o文件。
链接将不同文件中的代码和数据合并起来,进行符号解析和重定位,从而生成一个可执行文件。
在运行这个可执行文件的准备阶段,壳shell中,fork函数为hello创建进程,加载器用execve函数将原来的进程替换成这个可执行文件中的内容。
接着,各种存储机制会生成逻辑地址,最后再把它经由虚拟地址转换成物理地址。
在这个可执行文件运行时,若有异常信号,那么就对其进行对应的处理。
这之中,程序与文件的交互是由Unix I/O接口实现的。
在这个可执行文件运行结束后,它会被shell中的父进程回收,其中的信息也会对应的被内核回收。
通过将hello.c文件一步步生成可执行文件,说明每一步的用处;再在执行阶段执行各种操作并对应的解释其中的各种机制,我对于计算机系统的理解更加透彻了,感受到了计算机系统设计者设计的巧妙,并在一些针对更好的利用计算机系统方面有了一些自己的理解。

附件
hello.c 源代码
hello.i hello.c预处理生成的文本文件
hello.s hello.i经过编译器翻译成的文本文件
hello.o hello.s经汇编器翻译成机器语言指令打包成的可重定位目标文件
hello 经过hello.o链接生成的可执行目标文件hello.s经汇编器翻译成机器语言指令打包成的可重定位目标文件
hello.elf hello.o的elf格式文件
hello1elf.txt hello的elf格式文件

参考文献
[1]Randal E.Bryant,David R.O’Hallaron.深入理解计算机系统[M].机械工业出版社.
[2]梦小冷.存储管理-段式管理[EB/OL].https://blog.youkuaiyun.com/,2019.
[3]浓咖啡.内存管理第一谈:段式管理和页式管理[EB/OL].https://blog.youkuaiyun.com/,2015.
[4]pianistx.printf实现的深度刨析 [EB/OL]. https://www.cnblogs.com/pianist/2013
[5] shell教程[EB/OL]. https://www.runoob.com/,2019.
[6]xiaolh.详解bash命令行处理[EB/OL].https://blog.youkuaiyun.com/,2007.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值