哈工大计算机系统大作业——程序人生

 

计算机系统

大作业

题     目  程序人生-Hello’s P2P 

专       业     航天学院人工智能    

学     号     7203610228          

班     级     2036015             

学       生     韩雨萌              

指 导 教 师     史先俊               

计算机科学与技术学院

2022年5月

摘  要

本文介绍hello从hello.c程序编写完成开始到hello最终被回收结束,介绍了hello从预处理、编译、汇编、链接直到被回收的整个过程及原理,同时介绍了linux下的内存管理、进程管理、I/O管理、虚拟内存、异常信号的相关内容,勾勒了hello完整坎坷却不失华丽的一生。

关键词:预处理,编译,汇编,链接,linux,P2P                           

目  录

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

参考文献........................................ -  -

第1章 概述

1.1 Hello简介

Hello最初是一个.c文件,经过预处理得到hello.i文件,编译器编译得到hello.s文件,汇编器汇编得到hello.o目标文件,链接器与库函数链接生成一个可执行程序,shell为其fork产生子进程的过程即为P2P(From Program to Process )具体过程如下图:

 

图1.1p2p过程

O2O(From Zero to Zero):操作系统调用execve执行这个Process,映射虚拟内存,先删除当前虚拟地址的数据结构并为hello创建新的区域结构,进入程序入口后载入物理内存,进入main函数执行目标代码,程序结束后,shell父进程回收子进程hello,内核删除相关信息。

1.2 环境与工具

列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。

1.2.1 硬件环境

64位操作系统x64CPU;处理器AMD Ryzen 7 4800U with Radeon Graphics            1.80 GHz;机带RAM16.0GB

1.2.2 软件环境

Windows 10 64位;Vmware16;Ubuntu20.04。

1.2.3 开发工具

codeblocks64位20.04;Visual Studio2019;vi/vim/gedit+gcc;EDB

1.3 中间结果

列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。

文件名称

文件作用

hello.c

Hello源程序

hello.i

预处理生成文件

hello.s

汇编程序(文本)

hello.o

可重定位目标程序(二进制)

hello.elf

Hello.o的ELF格式可执行文件

hello

链接器生成的可执行目标文件

hello.asm

Hello.o的反汇编文件

hello1.asm

Hello的反汇编文件

hello1.elf

Hello的ELF格式可执行文件

表1 中间文件

1.4 本章小结

本章简单介绍了hello程序,以及hello的P2P、O2O过程,介绍了完成本论文的硬件环境和软件环境以及开发工具,最后列出了编写本论文生成的中间结果的文件以及作用。

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

2.1.1预处理的概念

   预处理是C语言的一个重要功能,它由预处理程序负责完成。当对一个源文件进行编译时,系统将自动引用预处理程序对源程序中的预处理部分作处理,处理完毕自动进入对源程序的编译。主要处理#开始的预编译指令,如宏定义(#define)、文件包含(#include)、条件编译(#ifdef)等。比如#include <stdio.h>命令告诉预处理器读取系统头文件 stdio.h 的内容,并把它直接插入程序文本中。结果就得到了另一个 C 程序,通常是以 .i 作为文件扩展名。

2.2.1预处理的作用

   1.将源文件中以“include”格式包含的文件复制到编译的源文件中;

2.用实际值替换用“#define”定义的字符串;

3.根据“#if”后面的条件决定需要编译的代码。

2.2在Ubuntu下预处理的命令

对hello.c文件进行预处理的命令是:gcc -E -o hello.i hello.c

 

图2.1预处理命令及结果

预处理后生成一个hello.i的文件。

2.3 Hello的预处理结果解析

经过预处理之后,生成hello.i文件,打开该文件可以发现,文件的内容明显增加,该文件主要为为对原文件中的头文件进行展开,例如声明函数、定义结构体、定义变量、定义宏等内容。另外,如果代码中有#define命令还会对相应的符号进行替换。但是函数主体部分并未发生改变。

   

 

图2.2hello.i文件内容

2.4 本章小结

本章主要介绍了预处理的概念及其作用,同时给出了在Linux下预处理的指令,并对生成的hello.i文件进行解析。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

概念:编译器将文本文件 hello.i 翻译成文本文件 hello.s,它包含一个汇编语言程序。

作用:将输入的高级程序设计语言翻译成以汇编语言或机器语言表示的目标程序作为输出,为不同高级语言的不同编译器提供了通用的输出语言。

3.2 在Ubuntu下编译的命令

hello.i进行编译的命令是:gcc -S -o hello.s hello.i

 

图3.1编译命令及结果

