ICS大作业-CSAPP-程序人生

计算机系统

计算机系统

大作业

题     目  程序人生-Hello’s P2P 

专       业       计算学部          

学     号      120L05****       

班     级        2003***        

学       生        赵**         

指 导 教 师          吴*           

计算机科学与技术学院

2022年5月

摘  要

本论文围绕hello.c程序在Linux系统下编译运行的完整的生命周期,深入研究了hello.c文件的执行过程,分析了计算机系统各个功能的实现原理,并通过hello程序的执行将计算机系统的体系串联在了一起,较为深入地分析了编译、汇编、链接、进程管理、存储管理、IO管理等计算机系统功能实现。

关键词:计算机系统;Linux系统;编译;链接;进程管理;存储管理;IO管理

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

目  录

第1章 概述... - 4 -

1.1 Hello简介... - 4 -

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

1.3 中间结果... - 5 -

1.4 本章小结... - 5 -

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

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

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

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

2.4 本章小结... - 7 -

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的虚拟地址空间... - 22 -

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

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

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

5.8 本章小结... - 26 -

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

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

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

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

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

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

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

6.7本章小结... - 32 -

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

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

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

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

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

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

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

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

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

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

7.10本章小结... - 34 -

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

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

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

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

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

8.5本章小结... - 38 -

结论... - 38 -

附件... - 40 -

参考文献... - 41 -

第1章 概述

1.1 Hello简介

根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。

Helllo的P2P是指,hello.c文件经过预处理、编译、汇编、链接成为可执行目标程序(Program)hello后,通过操作系统执行来产生子进程(Process)运行程序的过程,即从可执行程序(Program)变成为进程(Process)。

Hello的020是指,初始时内存并无hello文件的相关内容,Shell运行一个程序时,Shell进程生成一个子进程,子进程通过调用execve函数,将hello文件中的代码和数据从磁盘复制到内存中,运行该程序,当程序运行结束后,已终止的hello进程被回收,由内核从系统中删除hello的所有痕迹,即 “From 0 to 0”。

1.2 环境与工具

硬件:AMD Ryzen 7 Mobile 4700U CPU @ 2.60GHz

16GB RAM 512G HDD

软件:Windows10 64 位

VirtualBox 6.1.34

Ubuntu 20.04.4 LTS 64 位

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

gedit,gcc,notepad++,readelf, objdump, hexedit, edb

1.3 中间结果

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

文件名

作用

hello.i

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

hello_2.i

注释stdio.h头文件后预处理得到的文件

hello.s

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

hello.o

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

hello.elf

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

hello.asm

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

hello_2.elf

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

hello_2.asm

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

表1 中间结果文件

1.4 本章小结

       本章简述了Hello程序的执行过程,解释了关于Hello程序的P2P、020含义,同时列出了论文撰写过程中实验所采用的环境与工具及实验的中间结果。

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

2.1.1预处理的概念

预处理是指,预处理器(cpp)根据以字符#开头的命令,修改原始的C程序的过程。预处理通常生成一个以.i作为文件扩展名的C程序。

2.1.2预处理的作用

预处理主要有三种功能:宏定义文件包含条件编译

  1. 宏定义:在编译预处理时,对程序中所有出现的宏名,都用宏定义中的字符串去代换,这称为“宏代换”或“宏展开”。
  2. 文件包含:文件包含命令的功能是把指定的文件插入到该命令行位置取代该命令行,从而把指定的文件和当前的源程序文件连成一个源文件。使用文件包含指令可以节省时间并减少出错,方便后续处理。
  3. 条件编译:预处理程序提供了条件编译的功能。可以按不同的条件去编译不同的程序部分,因而产生不同的目标代码文件。

此外,预处理还可以删除程序中得到注释和多余的空行。

2.2在Ubuntu下预处理的命令

在Ubuntu下预处理的命令为:cpp hello.c >hello.i

预处理过程截图如下:

图1 预处理过程

2.3 Hello的预处理结果解析

在Linux下打开hello.i文本文件,可以观察到C代码从23行扩展到3060行,并且原先main函数的代码在文件最后。

图2  hello.s中的含main函数的部分代码

      由2.1可知预处理主要进行的操作是:将#include指令对应的代码插入文件,可以注释掉#include<stdio.h>进行分析,并再次对hello.c进行预处理得到hello_2.c,可以观察到代码由3060行缩减为2507行,如下图所示。

图3 注释stdio.h预处理的代码

      对应分析可以得知stdio.h展开的具体流程,首先cpp删除预处理指令,然后在Ubuntu系统的默认环境变量下寻找stdio.h,最终打开/usr/include/stdio.h目录的stdio.h文件,并对其中的预处理语句递归地进行展开处理,直到所有预处理指令都被处理完成。

2.4 本章小结

