HIT 计算机系统大作业 Hello程序人生P2P

计算机系统

大作业

题 目 程序人生-Hello’s P2P

专 业 人工智能(未来技术模块)

学 号 7203610725

班 级 2036014

学 生 黄鸿睿

指 导 教 师 刘宏伟

计算机科学与技术学院

2021年5月

摘 要

hello.c这样一个简单的C语言文件在Linux下是怎样编译执行的?在开发工具封装下的一键编译执行背后隐藏着多少过程与中间产物?本文就是带着这样的疑问,一步一步深入研究了hello.c文件的P2P和020,在Ubuntu下探究了hello程序的整个生命周期,将计算机系统整个体系串联到一起,真正将计算机知识形成体系,融会贯通.

**关键词:**计算机系统;计算机体系结构;程序生命周期;底层原理;

(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)

**
**

目 录

第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简介

Hello的P2P指的是hello.c文件从C语言程序(Program)转换为进程(Process)的过程。Linux系统下,hello.c经过cpp(C Pre-Processor)C预处理器预处理,ccl(C Compiler)C编译器编译,as(Assembler)汇编器汇编,ld(Linker,链接器)链接,最后成为可执行目标程序hello。在shell内输入命令./hello通过fork产生子进程,hello就运行成为进程。

020是指hello文件从0到0的过程(Form 0 to 0)。从0开始是指初始时内存中没有hello文件的内容。其运行时从Shell下调用execve函数后,系统把hello文件载入内存,执行hello程序。运行结束后hello进程回收,内核相关数据也被删除,恢复到与hello无关,这是到0结束。

1.2 环境与工具

硬件环境:AMD Ryzen 5 4600U with Radeon Graphics 2.10 GHz

16G RAM

512G SSD

软件环境:Windows 11 64位

VMware Workstation Pro/Ubuntu 20.04 LTS 64位

开发调试工具:gedit, gcc, Visual Studio Code, edb

1.3 中间结果

文件名功能
hello.i预处理后得到的文本文件
hello.s编译后得到的汇编语言文件
hello.o汇编后得到的可重定位目标文件
hello.elf用readelf读取hello.o得到的ELF格式信息
hello.asm反汇编hello.o得到的反汇编文件
hello2.elf由hello可执行文件生成的.elf文件
hello2.asm反汇编hello可执行文件得到的反汇编文件
hello链接hello.o得到的可执行文件

1.4 本章小结

本章概述了Hello程序的P2P和020,同时介绍了大作业过程中应用的软硬件环境和开发工具,列出了中间结果文件

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

C语言标准规定,预处理是指前4个编译阶段(phases of translation)。

(1)三字符组与双字符组的替换;(2)行拼接(Line splicing): 把物理源码行(Physical source line)中的换行符转义字符处理为普通的换行符,从而把源程序处理为逻辑行的顺序集合;(3)单词化(Tokenization): 处理每行的空白、注释等,使每行成为token的顺序集。(4)扩展宏与预处理指令(directive)处理.[2]

具体而言,hello.c文件中6到8行的#include命令会使预处理器读取系统头文件stdio.h、unistd.h、stdlib.h中的内容并插入程序文本,#define命令定义的字符串则会被直接替换为实际值。此外,预处理还会删除程序中的注释和多余空白字符。预处理会得到一个.i拓展名的文件。

预处理只是进行简单的文本分割、插入和替换,方便后续的处理,而并未对源代码内容进行任何直接的解析。

2.2在Ubuntu下预处理的命令

Ubuntu系统中预处理命令为
cpp hello.c > hello.i
图1 预处理命令

2.3 Hello的预处理结果解析

用gedit打开hello.i文件,发现其行数拓展至3060行。如下图所示,hello.c文件中的main函数保留在了3047到3060行。

图2 预处理文件main函数部分

main函数前的部分是头文件stdio.h unistd.h stdlib.h的展开。展开流程为,删除#include命令,到环境变量中寻找include的文件并打开替换,将#define语句定义的字符串解释替换,删除注释和多余空白字符等。

2.4 本章小结

本章主要介绍了预处理的概念和作用,并结合Ubuntu系统下hello.c文件实际的预处理过程和预处理结果进行解析。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

编译是指C编译器(C Compiler)通过词法和语法分析,将合法指令翻译成对应的汇编代码的过程。

通过编译,编译器将文本文件hello.i翻译成汇编语言文件hello.s。hello.s中,以文本形式记录了hello.c对应的机器语言汇编代码,以方便后续转换为二进制机器码。

3.2 在Ubuntu下编译的命令

Ubuntu系统下,编译命令为:

gcc –m64 –no-pie –fno-PIC -S -o hello.i hello

截图如下:

图3 编译过程截图

3.3 Hello的编译结果解析

3.3.1 文件结构分析

hello.s文件的结构如下表所示

表格 2 hello.s文件结构

内容作用
.file源文件
.text代码段
.global全局变量
.data存放已经初始化的全局和静态C 变量
.section .rodata存放只读变量
.align对齐方式
.type表示是函数类型/对象类型
.size表示大小
.long .string表示是long类型/string类型

3.3.2 数据类型

hello.s中有有三种数据类型:整数,字符串,数组。

(1)整数

hello.s中的整数有:

① int i

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k7zwYjoO-1653021981876)(media/1f0e0974a5ed94e0b7030c3029f06f15.png)]编译器将局部变量存储在寄存器或者栈空间中。i作为函数内部的局部变量,并不占用文件实际节的空间,只存在于运行时栈中。对于i的操作就是直接对寄存器或栈进行操作。

从hello.s的情况来看,i使用了4字节的地址空间

图4 int i在hello.s中

②int argc

argc是main函数的参数之一,64位编译下,由寄存器传入,进而保存在堆栈中。

③立即数4,0,1,2,3等

在汇编语句中直接以$3的形式出现。

(2)字符串

程序中有两个字符串常量,如下图所示

图5 字符串在hello.s中

可以看出均为字符串常量,储存于.text数据段中。\XXX是UTF-8编码,一个汉字占用3个字节

(3)数组

程序中的数组只有char *argv[],main函数的第二个参数。hello.s中,其存储于栈内(21-23行),用寄存器寻址方式访问(36行)。数组操作在下一部分不再赘述。

图6 数组argv[]在hello.s中

3.3.3 数据操作

(1)赋值操作

局部变量的赋值操作使用mov指令完成,其后缀b,w,l,q分别对应1,2,3,4大小的字节

对i的赋值就如图6的31行所示

(2)算数操作

hello.s中涉及的算数操作有:

21 subq $32, %rsp //开辟栈帧

35 addq $16, %rax //修改地址偏移量

51 addl $1, -4(%rbp)//i++

(3)关系操作

hello.s中涉及的关系操作有:

① argc!=4

检查argc是否不等于3。对应的汇编代码在24行使用cmpl指令比较3和argc的大小(见图6),并设置条件码为下一步je指令进行跳转做准备。

②i<8:

检查i是否小于8。在hello.s的第53行,使用cmpl指令比较7和i的大小,设置条件码为下一步jle的跳转做准备。

图7 i<8的汇编代码

3.3.4 控制转移

程序中的控制转移有两处:

13 if(argc!=4)

当argc不等于4时,执行代码块内部的内容。hello.s中为使用24行的cmpl指令比较argc与4是否相等。相等则跳转至.L2,不执行后续代码块内部的内容;不等则继续执行。

17 for(i=0;i<8;i++)

当i<8时进行循环,每次循环i自增1。在hello.s中为使用53行的cmpl指令比较i与7是否相等,在i<=9时继续循环,进入.L4,否则跳出循环。

3.3.5 函数操作

hello.c中的函数有main(),printf(),exit(),sleep(),atoi(),getchar()等。

在hello.s中,main()函数在程序入口处被调用,被标注为@function类型。之后对其他函数的调用都经过call指令进行。如下表所示

表格 3 hello.s中各函数对应的代码

函数对应代码
14 printf()26-27行(由于仅输出一个字符串被优化为puts函数)
15 exit()28-29行,传入参数1
18 printf()34-43行,传入三个参数
19 atoi()44-48行,将argv[3]转换为int类型整数
19 sleep()49-50行,传入一个参数,为atoi()返回值
21 getchar()55行,无参数

3.4 本章小结

本章介绍了编译的概念和作用,描述了从hello.i转换为hello.s的过程。同时以hello.s为例,介绍了编译器对各种数据类型和操作的的处理方式,验证了大部分数据和操作在汇编中的实现。

(第3章2分)

第4章 汇编

4.1 汇编的概念与作用

汇编是指汇编器(assembler)将汇编程序翻译成机器语言指令,并把这些指令打包成可重定位目标程序格式,最终结果保存在.o 目标文件中的过程。

汇编能将汇编语言译为机器语言,并将相关指令以可重定位目标程序的格式保存在.o文件中。

4.2 在Ubuntu下汇编的命令

Ubuntu下汇编命令为:

gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o

汇编过程如下:

图8 汇编过程

4.3 可重定位目标elf格式

分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。

首先获得hello.o的ELF格式,输入以下命令:

readelf -a hello.o > hello.elf

过程如下图所示:

图9 获得hello.o文件的ELF格式

其结构分析如下:

1. ELF头(ELF Header):

ELF头从一个16字节序列magic开始,描述了生成该文件的系统的字的大小和字节顺序,剩下部分包含帮助链接器语法分析和解释目标文件的信息,其中包括 ELF 头大小、目标文件类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量等相关信息。

图10 ELF头

2. 节头:

包含文件中各节的意义,有节的类型、位置和大小等信息。

图11 节头

3.重定位节.rela.text

这一节中有8条重定位信息,分别是对.L0(hello.c中14行printf()中的字符串),puts()(优化后的14行printf()),exit(),.L1(18行printf()中的字符串),printf()(18行),sleep(),getchar()进行重定向声明。

图12 .rela.text节

4.重定位节.rela.eh_frame

图13 .rela.eh_frame节

5. 符号表(Symbol table)

符号表中保存有定位、重定位程序中符号定义和引用信息。所有重定位需要引用的符号都在符号表中声明。

图14 符号表

4.4 Hello.o的结果解析

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lCnHOHam-1653021981884)(media/a2a2c8b1998387de583781633f175e02.png)]使用objdump -d -r hello.o > hello.asm 命令分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。过程如下:

图15 创建hello.asm过程

图16 hello.s与hello.asm对比

对比hello.asm和hello.s,发现两者在以下三个部分存在差异

(1)分支转移

hello.s中,跳转指令目标地址直接记为段名称(.L2,.L4等),在反汇编得到的hello.asm中则变为目标地址与当前地址下一条指令的地址差。

(2)函数调用

hello.s中,call指令直接接函数名称,但在反汇编代码中,call目标地址为当前指令的下一条指令,这是因为调用的函数为共享库函数,最终要动态链接器作用才能确定函数在运行时的执行地址。

(3)全局变量访问

与函数调用类似,rodata中的数据地址在运行时才能确定,访问时需要重定位,故存储于rodata段的全局变量(hello.c中表现为printf()中的字符串)需要在汇编为机器语言时将操作数置为0并添加重定向条目。

除此之外,二者几乎完全相同,说明汇编过程不会改变代码逻辑。

4.5 本章小结

本章介绍了汇编的概念和作用。展示了在Ubuntu下将hello.s文件汇编为hello.o文件,又由hello.o文件生成其ELF格式的hello.elf文件和反汇编得到的hello.asm文件。研究了ELF格式文件的结构,通过比较hello.asm和hello.s了解了汇编语言与机器语言的异同。

(第4章1分)

第5章 链接

5.1 链接的概念与作用

链接是指通过链接器(Linker)将程序编码和数据块收集并整理成为一个单一文件,生成完全链接的可执行的目标文件的过程(Windows系统下为.exe,Linux系统下一般省略后缀名)

链接提供了一种模块化的方式,使得程序可以被编写为一个较小的源文件的集合,可以分开编译更改源文件,从而减小整体文件的复杂度与大小,增加容错性,也方便对模块进行针对性修改。

5.2 在Ubuntu下链接的命令

Ubuntu下链接的命令如下:

ld -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 /usr/lib/gcc/x86_64-linux-gnu/9/crtbegin.o /usr/lib/gcc/x86_64-linux-gnu/9/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o hello.o -lc -z relro -o hello

图17 链接过程

5.3 可执行目标文件hello的格式

输入命令生成hello程序的ELF格式文件hello2.elf(与hello.o的做区别):

readelf -a hello > hello1.elf

图18 生成hello1.elf

对hello的ELF格式的的分析如下(主要介绍与hello.elf的不同之处):

1. ELF头

hello2.elf中ELF头与hello.elf的基本相同。不同之处在于类型发生改变,程序头大小和节头数量增加,并获得了入口地址。

图19 ELF头情况

2.节头

hello2.elf的节头在链接后丰富了内容。

图20 节头情况(hello2.elf中仅截取部分)

3. 程序头

程序头部分为结构数组,描述了系统准备程序执行所需的段和其他信息。本部分为hello2.elf新增。

图21 程序头情况

4.segment to section mapping[4]

这个部分是在hello2.elf中新增的,它是在链接过程被确定的。

图22 segment to section mapping

4. Dynamic section

图23 Dynamic section

5. Symbol table

符号表中有定位和重定位程序中符号定义与引用信息。

图24 符号表(部分)

5.4 hello的虚拟地址空间

使用edb加载hello,通过Data Dump查看程序代码与其虚拟地址。

图25 Data Dump界面

可以看出,程序是从0x4010f0的位置开始的,对照5.3中图19ELF头中入口点位置,发现两者匹配。