3.3 Hello的编译结果解析

在hello.s中用到的数据类型有整数,数组,字符串。

进行的操作有赋值、类型转换、算术操作、关系操作、数组/指针/结构操作、控制转移(分支、循环)、函数操作。

下面首先对程序开始部分进行解析。

3.3.1汇编指令介绍

 

图3.2汇编指令

.file:声明源文件

.text:代码节

.section:指示把代码划分成若干个段(Section)

.rodata:只读代码段

.align:数据或者指令的地址对其方式

.string:声明一个字符串(.LC0,.LC1)

.global:声明全局变量(main)

.type:声明一个符号是数据类型还是函数类型

在hello.s中用到的数据类型有整数,数组,字符串。

3.3.2整数

在hello.c程序中一共有两个变量为int,分别是i和argc。

  1. 对于i,i为局部变量,而局部变量通常是会被保存在栈或者寄存器中,在本程序中,我们可以看到i被存储在栈 %rbp处,由于是int类型,故占据4个字节。

 

   图3.3定义i                                                   图3.4对应i的循环加1操作

  1. 对于变量argc,这个变量为main的第一个形参,由寄存器%edi保存。

 

 

图3.5变量argc

(3)除了int型变量外还有一些常数,他们是作为立即数直接出现的,形式为$+常数。如下图的$32.

 

 

 

图3.6常数表示形式

3.3.3数组

程序中定义的数组char *argv[]为main函数的第二个形参,是一个字符型指针数组,保存在寄存器%rsi中。随后保存到寄存器%rbp中,同时申请32字节的空间。argv数组中一个元素的大小为8个字节,要想访问数组中的数据就需要加上相应的偏移量。

 

图3.7数组表示形式

 

 

3.3.4字符串

在本程序中的字符串为用法: Hello 学号 姓名 秒数!和Hello %s %s,对应存储在.rodata节中,其中汉字被编码成UTF-8格式,一个汉字占据3个字节,每个字节用\分割。

图3.8字符串表示形式

 

3.3.5赋值操作

在本程序中的赋值操作仅有i=0这一条,在汇编中用mov实现,在hello.s中对应代码如下。

 

图3.9赋值操作

3.3.6 类型转换

在本程序中利用atoi函数将argv[3]由字符串转换为了整型。对应操作如下:

 

 

 

图3.10类型转换操作

3.3.7算术操作

(1)在本程序中可以看到使用了i++的算术操作,通过addl $1,-4(%rbp)这条命令实现。

(2)同时,在为数组argv开辟栈空间时的subq     $32, %rsp也为算术操作

(3)以及在取出argv数组中的对应内容时进行的偏移量计算也是算术操作。如addq  $24, %rax等。

3.3.8关系操作

本程序中用到的关系操作有两处,分别为if(argc!=4)和for(i=0;i<8;i++)

(1)其中 if(argc!=4)对应的汇编语言为cmpl $4, -20(%rbp)。进行比较,然后设置条件码,根据条件码结果判断是否跳转。

图3.11 if操作

 

(2)for(i=0;i<8;i++)对应的汇编语言为cmpl $7, -4(%rbp)。进行比较后,根据条件码结果判断是否跳转。

 

图3.12 for循环操作

3.3.9数组/指针/结构操作

在本程序中访问argv为指针型数组,对此数组的操作通常由mov指令实现。取到argv数组的首地址,然后对首地址加相应字节得到对应的地址,然后再通过地址中的内容找到对应的字符串,存储在寄存器。

 

图3.13指针操作

3.3.10控制转移

(1)分支(if)

判断argc是否等于4, cmpl比较了argc与4的大小之后设置条件码,然后再判断是否跳转到L2,这个判断是基于条件码ZF位,如果为0则跳转,不为0则继续往下执行。

 

图3.14分支

(2)循环(for)

判断变量i是否满足循环条件i<8,首先在L2中将i赋值为0,跳转到L3,与7进行比较判定是进入循环还是继续执行,如果i<=7则跳转到L4进入循环,如果i>7则继续执行下一行,而不进入循环。

 

图3.15循环

3.3.11函数操作

本程序涉及到的函数操作包括参数传递、函数调用和函数返回

  1. 参数传递(main函数)

main函数被存储在.text节中,有两个参数,分别为命令行传入的argc和argv[],开始被保存在寄存器%rdi和%rsi中。

 

图3.16参数传递

  1. 函数调用(exit,printf, atoi,sleep,getchar)

