哈工大计算机系统大作业

计算机系统

大作业

题     目  程序人生-Hello’s P2P 

专       业  计算机类               

学     号                

班     级                   

学       生                 

指 导 教 师                    

计算机科学与技术学院

2021年6月



摘  要

本文以程序hello.c为线索,以其预处理、编译、汇编、链接、执行的过程为例,从P2P,O2O的角度系统地介绍了一个C语言程序如何从源程序变成可执行程序,到最终被CPU运行的整个过程。

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

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

                   目  录                 

第1章 概述... - 4 -

1.1 Hello简介... - 4 -

1.2 环境与工具... - 4 -

1.3 中间结果... - 4 -

1.4 本章小结... - 5 -

第2章 预处理... - 6 -

2.1 预处理的概念与作用... - 6 -

2.2在Ubuntu下预处理的命令... - 6 -

2.3 Hello的预处理结果解析... - 7 -

2.4 本章小结... - 7 -

第3章 编译... - 8 -

3.1 编译的概念与作用... - 8 -

3.2 在Ubuntu下编译的命令... - 8 -

3.3 Hello的编译结果解析... - 8 -

3.4 本章小结... - 12 -

第4章 汇编... - 13 -

4.1 汇编的概念与作用... - 13 -

4.2 在Ubuntu下汇编的命令... - 13 -

4.3 可重定位目标elf格式... - 13 -

4.4 Hello.o的结果解析... - 16 -

4.5 本章小结... - 17 -

第5章 链接... - 18 -

5.1 链接的概念与作用... - 18 -

5.2 在Ubuntu下链接的命令... - 18 -

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

5.4 hello的虚拟地址空间... - 21 -

5.5 链接的重定位过程分析... - 21 -

5.6 hello的执行流程... - 23 -

5.7 Hello的动态链接分析... - 24 -

5.8 本章小结... - 24 -

第6章 hello进程管理... - 25 -

6.1 进程的概念与作用... - 25 -

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

6.3 Hello的fork进程创建过程... - 25 -

6.4 Hello的execve过程... - 25 -

6.5 Hello的进程执行... - 26 -

6.6 hello的异常与信号处理... - 26 -

6.7本章小结... - 28 -

第7章 hello的存储管理... - 29 -

7.1 hello的存储器地址空间... - 29 -

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

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

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

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

7.6 hello进程fork时的内存映射... - 32 -

7.7 hello进程execve时的内存映射... - 32 -

7.8 缺页故障与缺页中断处理... - 33 -

7.9动态存储分配管理... - 34 -

7.10本章小结... - 35 -

第8章 hello的IO管理... - 36 -

8.1 Linux的IO设备管理方法... - 36 -

8.2 简述Unix IO接口及其函数... - 36 -

8.3 printf的实现分析... - 36 -

8.4 getchar的实现分析... - 38 -

8.5本章小结... - 39 -

结论... - 40 -

附件... - 41 -

参考文献... - 42 -





第1章 概述





1.1 Hello简介

P2P:首先在vim,记事本等编辑器下编辑好hello.c源文件,然后经过cpp预处理可得到修改了的源程序hello.i,经过cc1编译可得到汇编程序hello.s,经过as汇编可得到可重定位目标程序hello.o,最后经ld链接可得到可执行目标程序hello。在shell中输入运行该程序的命令后,shell会通过fork创建一个子进程,至此hello就从一个program变成了一个process,即P2P。

O2O:fork一个子进程后,子进程便有了独立的虚拟地址空间,然后调用execve加载并运行hello程序,加载完成后处理器便开始执行该程序的指令,期间将需要的指令或数据载入物理内存,CPU为运行着的hello程序分配时间片执行逻辑控制流,运行结束后,父进程shell负责回收终止的hello子进程,之后操作系统内核会从系统中删除hello的所有痕迹,即O2O。





1.2 环境与工具

硬件环境:Intel Core i5-9300H CPU @ 2.40GHz;8G RAM;512G SSD

软件环境:Microsoft Windows 10 家庭中文版;Vmware 11;Ubuntu 18.04.5 LTS

开发和调试工具:Visual Studio Code;vim;gcc;gdb;objdump;readelf

