CSAPP-计算机系统大作业

计算机系统

大作业

题     目  程序人生-Hellos P2P  

专       业        信息安全        

学     号              

班   级                

学       生            

指 导 教 师        刘宏伟       

计算机科学与技术学院

20234

摘  要

本篇报告的主要内容是展示hello程序的一生。通过对hello.c文件一步步被预处理、汇编、编译、链接生成可执行文件的分析,以及之后在Shell中加载hello程序到hello进程被回收这个过程的探讨和研究,来对计算机系统的运行方式进行阐述和梳理其中的关键概念和过程。

关键词:计算机系统;内存管理;进程管理;                            

目  录

第1章 概述

1.1 Hello简介

1.2 环境与工具

1.3 中间结果

1.4 本章小结

第2章 预处理

2.1 预处理的概念与作用

2.2在Ubuntu下预处理的命令

2.3 Hello的预处理结果解析

2.4 本章小结

第3章 编译

3.1 编译的概念与作用

3.2 在Ubuntu下编译的命令

3.3 Hello的编译结果解析

3.4 本章小结

第4章 汇编

4.1 汇编的概念与作用

4.2 在Ubuntu下汇编的命令

4.3 可重定位目标elf格式

4.4 Hello.o的结果解析

4.5 本章小结

第5章 链接

5.1 链接的概念与作用

5.2 在Ubuntu下链接的命令

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

5.4 hello的虚拟地址空间

5.5 链接的重定位过程分析

5.6 hello的执行流程

5.7 Hello的动态链接分析

5.8 本章小结

第6章 hello进程管理

6.1 进程的概念与作用

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

6.3 Hello的fork进程创建过程

6.4 Hello的execve过程

6.5 Hello的进程执行

6.6 hello的异常与信号处理

6.7本章小结

第7章 hello的存储管理

7.1 hello的存储器地址空间

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

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

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

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

7.6 hello进程fork时的内存映射

7.7 hello进程execve时的内存映射

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

7.9动态存储分配管理

7.10本章小结

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

8.2 简述Unix IO接口及其函数

8.3 printf的实现分析

8.4 getchar的实现分析

8.5本章小结

结论

附件

参考文献


第1章 概述

1.1 Hello简介

P2P的含义:P2P(From Program to Process 从程序到进程)指的是一个程序从一段代码到可执行程序的过程。

P2P的过程:hello.c最初原本是一个保存着一段文本文件,预处理器会根据文件中#开头的一行文本(例如#include<stdio.h>)修改原有的.c文件,进行预处理。经过预处理之后,会生成一个hello.i文件,保存了预处理的结果。之后调用编译器将c语言代码文件hello.i翻译成汇编语言,保存在hello.s文件中。接着,汇编器将hello.s翻译成一个个机器语言指令,并且把这些指令打包成一种叫做可重定位目标程序的格式,保存在hello.o文件中。最后调用链接器把需要的其他函数的.o文件链接起来,生成可执行目标程序(二进制)hello文件。

正在上传…重新上传取消

图1:hello.c文件P2P的过程

020的含义:020(From zero to zero)指的是程序从运行到结束,在内存中从0开始运行,结束后后又归于0的过程。

020的过程:首先我们通过Linux命令行启动./hello开始运行程序。Linux的命令行会调用fork()函数创建一个新进程,再调用execve来加载hello程序。之后计算机系统会分配内存,分配时间片等等来运行程序。当程序结束运行时,会向上级进程发送SIGCHLD信号,之后上级进程对其进行内存回收等等操作,完成020的整个过程。

1.2 环境与工具

软件环境:ubuntu18.04

硬件环境:CPU:11th Gen Intel(R) Core(TM) i9-11900H @ 2.50GHz

开发工具:gcc; vim; edb; objdump;

1.3 中间结果

hello.c(源文件)

hello.i(经过预处理之后的源文件)

hello.s(经过编译以后的汇编语言文件)

hello.o(汇编器汇编后的可重定位文件)

hello(程序文件)

elf1.txt(可重定位目标文件的ELF文件格式)

1.4 本章小结

本章大致的介绍了P2P和020的含义,之后介绍了hello.c文件的P2P过程和020的主要过程,并且声明了在本次大作业中需要使用到的软件环境、硬件环境以及开发工具,为接下来的各个章节做一个概述。之后的章节中会更加详细的介绍各个过程中hello.c文件会发生什么变化。


第2章 预处理

2.1 预处理的概念与作用

