计算机系统

大作业

题 目 程序人生-Hello’s P2P

专 业 计算学部

学   号 120L021801

班   级 2003006

学 生 郑卓  

指 导 教 师 吴锐

计算机科学与技术学院

2021年5月

摘 要

经过了计算机系统这一门课程的学习,我们系统的了解了一段程序在计算机中经历的种种过程。本文章依据课程内容,从计算机底层角度,以hello.c为例,沿着这段代码在linux系统下从产生到停止的过程逐步深入,展示一段代码的“一生”。

关键词:预处理;编译;汇编;链接;进程管理;存储管理;IO管理;

目 录

第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.c P2PProgram to Process)过程包括

Hello的P2P是指hello.c文件从可执行程序(Program)变为运行时进程(Process)的过程。Hello的生命周期开始于文件hello.c中,源程序hello.c首先进预处理器行cpp的预处理,结果是生成一个hello.i文件;这个hello.i文件又经过编译器cc1的编译过程,得到了hello.s汇编文件;hello.s文件经过汇编器as的处理得到了可重定位目标文件hello.o;连接器ld将可重定位目标文件hello.o和printf.o等链接,生成二进制可执行文件hello。

然后用户可以在shell中输入./hello和参数的指令运行hello,shell通过fork函数创建一个子进程。Hello的这个从程序(program)到进程(process)的过程就是P2P过程。

Hello.c 的020(Zero-0 to Zero-0)过程:

Hello的020是指hello.c文件“From 0 to 0”,初始时内存中并无hello文件的相关内容,这便是“From 0”。Shell通过execve加载并执行上面提到的这个进程,由内核为hello进程映射虚拟内存。在hello进入程序入口后,hello相关的数据就被内核加载到物理内存中,hello程序被执行。CPU为hello分配时间片、逻辑控制流。随后进程终止,hello成为僵尸进程,由shell回收进程,操作系统释放虚拟空间,这即为“to 0”。hello进程从0又回到0的这个过程就是020过程。

1.2 环境与工具

硬件环境:

X64 CPU;2GHz;2G RAM;256GHD Disk 以上

软件环境:

Windows7/10 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位/优麒麟 64位 以上;

开发与调试工具:

GCC,GDB,Objdump,vi/vim/gedit+gcc

1.3 中间结果

hello.c

Hello的源程序

hello.i

预处理生成的文件

hello.s

编译之后的汇编文件

hello.o

汇编之后的可重定位目标执行文件

hello.elf

hello.o 的 ELF 格式

Hello

hello 链接之后的可执行文件

ams.txt

Hello.o 的反汇编代码文件

hello1.elf

由hello可执行文件生成的.elf文件

表 1 中间结果

1.4 本章小结

本章总体上阐述了一个c程序的生命周期,将其分为两个部分——从源代码到被执行的进程,和最后运行结束被清回收的P2P020过程。随后又介绍了实验所需要的硬件环境和软件环境以及开发调试工具,展示了本次实验的中间结果与说明。

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

