Hello简单而不平凡的一生

摘  要

Hello.c程序,几乎是每个程序员所编写的第一个程序,他是我们踏入编程之路的一个起点。然而其在计算机中的运行,并非如其代码一般简单。本文通过对提供的hello.c程序在预处理、编译、汇编、链接等具体阶段以及进程管理、存储管理和IO管理等方面的分析,深入了解程序在计算机中的一生。

关键词:预处理;编译;汇编;进程;                           

目  录

第1章 概述............................................................................................................. - 4 -

1.1 Hello简介...................................................................................................... - 4 -

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

1.3 中间结果......................................................................................................... - 4 -

1.4 本章小结......................................................................................................... - 4 -

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

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

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

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

2.4 本章小结......................................................................................................... - 8 -

第3章 编译............................................................................................................. - 9 -

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

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

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

3.4 本章小结....................................................................................................... - 13 -

第4章 汇编........................................................................................................... - 14 -

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

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

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

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 链接的重定位过程分析............................................................................... - 22 -

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

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

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的异常与信号处理............................................................................ - 29 -

6.7本章小结....................................................................................................... - 32 -

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

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

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

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

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

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

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

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

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

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

7.10本章小结..................................................................................................... - 37 -

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

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

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

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

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

8.5本章小结....................................................................................................... - 41 -

结论......................................................................................................................... - 41 -

附件......................................................................................................................... - 43 -

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

第1章 概述

1.1 Hello简介

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

P2P:即 From Program to Process。即源程序从程序变为进程的过程。首先,预处理器(cpp)修改原始的C程序,将hello.c文件转化为hello.i;然后编译器(ccl)将文本文件hello.i翻译成为文本文件hello.s汇编程序;汇编器(as)将hello.s翻译成机器语言指令并打包成可重定位目标程序hello.o; 最后再由链接器(ld)将其变为可执行的目标程序。这时,在shell中运行它,OS(进程管理)会通过fork来为其创建一个新的进程(Process)。

020:即 From Zero-0 to Zero-0。shell通过execve在fork产生的子进程中加载hello,先删除当前虚拟地址的用户部分已存在的数据结构,为hello的创建新的区域结构,然后映射虚拟内存,设置程序计数器,并依需求载入物理内存,而后进入main函数,CPU为hello分配时间片执行逻辑控制流。hello通过Unix I/O管理来控制输入和输出。在程序运行结束后,父进程会对其进行回收,内核把它从系统中清除,hello完成了它的一生,这便是020的过程。

1.2 环境与工具

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

硬件环境:X64 CPU  2GHz  16G RAM  512GSSD Disk

X64 CPU  2GHz  4G RAM  40GSSD Disk

软件环境:Windows10 64位  VirtualBox  Ubuntu 16.04 LTS 64位

调试工具:gedit、vim、gcc、gdb、IDA Pro64、readelf、hexedit

1.3 中间结果

hello.i 预处理产生文件

hello.s 编译产生文件

hello.o 汇编产生文件

hello 链接产生可执行文件

hello.objdump hello反汇编生成文件

1.4 本章小结

本章主要介绍了hello程序从代码的生成,到编译,执行,最后到终止的过程(P2P和020)。并列出了研究所使用的软硬件环境和工具,以及在研究过程中所生成的中间文件。

第2章 预处理

2.1 预处理的概念与作用

预处理的概念:预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。

预处理的作用:

预处理器(cpp)根据以字符#开头的命令,修改原始的C程序,读取头文件如stdio.h等的内容,并把它直接插入程序文本中。命令包括:

1.文件包含:通过#include为文件的引用(库文件)组合源程序正文。

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

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

4.宏替换:#define,可以定义符号常量、函数功能等。

合理使用预处理功能编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。

2.2在Ubuntu下预处理的命令

cpp hello.c > hello.i

图2-1

2.3 Hello的预处理结果解析

(1)代码量大大增加,原程序main函数位于hello.i的末尾

图2-2预处理文件中的main函数

(2)程序加载了调用的<stdio.h>、<unistd.h>、<stdlib.h>三个头文件。

图2-3部分加载<stdio.h>的代码