预处理的概念:是指在进行编译的第一遍扫描(词法扫描和语法分析)之前所作的工作。预处理是C语言的一个重要功能,它由预处理程序负责完成。当对一个源文件进行编译时,系统将自动引用预处理程序对源程序中的预处理部分作处理, 处理完毕自动进入对源程序的编译。这个过程并不对程序的源代码进行解析,但它把源代码分割或处理成为特定的单位——(用C/C++的术语来说是)预处理记号(preprocessing token)用来支持语言特性(如C/C++的宏调用)。

预处理的作用:预处理指令一般被用来使源代码在不同的执行环境中被方便的修改或者编译。具体来说,预处理能够完成头文件的包含,将需要的文件插入到目标文件的指定位置等待编译生效;将宏里面的值与程序里面的用宏替换等。比如预处理器读取到hello.c中第1行的#include<stdio.h> ,就知道文件需要读取系统头文件stdio.h的内容,并把它直接插入程序文本中。结果就得到了另一个hello.i文本。

2.2在Ubuntu下预处理的命令

 预处理命令:gcc -m64 -no-pie -fno-PIC -E hello.c -o hello.i

图2 预处理生成hello.i文件

2.3 Hello的预处理结果解析

由图3可以看出,经过对hello.c文件的预处理之后,hello.c文件的前几行#include<>代码被替换成许多行的各自include的文件中包含的内容,又更进一步的写入了各个包含文件包含的内容。因为写入了原hello.c文件include的文件,所以预处理之后的文件比原有的hello.c文件大得多。在开头文件给出了一些include文件的信息,之后是各个包含文件的内容,最后是我们一开始编写的hello.c,具体内容可以见图3,4。

图3 预处理之后的hello.i文件内容

图4 原hello.c文件中的内容

2.4 本章小结

本章节首先介绍了hello.c文件P2P过程中的预处理这一个环节的概念和具体的过程,之后展示了在ubuntu下利用gcc对hello.c文件进行预处理的指令,和预处理之后得到的hello.i文件。我们可以看到在hello.i文件中,hello.c的大部分内容得到了保留,而开头的几行#include<>被替换为各自include的文件的内容,也正因为预处理过程将include的文件写入hello.i中,使得hello.i的文件大小比hello.c大得多。


第3章 编译

3.1 编译的概念与作用

编译的概念:编译指的是在c语言程序完成预处理之后,调用编译器,通过语法分析和语义分析,将预处理之后的.i文件转化为.s文件,其中.s文件为根据.i文件生成的汇编语言代码。

编译的作用:编译能够完成高级语言(c语言)到汇编语言的转化,使得生成之后的汇编代码更加接近计算机实际使用的二进制机器码,并且相比于高级语言,汇编语言代码更加贴近于实际计算机完成运算的方式。        

3.2 在Ubuntu下编译的命令

编译的命令:gcc -m64 -no-pie -fno-PIC -S hello.i -o hello.s

正在上传…重新上传取消

图5 在ubuntu下编译hello.i文件以及结果

3.3 Hello的编译结果解析

使用gcc编译hello.i之后,得到一个hello.s文件,里面是hello.i经过编译器汇编之后生成的汇编语言代码,具体见图6,7:

正在上传…重新上传取消

图6 hello.s文件的内容

正在上传…重新上传取消

图7 hello.s文件的内容

下面分析hello.s中出现的编译器处理C语言各个操作的方法:

3.3.1常量

正在上传…重新上传取消

图8 编译器对常量的处理

.section 和.rodata段声明下面的数据为常量数据,里面的数值、函数的声明或是字符串都是在程序内部不会变化的。如图8可见,编译器生成的hello.s文件中有两个字符串常量,即为图中的两个 .string 之后的内容,都来自于hello.i中printf打算输出的字符串。

3.3.2 变量

由于hello.c文件内部没有全局变量的出现,下图是一个hello.s程序使用局部变量的例子,程序对于局部变量的表示为:

正在上传…重新上传取消

图9 使用变量的例子

图中edi寄存器存的是main函数的第一个参数,rsi寄存器存储的是第二个参数,汇编语言将edi存入-20(%rbp),即把main函数的第一个参数 int argc 存入局部变量存在的栈上。之后将cmpl $4 -20(%rbp) 实际是把变量argc和4进行比较。

3.3.3 赋值

正在上传…重新上传取消

图10 赋值的例子

图中为一个赋值的例子,movq代码的含义是将-32(%rbp)位置上的元素取出并将其放置到%rax寄存器内部,即给这个寄存器赋值,之后把这个值加16。

3.3.4 算数运算

正在上传…重新上传取消

图11 算数运算的例子