概念:程序设计领域中,预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。由预处理器(preprocessor) 对程序源代码文本进行处理,得到的结果再由编译器核心进一步编译。这个过程把源代码分割或处理成为特定的单位预处理记号(preprocessing token)用来支持语言特性(。

作用:预处理指令一般被用来使源代码在不同的执行环境中被方便的修改或者编译。将#include后继的头文件内容插入程序文本中,完成字符串的替换。预处理过程中并未直接解析程序源代码的内容,而是对源代码进行相应的分割、处理和替换。简单来说,预处理是一个文本插入与替换的过程,生成的hello.i文件仍然是文本文件。

2.2在Ubuntu下预处理的命令

在Ubuntu系统下,进行预处理的命令为:

gcc –E hello.c –o hello.i

图 1 预处理指令

2.3 Hello的预处理结果解析

图 2 预处理结果

观察生成的hello.i文件 ,发现该文件的3046行后的文件与hello.c相同,hello.i 文件是在原有代码的基础上,引入了3046行的头文件,声明函数,定义结构体,定义变量,定义宏等内容。CPP先删除指令#include ,并到Ubuntu系统的默认的环境变量中寻找头文件,最终打开路径/usr/include/下的某个头文件文件。若文件中使用了#define语句,则按照上述流程继续递归地展开,直到所有#define语句都被解释替换掉为止。除此之外,CPP还会进行删除程序中的注释和多余的空白字符等操作,并对一些值进行替换。

2.4 本章小结

本章节介绍了预处理的概念和作用,介绍了Ubuntu下预处理的指令,对hello.c执行预处理,生成了hello.i文件。观察了hello.i文件,发现了预处理的本质是扫描c语言的头文件,声明函数,定义结构体,定义变量,定义宏等内容,对其进行展开得到一个仍可以阅读的 C 语言程序文本文件。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

概念:利用编译程序通过词法分析和语法分析,从源语言编写的源程序产生目标程序的过程。

作用:编译就是把高级语言变成计算机可以识别的机器语言。 编译程序把一个源程序翻译成目标程序的工作过程分为五个阶段:词法分析;语法分析;语义检查和中间代码生成;代码优化;目标代码生成。主要是进行词法分析和语法分析,又称为源程序分析,分析过程中发现有语法错误,给出提示信息。

3.2 在Ubuntu下编译的命令

Ubuntu系统下,进行预处理的命令为:

gcc –S hello.i –o hello.s

图 3 编译指令

3.3 Hello的编译结果解析

图 4 hello编译结果

3.3.1 数据

(1)常量

图 5 字符串

如图所示printf打印店字符串被存放在LC0和LC1中

(2)变量

图 6 传递参数

主函数中传递进来的第一个参数argc存放在寄存器%edi 第二个参数argv[]存放在寄存器%rsi

图 7 当作计数器的常数i

给for循环的计数器i赋初值0

3)算数操作

图 8 用来开辟栈帧

在hello.s中,开辟栈帧需要用到算数操作改变栈顶的位置。

图 9 i++

将上面提到的计数器i++

4)关系操作

Hello.c中cmpl 和 jmp的结构用于表示if语句

图 10 比较

根距条件码确定关系,分情况进行跳转。

(5)跳转

同上,Hello.c中cmpl 和 jmp的结构用于表示if语句根距条件码确定关系,分情况进行跳转。

图 11 12 两处跳转指令

(6)函数调用

图13 调用exit函数

图 14 调用sleep函数

图 15 调用printf函数

图16 main函数

程序入口处,调用了main 函数,其在hello.s中标注为@function函数类型。之后又调用 puts,printf,sleep,exit,getchar 函数,对函数的调用都通过call指令进行。

3.4 本章小结

本章主要讲述了编译阶段中编译器如何处理各种数据和操作,以及c语言中各种类型和操作所对应的的汇编代码的实现。以hello.s文件为例,介绍了编译器如何处理各个数据类型以及各类操作。

(第32分)

第4章 汇编

4.1 汇编的概念与作用

将之前得到的hello.s翻译成机器语言hello.o的过程称为汇编,同时这个机器语言文件也是可重定位目标文件。

作用:将汇编代码转为在链接后能被机器识别并执行的机器指令,并将相关指令以可重定位目标程序格式保存在.o文件中。

4.2 在Ubuntu下汇编的命令

在Ubuntu下汇编的命令为:

as hello.s -o hello.o

图 17 汇编指令

4.3 可重定位目标elf格式

图 18 ELF文件

ELF头:包含了系统信息,编码方式,ELF64版本,节的大小和数量,程序头起点,入口点地址等信息。

图 19 节头

节头:包含了各个节的名称、大小、类型、地址、偏移量等信息

图 20 .rela.text节

重定位节:描述了需要进行重定位的各种信息,包括需要进行重定位符号的位置、重定位的方式、名字。

图 21 符号表

符号表:包含在程序中定义和引用的函数和全局变量的信息

4.4 Hello.o的结果解析

使用objdump -d -r hello.o > hello.asm 分析hello.o的反汇编,并与第3章的 hello.s文件进行对照分析。