5.5 链接的重定位过程分析

使用命令:

objdump -d -r hello > hello2.asm

将生成的hello2.asm反汇编文件与hello.asm文件比较,有以下不同之处:

1.函数数量增加

hello2.asm中增加了_init,.plt等等若干函数的代码。这是动态链接器将hello.c使用的共享库函数也加入了hello中。

图26 hello2.asm与hello.asm(部分)

2.call指令参数发生变化
链接器解析了所有重定位条目。call之后的字节地址被链接器修改为调用函数代码所在的目标地址,指向对应的代码段。下图为getchar()的例子。

图27 getchar()函数call指令的变化

3.跳转指令参数发生变化

je等指令在链接过程中,其重定向条目被链接器解析并计算完成了跳转的相对距离,指令后的代码变为跳转目标位置的字节代码。

图28 第一个if对应的je指令

5.6 hello的执行流程

使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的有过程,其调用与跳转的各个子程序名或程序地址如下表所示。

表格 4 程序名称与程序地址

程序名称程序地址
ld-2.27.so!_dl_start0x7ffe68c3a148
ld-2.27.so!_dl_init0x7fce8cc47630
hello!_start0x400500
libc-2.27.so!__libc_start_main0x7fce8c867ab0
-libc-2.27.so!__cxa_atexit0x7fce8c889430
-libc-2.27.so!__libc_csu_init0x4005c0
hello!_init0x400488
libc-2.27.so!_setjmp0x7fce8c884c10
-libc-2.27.so!_sigsetjmp0x7fce8c884b70
–libc-2.27.so!__sigjmp_save0x7fce8c884bd0
hello!main0x400532
hello!puts@plt0x4004b0
hello!exit@plt0x4004e0
*hello!printf@plt
*hello!sleep@plt
*hello!getchar@plt
ld-2.27.so!_dl_runtime_resolve_xsave0x7fce8cc4e680
-ld-2.27.so!_dl_fixup0x7fce8cc46df0
–ld-2.27.so!_dl_lookup_symbol_x0x7fce8cc420b0
libc-2.27.so!exit0x7fce8c889128

5.7 Hello的动态链接分析

由于动态链接器对共享库函数的调用进行了延迟绑定,而延迟绑定是同通过GOT(全局偏移量表)和PLT(过程链接表)实现的。查询hello2.elf,找到GOT起始位置在0x404000

图29 hello2.elf中的.got

查看edb的Data Dump,在调用dl_init前,其内容如下

图30 调用前

调用后内容变为:

图31 调用后

比较其中变化可以发现,GOT中的内容发生了变化。在执行完dl_init后程序就可以从PLT和GOT进行动态链接了。

5.8 本章小结

本章先介绍了链接的概念与作用,并比较了将hello.o进行连接后得到的hello程序的ELF格式文件和反汇编文件与链接前的相同与不同之处,加深了对重定位和动态链接的理解。

(第5章1分)

第6章 hello进程管理

6.1 进程的概念与作用

进程是一个正在运行的程序实例,系统中的每个程序都运行在某个进程的上下文中。

进程能给应用程序提供两个关键抽象:

①一个独立的逻辑控制流,提供一个程序独占使用处理器的假象。

②一个私有地址空间,提供一个程序独占使用内存的假象。

6.2 简述壳Shell-bash的作用与处理流程

Shell是一个由C语言编写的交互型应用程序,它代表用户运行程序。同时Shell提供一个界面,使得用户能够对系统进行基本操作,访问操作系统内核的服务。

Shell处理流程如下图所示:

图6.2.1 Shell的处理流程图[5]

6.3 Hello的fork进程创建过程

fork进程的创建过程如下[6]:

(1)给新进程分配一个标识符

(2)在内核中分配一个PCB,将其挂在PCB表上

(3)复制它的父进程的环境(PCB中大部分的内容)

(4)为其分配资源(程序、数据、栈等)

(5)复制父进程地址空间里的内容(代码共享,数据写时拷贝)

(6)将进程置成就绪状态,并将其放入就绪队列,等待CPU调度。

打开Shell,输入命令./hello 7203610725 黄鸿睿 5,带参数执行之前生成的可执行文件。

图6.3.1 hello的执行情况

6.4 Hello的execve过程