本章介绍了预处理的概念及其作用,并在Linux系统下,对hello.c文件进行预处理得到hello.i文件,并对其做了解析实验,印证了预处理的作用,即在编译之前根据预处理指令,修改原始的C程序。

(第2章0.5分)
第3章 编译

3.1 编译的概念与作用

3.1.1编译的概念

编译是指编译器(ccl)通过词法分析、语法分析、语义检查和中间代码生成、代码优化以及目标代码生成这五个阶段来讲一个源程序翻译成目标程序的工作过程,总的来说,编译就是把代码转化为汇编指令的过程。

3.1.2编译的作用

编译程序的作用就是将高级程序语言源程序翻译为汇编语言程序,便于后续汇编与链接处理。此外,编译还可以进行错误分析,并给出提示信息;对程序中的代码进行优化。

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

3.2 在Ubuntu下编译的命令

参考hello.c文件中的注释,进行如下编译命令处理:

gcc -m64 -no-pie -fno-PIC -S hello.i -o hello.s

编译截图如下:

图4 编译过程

3.3 Hello的编译结果解析

3.3.1汇编文件结构分析

对hello.s汇编文件的结构分析如下:

内容

含义

.file

源文件名称

.text

代码段

.globl(不是global)

全局变量

.data

存放已经初始化的全局和静态C变量

.section  .rodata

存放只读变量

.align

对齐方式

.type

表示类型

.size

表示大小

.long   .string

long数据类型/string数据类型

表2 hello.s文件结构

3.3.2数据类型

  1. 常量
  1. 立即数常量,hello.s中有立即数常量,即$直接加数字:

图5 hello.s中的立即数

  1. 字符串常量,hello.s中有三个字符串常量,存储在.text数据段中。其中\xxx为utf-8编码,一个汉字对应三个字节。

图6 hello.s中的字符串常量

  1. 局部变量
  1. int i,编译器将局部变量存储在寄存器或者栈空间中。i是函数内部的局部变量,对i的操作就是直接对寄存器进行操作,i的处置为0,保存在-4(%rbp)地址对应的内存中,每次循环加一。

图7 hello.s中的局部变量int i

  1. argc/argv,调用main函数传入的参数,保存在%rdi寄存器里,进入main函数中,将其放入堆栈中调用。、

图8 hello.s中的局部变量argc/argv

3.3.3赋值操作

hello.s中对于赋值操作的实现主要是用movx指令(x为l或q),其中,有将立即数赋值到寄存器,将立即数复制到内存,有从寄存器赋值到寄存器,寄存器复制到内存,内存复制到寄存器。

3.3.4类型转换

程序中调用了atoi(argv[3])函数,将字符串转化为数字。

图9 hello.s中的类型转换函数调用

3.3.5算数操作

  1. 加法操作

使用add指令实现加法操作,对应C程序中的i++

图10 hello.s中的add指令

  1. 减法操作

使用sub指令实现减法操作,在汇编代码中用于开辟栈空间

图11 hello.s中的sub指令

3.3.6关系操作

在C程序中有argc!=4和i<8这两条关系判断,在汇编代码中使用cmpl来实现。

图12 hello.s中的cmpl指令

3.3.7数组操作

C程序中存在数组char *argv[],在汇编代码中首先开辟栈空间,然后将数组首地址保存在栈中,在需要访问时,通过寄存器寻址的方式访问。

图13 hello.s中的数组操作

3.3.8控制转移

C程序中控制转移体现在两处:

  1. if(argc!=4);

当argc不等于4,执行函数体内部的代码。在汇编代码中首先使用cmpl指令来判断是否相等,根据结果设置条件码后,使用je指令完成后续跳转。

图14 hello.s中的条件分支

  1. for(i=0;i<10;i++);

当i<10时进行循环,每次循环i加一。在汇编代码中,首先使用cmpl指令来判断i与10是否相等,根据结果设置条件码,使用jle指令完成后续跳转。

图15 hello.s中的循环

3.3.9函数操作

  1. main函数

参数传递:argc和argv,分别保存在寄存器%rdi和%rsi

函数调用:由系统来调用,更准确地说,由execve函数来调用

函数返回:使用movl指令将%rax寄存器中的值置为0,

  1. printf函数

参数传递:传入第二个字符串作为参数,通过引用地址和3.3.7中访问数组操作传递参数

  1. exit函数

参数传递:将立即数1传给寄存器%rdi

函数调用:在if条件判断为真后调用

  1. sleep函数

参数传递:将atoi()函数的返回值即%rax寄存器中的值作为参数传给%rdi

函数调用:每次循环中被调用

  1. atoi函数

参数传递:将argv[2]传给寄存器%rdi,argv[2]的访问使用3.3.7中数组操作

函数调用:每次循环中被调用