Disassembly of section .text:

0000000000000000

:

0: f3 0f 1e faendbr64

4: 55push %rbp

5: 48 89 e5mov %rsp,%rbp

8: 48 83 ec 20sub $0x20,%rsp

c: 89 7d ecmov %edi,-0x14(%rbp)

f: 48 89 75 e0mov %rsi,-0x20(%rbp)

13: 83 7d ec 04cmpl $0x4,-0x14(%rbp)

17: 74 16 je 2f

19: 48 8d 3d 00 00 00 00lea 0x0(%rip),%rdi # 20

1c: R_X86_64_PC32 .rodata-0x4

20: e8 00 00 00 00callq 25

21: R_X86_64_PLT32 puts-0x4

25: bf 01 00 00 00mov $0x1,%edi

2a: e8 00 00 00 00callq 2f

2b: R_X86_64_PLT32 exit-0x4

2f: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)

36: eb 48 jmp 80

38: 48 8b 45 e0mov -0x20(%rbp),%rax

3c: 48 83 c0 10add $0x10,%rax

40: 48 8b 10mov (%rax),%rdx

43: 48 8b 45 e0mov -0x20(%rbp),%rax

47: 48 83 c0 08add $0x8,%rax

4b: 48 8b 00mov (%rax),%rax

4e: 48 89 c6mov %rax,%rsi

51: 48 8d 3d 00 00 00 00lea 0x0(%rip),%rdi # 58

54: R_X86_64_PC32 .rodata+0x22

58: b8 00 00 00 00mov $0x0,%eax

5d: e8 00 00 00 00callq 62

5e: R_X86_64_PLT32 printf-0x4

62: 48 8b 45 e0mov -0x20(%rbp),%rax

66: 48 83 c0 18add $0x18,%rax

6a: 48 8b 00mov (%rax),%rax

6d: 48 89 c7mov %rax,%rdi

70: e8 00 00 00 00callq 75

71: R_X86_64_PLT32 atoi-0x4

75: 89 c7 mov %eax,%edi

77: e8 00 00 00 00callq 7c

78: R_X86_64_PLT32 sleep-0x4

7c: 83 45 fc 01addl $0x1,-0x4(%rbp)

80: 83 7d fc 07cmpl $0x7,-0x4(%rbp)

84: 7e b2 jle 38

86: e8 00 00 00 00callq 8b

87: R_X86_64_PLT32 getchar-0x4

8b: b8 00 00 00 00mov $0x0,%eax

90: c9 leaveq

91: c3 retq

数的进制:hello.s中的操作数按十进制,hello.o反汇编代码中的操作数都是0x的十六进制。

跳转和调用: hello.s中的跳转是.L4等段名称,汇编代码中跳转是相对偏移的地址。Hello.s的调用函数是call接函数名,hello.ocall接地址。

4.5 本章小结

通过将hello.s汇编指令转换成hello.o机器指令,通过readelf查看hello.o的ELF、反汇编的方式查看hello.o反汇编的内容,比较了与hello.s之间的差别。

(第41分)

第5章 链接

5.1 链接的概念与作用

概念:链接是将各种不同文件的代码和数据部分收集(符号解析和重定位)起来并组合生成完全链接的可执行的目标文件的过程。

作用:链接过程将多个可重定位目标文件合并以生成可执行目标文件。提供了一种模块化的方式,可以将程序编写为一个较小的源文件的集合,且实现了分开编译更改源文件,从而减少整体文件的复杂度与大小,增加容错性,也方便对某一模块进行针对性修改。

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

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

在Shell中输入命令 readelf -a hello > hello2.elf 生成 hello 程序的 ELF 格式文件,保存为hello1.elf(与第四章中的elf文件作区分):

图 22 ELF文件

ELF头:包含了系统信息,编码方式,ELF64版本,节的大小和数量,程序头起点,入口点地址等信息。与hello.elf相比较,hello1.elf中的基本信息未发生改变(如Magic,类别等),而类型发生改变,程序头大小和节头数量增加,并且获得了入口地址。