1.3 中间结果

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

中间结果文件名字

文件作用

hello.i

hello.c经cpp预处理得到的修改了的源文件

hello.s

hello.i经过cc1编译得到的汇编文件

hello.o

hello.s经过as汇编得到的可重定位目标文件

hello

hello.o经ld链接得到的可执行目标文件

1.4 本章小结

本章主要介绍了hello的P2P和O2O过程,列出了大作业所用到的硬件、软件环境,开发调试工具,列举出了生成的中间结果文件的名字及作用。

(第1章0.5分)





第2章 预处理





2.1 预处理的概念与作用

预处理的概念:预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。如将引用的库、头文件等插入到程序文本中,得到一个新的C程序,通常以 .i作为文件扩展名。预处理的内容包括:宏定义,文件包含,条件编译、布局控制等。

预处理的作用:

1、文件包含:将引用的文件插入源程序文本中。如#include。

2、条件编译:进行编译时进行有选择的挑选,注释掉一些指定的代码,以达到版本控制、防止对文件重复包含。如#if,#ifndef,#ifdef,#endif,#undef等。

3、布局控制:为编译程序提供非常规的控制流信息。如#pragma。

4、宏替换:这是最常见的用法,它可以定义符号常量、函数功能、重新命名、字符串的拼接等各种功能。如#define。

2.2在Ubuntu下预处理的命令

linux> gcc -E ./hello.c -o ./hello.i

图2-2-1预处理过程

2.3 Hello的预处理结果解析

打开预处理生成的hello.i文件可以看到,预处理后的文件有3110行,相比于预处理之前29行的源程序文件多出了很多内容。预处理器将预处理指令#include 替换为了系统头文件stdio,h中的内容,同理也将unistd.h,stdlib.h系统头文件里的内容插入到了源程序文本中。

图2-3-1预处理结果

2.4 本章小结

本章主要介绍了预处理的概念及作用,在linux下对hello.c源文件进行了预处理操作,并对预处理的结果进行了解析。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

编译的概念:编译即编译器(ccl)将文本文件hello.i翻译成文本文件hello.s的过程,得到的hello.s文件包含一个汇编语言程序。

编译的作用:编译器首先检查代码的规范性、是否有语法错误等,并确定代码实际要做的工作,在检查无误后,将高级语言程序翻译成汇编语言程序。除此基本功能外,编译器还具有目标程序优化等功能。

3.2 在Ubuntu下编译的命令

命令:linux> gcc -S ./hello.i -o ./hello.s

图3-2-1编译过程

3.3 Hello的编译结果解析

3.3.1 对各数据类型的处理

1.全局变量

hello.c中定义了一个int型全局变量sleepsecs,并赋初值为2.5,由于sleepsecs是int类型,所以只保留了整数部分2,如图3-3-1-1.

图3-3-1-1全局变量sleepsecs的信息

2.局部变量

局部变量一般存储在栈中或寄存器中,hello.c源文件的main函数里定义了一个局部变量int i,查看hello.s文件可以看到,i在栈中-4(%rbp)的地方,如图3-3-1-2。

图3-3-1-2局部变量i的位置

3.字符串

程序中共出现了两个字符串,均为printf函数的格式控制字符串,如图3-3-3.

图3-3-1-3程序中的字符串

3.3.2 赋值操作

hello.c中有一条赋值语句i=0,赋值在汇编语言中通过mov指令实现,如图3-3-2-1,根据操作数类型的不同,可分为

movb:传送字节

movw:传送字(2个字节)

movl:传送双字(4个字节)

movq:传送四字(8个字节)

图3-3-2-1汇编中的赋值语句

3.3.3类型转换

hello.c中出现了隐式类型转换,即给int型全局变量sleepsecs赋的初值为2.5,但由于sleepsecs是int类型,所以只保留了整数部分2。

图3-3-3全局变量sleepsecs的值

3.3.4 算术操作

hello.c中出现的算术操作为for循环中的i++,汇编中用addl指令实现,如图3-3-4。

图3-3-4 i++的汇编指令

3.3.5 关系操作

