计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 软件工程
学 号 xxxxxxxxxx
班 级 xxxxxxxx
学 生 xxxxx
指 导 教 师 吴锐
计算机科学与技术学院
2024年5月
本论文旨在通过运用在计算机系统课程中所学知识,在复习与回顾的基础上,逐步分析hello.c程序在预处理,编译,汇编,链接,加载到执行到回收等过程。通过使用Linux系统下的工具,来分析hello程序的一生,并借此了解c语言程序的执行流程和执行时各硬件的工作情况,对计算机系统编译源文件、运行进程等机制进行较深入的分析和介绍。
关键词:计算机系统;程序的整个过程;进程加载;C语言底层实现;CSAPP;
目 录
第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简介
P2P(Program to Process)过程:
1. 源代码:首先程序员将hello.c程序逐字地敲进电脑,保存成一个文本文件。
2. 预处理:源代码经过cpp预处理,生成hello.i的文件。
3. 编译:预处理后的代码经过编译器成汇编代码,由hello.i生成一个hello.s文件。
4. 汇编:汇编代码被汇编器转换成机器代码,生成目标文件(hello.o)。
5. 链接:目标文件与库文件链接,生成最终的可执行文件hello。
6. 执行包括以下步骤
加载:操作系统将可执行文件加载到内存中。
创建进程:OS为程序创建一个新的进程。
执行:新进程调用execve系统调用,开始执行程序。
内存映射:操作系统通过mmap分配内存空间。
时间片分配:操作系统调度器分配CPU时间片给进程,使其得以在硬件上运行(包括取指、译码、执行等操作)。
7. 内存管理:
OS与内存管理单元(MMU)负责将虚拟地址(VA)转换为物理地址(PA)。
使用TLB、4级页表、3级缓存和页面文件来加速内存访问。
8. I/O管理与信号处理:
I/O管理和信号处理确保程序能够与键盘、主板、显卡、屏幕等硬件设备交互。
O2O(From Zero-0 to Zero-0)过程:
1. 从无到有:
一个空文件开始,通过编辑器逐行输入代码,最终形成一个完整的hello.c程序。
通过编译、链接等步骤,生成可执行文件。
在shell中输入相关命令后,shell将调用fork函数为这一程序创建进程,之后将通过exceve在进程的上下文中加载并运行hello,将进程映射到虚拟内存空间,并加载需要的物理内存。执行时,在CPU的分配下,指令进入CPU流水线执行。
2. 程序执行到结束:
程序在操作系统的管理下运行,完成预定的功能。
在程序执行完毕后,操作系统负责清理资源,进程终止,内存释放,回到最初的状态。
通过上述P2P和O2O过程,Hello程序在计算机系统中完成了从源代码到进程执行,再到进程结束的完整生命周期。
1.2 环境与工具
硬件环境 AMD Ryzen 7 7840HS;3.8GHz;16G RAM;1.5TB SSD
软件环境 Windows10 64 位;VirtualBox7.0;Ubuntu 22.04.3 amd64
开发工具 VS2022,VsCode; gcc;vim;GDB;EDB;g++;objdump等
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
中间结果文件名称 | 文件作用 |
hello.i | 预处理后的文件 |
hello.s | 汇编程序 |
hello.o | 可重定位目标文件 |
hello | 可执行目标程序 |
elf.txt | hello.o的ELF格式 |
back_hello.txt | hello.o的反汇编代码 |
hello.elf | hello的elf格式文件 |
hello_dumpback.s | hello的反汇编代码 |
1.4 本章小结
本章介绍了hello程序的P2P过程和O2O过程,并介绍了介绍了本计算机的硬件环境、软件环境、开发工具与工具还有中间结果文件的作用与名称。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理是编译过程的第一步,它是指在编译之前对源代码进行初步处理。预处理器会处理代码中的一些特殊指令和宏定义,以生成一个中间文件,这个中间文件然后被传递给编译器进行进一步的编译。
预处理的主要作用为:
1. 宏定义:使用#define指令定义宏,宏在预处理阶段被替换成对应的值或代码片段。
2. 文件包含:使用#include指令包含头文件,将头文件的内容插入到当前文件中。
3. 条件编译:使用#if、#ifdef、#ifndef、#else、#elif和#endif等指令,根据条件编译不同的代码部分。
4. 宏函数:定义带参数的宏,类似于函数,但在预处理阶段展开,提高代码的可读性和复用性。
5. 去除注释:预处理器会去除源代码中的注释,简化代码,提高编译速度。
6. 行拼接:使用反斜杠(\)将多行代码拼接成一行,提高代码的可读性。
7. 编译器专用指令:预处理器还处理一些编译器特定的指令,这些指令可以改变编译器的行为。
2.2在Ubuntu下预处理的命令
cpp hello.c hello.i
2.3 Hello的预处理结果解析
源代码文件hello.c只有24行,预处理后的文件为3092行,原本的源代码部分在3078行之后。其中1-3073行是hello引用的所有的stdio.h,unistd.h,stdlib.h等头文件以及头文件递归包含头文件内容的展开。右图显示了预处理后代码包含的头文件信息。从左图对比源代码可知,我们可以发现注释部分已经在预处理过程中删除,缩进也被修改。
2.4 本章小结
本章介绍了在预处理过程中预处理器的工作(宏定义,文件包含,条件编译,宏函数,去除注释,行拼接等),并在linux环境下演示了hello.c文件的预处理过程与预处理结果。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译是将源代码或预处理文件转换为机器代码(机器可执行的二进制代码)的过程。这个过程由编译器(Compiler)完成,编译器会经过多个阶段处理源代码,最终生成可执行文件或者目标文件。
编译的主要作用如下:
1.语法分析:编译器检查源代码的语法是否正确。如果有语法错误,编译器会报告错误并停止编译。
2.语义分析:编译器检查源代码的语义是否正确。例如,确保类型匹配、变量作用域正确等。
3.优化:编译器对源代码进行优化,以提高生成代码的执行效率或减少代码大小。这包括常量折叠、循环优化、函数内联等。
4.目标代码生成:编译器将源代码转换为目标代码(通常是汇编代码或机器代码)。目标代码生成过程中,编译器根据目标平台的指令集生成相应的指令。
5.目标代码优化:对生成的目标代码进行进一步优化,提高代码的执行效率。包括删除未使用的代码、优化寄存器使用等。
6.汇编:将优化后的目标代码转换为机器代码,生成目标文件(.o或.obj)。汇编过程通常由汇编器(Assembler)完成。
7.错误检查与报告:在整个编译过程中,编译器会进行各种检查,报告语法错误、语义错误和其他编译错误,帮助程序员调试代码。
通过上述编译过程,源代码/预处理文件被转换为机器代码,可由计算机直接执行。编译器的这些阶段确保了代码的正确性、优化了性能,并生成了目标平台上的可执行文件。
3.2 在Ubuntu下编译的命令
编译命令:gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
3.3.1数据类型
1.数字常量:现在源代码中使用的数字常量都是储存在.text段,如图包括比较时使用的5和for循环使用的数字10(在也就是小于等于9)均存储在.text节,如下图所示:
2.字符串常量:在printf等函数中使用的字符串常量存储在.rodata节,如图所示的string实际上就是字符串的Unicode编码表示
3.局部变量:局部变量是储存在栈中的某一个位置的或直接储存在寄存器中,在本代码中有三个局部变量i,argc,argv。认真观察汇编代码可以找到他们的位置如下图所示,其中argc标志的是在程序运行的时候输入的变量的个数,argv是一个保存着输入变量的数组,三者均存储在栈上。
3.3.2赋值操作
赋值操作在本代码出现四次,分别用于初始化变量i,设置函数参数作为状态码,设置函数返回值,还有让变量i每次+1,具体的操作汇编代码如下:
3.3.3算术操作
对于局部变量 i,由于其是循环变量,因此在每一轮的循环中都要修改这个值, 对于这个局部变量的算术操作的汇编代码如下:
3.3.4关系操作
本代码中出现了两次比较操作,第一次是相等操作,用于确认argv是否等于5,如果相等则跳转;第二次是循环变量i的大小判断,如果不小于10则跳转
3.3.5数组/指针/结构操作
这一段代码中出现的数组操作只有一个,也就是对于argv数组的操作,观察汇编代码可以发现argv储存的两个值都存放在栈中,argv[1]的储存地址是-24(%rbp),而argv[1]的储存地址是-16(%rbp),对于数组操作的汇编代码如下
3.3.6函数调用
在这一段代码中出现了几个函数调用的情况,在X86系统中函数参数储存的规则为:第1~6个参数依次储存在%rdi、%rsi、%rdx、%rcx、%r8、%r9这六个寄存器中,其余的参数保存在栈中的某些位置。
首先是main函数,传入参数argc和argv,其中argv储存在栈中,argc储存在%rdi中返回,在源代码中最后的返回语句是return0,因此在汇编代码中最后是将%eax设置为0并返回这一寄存器,如图所示:
printf 函数第一次调用的时候只传入了字符串参数首地址,for循环中调用的时候传入了 argv[1],argc[2],argc[3]的地址。第一次是满足 if 条件的时候调用,第二次是在 for 循环条件满足的时候调用。
sleep函数以atoi(argv[4])为参数,这一参数储存在%edi中,这一函数在for循环的条件下被调用。而atoi函数以argv[4]为参数,这一参数存储在%rdi中。exit函数传入的参数为1,执行退出命令。当if条件满足的时候调用这一函数
3.4 本章小结
本章主要分析了在将修改了的源程序文件转换为汇编程序的时候主要发生的 变化以及汇编代码文件中主要存在的部分以及源代码中的一些主要的操作对应的 汇编代码中的汇编代码的展现形式。编译器做的就是在进行词义分析和语义分析之后判断源代码符合语法要求之后将其转换为汇编代码
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编语言是一种低级编程语言,它直接与计算机的硬件结构相关联。汇编语言位于机器语言与高级编程语言之间,通过汇编指令对硬件进行操作。汇编是汇编器as将hello.s翻译成机器语言指令,把这些指令打包成一种叫可重定位目标程序的格式,并将结果保存在目标文件hello.o中。其作用为将汇编代码根据特定的转换规则转换为机器代码,机器代码也是计算机真正能够理解的代码格式。
4.2 在Ubuntu下汇编的命令
汇编命令:as hello.s -o hello.o
4.3 可重定位目标elf格式
导出elf文件:readelf -a hello.o > ./elf.txt
ELF头以一个16字节的序列开始,这个序列描述了生成该文件系统下的字的大小以及一些其他信息。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。节头表描述了.o 文件中每一个节出现的位置,大小,目标文件中的每一个节都有一个固定大小的条目。ELF头和节头表如下图所示
重定位节中包含了在代码中使用的一些外部变量等信息,在链接的时候需要根据重定位节的信息对这些变量符号进行修改。链接的时候链接器会根据重定位节的信息对外部变量符号决定选择何种方法计算正确的地址,通过偏移量等信息计算出正确的地址。本程序需要重定位的信息有:.rodata中的模式串,puts,exit,printf,slepsecs,sleep,getchar这些符号同样需要与相应的地址进行重定位。
symtab是一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。例如本程序中的sleep、puts、exit等函数名都需要在这一部分体现。
4.4 Hello.o的结果解析
输入命令:objdump -d -r hello.o > back_hello.txt
反汇编结果如下:
与第3章的hello.s进行对照分析可发现以下几点不同。
1.进制不同:hello.s反汇编之后对于数字的表示是十进制的,而hello.o反汇编之后数字的表示是十六进制的。
2.分支转移:对于条件跳转,hello.s反汇编中给出的是段的名字,例如.L2等来表示跳转的地址,而hello.o由于已经是可重定位文件,对于每一行都已经分配了相应的地址,因此跳转命令后跟着的是需要跳转部分的目标地址。
3.函数调用:hello.s中,call指令后跟的是需要调用的函数的名称,而hello.o反汇编代码中call指令使用的是main函数的相对偏移地址。同时可以发现在hello.o反汇编代码中调用函数的操作数都为0,即函数的相对地址为0,因为再链接生成可执行文件后才会生成其确定的地址,所以这里的相对地址都用0代替。
4.5 本章小结
本章对汇编过程进行了一个完整的叙述。经过汇编器之后,生成了一个可重定位的文件,为下一步链接做好了准备。通过与hello.s的反汇编代码的比较,更加深入的理解了在汇编过程中发生的变化,这些变化都是为了链接做准备的。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
链接是将编译生成的目标文件和库文件合并成一个可执行文件的过程。链接器是执行这一过程的工具。链接的主要作用是解决程序各部分之间的引用关系,将独立编写和编译的代码模块整合在一起,以便生成最终的可执行程序。
链接有如下作用:
1.符号解析:链接器负责解析符号(如变量名和函数名),并将这些符号的引用替换为实际的内存地址。每个编译单元中的符号可能在其他编译单元中定义,链接器会解析这些外部符号引用。
2.地址和重定位:链接器负责为程序中的各个模块分配内存地址。由于各个模块在编译时不确定它们在最终程序中的实际地址,因此链接器在链接时需要调整代码中的地址引用,使其指向正确的内存位置。
3.合并和优化:链接器将多个目标文件和库文件合并成一个可执行文件。它还可以进行一些优化,如删除未使用的代码和数据、合并重复的段等。
4.生成可执行文件:链接器的最终目的是生成一个完整的可执行文件,包含所有代码和数据段,以及必要的符号表和重定位信息,使得程序能够加载并运行。
简单来说链接把预编译好了的若干目标文件合并成为一个可执行目标文件。使得分离编译称为可能,不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为可独立修改和编译的模块。当改变这些模块中的一个时,只需简单重新编译它并重新链接即可,不必重新编译其他文件。
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 > hello.elf
与汇编部分类似ELF头以一个16字节的序列开始,这个序列描述了生成该文件系统下的字的大小以及一些其他信息。
节头描述了各个节的大小、偏移量和其他属性。链接器链接时,会将各个文件的相同段合并成一个大段,并且根据这个大段的大小以及偏移量重新设置各个符号的地址。
5.4 hello的虚拟地址空间
使用edb打开hello可执行文件,可以在edb的DataDump窗口看到hello的虚拟地址空间分配的情况,具体内容截图如下:
可以发现这一段程序的地址是从 0x401000 开始的,并且该处有 ELF 的标识, 可以判断从可执行文件时加载的信息。接下来可以分析其中的一些具体的内容:其 中PHDR保存的是程序头表;INTERP保存了程序执行前需要调用的解释器;LOAD 记录程序目标代码和常量信息;DYNAMIC 储存了动态链接器所使用的信息; NOTE 记录的是一些辅助信息;GNU_EH_FRAME 保存异常信息;GNU_STACK 使 用系统栈所需要的权限信息;GNU_RELRO 保存在重定位之后只读信息的位置。
5.5 链接的重定位过程分析
命令:objdump -d -r hello > hello_dumpback.s
反汇编代码部分如下:
hello与hello.o的不同之处在于:
1.在链接过程中,hello中加入了代码中调用的一些库函数,例如sleep,puts,printf,等,同时每一个函数都有了相应的虚拟地址。例如exit函数的虚拟地址如下图:
2.对于全局变量的引用,由于hello.o中还未对全局变量进行定位,因此hello.o中用0加上%rip的值来表示全局变量的位置,而在hello中,由于已经进行了定位,因此全局变量的的值使用一个确切的值加上%rip表示全局变量的位置。
3.hello中增加了.init和.plt节,和一些节中定义的函数。
4.hello中无hello.o中的重定位条目,并且跳转和函数调用的地址在hello中都变成了虚拟内存地址。这是由于hello.o中对于函数还未进行定位,只是在.rel.text中添加了重定位条目,而hello进行定位之后自然不需要重定位条目。
5.地址访问:在链接完成之后,hello中的所有对于地址的访问或是引用都调用的是虚拟地址地址。例如下图中条件跳转代码所示:
链接的过程:链接主要分为两个过程即符号解析和重定位。
符号解析:目标文件定义和引用符号,符号解析将每个符号引用和一个符号定义关联起来。
重定位:编译器和汇编器生成从0开始的代码和数据节。链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使得它们指向这个内存位置。链接器使用汇编器产生的重定位条目的详细指令,不加甄别地执行这样的重定位。
5.6 hello的执行流程
当使用edb运行hello程序时,操作系统加载器首先将可执行文件加载到内存。程序的入口点设为_start,此地址为0x4010f0。_start函数调用__libc_start_main,该函数初始化运行时环境,然后调用main函数。在main函数中:检查命令行参数数量是否为5,如果不是,则输出用法信息并调用exit(1)终止程序。如果参数正确,进入一个循环,打印"Hello"消息并调用sleep函数等待指定秒数。循环结束后,调用getchar()等待用户输入,然后返回0,表示程序正常结束。最终__libc_start_main处理main函数的返回值并调用exit函数终止程序,清理所有资源。
根据反汇编代码可以看出执行函数及虚拟内存地址:
5.7 Hello的动态链接分析
当程序调用一个由共享库定义的函数时,由于编译器无法预测这时候函数的地址是什么,因此这时,编译系统提供了延迟绑定的方法,将过程地址的绑定推迟到第一次调用该过程时。通过GOT和过程链接表PLT的协作来解析函数的地址。在加载时,动态链接器会重定位GOT中的每个条目,使它包含正确的绝对地址,而PLT中的每个函数负责调用不同函数。那么,通过观察edb,便可发现dl_init后.got.plt节发生的变化。
5.8 本章小结
在链接过程中,各种代码和数据片段收集并组合为一个单一文件。利用链接器,分离编译称为可能,我们不用将应用程序组织为巨大的源文件,只是把它们分解为更小的管理模块,并在应用时将它们链接就可以完成一个完整的任务。经过链接,已经得到了一个可执行文件,接下来只需要在shell中调用命令就可以为这一文件创建进程并执行该文件。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程是计算机程序在操作系统中执行的实例。它包含程序的代码、数据、资源和执行时的状态信息,如寄存器内容、内存地址和打开的文件等。
进程的作用如下:
1.资源管理:进程是资源分配的基本单位。操作系统通过进程管理CPU时间、内存空间、文件句柄和其他资源。每个进程拥有独立的资源空间,保证互不干扰。
2.并发执行:进程支持并发执行,使得多个程序能在同一时间运行,提高系统效率。通过进程调度,操作系统可以在多个进程间切换,实现多任务处理。
3.隔离性和安全性:进程间相互独立,操作系统通过进程隔离机制保证一个进程的错误不会影响其他进程,提高系统的稳定性和安全性。
4.结构化编程:进程提供了一种结构化编程方式,可以将复杂任务分解为多个独立的进程,使得程序设计更模块化和易于管理。
5.通信与协作:进程间可以通过进程间通信(IPC)机制进行数据交换和协作,IPC方式包括管道、消息队列、共享内存和信号等,支持复杂应用的开发。
6.2 简述壳Shell-bash的作用与处理流程
Shell是操作系统的命令行解释器,是用户与操作系统之间的桥梁。Bash是Linux和Unix系统中最常用的壳之一。其主要作用包括:命令解释,脚本编写与执行,进程管理,环境管理,输入输出重定向等。
Shell-bash处理用户输入的流程如下:
①在shell命令行中输入命令:$./hello
②shell命令行解释器构造argv和envp;
③调用fork()函数创建子进程,其地址空间与shell父进程完全相同,包括只读代码段、读写数据段、堆及用户栈等
④调用execve()函数在当前进程(新创建的子进程)的上下文中加载并运行hello程序。将hello中的.text节、.data节、.bss节等内容加载到当前进程的虚拟地址空间
⑤调用hello程序的main()函数,hello程序开始在一个进程的上下文中运行。
6.3 Hello的fork进程创建过程
父进程通过调用fork函数创建一个新的运行的子进程。调用fork函数后,新创建的子进程几乎但不完全与父进程相同:子进程得到与父进程虚拟地址空间相同的(但是独立的)一份副本,包括代码、数据段、堆、共享库以及用户栈,子进程获得与父进程任何打开文件描述符相同的副本,这意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。fork被调用一次,却返回两次,子进程返回0,父进程返回子进程的PID。父进程和新创建的子进程之间最大的区别在于它们有不同的PID。
6.4 Hello的execve过程
exceve函数在当前进程的上下文中加载并运行一个新程序。exceve函数加载并运行可执行目标文件,并带参数列表和环境变量列表。只有当出现错误时,exceve才会返回到调用程序。所以,与fork一次调用返回两次不同,在exceve调用一次并从不返回。当加载可执行目标文件后,exceve调用启动代码,启动代码设置栈,将可执行目标文件中的代码和数据从磁盘复制到内存中,然后通过跳转到程序的第一条指令或入口点来运行该程序,由此将控制传递给新程序的主函数。
6.5 Hello的进程执行
1.逻辑控制流和时间片:
进程的运行本质上是CPU不断从程序计数器 PC 指示的地址处取出指令并执行,值的序列叫做逻辑控制流。操作系统会对进程的运行进行调度,进程A->上下文切换->进程B->上下文切换->进程A->… 如此循环往复。在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行,我们说内核调度了这个进程。在内核调度了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。在一个程序被调运行开始到被另一个进程打断,中间的时间就是运行的时间片。
2.用户模式和内核模式:
用户模式的进程不允许执行特殊指令,不允许直接引用地址空间中内核区的代码和数据。内核模式进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
3.上下文:
上下文就是内核重新启动一个被抢占的进程所需要恢复的原来的状态,由寄存器、程序计数器、用户栈、内核栈和内核数据结构等对象的值构成。
4.调度的过程:
在对进程进行调度的过程,操作系统主要做了两件事:加载保存的寄存器,切换虚拟地址空间。
5.用户态与核心态转换:
为了能让处理器安全运行,需要限制应用程序可执行指令所能访问的地址范围。因此划分了用户态与核心态。核心态可以说是拥有最高的访问权限,处理器以一个寄存器当做模式位来描述当前进程的特权。进程只有故障、中断或陷入系统调用时才会得到内核访问权限,其他情况下始终处于用户权限之中,保证了系统的安全性。
6.6 hello的异常与信号处理
正常运行的效果如下:
运行中异常以及处理方法如下:
类别 | 原因 | 异步/同步 | 返回行为 |
中断 | 来自I/O设备的信号 | 异步 | 总是返回到下一条指令 |
陷阱 | 有意的异常 | 同步 | 总是返回到下一条指令 |
故障 | 潜在可恢复的错误 | 同步 | 可能返回到当前指令 |
终止 | 不可恢复的错误 | 同步 | 不会返回 |
不停乱按:将屏幕的输入缓存到缓冲区。乱码被认为是命令,不影响当前进程的执行。
按下Ctrl-Z:程序运行时按Ctrl-Z,这时,产生中断异常,它的父进程会接收到信号SIGSTP并运行信号处理程序,然后便发现程序在这时被挂起了,并打印了相关挂起信息。
Ctrl-Z后运行ps,打印出了各进程的pid,可以看到之前挂起的进程hello。
Ctrl-Z后运行jobs,打印出了被挂起进程组的jid,可以看到之前被挂起的hello,以被挂起的标识Stopped。
Ctrl-Z后运行pstree,可看到它打印出的信息
Ctrl-Z后运行fg:因为之前运行jobs是得知hello的jid为1,那么fg 1可以把之前挂起在后台的hello重新调到前台来执行,打印出剩余部分,然后输入任意字符回车,程序运行结束,进程被回收。
Ctrl-Z后运行Kill:重新执行进程,可以发现hello的进程号为28695,那么便可通过kill 28695发送信号SIGKILL给进程28695,它会导致该进程被杀死。然后再运行ps,可发现已被杀死的进程hello。
按下Ctrl-C:进程收到SIGINT信号,结束hello。在ps中查询不到其PID,在job中也没有显示,可以看出hello已经被彻底结束。
6.7本章小结
本章主要介绍了hello可执行文件的执行过程,包括进程创建、加载和终止,以及通过键盘输入等过程。从创建进程到进程并回收进程,这一整个过程中需要各种各样的异常和中断等信息。程序的高效运行离不开异常、信号、进程等概念,正是这些机制支持hello能够顺利地在计算机上运行。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:逻辑地址指由程序产生的与段相关的偏移地址部分,也叫相对地址。要经过寻址方式的计算或变换才得到内存储器中的实际有效地址,即物理地址。从hello的反汇编代码中看到的地址,它们需要通过计算,通过加上对应段的基地址才能得到真正的地址,这些便是hello中的逻辑地址。
线性地址:是逻辑地址到物理地址变换之间的中间层。程序hello的代码会产生逻辑地址,hello的反汇编文件中看到的地址(即逻辑地址)中的偏移量,加上对应段的基地址,便得到了hello中内容对应的线性地址。
虚拟地址:有时我们也把逻辑地址称为虚拟地址。因为与虚拟内存空间的概念类似,逻辑地址也是与实际物理内存容量无关的,是hello中的虚拟地址。
物理地址:是指出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。在hello的运行中,在访问内存时需要通过CPU产生虚拟地址,然后通过地址翻译得到一个物理地址,并通过物理地址访问内存中的位置。
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址由段选择符和偏移量组成,线性地址为段首地址与逻辑地址中的偏移量组成。其中,段首地址存放在段描述符中。而段描述符存放在描述符表中,也就是GDT(全局描述符表)或LDT(局部描述符表)中。
段式管理特点:
1.段式管理以段为单位分配内存,每段分配一个连续的内存区。
2.由于各段长度不等,所以这些存储区的大小不一。
3.同一进程包含的各段之间不要求连续。
4.段式管理的内存分配与释放在作业或进程的执行过程中动态进行。
7.3 Hello的线性地址到物理地址的变换-页式管理
页式管理是一种内存空间存储管理的技术,页式管理分为静态页式管理和动态页式管理。将各进程的虚拟空间划分成若干个长度相等的页(page),页式管理把内存空间按页的大小划分成片或者页面(pageframe),然后把页式虚拟地址与内存地址建立一一对应页表,并用相应的硬件地址变换机构,来解决离散地址变换问题。页式管理采用请求调页或预调页技术实现了内外存存储器的统一管理。
线性地址(虚拟地址)由虚拟页号VPN和虚拟页偏移VPO组成。首先,MMU从线性地址中抽取出VPN,并且检查TLB,看他是否因为前面某个内存引用缓存了PTE的一个副本。TLB从VPN中抽取出TLB索引和TLB标记,查找对应组中是否有匹配的条目。若命中,将缓存的PPN返回给MMU。若不命中,MMU需从页表中的PTE中取出PPN,若得到的PTE无效或标记不匹配,就产生缺页,内核需调入所需页面,重新运行加载指令,若有效,则取出PPN。最后将线性地址中的VPO与PPN连接起来就得到了对应的物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
每次CPU产生一个虚拟地址,MMU(内存管理单元)就必须查阅一个PTE(页表条目),以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会从内存多取一次数据,代价是几十到几百个周期。如果PTE碰巧缓存在L1中,那么开销就会下降1或2个周期。然而,许多系统都试图消除即使是这样的开销,它们在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓存器(TLB)。
虚拟地址VA虚拟页号VPN和虚拟页偏移VPO组成。若TLB命中时,所做操作与7.3中相同;若TLB不命中时,VPN被划分为四个片,每个片被用作到一个页表的偏移量,CR3寄存器包含L1页表的物理地址。VPN1提供到一个L1PTE的偏移量,这个PTE包含L2页表的基地址。VPN2提供到一个L2PTE的偏移量,依次类推。最后在L4页表中对应的PTE中取出PPN,与VPO连接,形成物理地址PA。
7.5 三级Cache支持下的物理内存访问
MMU将物理地址发给L1缓存,缓存从物理地址中取出缓存偏移CO、缓存组索引CI以及缓存标记CT。若缓存中CI所指示的组有标记与CT匹配的条目且有效位为1,则检测到一个命中条目,读出在偏移量CO处的数据字节,并把它返回给MMU,随后MMU将它传递给CPU。若不命中,则在下一级cache或是主存中寻找需要的内容,储存到上一级cache后再一次请求读取。
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给他一个唯一的pid。
为了给这个新进程创建虚拟内存,系统创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有写时复制。
当fork从新进程返回,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve函数在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要:
- 删除已存在的用户区域
- 映射私有区域:为新程序hello的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。
- 映射共享区域:如果hello程序与共享对象(或目标)链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
- 设置程序计数器(PC),指向代码的入口点。
7.8 缺页故障与缺页中断处理
页面命中完全是由硬件完成的,而处理缺页是由硬件和操作系统内核协作完成的:
1.处理器生成一个虚拟地址,并将它传送给MMU
2.MMU生成PTE地址,并从高速缓存/主存请求得到它
3.高速缓存/主存向MMU返回PTE
4.PTE中的有效位是0,所以MMU出发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序。
5.缺页处理程序确认出物理内存中的牺牲页,如果这个页已经被修改了,则把它换到磁盘。
6.缺页处理程序页面调入新的页面,并更新内存中的PTE
7.缺页处理程序返回到原来的进程,再次执行导致缺页的命令。CPU将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面已经换存在物理内存中,所以就会命中。
7.9动态存储分配管理
定义:一种内存管理方法。对内存空间的分配、回收等操作在进程执行过程中进行,以便更好地适应系统的动态需求,提高内存利用率。
分配器的基本风格:
1.显示分配器:要求应用显示地释放任何已分配的块。
2.隐式分配器:要求分配器检测一个已分配的块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器。
基本方法与策略:
1.带边界标签的隐式空闲链表分配器管理:带边界标记的隐式空闲链表的每个块是由一个字的头部、有效载荷、可能的额外填充以及一个字的尾部组成的。当一个应用请求一个k字节的块时,分配器搜索空闲链表,查找一个符合大小的空闲块来放置这个请求块。分配器有三种放置策略:首次适配、下一次适配合最佳适配。在释放一个已分配块的时候需要考虑是否能与前后空闲块合并,减少系统中碎片的出现。
2.显示空间链表管理显式空闲链表是将空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。如,堆可以组织成一个双向链表,在每个空闲块中,都包含一个前驱与一个后继指针。放置策略与上述放置策略一致。
7.10本章小结
本章主要介绍了hello进程在执行的过程中的虚拟内存与物理内存之间的转换关系,以及一些支持这些转换的硬件或软件机制。同时介绍了在发生缺页异常的时候系统将会如何处理这一异常。最后介绍了动态内存分配的作用以及部分方法与策略。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件。所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入输出都被当做对相应文件的读和写来执行。
设备管理:unixio接口。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为UnixI/O。
8.2 简述Unix IO接口及其函数
UnixIO接口:
打开文件:一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
Linux Shell创建的每个进程开始时都有三个打开的文件:标准输入(文件描述符0)、标准输出(描述符为1),标准出错(描述符为2)。头文件定义了常量STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO,他们可用来代替显式的描述符值。
改变当前的文件位置,文件开始位置为文件偏移量,应用程序通过seek操作,可设置文件的当前位置为k。读写文件,读操作:从文件复制n个字节到内存,从当前文件位置k开始,然后将k增加到k+n;写操作:从内存复制n个字节到文件,当前文件位置为k,然后更新k。关闭文件:当应用完成对文件的访问后,通知内核关闭这个文件。内核会释放文件打开时创建的数据结构,将描述符恢复到描述符池中
UnixIO函数:
1.打开文件:int open(char*filename,intflags,mode_tmode);Open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程当中没有打开的最小描述符。Flags参数指明了进程打算如何访问这个文件,同时也可以是一个或者更多为掩码的或,为写提供给一些额外的指示。Mode参数指定了新文件的访问权限位。
2.关闭文件:int close(intfd);调用close函数,通知内核结束访问一个文件,关闭打开的一个文件。成功返回0,出错返回-1。
3.读文件:ssize_tread(intfd,void*buf,size_tn);调用read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示错误,返回值0表示EOF,否则返回值表示的是实际传送的字节数量。
4.写文件:ssize_twrite(intfd,constvoid*buf,size_tn);调用从内存位置buf复制至多n个字节到描述符fd的当前文件位置。返回值1表示出错,否则,返回值表示内存向文件fd输出的字节的数量。
8.3 printf的实现分析
可以发现printf的输入参数是fmt,但是后面是不定长的参数,同时在printf内存调用了两个函数,一个是vsprintf,一个是write。
由上述函数我们可以得出printf执行流程:vsprintf函数将所有的参数内容格式化之后存入buf,然后返回格式化数组的长度。write函数将buf中的i个元素写到终端。从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用int0x80或syscall.字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
getchar是读入函数的一种。它从标准输入里读取下一个字符,相当于getc(stdin)。返回类型为int型,为用户输入的ASCII码或EOF。getchar可用宏实现:#definegetchar()getc(stdin)。getchar有一个int型的返回值。当程序调用getchar时.程序就等着用户按键。用户输入的字符被存放在键盘缓冲区中。直到用户按回车为止(回车字符也放在缓冲区中)。当用户键入回车之后,getchar才开始从stdin流中每次读入一个字符。getchar函数的返回值是用户输入的字符的ASCII码,若文件结尾(End-Of-File)则返回-1(EOF),且将用户输入的字符回显到屏幕。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回
8.5本章小结
本章主要介绍了linux系统中的I/O设备基本概念和管理方法,同时简单介绍了printf和getchar函数的实现。
(第8章1分)
结论
目前为止我们已经走完了hello程序的一生,hello程序的执行在我们看来只是一瞬间的事情,可是对于程序本身和计算机而言是多么漫长而丰富的旅程!它的每个阶段仿佛人生的一个十年,从出生到死亡经历了如下阶段
1.hello.c由cpp预处理到hello.i文本文件
2.hello.i通过编译器处理得到hello.s汇编文件
3.hello.s由as汇编器处理得到二进制可重定位目标文件hello.o
4.hello.o由链接器将重定位目标文件链接为可执行目标文件hello
5.bash进程调用fork函数,生成子进程
6.execve函数加载运行当前进程的上下文中加载并运行新程序hello
7.hello的运行需要物理地址和虚拟地址,它将在cpu中执行每一条指令
8.hello的输入输出与外界交互,与linux I/O设备密切相关
9.hello最终被shell父进程回收,内核会收回为其分配的所有资源
程序的一生令我感叹不已,计算机系统的各个组件设计是如此的精妙,才能让程序运行这种不可思议的事情发生在我们面前!在这门课日常学习过程中我总只眼着于单个知识点,只是了解程序的单个运行过程,难免有些不知所以然。如今通过大作业,犹如上帝视角一般观察到了程序一生,才知“不识庐山真面目,只缘身在此山中”!
(结论0分,缺失 -1分,根据内容酌情加分)
附件
列出所有的中间产物的文件名,并予以说明起作用。
中间结果文件名称 | 文件作用 |
hello.i | 预处理后的文件 |
hello.s | 汇编程序 |
hello.o | 可重定位目标文件 |
hello | 可执行目标程序 |
elf.txt | hello.o的ELF格式 |
back_hello.txt | hello.o的反汇编代码 |
hello.elf | hello的elf格式文件 |
hello_dumpback.s | hello的反汇编代码 |
(附件0分,缺失 -1分)
参考文献
[1] 预处理 - 百度百科 https://link.zhihu.com/?target=https%3A//baike.baidu.com/item/%25E9%25A2%2584%25E5%25A4%2584%25E7%2590%2586/7833652%3Ffr%3Daladdin
[2] C语言编译全过程介绍 – 百度文库 https://link.zhihu.com/?target=https%3A//wenku.baidu.com/view/8976aeb765ce05087632130a.html
[3] fork()创建子进程步骤、函数用法及常见考点(内附fork过程图)https://link.zhihu.com/?target=https%3A//blog.csdn.net/yueyansheng2/article/details/7886004
[4] linux下的文件I/O编程https://link.zhihu.com/?target=https%3A//www.linuxprobe.com/linux-file-i-o.html
[5] 伍之昂. Linux Shell 编程从初学到精通 [M]. 北京:电子工业出版社
[6] 《深入理解计算机系统》Randal E.Bryant David R.O’Hallaron机械工业出版社,2016.
[7] 林来兴. 空间控制技术[M]. 北京:中国宇航出版社,1992:25-42.
[8] 辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.
[9] 赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).
[10] 谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.
[11] KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.
[12] 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.