程序人生-Hello’s P2P

​​​​​​​摘  要

本文通过对hello.c整个生命周期的分析,包括对预处理、编译、汇编、链接、进程管理、存储空间、IO管理等的分析,结合自己对计算机系统这门课程学习的理解,利用虚拟机、gcc、edb等工具进行试验,尝试对自己的学习做一个检验。同时,也向老师展示一下我学习的收获。

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

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

Hello的生命周期是从程序员编写的hello.c文件开始的。这是一个源程序。程序员编写hello.c后,hello需要经过一系列的操作在系统中运行起来。

首先hello.c经过预处理生成hello.i文件、经过编译产生hello.s文件、经过汇编产生hello.o文件,然后用链接器进行链接最终成为可执行文件hello。

此时我们就可以运行这个程序了。可以在shell中输入./hello直接运行,也可以使用./hello 字符串给main函数传递参数运行。无论使用哪种方法,shell都是调用一系列命令将整个输入的字符串读入到寄存器中。

然后shell通过调用fork创建一个新的子进程,然后调用execve映射虚拟内存,并通过mmap为hello程序开创了一片空间。

有了虚拟内存,cpu就可以读取虚拟内存中的.text,.data节获取代码和数据并分配时间片执行逻辑控制流。

当程序运行完毕,父进程回收hello以及其创建的子进程,内核删除相关数据结构。

1.2 环境与工具

硬件环境:X64 AMD Ryzen 7 4800U with Radeon Graphics 1.80 GHz;16.0G RAM

软件环境:Windows 10;VirtualBox-6.1.32;Ubuntu-20.04.4;

开发调试工具:Visual studio 2022 64位;codebooks 64位;vim;gcc;edb;readelf;

1.3 中间结果

hello.c 源程序

hello.i 预处理后文件

hello.s 编译后的汇编文件

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

hello 链接后的可执行文件

hello.elf hello.o的ELF格式

hello1.txt hello.o的反汇编

hello2.txt hello的反汇编代码

hello1.elf hello的ELF格式

1.4 本章小结

本章概括地介绍了hello程序的一生,其从源程序到运行到结束的整个过程。同时,列出了实验基本信息,包括实验软硬件环境、实验使用的工具以及实验过程中中间文件的作用。

(第1章0.5分)


第2章 预处理

2.1 预处理的概念与作用

预处理的概念:在源代码编译前对源代码进行的处理。具体为根据以字符“#”开头的命令修改原始的c程序,主要包括#define(宏定义),#include(源文件包含),#error(错误的指令)等。

预处理的作用:根据源代码中的预处理指令修改源代码,即将头文件中声明的文件复制到这个程序中,方便下一步编译。最终得到了另一个头文件等被相应代码替换的另一个文件,以.i作为拓展名。通过一个预处理命令使得一个源代码编译程序可以在不同的程序运行语言环境中被各种语言编译器方便的进行编译。

2.2在Ubuntu下预处理的命令

正在上传…重新上传取消

2.3 Hello的预处理结果解析

正在上传…重新上传取消

hello预处理结果hello.c部分截图

结果分析:可以看到,经过预处理后,原本短短的几行代码变成了3000多行,内容大大增加,而且依然是C语言格式的文件。不难发现预处理所做的工作就是对源程序中的宏进行宏展开,三个头文件中的内容stdio.h,unistd.h,stdlib.h被依次展开包含进文件中。原来的头文件之外的内容,即原始的程序内容被包含在hello.i的最后。

2.4 本章小结

这章介绍了预处理的概念和作用,结合预处理的结果进行了简单的分析,理解了预处理就是对源程序的补充和替换这一基本概念,为后面的步骤打下了基础。

(第2章0.5分)


第3章 编译

3.1 编译的概念与作用

编译的概念:编辑器将文本文件hello.i翻译为hello.c。

编译的作用:把源程序翻译成一个可以使用多种汇编语言的目标程序,进行词法分析和语法分析,如果发现错误给出提示信息;同时编译过程中会产生一种新的中间代码,为下一步编译器进行优化做准备。大部分编译器都会针对程序做一些或多或少的优化以提高程序的性能。

3.2 在Ubuntu下编译的命令

正在上传…重新上传取消

3.3 Hello的编译结果解析

3.3.1节名称及作用

伪指令用于指导汇编器和链接器的工作。

其中有:

.file   声明源文件

