计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 人工智能方向
学 号 2022110779
班 级 WL023
学 生 金智威
指 导 教 师 刘宏伟
计算机科学与技术学院
2024年5月
本文的目的是解析C语言程序如何从代码转换为可执行文件以及中间过程。本文将以hello.c为例,分析生成hello可执行程序过程中的预处理、编译、汇编、链接、进程管理等。本文将从理论上探讨这些过程的原理以及方法,并将实机演示操作结果。
关键词:计算机系统;C语言
目 录
2.2在Ubuntu下预处理的命令............................................................................. - 5 -
5.3 可执行目标文件hello的格式........................................................................ - 8 -
6.2 简述壳Shell-bash的作用与处理流程........................................................ - 10 -
6.3 Hello的fork进程创建过程......................................................................... - 10 -
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 -
8.2 简述Unix IO接口及其函数.......................................................................... - 13 -
第1章 概述
1.1 Hello简介
P2P:P2P即From Program to Process。是指hello.c变为运行时进程。要让hello.c这个C语言程序运行起来,需要先把它变成可执行文件,这个变化过程有四个阶段:预处理,编译,汇编,链接,完成后就得到了可执行文件,然后就可以在shell中执行它,shell会给它分配进程空间。
020:020即From Zero-0 to Zero-0。指从最初内存并无hello相关内容,shell用execve函数启动程序,将虚拟内存对应到物理内存,并从程序入口开始加载运行,进入main函数,结束后,shell父进程回收hello进程,内核删除hello相关的数据结构。
1.2 环境与工具
处理器:12th Gen Intel(R) Core(TM)i5-12500H 2.50 GHz
机带RAM:16.0GB
系统类型:64位操作系统,基于x64的处理器
软件环境:Windows11 64位,VMware,Ubuntu 20.04 LTS
开发与调试工具:Visual Studio 2021 64位;vim objump edb gcc readelf等工具
1.3 中间结果
hello.i 预处理后得到的文本文件
hello.s 编译后得到的汇编语言文件
hello.o 汇编后得到的可重定位目标文件
hello.asm 反汇编hello.o得到的反汇编文件
hello1.asm 反汇编hello可执行文件得到的反汇编文件
1.4 本章小结
本章介绍了hello中P2P、020的含义。同时说明了硬件配置、软件环境、开发工具以及各个中间结果文件的名称及功能。
第2章 预处理
2.1 预处理的概念与作用
预处理步骤是指预处理器在程序运行前,对源文件进行简单加工的过程。预处理过程主要进行代码文本的替换工作,用于处理以#开头的指令,还会删除程序中的注释和多余的空白字符。预处理指令可以简单理解为#开头的正确指令,它们会被转换为实际代码中的内容。
预处理过程中并不直接解析程序源代码的内容,而是对源代码进行相应的分割、处理和替换,主要有以下作用:
头文件包含:将所包含头文件的指令替代。
宏定义:将宏定义替换为实际代码中的内容。
条件编译:根据条件判断是否编译某段代码。
其他:如注释删除等。
简单来说,预处理是一个文本插入与替换的过程预处理器。
2.2在Ubuntu下预处理的命令
2.3 Hello的预处理结果解析
在linux下打开hello.i文件,我们发现,除了预处理指令被扩展外,源程序其余部分不变。
在main函数前出现的代码来自于头文件的展开。
以stdio.h的展开为例,当预处理器遇到#include<stdio.h>时,它会在系统的头文件路径下查找stdio.h文件,一般在/usr/include目录下,然后把stdio.h文件中的内容复制到源文件中。stdio.h文件中可能还有其他的#include指令,比如#include<stddef.h>或#include<features.h>等,这些头文件也会被递归地展开到源文件中。
预处理器不会对头文件中的内容做任何计算或转换,只是简单地复制和替换。
2.4 本章小结
本章讲述了在linux环境下,如何使用命令对C语言程序进行预处理,以及预处理的含义和作用。并且通过hello.c到hello.i这一过程进行了演示,并用具体的代码分析了预处理后的结果。通过分析我们发现预处理后的文件包含了头文件中的内容,以及一些宏和常量的定义,还有行号信息和条件编译指令
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译的概念是指将用高级程序设计语言书写的源程序,翻译成等价的汇编语言格式程序的翻译过程。
编译的作用是使高级语言源程序变为汇编语言,提高编程效率和可移植性。计算机程序编译的基本流程包括词法分析、语法分析、语义分析、中间代码生成、代码优化和目标代码生成等阶段。
3.2 在Ubuntu下编译的命令
3.3 Hello的编译结果解析
3.3.1汇编初始部分
在文件开头有一部分字段展示了节名称:
.file 声明出源文件
.text 表示代码节
.section .rodata 表示只读数据段
.align 声明对指令或者数据的存放地址进行对齐的方式
.string 声明一个字符串
.globl 声明全局变量
.type 声明一个符号的类型
3.3.2 数据部分
(1)字符串
程序中有两个字符串被存储在只读数据段中:
hello.c中的argv是一个char*类型的数组,其中每个元素都是指向字符类型的指针。其中数组起始地址存放在栈中-32(%rbp)的位置,被两次调用作为参数传到printf中。
如上图所示,分别将rdi设置为两个字符串的起始位置
(2)参数argc
参数argc是main函数的第一个参数,由寄存器%edi存储。
由该命令可知%edi被压入栈中,而语句
则将这部分内容与立即数5进行比较。
(3)局部变量
程序中的局部变量只有i。由
可知i是被存放在了栈上-4(%rbp)处。
3.3.3全局函数
hello.c中只有一个全局函数main,我们可以通过反汇编代码中
这一部分看出
3.3.4赋值操作
该程序中的赋值操作只有for循环中i=0这一操作,在汇编代码中使用mov实现:
。由上文可知-4(%rbp)是i存放的位置,该语句将立即数0赋值给i。同时由于i为int类型,所以使用movl进行双字传送。
3.3.5算术操作
该程序中的算数操作仅有for循环中的i++部分,在汇编代码中使用add指令实现:
。由于i为int类型,所以使用addl进行双字操作。
3.3.6关系操作与控制转移指令
程序中有两个关系操作,分别为判断语句if(argc!=5),以及在循环中的i<10
(1)
使用cmp命令比较立即数5与参数argc的大小,并设置条件码。如果两数相等则执行跳转命令。
(2)
在for循环结束后都要判断一次i<10,如果不符合则跳出,汇编代码如上。通过比较立即数9和i,并通过条件码控制跳转位置。
3.3.7函数操作
(1)main函数:
参数传递:该函数的参数为int argc,,char*argv[]。具体参数传递地址和值都在前面阐述过。
函数调用:通过使用call内部指令调用语句进行函数调用,并且将要调用的函数地址数据写入栈中,然后自动跳转到这个调用函数内部。main函数里调用了printf、exit、sleep函数。
局部变量:使用了局部变量i用于for循环。具体局部变量的地址和值都在前面阐述过。
(2)printf函数:
参数传递:printf函数调用参数argv[1],argv[2],argv[3]。
函数调用:该函数调用了两次。第一次将寄存器%rdi设置为待传递字符串"用法:Hello学号 姓名 手机号 秒数!\n"的起始地址;第二次将其设置为“Hello %s %s %s\n”的起始地址。具体已在前面讲过。使用寄存器%rsi完成对argv[1]的传递,用%rdx完成对argv[2]的传递,用%rcx完成argv[3]的传递。
(3)exit函数:
参数传递与函数调用:
将rdi设置为1,再使用call指令调用函数
(4)atoi、sleep函数
参数传递与函数调用:
可见,atoi函数将参数argv[4]放入%rdi中用于参数传递,并使用call指令,并将结果保存在%eax中,再传入%edi中作为函数sleep的参数,再进行call指令调用。
(5)getchar函数
无参数,直接使用call进行调用
3.3.8类型转换
atoi函数将字符转换成了sleep函数中参数需要的int类型。
3.4 本章小结
本章介绍了C语言编译器将hello.i文件转换成hello.s文件的过程,并简要说明了编译的含义和功能、演示了编译的指令,并分析hello.s中的汇编代码,讨论了数据处理、函数调用、赋值、算数、关系等运算、以及控制跳转和类型转换等方面,分析了汇编代码是如何实现这些功能的
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编是指汇编器(as)将包含汇编语言的.s文件翻译为机器语言指令,并把这些指令打包成为一个可重定位目标文件的格式,生成目标文件.o文件。.o文件是一个二进制文件,包含main函数的指令编码。
汇编就是将高级语言转化为机器可直接识别执行的代码文件的过程,汇编器将.s 汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式。 .o 文件是一个二进制文件,它包含程序的指令编码。
4.2 在Ubuntu下汇编的命令
4.3 可重定位目标elf格式
在shell中输入readelf -a hello.o > hello.elf指令获得ELF格式
接下来,我们查看ELF文件中的各部分内容
- ELF头:
ELF头(ELF header)以一个l6字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含了帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是有节头部表描述的,其中目标文件中每个节都有一个固定大小的条目(entry)。
- 头节:
记录各节名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐。
- 重定位节:
.rel.text节是一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改,而调用本地函数的指令不需修改。可执行目标文件中不包含重定位信息。
- 符号表
.symtab节中包含ELF符号表,这张符号表包含一个条目的数组,存放一个程序定义和引用的全局变量和函数的信息。该符号表不包含局部变量的信息。
4.4 Hello.o的结果解析
在shell中输入objdump -d -r hello.o > hello.asm指令输出hello.o的反汇编文件,并与第三章中的hello.s进行对照。
区别如下:
- 增加机器语言
每一条指令都增加了十六进制的机器语言,以cmpl语句为例:
- 操作数进制改变
反汇编文件中的所有操作数都改为十六进制。如(1)中的例子,立即数由hello.s中的$4变为了$0x4,地址表示也由-20(%rbp)变为-0x14(%rbp)。可见只是进制表示改变,数值未发生改变。
- 转移
反汇编的跳转指令中,所有跳转的位置被表示为主函数+段内偏移量这样确定的地址,而不再是段名称(例如.L3)。例如下面的je指令,反汇编文件中为:
- 函数调用
在反汇编文件中函数调用与重定位条目相对于,以printf语句为例
在call后面不再是函数名称,而是一条重定位条目指引的信息。
4.5 本章小结
本章介绍了汇编的含义和功能,并以linux下的hello.s文件为例,介绍了汇编编程hello.o文件的过程。同时,生成ELF格式文件,对文件中的每个节进行了简单解析。通过分析hello.asm与hello.s的区别,更加清楚地解释了汇编语言到机器语言的转变过程,以及为链接的准备工作。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
链接(linkng)是将各种代码和数据片段收集并组合为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行与编译时(compile time),也就是在源代码被翻译为机器代码时;也可以执行与加载时(load time),也就是程序被加载器加载到内存并执行时:甚至执行于运行时。
在现代系统中,链接是由叫做链接器(1iker)的程序自动执行的,它们使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用。
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解析ELF格式,得到hello的节信息和段信息:
- ELF头
hello.elf中的ELF头与hello.elf中的ELF头包含的信息种类基本相同。ELF头从描述生成该文件的系统的字大小和字节顺序的16字节序列Magic开始,后续部分包含帮助链接器进行语法分析和解释目标文件的信息。与hello.elf相比,hello.elf中的基本信息(如Magic和类别等)没有改变。然而,类型发生了改变,程序头大小和节头数量增加,并且获得了入口地址。
- 节头
描述了各个节的大小、偏移量和其他属性。链接器链接时,会将各个文件的相同段合并成一个大段,并且根据这个大段的大小以及偏移量重新设置各个符号的地址。
- 程序头
程序头部分是一个结构数组,描述了系统准备程序执行所需的段或其他信息。
- Dynamic section
(5) Symbol table
符号表中保存着定位、重定位程序中符号定义和引用的信息,所有重定位需要引用的符号都在其中声明。
5.4 hello的虚拟地址空间
观察程序头,可知程序头的LOAD可加载的程序段的地址为0x400000。
使用edb打开hello从Data Dump窗口观察hello加载到虚拟地址的情况,查
看各段信息。如图:
程序从地址0x400000开始到0x401000被载入,虚拟地址从0x4000000x400f0结束,根据5.3中的节头部表,可以通过edb找到各段的信息。
如.interp节,在hello.elf文件中能看到开始的虚拟地址:
在edb中找到对应的信息:
同样的,我们可以找到如.text节的信息:
5.5 链接的重定位过程分析
在Shell中使用命令objdump -d -r hello > hello1.asm生成反汇编文件hello1.asm,并与hello.asm比较,发现许多不同之处。
- 链接后函数数量增加
链接后的反汇编文件hello2.asm中,多出了.plt,puts@plt,printf@plt,getchar@plt,exit@plt,sleep@plt等函数的代码。这是因为动态链接器将共享库中hello.c用到的函数加入可执行文件中。
(2)函数调用指令call的参数发生变化
在链接过程中,链接器解析了重定位条目,call之后的字节代码被链接器直接修改为目标地址与下一条指令的地址之差,指向相应的代码段,从而得到完整的反汇编代码。
(3)跳转指令参数发生变化
在链接过程中,链接器解析了重定位条目,并计算相对距离,修改了对应位置的字节代码为PLT 中相应函数与下条指令的相对地址,从而得到完整的反汇编代码。
5.6 hello的执行流程
5.6.1过程
通过edb的调试,一步一步地记录下call命令进入的函数。
(I)开始执行:_start、_libe_start_main
(2)执行main:_main、printf、_exit、_sleep、getchar
(3)退出:exit
5.6.2子程序名或地址
程序名 程序地址
_start 0x4010f0
_libc_start_main 0x2f12271d
main 0x401125
_printf 0x4010a0
_sleep 0x4010e0
_getchar 0x4010b0
_exit 0x4010d0
5.7 Hello的动态链接分析
(以下格式自行编排,编辑时删除)
动态链接的基本思想是将程序按照模块拆分成相对独立的部分,只有在程序运行时才将这些部分链接在一起形成一个完整的程序。在调用共享库函数时,编译器无法预测该函数的运行时地址,因为定义该函数的共享模块在运行时可以被加载到任意位置。常规方法是为该引用生成一条重定位记录,由动态链接器在程序加载时进行解析。延迟绑定是通过GOT(全局偏移表)和PLT(过程链接表)实现的。根据hello.elf文件可知,GOT的起始位置为:0x404000。
GOT表位置在调用dl_init之前0x404008后的16个字节均为0:
调用了dl_init之后字节改变了:
对于变量而言,利用代码段和数据段的相对位置不变的原则去计算正确地址。
对于库函数而言,需要plt、got合作。plt初始存的是一批代码,它们跳转到got所指示的位置,然后调用链接器。初始时got里面存的都是plt的第二条指令,随后链接器修改got,下一次再调用plt时,指向的就是正确的内存地址。接下来执行程序的过程中,就可以使用过程链接表plt和全局偏移量表got进行动态链接。
5.8 本章小结
本章首先阐述了链接的基本概念和作用,展示了如何使用命令链接生成hello可执行文件,并观察了hello文件在ELF格式下的内容。接着,利用edb工具观察了hello文件的虚拟地址空间使用情况。最后,通过hello程序为例,详细分析了重定位过程、执行过程和动态链接。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
(以下格式自行编排,编辑时删除)进程的经典定义就是一个执行中程序的实例。进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。进程为程序提供了一种假象,程序好像是独占的使用处理器和内存,处理器好像是无间断地一条接一条地执行我们程序中的指令。进程作为一个执行中程序的实例,系统中每个程序都运行在某个进程的上下文中。
6.2 简述壳Shell-bash的作用与处理流程
6.2.1 Shell-bash的作用
Shell是一个交互型应用级程序,也被称为命令解析器,它为用户提供一个操作界面,接受用户输入的命令,并调度相应的应用程序。
6.2.2 Shell-bash的处理流程
首先从终端读入输入的命令,对输入的命令进行解析,如果该命令为内置命令,则立即执行命令,否则调用fork创建一个新的子进程,在该子进程的上下文中执行指定的程序。判断该程序为前台程序还是后台程序,如果为前台程序则等待程序执行结束,若为后台程序则将其放回后台并返回。在过程中shell可以接受从键盘输入的信号并对其进行处理。
6.3 Hello的fork进程创建过程
首先用户再shell界面输入指令:./hello 2022110779 金智威 18604427065 0
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 调用一次并不返回。在 main 函数运行时,用户栈的结构如下图所示:
6.5 Hello的进程执行
在程序运行时,操作系统为进程提供了以下抽象:
独立的逻辑控制流:这给人一种错觉,仿佛我们的进程独占地使用处理器。
私有的地址空间:这给人一种错觉,仿佛我们的程序独占地使用内存。
操作系统提供的抽象包括:
逻辑控制流:如果使用调试器单步执行程序,会看到一系列的程序计数器(PC)的值,这些值唯一地对应于程序的可执行目标文件或运行时动态链接到程序的共享对象中的指令。这个PC值的序列称为逻辑控制流,或简称逻辑流。当一个逻辑流的执行与另一个流在时间上重叠时,这称为并发流,这两个流被称为并发地运行。
上下文切换:操作系统内核通过一种称为上下文切换的高级形式的异常控制流来实现多任务。内核为每个进程维护一个上下文。上下文是内核重新启动一个被抢占的进程所需的状态。
时间片:一个进程执行其控制流的一部分的每一时间段称为时间片。因此,多任务也称为时间分片。
用户模式和内核模式:处理器通常使用某个控制寄存器中的一个模式位来提供这种功能。当设置了模式位时,进程运行在内核模式下。在内核模式下,进程可以执行指令集中的所有指令并访问系统中的任何内存位置。未设置模式位时,进程运行在用户模式下。在用户模式中,进程不允许执行特权指令,也不能直接引用地址空间中的内核代码和数据。
上下文信息:上下文是内核重新启动一个被抢占的进程所需的状态。它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。
在hello程序执行过程中,进程调用execve函数后,为hello程序分配新的虚拟地址空间。开始时,程序运行在用户模式下,调用printf函数输出“Hello 2022110779 金智威 18604427065”,之后调用sleep函数,进程进入内核模式,运行信号处理程序,再返回用户模式。在运行过程中,CPU不断进行上下文切换,使运行过程被分成时间片,与其他进程交替占用CPU,实现进程调度。
6.6 hello的异常与信号处理
6.6.1异常的分类
6.6.2异常的处理方式
6.6.3运行结果及相关命令
(1)程序正常运行
在程序正常运行时,打印10次信息,并以回车键为结束标志,之后回收进程
(2)运行时按下Ctrl+C
按下Ctrl+C,Shell进程收到SIGINT信号,结束并回收进程
由于sleep(0),所以仍然输出十个。
(3)运行时按下Ctrl+Z
按下Ctrl+Z,Shell进程收到SIGSTP信号,Shell显示提示信息并挂起进程。
(4)挂起进程可由ps和jobs命令查看,可以发现hello进程确实被挂起,并且job号为1。
(5)在Shell中输入pstree命令,可以将所有进程以树状图的方式显示:
(6)输入kill命令,可以杀死指定进程或进程组:
(7)输入fg 1命令可以将hello进程再次调到前台执行,可以发现Shell首先打印hello的命令行命令,hello再从挂起处继续运行,打印剩下的语句。程序仍然可以正常结束,并完成进程回收。
(8)不停乱按
在执行时乱按的输入会缓存到stdin中,当读入’\n’时结束,hello结束后,其他字符会被当作Shell的命令行输入。
6.7本章小结
本章主要探讨了计算机系统中的进程和 shell。首先,通过一个简单的 hello 程序,简要介绍了进程的概念及其作用,以及 shell 的功能和处理流程。随后,详细分析了 hello 程序的进程创建、启动和执行过程。最后,本章解释和说明了 hello 程序可能出现的异常情况及其运行结果中的各种输入。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
(以下格式自行编排,编辑时删除)
7.1.1逻辑地址
在具备地址转换功能的计算机中,访问指令给出的地址(操作数)称为逻辑地址,也叫相对地址。需要经过寻址方式的计算或转换才能得到内存中的物理地址。逻辑地址由一个段标识符和一个指定段内相对地址的偏移量组成。由 hello 程序生成的与段相关的偏移地址部分便是这种逻辑地址。
7.1.2线性地址
线性地址是逻辑地址到物理地址变换之间的一步,程序hello的代码会产生逻辑地址,在分段部件中逻辑地址是段中的偏移地址,加上基地址就是线性地址。
7.1.3虚拟地址
程序访问存储器所使用的逻辑地址称为虚拟地址。虚拟地址经过地址翻译得到物理地址。与实际物理内存容量无关,是hello中的虚拟地址
7.1.4物理地址
在存储器里以字节为单位存储信息,每一个字节单元给一个唯一的存储器地址,这个地址称为物理地址,是hello的实际地址或绝对地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段式管理指的是将一个程序分成若干个段进行存储,每个段都是一个逻辑实体。段式管理通过段表来实现,包括段号(段名)、段起点、装入位、段的长度等信息。程序通过分段被划分为多个块,例如代码段、数据段、共享段等。
一个逻辑地址由两部分组成:段标识符和段内偏移量。段标识符是一个16位长的字段,称为段选择符,其中前13位是索引号,后3位为一些硬件细节。索引号用于“段描述符”的索引,段描述符具体描述了一个段的地址,多个段描述符组成段描述符表。通过段标识符的前13位可以在段描述符表中找到一个具体的段描述符。
全局描述符表 (GDT) 整个系统只有一个,它包含:
操作系统使用的代码段、数据段、堆栈段的描述符
各任务、程序的局部描述符表 (LDT) 段
每个任务程序有一个独立的 LDT,包含:
对应任务/程序私有的代码段、数据段、堆栈段的描述符
对应任务/程序使用的门描述符:任务门、调用门等
段式管理的图示如下:
7.3 Hello的线性地址到物理地址的变换-页式管理
虚拟内存被组织为一个由存放在磁盘上的连续字节大小的单元组成的数组。虚拟内存系统将这些单元分割成虚拟页,类似地,物理内存也被分割成物理页。虚拟页通过页表来管理,页表是由多个页表条目(PTE)组成的数组。每个PTE包含一个有效位和一个地址字段。有效位表明虚拟页是否当前缓存在DRAM中,如果有效位被设置,那么地址字段就表示DRAM中相应物理页的起始位置。如果发生缺页,就需要从磁盘读取数据。
内存管理单元 (MMU) 使用页表来实现从虚拟地址到物理地址的翻译。
以下是页式管理的图示:
7.4 TLB与四级页表支持下的VA到PA的变换
Corei7 采用四级页表的层次结构。CPU 生成虚拟地址 (VA),并将其传送给内存管理单元 (MMU)。MMU 使用虚拟页号 (VPN) 的高位作为 TLB 标签 (TLBT) 和 TLB 索引 (TLBI),在 TLB 中查找匹配项。如果命中,则得到物理地址 (PA)。如果 TLB 未命中,MMU 会查询页表。
首先,控制寄存器 CR3 确定第一级页表的起始地址,VPN1 确定在第一级页表中的偏移量,查询出第一级页表条目 (PTE)。然后依次类推,通过 VPN2、VPN3 和 VPN4 在各级页表中找到相应的 PTE,最终在第四级页表中找到物理页号 (PPN),并与虚拟页偏移量 (VPO) 组合形成物理地址 (PA)。最后,MMU 将这个 PA 添加到页表缓存 (PLT)。其工作原理如下:
多级页表的工作原理展示如下:
7.5 三级Cache支持下的物理内存访问
高速缓存存储器组织结构如下:
高速缓存的结构将m个地址位划分成了t个标记位,s个组索引位和b个块偏移位:
如果选中的组存在一行有效位为1,且标记位与地址中的标记位相匹配,我们就得到了一个缓存命中,否则就称为缓存不命中。如果缓存不命中,那么它需要从存储器层次结构的下一层中取出被请求的块,然后将新的块存储在组索引位指示组中的一个高速缓存行中,具体替换哪一行取决于替换策略,例如LRU策略会替换最后一次访问时间最久远的那一行。
7.6 hello进程fork时的内存映射
当 fork 函数被当前进程调用时,内核会为新进程创建各种数据结构,并分配给它一个唯一的 PID。为了创建新进程的虚拟内存,内核会复制当前进程的 mm_struct、区域结构和页表。当 fork 在新进程中返回时,新进程的虚拟内存与调用 fork 时的虚拟内存完全相同。当这两个进程中的任何一个执行写操作时,写时复制机制 (Copy-On-Write) 会创建新页面,从而为每个进程保持私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve 函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件 hello 中的程序,有效地用 hello 程序替代了当前程序。加载并运行 hello 需要以下几个步骤:
删除已存在的用户区域:删除当前进程虚拟地址的用户部分中的已存在的区域结构。
映射私有区域:为新程序的代码、数据、.bss 和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为 hello 文件中的 .text 和 .data 区,.bss 区域是请求二进制零的,映射到匿名文件,其大小包含在 hello 中。栈和堆地址也是请求二进制零的,初始长度为零。
映射共享区域:hello 程序与共享对象 libc.so 链接,libc.so 是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
设置程序计数器:execve 最后设置当前进程上下文的程序计数器,使其指向代码区域的入口点。
如图所示:
7.8 缺页故障与缺页中断处理
如果程序执行过程中发生缺页故障,内核会调用缺页处理程序。处理程序执行如下步骤:
检查虚拟地址是否合法:如果虚拟地址不合法,内核会触发一个段错误,终止该进程。
检查权限:内核会检查进程是否具有读、写或执行该区域页面的权限。如果不具备相应权限,则会触发保护异常,程序终止。
页面交换:如果前两步检查都通过,内核会选择一个牺牲页面。如果该页面被修改过,则将其交换出去,然后换入新的页面并更新页表。最后,内核将控制权转移回 hello 进程,再次执行触发缺页故障的指令。
如图所示:
7.9动态存储分配管理
(以下格式自行编排,编辑时删除)
总的来说,动态内存分配器维护着一个进程的虚拟内存区域,称为堆(heap)。而分配器分为两种基本风格:显式分配器和隐式分配器。
而显式分配器必须在严格的约束条件下工作
必须处理任意请求序列;立即响应请求;只使用堆;对齐块;不修改已分配的块。
分配器的编写应该实现:吞吐率最大化;内存使用率最大化(两者相互冲突)。
我们需要注意这几个问题:空闲块组织方式;放置策略;分割策略;合并策略。
带边界标记的隐式空闲链表可以提高空闲块合并效率;显式空闲链表可以有效地实现空闲块的快速查找与合并等操作;分离空闲链表采用大小类的方式标记空闲块;分离适配方法快速而且内存使用效率较高。
适配块策略:首次适配或下一次适配或最佳适配。首次适配利用率较高;下一次适配时间较快;最佳适配可以很好的减少碎片的产生。我们在分离适配的时候采取的策略一般是首次适配,因为对分离空闲链表的简单首次适配的内存利用效率近似于整个堆的最佳适配的利用效率。
值得注意的是:我们的malloc就是采用的是分离适配的方法
7.10本章小结
本章主要介绍了hello程序的存储器地址空间、Intel的段式管理、hello程序的页式管理,并以Intel Core i7为例,详细介绍了虚拟地址(VA)到物理地址(PA)的转换和物理内存访问。还分析了hello进程在调用fork函数时的内存映射,以及在调用execve函数时的内存映射,最后探讨了程序在执行过程中发生缺页故障时的缺页中断处理。通过这些内容的介绍,全面解析了hello程序在不同场景下的内存管理机制。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
Linux的IO设备管理涉及以下几个方面:
设备驱动程序:设备驱动程序是Linux系统中最常见的设备管理方式之一。它是一种软件模块,负责将设备的硬件接口与操作系统之间的软件接口对接。Linux系统中的设备驱动程序分为字符设备驱动和块设备驱动两种类型。
文件系统:在Linux系统中,所有设备都被视为文件,并通过文件系统进行统一管理。文件系统不仅管理硬盘中的存储设备,还包括网络设备、USB设备等。
虚拟文件系统:虚拟文件系统是Linux系统的重要组成部分,用于管理所有文件系统的挂载点,并维护内核数据结构和缓存机制。虚拟文件系统实现了不同文件系统间的无缝切换,并提供了统一的IO操作接口,使得应用程序可以向任何文件系统请求数据。
IO调度器:IO调度器是Linux内核的一个核心模块,负责管理磁盘读写请求的顺序和优先级,以提高磁盘性能。常见的IO调度算法有CFQ、NOOP、Deadline等。
内存映射I/O:内存映射I/O是Linux系统中另一种常见的设备管理方式。它通过将磁盘上的数据直接映射到内存中,使得应用程序可以通过内存空间和磁盘数据直接交互,从而提高文件读写的效率和速度。
8.2 简述Unix IO接口及其函数
Unix IO接口是Unix操作系统提供的一组标准输入输出函数,用于实现与文件、设备、管道等IO资源的交互。这些函数包括:
打开和关闭函数:打开函数用于打开文件、设备或管道,常见的函数包括open()、creat();关闭函数用于关闭已打开的文件、设备或管道,常见的函数是close()。
读写函数:用于从文件、设备或管道中读取数据或将数据写入其中,常见的函数包括read()、write()。
文件描述符函数:用于创建、复制和操作文件描述符,常见的函数包括dup()、pipe()。
状态查询函数:用于查询已打开文件、设备或管道的状态信息,常见的函数包括fstat()、lstat()。
控制函数:用于控制文件、设备或管道的操作方式,常见的函数包括ioctl()。
此外,还有一些其他函数如文件操作函数、内存映射函数mmap()、选择函数select()和poll()等,用于提供更灵活和高效的IO操作。
8.3 printf的实现分析
在Linux系统中,printf函数和write系统调用的实现方式如下:
printf函数调用:当用户程序调用printf函数时,首先进入标准C库(libc)。该库会将要输出的内容格式化为一个字符串,并返回给用户程序。
write系统调用:用户程序得到printf函数返回的字符串后,使用write系统调用将其写入标准输出缓冲区中。write函数的定义如下:
ssize_t write(int fd, const void *buf, size_t count);
fd指定输出的文件描述符,对于标准输出,通常为1。
buf表示要输出的内容的缓冲区地址。
count表示需要输出的字节数。
系统调用机制:write函数的实际执行是通过触发系统调用来完成的。在Linux系统中,这个过程涉及到硬件和操作系统内核之间的交互,包括CPU模式的切换和内存映射等。在x86架构下,通常使用int 0x80汇编指令来触发系统调用,将控制权传递到操作系统内核中。当内核捕获到该中断信号时,程序计数器指向操作系统内核程序,引导用户态程序进入内核态。内核程序获取参数并执行相应的系统调用处理函数,如对应write函数的_syscall6函数,将要写入的数据从用户空间拷贝到内核空间,最终写入到设备文件中。
对于字符显示驱动和屏幕刷新,需要相应的驱动程序,具体如下:
字符显示驱动:在Linux内核中,字符显示驱动子程序负责将每个字符转化为对应的字模库,并将其转化成存储每个像素点RGB颜色信息的vram格式,最终向显示设备输出。这一过程的实现主要依赖于Linux内核的Video for Linux 2(V4L2)框架。V4L2是一个视频设备的抽象层,为用户程序提供了一个通用的接口,允许用户程序通过设备节点文件(如/dev/video0)向各种类型的视频设备进行读写操作。
屏幕刷新:刷新屏幕的过程则涉及到液晶显示器等设备的驱动程序。当显卡芯片从vram读取RGB分量并输出到数据端口时,液晶显示器将信号转换为亮度和色彩信号,并输出给人眼观察。显卡芯片根据预设的屏幕分辨率,逐行读取vram,并将RGB分量输出到信号线,完成对液晶显示器的控制。
8.4 getchar的实现分析
在Linux系统下,getchar函数是用于从标准输入流中获取一个字符的函数。这个函数的实现借助了read系统调用,通过读取文件描述符(stdin)的输入缓冲区来获取用户输入的字符。以下是对getchar的实现进行分析,包括异步异常-键盘中断的处理。
当用户输入字符时,会触发系统中的硬件中断,并将扫描码存储在键盘控制器中。而在Linux系统中,硬件中断被提前同步到内核空间,并由内核的键盘中断处理程序进行处理,具体的处理过程如下:
1.键盘控制器会向CPU发送一个中断请求信号,使CPU停止当前正在执行的程序,并切换到中断处理程序。
2.内核中的键盘中断处理程序会从键盘控制器中读取扫描码。
3.通过查询键码表,将扫描码转换成对应的ASCII码。
4.将转换后的ASCII码保存到系统的键盘缓冲区中。
5.通知等待用户输入的进程有新的输入可用。
接下来,当用户调用getchar时,会执行以下操作:
1.调用read系统调用以读取stdin文件描述符的输入缓冲区中的数据。
2.如果stdin文件描述符中没有数据或者数据不足,read系统调用会阻塞并等待数据的到来。
3.如果读取到了数据,就返回该数据中的第一个字符。
4.如果读取到回车键(即ASCII码为'\n'),则停止读取,并将读取到的数据返回给调用者。
8.5本章小结
(以下格式自行编排,编辑时删除)本章主要介绍了 Linux 系统下的 IO 管理,分为四个部分:
1.设备管理:介绍了 Linux 的 IO 设备管理方法,包括设备文件的创建、多路复用机制和异步 IO 等内容,以提高程序效率和系统资源利用率。
2.UNIX IO 接口:简述了 UNIX IO 接口及其函数,包括文件描述符、IO 系统调用和标准 IO 库等,为读写文件提供了便利。这些接口在不同操作系统中都有实现,有助于理解其他系统下的 IO 系统。
3.printf 函数:分析了 printf 函数的实现,包括格式化字符串解析、参数传递和输出等方面。printf 是 C 语言中常用的输出函数,能够将格式化的数据输出到标准输出流或文件中
4.getchar 函数:分析了 getchar 函数的实现,包括键盘中断处理和 read 系统调用等。getchar 是 C 语言中常用的输入函数,用于从标准输入流中读取用户输入的数据。
(第8章1分)
结论
hello程序经历的过程如下:
首先,程序员将hello代码从键盘输入,并依次经过以下步骤:
预处理(cpp):将hello.c文件进行预处理,包含文件调用的所有外部库文件被合并和展开,生成一个经过修改的hello.i文件。
编译(ccl):将hello.i文件翻译成一个包含汇编语言的文件hello.s。
汇编(as):将hello.s文件翻译成一个可重定位目标文件hello.o。
链接(ld):将hello.o文件与其他可重定位目标文件和动态链接库链接起来,生成一个可执行目标文件hello。
运行:在shell中输入./hello 2022110779 金智威 18604427065 0。
创建进程:终端判断输入的指令不是shell内置指令,于是调用fork函数创建一个新的子进程。
加载程序:shell调用execve函数,启动加载器,映射虚拟内存,进入程序入口后,程序开始载入物理内存,然后进入main函数。
执行指令:CPU为进程分配时间片,在一个时间片中,hello享有CPU资源,顺序执行自己的控制逻辑流。
访问内存:MMU将程序中使用的虚拟内存地址通过页表映射成物理地址。
信号管理:当程序在运行时输入Ctrl+C,内核会发送SIGINT信号给进程并终止前台作业。当输入Ctrl+Z时,内核会发送SIGTSTP信号给进程,并将前台作业停止挂起。
终止:当子进程执行完成时,内核安排父进程回收子进程,将子进程的退出状态传递给父进程。内核删除为该进程创建的所有数据结构。
感悟:
通过这次实验,我深切感受到计算机系统的精细和强大。每一个简单的任务都需要计算机的各种复杂操作来完成,这背后体现了严谨的逻辑和现代工艺的精巧。
论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] 林来兴. 空间控制技术[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.
(参考文献0分,缺失 -1分)