(3)在hello.i没有之前源代码中的注释部分,可见在预处理过程中注释部分被删除掉了。

(4)加载了其他的头文件。

图2-4部分加载其他头文件的代码

2.4 本章小结

本章主要介绍了hello.c文件在linux中进行预处理的方式,并且对处理后的结果进行了简略的解释。预处理器cpp为hello去掉hello.c中无用的包袱(代码中的注释),将引用的头文件展开插入到程序文本中并替换宏定义,为程序接下来的转变做好准备。

第3章 编译

3.1 编译的概念与作用

概念:编译,就是把代码转化为汇编指令的过程,并把预处理完的文件进行一系列语法分析及优化后生成相应的汇编文件。

作用:编译器ccl将文本文件如hello.i翻译成文本文件如hello.s,它包含一个汇编语言程序。它以高级程序设计语言书写的源程序作为输入,而以汇编语言或机器语言表示的目标程序作为输出。该程序包含函数main的定义等,其中每条语句都以一种文本格式描述了一条低级语言指令。

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

3.2 在Ubuntu下编译的命令

gcc -S hello.i -o hello.s

图3-1

3.3 Hello的编译结果解析

3.3.1 数据

(1)常量

A.立即数

在该程序的编译结果中,立即数常出现在栈指针的移动,判断跳转,赋值操作中。

图3-2部分涉及立即数的操作

B.字符串

字符串常量只存在于输出中,被保存在rodata段中。

图3-3字符串常量

(2)局部变量

变量只有一个局部变量i,可判断其储存在rbp向下四个字节的位置。

图3-4与i相关的部分操作

(3)数组

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

图3-5与数组相关的代码

3.3.2赋值操作

赋值操作使用的是mov指令。在hello.c中,只有一个赋值操作,就是循环式将局部变量i赋值为了0。

图3-5对i的赋值操作

3.3.3类型转换

hello.c中涉及的类型转换是:atoi(argv[3]),将字符串类型转换为整数类型。

3.3.4算术操作

算术操作以hello程序里的“i++”为例,通过ADD操作实现。

图3-6

3.3.5关系操作

关系操作均出现在判断跳转中

(1)使用argc与4进行比较来判断是否输出字符串

      

图3-7

    (2)在.L3部分中,使用i与7进行比较来实现i<8进入.L4的操作

图3-8

3.3.6数组操作

存在一个对数组argv的操作,在printf函数中引用了数组argv的两个元素argv[1],argv[2], 可以通过L4(循环部分)中输出函数前的两次取值找到argv[1],argv[2]的位置。

图3-9

3.3.7控制转移

(1)if(argv!=3)

首先 cmpl 比较 argv 和 4,不相等就直接跳转到.L2,否则顺序执行下一条语句,即执行 if 中的代码。

图3-10

(2)for(i=0;i<8;i++)

首先跳转到.L3中,使用 cmpl 进行比较,如果 i<=7,则跳入.L4 for 循环体执行,否则说明循环结束,之后顺序执行。在.L4中再执行i++,之后再进入.L3,使用cmpl进行比较。

图3-11

(3)无条件跳转

对i初始化为0后的无条件跳转,以跳到L4,即循环部分代码。

图3-12

3.3.8函数操作

(1)main函数:

 参数传递:将int argc, char argv[]传入main函数。

 函数调用:main函数调用了printf(),exit(),sleep(),getchar()四个函数

 函数返回:当main函数正常运行结束后返回1

(2)printf函数:

 参数传递:将需要输出的变量传入的printf函数

 函数返回:printf函数返回所需要打印的值

(3)exit函数:

 参数传递:传入了1作为参数

 函数返回:exit(1)表示程序出现异常时退出

(4)sleep函数:

 参数传递:传入了atoi(argv[3])

函数返回:传入了atoi(argv[3])之后,程序在传参大小时间过后继续进行

(5)getchar函数:

 函数返回:函数返回输入字符的ASCII码或者EOF表示输入有误

3.4 本章小结

本章主要介绍了编译器将预处理后的程序(hello.i)转换为汇编指令(hello.s)的过程,分析并展示了编译器处理C语言的各个数据类型以及各类操作的方法,按照不同的数据类型和操作格式,解释了hello.c文件与hello.s文件间的映射关系。