图 23 节头

节头:包含了各个节的名称、大小、类型、地址、偏移量等信息

图 24 程序头

程序头:提供了各段在虚拟地址空间和物理地址空间的位置、大小、标志、访问授权和对齐方面的信息。

5.4 hello的虚拟地址空间

使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。

图 25 加载到虚拟地址的程序代码

程序被载入至地址0x400000~0x401000中。在该地址范围内,每个节的地址都与前一节中节对应的 Address相同。在0x400fff之后存放的是.dynamic到.shstrtab节的内容。

5.5 链接的重定位过程分析

objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。

图 26 比较hello与hello.o

链接后增加了hello中导入的共享库中的函数如.plt,puts@plt,printf@plt,getchar@plt,exit@plt,sleep@plt等。这是因为动态链接器在连接过程中将共享库中hello.c用到的函数加入可执行文件中。此外,地址发生变化。在链接过程中,链接器解析重定位条目,并计算相对距离,从而得到实际的虚拟地址。

重定位:链接器将所有相同类型的节合并为一个节,然后链接器将运行时内存地址付给新的节和输入模块定义的每个符号,然后链接器利用可重定位条目,修改代码节和数据节中对符号的引用。

5.6 hello的执行流程

表 2 程序名称与程序地址

0x00007fde445b90b3

libc-2.31.so!__libc_start_main

004011d6

hello.out!main

00401090

hello.out!.plt

00401030

hello.out!puts@plt

00401020

hello.out!.plt

00007ff7:8f4a945e

libc-2.31.so!puts

00007ff7:8f4b4600

libc-2.31.so!_IO_file_xsputn

5.7 Hello的动态链接分析

分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。

编译器是没有办法预测函数的运行时实际的绝对地址的,所以需要添加重定位记录,等待动态链接器处理,为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表PLT和全局偏移量表 GOT 实现函数的动态链接,在 GOT 中存放函数目标地址,PLT 使用 GOT 中地址跳转到目标函数,在加载时,动态链接器会重定位 GOT 中的每个条目,使得它包含目标的正确的绝对地址。

图 27 elf文件中got的信息

在ELF文件中,我们可以找到关于.got的相关信息,根据这些信息我们可以在edb中找到相应位置。

图 28 edb中找到got的位置

在0X404000处可以看到got.plt在调用init前全是0

图 29 调用init后的内容

调用后的got.plt。经过对比我们可以发现前者保存的是指向已经加载的共享库的链表地址。后者保存的是是动态链接器在ld-linux.so模块中的入口。这样,接下来执行程序的过程中,就可以使用过程链接表PLT和全局偏移量表GOT进行动态链接。

5.8 本章小结

本章介绍了链接的概念及作用,在Ubuntu下链接的命令行,并对hello的elf格式进行了详细的分析对比,并通过反汇编hello文件,将其与hello.o反汇编文件对比,详细了解了重定位过程,遍历了整个hello的执行过程,在最后对hello进行了动态链接分析,对链接的一根完整的流程以及连接过程中计算机底层发生了什么有了一个深入的认识。

(第51分)

第6章 hello进程管理

6.1 进程的概念与作用

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

作用:

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

1. 一个独立的逻辑控制流,提供一个假象,好像程序独占地使用处理器

2. 一个私有地址空间,提供一个假象,好像程序独占地使用内存系统

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

Shell是一个命令行解释器,它为用户提供了一个向Linux内核发送请求以便运行程序的界面系统级程序,用户可以用Shell来启动、挂起、停止甚至时编写一些程序。Shell还是一个功能相当强大的编程语言,易编写,易调试,灵活性较强。Shell是解释执行的脚本语言,在Shell中可以直接调用Linux系统命令

Shell的处理流程大致如下:

1. 从用户处传入输入的命令。

2. 把输入字符串成若干参数,获得并识别所有的参数

3. 立即执行输入为内置命令,若输入参数并非内置命令,则调用相应的程序为其分配子进程并运行,若输入参数非法,则返回错误信息。