图中为一个赋值的例子,此处addq的含义为把%rax 寄存器上存储的值+16,完成了C语言中的加法运算。

3.3.5 关系操作

正在上传…重新上传取消

图12 关系操作的例子

图中为一个关系操作的例子,这里使用cmpl 指令将-20(%rbp)上存储的值和4进行比较,实际上是进行一个减法操作,操作之后的各个结果根据规则保存在标志位寄存器中,之后je .L2 代表如果比较的结果为两者相等,则跳转到.L2地址。

3.3.6 函数操作

正在上传…重新上传取消

图13 函数操作的例子

图中为一个函数操作的例子,汇编语言代码使用call来执行函数的调用操作,图中call printf的作用是调用printf函数,运行到该行代码时,计算机计算出该地址相对于调用函数的偏移地址,之后将接下来执行的指令地址改为被调用的函数的地址。

3.3.7 控制转移

正在上传…重新上传取消

图14 控制转移的例子

图中为一个控制转移的例子,如3.3.5一节中介绍,这里根据cmpl的结果决定是否转移到.L2 段代码,如果cmpl的两个操作数结果相等,则跳转,如果不相等,则继续顺序运行指令,实现了控制转移。

3.3.8 数组操作

正在上传…重新上传取消

图15 数组操作的例子

图中为一个数组操作的例子,第一条movq指令作用是获得数组argv的指针(基地址),也就是argv[0],第二条addq作用为在当前的指针argv上加了偏移量,第三条movq指令则访问了加偏移量后的地址,也就是argv[1]。之后再次进行类似的操作访问argv[2]。

3.4 本章小结

本章首先介绍了P2P过程中编译这一环节的概念和作用,了解到编译器能够完成高级语言向汇编语言的转化,之后使用gcc的编译指令完成了对hello.i文件的编译,得到一个汇编语言代码文件hello.s。之后针对hello.c中出现的c语言的数据结构和各种操作,我们通过对hello.s中代码片段的分析,了解了汇编语言是如何完成c语言中的各种算数操作,条件转移等等操作,也看到了函数,数组之类的调用在汇编语言中是如何表示的。


第4章 汇编

4.1 汇编的概念与作用

汇编的概念:汇编指的是计算机把汇编语言程序翻译成计算机真正可以理解的二进制机器语言的过程。汇编器将 .s文件翻译成二进制机器语言,并且把得到的结果写成一种叫做可重定位目标程序的格式,并保存在输出文件 .o文件中。

汇编的作用:汇编能够完成汇编语言到二进制机器语言的转化,使得我们可以看到计算机在实际运行一个程序的过程中,进行了什么样的操作,使用了怎么样的方法进行计算等等。

   

4.2 在Ubuntu下汇编的命令

汇编命令为:gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o

正在上传…重新上传取消

图16 汇编的命令和汇编得到的hello.o文件

4.3 可重定位目标elf格式

正在上传…重新上传取消

图17 使用readelf指令把hello.o 的信息读入elf.txt

4.3.1 ELF头

正在上传…重新上传取消

图18 hello.o文件生成的ELF头

ELF头位于ELF文件的头部,里面存储着一些机器和该ELF文件的基本信息。ELF 头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF 头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。

4.3.2 段头部

正在上传…重新上传取消

图19 elf.txt中的段头部

段头部(Section Header)中包含了程序的各个段(如.bss段)的类型,大小,偏移地址等等信息。

4.3.3 缺省的头部

正在上传…重新上传取消

图20 elf.txt中没有的头部

图20中列出了生成的elf.txt的中不存在的头部,可以看到,该文件中不包含程序头。

4.3.4 重定向项目

正在上传…重新上传取消

图21 elf.txt中的重定向项目

图21展示了elf.txt中的重定向项目。在编写hello.c程序的时候使用了内存操作,对应到hello.s中会产生一些内存地址的引用,这些地址在我们执行链接前是待定的,链接之后才会指定确切的地址。因此,计算机需要对这些地址进行重定位。每个代码段或数据段都对应一个重定位项目中的记录,计算机记录了段中的这些位置,方便之后对它们进行查找和操作。如图中sleep这一项,从左到右记录了sleep函数的偏移量,信息,类型,符号值和符号名+加数。

4.3.4 符号表

正在上传…重新上传取消

图22 elf.txt中的符号表

图22展示了elf.txt中的符号表,即Symbol table中的内容。符号表列举了程序中使用到的所有函数和全局变量

4.4 Hello.o的结果解析

反汇编命令:objdump -d -r hello.o > dump.txt

正在上传…重新上传取消