第4章 汇编

4.1 汇编的概念与作用

概念:汇编是指把汇编语言代码翻译成目标机器指令的过程。

作用:将.s文件如(hello.s)翻译成机器语言指令,把这些指令打包成一种叫可重定位目标程序的格式,并将结果保存在目标文件.o文件如(hello.o)中。

4.2 在Ubuntu下汇编的命令

as hello.s -o hello.o

图4-1

4.3 可重定位目标elf格式

通过 readelf –a 指令查看可重定位目标文件hello.o的ELF文件各节基本信息。

(1)ELF可重定位目标文件中首先是ELF头,它以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。

图4-2

(2)节头部表:描述了不同节的位置和大小等信息。

图4-3

3重定位节:一个.text 节中位置的列表,包含.text 节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。

图4-4

(4)符号表:它存放了程序中定义和引用的函数和全局变量的信息(不包含局部变量的条目)

图4-5

4.4 Hello.o的结果解析

图4-6反汇编后的部分代码

hello.o是hello.s经由gcc编译器通过as汇编指令后得到的,但是hello.o经过反汇编后二者却产生了细微的差别,其中反汇编代码所显示的不仅仅是汇编代码,还有机器代码,。

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

(2)函数调用:在.s文件中,函数调用之后直接跟着函数名称,而在反汇编程序中,call的目标地址是当前下一条指令。这是因为hello.c中调用的函数都是共享库中的函数,最终需要通过动态链接器才能确定函数的运行时执行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其call指令后的相对地址设置为全0(目标地址正是下一条指令),然后在.rela.text节中为其添加重定位条目,等待静态链接的进一步确定。

  机器语言是二进制的机器指令的集合,机器指令又是由操作码和操作数构成的,是电脑真正可以识别的语言。

汇编语言的主体是汇编指令,是以人们比较熟悉的词句直接表述CPU动作形成的语言,是最接近CPU运行原理的较为通俗的比较容易理解的语言。

机器语言与汇编语言具有一一对应的映射关系,一条机器语言程序对应一条汇编语言语句。汇编指令和机器指令的差别在于指令的表示方法上,汇编指令是机器指令便于记忆的书写格式。

4.5 本章小结

本章介绍了hello从hello.s到hello.o的汇编过程。首先给出了汇编的命令行,将其转换成可重定位目标文件。之后展示了可重定位文件的ELF头,节头表,.rela.text节和符号表的内容。之后通过反汇编,将其结果与hello.s汇编程序代码进行比较,比较两者间的区别与联系。

5章 链接

5.1 链接的概念与作用

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

作用:把预编译好了的若干目标文件合并成为一个可执行目标文件。使得分离编译称为可能,不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为可独立修改和编译的模块。当改变这些模块中的一个时,只需简单重新编译它并重新链接即可,不必重新编译其他文件。

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

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

使用 readelf -a hello命令、查看 hello 程序的 ELF 格式文件。

(1)ELF可重定位目标文件中首先是ELF头,它以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。

图5-2

(2)节头部表:在节头表中记录了各个节的名称、大小、偏移量、地址、旗标、对齐方式等信息。

图5-3节头部表的部分截图

3程序头表:程序头部表是一个数组,数组中的每一个元素就称为一个程序头,每一个程序头描述一个内存段或者一块用于准备执行程序的信息。

图5-4

(4)节到段的映射信息:展示了节到具体的段的映射信息。

图5-5

(5)符号表:它存放了程序中定义和引用的函数和全局变量的信息。

图5-6

5.4 hello的虚拟地址空间

通过查看edb,看出hello的虚拟地址空间开始于0x401000,结束与0x401ff0。

图5-7

图5-8

在edb中查看Symbols选项,并与5.3中列出的虚拟地址各段信息做对比,可以看到都显示出了匹配的相同地址。

图5-9

5.5 链接的重定位过程分析

使用 objdump -d -r hello > hello.objdump 获得 hello 的反汇编代码。