4.返回3继续处理下一参数,直到处理完毕结束

6.3 Hello的fork进程创建过程

以Hello程序为例,带参执行当前目录下的可执行文件hello时,父进程会通过fork函数创建一个新的运行的子进程hello。子进程获取了与父进程完全相同的内容,包括栈、通用寄存器、程序计数器,环境变量和打开的文件。子进程与父进程的的区别是有着不一样的PID。当我们在shell中传入./hello 120L021801 郑卓 4时,由于传入命令不是内置命令,父进程就会在此时通过fork创建一个子进程。

6.4 Hello的execve过程

调用函数fork创建新的子进程之后,子进程会调用execve函数,在当前进程的上下文中加载并运行一个新程序hello。加载器会删除子进程所有的虚拟地址段,和代码并将其初始化,然后通过跳转到程序的第一条指令或入口点来运行该程序。将私有的区域映射进来,例如打开的文件,代码、数据段,然后将公共的区域映射进来到可执行文件的页大小的片。后面加载器跳转到程序的入口点,即设置PC指向_start 地址。_start函数最终调用hello中的 main 函数,完成在子进程中的execve。

6.5 Hello的进程执行

在程序运行时,Shell识别到hello不是内置命令所以fork了一个子进程,这个子进程是父进程的一个拷贝,与Shell之间有独立的逻辑控制流。在hello的运行过程中,若hello进程不被抢占,则继续正常执行;若hello被其他程序抢占,进行上下文切换,调用另一个程序时hello进入内核模式,将 hello 进程从运行队列加入等待队列,并开始计时。当计时结束时,sleep函数返回,触发一个中断,计时器发送一个中断信号,处理器处理中断信号,使得hello进程重新被调度,将其从等待队列中移出,再次进行上下文切换,从内核模式转为用户模式。此时 hello 进程重新继续执行其逻辑控制流。

6.6 hello的异常与信号处理

hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。

程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。

图 30 运行hello

乱按时输入的字符串不会被传入到函数中,对hello的运行没有影响。

图 31 乱按键盘

在hello运行结束后才被shell接受。

图 32 CTRL z

CtrlZ停止前台作业,会使hello程序停止。

图 33 CTRL c

仍可在后台中查询到hello,输入fg指令后使hello从后台作业变为前台,继续运行。

图 34 无法查询到hello被暂停

输入CtrlC直接终止了hello,在后台中无法查询到hello

6.7本章小结

本章介绍了进程的概念与作用,以及Shell-bash的基本概念。针对进程,在这一章中根据hello可执行文件的具体示例研究了fork,execve函数的原理与执行过程,并给出了hello带参执行情况下各种异常与信号处理的结果。

(第61分)

第7章 hello的存储管理

7.1 hello的存储器地址空间

1. 逻辑地址

逻辑地址是指由程序产生的与段相关的偏移地址部分,逻辑地址由选择符和偏移量两部分组成。

2. 线性地址

逻辑地址经过段机制转化后为线性地址,是逻辑地址到物理地址变换之间的中间层,其为处理器可寻址空间的地址,用于描述程序分页信息的地址。线性地址标志着 hello 应在内存上哪些具体数据块上运行。

3. 虚拟地址

虚拟地址就是述线性地址。

4. 物理地址

在存储器里以字节为单位存储信息,为正确地存放或取得信息,每一个字节单元给以一个唯一的存储器地址,称为物理地址(Physical Address),又叫实际地址或绝对地址。CPU通过地址总线的寻址,找到真实的物理内存对应地址。

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

存储地址要经过从逻辑地址到线性地址再到物理地址的过程,而段式管理的过程就是从逻辑地址到线性地址的过程。

图 35 段式管理的工作原理

逻辑地址由48位组成,16位是段选择符后32位是段内偏移量。

图36 段选择符的

前16位的段选择符组成如图所示:其包含三部分:索引,TI,RPL

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

TI::根据TI的值判断选择全局描述符表或选择局部描述符表。如果 TI 是 0,描述符表是全局描述符表,如果 TI 是 1,描述符表是局部描述表(LDT)