函数返回:将转化后的整数传给寄存器%rax

  1. getchar函数

参数传递:从键盘中读入一个字符

函数调用:在main函数中被调用

  1. put函数

在汇编代码中还有一个call put操作,也是用于将字符串输出到屏幕

参数传递:通过引用地址将第一个字符串传给%rdi

函数调用:在if条件判断为真后调用

3.4 本章小结

本章主要介绍了编译的概念和作用,编译是把代码转化为汇编指令,为后续将其转化为机器语言做准备的过程。并具体分析了hello.s汇编代码,介绍了编译器如何处理各个数据类型以及各类操作。

(第32分)

第4章 汇编

4.1 汇编的概念与作用

4.1.1汇编的概念

汇编是指汇编器(as)将.s结尾的汇编程序翻译成机械语言指令,把这些指令打包成可重定位目标程序格式,并将结果保存在.o二进制目标文件中的过程。

4.1.2汇编的作用

将汇编指令转化为机器指令,生成不同的目标模块保存在可重定位目标文件。

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

4.2 在Ubuntu下汇编的命令

在Ubuntu下汇编的命令:gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o

图16 汇编过程

4.3 可重定位目标elf格式

分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。

在Shell中输入指令readelf -a hello.o > hello.elf指令获得hello.o的ELF格式可重定位目标文件

图17 获得ELF格式目标文件过程

其结构分析如下:

  1. ELF头(ELF Header)

以16字节序列Magic开始,这个序列描述了生成该文件的系统的字的大小和字节顺序,其中7f 45 4c 46为固定的魔法字节,02表示64位,第一个01表示小端序,第二个01表示ELF头版本。ELF头剩下的部分包括帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(可重定位的)、机器类型、节头部表的文件偏移。以及节头目表中条目的大小和数量。

图18 ELF头

  1. 节头

描述了不同节的名称、类型、位置、大小等信息。

图18 节头

  1. 重定位节.rela.text

一个.text节中位置的列表,存放着代码的重定位条目,其类型为 RELA,也就是说它是一个重定位表(也叫作重定位段),当链接器把这个目标文件和其他文件组合时,需要修改这些位置。在此重定位表中,每个要被重定位的地方叫重定位入口(Relocation Entry),我们可以看到每个重定位入口在段中的偏移位置,重定位入口的类型,重定位入口的名称以及重定位修正的辅助信息等。

表中一共有8条重定位信息,分别是对.rodata(第一个printf语句的格式串)、puts函数、exit函数、.rodata+1d(第二个printf语句的格式串)、printf函数、atoi函数、sleep函数、getchar函数进行了重定位声明

图19 重定位节.rela.text

  1. 重定位节.rela.eh_frame

.rela.eh_frame节同.rela.eh_frame一样属于重定位信息的节,包含的是eh_frame的重定位信息。

图20重定位节.rela.eh_frame

  1. 符号表Symbol table .symtab

符号表中存放着在程序中定义和引用的函数和全局变量的信息,与编译器的符号表不同,.symtab符号表不包含局部变量的条目。

图21符号表

4.4 Hello.o的结果解析

使用指令objdump -d -r hello.o > hello.asm生成反汇编文件,分析hello.o的反汇编,对照hello.s可以发现,反汇编得到的文件左边比hello.s多了机器语言,而机器语言实际上就是对汇编语言的编码,每条汇编代码都具有唯一的机器编码。就二者之间的差异更具体地分析如下:

  1. 操作数

机器语言和反汇编语言中的操作数都是十六进制的,而汇编语言是十进制的。

图21 二者操作数的差异

  1. 分支转移

在hello.s中,跳转指令的目标地址为段名称,如.L2、.L3;在反汇编代码中,跳转指令的目标地址为具体的地址;在机器代码中为目标指令地址与下一条指令地址的差值。

图22 二者分支转移的差异

  1. 函数调用

在hello.s中,对函数的调用是直接在call指令后边跟函数名,而在反汇编代码中,对函数的调用的目标地址都是当前指令的下一条指令的地址。这是因为,hello.c中调用的函数都是共享库中的函数,需要通过动态链接器作用才能确定函数在运行时的地址,所以在汇编的过程中,将call后边的目标地址设置为下一条指令的地址,并且在调用函数后边生成了一个重定位条目,来告诉链接器对该函数的引用要使用函数名前边的重定位类型来进行重定位(其中最主要的两个类型是,R_X86_64_PC32重定位一个使用32位PC相对寻址的引用和R_X86_64_32重定位一个使用32位绝对寻址的引用)

图23 hello.o反汇编代码中的函数调用

4.5 本章小结