1.if语句中出现的argc!=3,在汇编中用cmpl指令实现,该指令计算argc-3的值,并根据结果设置条件码,后续根据条件码中的零标志ZF判断argc和0是否相等,并决定是否跳转。

图3-3-5-1 关系操作!=的汇编指令

2.for循环的循环条件i<10,在汇编中用cmpl指令实现,此处计算i-9的值,并根据结果设置标志位,后续根据符号标志SF和零标志ZF判断i是否<=9,并决定是否跳转。

图3-3-5-2 关系操作<的汇编指令

3.3.6 数组/指针

main函数的第二个参数argv[]是一个字符串数组,数组内容为指向字符类型的指针。由汇编代码可以看出argv的首地址存放在栈中的-32(%rbp)处

图3-3-6-1 argv首地址在栈中的位置

图3-3-6-2中箭头所表示的指令为对argv[2]的访问,方框所标注的内容为对argv[1]的访问,均是通过取出数组首地址,再加上相应的偏移量来访问数组中的特定元素。

图3-3-6-2 对argv的访问

3.3.7 控制转移

1.if (argc != 3),在汇编中用cmpl指令实现,该指令计算argc-3的值,并根据结果设置条件码,后续根据条件码中的零标志ZF判断argc和0是否相等,并决定是否跳转。

2. for(i=0;i<10;i++),i被赋初值0后,程序无条件跳转到.L3,.L3中先用cmpl指令计算i-9的值,并根据结果设置标志位,若i<=9,则跳转到.L4执行循环体,.L4结束时将i++并执行.L3,以此规则重复循环。

图3-3-7-1 控制转移之if/else

图3-3-7-2 控制转移之for循环

3.3.8 函数操作

main函数的调用过程:

1.传递控制:系统启动函数__libc_start_main使用call指令,将返回地址(即下一条指令的地址)入栈,然后跳转到main 函数的起始地址。hello.c源程序中的return 0,对应汇编中将%eax 设置为0,然后使用ret指令返回。

2.参数传递:第一个参数argc保存在%rdi中,第二个参数argv保存在%rsi中,若有更多参数,依次保存在%rdx,%rcx,%r8,%r9中,若有更多参数则保存在栈中。

3.栈帧的分配和释放:将%rbp 作为栈底指针,栈顶指针%rsp下移,为被调用者开辟栈帧。程序结束时,调用leave 指令,恢复栈空间为调用之前的状态,然后通过ret返回。

3.4 本章小结

本章介绍了编译的概念及作用,在linux下实际执行了编译操作,并结合生成的汇编文件hello.s,说明了编译器处理hello.c中出现的C语言的各个数据类型(全局变量、局部变量、字符串)以及各类操作(赋值、类型转换、算术、关系、数组/指针、控制转移和函数操作)的流程和方法。

(第32分)

第4章 汇编

4.1 汇编的概念与作用

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

汇编的作用:将.s文件中的汇编指令翻译成对应的二进制机器指令,汇编生成的可重定位目标文件可以与其他可重定位目标文件链接,形成一个可执行目标文件。

4.2 在Ubuntu下汇编的命令

linux> gcc -c ./hello.s -o ./hello.o

图4-2-1汇编过程

4.3 可重定位目标elf格式

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

图4-3-1 ELF头的格式

2. 节头部表(section header table):描述了各节的名称、类型、地址、偏移量、大小、对齐要求等信息

图4-3-2 节头部表

3. 重定位节.rela.text:一个.text 节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。节中的8个重定位条目分别是对.L0、puts 函数、exit 函数、.L1、printf 函数、 sleepsecs、sleep函数、getchar 函数重定位信息的描述。其中各项的含义如下:

偏移量Offset:是需要被修改的引用的节偏移。

符号名称Symbol:标识被修改引用应该指向的符号。

类型Type:告知连接器如何修改新的引用。

加数Addend:一个有符号常数,一些类型的重定位需使用它对被修改引用的值做偏移调整。

图4-3-3 重定位节.rela.text

4. 符号表.symtab:存放在程序中定义和引用的函数和全局变量的信息。链接器进行重定位需要引用的符号都在其中声明。各项含义如下:

name:字符串表中的字节偏移,指向符号的以null结尾的字符串名称。