(1)在hello.o中,我们看不到各函数的代码段,而在hello中,存在了各个函数的代码段。并且可执行文件的反汇编结果中给出了重定位结果,即虚拟地址的确定。而hello.o的反汇编结果中,各部分的开始地址均为0。

图5-10可执行文件的反汇编结果

图5-11 hello.o的反汇编结果

(2)对于函数的调用,因为hello.o中尚未对函数定位,所以在调用时都用call加下一条指令地址来表示,而hello中各函数已拥有了各自的虚拟地址,所以在call后加其虚拟地址来实现函数调用。且在链接的过程中,已经将库函数与hello.o链接,加入了库函数的汇编部分。

(3)对于跳转指令,hello.o中在其后加上目的地址,为main从0开始对每条指令分配的地址;而hello中同样加上目的地址,但这里是每条指令的虚拟地址。
     在重定位的过程中,链接器将合并输入模块,并未每个符号分配运行时地址。重定位由两步组成:重定位节和符号定义。在这一步中,链接器将所有相同类型的节合并为同一类型的新的聚合节,这个节成为输出的可执行文件的节。然后,链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。当这一步完成时,程序中的每条指令和全局变量就有唯一的运行时内存地址了。

对于hello来说,链接器把hello中的符号定义都与一个内存位置关联了起来,重定位了这些节,并在之后对符号的引用中把它们指向重定位后的地方。hello中每条指令都对应了一个虚拟地址,而且对每个函数,全局变量也都它关联到了一个虚拟地址,在函数调用,全局变量的引用,以及跳转等操作时都通过虚拟地址来进行,从而执行这些指令。

5.6 hello的执行流程

    程序名称                   地址

ld-2.31.so!_dl_start            0x7f8e7cc38ea0

ld-2.31.so!_dl_init              0x7f8e7cc47630

hello!_start                  0x4010f0

libc-2.31.so!_libc_start_main    0x7ff825445fc0

libc-2.31.so!_cxa_atexit        0x7ff825736f60

hello!_libc_csu_int            0x4011c0

libc-2.31.so!_setjmp           0x7ff 82fdb2d00

libc-2.27.so!exit               0x7ff 82fdc3ebd0

5.7 Hello的动态链接分析

动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。虽然动态链接把链接过程推迟到了程序运行时,但是在形成可执行文件时(注意形成可执行文件和执行程序是两个概念),还是需要用到动态链接库。比如我们在形成可执行程序时,发现引用了一个外部的函数,此时会检查动态链接库,发现这个函数名是一个动态链接符号,此时可执行程序就不对这个符号进行重定位,而把这个过程留到装载时再进行。

PLT:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。

GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。

通过查看hello的ELF文件如下图:

图5-12

GOT表在调用dl_init之前的内容如下:

图5-13

在dl_init调用后内容如下图:

图5-14

从图中可以看出,在dl_init调用之后,该处的两个8字节的数据都发生了改变。和PLT联合使用时,GOT[0]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。其中GOT[1]指向重定位表(依次为.plt节需要重定位的函数的运行时地址)用来确定调用的函数地址, GOT[2]是动态链接器ld-linux.so模块中的入口点。

5.8 本章小结

本章主要对链接后的可执行文件进行了分析,利用objdump、edb等工具分析了重定位之后的代码。经过这个过程,可以清晰地看到代码在重定位前后发生的变化,在5.5、5.7节中对重定位的具体过程进行详细的剖析。

第6章 hello进程管理

6.1 进程的概念与作用

概念:进程的经典定义是一个执行中程序的实例,系统的每个程序都运行在某个进程的上下文。上下文是由程序正确运行所需的状态组成的,这个状态包括存放在内存里的程序的代码和数据,它的栈,通用目的寄存器的内容,程序计数器,环境变量以及打开文件描述符的集合。
    作用:向用户提供了一种假象。 我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存。处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。

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

Shell-bash的作用与处理流程Shell-bash是内核与用户之间的桥梁,充当命令解释器的作用,将用户输入的命令翻译给系统执行。Shell可以帮助用户打开文件或者代替执行一些工作。

处理流程:

①将读入的命令进行切分,获得参数信息

②如果是内置命令,就立即执行