在我们的程序中有两处调用了printf函数。第一处调用由于只是输入一串字符串,所以被优化成puts函数,之后通过call来调用puts,而第二处调用printf,有三个参数,因此我们需要取出参数,将参数分别保存在%rdx,%rsi和%edi寄存器中,之后通过call调用printf。

 

 

 

图3.17函数调用

对于exit()函数如果我们输入的参数不是4个,程序就会调用exit函数结束程序,先将1传给%edi,然后调用exit函数退出。

图3.18 exit()函数

对于atoi函数它是用来将我们输入的第四个参数从字符串转化为整型,将第四个参数存储在%rdi中,作为atoi函数的参数,然后调用atoi函数。

图3.19 atoi()函数

对于sleep函数,它的参数就是atoi的返回值,所以在atoi被调用完后,会将其返回值从%eax传给%edi作为sleep函数的参数。

图3.20 sleep()函数 

对于getchar()函数,不需要传递参数,直接调用即可。

 

图3.21getchar()函数

  1. 函数返回(return)

函数的返回值一般在寄存器%eax中,如果有返回值,则要先把返回值存到%eax中,再用return返回。源程序中有主函数的return 0;就是先把返回值立即数0存到%eax中,再用return返回。

 

图3.22return()函数

3.4 本章小结

概括了编译的概念和作用,通过对hello编译结果的分析重点分析了c程序的数据与操作翻译成汇编语言时的表示和处理方法。此时的hello程序以及变成了更底层的语言。

(第3章2分)

第4章 汇编

4.1 汇编的概念与作用

概念:汇编器(as)将汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序(relocatable object program)的格式,并将结果保存在.o 目标文件中,.o 文件是一个二进制文件,它包含程序的指令编码。

作用:将汇编代码转换为机器指令,使其在链接后能被机器识别并执行。

4.2 在Ubuntu下汇编的命令

gcc -c -o hello.o hello.s

 

图4.1生成hello.o命令及结果

4.3 可重定位目标elf格式

在linux下生成.elf格式文件的命令为:readelf -a hello.o > hello.elf,如下图所示:

 

图4.2生成hello.elf

 

图4.3 elf文件内容

在linux命令行输入命令readelf -h hello.o即可看到hello.elf中ELF Header的具体信息,结果如下:

 

图4.4ELF Header内容

ELF头(ELF header)以一个16B的序列Magic开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含了帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是有节头部表描述的,其中目标文件中每个节都有一个固定大小的条目(entry)。

在linux命令行输入命令readelf -S hello.o即可看到hello.elf中Section Headers的具体信息,结果如下:

 

图4.5 Section Headers内容

Section Headers:节头部表,记录各节名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐。 由于是可重定位目标文件,所以每个节都从0开始,用于重定位。

在linux命令行输入命令readelf -s hello.o即可看到hello.elf中Symbol table的具体信息,结果如下:

 

图4.6 Symbol table内容

Symbol table:符号表,存放程序中定义和引用的函数和全局变量的信息。每个可重定位目标文件在.symtab中都有一张符号表,name是符号名称, value是符号相对于目标节的起始位置偏移,对于可执行目标文件,该值是一个绝对运行的地址。size是目标的大小,type数据或者函数。Bind字段表明符号是本地的还是全局的。

查看重定位节,具体信息如下:

 

图4.7 .rela.text内容

.rela.text:重定位节,保存的是.text节中需要被修正进行重定位的信息;任何调用外部函数或者引用全局变量的指令都需要被修正;调用外部函数的指令需要重定位;引用全局变量的指令需要重定位; 调用局部函数的指令不需要重定位;在可执行目标文件中不存在重定位信息。包括以下几个部分:Offset:需要被修改的引用节的偏移;Info:包括symbol和type两个部分,symbol在前面四个字节,type在后面四个字节;symbol:标识被修改引用应该指向的符号;Type:告知链接器应该如何修改新的应用;Attend:一个有符号常数,一些重定位要使用它对被修改引用的值做偏移调整Name:重定向到的目标的名称。

4.4 Hello.o的结果解析

通过objdump -d -r hello.o  生成hello.o的反汇编,下面与第3章的 hello.s进行对照分析。如图为反汇编结果,同时objdump -d -r hello.o > hello.asm将结果保存在hello.asm中。

 

 

图4.8hello.o反汇编结果

机器语言的构成:机器语言是机器能直接识别的程序语言或指令代码,无需经过翻译,每一操作码在计算机内部都有相应的电路来完成它,或指不经翻译即可为机器直接理解和接受的程序语言或指令代码。机器语言使用绝对地址和绝对操作码。不同的计算机都有各自的机器语言,即指令系统。从使用的角度看,机器语言是最低级的语言。