RPL:段的级别。为 0,位于最高级别的内核态。为 11,位于最低级别的用户态。在 linux 中也仅有这两种级别。

描述符表:实际上就是段表,由段描述符(段表项)组成。有三种类型:

全局描述符 GDT:只有一个,用来存放系统内用来存放系统内每个任务共用的描述符,例如,内核代码段、内核数据段、用户代码段、用户数据段以及 TSS(任务状态段)等都属于 GDT 中描述的段。

局部描述符表 LDT:存放某任务(即用户进程)专用的描述符

中断描述符表 IDT:包含 256 个中断门、陷阱门和任务门描述符

段描述符:段描述符就是表项,一种记录每个段信息的数据结构。我们之前说到的段选择符就是描述符表(段表)中的索引。

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

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

地址从线性地址到物理地址要换通过对虚拟地址内存空间进行分页的分页机制完成,而由逻辑地址得到的线性地址一共 32 位。前10位是页目录索引,中间10位是页表索引,最后12位是页内偏移量。

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

由于虚拟内存与物理内存的页大小相同,因此VPO与PPO(物理页偏移量)一致。而PPN(物理页号)则需通过访问页表中的页表条目(PTE)获取。CPU取出虚拟页号,通过页表基址寄存器(PTBR)来定位页表条目,在有效位为1时,可以直接获取到信息物理页号(PPN),通过将物理页号与虚拟页偏移量(VPO)结合,得到由物理地址(PPN)和物理页偏移量(PPO)组合的物理地址。

若PTE的有效位为0,就发生了缺页故障,调用操作系统的内核的缺页处理程序,确定一个牺牲页调入新的页面。再返回到原来的进程,再次调用导致缺页的指令。此时有效位为1一定是页命中,直接获取到PPN,与PPO结合共同组成物理地址。

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

TLB:是Translation Lookaside Buffer的简称,可翻译为“地址转换后援缓冲器”,也可简称为“快表”。简单地说,TLB就是页表的Cache,其中存储了当前最可能被访问到的页表项,其内容是部分页表项的一个副本。只有在TLB无法完成地址翻译任务时,才会到内存中查询页表,这样就减少了页表查询导致的处理器性能下降。TLB中的项由标识和数据两部分组成。标识中存放的是虚地址的一部分,而数据部分中存放物理页号、存储保护信息以及其他一些辅助信息。

多级页表:多级页表其实就是对页表实施分页管理,即内存中只常驻一级页表,一级页表的每个PTE指向一个二级页表的基地址,以此类推,最后一级页表存储物理块号和块内偏移量,因为页和块的大小相等,所以页偏移量和块偏移量相等,只块号不同,即虚拟地址通过查页表就是完成了页号到块号的转换。页表分级后,使用到的虚拟地址空间对应的一系列页表加载到内存中,其他没用到的页表就可以不占用内存,若内存紧张,还可将虽有映射关系但不常用的页表调换到磁盘上,等到需要时再通过缺页中断调回内存中。

多级页表的工作原理以4级页表为例:根据虚拟地址,MMU根据VPN在TLB中搜索PTE,若有效位为1,则命中,MMU取出相应的PTE,根据PTE将VA翻译成PA;若有效位为0,则不命中,则通过4级页表查询PTE是否在页中,若在页中,找到对应的PIE,MMU将VA翻译成PA,若没有在页中,则进行缺页处理,调用操作系统的内核的缺页处理程序,确定一个牺牲页调入新的页面。再返回到原来的进程,再次调用导致缺页的指令。此时有效位为1一定是页命中,直接获取到PPN,与PPO结合共同组成物理地址。

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

根据虚拟地址,MMU先根据VPN在TLB查找,若命中,则直接根据PTE将VA翻译成PA,否则通过多级页表或缺页故障处理等方式得到物理地址。然后在Cache里逐层访问,不在L1就在L2中继续访问,不在L1就在L2中继续访问,不在L3就在主存中继续访问。

图 39 储存器层次结构

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. 设置程序计数器(PC)

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