execve() 系统调用的作用是运行另外一个指定的程序。它会把新程序加载到当前进程的内存空间内,当前的进程会被丢弃,它的堆、栈和所有的段数据都会被新进程相应的部分代替,然后会从新程序的初始化代码和 main()开始运行。同时,进程的 ID 将保持不变。在hello这里,这个过程表现为用fork()创建新的子进程之后,子进程调用execve(),在当前进程的上下文中加载并运行一个新程序hello。execve ()没有返回值,它将删除该进程的代码和地址空间内的内容并将其初始化,然后通过跳转到程序的第一条指令或入口点来运行该程序。将私有的区域映射进来,例如打开的文件,代码、数据段,然后将公共的区域映射进来。后面加载器跳转到程序的入口点,即设置PC指向_start 地址。_start函数最终调用hello中的 main (),这样,便完成了在子进程中的加载。[7]

6.5 Hello的进程执行

hello运行时,Shell为hello程序fork了一个子进程,这个子进程与Shell的逻辑控制流是独立的。若hello进程不被抢占,其正常运行;若被抢占则进入内核模式,进行上下文切换转入用户模式调度其他进程。当hello调用sleep函数时,为最大化利用处理器资源,此时sleep函数会像内核发送请求将hello挂起并进行上下文切换,进入内核模式切换其他进程,切换回用户模式运行抢占的进程。同时,将hello进程从运行队列加入等待队列,由用户模式变为内核模式,开始计时。计时结束后返回,触发中断,使hello进程被重新调度,从等待队列中移除,由内核模式转为用户模式,hello进程可以继续执行其逻辑控制流。

6.6 hello的异常与信号处理

6.6.1 程序正常运行

输入参数不足4个时,程序会打印提示信息并直接结束程序。输入参数符合要求时,程序会打印8次提示信息,并以输入回车结束程序回收进程。

图6.6.1 hello正常运行

6.6.2 程序运行时按回车

程序会在运行中打印空行,并在输入完成后直接结束。这是因为运行时按的回车也加载进了stdin流,作为最后结束程序的标志被程序接受。

图6.6.2 hello运行中输入回车

6.6.3 程序运行时按Ctrl+C

此时shell收到SIGINT信号,结束并回收hello进程。

图6.6.3 hello运行时输入Ctrl+C

6.6.4 程序运行时按Ctrl+Z

此时Shell收到SIGSTP信号,显示屏幕提示信息并挂起hello进程。

图6.6.4.1 hello运行时按Ctrl+Z

对hello进程的挂起可以用ps和jobs命令查看,发现其确为挂起而非被回收,且其job代号为1

图6.6.4.2 用ps和jobs命令查看

可用pstree用树状图显示所有进程。

图6.6.4.3 用pstree命令查看

输入kill命令可杀死指定进程

图6.6.4.4 用kill指令杀死进程

输入fg 1则可以将hello进程再次调到前台执行,hell首先打印命令行命令,hello再从挂起处继续运行,打印剩下的语句.程序可以正常结束并回收.

图6.6.4.5 用fg命令调回前台

6.6.5 不停乱按

程序运行时的输入均缓存到stdin流中,getchar时读入出一个’\n’结尾的字符串作为一次输入,hello结束后,stdin流中其他字符会当作Shell 的命令读入.

图6.6.5 不停乱按

6.7本章小结

本章介绍了进程的概念与作用,也介绍了Shell的基本概念和作用.在本章中,以hello为例研究了fork,execve函数的原理与执行过程,并给出了hello带参执行情况下各种异常与信号处理的结果.

(第6章1分)

第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址是指由程序产生的与段相关的偏移地址部分,逻辑地址由选择符和偏移量两部分组成。具体而言,其为hello.asm中的相对偏移地址。逻辑地址经过段机制转化后为线性地址,其为处理器可寻址空间的地址,用于描述程序分页信息的地址.具体以hello而言,线性地址标志着hello应在内存上哪些具体数据块上运行.虚拟地址即为上述线性地址.物理地址则是CPU通过地址总线寻址找到的真实物理内存对应地址.

7.2 Intel逻辑地址到线性地址的变换-段式管理

Intel处理器从逻辑地址到线性地址的变换通过段式管理的方式实现。每个程序在系统中都保存着一个段表,段表保存着该程序各段装入主存的状况信息,包括段号或段名、段起点、装入位、段的长度、主存占用区域表、主存可用区域表等,从而方便进行段式管理。

在段寄存器中,存放着段选择符,可以通过段选择符来得到对应段首地址。段选择符的结构如下:

图7.2 段选择符的情况

其包含三部分:索引,TI,RPL

索引:用来确定当前使用的段描述符在描述符表中的位置;

TI:根据TI的值判断选择全局描述符表(TI=0,GDT)或选择局部描述符表(TI=1,LDT);

RPL:判断重要等级。RPL=00,为第0级,位于最高级的内核,RPL=11,为第3级,位于最低级的用户状态;

通过一个索引,可以定位到段描述符,进而通过段描述符得到段基址。段基址与偏移量结合就得到了线性地址,虚拟地址。[8]