图23 使用objdump对hello.o进行反汇编

正在上传…重新上传取消

图24 反汇编之后得到的dump.txt

根据反汇编的结果,我们可以看出,机器语言由01串序列构成,也可以表示为一个个字节的构成,同时,机器语言和汇编语言之间有着映射关系。比如e8 这一机器码,在汇编语言中对应callq 指令。同时,汇编和反汇编的文件在操作数上存在差异。我们可以观察出反汇编文件和我们顺序生成的hello.s汇编语言文件的区别。

控制转移的跳转指令的寻址方式发生变化

正在上传…重新上传取消

图25 反汇编之后的jmp指令

如图25可以看到,反汇编之后的文件,在使用jmp时使用了相对地址寻址的方式,而我们hello.s中的jmp使用的是jmp .L2 跳转指定代码段的方法。因为程序编译后的实地址无法确定,采用相对寻址可以加强程序的可移植性。

②函数调用方式发生变化

正在上传…重新上传取消

图26 反汇编之后的callq指令

如图26,我们可以看到反汇编之后程序对于函数的调用同样采用相对寻址的方式,而且采用的是标签+偏移量的方法,如图中0x68行调用了main+0x6d的地址上存储的函数,而0x6d存储的是atoi函数,实际上是完成了call atoi的操作。因为函数在内存中的地址无法确定,所以采用相对寻址方式。

4.5 本章小结

本章,我们介绍了汇编的概念和作用。之后通过实际使用gcc 汇编我们的hello.s汇编语言文件,我们得到了机器语言文件hello.o。hello.o中的文件我们不可直接阅读,但是我们可以通过readelf 命令得到hello.o中保存的elf信息,比如elf头,重定向项目等等信息。我们还使用了objdump对hello.o文件进行了反汇编,得到了一个新的汇编语言代码dump.txt。通过对这个文件的观察和分析,我们了解到了机器码和汇编语言之间存在映射关系。并且我们看到反汇编之后的文件和我们编写的hello.s文件之间也存在差异,主要体现在反汇编文件使用相对寻址的方式,这向我们揭示了计算机实际工作的工作方法。
第5章 链接

5.1 链接的概念与作用

链接的概念:链接是计算机结合多个不同的可重定位目标文件(如hello.o)、得到具有统一内存地址,能够运行的可执行程序的过程。

链接的作用:链接将不同文件中的数据和程序段结合统一起来,在编程时计算机可以做到通过各个小文件组成大型程序,条理清晰,使得更加分散化、模块化的编程成为可能。

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

正在上传…重新上传取消

图27 在ubuntu下链接的命令

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

正在上传…重新上传取消

图28 使用readelf获得hello文件的elf信息

hello的ELF格式文件中各段的基本信息如下:

ELF头:

正在上传…重新上传取消

图29 hello文件的ELF头信息

可以看出,hello文件的ELF头信息大体上和我们在第四节中分析的一样,但是小部分内容发生了变化,主要体现在一些标志的参数上。

段头部表:

正在上传…重新上传取消

图30 hello文件的段头部表

如图可以看出,节头部表大体结构与第四节的分析相同,同样包含大小,类别,地址,偏移量等信息。区别体现在节的数量相较之前的.o文件增加了很多,增加了一些可执行文件所特有的段比如.init等。说明可执行文件与重定位文件之间存在差异。头部表中新增的段中,.text为程序代码,.data是初始化的全局变量,.bss是未初始化的全局变量,.rodata是只读数据节,.symtab是符号节,.strtab是字符串节

段节:

正在上传…重新上传取消

图31 hello文件的段节

符号表:

正在上传…重新上传取消

图32 hello文件的符号表

动态区域:

正在上传…重新上传取消

图33 hello文件的动态区域

重定位节:

正在上传…重新上传取消

图34 hello文件的重定位节

5.4 hello的虚拟地址空间

使用edb加载hello文件

正在上传…重新上传取消

图35 使用edb加载hello文件的结果

正在上传…重新上传取消

正在上传…重新上传取消

图36 hello文件的数据堆和5.3中的段节表

可以看到,程序的虚拟内存起始地址和5.3中的表中记录的.init的位置相同,均为4004c0,.plt和.text节在加载之后也放入了对应的位置。如果开始了地址随机化等等,加载之后的虚拟地址可能和5.3中的不一致。

5.5 链接的重定位过程分析