本章介绍了汇编的概念与作用,并且在Linux下将hello.s文件汇编为hello.o文件,使用readelf指令生成了可重定位目标文件的elf格式并对其具体结构进行了研究。此外,通过对比hello.o的反汇编代码与hello.s中的汇编代码,对hello.o机器代码做了进一步解析,了解了汇编语言与机器语言的相同与差异。

(第41分)

5章 链接

5.1 链接的概念与作用

5.1.1链接的概念

链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载(复制)到内存并执行。这个过程是有叫做链接器的程序执行的。

5.1.2链接的作用

链接实现了分离编译的效果,多个.c文件各自生成.o文件,最后合并到一起,使得程序开发具有很好的模块性,减小了整体文件编写过程中的复杂度,并且避免了对重要代码重复书写的弊病,同时方便对代码的修正改进。

注意:这儿的链接是指从 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 链接过程

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

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

输入命令:readelf -a hello > hello_2.elf,生成hello程序的ELF格式文件,并命名为hello_2.elf。

图25 获取ELF文件过程

打开文件hello_2.elf,对hello程序的ELF格式文件分析如下:

  1. ELF头(ELF Header)

可以观察到hello_2.elf与hello.elf中的ELF头包含的信息种类基本形同,都以16字节序列的Magic开始,这个序列描述了生成该文件的系统的字的大小和字节顺序,剩下的部分包括帮助链接器语法分析和解释目标文件的信息。与hello.elf相比,hello_2.elf中类型变为EXEC(可执行文件),更改了了入口地址、程序头起点、节头起点、程序头大小和节头大小等信息。

图26 ELF头

  1. 节头

hello_2.elf中的节头与hello.elf结构相同,描述了不同节的名称、类型、位置、大小等信息。与hello.elf不同的是,链接后的hello_2.elf中为不同节提供了一段新的地址范围,并且链接后增加了更多的节。

图27 节头

  1. 程序头

程序头部分是一个结构数组,描述了系统准备程序执行所需的段或其他信息。Offset是目标文件中的偏移,virtaddr/physaddr是内存地址,filesiz是目标文件中的段大小,memsiz是内存中的段大小,flags是运行时访问权限,align是对其要求。

图28 程序头

  1. Dynamic section

图29 Dynamic section

  1. Symbol table

符号表中存放着在程序中定义和引用的函数和全局变量的信息,与编译器的符号表不同,.symtab符号表不包含局部变量的条目。

图30 符号表

5.4 hello的虚拟地址空间

使用edb加载hello,点击Data Dump窗口查看本进程的虚拟地址空间各段信息。

图31 hello的虚拟地址空间

程序被加载到地址0x401000-0x402000中,与5.3节中各节的地址相对应,例如:.init的地址为0000000000401000、.text的地址为00000000004010f0、.rodata地址为0000000000402000等等。可以使用edb定位地址查看对应的内容,

5.5 链接的重定位过程分析

使用指令objdump -d hello >hello_2.asm来生成可执行文件的反汇编文件hello_2.asm。

图32 链接的重定位过程

打开hello_2.asm,对比之前的hello.asm可以发现,hello与hello.o有以下不同:

  1. hello_2.asm中删除了hello.asm中的重定位条目,将hello.o反汇编的代码移到了一段不同的地址范围中

图33 hello.o反汇编代码片段

  1. 对于引用指令、跳转指令在其后添加了其需要使用的地址。其中地址前有个‘*’表示间接跳转,只跟着标号的表示直接跳转。

图34 hello.o反汇编代码中的引用、跳转指令

  1. 还可以观察到在链接后代码中多了                                                                                            .init .plt, puts@plt,printf@plt,getchar@plt,exit@plt,sleep@plt 等函数的代码。

图35 hello.o反汇编代码中的函数调用

链接过程主要有两个任务,符号解析和重定位,符号解析将目标文件中的每个全局符号都绑定到一个唯一的定义,而重定位确定每个符号的最终内存地址,并修改对那些目标的引用。

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

图36 hello.o反汇编代码中的重定位条目

在链接过程中,对于hello.o中的重定位项目,链接器根据重定位条目指示的地址引用类型进行重定位。

对于PC相对引用类型(R_X86_64_PC32,在新版gcc中会标记为PTL32),比如:1f: R_X86_64_PLT32  puts-0x4,该条目告诉链接器修改开始于偏移量0x1f处的32为PC相对引用,链接器首先计算出引用的运行时地址,然后更新引用,使得其在运行时指向put程序。

对于绝对引用类型(R_X86_64_32),比如:1a: R_X86_64_32    .rodata,该条目告诉链接器要修改从偏移量0x1a开始的绝对引用,使其运行时指向.rodata的第一个字节。

5.6 hello的执行流程

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

程序名称

程序地址

_start

0x04010f0

_libc_csu_init

0x4011c0

_init

0x0401000

main

0x401125

printf@plt

0x4010a0

atoi@plt

0x4010c0