.text   指示为代码段

.section .rodata  只读数据段

.globl 声明全局变量
 .type 声明一个符号是函数类型还是数据类型
 .size 声明大小
 .string 声明一个字符串
 .align 声明对指令或者数据的存放地址进行对齐的方式

3.3.2数据类型

①字符串

正在上传…重新上传取消

程序中的字符串都在只读数据段中,如图所示。Hello程序中的两个字符串都在数组里作为main函数的第二个参数传递的。数组的每个参数都是指向字符类型的指针,这两个字符串作为参数传递给printf函数。

②参数argc

正在上传…重新上传取消

参数argc是作为main函数的参数传递的,被放在堆栈里,位于-20(%rbp)处。

③参数char *argv[]

正在上传…重新上传取消

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

正在上传…重新上传取消

然后程序中的字符串argv[1]和argv[2]被rax分两次读取并传递给printf作为参数,argv[3]的秒数被第三次读取作为sleep函数的参数传递。

④局部变量int i

正在上传…重新上传取消

c程序的局部变量通常放在寄存器或栈中,这里编译器将局部变量i放在栈上的-4(%rbp)处,作为int类型占据4个字节。

3.3.3声明全局函数

正在上传…重新上传取消

hello.c声明了一个全局函数main,汇编代码用.global说明它是全局函数。这个程序只有一个全局函数。

3.3.4赋值操作

汇编代码中的赋值操作用mov指令实现,具体表现如下:

movb:字节
movw:字
movl:双字
movq:四字

3.3.5算数操作

汇编语言下的算数操作总体来说有以下几种:

正在上传…重新上传取消

本程序中用到的操作只有循环中i++,这里使用到的指令是add。

3.3.6关系操作

本汇编代码中使用了多处条件跳转,如argc!=4使用的是je条跳转:

正在上传…重新上传取消

i<8使用了jle条件跳转:

正在上传…重新上传取消

3.3.7控制转移指令

基本原则是汇编语言中先设置条件码,然后根据条件码来进行控制转移。在本程序中有两处:

①if语句的控制转移

对于源程序中的if(argc!=4)...else...这个条件判断,意义是如果argc等于4则执行if语句,否则执行else,对应的汇编代码为:

正在上传…重新上传取消

比较如果4和argc,如果相等则执行.L2(if语句),否则向下继续执行(else语句)。

②for循环控制

在源程序中首先将i置为0,每次判断i是否小于8,是则继续循环,每次循环结束i自增1,否则循环结束,相应的汇编代码如图所示:

正在上传…重新上传取消

在L2中将i置为0,在L3中进行循环判断,i小于等于7则进入L4(循环部分),每次L4末尾进行i自增1的操作。

3.3.8函数操作

函数操作分为三步。

传递控制:调用函数时,程序计数器设置为函数第一条代码,并保存调用指令下一条代码的地址。当函数返回时要把程序计数器设置为调用后的下一条指令。

参数传递:在调用函数时,可以向函数传递一个或多个数据;相应的函数可以返回一个值。

内存管理:在调用函数时,为函数分配空间;返回时,释放这些空间。

本程序中调用的函数如下:
main函数:参数是int argc,char *argv[];
printf函数:参数是argv[1],argv[2];
exit函数:参数是1;
sleep函数:参数是atoi(argv[3]);

atoi函数:参数是argv[3];
getchar函数:无参数。

3.3.9类型转化

前文说过argv是一个字符指针的数组,而sleep函数需要一个整型变量作为参数,这个参数就是argv[3]。所以要进行类型转化。这里是调用了atoi函数进行类型转化。

3.4 本章小结

本章主要介绍了编译器处理一个c程序的基本过程。编译器将源代码转换为等价的汇编代码,我们通过对c语言的数据,赋值语句,算术操作,关系操作,控制转移,函数操作,类型转换这几点进行分析,理解了编译器编译的机制,了解了编译器对c语言程序中的不同操作分别会采取什么策略。

编译器在将hello.c转换为等价的hello.s后,hello程序由较高级的c语言形式变为了较低级但更贴近底层的汇编语言形式。虽然还不是机器可以直接识别的语言形式,但为后面的汇编等操作打下基础。

(第3章2分)

第4章 汇编

4.1 汇编的概念与作用

汇编器将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,将结果保存在目标文件hello.o中,hello.o文件是一个二进制文件,它包含目标程序的所有指令编码,这个过程称为汇编。