重定位的过程:在重定位中,链接器首先将链接文件中所有的同名节合并为链接后的文件中的一个节。比如来自所有输入模块的.text节被全部合并成一个节,这个节成为输出的可执行目标文件的.text节。然后,链接器将运行时内存地址赋给新的聚合节,同时赋给输入模块定义的每个符号。当这一步完成时,程序中的每条引用的函数和全局变量都有唯一的运行时内存地址了。

使用objdump反汇编hello文件,得到的结果如下:

正在上传…重新上传取消

正在上传…重新上传取消

与反汇编hello.o的结果相比,不同有:①增加了一些新的函数,如图中的_init和puts@plt函数。②call指令之后从原来的00 00 00 00变为了实际的目标地址(采用相对RIP寻址),如图中e8 85 ff ff ff 之后就是RIP相对于exit@plt的位移。

分析hello中具体如何进行重定位:取callq 400530 <exit@plt>为例子,链接器确定exit@plt的位置为400530,确定refaddr=4005ab,之后将400530+addend(0)-refaddr,得到-0x7B,16进制表示下即为FF85。

5.6 hello的执行流程

Hello程序执行过程中的流程如下:

1.<ld-2.27.so!_dl_start>      地址:0x7f9cf1450090

2.<ld-2.27.so!_dl_init>             地址:0x7f9cf146d100

3.<_start>                       地址:400550

4.<libc-2.27.so!__libc_start_main>   地址: 0x7f9cf106d580

5.<_init> 地址:4004c0

6.<main> 地址:400582

7.<puts@plt> 地址:4004f0

8.<exit@plt> 地址:400530

9.<libc-2.27.so!exit> 地址:0x7f9cf18a2128

一些未执行的函数如下:

1.<.plt> 地址:4004e0

2.<printf@plt> 地址:400500

3.<getchar@plt> 地址:400510

4.<atoi@plt> 地址:400520

5.<sleep@plt> 地址:4010e0

6.<__libc_csu_init> 地址:400610

7.<__libc_csu_fini> 地址:400680

8.<_fini> 地址:400684

5.7 Hello的动态链接分析

 动态链接:等到程序运行时才进行链接,它提高了程序的可扩展性(可作为为插件)和兼容性。动态链接的基本思想是把程序按照模块拆分成各个相对独立的部分,在程序运行时才将它们连接在一起形成完整的程序。

我们找到.got.plt段,进行动态链接时会对之进行修改,我们将其放到edb运行,我们观察这一部分是否真的有修改。结果如下图:

正在上传…重新上传取消

图37 dl_init之前的.got.plt段

正在上传…重新上传取消

图38 dl_init之后的.got.plt段

可以看出,.got.plt段发生了变化,例如600100d从00变为了7f,说明程序完成了动态链接的过程。

5.8 本章小结

在本章中我们对链接的过程进行了深入的分析,通过查看程序生成的ELF文件,了解了ELF文件和实际文件中的关联。同时观察hello的虚拟地址空间内容,了解到各个数据段的存放方式。最后通过edb工具,研究了动态链接中的程序的相关变化。


6hello进程管理

6.1 进程的概念与作用

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

进程的作用:进程提供给了我们假象,好像我们的程序是系统当前运行的唯一的程序一样,我们的程序好像是独占地使用内存和处理器,处理器就好像是无间断地一条一条执行我们的指令,我们的代码和数据好像是系统内存中唯一的对象。

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

原理:在Linux系统中,Shell是一个交互式应用程序,代表用户运行其他程序,或者说是一个命令行解释器,以用户态方式运行的终端进程。

流程:①Shell读取用户由键盘输入的命令行。②Shell分析命令行,构造argv和envp数组。③Shell检查第一个命令是否是内置的命令,如果是则执行对应功能。④如果不是内置命令,则首先调用fork()函数生成一个子进程。⑤在子进程中,调用execve()函数并用②中加载的argv数组加载并执行所指定的程序。⑥如果用户没要求后台运行,则Shell使用waitpid等待子进程返回。如果要求则不等待。

6.3 Hello的fork进程创建过程

当输入命令执行可执行目标文件hello时,父进程会通过shell函数创建一个新的运行的子进程。子进程得到与父进程用户级虚拟空间相同但是独立且写时复制的一个副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程的内容,但是它们有着不同的pid,但在父进程中,fork返回子进程的pid,在子进程中,fork返回0.

6.4 Hello的execve过程

调用函数fork创建新的子进程之后,子进程会调用execve函数,在当前进程的上下文中加载并运行一个新程序hello。execve 函数一般不返回,除非运行错误、它将新的栈和堆段初始化为零。通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容。然后通过跳转到程序的第一条指令或入口点来运行该程序。它调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数。

6.5 Hello的进程执行