sleep@plt

0x4010e0

getchar@plt

0x4010b0

_fini

0x401238

表3 程序名称与地址信息

5.7 Hello的动态链接分析

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

首先,动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才形成一个完整的程序。而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件,但在形成可执行文件的时候,还是会用到动态链接库,如果此时发现引用了一个外部函数,就会检查动态链接库,找到动态链接符号,但并不对符号进行重定位,直到程序执行过程中装载时再进行

动态链接器使用过程链接表PLT+全局偏移量GOT实现函数的动态链接。每个被可执行程序调用的库函数都有它自己的PLT条目,每个条目负责调用一个具体的函数。GOT包含动态链接器解析函数地址时会使用的信息。加载时,动态链接器会重定位GOT中的每个条目,使得它包含目标的正确的绝对地址。

在hello_2.elf文件中找到.got的存放地址:

图37 hello_2.elf文件片段

       使用edb查看在dl_init调用前内容如下:

图38 dl_init调用前内容

       使用edb查看在dl_init调用后内容如下:

图39 dl_init调用后内容

       比较内容可知,对应的全局偏移量表GOT[1]和GOT[2]内容发生了变化,GOT[1]保存的是指向已经加载的共享库的链表地址。GOT[2]是动态链接器ld-linux.so 模块中的入口。这样,接下来执行程序的过程中,就可以使用过程链接表PLT和全局偏移量表GOT进行动态链接。

5.8 本章小结

本章介绍了链接的概念与作用,根据链接后得到的可执行文件的ELF格式进行了结构分析,对比可重定位目标文件的ELF分析了不同的类型信息;然后通过使用edb调试hello程序,分析了hello程序的虚拟地址空间,验证了链接的过程,清楚了hello的执行流程,并对其动态链接做了分析,加深了对可执行文件执行过程与动态链接的理解。

(第51分)

6章 hello进程管理

6.1 进程的概念与作用

6.1.1进程的概念

进程就是一个执行中的程序的实例。系统中的每个程序都运行在某个进程的上下文中。

6.1.2进程的作用

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

  1. 一个独立的逻辑控制流,提供一个假象,好像程序独占地使用处理器
  2. 一个私有地址空间,提供一个假象,好像程序独占地使用内存系统

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

6.2.1Shell-bash的作用

Shell 是一种交互型的应用级程序,是用户和Linux内核之间的接口程序,它可以代表用户执行程序。Shell-Bash是一种命令行式shell。

6.2.2Shell-bash的处理流程

Shell执行一系列的读/求值步骤然后终止。该步骤读取来自用户的一个命令行。求值步骤读取来自用户的一个命令行,求值步骤解析该命令行,并代表用户执行程序。在解析命令后,如果是内置命令,则解析指令并执行,否则就根据相关文件路径执行可执行目标文件。

6.3 Hello的fork进程创建过程

每次用户通过向Shell输入一个可执行目标文件的名字,运行程序时,Shell就会创建一个新的进程,然后再这个新进程的上下文中运行这个可执行目标文件。

父进程通过调用fork函数创建一个新的运行的子进程,子进程得到与父进程完全相同但是独立的一个副本,包括代码段、段、数据段、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,二者最大的区别再与它们有不同的PID

hello为例,输入指令:./hello 120L052314 赵伟东 1 后,shell会对指令进行解析,我们输入的是一个可执行目标文件,Shell就调用fork()创建一个新的进程。

6.4 Hello的execve过程

父Shell进程生成一个子进程后,子进程通过execve系统调用启动加载器。execve函数在当前进程的上下文中加载并运行一个新的程序,加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零,通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件的内容。最后加载器跳转到_start地址,它最终会调用应用程序的main函数。

6.5 Hello的进程执行

在程序运行时,shell父进程首先生成一个子进程,进入待执行的

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

首先,对上下文、进程时间片、用户态和核心态做出如下阐述:

  1. 上下文及上下文切换:进程的物理实体(代码和数据等)和支持进程运行的环境。系统通过处理器调度让处理器轮流执行多个进程,实现不同进程中指令交替执行的机制称为进程的上下文切换。
  2. 进程时间片:连续执行同一个进程的时间段称为时间片。
  3. 用户态与核心态转换:处理器通过某个控制寄存器中的一个模式位来提供限制一个应用可以执行的指令以及它可以访问的地址空间范围的功能。当设置了模式位时,进程就运行在内核模式中。没有设置模式位时,进程就运行在用户模式中。

在Hello的进程执行过程中,首先在shell中输入命令加载可执行目标文件hello,经过一次上下文切换,切换的过程中处于内核状态,切换后内核代表进程hello在用户模式下运行,直到磁盘发出中断信号,执行从hello到进程A的上下文切换,将控制权返回给进程A,进程A继续运行,直到下一次异常发生。