汇编的作用是将汇编代码转换为机器指令,使其能被机器识别运行。

4.2 在Ubuntu下汇编的命令

正在上传…重新上传取消

4.3 可重定位目标elf格式

    

正在上传…重新上传取消

利用该命令生成hello.elf。

正在上传…重新上传取消

4.3.1 ELF头

elf头以一个16字节的目标序列开始,这个字节的序列主要描述了一个生成该目标文件的操作系统的目标文件大小和生成目标字节的顺序。其他的描述还包括elf头的位置和大小,目标文件的位置和类型(例如是可重定位,可执行或者目标文件所共享的)机器文件类型,节头部表的目标文件位置和偏移,节头部表的偏移大小和目标文件数量。不同节的目标文件位置和偏移大小都是由一个节头部表条目来描述的,其中每个目标文件中每个目标字节都有一个固定大小的节头部表条目。

正在上传…重新上传取消

4.3.2节头部表

记录各节名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐。

正在上传…重新上传取消

4.3.3重定位节

正在上传…重新上传取消

包括5个部分

偏移量:通常对应于一些需要重定向的程序代码所在.text或.data节中的偏移位置。
信息:重定位到的目标在符号表中的偏移量。
类型:代表重定位的类型,与信息相互对应。
名称:重定向到的目标的名称。
加数:用来作为计算重复或定位文件位置的一个辅助运算信息。

4.3.4符号表

正在上传…重新上传取消

用来存放程序中保存的函数和全局变量信息。但是不包括局部变量,局部变量存放在栈里,不在这里。

4.4 Hello.o的结果解析

正在上传…重新上传取消

通过上述命令生成txt文件,得到下面的内容:

正在上传…重新上传取消

发现这里也是汇编代码,但是与前面生成的hello.s有些许不同。反汇编与编译过程中生成的汇编代码的差异主要表现在以下几个部分:

4.4.1分支转移

正在上传…重新上传取消

在反汇编生成的汇编代码中,并不是使用.L2这种段名称,而是确定的地址。因为段名称只是机器在编译时为了方便采用的辅助方式,机器并不能识别这种表示。在汇编成为机器语言后,程序想要找下一条地址就不能用.L2这样的标记了,而要采用如图所示的,指示明确地址的方式。

4.4.2函数调用

正在上传…重新上传取消

和上面相似,反汇编后的代码函数调用部分call后面不是函数名称,而是具体的地址。因为我们在编译时所调用的函数都是共享库中的重定位函数,需要后续等待链接过程将重定位的函数目标地址链接到我们的共享库程序中来。没有重定位函数,调用的函数的地址是不确定的。只有当链接器确定了目标函数的地址和运行时地址,汇编代码才可以直接把调用函数的地址设置为下一条指令执行时的目标地址。

4.4.3全局变量的访问

在使用全局变量时,反汇编代码直接使用.rodata节的数据。和上述原因类似。编译时没有其真实地址,反汇编时直接使用真实地址

4.4.4数据表示

可以看到,编译过程中使用的时十进制表示,而反汇编中使用的是16进制表示。

4.5 本章小结

在本章中我们对hello.s进行了汇编,生成hello.h可重定位目标文件,同时分析了可重定位文件的ELF头、节头部表、符号表和可重定位节等内容,并使用反汇编方法将hello.h生产的反汇编代码与hello.s进行比较,分析了汇编语言进一步翻译到机器语言的过程

(第4章1分)

5链接

5.1 链接的概念与作用

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

链接的作用:链接器在软件开发过程中扮演着一个关键的角色,因为它们使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其它文件。

5.2 在Ubuntu下链接的命令

命令:

正在上传…重新上传取消

生成hello的反汇编代码:

正在上传…重新上传取消

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

命令:

正在上传…重新上传取消

5.3.1ELF头部表

正在上传…重新上传取消

与链接前相比,section headers和program headers均有增加,且Type变为EXEC。

5.3.2节头

正在上传…重新上传取消

正在上传…重新上传取消

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

5.3.3程序头部表

正在上传…重新上传取消

描述了整个文件的布局情况。

5.3.4符号表

正在上传…重新上传取消

5.3.5重定位节

正在上传…重新上传取消

包含了.text节中需要重定位的函数信息。

5.4 hello的虚拟地址空间