③如果不是内置命令,就分配子程序运行命令

④shell也可以接受信号,并对信号进行相应的处理

6.3 Hello的fork进程创建过程

父进程通过调用fork函数创建一个新的运行的子进程。调用fork函数后,新创建的子进程几乎但不完全与父进程相同:子进程得到与父进程虚拟地址空间相同的(但是独立的)一份副本(包括代码、数据段、堆、共享库以及用户栈),子进程获得与父进程任何打开文件描述符相同的副本,这意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。fork被调用一次,却返回两次,子进程返回0,父进程返回子进程的PID。子进程有不同于父进程的PID。

图6-1 fork的进程创建

6.4 Hello的execve过程

execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境变量列表envp。只有当出现错误时,例如找不到filename,execve才会返回到调用程序。加载并运行需要以下几个步骤:

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

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

3.映射共享区域。如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域。

4.设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。下一次调用这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。

6.5 Hello的进程执行

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

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

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

上下文切换:当一个进程正在执行时,内核调度了另一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程。在进行上下文切换时,需要保存以前进程的上下文,恢复新恢复进程被保存的上下文,将控制传递给这个新恢复的进程来完成上下文切换。

hello的进程执行过程如下:

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

6.6 hello的异常与信号处理

图6-2可能产生的异常

正常运行:

按照要求运行可执行文件hello,可以看到正常的运行结果是每隔一个时间间隔(我这里设置的是一秒一次)输出一次,共输出八次,最后随便输入字符回车后结束:

图6-3 正常运行的结果

 不停乱按,包括回车:

如果乱按过程中没有按回车,则只会在屏幕上显示输入的内容。如果输入回车,则getchar读回车,并把回车前的字符串当作shell输入的命令。

图6-4

Ctrl-C

如果在程序运行过程中输入Ctrl+C,会让内核发送一个SIGINT信号给到前台进程组中的每个进程,结果是终止前台进程。

图6-5

Ctrl-Z

Ctrl-Z后运行ps,打印出了各进程的pid,可以看到之前挂起的进程hello。

Ctrl-Z后运行jobs,和上述ps命令的效果差不多,只不过jobs命令列出的是作业,而ps命令列出的是进程。

Ctrl-Z后运行pstree,可看到它打印出的信息。

   

图6-6

Ctrl-Z后运行fg,运行fg 可以把之前挂起在后台的hello重新调到前台来执行,打印出剩余部分。

图6-7

Ctrl-Z后运行kill,得知hello的进程号为15064,那么便可通过kill -9 15064发送信号SIGKILL给进程15064,它会导致该进程被杀死。然后再运行ps,可发现已被杀死的进程hello。

图6-8

6.7本章小结

本章中主要讨论了hello进程管理的各个方面,包括从加载到运行再到运行时各种异常与信号处理的测试。

首先对进程的概念、作用进行了说明,然后详细剖析了加载运行程序时使用的fork、execve函数的作用以及工作流程。最后,在hello运行的过程中分别配合ps、fg等命令测试了ctrl-c、ctrl-z、乱按等情况下信号的接收与处理情况,展示了详细的测试结果,并分析了相应的处理机制。

7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:有地址变换功能的计算机中,访内指令给出的地址 (操作数) 叫逻辑地址,也叫相对地址。要经过寻址方式的计算或变换才得到内存储器中的实际有效地址,即物理地址。在hello的程序中,程序的地址都是相对地址,因为hello在运行之前并不知道将会被放在哪个实际的内存当中,所以采用相对寻址的方式可以让程序有更大的灵活性。即hello.o 里面的相对偏移地址。

线性地址:线性地址是一个32位无符号整数,可以用来表示高达4GB的地址,也就是,高达4294967296个内存单元。线性地址通常用十六进制数字表示,值的范围从0x00000000到0xffffffff)。即 hello 里面的虚拟内存地址。

虚拟地址:CPU启动保护模式后,程序运行在虚拟地址空间中。并不是所有的“程序”都是运行在虚拟地址中。CPU在启动的时候是运行在实模式的,Bootloader以及内核在初始化页表之前并不使用虚拟地址,而是直接使用物理地址的。Hello在执行时会被分配一个虚拟地址,虚拟地址需要通过地址翻译成物理地址才可以访问内存。hello.s中使用的就是虚拟空间的虚拟地址)