6.6 hello的异常与信号处理

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

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

Hello执行的过程中出现的异常为:中断、故障,其中可能会产生来自终端的停止信号SIGSTP,来自键盘的终端的中断信号SIGINT。

  1. 正常终止

图40 进程正常终止

  1. 在程序运行时输入回车,会多打印换行,但并不影响程序运行

图41 进程运行时输入回车

  1. 在程序运行时输入乱按键盘,输入其他字符后,所输入的字符会被缓存到stdin直到输入一个‘\n‘,’\n’之前的字符会被当作getchar函数的输入,’\n’之后的字符会在hello结束后被当作Shell命令行的输入

图41 进程运行时输入乱按键盘输入其他字符

  1. 按下Ctrl+C,shell父进程收到SIGINT信号,信号处理程序终止hello进程并由init进程回收

图42 进程运行时按下Ctrl+C

  1. 按下Ctrl+Z,shell父进程收到SIGSTP信号,信号处理程序会将hello进程挂起

图43 进程运行时按下Ctrl+Z

可以通过指令ps查看到hello进程并没有被回收,并且可以通过ps a看到当前进程的状态为T即已停止的。

图44 进程运行时按下Ctrl+Z后的进程列表

可以通过指令pstree命令,以树状图显示系统中所有进程。(图中显示包含hello进程的一部分)

图45 进程运行时按下Ctrl+Z后的进程树

可以通过fg指令来使进程重新在前台执行,shell会先打印执行hello的命令,然后重新运行hello打印剩下的语句,运行结束后,进程停止然后被回收

图46 重新在前台运行进程

可以通过kill命令来杀死指定的进程

图47  kill命令杀死进程

6.7本章小结

       本章介绍了进程的概念与作用,了解了Shell-bash的作用与执行流程,结合Shell-bash下对于hello的执行,研究了fork函数创建子进程的过程、execve函数的执行过程以及各种异常与信号处理的过程与结果。

(第61分)

7章 hello的存储管理

7.1 hello的存储器地址空间

  1. 逻辑地址

逻辑地址指由程序产生的段内偏移地址。逻辑地址与虚拟地址二者之间没有明确的界限。在hello中,逻辑地址为hello.asm中的相对偏移地址。

  1. 线性地址

指虚拟地址到物理地址变换的中间层,是处理器可寻址的内存空间(称为线性地址空间)中的地址。逻辑地址,或者说段中的偏移地址,加上相应段基址就成了一个线性地址。在hello中,线性地址标志着hello应在内存上哪些具体数据块上运行。

  1. 虚拟地址

是由程序产生的由段选择符和段内偏移地址组成的地址。这两部分组成的地址并不能直接访问物理内存,而是要通过分段地址的变化处理后才会对应到相应的物理内存地址。

  1. 物理地址

指内存中物理单元的集合,地址转换的最终地址,进程在运行时执行指令和访问数据最后都要通过物理地址来存取主存。

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

 段式内存管理,是指把一个程序分成若干个段(segment)进行存储,每个段都是一个逻辑实体(logical entity),程序员需要知道并使用它。Intel处理器从逻辑地址到线性地址的变换通过段式管理的方法实现。每个程序在系统中都保存着一个段表,段表保存着该程序各段装入主存的状况信息,包括段号或段名、段起点、装入位、段的长度、主存占用区域表、主存可用区域表等,从而方便进行段式管理。

在段寄存器中,存放着段选择符,可以通过段选择符来得到对应段首地址。段选择符的结构如下:

 

图48 kill段选择符结构

       段选择符包含三部分:索引、TI、RPL

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

       TI:用来判断选择全局描述符表(GDT)或局部描述符表(LDT)。其中TI=0时,选择GDT;TI=1时,选择LDT。

       RPL:判断重要等级。RPL=00,为第0级,位于最高级的内核,RPL=11,为第3级,位于最低级的用户状态;

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

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

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

通过 7.2 节中的段式管理过程,可以得到了线性地址/虚拟地址,记为VA。虚拟地址可被分为两个部分:VPN(虚拟页号)和 VPO(虚拟页偏移量),根据计算机系统的特性可以确定 VPN 与 VPO 的具体位数,由于虚拟内存与物理内存的页大小相同,因此 VPO 与 PPO(物理页偏移量)一致。而 PPN(物理页号)则需通过访问页表中的页表条目(PTE)获取,如下图所示。

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

若 PTE 的有效位为 1,则发生页命中,可以直接获取到物理页号PPN,PPN 与 PPO 共同组成物理地址。若 PTE 的有效位为 0,说明对应虚拟页没有缓存到物理内存中,产生缺页故障,调用操作系统的内核的缺页处理程序,确定牺牲页,并调入新的页面。再返回到原来的进程,再次调用导致缺页的指令。此时发生页命中,获取到 PPN,与 PPO共同组成物理地址。

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