value:对于可重定位目标模块,是距定义目标的节的起始位置偏移,对于可执行目标文件,该值是一个绝对运行的地址。

size:目标的大小(以字节为单位)。

type:通常要么是数据要么是函数。

Bind:字段表明符号是本地的还是全局的。

图4-3-4 符号表

4.4 Hello.o的结果解析

1. 汇编语言与机器语言是一一对应的关系,即一条汇编指令对应一条机器指令,因为汇编指令本质上就是机器语言的助记符。

2. 对全局变量sleepsecs的访问,hello.s中使用sleepsecs(%rip) ,hello.o的反汇编中使用0x0(%rip),默认值为0,重定位后将更新为sleepsecs的实际值。

3. 分支转移:hello.s中直接跳转的跳转目标是用.L2,.L3,.L4等标号表示的,而hello.o的反汇编中跳转目标main+偏移量来表示的。

4. 函数调用:hello.s中call指令后跟的是函数名称,hello.o的反汇编中call指令的目标地址是main+偏移量(定位到call的下一条指令),汇编器会在.rela.text 节中为其添加重定位条目,待链接时确定函数的运行时地址,然后更新call指令的编码。

图4-4-1 hello.o的反汇编

4.5 本章小结

本章介绍了汇编的概念及作用,在linux下实际执行了汇编操作,并对生成的可重定位目标文件hello.o的ELF格式进行了详细分析,着重介绍了ELF头、节头部表、重定位节、符号表,最后将hello.o的反汇编结果与之前生成的hello.s文件进行对比,更清晰地理解了汇编的工作原理。

(第41分)

第5章 链接

5.1 链接的概念与作用

链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时,也就是在源代码被编译成机器代 码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至于运行时,也就是由应用程序来执行。链接是由叫做链接器的程序自动执行的,链接器使得分离编译成为可能。

5.2 在Ubuntu下链接的命令

linux> ld -o hello hello.o -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/x86_64-linux-gnu/libc.so /usr/lib/ x86_64-linux-gnu/crtn.o

图5-2-1 执行链接命令

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

分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。

1. ELF头:描述了文件的总体格式,相比于hello.o可重定位目标文件,hello的类型变成了EXEC(可执行文件),入口点地址也已确定。

图5-3-1 ELF头

2. 程序头部表(program header table):为链接器提供运行时的加载内容和提供动态链接的信息,每一个条目包含各段在目标文件中的偏移、在虚拟地址空间中的位置、目标文件中的段大小、内存中的段大小、运行时访问权限和对齐方式。

图5-3-2 程序头部表

3. 节头部表(Section Headers):共有25个节头,描述了各个节的名称、类型、地址、相对于文件开始的偏移量、大小、对其要求等信息。各节已经被重定位到它们最终的运行时内存地址。

图5-3-3节头部表

5.4 hello的虚拟地址空间

由图5-3-2程序头部表可以看出,代码段从虚拟地址空间的0x400000处开始,用edb加载hello,可以看出程序确实从0x400000处开始,第一部分为ELF头。

图5-4-1 edb查看代码段

5.5 链接的重定位过程分析

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

结合hello.o的重定位项目,分析hello中对其怎么重定位的。

1. hello的反汇编代码有确定的运行时地址,说明已经完成了重定位,而hello.o反汇编代码中涉及到运行时地址的地方均标记为0,如图5-5-1。

图5-5-1 hello反汇编中确定的运行时地址

2. hello的反汇编代码增加了.plt,.init,.fini节。与hello.o链接的库函数的代码都已经插入到了程序中,如图5-5-2。

图5-5-2 hello反汇编中增加的部分

重定位过程分析:

1. 重定位节和符号定义:在这一步中,链接器将所有相同类型的节合并为同一类型的新的聚合节,然后连接器将运行时地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。这一步完成后,程序中每条指令和全局变量都有了唯一的运行时内存地址。

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

偏移量Offset:是需要被修改的引用的节偏移。

符号名称Symbol:标识被修改引用应该指向的符号。

类型Type:告知连接器如何修改新的引用。

加数Addend:一个有符号常数,一些类型的重定位需使用它对被修改引用的值做偏移调整。