7.3 Hello的线性地址到物理地址的变换-页式管理

线性地址(VA)到物理地址(PA)之间的转换通过对虚拟地址内存空间进行分页的分页机制完成。

通过7.2节中的段式管理过程,可以得到了线性地址/虚拟地址,记为VA。虚拟地址可被分为两个部分:VPN(虚拟页号)和VPO(虚拟页偏移量),根据计算机系统的特性可以确定VPN与VPO的具体位数,由于虚拟内存与物理内存的页大小相同,因此VPO与PPO(物理页偏移量)一致。而PPN(物理页号)则需通过访问页表中的页表条目(PTE)获取,如下图所示。

图7.3 Hello的线性地址到物理地址的变换-页式管理

若PTE的有效位为1,则发生页命中,可以直接获取到物理页号PPN,PPN与PPO共同组成物理地址。

若PTE的有效位为0,说明对应虚拟页没有缓存到物理内存中,产生缺页故障,调用操作系统的内核的缺页处理程序,确定牺牲页,并调入新的页面。再返回到原来的进程,再次调用导致缺页的指令。此时发生页命中,获取到PPN,与PPO共同组成物理地址。

7.4 TLB与四级页表支持下的VA到PA的变换

编写本文设备的CPU的基本参数如下:

  1. 虚拟地址空间48位(n=48)
  2. 物理地址空间52位(m=52)
  3. TLB四路十六组相连
  4. L1,L2,L3块大小为64字节
  5. L1,L2八路组相连
  6. L3十六路组相连
  7. 页表大小4KB(P=4x1024=2^12),四级页表,页表条目(PTE)大小8字节

由上述信息可以得知,VPO与PPO有p=12位,故VPN为36位,PPN为40位。单个页表大小4KB,PTE大小8字节,则单个页表有512个页表条目,需要9位二进制进行索引,而四级页表则需要36位二进制进行索引,对应着36位的VPN。TLB有16组,故TLBI有t=4位,TLBT有36-4=32位。

图7.4.1 TLB与四级页表支持下的VA到PA的变换

如图所示, CPU产生虚拟地址VA,并将其传送至MMU,MMU使用前36位VPN作为TLBT(前32位)+TLBI(后4位)在TLB中进行匹配,若命中,则得到PPN(40bit)与VPO(12bit)组合成物理地址PA(52bit)。若TLB没有命中,则MMU向页表中查询,由CR3确定第一级页表的起始地址,VPN1(9bit)确定在第一级页表中的偏移量,查询出PTE,如果在物理内存中且权限符合,则执行下一步确定第二级页表的起始地址,以此类推,最终在第四级页表中查询到PPN,与VPO组合成PA,并向TLB中添加条目。多级页表的工作原理展示如下:

图7.4.2 多级页表工作原理

若查询PTE的时候发现不在物理内存中,则引发缺页故障。如果发现权限不够,则引发段错误。

7.5 三级Cache支持下的物理内存访问

因为三级Cache的工作原理基本相同,所以在这里以L1 Cache为例,介绍三级Cache支持下的物理内存访问。

L1 Cache的基本参数如下:

  1. 8路64组相连
  2. 块大小64字节

由L1 Cache的基本参数,可以分析知:

块大小64字节→需要6位二进制索引→块偏移6位

共64组→需要6位二进制索引→组索引6位

余下标记位→需要PPN+PPO-6-6=40位

故L1 Cache可被划分如下(从左到右):

CT(40bit)CI(6bit)CO(6bit)

在7.4中我们已经由虚拟地址VA转换得到了物理地址PA,首先使用CI进行组索引,每组8路,对8路的块分别匹配CT(前40位)如果匹配成功且块的valid标志位为1,则命中(hit),根据数据偏移量CO取出相应的数据后返回。

若没有匹配成功或者匹配成功但是标志位是1,则不命中(miss),向下一级缓存中请求数据(请求顺序为L2 Cache→L3 Cache→主存,若仍不命中才继续向下一级请求)。查询到数据之后,需要对数据进行读入,一种简单的放置策略如下:若映射到的组内有空闲块,则直接放置在空闲块中,若当前组内没有空闲块,则产生冲突(evict),采用LFU策略进行替换。

7.6 hello进程fork时的内存映射

当fork函数被当前进程hello调用时,内核为新进程hello创建各种数据结构,并分配唯一PID。为给hello创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。

当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当两个进程中的任一个进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。

7.7 hello进程execve时的内存映射