针对 Intel Core i7 CPU 研究 VA 到 PA 的变换。

Intel Core i7 CPU 的基本参数如下:

1. 虚拟地址空间 48 位(n=48)

2. 物理地址空间 52 位(m=52)

3. TLB 四路十六组相连

4. L1,L2,L3 块大小为 64 字节

5. L1,L2 八路组相连

6. L3 十六路组相连

7. 页表大小 4KB(P=4x1024=2^12),四级页表,页表条目(PTE)大小8 字节由上述信息可以得知,VPO与PPO有 p=12位,故 VPN 为 36 位,PPN为 40 位。单个页表大小4KB,PTE 大小8字节,则单个页表有512个页表条目,需要 9 位二进制进行索引,而四级页表则需要36位二进制进行索引,对应着36位的VPN。TLB有16组,故TLBI有t=4 位,TLBT 有36-4=32位。

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

如图所示,CPU产生虚拟地址VA,并将其传送至MMU,MMU使用前36位VPN作为TLBT(前32位)+TLBI(后4位)在TLB中进行匹配,若命中,则得到PPN(40bit)与VPO(12bit)组合成物理地址PA(52bit)。若TLB没有命中,则MMU向页表中查询,由CR3 确定第一级页表的起始地址,VPN1(9bit)确定在第一级页表中的偏移量,查询出 PTE,如果在物理内存中且权限符合,则执行下一步确定第二级页表的起始地址,以此类推,最终在第四级页表中查询到 PPN,与VPO组合成 PA,并向TLB中添加条目。多级页表的工作原理展示如下:

图 50  多级页表工作原理

       若查询 PTE 的时候发现不在物理内存中,则引发缺页故障。如果发现权限不 够,则引发段错误。

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

因为三级 Cache 的工作原理基本相同,所以在这里以 L1 Cache 为例,介绍三 级 Cache 支持下的物理内存访问。

L1 Cache 的基本参数如下:

1. 8 路 64 组相连

2. 块大小 64 字节 由 L1 Cache 的基本参数,可以分析知:

块大小 64 字节→需要 6 位二进制索引→块偏移 6 位

共 64 组→需要 6 位二进制索引→组索引 6 位

余下标记位→需要 PPN+PPO-6-6=40 位

故 L1 Cache 可被划分如下(从左到右):

CT(40bit)CI(6bit)CO(6bit)

在 7.4 中我们已经由虚拟地址 VA 转换得到了物理地址 PA,首先使用 CI 进行组索引,每组 8 路,对 8 路的块分别匹配 CT(前 40 位)如果匹配成功且块的 valid 标志位为 1,则命中(hit),根据数据偏移量 CO 取出相应的数据后返回。若没有匹配成功或者匹配成功但是标志位是 1,则不命中(miss),向下一级 缓存中请求数据(请求顺序为 L2 Cache→L3 Cache→主存,若仍不命中才继续向下一级请求)。查询到数据之后,需要对数据进行读入,一种简单的放置策略如下:若映射到的组内有空闲块,则直接放置在空闲块中,若当前组内没有空闲块,则产生冲突(evict),采用 LFU 策略进行替换。

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. 设置程序计数器 最后,execve 设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。

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

发生一个缺页异常后,控制会转移到内核的缺页处理程序。判断虚拟地址是 否合法,若不合法,则产生一个段错误,然后终止这个进程。

若操作合法,则缺页处理程序从物理内存中确定一个牺牲页,若该牺牲页被 修改过,则将它换出到磁盘,换入新的页面并更新页表。当缺页处理程序返回时, CPU 再次执行引起缺页的指令,将引起缺页的虚拟地址重新发送给 MMU。因为 虚拟页面现在缓存在物理内存中,所以就会命中,主存将所请求字返回给处理器。

7.9动态存储分配管理

Printf会调用malloc,请简述动态内存管理的基本方法与策略。

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

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

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

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

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

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

    1. 隐式链表 堆中的空闲块通过头部中的大小字段隐含地连接,分配器通过遍历堆中所有的块,从而间接遍历整个空闲块的集合。对于隐式链表,其结构如下:

图51 隐式链表的结构

    1. 显式链表 在每个空闲块中,都包含一个前驱(pred)与后继(succ)指针,从而减少了搜索与适配的时间。显示链表结构如下

图51 显式链表的结构

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

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

7.10本章小结

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

(第7 2分)

8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

设备管理:unix io接口

IO设备在Linux系统下被抽象为文件处理,所有的输入和输出都被当作对相应的文件的读和写来执行。通过这种从设备到文件的映射,Linux内核引出一个简单、低级的应用接口,称为Unix I/O。通过这种方法,所有的输入和输出都能够以一种统一且一致的方式来执行,即:打开文件、改变当前的文件位置、读写文件、关闭文件。

