计算机系统
大作业
题 目 程序人生-Hello’s P2P 专 业 计算学部 学 号 120L020102 班 级 2003003 学 生 秦易 指 导 教 师 史先俊
计算机科学与技术学院 2022年5月 摘 要 每个程序员的启蒙必须是Hello World,即使是最简单的程序操作,我们也需要学习计算机系统的知识来理解它的生活。本文阐述了hello程序从生成到结束的运行过程,根据所学知识和参考资料,对程序进行处理Linux借助分析系统中的生命周期,分析系统中的生命周期edb、gcc、gdb等工具探讨hello.c可执行文件在预处理、编译、汇编和链接后生成的过程hello对程序有更好的理解。同时探索hello动态链接、过程运行、内存管理I/O对计算机系统的管理过程有更详细的了解。
关键词: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 - 结论 - 14 - 附件 - 15 - 参考文献 - 16 -
第1章 概述 1.1 Hello简介 P2P:程序员通过高级语言编写代码获得代码hello.c文件,hello.c通过cpp预处理hello.i文件,编译器ccl将hello.i编译成汇编语言文件hello.s,然后通过汇编器cs将hello.s将文件翻译成机器语言,将指令打包成可重定位hello.o目标文件,最后,通过链接器ld可执行文件链接到库函数hello。在Linux终端中输入./hello执行文件,shell调用fork函数为hello创建一个过程。 020:在此过程中,shell调用execve函数来加载hello,虚拟内存映射,然后载入物理内存,CPU给时间片,控制逻辑流。hello操作结束后,操作系统将清除程序运行中占用的内存,关闭程序打开的文件描述符,释放程序运行中占用的物理资源,清除程序内核中的数据结构,shell回收此过程并取回控制权。 1.2 环境与工具 硬件环境:Intel? Core? i7-10510U CPU;2.30 GHz;16GB RAM 软件环境:Windows10 64位;Ubuntu 16.04 LTS 64位;VMware 15pro 开发与调试工具: gcc;edb;gdb;objdump;readelf;codeblocks 1.3 中间结果 文件名 文件描述 hello.c hello源程序 hello.i 预处理生成的文件 hello.s 编译后的汇编文件 hello.o 汇编后的可重定位文件 hello 链接后的可执行文件
1.4 本章小结 本章对hello简要介绍了程序,并进行了探索hello程序中使用的硬件软件工具等信息简要阐述了中间文件。
第2章 预处理 2.1 预处理的概念和作用 概念:在C语言中,没有任何内部机制来完成以下功能:包括其他源文件、定义宏,并根据条件决定是否包含某些代码。为了完成这些工作,需要使用预处理程序。预处理器cpp扫描源代码,修改#开头的命令,初步转换,为编译器生成新的源代码。 功能:以#开头的命令进行预处理,如#include、#define等等,这些指令将在编译器编译之前转换源代码。 一般有三种类型: (1)文件包括:#include预处理指令的作用是在指令处展开包含的文件。 (2)宏:宏定义了代表特定内容的标识符,如#define。在预处理过程中,将源代码中的宏标识符替换为宏定义值。 (3)条件编译:条件编译指令将决定哪些代码被编译,哪些代码不被编译,如#if。编译条件可以根据表达值或特定宏是否定义来确定。 2.2在Ubuntu下一个预处理命令 预处理命令:gcc -E hello.c -o hello.i 或 cpp hello.c > hello.i
图2.2.1 预处理前
图2.2.2 预处理后 2.3 Hello预处理结果分析 预处理后生成hello.i文件,打开hello.i查看发现预处理后的代码变成了3060行,而且hello程序的main函数在3047行。前面的代码是预处理器#include展开,然后遇到#define继续展开 ,因此.i文件中不会出现#include和#define。对于有#ifdef等条件编译的句子,cpp判断条件值是否包含逻辑。
图2.3.1 hello.i文件尾部 2.4 本章小结 本章介绍了预处理的概念和功能,并通过指令生成hello.i并查看文件,完成预处理工作。对预处理后的文件内容进行分析,有助于以后编译文件。
第3章 编译 3.1 概念与作用的编译 概念:高级计算机语言易于编写和阅读。低级机器语言可以直接由计算机解释和操作。编译器将hello.i翻译hello.s,它以高级程序设计语言书写的源程序为输入,以汇编语言或机器语言的目标程序为输出,称为编译。 功能:编译可以将先进的编程语言转换为相对简单的汇编语言,并与机器指令转换。此外,编译程序还具有语法检查、调试措施、修改手段、覆盖处理、目标程序优化和不同语言共享等重要功能。 3.2 在Ubuntu下面编译的命令 编译指令:gcc -S hello.i -o hello.s
图3.2.1 编译前
图3.2.2 编译后 3.3 Hello分析编译结果
图3.3.1 编译结果 3.3.1数据 (1)字符串 C程序中两个printf使用字符串,即
图3.3.2 .c中字符串1
图3.3.3 .c中字符串2 编译器将字符串存.rodata节。
图3.3.4 .s中字符串 字符串1中的汉字被编码成UTF-8格式,一个汉字占三个字节。 字符串2中%s终端运行用户hello输入的两个参数。
图3.3.5 .s作为字符串printf的参数 作为两个字符串printf函数参数。 (2)整数 C程序中的两个整形变量是argc和i
图3.3.6 .c中整型 main函数声明的 局部变量I ,编译时,编译器会把它放在堆栈里(如图所示 i放在-4(%)rbp)的位置,int类型占4个字节 ) 并给出初始值。
图3.3.7 .s中的i argc是main 函数的第一个参数 ,通常由%rdi(?i)保存。
图3.3.8 .s中的argc 除变量外,在.s文件中 常量 通常由$开头的立即数(十进制)表示。 (3)数组 main 函数的第二个参数 char* argv[]数组的起始地址存储在栈中-32(%)rbp)的位置,通过每次地址值加8来访问下一次元素。
图3.3.9 .s中的数组 3.3.2赋值 本程序的赋值操作仅有对局部变量i的初始赋值操作,见图3.3.7。movl指令将i赋值为立即数$0。 3.3.3算术 本程序中对循环变量i有+1操作。图3.3.10 .s中的i=i+1 除此之外还有对地址的加法操作,见图3.3.9。 更多算术指令见下图:
图3.3.11 汇编语言中算数和逻辑操作指令 3.3.4关系 C语言中关系操作有:==、!=、>、<、>=、<=。 汇编语言中一般通过cmp比较和jmp跳转来实现。 cmp 的功能相当于减法指令,只是对操作数之间运算比较,不保存结果。cmp指令执行后,将对标志寄存器产生影响,jmp指令会根据标志位进行跳转。 程序中if条件判断:
图3.3.12 .c中的if判断
图3.3.13 if条件判断的实现 图中可以看出i先会与$4相比较,比较的结果会影响后续的跳转。 循环中的条件判断类似:
图3.3.13 for中条件判断的实现 3.3.5控制转移 C语言中if,for,while等的语句,如果满足某个条件,则跳转至某个位置。在汇编语言中主要有 jmp 命令来实现。一般与cmp搭配使用,有时也可以强制跳转。见图3.3.12和3.3.13。 根据标志寄存器的数值,jmp指令类型如下图:
图3.3.14 jmp指令 3.3.6函数调用 在函数调用前,设置用于参数传递的寄存器的值,之后通过call指令进行函数的调用,本程序调用了puts、exit、printf、sleep、atoi、getchar。
图3.3.15 调用puts和exit
图3.3.16 调用printf
图3.3.17 调用atoi
图3.3.18 调用sleep
图3.3.19 调用getchar puts函数是由编译器将第一个printf函数优化而来的,因为这一个printf仅仅是输出字符串,可见编译器具有一定的优化功能。 exit函数通常是用在子程序中用来终结程序用的,使用后程序自动结束,跳回操作系统。exit(0)表示程序正常退出,其余表示异常退出。这里程序把立即数1赋给%edi,说明exit要以状态1退出。 3.3.7文件信息
图3.3.20 .s文件的相关信息 .file:声明源文件 .text:代码节 .section .rodata:只读代码段 .align:数据或者指令的地址对其方式 .string:声明一个字符串 .global:声明全局变量 .type:声明一个符号是数据类型还是函数类型
3.4 本章小结 本章介绍了编译的概念和作用,分析了编译器是如何将一个.i文件编译成一个.s文件的,解析了hello的编译结果,并发现了编译器具有一定的优化功能。对汇编语言的指令有了更加深刻的了解和体会。
第4章 汇编 4.1 汇编的概念与作用 概念:汇编是指的将汇编语言程序翻译成机器指令,将其打包成可重定位目标程序,并生成.o目标文件。 作用:汇编生成的.o文件是一个不可读的二进制文件,实现了文本文件到二进制文件的转化,汇编指令被翻译成计算机能够读懂的二进制机器指令。 4.2 在Ubuntu下汇编的命令 指令:gcc -c hello.s -o hello.o或as hello.s -o hello.o
图4.2.1 汇编前
图4.2.2 汇编后 4.3 可重定位目标elf格式 4.3.1 ELF头(命令:readelf -h hello.o)
图4.3.1 hello.o的elf头 ELF头以一个16字节的Magic序列开始,Magic描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助ELF头的大小(64)、目标文件的类型(REL)、机器类型(X86-64)、节头部表的文件偏移(13),以及节头部表中条目的大小(64)和数量(14)。不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的条目(entry)。
4.3.2 节头部表(命令:readelf -S hello.o)
图4.3.2 hello.o的节头部表 节头部表记录了各个节的信息。如.text节的大小是92,偏移量也就是起始位置是40,是可装入可执行的。由于是可重定位目标文件,所以每个节的地址都是0。 4.3.3 符号表(命令:readelf -s hello.o)
图4.3.3 hello.o的符号表 Value表示偏移量,Size表示大小,Type表示其类型,Bind标志着符号是本地的还是全局的,Ndx中UND表示未定义的符号,ABS表示不该被重定义,COM表示未初始化的变量,数字代表该符号在第几节中。 如图,main是第一节(.text)中偏移量为0的符号,是全局函数,占146B;puts是一个未定义的符号,不知道类型和大小,需要到其他模块中去寻找。
4.3.4 重定位节(命令:readelf -r hello.o)
图4.3.4 hello.o的重定位节 .rela.text保存着符号重定位的各种信息。图中8条重定位信息分别是:第一个printf中的字符串,puts函数,exit函数,第二个printf中的字符串,printf函数,atoi函数,sleep函数,getchar函数。 R_X86_64_PC32:重定位一个使用 32 位PC相对地址的引用 R_X86_64_32:重定位一个使用 32 位绝对地址的引用 图中类型和书上的类型有所区别,是因为gcc升级了。R_X86_64_PLT32是对外部函数调用的重定位。 4.4 Hello.o的结果解析
图4.4.1 hello.o的反汇编 反汇编得到的代码和hello.s中的代码大致相同,有些许不一样的地方: 汇编器在对.s 文件进行汇编时会对每一个全局符号的引用产生一个重定位条目。 (1)分支转移:汇编代码中以.L0 .L1等助记符表示,反汇编代码中则是具体的地址。 (2)函数调用:汇编代码中直接使用函数名称,反汇编代码中用<main+偏移量>来表示。且在.rela.text节中为其添加重定位条目等待链接,可以看到call指令(e8)后面的地址值全为0。 (3)访问全局变量:这里0x0(%rip)表示程序printf需要的字符串的位置,因为需要重定位,所以这里也将操作数设为0。 汇编代码: 反汇编代码: (4)进制:汇编代码中使用十进制,反汇编代码中使用十六进制 4.5 本章小结 本章介绍了汇编的概念和作用,生成了hello.o,这是一个不可读的二进制文件。通过readelf查看ELF头等信息,并作了较为详细的分析。接着通过objdump反汇编过后比较汇编代码和反汇编代码的区别,对汇编过程中对符号的解析和为重定位做的工作有了比较深刻的印象,为接下来探究链接做好准备。
第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.1 hello的elf头 ELF头包含了程序的各类信息。hello是一个可执行文件。
图5.3.2 hello的节头 通过节头表可以得知各个段的信息:类型、载入到虚拟内存后的地址(Address)、节头表所对应字节大小(Size)以及这个节的地址偏移量(Off)等信息。 5.4 hello的虚拟地址空间 使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
图5.4.1 虚拟地址空间的elf头 elf头从0x00400000开始。
图5.4.2 虚拟地址空间的.interp .interp存的是 linux 动态共享库的路径。从0x004002e0开始,从5.3得知该节的大小为1c,0x004002e0加上0x1c为0x004002fc,在这个位置结束,对齐过后,下一段0x00400300开始。
图5.4.3 虚拟地址空间的.dynstr 地址0x00400470保存的是与动态链接相关的导入导出符号,对应于.dynstr 节,该节保存动态符号表。
图5.4.4 虚拟地址空间的.rodata .rodata节起始地址对应虚拟地址为 0x00402000,在这里我们可以看到printf输出的两个字符串。 5.5 链接的重定位过程分析 (以下格式自行编排,编辑时删除) objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。 结合hello.o的重定位项目,分析hello中对其怎么重定位的。
图5.5.1 objdump下hello的main 与hello.o的代码比较,代码大体是一样的,但是地址变成了可以直接访问的虚拟地址。链接器将hello.o中的偏移量加上程序在虚拟内存中的起始地址值0x00400000和.text节的偏移量就得到了这些虚拟地址。 (1)函数个数:在使用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之后的值为目标地址与下一条指令的地址之差,指向相应的字符串。 hello 和 hello.o 除了在反汇编生成的汇编代码有所不同,hello 的反汇编文件还在开头比 hello.o 多了.init、.fini、.plt 和.plt.got 节,其中.init 节是程序初始 化需要执行的代码,.fini 是程序正常终止时需要执行的代码,.plt 和.plt.got 节 分别是动态链接中的过程链接表和全局偏移量表。 5.6 hello的执行流程 程序名 程序地址 ld-2.27.so!_dl_start 0x7fc560f02ea0 ld-2.27.so!_dl_init 0x7fc560f11630 hello!_start 0x400500 libc-2.27.so!__libc_start_main 0x7fce8c867ab0 libc-2.27.so!__cxa_atexit 0x7fce8c889430 libc-2.27.so!__libc_csu_init 0x4005c0 hello!_init 0x400488 libc-2.27.so!_setjmp 0x7fce8c884c10 libc-2.27.so!_sigsetjmp 0x7fce8c884b70 libc-2.27.so!__sigjmp_save 0x7fce8c884bd0 hello!main 0x400532 hello!puts@plt 0x4004b0 hello!exit@plt 0x4004e0 *hello!printf@plt - *hello!sleep@plt - *hello!getchar@plt - ld-2.27.so!_dl_runtime_resolve_xsave 0x7fce8cc4e680 ld-2.27.so!_dl_fixup 0x7fce8cc46df0 ld-2.27.so!_dl_lookup_symbol_x 0x7fce8cc420b0 libc-2.27.so!exit 0x7fce 8c889128
5.7 Hello的动态链接分析 对于动态共享链接库中PIC函数,编译器没有办法预测函数的运行时地址,所以需要为其添加重定位记录,并等待动态链接器处理。为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表PLT和全局偏移量表GOT实现函数的动态链接。其中GOT 中存放函数目标地址,PLT使用 GOT中地址跳转到目标函数。 GOT表位置在调用_init之前0x404008后的16个字节均为0。
图5.7.1 edb中调用_init前的GOT 调用_init函数后,从地址0x4008处,由原来的00 00 00 00 00 00 变为90 91 df ce 9e 7f;由原来的00 00 00 00 00 00变为b0 2b de ce 9e 7f。由于小端的缘故,则这两处的地址应该是0x 7f 9e ce df 91 90和0x 7f 9e ce de 2b b0。
图5.7.1 edb中调用_init后的GOT 在之后的函数调用时,首先跳转到PLT执行.plt中逻辑,第一次访问跳转时,GOT 地址为下一条指令,将函数序号压栈,然后跳转到PLT[0],在 PLT[0]中将重 定位表地址压栈,然后访问动态链接器,在动态链接器中使用函数序号和重定位 表确定函数运行时地址,重写 GOT,再将控制传递给目标函数。之后如果对同样函数调用,第一次访问跳转直接跳转到目标函数 5.8 本章小结 本章介绍了链接的概念及作用,在Ubuntu下链接的命令行,并对hello的elf格式进行了详细的分析对比,并通过反汇编hello文件,将其与hello.o反汇编文件对比,详细了解了重定位过程,遍历了整个hello的执行过程,在最后对hello进行了动态链接分析,使得对hello的链接过程有了一个深刻的理解和体会。
第6章 hello进程管理 6.1 进程的概念与作用 概念:进程的概念主要有两点: 第一, 进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域、数据区域和堆栈。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。 第二, 进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时(操作系统执行之),它才能成为一个活动的实体,我们称其为进程。进程是操作系统中最基本、重要的概念。是多道程序系统出现后,为了刻画系统内部出现的动态情况,描述系统内部各道程序的活动规律引进的一个概念,所有多道程序设计操作系统都建立在进程的基础上。 作用:进程提供给应用程序两个关键抽象: (1)逻辑控制流 ·每个程序似乎独占地使用 CPU ·通过 OS 内核的上下文切换机制提供 (2)私有地址空间 ·每个程序似乎独占地使用内存系统 ·OS 内核的虚拟内存机制提供 6.2 简述壳Shell-bash的作用与处理流程 作用:shell是用户和Linux内核之间的接口程序,用户输入的命令通过shell解释,传给Linux内核,然后将内核的处理结果翻译给用户。 处理流程:首先shell读取用户输入的命令并进行解析,得到参数列表,然后检查这条命令是否是内核命令,如果是则直接执行,否则fork子进程,启动加载器在当前进程中加载并运行程序。 6.3 Hello的fork进程创建过程 首先运行hello程序,在终端输入./hello,接下来shell会判断是否是内置命令,它不是内置命令,所以shell会找到当前目录下的 hello并执行。 父进程通过调用fork函数创建一个新的运行的子进程:pid_t fork(void)。 fork子进程时,系统创建一个与父进程几乎但不完全相同的子进程,子进程得到与父进程用户级虚拟地址空间相同但独立的一份副本,包括代码、数据段、堆、共享库以及用户栈,子进程获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中的内容,但它们有着不同的PID,在父进程中,fork返回子进程的PID,在子进程中,fork返回0。 然后hello将在fork创建的子进程中执行。 6.4 Hello的execve过程 创建进程后,在子进程中通过判断pid即fork()函数的返回值,判断处于子进程,则会通过execve函数在当前进程的上下文中加载并运行一个新程序。execve加载并运行可执行目标文件,且带参数列表argv和环境变量列表envp。只有当出现错误时,execve才会返回到调用程序。 在execve加载了可执行程序之后,它调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数,即可执行程序的main函数。此时用户栈已经包含了命令行参数与环境变量,进入main函数后便开始逐步运行程序。 6.5 Hello的进程执行 上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由 通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内 核数据结构等对象的值构成。 进程时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。多任务也叫做多时间片。 调度:在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被强占的进程。这种决策就叫调度(是由内核中的调度器的代码处理的)。当内核调度一个新的进程的运行的时,内核就会抢占当前进程,通过使用一种上下文切换的较为高层的形式异常控制流将控制转移到新的进程。具体如下:内核首先保存当前进程的上下文,之后恢复之前被抢占的进程保存的上下文,将控制传递给这个恢复的进程。 用户态与核心态转换:进程hello初始运行在用户模式中,直到它通过执行系统调用函数sleep或者exit时便陷入到内内核。内核中的处理程序完成对系统函数的调用。之后,执行上下文切换,将控制返回给进程hello系统调用之后的那条语句。 6.6 hello的异常与信号处理 (以下格式自行编排,编辑时删除) hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。 程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。 hello程序出现的异常可能有: 中断:在hello程序执行的过程中可能会出现外部I/O设备引起的异常。 陷阱:陷阱是有意的异常,是执行一条指令的结果,hello执行sleep函数的时候会出现这个异常。 故障:在执行hello程序的时候,可能会发生缺页故障。 终止:终止时不可恢复的错误,在hello执行过程可能会出现DRAM或者SRAM位损坏的奇偶错误。 可能产生的信号有:SIGINT、SIGKILL、SIGSEGV、SIALARM、SIGCHLD
乱按:随意乱按键盘对hello程序的进行没有任何影响。按下回车shell会认为将此行看作是新的命令。
图6.6.1 随意乱按 Ctrl-C:在键盘上输入Ctrl-c会导致内核发送一个SIGINT信号到前台进程组的每个进程,默认情况是终止前台作业。
图6.6.2 Ctrl-c Ctrl-Z: 输入Ctrl-z会导致内核发送SIGSTP,默认挂起前台hello作业,但 hello进程并没有回收,而是运行在后台下,通过ps指令可以对其进行查看。
图6.6.2 Ctrl-z Ctrl-Z后fg:程序继续进行。
图6.6.3 Ctrl-z后fg Ctrl-Z后jobs:
图6.6.4 Ctrl-z后jobs Ctrl-z后pstree:
图6.6.5 Ctrl-z后pstree Ctrl-Z后kill:内核会发送SIGKILL信号给我们指定的pid(hello程序),并杀死hello程序。
图6.6.6 Ctrl-z后kill 6.7本章小结 本章主要讲述了hello程序再从可执行文件到能真正再系统中执行的一个过程,以及是怎么再程序之中处理异常的方法,其中涉及中断和陷阱的两种的异常情况。并实践了再程序中测试不同的shell程序对于hello程序的影响。对hello执行过程中产生信号和信号的处理过程有了更多的认识,加深了对异常的理解。
第7章 hello的存储管理 7.1 hello的存储器地址空间 逻辑地址是指由程序hello产生的与段相关的偏移地址部分。 线性地址是逻辑地址到物理地址变换之间的中间层。程序hello的代码会产生逻辑地址,或者说是段中的偏移地址,它加上相应节的基地址就生成了一个线性地址。 虚拟地址也就是线性地址。因为与虚拟内存空间的概念类似,逻辑地址也是与实际物理内存容量无关的,是hello中的虚拟地址。 物理地址是指出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。如果启用了分页机制,那么hello的线性地址会使用页目录和页表中的项变换成hello的物理地址;如果没有启用分页机制,那么hello的线性地址就直接成为物理地址了。 7.2 Intel逻辑地址到线性地址的变换-段式管理 (以下格式自行编排,编辑时删除) 段寄存器用于存放段选择符: CS(代码段):程序代码所在段 SS(栈段):栈区所在段 DS(数据段):全局静态数据区所在段 ES、GS 和 FS 可指向任意数据段
图7.2.1 段选择符中字段的含义 TI=0,选择全局描述符表(GDT),TI=1,选择局部描述符表(LDT) RPL=00,为第 0 级,位于最高级的内核态,RPL=11,为第 3 级,位于最低级的用户态,第 0 级高于第 3 级。 实模式下:逻辑地址 CS:EA=物理地址 CS * 16 + EA。 保护模式下:以段描述符作为下标,到 GDT/LDT 表查表获得段地址,段地址+偏移地址=线性地址。 7.3 Hello的线性地址到物理地址的变换-页式管理 线性地址被分以固定长度为单位的组,称为页。例如一个32位的机器,线性地址最大可以为4G,用4KB来划分的话整个地址就被划分为2^20个页,这个数组称为页目录,目录中的每个目录项,就是对应页的地址;另一类“页”,我们称之为物理页,或者是页框、页桢的。是分页单元把所有的物理内存也划分为固定长度的管理单位,它的长度一般与内存页是一一对应的。 1.分页单元中,页目录是唯一的,它的地址放在CPU的CR3寄存器中,是进行地址转换的开始点。 2.每一个活动的进程,因为都有其独立的对应的虚似内存(页目录也是唯一的),那么它也对应了一个独立的页目录地址。——运行一个进程,需要将它的页目录地址放到CR3寄存器中,将别个的保存下来。 3.每一个32位的线性地址被划分为三部份,面目录索引(10位),页表索引(10位),偏移(12位)。 依据以下步骤进行转换: 1.从CR3中取出进程的页目录地址; 2.根据线性地址前十位,在数组中,找到对应的索引项,因为引入了二级管理模式,页目录中的项,不再是页的地址,而是一个页表的地址。(又引入了一个数组),页的地址被放到页表中。 3.根据线性地址的中间十位,在页表(也是数组)中找到页的起始地址。 4.将页的起始地址与线性地址中最后12位相加,得到最终我们想要的地址。 hello的线性地址到物理地址的变换需要查询页表得出,hello的线性地址被分成两个部分,第一部分虚拟页号VPN用于在页表查询物理页号PPN,而第二部分虚拟页偏移量VPO则与查询到的物理页号 PPN 一起组成物理地址。 7.4 TLB与四级页表支持下的VA到PA的变换 在Corei7中48位虚拟地址分为36位的虚拟页号以及12位的页内偏移。四级页表中包含了一个地址字段,它里面保存了40位的物理页号(PPN),这就要求物理页的大小要向4kb对齐。 四级页表每个表中均含有512个条目,故计算四级页表对应区域如下: 第四级页表:每个条目对应4kb区域,共512个条目。 第三级页表:每个条目对应4kb512=2MB区域,共512个条目。 第二级页表:每个条目对应2MB512 = 1GB区域,共512个条目。 第一级页表:每个页表对应1GB*512 = 512GB区域,共512个条目。 从VA到PA的变换: 从VA中分出36位的VPN并根据其中的TLBI索引到对应的TLB组,结合TLBT找到对应的行并判断TLB是否命中。若是命中,则取出其中的PPN;否则转到页表索引。将VPN分为四段,每段9位,里面保存的是对应页表的偏移量。从第一级页表开始索引,找到对应的PTE条目,从中取出相应的第二级页表的首地址。这个首地址加上VPN2的偏移即得到第二级PTE,取出其中的内容即为第三级页表的首地址……以此类推从第四级页表中取出的即为PP将前面得到的PPN与VPO相加就可以得到虚拟地址翻译对应的物理地址。 7.5 三级Cache支持下的物理内存访问 MMU发送物理地址PA给L1缓存,L1缓存从物理地址中抽取出缓存偏移CO、缓存组索引CI以及缓存标记CT。 高速缓存根据CI找到缓存中的一组,并通过CT判断是否已经缓存地址对应的数据,若缓存命中,则根据偏移量直接从缓存中读取数据并返回;若缓存不命中,则继续从L2、L3缓存中查询,若仍未命中,则从主存中读取数据。 7.6 hello进程fork时的内存映射 当fork函数被shell调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID。为了给hello进程创建虚拟内存,它创建了hello进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。 当fork 在hello进程中返回时,hello进程现在的虚拟内存刚好和调用fork 时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间。 7.7 hello进程execve时的内存映射 execve函数在当前进程中加载并运行新程序的步骤: 1、删除已存在的用户区域 2、创建新的区域结构: 私有的、写时复制 代码和初始化数据映射到.text和.data区(目标文件提供) .bss和栈堆映射到匿名文件,栈堆的初始长度0 3、共享对象由动态链接映射到本进程共享区域 4、设置PC,指向代码区域的入口点: Linux根据需要换入代码和数据页面 7.8 缺页故障与缺页中断处理 缺页故障:一种常见的故障,当指令引用一个虚拟地址,在MMU中查找页表时发现与该地址相对应的物理地址不在内存中,因此必须从磁盘中取出的时候就会发生故障。即缓存不命中。 缺页中断处理:缺页处理程序是系统内核中的代码,选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令再次发送VA到MMU,这次MMU就能正常翻译VA了。 7.9动态存储分配管理 动态储存分配管理使用动态内存分配器来进行。动态内存分配器维护着一个进程的虚拟内存区域,称为堆。 分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可以用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配的状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。 1,隐式空闲链表: 空闲块通过头部中的大小字段隐含地连接着。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。 (1)放置策略:首次适配、下一次适配、最佳适配。首次适配从头开始搜索空闲链表,选择第一个合适的空闲块。下一次适配从上一次查询结束的地方开始。最佳适配检查每个空闲块,选择适合所需请求大小的最小空闲块。 (2)合并策略:立即合并、推迟合并。立即合并就是在每次一个块被释放时,就合并所有的相邻块;推迟合并就是等到某个稍晚的时候再合并空闲块。 2,显式空闲链表: 每个空闲块中,都包含一个pred(前驱)和succ(后继)指针。使用双向链表使首次适配的时间减少到空闲块数量的线性时间。 空闲链表中块的排序策略:一种是用后进先出的顺序维护链表,将新释放的块放置在链表的开始处,另一种方法是按照地址顺序来维护链表,链表中每个块的地址都小于它后继的地址。 3,分离的空闲链表 维护多个空闲链表,每个链表中的块有大致相等的大小。将所有可能的块大小分成一些等价类,也叫做大小类。 7.10本章小结 本章讲述了怎么解决hello可执行文件从磁盘到进程的上下文中,之后怎么解决该进程的“独占内存”(虚拟内存)手段,以及在执行的过程之中,处理器是怎么将这一虚拟内存转化为物理地址,遇到页表中有未加载到内存的数据时怎么处理(缺页中断处理程序),最后阐述了一下动态分配内存是怎么在底层实现的。
第8章 hello的IO管理 8.1 Linux的IO设备管理方法 设备的模型化:所有IO设备都被模型化为文件,所有的输入和输出都能被当做相应文件的读和写来执行。 设备管理:Linux内核有一个简单、低级的接口,成为Unix I/O,是的所有的输入和输出都能以一种统一且一致的方式来执行。 8.2 简述Unix IO接口及其函数 Unix I/O接口统一操作: 打开文件:int open (char *filename, int flags, mode_t mode); 关闭文件:int close (int fd); 读文件:ssize_t read (int fd, void *buf, size_t n); 写文件:ssize_t write (int fd, const void *buf, size_t n); 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; } (char)(&fmt) + 4)表示的是…中的第一个参数的地址。 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 返回的是要打印出来的字符串的长度,write是把buf中的i个元素的值写到终端。 vsprintf接受确定输出格式的格式字符串 fmt,用格式字符串对个数变化的参数进行格式化,产生格式化输出。在Linux下,write函数的第一个参数为fd,也就是描述符,而1代表的就是标准输出。它首先给寄存器传递了几个参数,然后执行 int INT_VECTOR_SYS_CALL,代表通过系统调用 syscall,syscall 将寄存器中的字节通过总线复制到显卡的显存中。 字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。 显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。 由此 write 函数显示一个已格式化的字符串。 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; } 异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。 getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。 8.5本章小结 本章对I/O做了简单的了解。I/O时在主存和外部设备之间复制数据的过程。在Linux中,I/O的实现是通过Unix I/O函数来执行的。Linux把所有的I/O设备模型化为文件,并提供统一的Unix I/O接口,这使得所有的输入输出都能以一种统一且一致的方式来执行。 结论 回顾hello 的一生: 程序员编写C语言代码,得到hello.c源文件。 预处理器对hello.c进行预处理得到hello.i,预处理将#开头的代码进行了扩展。 编译器对hello.i进行编译,使其成为一个汇编代码的可读文件hello.s。 汇编器对hello.s进行汇编,得到可重定位目标文件 hello.o。 链接器对hello.o进行链接,并得到可执行目标文件 hello,此时 hello 已经可以被操作系统加载和执行。 在shell中执行hello,首先shell会fork一个进程,然后在这个新的进程中 execve hello,execve会清空当前进程的数据并加载hello,栈帧指向 hello 的程序入口,把控制权交给 hello。 hello 与许多进程并行执行,执行的过程中可能收到来自键盘或者其它进程的信号,当收到信号时hello 会调用信号处理程序来进行处理,可能出现停止终止忽略等行为。 hello 输出信息时需要调用 printf 和 getchar,而 printf 和 getchar 的实现需要调用 Unix I/O 中的 write 和 read 函数。 hello 中的访存操作,需要经历逻辑地址到线性地址最后到物理地址的变换。 hello 结束进程后,shell作为 hello 的父进程会回收 hello 进程,至此hello结束了一生。 即使是hello这样如此简单的程序,也需要计算机内部许多系统协同合作完成。
附件 文件名 文件描述 hello.c hello源程序 hello.i 预处理生成的文件 hello.s 编译后的汇编文件 hello.o 汇编后的可重定位文件 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] 计算机系统课程PPT. [8] Randal E· Bryant 深入理解计算机系统第三版,机械工业出版社 2017.10 第一版