图5-5-3 hello.o的重定位条目

以全局变量sleepsecs的重定位为例:

图5-5-4 sleepsecs的重定位条目

ADDR(main)= 0x400500

ADDR(sleepsecs)= 0x601040

offset = 0x60

addend = -4

refaddr= ADDR(main)+offset=0x400500+0x60=0x400560

*refptr=ADDR(sleepsecs)+r.addend-refaddr=0x601040+(-0x4)-0x400560

=(unsigned)0x200ADC

图5-5-5 hello反汇编中重定位后对sleepsecs的引用

5.6 hello的执行流程

子程序地址                 子程序名称

0x0000000000400488  _init

0x00000000004004b0  puts@plt

0x00000000004004c0  printf@plt

0x00000000004004d0  getchar@plt

0x00000000004004e0  exit@plt

0x00000000004004f0  sleep@plt

0x0000000000400500  main

0x0000000000400590  _start

0x00000000004005c0  _dl_relocate_static_pie

0x00000000004005d0  __libc_csu_init

0x0000000000400640  __libc_csu_fini

0x0000000000400644  _fini

5.7 Hello的动态链接分析

假设程序调用了共享库里的函数,编译器无法其运行时地址,为了能使得代码段里对数据及函数的引用与具体地址无关,链接器采用延迟绑定的策略。延迟绑定是通过全局偏移量表(GOT)和过程链接表(PLT)之间的交行实现的。如果一个目标模块调用定义在共享库中的任何函数,那么它就有自己的GOT和PLT。GOT是数据段的一部分,PLT是代码段的一部分。

图5-7-1 GOT的信息

5.8 本章小结

本章主要介绍了链接的概念及作用,并在linux下实际进行了链接操作,并对生成的hello可执行文件的ELF格式、虚拟地址空间、重定位过程、执行流程、动态链接等方面进行了详细分析。

(第51分)

第6章 hello进程管理

6.1 进程的概念与作用

进程的概念:进程就是一个运行中的程序的实例。

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

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

Shell-bash的作用:Linux系统中,Shell是一个交互型应用级程序,代表用户运行其他程序,其基本功能是解释并运行用户的指令。

处理流程:

1. shell读取用户由键盘输入的命令行。

2. shell解析命令,获取命令行参数,将各参数保存在argv中。

3. 检查第一个命令行参数是否是一个内置命令,若是,调用相应处理程序,否则调用fork()创建子进程,在子进程中,根据argv中的的参数,调用execve执行指定程序。

4. 若用户未要求后台运行,则shell等待作业终止后将其回收。否则将进程转入后台运行,开始等待用户输入下一个命令。

6.3 Hello的fork进程创建过程

当用户在shell中输入./hello 1190202306 宁天弛,shell就会通过调用fork()函数创建一个新的运行的子进程,新创建的子进程几乎但不完全与父进程相同,子进程得到与父进程虚拟地址空间相同的(但是独立的)一份副本,包括代码段、数据段、共享库以及用户栈。子进程还获得与父进程打开文件描述符相同的一份副本,父进程和子进程的不同在于他们的PID不同。父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的逻辑控制流的指令。

6.4 Hello的execve过程

fork 创建子进程之后,子进程调用execve 函数在当前进程的上下文中加载并运行一个新程序hello。新程序会覆盖覆盖当前进程的代码、数据、栈,但拥有和当前进程相同的PID,并继承已打开的文件描述符和信号上下文。execve函数加载并运行可执行目标文件hello,创建一组新的代码、数据、堆和栈段,设置PC 指向_start 的地址,调用main函数,并将控制传递给新程序的主函数,同时传递参数列表argv和环境变量envp。如果出现错误,如hello文件不存在,execve会返回到调用程序,否则execve调用一次从不返回。

6.5 Hello的进程执行

上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构。

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

进程调度:当内核选择一个新的进程运行时,称内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程,上下文切换包含:

1. 保存当前进程的上下文

2. 恢复某个先前被抢占的进程被保存的上下文

3. 将控制传递给这个新恢复的进程。