首先分析5.3.3中程序头的内容,发现可加载的程序段的地址为0x400000。

利用edb打开hello程序查看地址信息。 

正在上传…重新上传取消 在0x400000~0x401000段中,程序被载入,虚拟地址0x400000开始。根据5.3.2的节头部表,可以查找各个节的信息。比如.rodata节,虚拟地址开始与0x402000:

正在上传…重新上传取消

5.5 链接的重定位过程分析

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

正在上传…重新上传取消

与上一次查看反汇编时有一些区别,包括:

①重定位

一旦链接器完成了符号解析这一步,就把代码中的每个符号引用和正好一个符号定义(即其一个输入目标模块中的一个符号表条目)关联起来。此时,链接器就知道其输入目标模块中的代码节和数据节的确切大小。现在就可以开始重定位步骤了,在这个步骤中,将合并输入模块,并为每个符号分配运行时地址。重定位由两步组成:

重定位节和符号定义:在这一步中,链接器将所有相同类型的节合并为同一类型的新的聚合节。例如,来自所有输入模块的.data节被全部合并成一个节,这个节成为输出的可执行目标文件的.data节。然后,链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。

当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。
重定位节中的符号引用:在这一步中,链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。重定位算法如下图所示:

正在上传…重新上传取消

②节的增加

增加了部分节以及一些函数。

5.6 hello的执行流程

_init 0x401000;

_start 0x4010f0;

main 0x401125;

puts@plt 0x401090;

printf@plt 0x4010a0;

getchar@plt 0x4010b0;

atoi@plt 0x4010c0;

exit@plt 0x4010d0;

sleep@plt 0x4010e0;

5.7 Hello的动态链接分析

动态链接:

共享库(shared library)是致力于解决静态库缺陷的一个现代创新产物。共享库是一个目标模块,在运行或加载时,可以加载到任意的内存地址,并和一个在内存中的程序链接起来。这个过程称为动态链楼(dynamic linking),是由一个叫做动态链接器(dytamic linkeg的程序来执行的。共享库也称为共享目标(shared object),在Linux系统中通常用,s0后缀来表示,微软的操作系统大量地使用了共享库,它们称为DLL(动态链接库)。

共享库是以两种不同的方式来“共享”的。首先,在任何给定的文件系统中,对于一个库只有一个.so文件。所有引用该库的可执行目标文件共享这个.so文件中的代码和数据,而不是像静态库的内容那样被复制和嵌入到引用它们的可执行的文件中。其次,在内存中,一个共享库的.text节的一个副本可以被不同的正在运行的进程共享。

got链接器叫做全局变量过程偏移链接表,在plt和got中分别存放着链接器的目标变量和函数的运行时地址。一个动态的链接器通过静态的过程偏移链接表plt+got链接器实现了函数的一个动态过程链接,这样一来,它就已经包含了正确的绝对运行时地址。

据hello的ELF文件可知GOT起始表位置为0x404000(5.3.2)。在调用dl_init之前0x404008后的16个字节均为0,调用dl_init后.got.dot条目发生变化。

5.8 本章小结

通过对hello可执行文件的分析,我介绍了链接的概念和作用,并说明了hello是如何与其它文件链接成一个可执行目标文件的过程,展示了hello.o的elf的内容,分析了hello的虚拟地址空间,重定位过程,执行过程,动态连接过程,对链接的理解更加深刻了。

(第5章1分)

6hello进程管理

6.1 进程的概念与作用

进程的概念:进程是一个针对执行中的应用程序的程序的实例。系统中的每个应用程序都可以运行在某个应用进程的可执行上下文中。当用户向Shell中输入一个可执行文件时,shell会创建一个子进程,并在这个子进程的上下文中运行这个可执行文件。应用程序本身也可以创建子进程,并在新的进程中运行自己的代码或是其它的程序。

进程的作用:进程作为一个执行中程序的实例,系统中每个程序都运行在某个进程的上下文中,上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。我们应当感谢局部性的存在,进程所对应的处理功能部件会把它提供出来给所有的应用程序。它有两个关键抽象:一个独立的程序逻辑控制流:它可以提供一个独立的假象,好像我们的应用程序在一个独占的空间使用内存处理器。一个应用程序私有的地址处理器空间,它可以提供一个独立的假象,好像我们的应用程序独占的一个使用内存的系统。

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

在Linux中,shell壳其实就是一个交互级应用程序,它的作用是为使用者提供一个可操作界面,通过读取用户输入的字符串进行解析,自动调用相关命令执行应用程序。

Shell的处理流程:首先读入用户输入的字符串,然后进行切割得到所有参数。解析所读的内容,如果第一个命令行参数是一个内置的shell命令,则立即执行,否则调用一个应用程序为申请的程序创建一个子进程,在子进程的上下文中运行指定程序。同时shell运行通过键盘输入外部信号,如ctrl-c、ctrl-z、kill等并根据不同的信号采取不同的处理方式。

6.3 Hello的fork进程创建过程

通过阅读源程序,我们在终端输入“./hello 120L021107 kfz 1”。Shell读取输入的命令。首先判断我们输入的不是一个内置指令,于是找到当前所在目录下的可执行文件hello。然后shell调用fork()创建一个子进程,子进程就会因此得到与父进程(即shell)虚拟地址空间相同的一段各种的数据结构的副本(包括代码和数据段,堆,共享库和用户栈)。不同的是子进程与父进程的PID不同。之后准备在子进程上下文中运行这个可执行文件。

6.4 Hello的execve过程

调用fork()函数创建了一个子进程之后,子进程调用exceve函数在当前子进程的上下文加载并运行一个新的程序,也就是我们的hello程序。主要步骤如下:

①清空子进程的上下文。

②创建新的代码、数据、堆和栈段。所有这些区域都是私有的,写时复制的。虚拟地址空间的代码和数据区域被映射为hello的.text和.data区。新的栈和堆区域请求二进制零,初始长度为零。

③映射共享区域。若hello程序和共享对象链接,将这些对象动态连接到这个程序,并映射到虚拟内存的共享区域。

④设置程序计数器PC指向_start 地址,_start 最终调用hello中的main 函数。

要注意,在加载过程中没有磁盘到内存的数据复制。直到CPU引用一个被映射的虚拟页时才会进行复制,这时,操作系统利用它的页面调度机制将页面从磁盘传送到内存。

6.5 Hello的进程执行

Hello的进程执行依赖于进程提供的两个抽象,即一个独立的逻辑控制流和一个私有的地址空间。主要内容有:

①逻辑控制流:一系列程序计数器PC 的值的序列叫做逻辑控制流,这些值唯一地对应于包含在程序的可执行目标文件中的指令。进程是轮流使用处理器的,在同一个处理器核心中,每个进程执行它的流的一部分后被抢占,然后轮到其他进程。

②上下文信息

上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。

③上下文切换

指cpu从一个进程到另一个进程的过程。基本步骤为:

1.暂停当前进程执行流程,将各个寄存器内容存到内存中;

2.从内存中取出下一个将要执行的进程的上下文,存进各个寄存器中;

3.返回程序计数器记录的指令地址,用于恢复进程执行。

发生上下文切换一般是因为系统调用因某种原因而阻塞,内核就让当前进程休眠,转而运行另一个进程。

④时间片

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

⑤用户模式和内核模式

处理器有两个不同模式:“用户模式”和“内核模式”。根据处理器上运行的代码的类型,处理器在两个模式之间切换。用户模式的进程会受到限制,不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据。内核模式可以执行任何指令,访问内存中任何位置。这么做的原因是限制用户模式应用程序的虚拟地址空间可防止应用程序更改并且可能损坏关键的数据。

Hello程序的执行:前面已经讲到shell创建一个子进程来运行hello。子进程先调用execve为hello分配新的虚拟地址空间,然后运行在用户模式下,输出Hello 120L021107 kfz,再调用sleep函数进入内核模式,再返回用户模式,重复8次。在这个过程中,cpu不断进行上下文切换。

正在上传…重新上传取消

6.6 hello的异常与信号处理

首先回忆一下异常的种类。异常有中断、故障、陷阱和终止。中断通常是收到一个信号,此时进程停止,中断结束后返回下一条指令;故障通常来自一个可恢复的错误,处理错误后返回当前指令或终止;陷阱通常是有意的异常,如读取文件等,处理完毕返回下一条指令;终止通常来自不可恢复的错误,不会返回,进程直接结束。

在本程序中,我们可以尝试以下操作:正常停止,运行过程中按下ctrl-c,运行过程中按下ctrl-z,不停乱按,结果如下:
①正常运行

正在上传…重新上传取消

②按下ctrl-c

正在上传…重新上传取消

按下ctrl-c后父进程收到一个SIGINT信号,它的作用是结束正在运行的hello并让父进程回收它。我们按下ctrl-c后利用ps查看,发现没有hello,它已经被终止并回收了。

③按下ctrl-z

正在上传…重新上传取消

Ctrl-z的作用是将hello挂起。Hello并没有被回收,而是运行在后台,利用ps查看可发现hello没有被回收。然后可以输入fg将其调回前台运行。执行完毕循环的内容任意输入一个字符串结束进程并回收。此时再利用ps可以发现hello已经被回收。

④随意乱按

正在上传…重新上传取消

仅仅是将乱按的内容放到屏幕上输出。

6.7本章小结

本章回顾了进程的概念及其作用,回忆了shell的工作方式,了解了创建进程、利用execve运行hello的细节,以及遇到异常以及信号时的处理方式。

(第6章1分)

7hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:逻辑地址是程序经过编译后出现在汇编程序的地址,用来指定一个操作数或是一条指令的地址。表现为段地址:偏移地址。

线性地址&虚拟地址:线性地址和虚拟地址是同一概念。一个逻辑地址在经过段地址机制的转化之后变成一个线性分页地址,是逻辑地址向物理地址转化的一步。

物理地址:在存储器里以字节为单位存储信息,每一个字节单元给以一个唯一的存储器地址,称为物理地址。CPU通过地址总线的寻址,找到真实的物理内存对应地址。

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

上面已经提到,逻辑地址由两部分组成,表现为段标识符:偏移地址。段标识符是一个16位的字段,称为段选择符。其含义如下:

TI=0,选择全局描述符表(GDT),TI=1,选择局部描述符表(LDT)

RPL=00,为第0级,位于最高级的内核态,RPL=11,为第3级,位

于最低级的用户态,第0级高于第3级 。(内核工作在0环,用户工作在3环,中间环留给中间软件用。Linux仅用第0和第3环)

高13位-8K个索引用来确定当前使用的段描述符在描述符表中的位置。

段描述符是一种数据结构,实际上就是段表项。索引号就是“段描述符”的索引。可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符。每一个段描述符由8个字节组成。

将一个逻辑地址转为线性地址,步骤为:首先看TI是0还是1,确定选择GFT还是LDT。然后根据段描述符前13位确定段基址,将段基址于偏移量相加得到线性地址。

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

由线性地址,即虚拟地址向物理地址的转化,依赖于分页机制。通过分页机制以及页式管理,完成线性地址与物理地址间的映射。

所谓分页机制,就是系统把一个虚拟页作为信息传输的单元。一个虚拟页的大小等于物理页的大小,一般为4kb。利用MMU进行地址翻译,即虚拟页到物理页的映射。

要理解虚拟地址到物理地址的映射方法,必须了解一个结构,即页表,它是一个页表条目 (Page Table Entry, PTE)的数组,将虚拟页地址映射到物理页地址,每个进程一个。

从虚拟页到物理页的映射方法如下图所示:

首先一个虚拟地址有两部分,VPN和VPO。首先利用vpn选择适当的PTE,根据PTE提供的信息,我们知道虚拟页的情况。如果虚拟页已缓存,那直接将PTE中的物理页号和VPO合起来就得到了一个物理地址。如果虚拟页未缓存,则触发一个缺页故障调用一个缺页处理子程序将磁盘的虚拟页重新加载到内存中,然后再执行这个导致缺页的指令。

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

首先我们先回忆一下两个概念。

第一个是TLB。根据7.3的内容我们以及知道了把一个虚拟地址翻译成物理地址的方法。上述方法中页表是在内存中存放的。即使页表条目缓存在L1,也会花费至少1~2周期命中。为了消除该操作带来的时间开销,MMU设计了一个有高度相联度的集合,叫TLB,对于页码数很少的页表可以完全包含在TLB中,利用它来实现虚拟地址到物理地址的映射。

第二个概念是多级页表。这种方式减少了对内存的需求。即如果上一级页表中的PTE为空,则下一级页表根本不会存在。利用这种办法节省空间。

Core i7就使用四级页表的层次结构。Cpu产生一个虚拟地址,发送给MMU,MMU利用虚拟地址中的VPN,将高位作为TLBT,低位作为TLBI,在TLB中查找相应的PTE。如果找不到,则查询页表,CR3为一级页表起始地址,VPN1为偏移量,查询出下一级页表的起始地址,以此类推,直到四级页表,找到PPN,与VPO一起组合为物理地址,如下图所示:

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

获取物理地址后,利用组索引位查找组,在L1中比较,如果存在则比较标志位以及检验有效位是否为1,都满足则根据偏移量取出数据传给cpu,否则向下查找L2、L3,直到命中,然后一级一级往上传。向上传送过程中如果有空闲块则放在空闲块里,没有则驱逐一个不用的块放进里面。

7.6 hello进程fork时的内存映射

创建子进程时的内存映射我们前面已经提及过了,这里简单复述一下。shell或其它进程通过调用fork函数可以创建一个新的进程,这个新的进程拥有各自新的数据结构,并且被内核分配了一个唯一的、与父进程不同的pid。它有着自己独立的虚拟内存空间,并且还拥有自己独立的逻辑控制流,它可以访问当前已经可以打开的各类文件信息和页表的原始数据和样本,为了有效保护进程的私有数据和信息,同时为了节省对内存的消耗,进程的每个数据区域都被内核标记起来作为写时复制。也就是说,新进程现在的虚拟内存与父进程的虚拟内存相同,当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间。

7.7 hello进程execve时的内存映射

execve函数在当前进程的上下文中加载并自动运行一个新的代码共享程序,它会覆盖当前进程的所有虚拟地址和空间,删除当前进程虚拟地址的所有用户虚拟和部分空间中的已存在的代码共享区域和结构。新的运行程序仍然在堆栈中拥有相同的区域pid。主要步骤如下:

①清空子进程的上下文。

②创建新的代码、数据、堆和栈段。所有这些区域都是私有的,写时复制的。虚拟地址空间的代码和数据区域被映射为hello的.text和.data区。新的栈和堆区域请求二进制零,初始长度为零。

③映射共享区域。若hello程序和共享对象链接,将这些对象动态连接到这个程序,并映射到虚拟内存的共享区域。

④设置程序计数器PC指向_start 地址,_start 最终调用hello中的main 函数。

要注意,在加载过程中没有磁盘到内存的数据复制。直到CPU引用一个被映射的虚拟页时才会进行复制,这时,操作系统利用它的页面调度机制将页面从磁盘传送到内存。

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

前面提到虚拟地址查找PTE时,可能找不到相应的PPN,因为虚拟页面并没有映射到内存中,此时就触发一个缺页错误。缺页处理方法如下:

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

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

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

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

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

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

而对于缺页故障,引起方式的多样的,

处理步骤如下:
1.检查虚拟地址是否合法,如果不合法则触发一个段错误,程序终止。
2.检查进程是否有读、写或执行该区域页面的权限,如果不具有则触发保护异常,程序终止。
3.两步检查都无误后,执行上述缺页处理程序,内核选择一个牺牲页面,如果该页面被修改过则将其交换出去,换入新的页面并更新页表。然后将控制转移给hello进程,再次执行触发缺页故障的指令。

7.9动态存储分配管理

动态内存管理的基本方法与策略:
动态内存分配器维护着一个进程的虚拟内存区域,称为堆,系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高地址)。对于每个进程,内核维护着一个变量brk,它指向对的顶部。

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