图 40 堆栈结构

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

页缺失(英语:Page fault,又名硬错误、硬中断、分页错误、寻页缺失、缺页中断、页故障等)指的是当软件试图访问已映射在虚拟地址空间中,但是并未被加载在物理内存中的一个分页时,由中央处理器的内存管理单元所发出的中断。

通常情况下,用于处理此中断的程序是操作系统的一部分。如果操作系统判断此次访问是有效的,那么操作系统会尝试将相关的分页从硬盘上的虚拟内存文件中调入内存。而如果访问是不被允许的,那么操作系统通常会结束相关的进程。虽然其名为“页缺失”错误,但实际上这并不一定是一种错误。而且这一机制对于利用虚拟内存来增加程序可用内存空间的操作系统(比如Microsoft Windows和各种类Unix系统)中都是常见且有必要的。

PTE中的有效位是零,所以MMU触发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序。缺页处理程序确定出物理内存中的牺牲页,如果这个页面已经被修改了,则把它换出到磁盘。缺页处理程序页面调人新的页面,并更新内存中的PTE。缺页处理程序返回到原来的进程,再次执行导致缺页的指令。CPU将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面现在缓存在物理内存中,所以就会命中。

7.9动态存储分配管理

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

分配器有两种基本风格,他们的不同之处在于由那个实体来负责释放已分配的块:

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

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

动态存储分配管理基本方法与策略如下:

隐式空闲链表:

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

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

图 41 隐式链表结构

显式空闲链表:

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

显式链表的结构如下:

图 41 显式链表结构

分离的空闲链表:

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

带边界标记的合并:

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

7.10本章小结

通过对段式和页式存储,页表的存储管理,虚拟地址物理地址的转换,进程的加载时的内存映射,缺页故障和处理,动态内存分配等内容的回顾,加深了对存储管理的理解。

(第7 2分)

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

设备管理:unix io接口

所有的IO设备都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行。这种将设备映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。这使得所有的输入和输出都能以一种统一且一致的方式来执行

8.2 简述Unix IO接口及其函数

IO接口:

1.打开文件

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

2.linux shell 创建的每个进程开始时都有三个打开的文件

标准输入(描述符为0) 、标准输出(描述符为1) 和标准错误(描述符为2) 。头文件 定义了常量STDIN_FILENO 、STOOUT_FILENO 和STDERR_FILENO, 它们可用来代替显式的描述符值。

3.改变当前的文件位置

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

4.读写文件

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

5.关闭文件

当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。

IO函数:

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

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

  1. int close(fd)

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

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

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

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

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

8.3 printf的实现分析

Printf函数的函数体为:

  1. intprintf(constchar*fmt,…){
  2. inti;
  3. charbuf[256];
  4. va_listarg=(va_list)((char*)(&fmt)+4);
  5. i=vsprintf(buf,fmt,arg);
  6. write(buf,i);
  7. returni;
  8. }

参数中的…表示不确定传入参数的个数,printf函数内部调用了vsprintf,传入的其中一个参数arg是((char*)(&fmt)+4)表示的是printf传入参数的第一个。va_list的定义为typedef char *va_list 这说明它是一个字符指针。

进一步查看vsprintf函数体:

  1. intvsprintf(char*buf,constchar*fmt,va_listargs){
  2. char*p;
  3. chartmp[256];
  4. va_listp_next_arg=args;
  5. for(p=buf;*fmt;fmt++){
  6. if(*fmt!=‘%’){
  7. *p++=*fmt;
  8. continue;
  9. }
  10. fmt++;
  11. switch(*fmt){
  12. case‘x’:
  13. itoa(tmp,*((int*)p_next_arg));
  14. strcpy(p,tmp);
  15. p_next_arg+=4;
  16. p+=strlen(tmp);
  17. break;
  18. case‘s’:
  19. break;
  20. default:
  21. break;
  22. }
  23. }
  24. return(p-buf);
  25. }

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