execve函数加载并运行hello需要以下几个步骤:

  1. 删除已存在的用户区域

    删除当前进程hello虚拟地址的用户部分中的已存在的区域结构。

  2. 映射私有区域

    为新程序的代码、数据、bss和栈区域创建新的私有的、写时复制的区域结构。其中,代码和数据区域被映射为hello文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。

  3. 映射共享区域

    若hello程序与共享对象或目标(如标准C库libc.so)链接,则将这些对象动态链接到hello程序,然后再映射到用户虚拟地址空间中的共享区域内。

  4. 设置程序计数器

    最后,execve设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。

7.8 缺页故障与缺页中断处理

(1)段错误:首先判断这个缺页的虚拟地址是否合法,遍历所有的合法区域结构,如果这个虚拟地址对所有的区域结构都无法匹配,就返回一个段错误。

(2)非法访问:查看地址的权限,判断一下进程是否有读写改这个地址的权限。

(3)如果不是上面两种情况那就是正常缺页,就选择一个页面换入新的页面并更新到页表

7.9动态存储分配管理

动态内存管理的基本方法与策略介绍如下:

动态内存分配器维护着一个称为堆的进程的虚拟内存区域。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放可以由应用程序显式执行或内存分配器自身隐式执行。

具体而言,分配器分为两种基本风格:显式分配器、隐式分配器。

显式分配器:要求应用显式地释放任何已分配的块。

隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。

下面介绍动态存储分配管理中较为重要的概念:

  1. 隐式链表

    堆中的空闲块通过头部中的大小字段隐含地连接,分配器通过遍历堆中所有的块,从而间接遍历整个空闲块的集合。

    对于隐式链表,其结构如下:
    图7.9.1 隐式链表的结构

  2. 显式链表

    在每个空闲块中,都包含一个前驱(pred)与后继(succ)指针,从而减少了搜索与适配的时间。

    显式链表的结构如下:

图7.9.2 显式链表的结构

  1. 带边界标记的合并

    采取使用边界标记的堆块的格式,在堆块的末尾为其添加一个脚部,其为头部的副本。添加脚部之后,分配器就可以通过检查前面一个块的脚部,判断前面一个块的起始位置和状态。从而实现快速合并,减小性能消耗。

  2. 分离存储

    维护多个空闲链表,其中,每个链表的块具有相同的大小。将所有可能的块大小分成一些等价类,从而进行分离存储。

7.10本章小结

本章主要介绍了hello 的存储器地址空间、intel 的段式管理、hello 的页式管理, VA 到PA 的变换、物理内存访问,hello进程fork、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理.全面加深了对计算机系统存储管理的了解.

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

设备管理:unix io接口

IO设备均被模型化为文件,输入输出便成为对应文件的读写.这种方式使得Linux能简单地管理设备

8.2 简述Unix IO接口及其函数

  1. Unix I/O接口:

  2. 打开文件

    一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。对于Shell创建的每个进程,其都有三个打开的文件:标准输入,标准输出,标准错误。

  3. 改变当前的文件位置

    对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。

  4. 读写文件

    一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。

  5. 关闭文件

    内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去

  6. Unix I/O函数:

  7. int open(char* filename,int flags,mode_t mode)

    进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。

  8. int close(fd)

    fd是需要关闭的文件的描述符,close返回操作结果。

  9. ssize_t read(int fd,void *buf,size_t n)

    read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。

  10. ssize_t wirte(int fd,const void *buf,size_t n)

    write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。

8.3 printf的实现分析

查看windows系统下的printf函数体:

图 53 printf的函数体

形参列表中的…是可变形参的一种写法,当传递参数的个数不确定时,用这种方式来表示。

va_list的定义:typedef char *va_list,说明它是一个字符指针,其中 (char*)(&fmt) + 4) 即arg表示的是…中的第一个参数。

再进一步查看windows系统下的vsprintf函数体:

图 54 vsprintf的函数体

则知道vsprintf程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。

在printf中调用系统函数write(buf,i)将长度为i的buf输出。write函数如下:

printf函数的功能为接受一个格式化命令,并按指定的匹配的参数格式化输出,故i = vsprintf(buf, fmt, arg)是得到打印出来的字符串长度,其后的write(buf, i)是将buf中的i个元素写到终端。

因此,vsprintf的作用为接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,进而产生格式化输出。

再进一步对write进行追踪:

图 55 write的情况

这里给几个寄存器传递了参数,然后以一个int INT_VECTOR_SYS_CALL结束。INT_VECTOR_SYS_CALL代表通过系统调用syscall,查看syscall的实现:

图 56 syscall的情况

syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码,符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)

8.4 getchar的实现分析

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。

getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