物理地址:在计算机科学中,物理地址,也叫实地址、二进制地址,它是在地址总线上,以电子形式存在的,使得数据总线可以访问主存的某个特定存储单元的内存地址。在和虚拟内存的计算机中,物理地址这个术语多用于区分虚拟地址。Hello访问内存最终需要物理地址对内存的访问。即 hello 在运行时虚拟内存地址对应的物理地址。

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

段式管理,是指把一个程序分成若干个段进行存储,每个段都是一个逻辑实体,程序员需要知道并使用它。它的产生是与程序的模块化直接有关的。段式管理是通过段表进行的,它包括段号或段名、段起点、装入位、段的长度等。此外还需要主存占用区域表、主存可用区域表。

为了进行段式管理,每道程序在系统中都有一个段(映象)表来存放该道程序各段装入主存的状况信息。在程序的执行过程中,各段的装入位随该段是否活跃而动态变化。当装入位为“1”时,地址字段用于表示该段装入主存中起始(绝对)地址,当装入位为“0”时,则无效(有时机器用它表示该段在辅存中的起始地址)。 假设系统在主存中最多可同时有N道程序,可设N个段表基址寄存器。对应于每道程序,由基号(程序号)指明使用哪个段表基址寄存器。段表基址寄存器中的段表基址字段指向该道程序的段表在主存中的起始地址。段表长度字段指明该道程序所用段表的行数,即程序的段数。

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

线性地址(虚拟地址)由虚拟页号VPN和虚拟页偏移VPO组成。首先,MMU从线性地址中抽取出VPN,并且检查TLB,看他是否因为前面某个内存引用缓存了PTE的一个副本。TLB从VPN中抽取出TLB索引和TLB标记,查找对应组中是否有匹配的条目。若命中,将缓存的PPN返回给MMU。若不命中,MMU需从页表中的PTE中取出PPN,若得到的PTE无效或标记不匹配,就产生缺页,内核需调入所需页面,重新运行加载指令,若有效,则取出PPN。最后将线性地址中的VPO与PPN连接起来就得到了对应的物理地址。

图7-1 线性地址到物理地址的变换

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

首先,48位的虚拟地址将会被分为36位的VPN以及12位的VPO,前者按照9/9/9/9划分为四级页表的索引,而后者是页面内的位置(页面大小为4k,对应着12位)。

四级页表中每一级都对应着512个表项,其中只有第四级页表的表项中装着PPN。CR3指向的是第一级页表首地址,前9位VPN是一级页表的偏移量,用来索引一级页表中的表项,其表项内容为二级页表的首地址。第10-18位VPN是二级页表的偏移量……以此类推,最后9位VPN对应着第四级页表的偏移量,于是能找到对应的PPO。得到的40位PPO再与12位的VPO拼接,就得到了52位的物理地址。

图7-2VA到PA的变换

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

MMU将物理地址发给L1缓存,缓存从物理地址中取出缓存偏移CO、缓存组索引CI以及缓存标记CT。若缓存中CI所指示的组有标记与CT匹配的条目且有效位为1,则检测到一个命中,读出在偏移量CO处的数据字节,并把它返回给MMU,随后MMU将它传递给CPU。若不命中,则需到低一级Cache(若L3 cache中找不到则到主存)中取出相应的块将其放入当前cache中,重新执行对应指令,访问要找的数据。

7.6 hello进程fork时的内存映射

当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给他一个唯一的pid。为了给这个新进程创建虚拟内存,他创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有写时复制。当fork从新进程返回,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,也就为每个进程保持了私有地址空间的抽象概念。

7.7 hello进程execve时的内存映射

Execve函数在当前进程中加载并运行包含可执行目标文件hello.out中的程序,加载、运行 hello 需要以下步骤(如6.4)

1 . 删除已存在的用户区域。删除 shell 虚拟地址的用户部分中的已存在的区域结构。

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

3. 映射共享区域。如果 hello 程序与共享对象(或目标)链接,比如标准 C 库libc. so, 那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。

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