进一步对write进行跟踪:

  1. write:
  2. moveax,_NR_write
  3. movebx,[esp+4]
  4. movecx,[esp+8]
  5. intINT_VECTOR_SYS_CALL

int INT_VECTOR_SYS_CALL表示要通过系统来调用sys_call这个函数,再看sys_call的实现:

  1. sys_call:
  2. callsave
  3. pushdword[p_proc_ready]
  4. sti
  5. pushecx
  6. pushebx
  7. call[sys_call_table+eax*4]
  8. addesp,4*3
  9. mov[esi+EAXREG-P_STACKBASE],eax
  10. cli
  11. ret

字符显示驱动子程序将通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中。显示芯片会按照一定的刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

于是我们的打印字符串就显示在了屏幕上。

8.4 getchar的实现分析

以下格式自行编排,编辑时删除

getchar()的源代码是:

  1. intgetchar(void){
  2. staticcharbuf[BUFSIZ];
  3. staticchar*bb=buf;
  4. staticintn=0;
  5. if(n==0){
  6. n=read(0,buf,BUFSIZ);
  7. bb=buf;
  8. }
  9. return(–n>=0)?(unsignedchar)*bb++:EOF;
  10. }

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

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

getchar调用系统函数read,发送一个中断信号,内核中断当前进程,控制权交给键盘中断处理程序,用户输入字符串,字符串和回车都保存在缓冲区内,直到接受到回车键,再次发送信号,内核重新调度这个进程,才开始从stdio流中每次读入字符。getchar()函数的返回值是用户输入的字符的ASCII码,若文件结尾则返回-1(EOF),且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。

8.5本章小结

本章主要介绍了linux的IO设备管理方法和及其接口和函数,printf函数和getchar函数的底层实现,对IO管理进行了更加深入的学习。

结论

Hello程序的一生包括P2P和020两个过程:

Hello.c P2PProgram to Process)过程包括

预处理:将hello.c文件原有代码的基础上,引入了include的所有外部3046行的头文件,声明函数,定义结构体,定义变量,定义宏等内容,完成字符串的替换。

编译:通过词法分析和语法分析,将hello.i中的合法指令翻译成等价汇编代码,生成了hello.s文件。

汇编:将hello.s的汇编语言程序转换成机器语言指令,形成二进制可重定位目标文件hello.o

链接:将二进制可重定位目标文件hello.ohello程序当中需要的外部库动态链接库打包形成一个可执行文件hello

Hello.c 的020(Zero-0 to Zero-0)过程:

加载:打开终端terminal,输入./hello 120L021801 郑卓 3开始加载和运行hello程序,shell识别到hello不是内置指令会在父进程中fork一个子进程,并通过execve把代码和数据加载入虚拟内存空间,hello程序开始执行。

执行:hello进程被调度,CPU为其分配时间片,在一个时间片中,hello享有CPU资源,顺序执行自己的控制逻辑流。

访存:存储地址要经过从逻辑地址到线性地址再到物理地址的过程, MMU经过段式管理,页式管理,TLB与四级页表支持下的VA到PA的变换,进而通过三级cache访问物理内存和盘中的数据。

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

信号处理:如果运行途中键入Ctrl-C会将HELLO进程终止,Ctrl-Z会将HELLO进程暂停。

终止并被回收:hello成为僵尸进程,由shell回收进程,操作系统释放虚拟空间。

附件

文件名

功能

hello.i

预处理后得到的文本文件

hello.s

编译后得到的汇编语言文件

hello.o

汇编后得到的可重定位目标文件

hello.elf

用readelf读取hello.o得到的ELF格式信息

hello.asm

反汇编hello.o得到的反汇编文件

hello1.elf

由hello可执行文件生成的.elf文件

asm.txt

反汇编hello可执行文件得到的反汇编文件

参考文献

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

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

[2] Pianistx.printf 函数实现的深入剖析[EB/OL].2013[2021-6-9]. https://www.cnblogs.com/pianist/p/3315801.html

[3]Randal E.Bryant, David O’Hallaron. 深入理解计算机系统[M]. 机械工业出版社.2018.4

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