机器语言与汇编语言的映射关系:机器语言是用于控制计算机中处理器的实际位,通常被视为十六进制数字序列(通常为字节).处理器从程序存储器中读取这些位,这些位表示下一步操作的"指令".因此,机器语言提供了一种将指令输入计算机的方式(无论是通过交换机,穿孔带还是二进制文件).

汇编语言是一种更易读的机器语言视图.指令和寄存器不是将机器语言表示为数字,而是给出名称(通常是缩写词或助记符,例如ld表示"加载").与高级语言不同,汇编程序非常接近机器语言.主要的抽象(除了助记符)是使用标签而不是固定的内存地址和注释.

汇编语言程序(即文本文件)由汇编程序转换为机器语言.反汇编程序执行反向功能(尽管标签的注释和名称将在汇编程序进程中被丢弃).

与hello.s对比的差别如下:

(1)分支转移:在汇编代码中,分支跳转是直接以.L0等助记符表示,但在反汇编代码中,分支转移不在依靠段名称,而是表示为主函数+段内偏移量。因段名称在汇编语言中为便于编写的助记符,所以在汇编成机器语言之后就不存在了,而是确定的地址。

(2)函数调用:汇编代码中函数调用时直接使用函数名称,而在反汇编的文件中call之后定位到call的下一条指令,即用具体的地址表示。在.rela.text节中为其添加重定位条目等待链接。

(3)访问全局变量:汇编代码中使用.LC0(%rip),反汇编代码中为0x0(%rip),因为访问时需要重定位,所以初始化为0并添加重定位条目等链接之后再确定。

4.5 本章小结

本章主要介绍汇编的概念与作用,如何得到汇编文件hello.o的操作命令。同时利用readelf对elf文件做了详细的分析,最后比较hello.o的反汇编文件与之前得到的hello.s文件,比较了机器语言和反汇编文件的差别。

(第4章1分)

第5章 链接

5.1 链接的概念与作用

概念:链接(linking)是将各种代码和数据片段收集并合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时(compile time),也就是在源代码被翻译成机器代码时;也可以执行于加载时(load time),也就是在程序被加载器(loader)加载到内存并执行时;甚至执行于运行时(run time),也就是在由应用程序来执行。

作用;链接器在软件开发过程中扮演着一个关键的角色,负责处理不同函数预编译好的目标文件,因为它们使得分离编译(separate compilation)成为可能。

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.1生成hello可执行文件

执行结束后生成hello可执行文件。

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

   在linux命令行输入命令readelf -h hello即可看到ELF Header的具体信息,结果如下:

 

图5.2 hello的ELF Header内容

Type类型为EXEC表明hello是一个可执行目标文件,由之前的14个节变为27个节。

在linux命令行输入命令readelf -S hello即可看到Section Headers的具体信息,结果如下:

 

图5.3 hello的Section Headers内容

Section Headers:节头部表,记录各节名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐。 因此根据 Section Headers 中的信息我们就可以用 HexEdit 定位各个节所占的区间(起始位置,大小)。其中 Address 是程序被载入到虚拟地址的起始地址。

在linux命令行输入命令readelf -s hello即可看到hello.elf中Symbol table的具体信息,结果如下:

 

图5.4 hello的Symbol table内容

Symbol table:符号表,存放程序中定义和引用的函数和全局变量的信息。

查看重定位节,具体信息如下:

 

图5.5 hello的重定位节内容

5.4 hello的虚拟地址空间

使用edb加载hello,详细信息如下:

 

图5.5利用edb反汇编结果

观察Data Dump窗口,发现虚拟地址从0x400000开始到0x401ff0结束。根据的节头部表,可以通过edb找到各个节的信息

 

 

图5.6 Data Dump信息

5.5 链接的重定位过程分析

在linux命令行输入objdump -d -r hello 得到hello的反汇编文件,与hello.o的反汇编文件相比节的数目和文件内容有一些差异。

 

图5.7利用odjdump得到反汇编文件

  1. 节的数目不同

   Hello生成的.asm文件中节的个数多余hello.o生成的.asm文件中节的个数,多了比如.init和.plt等等.

 

 

 

图5.8hello生成的asm文件与hello.o生成asm文件对比

  1. 文件内容差异

hello反汇编的代码有确定的虚拟地址,也就是说已经完成了重定位,而hello.o反汇编代码中代码的虚拟地址均为0,未完成可重定位的过程。

 

图5.9 Hello反汇编结果

 

图5.10 Hello.o反汇编结果