分配器有两种基本风格. 两种风格都要求应用显式地分配块.它们的不同之处在于由哪个实体来负责释放已分配的块

显式分配器:要求应用显式地释放任何已分配的快。例如C语言中的 malloc 和 free。

隐式分配器: 应用检测到已分配块不再被程序所使用,就释放这个块,比如Java,ML和Lisp等高级语言中的垃圾收集 (garbage collection) ---性能的瓶颈之一。

隐式空闲链表的块:

隐式空闲链表的带边界标记的堆块格式:这种格式的头部和脚部存放了块的大小和是否分配的信息:

显示空闲链表:
显示空闲链表是将空闲块组织为某种形式的显示数据结构。堆被组织为一个双向空闲链表,在每个空闲块中,都包含一个前驱和后继的指针:

与隐式链表相比较:

分配时间从块总数的线性时间减少到空闲块数量的线性时间;

当大量内存被占用时快得多Much faster;

因为需要在列表中拼接块,释放和分配稍显复杂一些。

7.10本章小结

这章通过对程序运行时地址变化的分析,回忆分析了逻辑地址、虚拟地址、物理地址的概念以及转化方法,页表、TLB的使用,页表的命中和缺页及缺页处理方法。还有动态内存管理的操作,fork时的动态内存中断与映射、execve时的动态内存中断与映射、缺页的中断与缺页映射和中断的处理。通过梳理这些内容,对程序运行的过程理解更加深刻了。虽然我们编程中大多情况下并不需要考虑这些内容,但了解它们是编出高效程序的基础。

