计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 数据科学与大数据技术
学 号 2022112020
班 级 2203501
学 生 丁勇赫
指 导 教 师 吴锐
计算机科学与技术学院
2024年5月
本篇论文的目的是解释C语言程序如何从源代码转换为可执行文件。以hello.c程序为例,本文详细分析了计算机在生成hello可执行文件的预处理、编译、汇编、链接、进程管理等整个过程的生命周期。同时本文详细解释了计算机系统的工作原理和体系结构,让读者更深入地理解和掌握C语言程序的编译和执行过程。
关键词:计算机系统,生命周期,工作原理,体系结构;
目 录
第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:即From Program to Process。指从hello.c(Program)变为运行时进程(Process)。要让hello.c这个C语言程序运行起来,需要先把它变成可执行文件,这个变化过程有四个阶段:预处理,编译,汇编,链接,完成后就得到了可执行文件,然后就可以在shell中执行它,shell会给它分配进程空间。
020:即From Zero-0 to Zero-0。指最初内存并无hello文件的相关内容,shell用execve函数启动hello程序,把虚拟内存对应到物理内存,并从程序入口开始加载和运行,进入main函数执行目标代码,程序结束后,shell父进程回收hello进程,内核删除hello文件相关的数据结构。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件环境:
处理器:12th Gen Intel(R) Core(TM)i9-12900H 2.50 GHz
机带RAM:16.0GB
系统类型:64位操作系统,基于x64的处理器
软件环境:Windows11 64位,VMware,Ubuntu 20.04 LTS
开发与调试工具:Visual Studio 2023 64位;vim objump edb gcc等工具
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
hello.i 预处理后得到的文本文件
hello.s 编译后得到的汇编语言文件
hello.o 汇编后得到的可重定位目标文件
hello.asm 反汇编hello.o得到的反汇编文件
hello1.asm 反汇编hello可执行文件得到的反汇编文件
1.4 本章小结
本章介绍了hello的P2P,020流程,包括流程的概念,设计思路和实现方法;之后介绍了大作业完成时的硬件配置、软件平台、开发工具以及生成的各个中间结果文件的名称和功能。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理步骤是指预处理器在程序运行前,对源文件进行简单加工的过程。预处理过程主要进行代码文本的替换工作,用于处理以#开头的指令,还会删除程序中的注释和多余的空白字符。预处理指令可以简单理解为#开头的正确指令,它们会被转换为实际代码中的内容(替换)。
2.2在Ubuntu下预处理的命令
预处理的命令:gcc -E hello.c -o hello.i
2.3 Hello的预处理结果解析
我们可以看到,预处理之后的结果就是单纯的预处理指令被扩展到了三千多行,而程序源代码部分并未改变,但是这也能证明hello.c文件被处理(修改)过
在main函数代码出现之前的大段代码源自头文件<stdio.h> <unistd.h> <stdlib.h> 的依次展开。
以 stdio.h 的展开为例:预处理过程中,#include指令的作用是把指定的头文件的内容包含到源文件中。stdio.h是标准输入输出库的头文件,它包含了用于读写文件、标准输入输出的函数原型和宏定义等内容。
预处理器的主要作用就是把通过预处理的内建功能对一个资源进行等价替换
2.4 本章小结
本章主要介绍了hello.c程序预处理方面的内容,包括预处理的原理,流程和作用。通过分析,我们可以发现预处理后的文件hello.i包含了标准输入输出库stdio.h的内容,以及一些宏和常量的定义,还有一些行号信息和条件编译指令。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译的概念
计算机程序编译的概念是指将用高级程序设计语言书写的源程序,翻译成等价的汇编语言格式程序的翻译过程。
编译的作用
计算机程序编译的作用是使高级语言源程序变为汇编语言,提高编程效率和可移植性。计算机程序编译的基本流程包括词法分析、语法分析、语义分析、中间代码生成、代码优化和目标代码生成等阶段。
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函数
本程序只有main一个函数,且为全局函数,通过.s文件第十行可知
3.3.3数据
(1)字符串程序有两个字符串存放在只读数据段中,如图:
hello.c中唯一的数组是main函数中的第二个参数(即char**argv),数组的每个元素都是一个指向字符类型的指针。由知数组起始地址存放在栈中-32(%rbp)的位置,被两次调用作为参数传到printf中。
如图,分别将rax设置为两个字符串的起始地址:
(2)参数argc
参数argc是main函数的第一个参数,被存放在寄存器%edi中,由语句
可见寄存器%edi地址被压入栈中,而语句
可知该地址上的数值与立即数5判断大小,从而得知argc被存放在寄存器并被压入栈中。
(3)局部变量
程序中的局部变量只有i,我们根据
可知局部变量i是被存放在栈上-4(%rbp)的位置。
3.3.4赋值
mov指令有传送赋值的功能,在本程序中以for循环为例,将开头的i赋值为0
对应汇编代码中
,-4(%rbp)存放的就是变量i
3.3.5算术运算
在for循环中,每次i++对应的就是加法运算,但是源和目的只有i一个,所以用add指令即可,由于是int型,所以用addl
3.3.6关系操作
hello.c中存在两个关系操作,分别为:
- 条件判断语句if(argc!=5)
使用了cmpl指令比较立即数5和参数argc大小,并且设置了条件码。根据条件码,如果相等(je)则跳转到L2,不相等就顺序执行下面的指令。
- 在for循环每次循环结束要判断一次i<10
使用了cmpl指令比较立即数9和循环变量i大小,并且设置了条件码。根据条件码,如果小于等于(jle)则跳转到L4,否则就顺序执行下面的指令。
3.3.7控制转移
在本程序中存在两个控制转移:
(1)
判断argc是否为5,如果不为5,则执行if语句,否则执行其他语句,在汇编代码中则表现为如果条件码为1,则跳到.L2,否则执行cmpl指令后的指令。
(2)
在for循环每次结束判断一次i<10,翻译为汇编语言后,通过条件码判断每次循环是否跳转到.L4。而在for循环初始要对i设置为0,如下:
然后直接无条件跳转(jmp)到.L3循环体。
3.3.8参数传递和函数调用
本程序中main函数调用了exit和sleep和atoi(sleep调用)函数,我们以这两个函数为例进行分析
exit函数
参数传递与函数调用:
将rdi(exit的参数)设置为1,再使用call指令调用函数。
atoi、sleep函数
参数传递与函数调用:
可见,atoi函数将参数argv[3]放入寄存器%rdi中用作参数传递,简单使用call指令调用。
然后,将转换完成的秒数从%eax传递到%edi中,edi存放sleep的参数,再使用call调用。
3.4 本章小结
本章介绍了C编译器把hello.i文件转换成hello.s文件的过程,解释了编译的含义和功能,并给出了Linux下汇编指令。本章还详细分析生成的hello.s文件中的汇编代码,解释了数据处理,函数调用,各种运算以及控制转移等操作方面的工作原理和指令逻辑,比较了源代码和汇编代码分别是怎样实现这些操作的。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编的概念
汇编是指汇编器(as)将包含汇编语言的.s文件翻译为机器语言指令,并把这些指令打包成为一个可重定位目标文件的格式,生成目标文件.o文件。.o文件是一个二进制文件,包含main函数的指令编码。
汇编的作用
汇编就是将高级语言转化为机器可直接识别执行的代码文件的过程,汇编器将.s 汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式。 .o 文件是一个二进制文件,它包含程序的指令编码。
4.2 在Ubuntu下汇编的命令
gcc hello.s -c -o hello.o
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
输入readelf -a hello.o > hello.elf(重定位到hello.elf) 指令获得 hello.o 文件的 ELF 格式:
(1)ELF头
ELF头(ELF header)以一个l6字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含了帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是有节头部表描述的,其中目标文件中每个节都有一个固定大小的条目(entry)。ELF头展示如下:
(2)节头(section header)
记录各节名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐。
(3)重定位节
.rel.text节是一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改,而调用本地函数的指令不需修改。可执行目标文件中不包含重定位信息。如图,需要重定位的内容如下:
(4)符号表
.symtab节中包含ELF符号表,这张符号表包含一个条目的数组,存放一个程序定义和引用的全局变量和函数的信息。该符号表不包含局部变量的信息。符号表如下:
4.4 Hello.o的结果解析
1.命令
在shell中输入 objdump -d -r hello.o > hello.asm 指令输出hello.o的反汇编文件,并与第3章的hello.s文件进行对照分析。
2.与hel1o.s的对照分析
1.操作数进制
反汇编文件中的所有操作数都改为十六进制。立即数由hello.s中的$5变为了$0x5,地址表示也由-20(%rbp)变为-0x14(%rbp)。可见只是进制表示改变,数值未发生改变。
2.分支转移
反汇编的跳转指令中,所有跳转的位置被表示为主函数+段内偏移量这样确定的地址,而不再是段名称(例如.L3)。例如下面的jmp指令,反汇编文件中为
而hello.s文件中为
3.函数调用
反汇编文件中对函数的调用与重定位条目相对应。call指令调用函数在hello.s中为
而在反汇编文件中调用函数为
在可重定位文件中call后面不再是函数名称,而是一条重定位条目指引的信息。
4.5 本章小结
本章介绍了汇编的含义,功能,以及汇编的结构体系,内容和格式。以hello.s文件为例,说明了如何把它汇编成hello.o文件,并生成ELF格式文件hello.elf。对文件中的每个节进行解释说明并加以分析。通过分析hello.o的反汇编代码(保存在hello.asm中)和hello.s的区别和相同点,让读者了解怎么将汇编代码变成能让机器看懂的形式。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
链接的概念
链接(linkng)是将各种代码和数据片段收集并组合为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行与编译时(compile time),也就是在源代码被翻译为机器代码时;也可以执行与加载时(load time),也就是程序被加载器加载到内存并执行时:甚至执行于运行时。
链接的作用
在现代系统中,链接是由叫做链接器(1iker)的程序自动执行的,它们使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用。
5.2 在Ubuntu下链接的命令
在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 -h hello来解析hello的ELF格式,得到相应节信息和段信息
1.ELF头(ELF Header)
hello1.elf中的ELF头与hello.elf中的ELF头包含的信息种类基本相同,以描述了生成该文件的系统的字的大小和字节顺序的16字节序列Magic开始,剩下的部分包含帮助链接器语法分析和解释目标文件的信息。与hello.elf相比较,hello1.elf中的基本信息未发生改变(如Magic,类别等),而类型发生改变,程序头大小和节头数量增加,并且获得了入口地址。
2.节头
描述了各个节的大小、偏移量和其他属性。链接器链接时,会将各个文件的相同段合并成一个大段,并且根据这个大段的大小以及偏移量重新设置各个符号的地址。
3.符号表
符号表中保存着定位、重定位程序中符号定义和引用的信息,所有重定位需要引用的符号都在其中声明。
4.可重定位段
可重定位段可以保证多目标文件链接时不发生目标地址重叠,绝对地址段只适用于某些特殊场合,如固定I/O口或中断向量的入口地址
5.4 hello的虚拟地址空间
使用edb打开hello从Data Dump窗口观察hello加载到虚拟地址的情况,查看各段信息。如图:
可以看到hello虚拟地址空间的起始地址为0x401000,结束地址为0x401ff0。
根据5.3中的节头部表,可以通过edb找到各段的信息。
例如.text节,首先先从节头部表找到开始的虚拟地址
在edb中找到对应的信息:
5.5 链接的重定位过程分析
使用命令objdump -d -r hello > hello1.asm查看hello反汇编文件hello1.asm
hello的反汇编代码与hello.s的反汇编代码基本完全相同的,但是hello的反汇编代码多了很多说明内容,比较看一下区别:
hello反汇编代码中函数调用时不再仅仅储存call当前指令的下一条指令,而是已经完成了重定位,调用的相应函数已经有对应的明确的虚拟地址空间
hello反汇编代码中相比.s反汇编代码多出来的节都是经过链接之后加入进来的。例如.init节就是程序初始化需要执行的代码所在的节,.dynamic节是存放被ld.so调用过的 动态链接信息的节等等。
重定位的过程分为两大步:
1.重定位节和符号定义。在这一步中,链接器将所有相同类型的节合并为同一类型的聚合节。然后链接器将运行时的内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。至此程序中每条指令和全局变量都有唯一的运行内存地址。
2.重定位节中的符号引用。这一步中链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。要执行这一步,链接器依赖于可重定位目标模块中称为重定位条目的数据结构。
5.6 hello的执行流程
通过edb的调试,一步一步地记录下call命令进入的函数。
(1)开始执行:_start、_libe_start_main
(2)执行main:_main、printf、_exit、_sleep、getchar
(3)退出:exit
子程序名或地址:
5.7 Hello的动态链接分析
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它,延迟绑定是通过GOT和PLT实现的。
.plt:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。
.got:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。
GOT表位置在调用dl_init之前0x404008后的16个字节均为0:
调用了dl_init之后字节改变了:
5.8 本章小结
本章解释了链接的基本概念和作用,使用命令链接生成hello可执行文件,观察了hello文件ELF格式下的内容,利用edb观察了hello文件的虚拟地址空间使用情况,对其链接过程进行分析,最后阐明了他的重定位,执行流程和动态链接。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程的概念
进程的经典定义就是一个执行中程序的实例。进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
进程的作用
进程为程序提供了一种假象,程序好像是独占的使用处理器和内存,处理器好像是无间断地一条接一条地执行我们程序中的指令。进程作为一个执行中程序的实例,系统中每个程序都运行在某个进程的上下文中。
6.2 简述壳Shell-bash的作用与处理流程
Shell-bash的作用
Shell是一个交互型应用级程序,也被称为命令解析器,它为用户提供一个操作界面,接受用户输入的命令,并调度相应的应用程序。
Shell-bash的处理流程
首先从终端读入输入的命令,对输入的命令进行解析,如果该命令为内置命令,则立即执行命令,否则调用fork创建一个新的子进程,在该子进程的上下文中执行指定的程序。判断该程序为前台程序还是后台程序,如果为前台程序则等待程序执行结束,若为后台程序则将其放回后台并返回。在过程中shell可以接受从键盘输入的信号并对其进行处理。
6.3 Hello的fork进程创建过程
首先用户在shel1界面输入指令:./hello 2022112020 丁勇赫 18245698686 1
Shell判断该指令不是内置命令,于是父进程调用fork函数创建一个新的子进程,该子进程得到与父进程用户级虚拟地址空间相同的一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程与父进程最大的区别就是具有不同的PID。在父进程中,fork返回子进程的PID,而在子进程中fork返回0,返回值提供一个明确的方法来分辨程序是父进程还是在子进程中执行。
6.4 Hello的execve过程
execve函数在当前进程的上下文中加载并运行一个程序。函数声明如下:
int execve(const char *filename, const char *argv[], const char *envp[]);
execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境变量envp。只有当出现错误时,例如找不到filename,execve才会返回到调用程序。所以,与fork一次调用返回两次不同,execve调用一次并不返回。
6.5 Hello的进程执行
hello程序在运行时,进程提供给应用程序的抽象有:(1)一个独立的逻辑控制流,它提供一个假象,好像我们的进程独占地使用处理器;(2)一个私有的地址空问,它提供一个假象,好像我们的程序独占地使用CPU内存。
操作系统提供的抽象有:
(1)逻辑控制流。如果想用调试器单步执行程序,我们会看到一系列的程序计数器(PC)的值,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或是包含在运行时动态链接到程序的共享对象中的指令。这个PC值的序列叫做逻辑控制流,或者简称为逻辑流。一个逻辑流的执行在时间上与另一个流重叠,称为并发流,这两个流被称为并发地运行。
(2)上下文切换。操作系统内核使用一种称为上下文切换的叫高层形式的异常控制流来实现多任务。内核为每一个进程维持一个上下文。上下文就是内核重新启动一个被抢占的进程所需状态。
(3)时间片。一个进程执行它的控制流的一部分的每一时间段叫做时间片。因此,多任务也叫做时间分片。
(4)用户模式和内核模式。处理器通常使用某个控制寄存器中的一个模式位来提供这种功能。当设置了模式位时,进程就运行在内核模式里。一个运行在内核模式的进程可以执行指令集中的所有指令且可以访问系统中的任何内存位置。没有设置模式位时,进程就运行在用户模式中。用户模式中的进程不允许执行特权指令,也不能直接引用地址空间中内核区内的代码和数据。
(5)上下文信息。上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。hello程序执行过程中,在进程调用execve函数后,进程就为hello程序分配新的虚拟地址空间,开始时程序运行在用户模式中,调用printf函数输出“Hello 2022112020 丁勇赫 18245698686”,之后调用sleep函数,进程进入内核模式,运行信号处理程序,再返回用户模式,运行过程中,cpu不断切换上下文,使运行过程被切分成时间片,与其他进程交替占用cpu,实现进程的调度。
6.6 hello的异常与信号处理
1.异常的分类
2.异常的处理方式
3.运行结果及相关命令
1.正常运行状态
在程序正常运行时,打印10次提示信息,以输入1为标志结束程序,并回收进程。
2.运行时按下Ctrl + C
按下Ctrl + C,Shell进程收到SIGINT信号,Shell结束并回收hello进程。
3.运行时按下Ctrl + Z
按下Ctrl + Z,Shell进程收到SIGSTP信号,Shell显示屏幕提示信息并挂起hello进程。
4.对hello进程的挂起可由ps和jobs命令查看,可以发现hello进程确实被挂起而非被回收,且其job代号为1。
5.输入kill命令,则可以杀死指定(进程组的)进程:
6.7本章小结
本章介绍了Hello进程的运行过程,以及进程相关一些知识和概念。并且实际模拟了异常情况并给出相应结果和处理方式。让读者深入了解进程对于程序运行所提供的重要假象:逻辑控制流和私有地址空间。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址
在有地址变换功能的计算机中,访问指令给出的地址(操作数)叫逻辑地址,也叫相对地址。要经过寻址方式的计算或变换才得到内存储器中的物理地址。逻辑地址是由一个段标识符加上一个指定段内相对地址的偏移量,由程序hello产生的与段相关的偏移地址部分
线性地址
线性地址是逻辑地址到物理地址变换之间的一步,程序hello的代码会产生逻辑地址,在分段部件中逻辑地址是段中的偏移地址,加上基地址就是线性地址。
虚拟地址
程序访问存储器所使用的逻辑地址称为虚拟地址。虚拟地址经过地址翻译得到物理地址。与实际物理内存容量无关,是hello中的虚拟地址
物理地址
在存储器里以字节为单位存储信息,每一个字节单元给一个唯一的存储器地址,这个地址称为物理地址,是hello的实际地址或绝对地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段式管理是指把一个程序分成若干个段进行存储,每个段都是一个逻辑实体。段式管理是通过段表进行的,包括段号(段名)、段起点、装入位、段的长度等。程序通过分段划分为多个块,如代码段、数据段、共享段等。
一个逻辑地址是两部分组成的,包括段标识符和段内偏移量。段标识符是由一个16位长的字段组成的,称为段选择符。其中前13位是一个索引号,后3位为一些硬件细节。索引号即是“段描述符”的索引,段描述符具体地址描述了一个段,很多个段描述符就组成了段描述符表。通过段标识符的前13位直接在段描述符表中找到一个具体的段描述符。
全局描述符表(GDT)整个系统只有一个,它包含:(1)操作系统使用的代码段、数据段、堆栈段的描述符(2)各任务、程序的LDT(局部描述符表)段。
每个任务程序有一个独立的LDT,包含:(1)对应任务/程序私有的代码段、数据段、堆栈段的描述符(2)对应任务/程序使用的门描述符:任务门、调用门等。
段式管理图示如下:
7.3 Hello的线性地址到物理地址的变换-页式管理
虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。VM系统将虚拟内存分割,称为虚拟页,类似地,物理内存也被分割成物理页。利用页表来管理虚拟页,页表就是一个页表条目(PTE)的数组,每个PTE由一个有效位和一个位地址字段组成,有效位表明了该虚拟页当前是否被缓存在DRAM中,如果设置了有效位,那么地址字段就表示DRAM中相应的物理页的起始位置,如果发生缺页,则从磁盘读取。
MMU利用页表来实现从虚拟地址到物理地址的翻译。
下面为页式管理的图示:
7.4 TLB与四级页表支持下的VA到PA的变换
Core i7采用四级页表的层次结构。CPU产生虚拟地址VA,虚拟地址VA传送给MU,MMU使用VPN高位作为TLBT和TLBI,向TLB中寻找匹配。如果命中,则得到物理地址PA。如果TLB中没有命中,MMU查询页表,CR3确定第一级页表的起始地址,VPN1确定在第一级页表中的偏移量,查询出PTE,以此类推,最终在第四级页表中找到PPN,与VPO组合成物理地址PA,添加到PLT。工作原理如下:
多级页表的工作原理展示如下:
7.5 三级Cache支持下的物理内存访问
高速缓存的结构将m个地址位划分成了t个标记位,s个组索引位和b个块偏移位:
如果选中的组存在一行有效位为1,且标记位与地址中的标记位相匹配,我们就得到了一个缓存命中,否则就称为缓存不命中。如果缓存不命中,那么它需要从存储器层次结构的下一层中取出被请求的块,然后将新的块存储在组索引位指示组中的一个高速缓存行中,具体替换哪一行取决于替换策略,例如LRU策略会替换最后一次访问时间最久远的那一行。
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_ struct、.区域结构和页表的原样副本。当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任何一个。后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下几个步骤:
(1)删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
(2)映射私有区域。为新程序的代码、数据、.bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区,.bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零。
(3)映射共享区域。hello程序与共享对象1ibc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
(4)设置程序计数器。execve做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。如图所示:
7.8 缺页故障与缺页中断处理
如果程序执行过程中发生了缺页故障,则内核调用缺页处理程序。处理程序执行如下步骤:
(1)检查虚拟地址是否合法,如果不合法则触发一个段错误,终止这个进程。
(2)检查进程是否有读、写或执行该区域页面的权限,如果不具有则触发保护异常,程序终止。
(3)两步检查都无误后,内核选择一个牺牲页面,如果该页面被修改过则将其交换出去,换入新的页面并更新页表。然后将控制转移给hello进程,再次执行触发缺页故障的指令。
7.9动态存储分配管理
概念:使程序在运行过程中能根据需要分配内存空间,其分配的内存空间通常称为堆。
具体操作:用一指针指向动态存储分配得到的内存空间,此后通过对指针的操作,就可以使用这个内存空间了。
使用的四个函数:malloc函数,calloc函数,realloc函数,free函数。使用这些函数必须包含<stdlib.h>文件。
1.malloc函数
void* malloc(size_t size);
在c99之前要通过malloc来进行动态存储分配,malloc不会对分配内存空间初始化。
malloc的用法:
#include<stdio.h>
#include<stdlib.h>
int main() {
int length;
scanf_s("%d", &length);
//int array[length];
int* p = (int*)malloc(length * sizeof(int));
/*将指针p指向malloc分配得到的内存地址。*/
for (int i = 0;i < length;i++) {
printf("array[%d]的地址:%p,值:%d\n", i, &p[i], p[i]);
}
return 0;
}
2.calloc函数
void* calloc(size_t nmemb,size_t size);
与malloc的使用相似,只是calloc函数有两个参数。第一个为空间个数,第二个为每个空间的大小。并且calloc函数会为分配的内存空间初始化
3.realloc函数
void* realloc(viod* ptr,size_t size);
realloc函数用于调整先前分配的内存空间的大小,并返回该内存空间的通用指针,ptr指向的是先前已分配的内存空间,size表示新内存空间的大小。
4.free函数
void free(void* ptr);
free释放由malloc / calloc函数分配的内存空间,指针ptr指向要释放的空间。
如果上述的函数不能成功分配指定内存大小的空间,那么就会返回空指针NULL。
5.动态内存分配器管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。对每个进程,内核维护一个变量brk指向堆顶(brk指针)。
分配器将堆视为一组不同大小的块的集合来维护。每个块是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用分配。一个已分配的块保持已分配的状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器的具体操作过程如下:
1.放置已分配块:当一个应用请求一个k字节的块时,分配器搜索空闲链表。查找一个足够大可以放置所请求的空闲块。
2.分割空闲块:一旦分配器找到了匹配的空闲块,它就需要决定分配这个空闲块中多少空间。可以选择用整个块,虽然简单快捷,但会造成额外的内部碎片;也可以选择将空闲块分割为两部分,第一部分变成已分配块,剩下的变成新的空闲块。
3.获取额外的堆内存:如果分配器不能为请求块找到空闲块,首先是通过合并那些在内存中物理上相邻的空闲块来创建一些更大的空闲块。而如果这样还不能生成足够大的块或者空闲块已经最大程度地合并了,那么分配器就会通过调用sbrk函数,向内核请求额外的堆内存。分配器将额外的内存转化成一个大的空闲块,将这个块插入到空闲链表中,然后将被请求的块放置在这个新的空闲块中。
4.合并空闲块:分配器释放一个已分配块时,可能有其他空闲块与这个新释放的空闲块相邻,引起假碎片现象,因此为了解决这个问题,分配器必须合并相邻的空闲块。分配器可以立即合并,也就是在每次一个块被释放时,就合并所有的相邻的空闲块;也可以推迟合并,也就是等到某个稍晚的时候在合并空闲块。
7.10本章小结
本章从多个角度分析了hello的动态存储管理,包括存储器地址空间、intel的段式管理、hello的页式管理,以intel Core i7在指定环境下介绍了虚拟地址VA到物理地址PA的转换、物理内存访问,分析了hello进程fork时的内存映射、hello进程、execve时的内存映射、缺页故障与缺页中断处理以及动态存储分配管理。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单低级的应用接口,称为Unix I/O。
8.2 简述Unix IO接口及其函数
1.函数open()和opennat()
int open(const char* path, int oflag, .../*mode_t mode*/);
int openat(int fd, const char* path, int oflag, .../*mode_t mode*/);
若文件打开失败则返回-1,失败原因可以通过errno查看;若成功将返回最小的未用的文件描述符的值。其中参数path为要打开的文件的文件路径,oflag为文件打开模式。
2.函数create()
int create(const char *path, mode_t mode);
若文件创建失败返回-1;若创建成功返回当前创建文件的文件描述符。参数与open中对应的参数含义相同。create(path, mode)函数功能为创建新文件,与open(path, O_CREATE|O_TRUNC|O_WRONLY)功能相同。
3.函数lseek()
int lseek(int fd, off_t offset, int whence);
成功则返回新的文件的偏移量;失败则返回-1。使用lseek()函数显式的为一个打开的文件设置偏移量。lseek仅将文件的偏移量记录在内核中,并不引起IO开销。
4.函数read()
ssize_t read(int fd, void *buf, size_t nbytes);
若读取成功,读到文件末尾返回0,未读到文件末尾返回当前读的字节数。若读取失败,返回-1。fd为要读取文件的文件描述符。buf为读取文件数据缓冲区,nbytes为期待读取的字节数,通常为sizeof(buf)。
5.函数write()
若写入成功则返回写入的字节数;失败返回-1。buf为写入内容的缓冲区,ntyes为期待写入的字节数,通常为sizeof(buf)。一般情况下返回值与ntypes相等,否则写入失败。
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;
}
在形参列表里有这么一个token:...
这个是可变形参的一种写法。
当传递参数的个数不确定时,就可以用这种方式来表示。
printf调用了两个函数,一个是vsprintf,还有一个是write。
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的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。write函数将buf中的i个元素写到终端。
8.4 getchar的实现分析
int getchar(void)
{
static char buf[bufsize];
static char* b=buf;
static int n=0;
if(n==0)
{
n=read(0,buf,bufsize);
b=buf;
}
return(--n>=0)?(unsigned char)*b++:EOF;
}
getchar由宏实现:#define getchar() getc(stdin)。getchar有一个int型的返回值。当程序调用getchar时.程序就等着用户按键。用户输入的 字符被存放在键盘 缓冲区中。直到用户按回车为止(回车字符也放在缓冲区中)。当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的字符的ASCII码,若文件结尾(End-Of-File)则返回-1(EOF),且将用户输入的字符 回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完后,才等待用户按键。
8.5本章小结
本章总结了Linux函数I/O设备的管理方法,以及UnixI/O函数在使用时的用法,参数含义和函数功能。
(第8章1分)
结论
hello所经历的过程:
首先由程序员将hello代码从键盘输入,依次要经过以下步骤:
1、预处理(cpp)。将hello.c进行预处理(gcc -E),生成一个经过修改的hello.i文件。
2、编译(ccl)。将hello.i文件翻译(gcc -S)生成一个包含汇编语言的文件hello.s。
3、汇编(as)。将hello.s汇编(gcc -c)生成一个可重定位目标文件hello.o。
4、链接(ld)。将hello.o文件和可重定位目标文件和动态链接库链接起来,生成一个可执行目标文件hello。
5、运行。在shel1中输入./hello 2022112020 丁勇赫 18245698686 1
6、创建进程。终端判断输入的指令不是shell内置指令,于是调用fork函数创建一个新的子进程,并将其当做程序执行。
7、加载程序。shell调用execve函数,启动加载器,映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入main函数。
8、执行指令:CPU为进程分配时间片,在一个时间片中,hello享有CPU资源,顺序执行自己的控制逻辑流。
9、访问内存:MU将程序中使用的虚拟内存地址通过页表映射成物理地址。
10、信号管理:当程序在运行的时候我们输入Ctrl+c,内核会发送SIGINT信号给进程并终止前台作业。当输入Ctrl+z时,内核会发送SIGTSTP信号给进程,并将前台作业停止挂起,输入kill -9 %1,会杀死被挂起的进程。
11、终止:当子进程执行完成时,内核安排父进程回收子进程,将子进程的退出状态传递给父进程。内核删除为这个进程创建的所有数据结构以及回收分配的空间。
感悟:
通过本次大作业,我深入理解到了计算机如何系统化地执行一个程序。从用户到系统内核,一道道复杂但是精致的工序成就了现代高性能的计算机,这背后都是人们智慧的结晶和严谨的逻辑。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
文件名 | 功能 |
hello.c | 源程序 |
hello.i | 预处理后得到的文本文件 |
hello.s | 编译后得到的汇编语言文件 |
hello.o | 汇编后得到的可重定位目标文件 |
hello.elf | 用readelf读取hello.o得到的ELF格式信息 |
hello.asm | 反汇编hello.o可重定位文件得到的反汇编文件 |
hello1.asm | 反汇编hello可执行文件得到的反汇编文件 |
hello | 可执行文件 |
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] Randal E.Bryant David R.O'Hallaron.深入理解计算机系统(第三版).机械工业出版社
[2] https://blog.csdn.net/weixin_51744028/article/details/124716781
[3] https://blog.csdn.net/qq_41071068/article/details/90741413?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522171818380216800227411147%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=171818380216800227411147&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~top_positive~default-1-90741413-null-null.nonecase&utm_term=%E5%8A%A8%E6%80%81%E5%AD%98%E5%82%A8%E5%88%86%E9%85%8D&spm=1018.2226.3001.4450
[4] [转]printf 函数实现的深入剖析 - Pianistx - 博客园 (cnblogs.com)
[5] https://www.cnblogs.com/diaohaiwei/p/5094959.html
(参考文献0分,缺失 -1分)