8.2 简述Unix IO接口及其函数

    1. 打开文件

进程通过调用open函数来打开一个已经存在的文件或创建一个新文件,对于Shell创建的每个进程,其都有三个打开的文件:标准输入,标准输出,标准错误。其中open函数的函数原型是int open(char * path,int flags,mode_t mode),将filename转换成一个文件描述符,并返回描述符数字。

    1. 改变当前的文件位置

对于每个打开的文件,由内核保存着一个文件位置k,k是初始值为0的从文件开头起始的字节偏移量,应用程序通过lseek函数来显示地修改当前文件的位置。其中lseek函数的函数原型为off_t lseek(int fd, off_t offset , int whence),修改成功返回新的文件偏移量,否则返回-1。

    1. 读写文件

读和写文件应用程序是通过分别调用read和write函数来执行输入和输出的。其中read函数函数原型是ssize_t read(int fd ,void buf , size_t n),从描述符为fd的当前文件位置复制最多n个字节到内存位置buf,正确读入返回实际传送的字节,EOF返回0,读错误返回-1;write函数函数原型是ssize_t write(int fd , const void buf,size_t n),从内存位置buf复制最多n个字节到描述符为fd的当前文件位置。

    1. 关闭文件

进程通过调用close函数关闭一个打开的文件,由内核来释放打开文件时创建的数据结构,并将此文件的描述符恢复到可用的描述符池中。其中close函数原型是int close(int fd),fd是要关闭的文件的描述符。

8.3 printf的实现分析

  1. 首先查看printf函数的函数体

图52     printf函数的函数体

其中形参列表中的...是可变形参的一种写法,当传递参数的个数不确定时可以采用这种方式来表示。va_list被定义为一个字符指针,而arg表示...中的第一个参数。

  1. 查看vsprintf函数的函数体

printf函数通过调用vsprintf函数生成显示信息,根据参数fmt和args生成格式化后的字符串,返回要打印出来的字符串的长度。

图53 vsprintf函数的函数体

  1. 查看write系统函数

图54 write系统函数

write函数中,先向寄存器传递参数,然后是用于系统调用的陷阱指令syscall。

  1. Syscall的调用与字符显示

syscall指令调用系统服务,执行打印操作。内核通过字符显示子程序,根据传入的ASCII码到字模库读取字符对应的点阵,然后通过vram对字符串进行输出。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量),最终在终端输出字符串。

8.4 getchar的实现分析

  1. 首先查看getchar函数的函数体

图54 getchar函数的函数体

从标准输入里读入下一个字符,返回类型为int型,即用户输入的ASCII码或EOF。

  1. read函数

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

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

8.5本章小结

本章主要介绍了Linux的IO设备管理方法和Unix IO接口及函数,深入分析了解了printf函数和getchar函数。

(第81分)

结论

用计算机系统的语言,逐条总结hello所经历的过程。

你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。

  1. 预处理

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

  1. 编译

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

  1. 汇编

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

  1. 链接

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

  1. 加载运行

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

  1. 执行指令

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

  1. 访存

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

  1. 动态申请内存

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

  1. 信号处理

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

  1. 终止并回收

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

       对计算机系统的设计与实现的深切感悟:

hello程序的一生繁荣复杂却丰富多彩耐人寻味,是计算机系统发展的智慧结晶。在学习计算机系统的设计与实现后,可以很强烈地感受到当解决一个高复杂的问题的过程中,基本操作与底层实现的重要性。今后,在设计或实现一个方法的过程中,不妨将问题碎片化,针对不同的碎片实现不同的基础性的、可复用性的方法,最终将这些碎片有机结合起来,来解决不同的问题。此外,对于抽象这个概念有了更具象的理解,从编译过程到进程,再到内核,再到虚拟内存,抽象无处不在,意义非凡,帮助我们更好地去理解、去实现。

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

附件

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

文件名

作用

hello.i

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

hello_2.i

注释stdio.h头文件后预处理得到的文件

hello.s

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

hello.o

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

hello.elf

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

hello.asm

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

hello_2.elf

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

hello_2.asm

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

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

参考文献

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

[1]  深入理解计算机系统第三版 (美)兰德尔·E·布莱恩特(Randal E.Bryant)等著:龚奕利,贺莲译[M].北京:机械工业出版社,2016

[2]  关于Linux下gcc 编译 C 源文件时,生成的是Shared object file而不是Executable file_OopspoO的博客-CSDN博客

关于Linux下gcc 编译 C 源文件时的选项

[3]  认识目标文件的结构_恋喵大鲤鱼的博客-CSDN博客 目标文件的结构

[4]  程序头 - 链接程序和库指南 链接程序和库指南

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值