(第7章 2分)

8hello的IO管理

8.1 Linux的IO设备管理方法

一个Linux文件就是一个m个字节的序列,所有的 IO 设备都被模型化为文件,而所有的输入和输出都被 当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单低级的应用接口,称为 Unix I/O。

8.2 简述Unix IO接口及其函数

Unix IO接口:

1.打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。

2.Linux Shell创建的每个进程都有三个打开的文件:标准输入(描述符为0),标准输出(描述符为1),标准错误(描述符为2)。

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

4.读写文件。一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。

5.关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件,作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放他们的内存资源。

Unix I/O函数:
①int open(char* filename,int flags,mode_t mode)

进程通过调用open函数来打开一个存在的文件或是创建一个新文件。open函数将filename转换为一个文件描述符,并且返回描述符数字。

②int close(fd)

进程通过调用close关闭一个打开的文件。其中fd 是需要关闭的文件的描述符(C 语言中为指针),close 返回操作结果。

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

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

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

write 函数从内存位置buf 复制至多n 个字节到描述符fd 的当前文件位置。若成功则返回写的字节数,若出错则返回-1.

8.3 printf的实现分析

从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.

字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。

显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

Printf函数实现

int printf(const char *fmt, ...)

{

int i;

char buf[256];

     va_list arg = (va_list)((char*)(&fmt) + 4);

     i = vsprintf(buf, fmt, arg);

     write(buf, i);

     return i;

    }