进程上下文信息:进程的物理实体(代码和数据等)和支持进程运行的环境合称为进程的上下文。由进程的程序块、数据块、运行时的堆和用户栈(两者通称为用户堆栈)等组成的用户空间信息被称为用户级上下文;由进程标识信息、进程现场信息、进程控制信息和系统内核栈等组成的内核空间信息被称为系统级上下文;处理器中各寄存器的内容被称为寄存器上下文(也称硬件上下文),即进程的现场信息。用户级上下文地址空间和系统级上下文地址空间一起构成了一个进程的整个存储器映像。

进程时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。

进程调度的过程:内核为每一个进程维持一个上下文,上下文就是内核重新启动一个被抢占的进程所需的状态。在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策就叫做调度,由内核内称为调度器的代码处理的。当内核选择一个新的进程运行时,我们说内核调度了这个进程。

用户态与核心态转换:运行应用程序代码的进程初始时是在用户模式中的。进程从用户模式变为内核模式的唯一方法是通过诸如中断、故障或者陷人系统调用这样的异常。当异常发生时,控制传递到异常处理程序,处理器将模式从用户模式变为内核模式。处理程序运行在内核模式中,当它返回到应用程序代码时,处理器就把模式从内核模式改回到用户模式。

6.6 hello的异常与信号处理

Hello执行过程中可能会出现的异常的种类有:中断、陷阱、故障、终止。

①:中断是异步发生的,是来自处理器外部的 I/O 设备的信号的结果。硬件中断不是由任何一条专门的指令造成的,从这个意义上来说它是异步的通常会使得程序暂时挂起

②:陷阱是有意的异常,是执行一条指令的结果。就像中断处理程序一样,陷阱处理程序将控制返回到下一条指令。陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用通常会转移到信号处理程序

③:故障由错误情况引起,它可能能够被故障处理程序修正。当故障发生时,处理器将控制转移给故障处理程序。如果处理程序能够修正这个错误情况,它就将控制返回到引起故障的指令,从而重新执行它。否则,处理程序返回到内核中的 abort例程,abort例程会终止引起故障的应程序。

④:终止是不可恢复的致命错误造成的结果,通常是一些硬件错误,比如 DRAM或者SRAM位被损坏时发生的奇偶错误。终止处理程序从不将 控制返回给应用程序。通常会使得程序直接终止。

测试hello程序,结果如下:

正在上传…重新上传取消

图39 正常运行

正在上传…重新上传取消

图40 键盘输入Ctrl+Z

正常运行下,按下ctrl + z 会向shell发送SIGTSTP信号,shell捕获信号后会停止当前进程。并将其添加至jobs。

正在上传…重新上传取消

图41 键盘输入Ctrl+C

按下ctrl + c会向shell发送SIGINT信号,shell捕获信号后会终止当前进程。之后回收该进程。

正在上传…重新上传取消

图42 运行ps命令

正在上传…重新上传取消

图43运行jobs命令

正在上传…重新上传取消

图44 运行pstree命令

此时运行ps会发现当前进程多了两个hello,并且两个hello的状态均为Stopped(源于之前重复了一次Ctrl+Z,正常来说是一个hello)。运行jobs则可以看到多了一个hello job;运行pstree则能看到整个进程树。

正在上传…重新上传取消

图45 运行fg命令

正在上传…重新上传取消

图46 运行kill命令

运行fg将会把hello进程从后台转到了前台,之后shell向hello进程发送一个SIGCONT信号,hello进程开始继续运行。运行kill -9 %1,则是向jobID为1的进程,也就是hello进程发送了SIGINT信号,终止了hello进程,并被shell回收。

6.7本章小结

本章我们学习到了了进程的概念、什么是shell、以及上下切换的过程与异常与信号的处理。同时我们对这一部分进行梳理,使得对hello程序的理解进一步提升。 


7hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:逻辑地址是用户编程时使用的地址,分为段地址和偏移地址两部分。

线性地址:如果地址空间中的整数是连续的,那么我们说他是一个线性地址空间。如hello中代码的存储是从0x400000地址一个一个字节往上增加的。

虚拟地址:是一种虚拟的地址,是由CPU生成的用来访问主存的中间地址,由MMU硬件将虚拟地址翻译成物理地址。如hello中使用的0x400100地址就是一个虚拟地址,实际主存中可能该位置并不是我们需要的数据。

物理地址:计算机系统的主存被组织成一个由M个字节大小的单元组成的数组。这里对应的主存的地址就是唯一的物理地址。hello程序中使用的虚拟地址经过MMU翻译后得到的物理地址中存储的才是想要的信息。

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