getchar调用系统函数read,发送一个中断信号,内核抢占这个进程,用户输入字符串,键入回车后(字符串和回车都保存在缓冲区内),再次发送信号,内核重新调度这个进程,getchar从缓冲区读入字符。

8.5本章小结

本章主要介绍了Linux系统的IO设备管理方法及其接口和函数,也了解了printf()和getchar()的底层实现.

(第8章1分)

**
**

结论

hello程序的一生经历了如下过程:

  1. 预处理

    将hello.c中include的所有外部的头文件头文件内容直接插入程序文本中,完成字符串的替换,并删除多余空白字符得到hello.i供后续处理;

  2. 编译

    通过词法分析和语法分析,将合法指令翻译成等价汇编代码。通过编译过程,编译器将hello.i 翻译成汇编语言文件 hello.s;

  3. 汇编

    将hello.s汇编程序翻译成机器语言指令,并把这些指令打包成可重定位目标程序格式,最终结果保存在hello.o 目标文件中;

  4. 链接

    通过链接器,将hello的程序编码与动态链接库等收集整理成为一个单一文件,生成完全链接的可执行的目标文件hello;

  5. 加载运行

    打开Shell,在其中键入 ./hello 7203610725 黄鸿睿,终端为其fork新建进程,并通过execve把代码和数据加载入虚拟内存空间,程序开始执行;

  6. 执行指令

    在该进程被调度时,CPU为hello其分配时间片,在一个时间片中,hello享有CPU全部资源,PC寄存器一步一步地更新,CPU不断地取指,顺序执行自己的控制逻辑流;

  7. 访存

    内存管理单元MMU将逻辑地址,一步步映射成物理地址,进而通过三级高速缓存系统访问物理内存/磁盘中的数据;

  8. 动态申请内存

    printf 会调用malloc 向动态内存分配器申请堆中的内存;

  9. 信号处理

    进程时刻等待着信号,如果运行途中键入Ctrl+C/Ctrl+Z 则调用shell 的信号处理函数分别进行停止、挂起等操作,对于其他信号也有相应的操作;

  10. 终止并被回收

    Shell父进程等待并回收hello子进程,内核删除为hello进程创建的所有数据结构。

(结论0分,缺失 -1分,根据内容酌情加分)

附件

文件名功能
hello.i预处理后得到的文本文件
hello.s编译后得到的汇编语言文件
hello.o汇编后得到的可重定位目标文件
hello.elf用readelf读取hello.o得到的ELF格式信息
hello.asm反汇编hello.o得到的反汇编文件
hello2.elf由hello可执行文件生成的.elf文件
hello2.asm反汇编hello可执行文件得到的反汇编文件
hello链接hello.o得到的可执行文件

(附件0分,缺失 -1分)

参考文献

为完成本次大作业你翻阅的书籍与网站等

[1] 兰德尔 E.布莱恩特. 深入理解计算机系统[M]. 北京:机械工业出版社,2018.

[2] C预处理器 - 维基百科,自由的百科全书 (wikipedia.org)

[3] ELF-64 Object File Format (uclibc.org)

[4] From .rodata to .rwdata – introduction to memory mapping and LD scripts – Guy on BITS

[5] bash处理的12个步骤流程图_AstrayLinux的博客-CSDN博客

[6] 进程的创建过程(fork函数)_lyl194458的博客-CSDN博客_fork创建进程

[7] fork和execve和Linux内核的一般执行过程 - 知乎 (zhihu.com)

[8] 段页式访存——逻辑地址到线性地址的转换 - 简书 (jianshu.com)

(参考文献0分,缺失 -1分)
文件 |
| hello2.elf | 由hello可执行文件生成的.elf文件 |
| hello2.asm | 反汇编hello可执行文件得到的反汇编文件 |
| hello | 链接hello.o得到的可执行文件 |

(附件0分,缺失 -1分)

参考文献

为完成本次大作业你翻阅的书籍与网站等

[1] 兰德尔 E.布莱恩特. 深入理解计算机系统[M]. 北京:机械工业出版社,2018.

[2] C预处理器 - 维基百科,自由的百科全书 (wikipedia.org)

[3] ELF-64 Object File Format (uclibc.org)

[4] From .rodata to .rwdata – introduction to memory mapping and LD scripts – Guy on BITS

[5] bash处理的12个步骤流程图_AstrayLinux的博客-CSDN博客

[6] 进程的创建过程(fork函数)_lyl194458的博客-CSDN博客_fork创建进程

[7] fork和execve和Linux内核的一般执行过程 - 知乎 (zhihu.com)

[8] 段页式访存——逻辑地址到线性地址的转换 - 简书 (jianshu.com)

(参考文献0分,缺失 -1分)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值