打印操作最终封装给用户的形式是printf()函数,它的定义在文件printf.c中。查看printf()的定义,函数中调用了putc()函数来进行输出,继续跟踪putc()函数的定义,我们发现write函数被调用了,在这里继续跟踪write函数,会发现它的声明在user.h中: int write(int,void*,int),但是并不能找到这个声明所对应的C代码形式的具体实现,这就是一个系统调用。追踪下write:

    write:

     mov eax, _NR_write

     mov ebx, [esp + 4]

     mov ecx, [esp + 8]

     int INT_VECTOR_SYS_CALL

8.4 getchar的实现分析

Getchar中的read函数将缓冲区都读入buf数组中,返回缓冲区的长度。当buf数组为空,调用read函数,如果不空就返回buf中的第一个元素。

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

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

8.5本章小结

本章了解了设备I/O的基本原理,了解了一些输入输出函数的底层实现,基本了解了Linux的IO设备管理方法。

(第8章1分)

结论

经过上面的分析,我们大致可以总结出hello的一生了:

  1. 出生:当我们程序员写出一个正确的.c程序时,hello就出生了,hello的一生从这里开始。
  2. 预处理:将hello.c进行预处理时,将宏定义转换为字符串,并将调用的所有外部的库展开合并,生成hello.i文件。
  3. 编译:将hello.i文件进行翻译生成汇编语言文件hello.s。
  4. 汇编:将hello.s翻译成一个可重定位目标文件hello.o。
  5. 链接:将hello.o与可重定位目标文件和动态链接库链接成为可执行目标程序hello。
  6. 运行:
    ①在shell中输入./hello 120L021107 kfz 1;

②发现我们输入的不是内置shell命令,shell调用fork创建子进程;

③shell调用execve函数,映射虚拟内存,运行hello;

④在运行过程中,会有信号处理的过程

(7)回收:运行完毕后,shell回收子进程。

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


附件

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

hello.c 源程序

hello.i 预处理后文件

hello.s 编译后的汇编文件

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

hello 链接后的可执行文件

hello.elf hello.o的ELF格式

hello1.txt hello.o的反汇编

hello2.txt hello的反汇编代码

hello1.elf hello的ELF格式

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


参考文献

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

[1]  林来兴. 空间控制技术[M]. 北京:中国宇航出版社,1992:25-42.

[2]  辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.

[3]  赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).

[4]  谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.

[5]  KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.

[6]  CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.

[7]  深入理解计算机系统第三版

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值