重定位就是把程序的逻辑地址空间变换成内存中的实际物理地址空间的过程。它是实现多道程序在内存中同时运行的基础。重定位有两种,分别是动态重定位与静态重定位。

hello重定位的过程:

(1)重定位节和符号定义链接器将所有类型相同的节合并在一起后,这个节就作为可执行目标文件的节。然后链接器把运行时的内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号,当这一步完成时,程序中每条指令和全局变量都有唯一运行时的地址。

(2)重定位节中的符号引用这一步中,连接器修改代码节和数据节中对每个符号的引用,使他们指向正确的运行时地址。执行这一步,链接器依赖于可重定位目标模块中称为的重定位条目的数据结构。

(3)重定位条目当编译器遇到对最终位置未知的目标引用时,它就会生成一个重定位条目。利用在.rela.data和.rela.txt节中保存的重定位信息,来修改对各个符号的引用,即修改他们的地址。

5.6 hello的执行流程

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

表2过程及对应地址:

ld-2.31.so!_dl_start

0x00007f807a1180b3

ld-2.31.so!_dl_init

0x00007f5a70281c10

Hello!_start

0x000000000040111e

libc-2.31.so!__libc_start_main

0x00007fd1c3e7a550

Hello!main

0x00000000004010d0

Hello!printf@plt

0x00000000004010a0

hello!atoi@plt

0x00000000004010c0

Hello!sleep@plt

0x00000000004010e0

hello!getchar@plt

0x00000000004010b0

libc-2.31.so!exit

0x00007d1c3e6f460

5.7 Hello的动态链接分析

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

在进行动态链接前,首先要进行静态链接,生成部分链接的可执行目标文件hello。动态链接采用了延迟加载的策略,即在调用函数时才进行符号的映射。使用偏移量表got+过程链接表plt实现函数的动态链接。got中存放函数目标地址,为每个全局函数创建一个副本函数,并将对函数的调用转换成对副本函数调用。.got全局偏移表   .plt程序链接表

查看dl_init函数调用前后.got.plt节的变化。

根据.elf文件可知,got起始表位置为0x403ff0,

 

图5.11 got起始位置

    调用前:.got表位置在调用dl_init之前0x403ff0后的16个字节均为0.

 

图5.12 .got初始状态

    调用后:.got位置发生变化,存入地址。

 

图5.13 .got结束状态

5.8 本章小结

本章主要介绍了链接的概念与作用,并介绍了hello.o通过链接生成可执行文件的过程,同时详细介绍了hello的ELF格式和各个节的含义并且与上一章中hello.o生成的ELF文件进行对比,并且分析了hello的虚拟地址空间、重定位过程、执行流程、动态链接过程。

(第5章1分)

第6章 hello进程管理

6.1 进程的概念与作用

概念:进程是操作系统对一个正在运行的程序的一种抽象,是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。狭义定义:进程是正在运行的程序的实例(an instance of a computer program that is being executed)。广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。

作用:它提供一个假象,好像我们的程序独占地使用内存系统,处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。多进程可以完成多任务,每个进程就好比一家独立的公司,每个公司都各自在运营,每个进程也各自在运行,执行各自的任务。

 

图6.1进程

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

作用:交互性命令解释器,把用户输入的命令翻译给操作系统。提供了一个界面,用户可以通过这界面访问操作系统内核。

处理流程:

(1)从终端读取用户输入的命令行,并将命令行进行切分得到参数;

(2)分析命令行字符串,若是内置命令,则立即执行;

 (3)如果不是内置命令,则调用fork()创建新子进程,再调用execve()执行指定程序。

6.3 Hello的fork进程创建过程

调用fork ()函数,操作系统会建立当前线程的副本以实现进程的创建,此时原有的进程被称为父进程,复制的进程被称为子进程。需要注意的是,fork()函数的一次调用产生两个结果:若当前执行是父进程,fork()函数返回子进程ID;若当前执行的进程是子进程,fork()函数返回0。如果fork()函数调用时出现错误,进程创建失败,将返回一个负值。

对于实验中的hello.c程序,当我们输入 ./hello 7203610228 韩雨萌 1 的时候,首先shell对我们输入的命令进行解析,由于我们输入的命令不是一个内置的shell命令,shell会认为是要执行当前目录下的可执行目标文件hello,因此shell会调用fork()创建一个子进程,得到与父进程完全相同的数据空间,栈,堆等资源,程序开始执行。

6.4 Hello的execve过程