hello程序开始时运行在用户模式,在调用 sleep 之后转入内核模式,内核处理休眠请求,定时器开始计时,内核进行上下文切换将当前进程的控制权交给其他进程,当休眠结束时定时器发送一个中断信号,此时进入内核状态执行中断处理程序,将hello进程从等待队列中移出并重新加入到运行队列,之后hello进程就可以继续进行自己的逻辑控制流。

图6-5 上下文切换(图源百度)

6.6 hello的异常与信号处理

6.6.1 异常

1. 中断:程序运行过程中随时可能有来自外部I/O设备的信号引起的中断。

2. 陷阱:使用了sleep,exit等系统调用。当用户程序调用sleep函数时,会执行一个syscall指令,将控制转移给内核,内核将运行陷阱处理程序,解析参数,调用sleep函数,调用后返回到用户进程中引起异常的下一条指令。

3. 程序执行时可能存在缺页故障。

6.6.2 信号处理

1. 程序正常执行

图6-6-1 程序正常执行

2. Ctrl-Z

在程序执行过程中,用户键入Ctrl+Z,内核会发送一个SIGTSTP信号到前台进程组中的每个进程,子进程hello被停止(挂起),成为后台挂起进程。同时父进程shell收到SIGTSTP信号后,调用信号处理程序,打印提示信息,并开始等待用户输入下一条命令。输入ps命令,可以看到hello在进程列表中。输入fg 1将后台hello程序变更到前台继续运行,hello子进程继续从被停止的位置执行,打印完10条信息后,读入用户输入的任意字符,然后进程终止。此时再次使用ps命令,发现hello已不在进程列表中。

图6-6-2 用户键入Ctrl-Z

3. Ctrl-C

用户通过键盘输入Ctrl-C会导致内核发送一个SIGINT信号到前台进程组的每个进程,默认情况是终止前台作业。按下Ctrl-C后使用ps命令,发现进程列表中没有hello,表明hello已被终止。

图6-6-3 用户键入Ctrl-C

4. 不停乱按

在程序运行中乱按不会影响程序正常运行,输入会被缓存到stdin,当程序运行getchar函数的时候,会读取一个以’\n’结尾的字符串作为输入。

图6-6-4 乱按

6.7本章小结

本章介绍了进程的概念及作用,shell的作用及其处理流程,并分析了hello的fork进程创建过程、execve过程和进程执行过程,最后根据不同情况分析了hello运行过程中的异常和信号处理。

(第61分)

第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:在有地址变换功能的计算机中,访存指令给出的地址 (操作数) 叫逻辑地址,也叫相对地址。要经过寻址方式的计算或变换才得到内存中的实际有效地址,即物理地址。

线性地址:线性地址是逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。

虚拟地址:即线性地址。

物理地址:物理地址用于内存芯片级的单元寻址,CPU对内存的访问是通过连接着CPU和北桥芯片的前端总线来完成的。在前端总线上传输的内存地址都是物理内存地址。

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

在保护模式下,段描述符占8个字节,无法直接存放在段寄存器中(段寄存器只有2字节)。X86的设计是段描述符存放在GDT或LDT中,而段寄存器存放的是段描述符在GDT或LDT内的索引值。一个逻辑地址由两部份组成,段标识符: 段内偏移量。

逻辑地址到线性地址的变换方法:

1. 给定一个完整的逻辑地址[段选择符:段内偏移地址],首先根据T1的值,确定当前要转换是GDT中的段,还是LDT中的段,再依据对应寄存器,得到其地址和大小。

2. 根据段选择符中前13位,在数组中查找到相应的段描述符,获得基地址。

3. 将基地址加上偏移量,就得到要转换的线性地址了。

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

线性地址,即虚拟地址,虚拟内存被分割成称为多个虚拟页(VP),类似地,物理内存被分割为物理页(PP)。系统使用页表将虚拟页映射到物理页,每一个页表条目由有效位和一个n位的地址字段组成。如果设置有效位说明该页已缓存到物理内存,否则未缓存。有效位为0且地址字段不为空时指向一个虚拟页在磁盘上的起始地址。