Intel中逻辑地址到线性地址的变换如下:一个逻辑地址由段标识符和段内偏移量组成。段标识符是一个16位长的字段。可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。全局的段描述符,放在全局段描述符表中,一些局部的段描述符,放在局部段描述符表(LDT)中。 给定一个完整的逻辑地址段选择符和段内偏移地址,确定要转换的是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,就得到了其基地址。

正在上传…重新上传取消

图47 Intel中的16位段寄存器

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

Hello程序中线性地址到物理地址的变化过程如下图:

正在上传…重新上传取消

图48 线性地址到物理地址的变化过程图

首先通过页表基址寄存器找到查询的页表,通过虚拟地址得出虚拟页号,接着查询有效位是否有效,若有效,则可以查到对应的物理页号,即可以找到物理地址中的基址。虚拟页偏移量与物理页偏移量完全相同,通过物理地址的基址和偏移量,即可以实现虚拟地址到物理地址的转化。若无效则触发缺页中断,从低一级的缓存中查找并映射到这一级中。

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

正在上传…重新上传取消

图49 四级页表的翻译过程

CPU生成一个虚拟地址。MMU用虚拟地址中的虚拟页号向TLB请求对应的PTE,如果PTE不在TLB中,则MMU将转而查询四级页表,如上图:n位VPN被划分成四个m位的片,每个片被用作到一个页表的偏移量。CR3寄存器包含Ll页表的物理地址。VPN1提供到一个Ll PTE的偏移量,这个PTE包含L2页表的基地址。VPN2提供到一个L2 PTE的偏移量,以此类推。直到最后找到虚拟页号对应的PTE,PTE中若有记录物理页号,则完成翻译,没有记录则引发缺页故障。

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

正在上传…重新上传取消

图50 将VA转化为物理内存访问的过程

Cpu产生虚拟地址传给MMU,MMU将PTEA传给L1cache,L1cache不命中,则再将PTEA传给L2cache,L2cache不命中,则再将PTEA传给L3cache,L3cache不命中,则再将PTEA传给内存,找到PTE将其一级一级传上去,将PTE加载到缓存中。MMU将得到的PTE进行翻译,如果PTE的有效位为0,则触发缺页异常处理程序。如果能正常翻译则将生成的物理地址传给三级cache/内存来取数据。

7.6 hello进程fork时的内存映射

正在上传…重新上传取消

图51 fork()函数的工作过程

fork为hello创建虚拟内存,创建当前进程的的mm_struct, vm_area_struct和页表的原样副本,两个进程中的每个页面都标记为只读,两个进程中的每个区域结构(vm_area_struct)都标记为私有的写时复制(COW),在新进程中返回时,新进程拥有与调用fork进程相同的虚拟内存,随后的写操作通过写时复制机制创建新页面。

7.7 hello进程execve时的内存映射

正在上传…重新上传取消

图52 execve()函数的工作过程

execve函数在当前进程中加载并运行新程序hello的步骤:删除已存在的用户区域,创建新的区域结构,私有的、写时复制,代码和初始化数据映射到.text和.data区(目标文件提供),.bss和栈堆映射到匿名文件,栈堆的初始长度0,共享对象由动态链接映射到本进程共享区域,设置PC,指向代码区域的入口点,Linux根据需要换入代码和数据页面。

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

正在上传…重新上传取消

缺页故障的概念虚拟内存中的字不在物理内存中 (DRAM 缓存不命中)

缺页中断处理的过程如下

1) 处理器将虚拟地址发送给 MMU

2) MMU 使用内存中的页表生成PTE地址

3) 有效位为零, 因此 MMU 触发缺页异常

4) 缺页处理程序确定物理内存中牺牲页 (若页面被修改,则换出到磁盘)

5) 缺页处理程序调入新的页面,并更新内存中的PTE

6) 缺页处理程序返回到原来进程,再次执行导致缺页的指令

7.9动态存储分配管理

动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放。C语言通过malloc函数来分配一个块,再通过free来释放这个块。

分配管理的类型有:

①:隐式分配器: 应用检测到已分配块不再被程序所使用,就释放这个块。

正在上传…重新上传取消

对于每个块都需要知道块的大小和分配状态, 如果块是对齐的,那么一些地址低位总是0,使用0位作为一个已分配/未分配的标志,读块大小字段时,必须将其屏蔽掉。分配器可以通过遍历堆中的所有块,从而间接地遍历整个空闲块的集合。

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

正在上传…重新上传取消