当创建了一个子进程之后,子进程调用exceve()函数在当前子进程的上下文加载并运行一个新的程序即hello程序它会覆盖当前进程的地址空间,但并没有创建一个新进程。具体过程如下:

    exceve函数加载并运行可执行目标文件,当出现错误时,exceve会返回到调用程序,否则,exceve调用一次且不返回。在exceve调用加载器加载可执行目标文件,加载器删除子进程现有的虚拟内存段,并创建新的代码、数据、堆和栈段调用启动代码,启动代码设置栈,将可执行目标文件中的代码和数据从磁盘复制到内存中,然后通过跳转到hello程序的第一条指令或入口点来运行该程序,由此将控制传递给新程序的主函数。

6.5 Hello的进程执行

结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。

进程上下文信息:操作系统内核采用一种成为上下文切换的较高层形式的异常控制刘来实现多任务,内核为每一个进程维持一个上下文。上下文切换:(1)保存当前进程的上下文;(2)恢复某个先前被抢占的进程被保存的上下文;(3)将控制传递给这个新恢复分进程。

 

图6.2 进程执行过程

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

进程调度过程:在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就是调度。

用户态与核心态转换:处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中, 用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。

对于hello的进程执行,具体过程如下:键盘输入./hello 7203610228 韩雨萌 1,首先shell通过加载器加载可执行目标文件hello,操作系统进行上下文切换,切换到hello的进程中,此时为用户态,执行完相应函数后,调用sleep函数,进入内核态,当sleep的时间完成后时定时器发送一个中断信号,此时进入内核状态执行中断处理,将hello进程从等待队列中移出重新加入到运行队列,上下文切换再进入hello进程,回到用户态。

 

图6.3运行hello

6.6 hello的异常与信号处理

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

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

  1. 异常和信号异常可以分为四类:中断、陷阱、故障、终止

中断:处理器外部I/O设备引起,异步异常,例如:时钟中断,键盘上敲击Ctrl-C.

陷阱:有意的异常,执行指令产生的结果,发生时间可预知,同步异常,例如:系统调用。

故障:不是有意的,但可能被修复,同步异常,例如:缺页故障,保护故障。

终止:非故意,不可恢复非致命错误造成,例如:非法指令,奇偶校验错误。

  1. 键盘上各种操作导致的异常:

程序正常执行结果:

 

图6.4 正常运行结果

执行过程中按回车:

 

图6.5执行过程中按回车

执行过程中按Ctrl-C,程序终止:

 

图6.6执行过程中按Ctrl-C

执行过程中按Ctrl-Z:

 

图6.6执行过程中按Ctrl-Z

输入ps:

 

图6.7输入ps

输入jobs:

 

图6.7输入jobs

输入pstree:

 

图6.8输入pstree

输入fg:

 

图6.9输入fg

输入kill:

 

图6.10输入kill

执行过程中乱按:

 

图6.11执行过程中按键盘

输入ctrl-z默认结果是挂起前台的作业,hello进程并没有回收,而是运行在后台下,使用ps命令可以看到,hello进程并没有被回收。输入fg将其调到前台,此时 shell 程序首先打印 hello 的命令行命令, hello 继续运行打印剩下的 内容,之后输入kill,程序结束,同时进程被回收。

6.7本章小结

本章介绍了进程的概念和作用,以及壳Shell-bash的作用与处理流程,调用 fork 创建新进程,调用 execve函数执行 hello,最后介绍了执行过程中的异常与信号处理。

(第6章1分)

第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:在有地址变换功能的计算机中,访内指令给出的地址 (操作数) 叫逻辑地址,也叫相对地址。逻辑地址用来指定一个操作数或指令,它由选择符和偏移量组成。是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 [段地址:偏移地址]。在hello.o及hello.o的ELF格式文件中可以看到偏移量及相对偏移地址

线性地址:(Linear Address)也叫虚拟地址(virtual address)是逻辑地址到物理地址变换之间的中间层,即连续的虚拟地址。

虚拟地址:是Windows程序时运行在386保护模式下,这样程序访问存储器所使用的逻辑地址称为虚拟地址,与实地址模式下的分段地址类似,虚拟地址也可以写为“段:偏移量”的形式,这里的段是指段选择器。Hello反汇编的地址即为虚拟地址。

物理地址:计算机系统的主存被组织成一个由M个连续的字节大小的单元组成的数组,每字节都有一个唯一的物理地址。在存储器里以字节为单位存储信息,为正确地存放或取得信息,每一个字节单元给以一个唯一的存储器地址,称为物理地址(Physical Address),又叫实际地址或绝对地址。在得到hello虚拟地址后,通过查询页表等分页机制,将其转化为物理地址,到内存或磁盘中寻址,读取文件。

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

一个逻辑地址由两部份组成,段标识符: 段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节。段选择符格式如下:

​​​​​​​

 

图7.1段选择符格式

段描述符格式如下:

​​​​​​​

 

图7.2段描述符格式

逻辑地址到线性地址的转换过程:linux是通过分段机制,将逻辑地址转化为线性地址。通过数据段寄存器ds,可以找到此进程数据段所在的段描述符,再通过段描述符找到相应的线性地址。寄存器ds中保存了16位的段选择符,段选择符格式如下: TI:指明段描述符是在全局描述符表(GDT, TI=0)中或局部描述符表(LDT, TI=1)中 index:指定该段在GDT或LDT中的位置。看段选择符的T1=0还是1,声明当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。使用段选择符中的前13位,在这个数组中查找到对应的段描述符,即可得到它的基地址,基地址Base + offset即要转换的线性地址。

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

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

任意时刻虚拟页都被分为三个不相交的子集:

未分配的:VM系统还未分配的页

缓存的:当前已经缓存在物理内存的已分配页

未缓存的:当前未缓存在物理内存的已分配页

使用页表的地址翻译:CPU中的控制寄存器,页表基址寄存器指向当前页表,n位虚拟地址包含两部分,一个p位的虚拟页面偏移(VPO)和一个(n-p)位的虚拟页号(VPN),MMU利用VPN来选择适当的PTE,例如:VPN 0选择PTE 0,VPN 1选择PTE 1,以此类推。将页表中的物理页号PPN与虚拟地址中的VPO串联起来,就得到相应的物理地址,具体过程如下图:

 

图7.3变换过程

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

利用TLB加速地址翻译:TLB是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块。TLB命中时具体过程如下:

第一步:CPU产生一个虚拟地址;

第二步和第三步:MMU从TLB中取出相应的PTE。

第四步:MMU将这个虚拟地址翻译成一个物理地址,并将它发送到高速缓存/主存。

第五步:高速缓存/主存将所请求的数据字返回给CPU。

 

图7.4变换过程

当TLB不命中时,MMU必须从L1缓存中取出相应的PTE,新取的PTE存放在TLB中,可能覆盖一个以及存在的条目。

四级页表支持:在四级页表层次结构的地址翻译中,虚拟地址被划分为4个VPN和1个VPO。每个第i个VPN都是一个到第i级页表的索引,第j级页表中的每个PTE都指向第j+1级某个页表的基址,第四级页表中的每个PTE包含某个物理页面的PPN,或者一个磁盘块的地址。为了构造物理地址,在能够确定PPN之前,MMU必须访问四个PTE。

 

图7.5变换过程

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

使用上一步得到的PA,首先取组索引对应位,向L1cache中寻找对应组。如果存在,则比较标志位,并检查对应行的有效位是否为1。如果上述条件均满足则命中。否则按顺序对L2cache、L3cache、内存进行相同操作,直到出现命中。然后向上级cache返回直到L1cache。如果有空闲块则将目标块放置到空闲块中,否则将缓存中的某个块驱逐,将目标块放到被驱逐块的原位置。

 

图7.6将VM于物理寻址的高速缓存结合起来

7.6 hello进程fork时的内存映射

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

7.7 hello进程execve时的内存映射

execve函数在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。

加载并运行过程如下

(1)删除已存在的用户区域

(2)映射私有区域

(3)映射共享区域

(4)设置程序计数器(PC)

 

图7.7堆

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

DRMA缓存不命中称为缺页,如果程序执行过程中遇到了缺页故障,触发缺页异常,则内核调用缺页处理程序。处理程序会进行如下步骤:检查虚拟地址是否合法,如果不合法则触发一个段错误,程序终止。然后检查进程是否有读、写或执行该区域页面的权限,如果不具有则触发保护异常,程序终止。在两步检查都无误后,内核选择一个牺牲页面,如果该页面被修改过则内核会将它复制回磁盘,换入新的页面并更新页表。然后将控制转移给hello进程,再次执行触发缺页故障的指令。

 

图7.8Linux缺页处理

7.9动态存储分配管理

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

 

图7.9 堆

分配器分为两种基本风格:显式分配器、隐式分配器。

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

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

堆中的块主要组织为两种形式:

1.隐式空闲链表(带边界标记)

对比于显式空闲链表,代表并不直接对空闲块进行链接,而是将对内存空间中的所有块组织成一个大链表,在块的首尾的四个字节分别添加header和footer,负责维护当前块的信息(大小和是否分配)。由于每个块是对齐的,所以每个块的地址低位总是0,可以用该位标注当前块是否已经分配。可以利用header和footer中存放的块大小寻找当前块两侧的邻接块,方便进行空闲块的合并操作。优点是简单,缺点是搜索所需时间与堆中以分配块和空闲块的总数成线性关系。

 