下图具体反映了映射方式:

图7-3

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

假设MMU在试图翻译某个虚拟地址A时,出发了一个缺页。这个异常导致控制转移到内核缺页处理程序,处理程序随后就执行下面的步骤:

1.判断虚拟地址A是否合法。也就是说,A在某个区域结构定义的区域内吗?却也异常处理程序搜索区域结构的链表,将A与每一个区域结构的头和尾相比较,如果没有匹配到任何结果,说明是不合法的,于是报出段错误。实际上系统采用更快的方式搜索链表,借助了树的数据结构。

2.判断进行的内存访问是否合法。也就是说进程是否有读、写或者执行这个区域内页面的权限?如果试图进行的访问是不合法的,那么缺页处理程序会触发一个保护异常,从而终止这个进程。

3.此时,如果已知这个缺页是由对合法的虚拟地址进行合法的操作造成的,就开始处理缺页。选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的命令,这条指令将再次发送A到MMU。这次MMU就能正常地翻译A,而不会再产生缺页中断了。

7.9动态存储分配管理

方法:

动态内存分配器维护着一个进程的虚拟内存区域,称为堆。它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址)。对于每个进程,内核维护着一个变量brk,它指向堆的顶部。

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

在hello中,printf函数会调用malloc函数。malloc函数返回一个指针,指向大小至少为size字节的内存块。有时,当我们运行程序时,才会直到某些数据结构的大小,这样便需要使用到动态内存分配。

当进行动态内存分配时,任何分配器都需要一些数据结构,如隐式空闲链表为例,带边界标签的隐式空闲链表中的每个块是由一个字的头部和一个字的脚部,有效载荷以及可能的额外填充组成的。头部和脚部编码了块的大小以及块是已分配还是空闲的。它们之间便是malloc时请求的有效载荷以及填充部分。他是通过头部脚部中的大小字段隐式连接的。在应用请求k字节的块时,分配器去查找一个足够大可以放置所请求块的空闲块。一旦其找到匹配的空闲块,就要分配空闲块的空间,若剩余部分足以形成新的空闲块,则将其分割。若找不到合适的空闲块,则需要向内核额外请求堆内存,将其转化为大的空闲块,插入到空闲链表中。当分配器释放一个已分配块时,可能有其他空闲块与新释放的相邻,这时需要进行合并,这时由于每个块的头部脚部记录了块是否空闲,那么便可通过检查其前面块的脚部和后面块的头部来判断是否有空闲块相邻。若是,也只需通过修改头部脚部便可进行合并。

策略:首先,对与堆中的块的组织,可以选择隐式、显示、分离空闲链表等。当分配器查找空闲块时,又可以采用不同的放置策略,如首次适配(从头开始搜索链表)、下一次适配(从上一次找到的空闲块的剩余块除法)以及最佳适配等。在分割空闲块时,又可以采用将剩余块分割为新的空闲块的策略。在合并时,可以采用带边界标记的合并,就像在上一段基本方法中所描述,通过边界标记来判断当前块周围是否也同样是空闲块,以此来判断是否需要合并。

7.10本章小结

本章主要涉及从磁盘到内存,再到高速缓存,最后到寄存器的层级储存。介绍了hello的地址空间,段式管理,页式管理,VA-PA的变换,以及hello进程 fork 时的内存映射、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。

8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

设备管理:unix io接口

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

8.2 简述Unix IO接口及其函数

Unix I/O接口:

1.打开文件。一个应用程序通过要求内核打开相应的文件,通知它想要访间一个I/O 设备。内核返回一个小的非负整数,它在后续对此文件的所有操作中标识这个文件。

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

3.改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k, 初始为0。这个文件位置是从文件开头起始的字节偏移量。

4.读写文件。一个读操作就是从文件复制n>0 个字节到内存,从当前文件位置k 开始,然后将k增加到k+n 。类似的写操作就是从内存复制n>0 个字节到一个文件,从当前文件位置k开始,然后更新k 。

5.关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。

Unix I/O函数:

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

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

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

