哈工大2022春CSAPP大作业-程序人生(Hello‘s P2P)

计算机系统

大作业

题     目  程序人生-Hello’s P2P 

专       业        计算学部          

学     号        120L020330        

班     级        2003003           

学       生         张仁杰        

指 导 教 师          史先俊         

计算机科学与技术学院

2022年5月

摘  要

    本文以最简单的 hello 程序为例介绍整个程序的生命周期。以 hello.c 源程序为起点,从预处理、编译、汇编、链接,到加载、运行,再到终止、回收。对各个过程的内容与实现进行分析与解释。该论文以hello.c文件为研究对象,结合《深入理解计算机系统》书中的内容与课上老师的讲授,在Ubuntu系统下对hello程序的整个生命周期进行了研究,通过对hello.c程序的深入研究,目的是增强对整个计算机系统的了解。

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

(摘要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简介

P2P: From Program to Process。通过编译器中输入代码得到的hello.c就是一个Program,此时它只是一个文本文件。hello.c经过 cpp 的预处理、ccl的编译、as的汇编、ld的链接最终成为可执目标程序hello在shell中输入./hello后,shell对命令行进行解析,随后fork产生子进程,在子进程中调用execeve,实际执行hello中的代码,就变成了Process。

O2O: From Zero-0 to Zero-0。初始时内存中并无hello文件的相关内容,这便是“From 0”。通过在Shell下调用execve函数,系统会将hello文件载入内存,执行相关代码,当程序运行结束后, hello进程被回收,并由内核删除hello相关数据,这即为“to 0”。

1.2 环境与工具

硬件:Intel(R) Core(TM) i7-8565U CPU @ 1.80GHz

         4GB RAM

软件:Windows10 64位

             Ubuntu 19.04 LTS 64位

调试工具:Visual Studio 2022 64-bit;

gedit,gcc,readelf, objdump, hexedit, edb

1.3 中间结果

hello.i   预处理后得到的文本文件

hello.s  编译后得到的汇编语言文件

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

hello   链接后的可执行文件

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

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

hello1.txt   hello.o的反汇编代码

hello2.txt   hello的反汇编代码

1.4 本章小结

本章简要介绍了hello 的P2P,020的具体含义,同时列出了论文研究时采用的具体软硬件环境和中间结果。

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

概念:预处理步骤是指程序开始运行时,预处理器(cpp,C Pre-Processor,C预处理器)根据以字符#开头的命令,修改原始的C程序的过程。例如,hello.c文件6到8行中的#include 命令会告诉预处理器读取系统头文件stdio.h,unistd.h,stdlib.h 的内容,并把这些内容直接插入到程序文本中。用实际值替换用#define定义的字符串。除此之外,预处理过程还会删除程序中的注释和多余的空白字符。预处理通常得到另一个以.i作为拓展名的C程序。

作用:主要作用是将头文件中的内容直接插入到源文件中,其次是根据宏定 义在源程序中做一些替换工作,最后是删除所有注释。经过预处理过程就可以得 到便于编译器工作的、以.i为后缀的文件。

2.2在Ubuntu下预处理的命令

命令为gcc hello.c -E -o hello.i

图1 输入命令得到的hello.i文件

2.3 Hello的预处理结果解析

以下为hello.i 的部分代码

图2 hello.i的部分代码

在main函数内代码出现之前是大段的头文件 stdio.h unistd.h stdlib.h 的依次展开。展开的具体流程概述如下(以stdio.h为例):CPP先删除指令#include <stdio.h>,并到Ubuntu系统的默认的环境变量中寻找 stdio.h,最终打开路径/usr/include/stdio.h下的stdio.h文件。若stdio.h文件中使用了#define语句,则按照上述流程继续递归地展开,直到所有#define语句都被解释替换掉为止。除此之外,CPP还会进行删除程序中的注释和多余的空白字符等操作,并对一些值进行替换.

2.4 本章小结

本章主要介绍了预处理的概念及作用、并结合Ubuntu系统下hello.c文件实际预处理之后得到的hello.i程序对预处理结果进行了解析。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

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

作用:把源程序翻译成目标程序,进行词法分析和语法分析,分析过程中发现有语法错误,给出提示信息。

注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序

3.2 在Ubuntu下编译的命令

命令:gcc -S hello.i -o hello.s

图3 输入命令得到的hello.s文件

3.3 Hello的编译结果解析

3.3.1 汇编初始部分

下图为hello.s初始的代码

图4 hello.s的部分代码

这些内容分别代表:

.file:声明源文件的文件名

.text:声明代码节

.section .rodata:声明只读数据节

.align 8:表示地址以 8 字节对齐

.string:用于声明字符串,这里声明了 2 个,分别是.LC0 和.LC1

.global:声明 main 为一个全局变量

.type:指定 main 是一个函数

3.3.2 数据类型

       ①字符串常量

       程序中有两个字符串常量,又 hello.s 的头部信息可知,这两个字符串都是存储在只读数据节的。

图5 hello.s的头部信息

从 hello.c 中看,这两个字符串分别是 printf 中输出的固定的字符串,另一个是

printf 中的格式串。

图6 hello.c的输出部分

       ②局部变量

       main函数中定义了一个局部变量i作为循环变量,这里i被编译器处理成存储在栈上,并使用movl为其赋初值。

     图7 hello.s的局部变量

       ③参数argc

       参数 argc 作为用户传给main的参数。也是被放到了堆栈中。

       ④数组:char *argv[]

       char *argv[]是作为main函数的第二个参数,数组的每个元素都是一个指向字符类型的指针。数组的起始地址存放在栈中-32(%rbp)的位置,被两次调用传给printf

   图8 main函数两个参数的行为

3.3.3 赋值

       本程序中只有一个赋值操作:对循环变量i赋初值。这里使用命令”movl $0,-4(%rbp)”对i进行赋初值。”movl”是四字节数据传送指令,对应变量i的数据类型为int型。

3.3.4 算数操作

       本程序中涉及的算数操作只有每次循环后循环变量i自增1的操作(i++),编译器在这里并没有使用INC指令,主要是因为i并没有存储在寄存器上,i在汇编代码中一直体现为存储在栈上,所以只能使用ADD指令。具体来说这里的指令是:”addl $1, -4(%rbp)”。

3.3.5 关系操作

在hello.s中,具体涉及的关系操作包括:

       检查argc是否不等于4。在hello.s中,使用cmpl $3,-20(%rbp),比较 argc与3的大小并设置条件码,为下一步je利用条件码进行跳转作准备。

图9  hello.s中涉及的算数操作

       检查i是否小于8。在hello.s中,使用cmpl $9, -4(%rbp)比较i与7的大小,然后设置条件码,为下一步jle利用条件码进行跳转做准备。

       图10  hello.s中涉及的算数操作

3.3.6 数组操作

       程序中涉及的数组只有 main 函数的参数列表,定义为 char *argv[],是一个字 符串数组,数组中每一个元素都是一个字符串。观察汇编代码中的行为,可以发 现利用索引获取数组元素时的步骤:

1. 获取数组首地址

2. 根据索引值对首地址进行加法操作得到目标元素的首地址

3. 通过内存解析得到目标元素并存储在某块其他区域

       图11  hello.s中涉及的数组操作

3.3.7 控制转移指令

       汇编语言中先设置条件码,然后根据条件码来进行控制转移,在hello.c中,有以下控制转移指令:

①判断argc是否等于4,如果argc等于4,则不执行if语句,否则执行if语句,对应的汇编代码为:

       图12  hello.s中涉及的控制转移指令

②for循环中,每次判断i是否小于8来决定是否继续循环,对应的汇编代码为:

图13  hello.s中涉及的控制转移指令

       先对i赋初值然后无条件跳转至判断条件的.L3中,然后判断i是否符合循环的条件,符合直接跳转至循环体内部.L4中。

3.3.8 函数操作

C语言中,调用函数时进行的操作如下:

       1.传递控制:

       进行过程Q的时候,程序计数器必须设置为Q的代码的起始地址,然后在返回时,要把程序计数器设置为P中调用Q后面那条指令的地址。

       2.传递数据:

       P 必须能够向Q提供一个或多个参数,Q必须能够向P中返回一个值。

       3.分配和释放内存:

       在开始时,Q可能需要为局部变量分配空间,而在返回前,又必须释放这些空间。

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

   图14  hello.s中的函数操作

3.4 本章小结

本章介绍了编译的概念与作用,编译是将文本文件翻译成汇编语言程序,为后续将其转化为二进制机器码做准备的过程。同时,本章以hello.s文件为例,介绍了编译器如何处理各个数据类型以及各类操作,验证了大部分数据、操作在汇编代码中的实现。

(第32分)

第4章 汇编

4.1 汇编的概念与作用

       概念:在这里指使用汇编器 as,将以.s 为后缀的文件处理成.o 为后缀的文件的 过程,这些文件服从可重定位目标文件的格式。

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

注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。

4.2 在Ubuntu下汇编的命令

命令:gcc hello.s -c -o hello.o

图15 输入命令得到的hello.o文件

4.3 可重定位目标elf格式

在linux下生成hello.o文件elf格式的命令:readelf -a hello.o > hello.elf

图15 输入命令得到的hello.elf文件

1.ELF 头

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

图16  ELF头的部分代码

2.节头表(节头部表)

简单来说,节头部表显示了 hello.o 中每个节的名字、类型、地址、偏移、每一个节的操作权限等信息。由于 hello.o 还是可重定位目标文件,所以每个节的地址都从 0 开始。在文件头中得到节头表的信息,然后再使用节头表中的字节偏移信息得到各节在文件中的起始位置,以及各节所占空间的大小。

图17  ELF节头的部分代码

3.重定位条目所在节

这个节用于记录.text 节中需要进行重定位的信息,根据这些信息之后链接器会对.text 节进行修改,在链接器执行完连接后这个节会被舍弃。

图18 ELF的重定位所在节

4.符号表

符号表用于存放程序中定义和引用的函数和全局变量的信息。链接器进行重定位时需要使用这张符号表。符号表中每一项包含符号的名称、在符号表中索引原来所在节的索引、类型、绑定属性等信息。

图19  ELF的符号表

4.4 Hello.o的结果解析

命令:objdump -d -r hello.o > hello1.txt

图20 输入命令得到的hello1.txt

                 

图21  hello反汇编的部分代码

与hello.s的差异:

①分支转移:

图22  hello的转移操作

反汇编的跳转指令用的不是段名称比如.L3,而是用的确定的地址。但在反汇编代码中,分支转移表示为主函数+段内偏移量。反汇编代码跳转指令的操作数使用的不是段名称,因为段名称只是在汇编语言中便于编写的助记符,所以在汇编成机器语言之后显然不存在,而是确定的地址。

②对函数的调用与重定位条目对应

图23  hello的重定位代码

在可重定位文件中call后面不再是函数的具体名称,而是一条重定位条目指引的信息。而在汇编文件中可以看到,call后面直接加的是文件名。

③ 立即数变为16进制格式

   图24  hello的部分代码

在编译文件中,立即数全部是以16进制表示的,因为16进制与2进制之间的转换比十进制更加方便,所以都转换成了16进制。

4.5 本章小结

本章介绍了汇编的概念与作用,在Ubuntu下通过实际操作将hello.s文件翻译为hello.o文件,并生成hello.o的ELF格式文件hello.elf,研究了ELF格式文件的具体结构。通过比较hello.o的反汇编代码与hello.s中代码,了解了汇编语言与机器语言的异同之处。

(第41分)

5章 链接

5.1 链接的概念与作用

      概念:指将各种代码和数据片段收集并组合称为一个单一文件的过程,这个文 件可以被加载到内存并执行。链接可以执行与编译时,也可以执行于加载时,还 可以执行于运行时。

      作用:将不同功能的片段组合在一起,形成一个能执行完整功能的程序。链接 的存在可以使得分离编译成为可能。这样就可以不用将一个大型的应用程序组成 一个巨大的源文件,而是将其分解成更小、更好管理的模块。可以通过独立修改 这些模块实现功能的改变。

注意:这儿的链接是指从 hello.o 到hello生成过程。

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

图24  输入命令得到hello可执行文件

生成反汇编代码

图25  输入命令得到反汇编代码

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

命令:readelf -a hello > hello1.elf

图26  输入代码得到反汇编代码

①ELF头:hello的文件头和hello.o文件头的不同之处如下图标记所示,hello是一个可执行目标文件,有27个节。

图27  ELF头的代码

节头:对 hello中所有的节信息进行了声明,包括大小和偏移量

图28  ELF节头的部分代码

重定位节.rela.text:

对于可执行目标文件来说,仍然会存在重定位信息,因为有些需要动态链接的 块还没有被链接,重定位节中就给出了这些符号的相关信息。

图29  ELF的重定位节

符号表.symtab

对于可执行目标文件来说,包含两个符号表,一个符号表的名称为.dynsym, 从名称和符号表中的内容来看应该是还没有动态链接的一些未知符号。另一张符 号表就是熟知的.symtab,里面保存了程序中定义和引用的函数以及全局变量等的 信息。

图30  ELF的符号表

5.4 hello的虚拟地址空间

    从程序头表中可以得知,程序虚拟地址的最低地址大小为 0x400000,大小为 0x5c0,也就是说从 0x400000 到 0x4005c0 是保存有有效数据的。利用 edb 的 data dump 功能查看这一段的内存,发现是符合我们的预期的

 

图31  hello的虚拟地址

图32  hello的地址在edb中的位置

5.5 链接的重定位过程分析

命令: objdump -d -r hello > hello2.txt

hello 的反汇编代码中已经有明确的虚拟地址,而 hello.o 的反汇编代码中的 main 函数的起始地址还是 0,说明还没有经过重定位。说明链接的过程中需要为 代码确定虚拟内存空间中的地址。

图33  hello2.txt的部分代码

图34  hello1.txt的部分代码

与hello.o的反汇编文件对比发现,hello2.txt中多了许多节。hello1.txt中只有一个.text节,而且只有一个main函数,函数地址也是默认的0x000000.hello2.txt中有.init,.plt,.text三个节,而且每个节中有很多函数。库函数的代码都已经链接到了程序中,程序各个节变的更加完整,跳转的地址也具有参考性。

图35  hello2.txt的部分代码

重定位过程分析:

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

2. 重定位时还有一个主要解决的问题是符号解析,链接器修改代码节和数据 节中对每个符号的引用,使他们指向正确的运行时地址。执行这一步,链接器依赖于可重定位目标模块中在汇编阶段创建的重定位条目。重定位条目的每一项是 一个固定的数据结构。利用重定位算法即可在数据节中确定地址。

图36  hello的充重定位算法

5.6 hello的执行流程

地址:           名称:

00000000 00401000 <_init>

00000000 00401020 <.plt>

00000000 00401030 <puts@plt>

00000000 00401040 <printf@plt>

00000000 00401050 <getchar@plt>

00000000 00401060 <atoi@plt>

00000000 00401070 <exit@plt>

00000000 00401080 <sleep@plt>

00000000 004010f0 <_start>

00000000 00401120 <_dl_relocate_static_pie>

00000000 00401125 <main>

00000000 004011c0 <__libc_csu_init>

00000000 00401230 <__libc_csu_fini >

00000000 00401238 <_fini >

5.7 Hello的动态链接分析

       使用readelf对可执行目标文件hello进行解析后,可以在其节头部表中发现如 下的内容:

图37  反汇编代码中got的地址

可以发现 got 的地址在 00000000 00403ff0 处,进入edb进行调试,在执行 dl_init 前,内容如下:

图38  edb中got的代码

调用dl_init后的.got.plt

图39  调用dl_init后got的地址

从图中可以看到.got.plt的条目已经发生变化。

5.8 本章小结

本章主要介绍了链接的概念与作用,链接可分为符号定义和重定位,了解了可执行文件的ELF格式,分析了hello的虚拟地址空间,重定位过程,执行过程,动态连接过程,对链接有了更深的理解。

(第51分)

6章 hello进程管理

6.1 进程的概念与作用

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

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

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

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

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

Shell 的作用:

Shell 是一个用C语言编写的交互型应用程序,代表用户运行其他程序。Shell 应用程序提供了一个界面,用户可以通过这个界面进行系统的基本操作,访问操作系统内核的服务。

Shell的处理流程大致如下:

  1. 从Shell终端读入输入的命令。
  2. 切分输入字符串,获得并识别所有的参数
  3. 若输入参数为内置命令,则立即执行
  4. 若输入参数并非内置命令,则调用相应的程序为其分配子进程并运行
  5. 若输入参数非法,则返回错误信息
  6. 处理完当前参数后继续处理下一参数,直到处理完毕

6.3 Hello的fork进程创建过程

在终端中输入命令行./hello 120L020330 张仁杰 1 后,首先shell对我们输入的命令进行解析,由于我们输入的命令不是一个内置的shell命令,因此shell会调用fork()创建一个子进程,子进程几乎但不完全与父进程相同。通过fork函数,子进程得到与父进程用户级虚拟地址空间相同的但是独立的一份副本,拥有不同的PID。

图40  hello的编译

6.4 Hello的execve过程

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

6.5 Hello的进程执行

上下文信息:上下文就是内核重新启动一个被抢占的进程所需要恢复的原来的状态,由寄存器、程序计数器、用户栈、内核栈和内核数据结构等对象的值构成。

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

(1)保存以前进程的上下文

(2)恢复新恢复进程被保存的上下文

(3)将控制传递给这个新恢复的进程,来完成上下文切换。

逻辑控制流:一系列程序计数器PC的值的序列叫做逻辑控制流。由于进程是轮流使用处理器的,同一个处理器每个进程执行它的流的一部分后被抢占,然后轮到其他进程。

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

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

6.6 hello的异常与信号处理

1.   在程序正常运行时,打印八次提示信息,以输入回车为标志结束程序,并回收进程。

图41  hello的正常编译情况

2.   在程序运行时按回车,会多打印几处空行,程序可以正常结束。

图42  hello编译中输入回车的情况

3. 按下Ctrl + C,Shell进程收到SIGINT信号,Shell结束并回收hello进程。

图43  hello编译时输入ctrl^c的结果

4. 按下Ctrl + Z,Shell进程收到SIGSTP信号,Shell显示屏幕提示信息并挂起hello进程。

图44  hello编译时输入ctrl^z的结果

对hello进程的挂起可由ps和jobs

命令查看,可以发现hello进程确实被挂起而非被回收,且其job代号为1

图45  hello挂起时输入ps和jobs的结果

在Shell中输入pstree命令,可以将所有进程以树状图显示(此处仅展示部分):

图45  hello挂起时输入pstree的结果

输入kill命令,则可以杀死指定(进程组的)进程:

图46  hello挂起时输入kill的结果

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

图47  hello挂起时输入fg 1的结果

5. 不停乱按:无关输入被缓存到stdin,并随着printf指令被输出到结果。

图48  hello编译时不停乱按的结果

6.7本章小结

本章介绍了进程的概念和作用、shell-bash的处理过程与作用并且着重分析了调用fork创建新进程,调用execve函数执行hello,hello的进程执行过程,以及hello在运行时遇到的异常与信号处理。

(第61分)

7章 hello的存储管理

7.1 hello的存储器地址空间

1.   逻辑地址

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

2.   线性地址

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

3.   虚拟地址

根据CSAPP教材,虚拟地址即为上述线性地址。

4.   物理地址

CPU通过地址总线的寻址,找到真实的物理内存对应地址。

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

一个逻辑地址由两部分组成,段标识符和段内偏移量。其中段标识符是一个16位长的字段组成,称为段选择符,其中前13位是一个索引号。后面三位包含一些硬件细节。可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段,也就是段开始位置的线性地址。

全局的段描述符,放在“全局段描述符表(GDT)”中,一些局部的段描述符,放在“局部段描述符表(LDT)”中。GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。

给定一个完整的逻辑地址段选择符+段内偏移地址,看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。

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

页式管理是一种内存空间存储管理的技术,思路是将各进程的虚拟空间划分成若干个长度相等的页,然后把内存空间划分成一些页框,页框的大小和页的大小相等,然后把页式虚拟地址与内存地址建立一一对应页表,以实现虚拟地址到物理地址的映射。在这种情况下,CPU只需要生成需要访问的虚拟地址,然后根据页表就可以访问到实际的物理地址,从而解决主存大小和磁盘大小不一致问题下,保证行为的相似性。

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

TLB是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块。TLB通常有着高度的相联度以保证更高的命中率。以Core i7为例,从VA到PA的转换如下图所示:

图49  Core i7 地址翻译流程

同样地,对于Core i7而言,由于虚拟地址空间非常大,如果只使用一张页表,那么页表本身的大小将使主存无法接受,所以必须使用多级页表,具体来说,使用了四级页表,如下图所示:

图50  Core i7 四级页表翻译流程

简单来说,CPU会产生一个虚拟地址VA,然后传送给MMU进行翻译。MMU首先会在TLB中查是否有缓存,如果有缓存的话,VA此时就直接转换成PA;如果没有缓存,那么就需要在主存中根据逐级页表进行查询,最终在第四级页表中查到对应结果,形成PA。如果此时没有查到,说明页表还在磁盘中,没有加载,此时就会引发缺页故障,从而执行对应的异常处理程序。

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

获得物理地址之后,先取出组索引对应位,在L1中寻找对应组。如果存在,则比较标志位,相等后检查有效位是否为1.如果都满足则命中取出值传给CPU,否则按顺序对L2cache、L3cache、内存进行相同操作,直到出现命中。然后再一级一级向上传,如果有空闲块则将目标块放置到空闲块中,否则将缓存中的某个块驱逐,将目标块放到被驱逐块的位置。

7.6 hello进程fork时的内存映射

在shell输入命令行后,内核调用fork创建子进程,为hello程序的运行创建上下文,并分配一个与父进程不同的PID。同时为这个新进程创建虚拟内存,创建当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记位只读,并将两个进程中的每个区域结构都标记为私有的写时复制。

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

7.7 hello进程execve时的内存映射

execve 函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件 hello 中的程序,用 hello 程序有效地替代了当前程序。加载并运行 hello 需要以下几个步骤:

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

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

(3) 映射共享区域。hello 程序与共享对象 libc.so 链接,libc.so 是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。

(4) 设置程序计数器。execve 做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。

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

页面命中完全是由硬件完成的,而处理缺页是由硬件和操作系统内核协作完成的,在指令请求一个虚拟地址时,MMU中查找页表,如果这时对应得物理地址没有存在主存的内部,我们必须要从磁盘中读出数据。在虚拟内存的习惯说法中,DRAM缓存不命中成为缺页。在发生缺页后系统会调用内核中的一个缺页处理程序,选择一个页面作为牺牲页面。具体流程如下:

①处理器生成一个虚拟地址,并将它传送给MMU

②MMU生成PTE地址,并从高速缓存/主存请求得到它

③高速缓存/主存向MMU返回PTE

④PTE中的有效位是0,所以MMU出发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序。

⑤缺页处理程序确认出物理内存中的牺牲页,如果这个页已经被修改了,则把它换到磁盘。

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

⑦缺页处理程序返回到原来的进程,再次执行导致缺页的命令。CPU将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面已经换存在物理内存中,所以就会命中。

图51  进程地址空间

7.9动态存储分配管理

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

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

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

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

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

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

  1. 隐式链表

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

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

图52  一个简单的堆块的格式

  1. 显式链表

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

显式链表的结构如下:

图53  双向空闲链表的堆块格式

  1. 带边界标记的合并

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

  1. 分离存储

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

7.10本章小结

本章主要介绍了hello的存储器地址空间、intel的段式管理、hello的页式管理,VA到PA的变换、物理内存访问,hello进程fork、execve时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。

(第7 2分)

8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

设备管理:unix io接口

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

8.2 简述Unix IO接口及其函数

UnixI/O接口:

打开文件:内核返回一个非负整数的文件描述符,通过对此文件描述符对文件进行所有操作。

Linuxshell创建的每个进程开始时都有三个打开的文件:标准输入(文件描述符0)、标准输出(描述符为1),标准出错(描述符为2)。头文件定义了常量STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO,他们可用来代替显式的描述符值。

改变当前的文件位置,文件开始位置为文件偏移量,应用程序通过seek操作,可设置文件的当前位置为k。

读写文件,读操作:从文件复制n个字节到内存,从当前文件位置k开始,然后将k增加到k+n;写操作:从内存复制n个字节到文件,当前文件位置为k,然后更新k

关闭文件:当应用完成对文件的访问后,通知内核关闭这个文件。内核会释放文件打开时创建的数据结构,将描述符恢复到描述符池中。

UnixI/O函数:

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

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

3.ssize_tread(intfd,void*buf,size_tn),read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回-1表示一个错误,0表示EOF,文件读取结束,否则返回值表示的是实际传送的字节数量。

4.ssize_twirte(intfd,constvoid*buf,size_tn),write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。

8.3 printf的实现分析

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

图54  printf函数代码

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

vsprintf代码:

图55  vprintf函数代码

可以知道这个函数的功能就是返回打印字符的长度,在获得长度后,就可以通过调用write系统函数来实现打印功能。

最终可以查看write这个函数的汇编代码,得到如下的结果:

      图56  write的汇编代码

这里[esp+4]和[esp+8]分别代表需要打印字符所在的地址和打印字符长度,也就是调用write时的两个参数,最后一行代码以 int 开头,说明这是一个系统调用。 syscall 将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储 的是字符的 ASCII 码。字符显示驱动子程序将通过 ASCII 码在字模库中找到点阵 信息将点阵信息存储到 vram 中。显示芯片会按照一定的刷新频率逐行读取 vram, 并通过信号线向液晶显示器传输每一个点(RGB 分量)。于是我们的打印字符串 就显示在了屏幕上。

代码来源:[转]printf 函数实现的深入剖析 - Pianistx - 博客园

8.4 getchar的实现分析

getchar函数源码如下:

图57  getchar的函数代码

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

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

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

8.5本章小结

本章介绍了Linux的I/O设备的基本概念和管理方法,以及Unix I/O接口及其函数。最后分析了printf函数和getchar函数的工作过程。

(第81分)

结论

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

1.   预处理

将hello.c中include的所有外部的头文件头文件内容直接插入程序文本中,完成字符串的替换,方便后续处理;

2.   编译

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

3.   汇编

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

4.   链接

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

5.   加载运行

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

6.   执行指令

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

7.   访存

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

8.   动态申请内存

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

9.   信号处理

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

10. 终止并被回收

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

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

附件

文件名称      功能

hello.c        源程序

hello.i         预处理后文件

hello.s        编译后的汇编文件

hello.o        汇编后的可重定位目标执行文件

hello          链接后的可执行文件

hello.elf       hello.o的ELF格式

hello1.elf      hello的ELF格式

hello1.txt      hello.o的反汇编

hello2.txt      hello的反汇编代码

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

参考文献

[1]  深入理解计算机系统原书第3版-文字版.pdf

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

[3]  https://www.cnblogs.com/diaohaiwei/p/5094959.html

[4]  https://blog.csdn.net/drshenlei/article/details/4261909

[5]  https://blog.csdn.net/rabbit_in_android/article/details/49976101

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值