图7.10 用隐式链表来组织堆

2.显式空闲链表

堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个pred(前驱)和succ(后继)指针。分别指向前一个空闲块和后一个空闲块。采用该策略,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。将空闲块组织成链表形式的数据结构。

7.10本章小结

本章介绍了在hello的内存管理,虚拟地址、物理地址、线性地址、逻辑地址的区别以及它们之间的变换模式,以及段式、页式的管理模式,基于内存映射重新认识fork和execve,同时介绍了动态存储分配的方法与原理。

(第7章 2分)

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:所有IO设备都被模型化为文件甚至是内核,所有的输入和输出都能被当做相应文件的读和写来执行。

设备管理:unix io接口,Linux内核有一个简单、低级的接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。

8.2 简述Unix IO接口及其函数

1. 接口的操作

(1)打开文件:程序要求内核打开相应文件,表示想要访问I/O文件,内核返回一个小的非负整数(描述符),用于标识这个文件。返回的描述符总是在当前没有打开的最小描述符。内核记录这个描述符,程序在只要记录这个描述符便能记录打开文件的所有信息。

(2)shell在进程的开始为其打开三个文件:标准输入、标准输出和标准错误。

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

(4)读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的文件,当k>=m时执行读操作会出发一个称为EOF的条件,应用程序能检测到这个条件,在文件结尾处并没有明确的EOF符号。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。

(5)关闭文件:内核释放打开文件时创建的数据结构以及占用的内存资源,并将描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。

2. 函数

(1)打开文件:int open(char* filename,int flags,mode_t mode)

Flags参数指明进程如何访问文件: O_RDONLY(只读),O_WRONLY(只写),O_RDWR(可读写)O_CREAT(如果文件不存在,就创建它的一个截断文件),0_TRUNC(如果文件已存在,就截断它),0_APPEND(在每次写操作前设置文件到文件结尾处)

mode: 指定新文件的访问权限位

返回值:成功则为文件描述符,失败则返回-1

(2)关闭文件:int close(int fd)

返回值:成功则返回0,失败则返回-1

(3)读文件ssize_t read(int fd,void *buf,size_t n)

read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。

返回值:成功则返回读的字节数,出错则返回-1,EOF返回0

(4)写文件ssize_t wirte(int fd,const void *buf,size_t n)

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

返回值:成功则返回写的字节数,出错则返回-1

8.3 printf的实现分析

  1. 查看printf函数:

 

图8.1 printf函数

  1. 查看vsprintf函数:vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出,vsprintf只实现了对16进制的格式化。

 

图8.2 vsprintf函数

  1. 查看write函数:

 

图8.3 write函数

  1. 查看sys_call函数:

 

图8.4 sys_call函数

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

8.4 getchar的实现分析

1.getchar()函数运行时,用户通过键盘输入控制权交给os,输入的内容便会显示在屏幕上。按下回车键表示输入完成,这时控制权将被交还给程序。

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

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

8.5本章小结

本章介绍了Linux下的IO设备管理方法,Unix I/O接口及其函数,最后分析了printf和getchar两个函数的实现过程。

(第8章1分)

结论

Hello的一生不可不称的上是历经坎坷与风霜的一生,但又是无限荣光的一生。数不清的操作只为屏幕上华丽的却又转瞬即逝的表演。下面是对hello一生的总结。

1.hello.c(program)的编写,在IDE中编写c语言程序。

2.预处理,得到hello.i文件。

3.编译,得到hello.s汇编文件。

4.汇编,得到二进制可重定位目标文件hello.o。

5.链接,得到可执行文件hello。至此,hello才可以被执行。

6.创建子进程(process):在shell中运行hello程序时,会调用fork()函数创建子进程。这就是Hello的P2P过程。

7.加载程序:exceve()函数加载运行hello程序。

8.执行指令:CPU为hello分配时间片,使hello独享,顺序执行逻辑控制流。

9.访问内存:存储管理(OS)与MMU实现从虚拟地址(VA)到物理地址(PA)的转换。TLB、多级页表、cache、DRAM内存、动态内存分配器相互协作,共同完成内存的管理。

10.交互:hello输入输出与外界交互通过linux I/O实现。

11.终止:hello程序结束,父进程负责回收终止的hello进程,内核删除为hello进程创建的所有数据结构。至此,hello的一生落下帷幕,赤手而来,赤手而去,无牵无挂,孑然一身。

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

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值