4.ssize_t wirte(int fd,const void *buf,size_t n),write 函数从内存位置 buf 复制至多 n个字节到描述符为 fd的当前文件位置。

8.3 printf的实现分析

分析首先查看printf函数的函数内容:

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

2.   { 

3.       va_list args; 

4.       int i; 

5.       va_start(args, fmt); 

6.       write(1,printbuf,i=vsprintf(printbuf, fmt, args)); 

7.       va_end(args); 

8.       return i; 

9.}

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

接下来是write函数:

1.write:

2. mov eax, _NR_write

3. mov ebx, [esp + 4]

4. mov ecx, [esp + 8]

5. int INT_VECTOR_SYS_CALL

在printf中调用系统函数write(buf,i)将长度为i的buf输出,在write函数中,将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址,int INT_VECTOR_SYS_CALLA代表通过系统调用syscall。

syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。

字符显示驱动子程序将通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中。

显示芯片会按照一定的刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。于是我们的打印字符串就显示在了屏幕上。

8.4 getchar的实现分析

getchar由宏实现:#define getchar() getc(stdin)它有一个int型的返回值。当程序调用getchar()时,它会等待用户按键来输入字符。用户输入的字符被存放在缓冲区中,直到用户按了回车键,这时getchar()才会从stdio流中读入一个字符。

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

这可以看做一个异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。getchar函数调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

本章主要对hello的I/O管理进行了简述,囊括了I/O设备管理办法、Unix I/O的接口及函数两个部分,并简单分析了printf和getchar两个函数的实现。

结论

1.首先编写hello.c的源程序,从此hello.c诞生。此时hello.c仍是一个文本文件,还没有变成二进制文件。
2.对hello.c进行预处理(gcc -E),hello.c变成了hello.i。
3.对hello.i进行编译处理(gcc -S),hello.i变成了hello.s。
4.对hello.s进行汇编处理(gcc -c),hello.s变成了hello.o。此时hello变成了二进制文件。
5.对hello.o进行链接处理,将其与其它可重定位目标文件以及动态链接库进行链接生成可执行目标文件hello。此时hello程序就可以在计算机上执行。

6.在shell命令行来运行hello程序。

7.shell首先判断输入命令是否为内置命令。经过检查后发现其不是内置命令,则shell将其当作程序执行。

8.shell调用fork函数创建一个子进程。

9.shell调用execve函数,execve函数会将新创建的子进程的区域结构删除,然后将其映射到hello程序的虚拟内存,然后设置当前进程上下文中的程序计数器,使其指向hello程序的入口点。

10.运行hello时,内存管理单元MMU、翻译后备缓冲器TLB、多级页表机制、三级cache协同工作,完成对地址的翻译和请求。

11.在hello进程执行时,当在命令行中输入不同命令时,shell会向前台作业发送不同的信号,该信号会对前台作业进行对应的处理。

12.当hello进程执行完成后,父进程会对子进程进行回收。内核删除为这个进程创建的所有数据结构。

计算机系统的伟大之处在于各个阶段之间精密的协调与转换,在于各个精妙策略的设计。它们共同工作来运行应用程序。

抽象是计算机系统设计与实现的重要基础:文件是对I/O设备的抽象,虚拟内存是对程序存储器的抽象,进程是对一个正在运行的程序的抽象,虚拟机是对整个计算机的抽象。

为了满足不同的需求,计算机系统设计考虑一切可能的实际情况,设计出一系列的满足不同情况的策略。如写回和直写,写分配和非写分配,直接映射高速缓存和组相连高速缓存等等。相信在未来还会有更好更新的策略出现,能够更好的提升计算机的性能与处理问题的能力。

附件

hello.i  hello.c经预处理后生成的文件 进行了编译预处理,进行了宏替换、头文件加载和条件编译

hello.s  hello.i经编译后生成的汇编文件 将文件翻译成了汇编语言

hello.o  hello.s经汇编后生成的可重定位目标文件 将文件翻译成了机器语言

hello  经链接后生成的最终文件

hello.objdump  hello的反汇编文本

参考文献

[1]  Eandal E. Bryant,David R.O Hallaron.深入理解计算机系统[M]

北京:机械工业出版社2016-10

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值