将空闲块组织成链表形式的数据结构。堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个pred(前驱)和succ(后继)指针。维护链表时,可以采用先进后出的方式,使得新释放的块放在量表的开始处,使用后进先出的顺序和首次适配的放置策略,分配器会检查最近使用过的块。另一种是按照地址顺序来维护链表,其中链表每个块的地址都小于它后继的地址。在这种情况下每释放一个快需要线性时间搜索来定位合适的前驱。

7.10本章小结

本章节中,我们了解了hello程序的存储器地址空间的一些关键概念,也重温了课堂上学习过的内存管理的知识,重温了虚拟内存到物理内存的转化和物理内存的具体访问,以及fork()和execve()函数实现的具体原理。


8hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

设备管理:unix io接口

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

一些Linux中提供的接口:

打开和关闭文件:open()和close()

读写文件:read()和write()

8.2 简述Unix IO接口及其函数

Linux通过将IO设备映射为文件,并提供给用户一个简单的应用接口的方式完成IO设备管理,其中这个接口就被成为Unix IO接口。

一些Unix IO接口函数:

①:int open(char* filename,int flags,mode_t mode)

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

②:int close(fd)

进程通过调用close函数关闭一个打开的文件,fd是需要关闭的文件的描述符。

③:ssize_t read(int fd,void *buf,size_t n)

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

④:ssize_t wirte(int fd,const void *buf,size_t n)

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

8.3 printf的实现分析

分析printf()函数的源代码:

正在上传…重新上传取消

其中调用了vsprintf()函数,分析该函数:

正在上传…重新上传取消

vsprintf()函数的作用是按照格式fmt 结合参数args 生成格式化之后的字符串,并返回字串的长度。

之后printf()函数调用write()函数输出内容,write()的汇编代码如下:

正在上传…重新上传取消

在printf()中,将buf,i分别作为第1,2个参数置于栈上。之后调用SYS_CALL,

正在上传…重新上传取消

查阅资料后可得,sys_call函数的call指令的作用是访问字库模板并且获取每一个点的RGB信息,最后放入到eax也就是输出,然后系统显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

分析getchar函数的源码:

正在上传…重新上传取消

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

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

8.5本章小结

本章节中,我们介绍了Linux/Unix系统中对于IO设备的管理和IO设备接口以及其提供给用户的API函数。同时我们还分析了C语言中两个典型的输入输出函数printf()和getchar()函数的实现。

结论

一个程序hello的一生包括如下的过程:

①:程序员使用高级语言(C语言等)编写好一个hello.c文件。

②:经过预处理之后,hello.c文件进行宏替换,生成了hello.i文件。

③:之后hello.i文件经过汇编,文件中的高级语言语句被翻译为汇编语言语句。同时使用许多的数据段来表示出不同的数据结构,为接下来的编译做出准备,生成hello.s文件。

④:经过编译阶段,生成了hello.o文件(可重定位目标文件),此时的目标文件已经变成了机器可以理解的机器语言写成的文件,但是依旧无法直接执行,需要程序进一步对其中缺少的一些语句进行分析。

⑤:计算机对hello.o文件进行链接,此时hello.o中缺失的一些符号引用和定位都被补上,此时生成的hello文件可以直接在Shell中运行。

⑥:如果在Shell中运行./hello命令,则Shell使用fork()函数生成一个子进程,在子进程中调用execve()函数加载并运行hello程序。

⑦:hello在运行完毕之后,计算机通过内部的内存管理机制和信号机制将hello进程回收,到此,hello的一生(暂时)结束了。

我的感悟:作为一个程序员,我们要做的可能就是写一行printf(“hello world”),然后用编译器直接就可以得到一个可以在屏幕上显示内容的可执行文件,但是一旦深入的剖析这其中的过程,就会发现,计算机语言从一开始的机器语言一步步进化到高级语言,其中蕴含的翻译机制,内存管理机制,IO设备管理机制,都是十分精妙,不简单的。任何看似微不足道的过程背后都可能许多个计算机系统结构的配合工作的结果,也包含了当初设计计算机系统的科学家的心血。


附件

列出所有的中间产物的文件名,并予以说明起作用。

hello.c(源文件)

hello.i(经过预处理之后的源文件)

hello.s(经过编译以后的汇编语言文件)

hello.o(汇编器汇编后的可重定位文件)

hello(程序文件)

elf1.txt(可重定位目标文件的ELF文件格式)


参考文献

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

  1. 《深入理解计算机系统》,Bryant,R.E. ,机械工业出版社,2016.11.15
  2.  https://www.csdn.net/
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值