CPU通过MMU将虚拟地址翻译成物理地址,通过VPN找到对应的页表条目,如果已缓存则命中,否则不命中,发生缺页故障,需要操作系统内核与硬件合作处理。此时MMU会选择一个牺牲页,用将产生缺页的虚拟页替换牺牲页,并更新页表,然后重新执行地址翻译。

图7-3-1 基于页表的地址翻译

图7-3-2 地址翻译用到的符号

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

TLB:每次CPU产生一个虚拟地址,MMU(内存管理单元)就必须查阅一个PTE,以便将虚拟地址翻译为物理地址。为了降低不命中带来的巨大时间开销,在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓存器(TLB)。TLB是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单一PTE组成的块。TLB通常有高的相连度,从虚拟地址中的页号提取出组选择和行匹配的索引和标记字段。

四级页表:VPN被解释成从低位到高位的4段,从高地址开始,第一段VPN作为第一级页表的索引,用以确定第二级页表的基址;第二段VPN作为第二级页表的索引,用以确定第三级页表的基址;第三段VPN作为第三级页表的索引,用以确定第四级页表的基址;第四段VPN作为第四级页表的索引,若该位置的有效位为1,则该表项存储的是PPN。在上述过程中,只要有一级页表条目的有效位为0,下一级页表就不存在,对子页表的访问将产生缺页故障,需要从磁盘载入内存。

图7-4-1 四级页表的原理

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

L1 Cache是8路64 组相联。块大小为 64字节。因为共64组,所以需要 6位 CI作为组索引,因为块大小为64字节所以需要 6位CO表示数据块内偏移,因为PA 共52位,所以 CT 共40位。7.4中我们已经将VA转换为PA,,使用CI进行组索引,将CT与组内的8个块的标记分别进行比较,如果匹配成功 且块的有效位为1,则命中,根据数据块内偏移CO取出数据返回给CPU。如果不命中,就去下一 级缓存中查询数据,以此类推,直到主存。

图7-5-1 三级Cache支持下的物理内存访问

7.6 hello进程fork时的内存映射

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

7.7 hello进程execve时的内存映射

fork创建hello子进程后,在子进程中调用execve函数,加载并运行可执行程序hello,主要步骤如下:

1. 删除已存在的用户区域,也就是将shell与hello都有的区域结构删除。

2. 然后映射私有区域,即为新程序的代码、数据、bss和栈区域创建新的区域结构,均为私有的、写时复制的。映射共享区域,将一些动态链接库映射到hello的虚拟地址空间。

3. 设置PC,使之指向hello程序的代码入口。

经过这个内存映射的过程,在下一次调度hello进程时,就能够从hello的入口点开始执行了。

图7-7-1 execve的内存映射

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

当指令引用一个虚拟地址,通过查找页表发现,该虚拟地址对应的物理地址所在的物理页不在内存中,需要从磁盘调入,即发生了缺页故障。

当发生缺页故障时,控制转移到处理程序,处理程序从磁盘加载相应的页面,然后将控制转移给引起缺页故障的指令。接着指令再次执行,相应的物理页面已被加载到内存中,页面命中。

图7-8-1 缺页中断处理

7.9动态存储分配管理

动态存储分配管理由动态内存分配器完成。动态内存分配器维护着一个进程的虚拟内存区域,称为堆。堆是一个请求二进制零的区域,它紧接在未初始化的数据区后开始,并向上生长(向更高的地址)。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可以用来分配。空闲块保持空闲,直到它显示地被应用程序所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。

显式分配器的实现:

1. 隐式空闲链表:空闲块是通过头部中的大小字段隐含地连接着的。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。

图7-9-1 隐式空闲链表

2. 显式空闲链表:显式空闲链表是将空闲块组织为某种形式的显式数据结构。因为程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。如在每个空闲块中,都包含一个前驱与一个后继指针。

图7-9-2 显式空闲链表的块结构

3. 分离空闲链表:分配器维护一个空闲链表数组,每个空闲链表和一

个大小类关联,链表是显式或隐式的。

图7-9-3 分离空闲链表

7.10本章小结

