计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术
学 号 2021113352
班 级 2103101
学 生 乔宇凡
指 导 教 师 刘宏伟
计算机科学与技术学院
2022年5月
本文对hello程序的整个生命周期进行了系统的分析,一开始是hello.c源程序,之后运行C预处理器(cpp)将其进行预处理生成hello.i文件,运行C编译器(ccl)将其进行翻译生成汇编语言文件hello.s,然后运行汇编器(as)将其翻译成一个可重定位目标文件hello.o,最后运行链接器程序ld将hello.o和系统目标文件组合起来,创建了一个可执行目标文件hello。当shell接收到./hello的指令后开始调用fork函数创建进程,execve加载hello进入内存,由CPU控制程序逻辑流的运行,中断,上下文切换和异常的处理,最后结束进程并由父进程进行回收,hello的生命周期结束。
关键词:预处理;编译;汇编;链接;进程。
目 录
第1章 概述................................................................................... - 4 -
1.1 Hello简介............................................................................ - 4 -
1.2 环境与工具........................................................................... - 4 -
1.3 中间结果............................................................................... - 4 -
1.4 本章小结............................................................................... - 4 -
第2章 预处理............................................................................... - 5 -
2.1 预处理的概念与作用........................................................... - 5 -
2.2在Ubuntu下预处理的命令................................................ - 5 -
2.3 Hello的预处理结果解析.................................................... - 5 -
2.4 本章小结............................................................................... - 5 -
第3章 编译................................................................................... - 6 -
3.1 编译的概念与作用............................................................... - 6 -
3.2 在Ubuntu下编译的命令.................................................... - 6 -
3.3 Hello的编译结果解析........................................................ - 6 -
3.4 本章小结............................................................................... - 6 -
第4章 汇编................................................................................... - 7 -
4.1 汇编的概念与作用............................................................... - 7 -
4.2 在Ubuntu下汇编的命令.................................................... - 7 -
4.3 可重定位目标elf格式........................................................ - 7 -
4.4 Hello.o的结果解析............................................................. - 7 -
4.5 本章小结............................................................................... - 7 -
第5章 链接................................................................................... - 8 -
5.1 链接的概念与作用............................................................... - 8 -
5.2 在Ubuntu下链接的命令.................................................... - 8 -
5.3 可执行目标文件hello的格式........................................... - 8 -
5.4 hello的虚拟地址空间......................................................... - 8 -
5.5 链接的重定位过程分析....................................................... - 8 -
5.6 hello的执行流程................................................................. - 8 -
5.7 Hello的动态链接分析........................................................ - 8 -
5.8 本章小结............................................................................... - 9 -
第6章 hello进程管理.......................................................... - 10 -
6.1 进程的概念与作用............................................................. - 10 -
6.2 简述壳Shell-bash的作用与处理流程........................... - 10 -
6.3 Hello的fork进程创建过程............................................ - 10 -
6.4 Hello的execve过程........................................................ - 10 -
6.5 Hello的进程执行.............................................................. - 10 -
6.6 hello的异常与信号处理................................................... - 10 -
6.7本章小结.............................................................................. - 10 -
第7章 hello的存储管理...................................................... - 11 -
7.1 hello的存储器地址空间................................................... - 11 -
7.2 Intel逻辑地址到线性地址的变换-段式管理................... - 11 -
7.3 Hello的线性地址到物理地址的变换-页式管理............. - 11 -
7.4 TLB与四级页表支持下的VA到PA的变换.................... - 11 -
7.5 三级Cache支持下的物理内存访问................................ - 11 -
7.6 hello进程fork时的内存映射......................................... - 11 -
7.7 hello进程execve时的内存映射..................................... - 11 -
7.8 缺页故障与缺页中断处理................................................. - 11 -
7.9动态存储分配管理.............................................................. - 11 -
7.10本章小结............................................................................ - 12 -
第8章 hello的IO管理....................................................... - 13 -
8.1 Linux的IO设备管理方法................................................. - 13 -
8.2 简述Unix IO接口及其函数.............................................. - 13 -
8.3 printf的实现分析.............................................................. - 13 -
8.4 getchar的实现分析.......................................................... - 13 -
8.5本章小结.............................................................................. - 13 -
参考文献....................................................................................... - 16 -
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
1.最初通过编辑器编写hello的程序建立.c文件,得到hello.c的源程序。
2.运行C预处理器(cpp)将其进行预处理生成hello.i文件。
3.运行C编译器(ccl)将其进行翻译生成汇编语言文件hello.s。
4.运行汇编器(as)将其翻译成一个可重定位目标文件hello.o。
5.运行链接器程序ld将hello.o和系统目标文件组合起来,创建了一个可执行目标文件hello,如图所示。
6.运行hello程序
初始时,shell程序执行它的指令,等待我们输入一个命令。当我们在键盘上输入字符串“./hello“后,shell程序将字符逐一读入寄存器,再把它存放到内存中,如图所示。
6.2.当我们在键盘上敲回车键时,shell程序就知道我们已经结束了命令的输入。然后shell执行一系列指令来加载可执行的hello文件,这些指令将hello目标文件中的代码和数据从磁盘复制到主存。数据包括最终会输出的字符串“hello,world\n”。利用直接存储器存取技术,数据可以不通过处理器而直接从磁盘到达内存,如图所示。
6.3.一旦目标文件hello中的代码和数据被加载到主存,处理器就开始执行hello程序的main程序中的机器语言指令。这些指令将“hello,world\n”字符串中的字节存主存复制到寄存器文件,再从寄存器文件中复制到显示设备,最终显示到屏幕上,如图所示。
7. 通过shell输入./hello,shell通过fork函数创建了一个新的进程,之后调用execve映射虚拟内存,通过mmap为hello程序开创了一片空间。
8.CPU从虚拟内存中的.text,.data节取代码和数据,调度器为进程规划时间片,有异常时触发异常处理子程序。
9.程序运行结束时,父进程回收hello进程和它创建的子进程,内核删除相关数据结构。
1.2 环境与工具
硬件环境:X64 CPU;2GHz;4GRAM;256Disk
软件环境:Windows10 64位;Vmware 10;Ubuntu 22.04 LTS 64位
工具:codeblocks;gdb;Objdump;HexEditor
1.3 中间结果
hello.c 源程序
hello.i 预处理后文件
hello.s 编译后的汇编文件
hello.o 汇编后的可重定位目标执行文件
hello 链接后的可执行文件
hello.elf hello.o的ELF格式
hello1.txt hello.o的反汇编
hello2.txt hello的反汇编代码
hello1.elf hello的ELF格式
1.4 本章小结
本章总体介绍了hello程序一生的过程,进行实验时的软硬件环境及开发与调试工具等基本信息,以及hello程序的中间结果。
第2章 预处理
2.1 预处理的概念与作用
1.预处理概念:预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。
2.预处理作用:根据源代码中的预处理指令修改源代码,预处理从系统的头文件包中将头文件的源码插入到目标文件中,宏和常量标识符已全部被相应的代码和值替换,最终生成.i文件。
2.2在Ubuntu下预处理的命令
Linux中hello.c文件进行预处理的命令是:gcc -E -o hello.i hello.c
应截图,展示预处理过程!
2.3 Hello的预处理结果解析
经过预处理之后,hello.c变为hello.i文件,仍为可以阅读的C语言程序文本文件。对原程序中的宏进行了宏展开,头文件中的内容被包含进该文件中。例如声明函数、定义结构体、定义变量、定义宏等内容,如果代码中有#define命令还会对相应的符号进行替换。
2.4 本章小结
本章介绍了预处理的相关概念和作用,进行实际操作生成并查看了hello.i文件,是对源程序进行补充和替换。
第3章 编译
3.1 编译的概念与作用
1.编译的概念:编辑器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。
2.编译的作用:把源程序翻译成目标程序,进行词法分析和语法分析,分析过程中发现有语法错误,给出提示信息。
3.2 在Ubuntu下编译的命令
编译的命令:gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
3.3.1汇编初始部分
节名称 作用
.file 声明源文件
.text 代码节
.section.rodata 只读数据段
.align 声明对指令或者数据的存放地址进行对齐的方式
.string 声明一个字符串
.globl 声明全局变量
.type 声明一个符号是函数类型还是数据类型
3.3.2数据
①字符串
程序中有两个字符串,这两个字符串都在只读数据段中,分别如图所示:
hello.c中唯一的数组是作为main函数的第二个参数,数组的每个元素都是一个指向字符串的指针。数组的起始地址存放在栈中-32(%rbp)的位置,被两次调用找参数传给printf。
这两个字符串作为printf函数的参数,如图所示:
②局部变量i
main函数声明了一个局部变量i,编译器进行编译的时候将局部变量i会放在堆栈中。如图所示,局部变量i放在栈上-4(%rbp)的位置。
③参数argc
参数 argc 作为用户传给main的参数。也是被放到了堆栈中。
④数组:char *argv[]
char *argv[]是作为main函数的第二个参数,数组的每个元素都是一个指向字符类型的指针。数组的起始地址存放在栈中-32(%rbp)的位置,被两次调用传给printf
⑤立即数
立即数直接体现在汇编代码中。
3.3.2全局函数
hello.c声明了一个全局函数int main(int argc,char *argv[]),汇编代码说明main函数是全局函数,如图所示:
3.3.3赋值操作
hello.c中赋值操作是for循环中i=0;在汇编代码中使用mov指令实现,mov指令根据操作数的字节大小分为:movb(字节),movw(字),movl(双字),movq(四字)。
3.3.4算数操作
hello.c中的算术操作是i++,汇编语言addl $1, -4(%rbp)。
3.3.5关系操作
①hello.c中argc!=4;是条件判断语句,进行编译时,这条指令被编译为:cmpl $4,-20(%rbp),在比较之后还设置了条件码,根据条件码判断是否需要跳转。
②hello.c中i<9,作为判断循环条件指令被编译为cmpl $8,-4(%rbp),并设置条件码,为下一步 jle 利用条件码进行跳转做准备。
3.3.6控制转移指令
汇编语言中先设置条件码,然后根据条件码来进行控制转移,在hello.c中,有以下控制转移指令:
①判断argc是否等于4,如果argc等于4,则不执行if语句,否则执行if语句,对应的汇编代码为:
②for循环中,每次判断i是否小于或等于8来决定是否继续循环,对应的汇编代码为:
先对i赋初值然后无条件跳转至判断条件的.L3中,然后判断i是否符合循环的条件,符合直接跳转至循环体内部.L4中。
3.3.7函数操作
调用函数时有以下操作:(假设函数P调用函数Q)
①传递控制:调用过程Q的时候,程序计数器(%rip)必须设置为函数Q代码的起始地址,然后在返回时,要把程序计数器(%rip)设置为P中调用Q后面那条指令的地址。
②传递数据:函数P必须能够向函数Q提供一个或多个参数Q必须能够向P中返回一个值。
③分配和释放内存:在开始时,Q可能需要为局部变量分配空间,而在返回前,又必须释放这些空间。
hello.c中的函数操作:
main函数:参数是int argc,char *argv[]
printf函数:参数是argv[1],argv[2]
exit函数:参数是1
sleep函数:参数是atoi(argv[3])
getchar函数:无参数
3.3.8类型转换
hello.c中atoi(argv[3])将字符串类型转换为整型。
3. 本章小结
本章主要介绍了编译器处理c语言程序的基本过程,函数从源代码变为等价的汇编代码,编译器分别从c语言的数据,赋值语句,类型转换,算术操作,逻辑/位操作,关系操作,控制转移与函数操作这几点进行分析,理解了这些编译器编译的机制。
第4章 汇编
4.1 汇编的概念与作用
1.汇编的概念
汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,将结果保存在目标文件hello.o中,hello.o文件是一个二进制文件,它包含函数main的指令编码,这个过程称为汇编。
2.汇编的作用
将汇编代码转换为机器指令,使其在链接后能被机器识别并执行.
4.2 在Ubuntu下汇编的命令
gcc -c -o hello.o hello.s
4.3 可重定位目标elf格式
在linux下生成hello.o文件elf格式的命令:readelf -a hello.o > hello.elf
分析.elf文件中的内容:
①ELF头:ELF头(ELF header)以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含了帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是有节头部表描述的,其中目标文件中每个节都有一个固定大小的条目(entry)。
②节头:记录各节名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐。
③重定位节:
.rela.text,保存的是.text节中需要被修正的信息;任何调用外部函数或者引用全局变量的指令都需要被修正;调用外部函数的指令需要重定位;引用全局变量的指令需要重定位; 调用局部函数的指令不需要重定位;在可执行目标文件中不存在重定位信息。本程序需要被重定位的是printf、puts、exit、sleepsecs、getchar、sleep和.rodata中的.L0和.L1。
.rela.eh_frame节是.eh_frame节重定位信息。
④符号表:.symtab,一个符号表,它存放在程序中定义和引用的函数和全局变量的信息,一些程序员错误地认为必须通过-g选项来编译一个程序,才能得到符号表信息。实际上每个可重定位目标文件在.symtab中都有一张符号表(除非程序员特意用STRIP命令去掉它)。然而,和编译器中的符号表不同,.symtab符号表不包含局部变量的条目。
4.4 Hello.o的结果解析
命令:objdump -d -r hello.o > hello1.txt
与hello.s的差异:
①分支转移:
hello.s:
hello1.txt:
反汇编的跳转指令用的不是段名称比如.L3,而是用的确定的地址。但在反汇编代码中,分支转移表示为主函数+段内偏移量。反汇编代码跳转指令的操作数使用的不是段名称,因为段名称只是在汇编语言中便于编写的助记符,所以在汇编成机器语言之后显然不存在,而是确定的地址。
②对函数的调用与重定位条目对应
hello.s:
hello1.txt:
在可重定位文件中call后面不再是函数的具体名称,而是一条重定位条目指引的信息。而在汇编文件中可以看到,call后面直接加的是文件名。
③ 立即数变为16进制格式
hello.s:
hello1.txt:
在编译文件中,立即数全部是以16进制表示的,因为16进制与2进制之间的转换比十进制更加方便,所以都转换成了16进制。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
4.5 本章小结
本章对hello.s进行了汇编,生成了hello.o可重定位目标文件,并且分析了可重定位文件的ELF头、节头部表、符号表和可重定位节,比较了hello.s和hello.o反汇编代码的不同之处,分析了汇编语言与机器语言的对应关系。
第5章 链接
5.1 链接的概念与作用
1.链接的概念:
链接是将各种代码和数据片段收集并合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时,也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至执行于运行时,也就是由应用程序来执行。
2.链接的作用:
链接器在软件开发过程中扮演着一个关键的角色,因为它们使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其它文件。
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
生成hello的反汇编代码:
5.3 可执行目标文件hello的格式
命令:readelf -a hello > hello1.elf
- ELF头:hello的文件头和hello.o文件头的不同之处如下图标记所示,hello是一个可执行目标文件,有27个节。
- 节头:对 hello中所有的节信息进行了声明,包括大小和偏移量。
- 重定位节.rela.text:
- 符号表.symtab:
5.4 hello的虚拟地址空间
分析程序头LOAD可加载的程序段的地址为0x400000
使用edb打开hello从Data Dump窗口观察hello加载到虚拟地址的状况,并查看各段信息。
5 链接的重定位过程分析
命令: objdump -d -r hello > hello2.txt
与hello.o的反汇编文件对比发现,hello2.txt中多了许多节。
hello1.txt中只有一个.text节,而且只有一个main函数,函数地址也是默认的0x000000.
hello2.txt中有.init,.plt,.text三个节,而且每个节中有很多函数。库函数的代码都已经链接到了程序中,程序各个节变的更加完整,跳转的地址也具有参考性。
hello比hello.o多出的节头表:
.interp 保存ld.so的路径
.note.ABI.tag Linux下特有的section
.note.gnu.build-i 编译信息表
.gnu.hash gnu的扩展符号hash表
.dynsym 动态符号表
.dynstr 动态符号表中的符号名称
.gnu.version 符号版本
.gnu.version_r 符号引用版本
.rela.dyn 动态重定位表
.rela.plt .plt节的重定位条目
.init 程序初始化
.plt 动态链接表
.fini 程序终止时需要的执行的指令
.eh_frame 程序执行错误时的指令
.dynamic 存放被ld.so使用的动态链接信息
.got 存放程序中变量全局偏移量
.got.plt 存放程序中函数的全局偏移量
.data 初始化过的全局变量或者声明过的函数
hello重定位的过程:
(1)重定位节和符号定义链接器将所有类型相同的节合并在一起后,这个节就作为可执行目标文件的节。然后链接器把运行时的内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号,当这一步完成时,程序中每条指令和全局变量都有唯一运行时的地址。
(2)重定位节中的符号引用这一步中,连接器修改代码节和数据节中对每个符号的引用,使他们指向正确的运行时地址。执行这一步,链接器依赖于可重定位目标模块中称为的重定位条目的数据结构。
(3)重定位条目当编译器遇到对最终位置未知的目标引用时,它就会生成一个重定位条目,代码的重定位条目放在.rel.txt中
(4)重定位过程的地址计算算法如图所示:
5.6 hello的执行流程
①开始执行:_start、_libc_start_main
②执行main:_main、_printf、_exit、_sleep、_getchar
③退出:exit
程序名及地址:
_start 0x4010f0
_libc_start_main 0x2f12271d
main 0x401125
_printf 0x401040
_exit 0x401070
_sleep 0x401080
_getchar 0x401050
5.7 Hello的动态链接分析
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。
GOT表位置在调用dl_init之前0x404008后的16个字节均为0:
调用dl_init之后的.got.plt
从图中可以看到.got.plt的条目已经发生变化。
5.8 本章小结
本章主要介绍了链接的概念与作用,链接可分为符号定义和重定位,了解了可执行文件的ELF格式,分析了hello的虚拟地址空间,重定位过程,执行过程,动态连接过程.
第6章 hello进程管理
6.1 进程的概念与作用
1.进程的概念:
经典定义就是一个执行中程序的实例。广义定义是进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
2.进程的作用:
进程作为一个执行中程序的实例,系统中每个程序都运行在某个进程的上下文中,上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
6.2 简述壳Shell-bash的作用与处理流程
1.Shell-bash的作用:
Linux系统中,Shell是一个交互型应用级程序,为使用者提供操作界面,接收用户命令,然后调用相应的应用程序。
2.处理流程:
①从终端读入输入的命令。
②将输入字符串切分获得所有的参数。
③检查第一个命令行参数是否是一个内置的shell命令,如果是则立即执行。
④如果不是内部命令,调用fork( )创建新进程/子进程执行指定程序。
⑤shell应该接受键盘输入信号,并对这些信号进行相应处理。
6.3 Hello的fork进程创建过程
在终端中输入命令行./hello 2021113352 乔宇凡 1后,首先shell对我们输入的命令进行解析,由于我们输入的命令不是一个内置的shell命令,因此shell会调用fork()创建一个子进程,子进程几乎但不完全与父进程相同。通过fork函数,子进程得到与父进程用户级虚拟地址空间相同的但是独立的一份副本,拥有不同的PID。
6.4 Hello的execve过程
当调用fork()函数创建了一个子进程之后,子进程调用exceve函数在当前子进程的上下文加载并运行一个新的程序即hello程序,需要以下步骤:
①删除已存在的用户区域。删除之前进程在用户部分中已存在的结构。
②创建新的代码、数据、堆和栈段。
③映射共享区域。如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域。
④设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。
6.5 Hello的进程执行
hello程序的执行是依赖于进程所提供的抽象的基础上,进程提供给应用程序的抽象有:
1.一个独立的逻辑控制流,它提供一个假象,好像我们的进程独占的使用处理器
2. 一个私有的地址空间,它提供一个假象,好像我们的程序独占的使用CPU内存。
操作系统所提供的进程抽象:
①逻辑控制流:如果想用调试器单步执行程序,我们会看到一系列的程序计数器(PC)的值,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或是包含在运行时动态链接到程序的共享对象中的指令。这个PC值的序列叫做逻辑控制流,或者简称为逻辑流。
②上下文切换:如果系统调用因为等待某个事件发生而阻塞,那么内核可以让当前进程休眠,切换到另一个进程,上下文就是内核重新启动一个被抢占的进程所需要的状态,是一种比较高层次的异常控制流。
③时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
④用户模式和内核模式:shell使得用户可以有机会修改内核,所以需要设置一些防护措施来保护内核,如限制指令的类型和可以作用的范围。
⑤上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由 通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内 核数据结构等对象的值构成。
hello进程的执行:在进程调用execve函数之后,进程已经为hello程序分配了新的虚拟的地址空间,最初hello运行在用户模式下,输出hello 2021113352 乔宇凡,然后调用sleep函数进程进入内核模式,运行信号处理程序,之后再返回用户模式。运行过程中,cpu不断切换上下文,使运行过程被切分成时间片,与其他进程交替占用cpu,实现进程的调度。
6.6 hello的异常与信号处理
1.异常和信号异常种类
类别 原因 异步/同步 返回行为
中断 来自I/O设备的信号 异步 总是返回到下一条指令
陷阱 有意的异常 同步 总是返回到下一条指令
故障 潜在可恢复的错误 同步 可能返回到当前指令或终止
终止 不可恢复的错误 同步 不会返回
2.运行结果
①正常运行
②按下 ctrl-z
输入ctrl-z默认结果是挂起前台的作业,hello进程并没有回收,而是运行在后台下,用jobs命令可以看到,hello进程并没有被回收。调用 fg 将其调到前台,此时 shell 程序首先打印 hello 的命令行命令, hello 继续运行打印剩下的,之后程序结束,同时进程被回收,如下图。
③按下Ctrl+c
在键盘上输入Ctrl+c会导致内核发送一个SIGINT信号到前台进程组的每个进程,默认情况是终止前台作业,用jobs查看前台进程组发现没有hello进程,如图所示。
④不停乱按
无关输入被缓存到stdin,并随着printf指令被输出到结果。
6.7本章小结
本章介绍了进程的概念和作用、shell-bash的处理过程与作用并且着重分析了调用fork创建新进程,调用execve函数执行hello,hello的进程执行过程,以及hello在运行时遇到的异常与信号处理。
第7章 hello的存储管理
7.1 hello的存储器地址空间
1.逻辑地址:程序经过编译后出现在汇编代码中的地址。逻辑地址用来指定一个操作数或者是一条指令的地址。是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 段标识符:段内偏移量。
2.线性地址:逻辑地址向物理地址转化过程中的一步,逻辑地址经过段机制后转化为线性地址,为描述符:偏移量的组合形式,分页机制中线性地址作为输入。
3.虚拟地址:就是线性地址。
4.物理地址:CPU通过地址总线的寻址,找到真实的物理内存对应地址。CPU对内存的访问是通过连接着CPU和北桥芯片的前端总线来完成的。在前端总线上传输的内存地址都是物理内存地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部份组成,段标识符: 段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节。
索引号就是“段描述符”的索引,段描述符具体地址描述了一个段。很多个段描述符,就组了一个数组,叫“段描述符表”,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符。每一个段描述符由8个字节组成。
Base字段,它描述了一个段的开始位置的线性地址。Intel设计的本意是,一些全局的段描述符,就放在“全局段描述符表”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表”中,用段选择符中的T1字段来判断用全局段描述符表还是局部段描述符表,=0,表示用全局段描述符表,=1,表示用局部段描述符表。
先给定一个完整的逻辑地址 段标识符:段内偏移量
1.看段选择符的T1=0还是1,知道当前要转换是全局段描述符表中的段还是局部段描述符表中的段。
2.取出段选择符的前13位查找到对应的段描述符,确定了Base基地址。
3.将Base+offset,就是线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址即虚拟地址(VA)到物理地址(PA)之间的转换通过分页机制完成,而分页机制是对虚拟地址内存空间进行分页。
系统将虚拟页作为进行数据传输的单元。Linux下每个虚拟页大小为4KB。物理内存也被分割为物理页, MMU(内存管理单元)负责地址翻译,MMU使用页表将虚拟页到物理页的映射,即虚拟地址到物理地址的映射。
n位的虚拟地址包含两个部分:一个p位的虚拟页面偏移(VPO),一个n-p位的虚拟页号(VPN),MMU利用VPN选择适当的PTE,根据PTE,我们知道虚拟页的信息,如果虚拟页是已缓存的,那直接将页表条目的物理页号和虚拟地址的VPO串联起来就得到一个相应的物理地址。VPO和PPO是相同的。如果虚拟页是未缓存的,会触发一个缺页故障。调用一个缺页处理子程序将磁盘的虚拟页重新加载到内存中,然后再执行这个导致缺页的指令。
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支持下的物理内存访问
获得物理地址之后,先取出组索引对应位,在L1中寻找对应组。如果存在,则比较标志位,相等后检查有效位是否为1.如果都满足则命中取出值传给CPU,否则按顺序对L2cache、L3cache、内存进行相同操作,直到出现命中。然后再一级一级向上传,如果有空闲块则将目标块放置到空闲块中,否则将缓存中的某个块驱逐,将目标块放到被驱逐块的位置。
7.6 hello进程fork时的内存映射
在shell输入命令行后,内核调用fork创建子进程,为hello程序的运行创建上下文,并分配一个与父进程不同的PID。通过fork创建的子进程拥有父进程相同的区域结构、页表等的一份副本,同时子进程也可以访问任何父进程已经打开的文件。当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同,当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间。
7.7 hello进程execve时的内存映射
execve函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下几个步骤:
①删除已存在的用户区域
删除当前进程虚拟地址的用户部分中的已存在的区域结构。
②映射私有区域
为新程序的代码、数据、bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区,bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零。
③映射共享区域
hello程序与共享对象libc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
④设置程序计数器(PC)
execve做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
如果程序执行过程中发生了缺页故障,则内核调用缺页处理程序。
处理程序执行如下步骤:
1.检查虚拟地址是否合法,如果不合法则触发一个段错误,程序终止。
2.检查进程是否有读、写或执行该区域页面的权限,如果不具有则触发保护异常,
程序终止。
3.两步检查都无误后,内核选择一个牺牲页面,如果该页面被修改过则将其交换
出去,换入新的页面并更新页表。然后将控制转移给hello进程,再次执行触发缺页故障的指令。
7.9动态存储分配管理
动态内存管理的基本方法与策略:
动态内存分配器维护着一个进程的虚拟内存区域,称为堆,系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高地址)。对于每个进程,内核维护着一个变量brk,它指向对的顶部。
分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显示地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显示地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显示执行的。要么是内存分配器自身隐式执行的。分配器有两种基本风格。两种风格都要求应用显示地分配块。他们的不同之处在于由哪个实体来负责释放已分配的块。
1.显示分配器:要求应用显示的释放任何已分配的块。例如C标准库提供一个叫
做malloc程序包的显示分配器。
2.隐式分配器:要求分配器检测一个已分配块何时不再被程序使用,那么就释放
这个块。隐式分配器也叫垃圾收集器。
7.10本章小结
本章主要介绍了hello的存储器地址空间、intel的段式管理、hello的页式管理,以intel Core7在指定环境下介绍了虚拟地址VA到物理地址PA的转换、物理内存访问,分析了hello进程fork时的内存映射,hello进程execve时的内存映射、缺页故障与缺页中断处理和动态存储分配管理。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:所有IO设备都被模型化为文件,所有的输入和输出都能被当做相应文件的读和写来执行。
设备管理:Linux内核有一个简单、低级的接口,成为Unix I/O,是的所有的输入和输出都能以一种统一且一致的方式来执行。
8.2 简述Unix IO接口及其函数
Unix IO接口:
1.打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
2.Linux 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.关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件,作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放他们的内存资源。
Unix I/O函数:
①int open(char* filename,int flags,mode_t mode)
进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
②int close(fd)
进程通过调用close函数关闭一个打开的文件,fd是需要关闭的文件的描述符。
③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的实现分析
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);
}
在printf中调用系统函数write(buf,i)将长度为i的buf输出。write函数如下:
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
在write函数中,将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址,int INT_VECTOR_SYS_CALLA代表通过系统调用syscall
syscall实现:
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,通过系统调用read读取存储在键盘缓冲区中的ASCII码直到读到回车符然后返回整个字串,getchar进行封装,大体逻辑是读取字符串的第一个字符然后返回。
8.5本章小结
本章介绍了Linux的IO设备管理方法,Unix IO接口及其函数,分析了printf函数和getchar函数的实现。
结论
hello所经历的过程:
1.最初通过编辑器编写hello的程序建立.c文件,得到hello.c的源程序。
2.运行C预处理器(cpp)将其进行预处理生成hello.i文件。
3.运行C编译器(ccl)将其进行翻译生成汇编语言文件hello.s。
4.运行汇编器(as)将其翻译成一个可重定位目标文件hello.o。
5.运行链接器程序ld将hello.o和系统目标文件组合起来,创建了一个可执行目标文件hello。
6.运行hello程序
7. 通过shell输入./hello,shell通过fork函数创建了一个新的进程,之后调用execve映射虚拟内存,通过mmap为hello程序开创了一片空间。
8.CPU从虚拟内存中的.text,.data节取代码和数据,调度器为进程规划时间片,有异常时触发异常处理子程序。
9.程序运行结束时,父进程回收hello进程和它创建的子进程,内核删除相关数据结构。
你对计算机系统的设计与实现的深切感悟:
抽象对计算机系统是很重要的概念,底层信息用二进制来抽象表示,进程是对处
理器、主存和I/O设备的抽象,虚拟内存是对主存和磁盘设备的抽象,文件是对I/O
设备的抽象。
CPU的存储结构十分精妙,从寄存器到三种高速缓存,再到主存,再到本地磁盘,再到远程二级存储器,大大提高了访问内存的效率。
附件
列出所有的中间产物的文件名,并予以说明起作用。
hello.c 源程序
hello.i 预处理后文件
hello.s 编译后的汇编文件
hello.o 汇编后的可重定位目标执行文件
hello 链接后的可执行文件
hello.elf hello.o的ELF格式
hello1.txt hello.o的反汇编
hello2.txt hello的反汇编代码
hello1.elf hello的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.