本章首先介绍了逻辑地址、线性地址、虚拟地址、物理地址的概念及其关系,接着分析了逻辑地址到线性地址的转化、线性地址到物理地址的转化,TLB与四级页表支持下的VA到PA的变换以及三级Cache支持下的物理内存访问。最后介绍了hello进程fork、execve时的内存映射,缺页故障及其处理以及动态存储分配管理。

(第7 2分)

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为UnixI/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。

8.2 简述Unix IO接口及其函数

1. 打开和关闭文件

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

关闭文件:进程通过调用close 函数关闭一个打开的文件。int close(int fd)。

2. 读写文件

应用程序是通过分别调用read 和write 函数来执行输入和输出的。

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

ssize_t write(int fd, const void *buf, size_t n);

3. 读取文件元数据

应用程序可以通过调用stat和fstat函数,检索到关于文件的信息(元数据)。stat函数以一个文件名作为输入,并填写一个stat数据结构中的各个成员。Fstat函数是相似的,只不过是以文件描述符而不是文件名作输入。

int stat(const char *filename, struct stat *buf);

int fstat(int fd, struct stat *buf);

8.3 printf的实现分析

printf参数中的…表示传递参数的个数不确定,arg是一个字符指针,表示…中的第一个参数,即输出的时候格式化串对应的值。

图8-3-1 printf的函数体

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

图8-3-2 vsprintf函数

write(buf,i)函数接受buf与需要输出的参数个数,执行写操作,把buf中的i个元素的值输出。write函数中,先给寄存器传递参数,然后执行系统调用sys_call

图8-3-3 write函数的汇编代码

sys_call将字符串“Hello 1190202306 宁天弛”中每个字符对应的ASCII码值复制到显存中。字符显示驱动子程序根据ASCII找到字模库相应的字形,并将每一个点的RGB颜色信息写入到显示vram,然后系统显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

图8-3-4 sys_call的实现

8.4 getchar的实现分析

用户输入的字符被存放在键盘缓冲区中,当用户键入回车之后,getchar开始从stdin流中每次读入一个字符,getchar函数的返回值是用户输入的第一个字符的ASCII码,如出错返回-1,且将用户输入的字符输出到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完为后,才等待用户按键。

异步异常-键盘中断的处理:当用户按键时,键盘接口会收到一个该按键的键盘扫描码,同时产生一个中断请求,中断请求运行键盘中断子程序,从键盘接口取得该按键的扫描码,将按键扫描码转成ascii码,保存到系统的键盘缓冲区。

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

8.5本章小结

本章介绍了linux的IO设备管理办法、unix IO接口及函数、printf的实现分析和getchar的实现分析。

(第81分)

结论

1、首先在vim,记事本等编辑器下编辑好hello.c源文件。

2、hello.c经过cpp预处理可得到hello.i,cpp进行了向源程序中插入包含的外部库、宏替换等操作。

3、cc1将hello.i编译成汇编程序hello.s。

4、hello.s经过as汇编可得到可重定位目标程序hello.o。

5、hello.o最后经ld链接可得到可执行目标程序hello。

6、在shell中输入运行hello的命令后,shell会通过fork创建一个子进程,至此hello就从一个program变成了一个process。

7、fork一个子进程后,hello进程便有了独立的虚拟地址空间。

8、调用execve加载并运行hello。

9、CPU为运行着的hello程序分配时间片执行逻辑控制流。

10、执行期间需要的指令或数据被从磁盘载入物理内存,还有可能被缓存在cache中。

11、运行结束后,父进程shell负责回收终止的hello子进程,之后操作系统内核会从系统中删除hello的所有痕迹。

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





附件

文件名

作用

hello.i

hello.c经cpp预处理得到的修改了的源文件

hello.s

hello.i经过cc1编译得到的汇编文件

hello.o

hello.s经过as汇编得到的可重定位目标文件

hello

hello.o经ld链接得到的可执行目标文件

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

参考文献

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

[1]  Randal E.Bryant,David R.O'Hallaron.深入理解计算机系统[M].机械工业出版社:北京,2016.7:1.

[2]  https://baike.baidu.com/item/%E9%80%BB%E8%BE%91%E5%9C%B0%E5%

9D%80/3283849?fr=aladdin

[3]  https://www.cnblogs.com/pianist/p/3315801.html

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值