计算机系统 大作业(HIT)

题     目  程序人生-Hello’s P2P 

计算机科学与技术学院

2021年5月

摘  要

本文对hello程序的整个生命周期进行了的分析,首先进行hello.c源程序的编写,之后运行C预处理器(cpp)将其进行预处理,生成hello.i文件,运行C编译器(ccl)将其进行翻译生成汇编语言文件hello.s,然后运行汇编器(as)将其翻译成一个可重定位目标文件hello.o,最后运行链接器程序ld将hello.o和系统目标文件组合起来,创建了一个可执行目标文件hello。当shell接收到./hello的指令后开始调用fork函数创建进程,execve加载hello进入内存,由CPU控制程序逻辑流的运行,中断,上下文切换和异常的处理,最后结束进程并由父进程进行回收,hello走向“生命”的尽头

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

目  录

第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程序到二进制的可执行文件hello的过程。

在Unix系统上,从源文件到目标文件的转化是由编译器驱动程序完成的。

Linux> gcc -o hello hello.c

GCC编译器驱动程序读取源文件hello.c,并把它翻译成一个可执行目标文件hello。这个翻译过程可以分为四个阶段完成,如图所示。执行这四个阶段的程序(预处理器、编译器、汇编器、链接器)一起构成了编译系统。

通过编辑器编写hello.c文件,得到hello.c的源程序。

运行C预处理器(cpp)将hello.c进行预处理生成hello.i文件。

运行C编译器(ccl)将hello.i进行翻译生成汇编语言文件hello.s。

运行汇编器(as)将hello.s翻译成一个可重定位目标文件hello.o。

运行链接器ld将hello.o和系统目标文件组合起来,创建了一个可执行目标文件hello。

通过shell输入./hello,shell通过fork函数创建新的进程,然后调用了execve对虚拟内存进行映射,通过mmap为hello开辟一片空间。

中央处理器CPU从虚拟内存中的.text,.data截取代码和数据,调度器为进程规划时间片,在发生异常时触发异常处理子程序。

1.2 环境与工具

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

  1. 硬件环境:X64 CPU:2.38 GHz  RAM  16.0 GB (15.4 GB 可用)
  2. 软件环境:Windows10 64位 ; VirtualBox11;Ubuntu16 64位
  3. 工具:Visual studio 2019 64位;CodeBlocks 64位;gedit+gcc

1.3 中间结果

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

hello.c           源程序(文本文件)

hello.i           预处理之后的程序(文本文件)

hello.s           汇编语言程序(文本文件)

hello.o          可重定位目标程序(二进制文件)

hello            可执行目标程序(二进制文件)

hello1_asm.txt    hello.o的反汇编文件

hello2_asm.txt    hello的反汇编文件

hello1_elf.txt     hello.o的elf头信息

hello2_elf.txt     可执行文件hello的elf头信息

1.4 本章小结

本章总体介绍了hello程序“一生”的过程,以及进行实验时的软硬件环境及开发与调试工具等基本信息。

第2章 预处理

2.1 预处理的概念与作用

预处理(pre-treatment):在程序设计领域,一般是指程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。典型地,由预处理器(preprocessor)对程序源代码文本进行处理,得到的结果再由编译器核心进一步编译。这个过程并不对程序的源代码进行解析,但它把源代码分割或处理为特定的单位。

作用:预处理阶段读取C源程序,对其中的预处理指令(以#开头的指令)和特殊符号进行处理。或者说是扫描源代码,对其进行初步的转换,产生新的源代码提供给编译器。

预处理过程先于编译器对源代码进行处理,读入源代码,检查包含预处理指令的语句和宏定义,并对源代码进行转换。预处理过程还会删除程序中的注释和多余的空白字符。

2.2在Ubuntu下预处理的命令

以下格式自行编排,编辑时删除

应截图,展示预处理过程!

Linux中预处理hello.c文件的命令是:gcc -E -o hello.i hello.c

2.3 Hello的预处理结果解析

以下格式自行编排,编辑时删除

查看hello.i

结果分析:

main函数体以及全局变量sleepsecs的代码保持不变,在这之前的部分约有三千行,这是由于头文件stdio.h,unistd.h,stdlib.h依次被展开。如果代码中有#define命令还会对相应符号进行替换。

2.4 本章小结

本章介绍了预处理的相关概念和作用,进行实际操作查看了hello.i文件,是对源程序进行补充和替换。

第3章 编译

3.1 编译的概念与作用

编译:编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。编译的过程实质上是把预处理文件进行词法分析、语法分析、语义分析、优化,从C语言等高级语言转换为成机器更好理解的汇编语言程序,转换后的文件仍为ASCII文本文件。

作用:编译后生成的.s汇编语言程序文本文件比预处理文件更容易让机器理解、比.o可重定位目标文件更容易让程序员理解,是对于程序像机器指令的一步关键过程。

3.2 在Ubuntu下编译的命令

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

    

3.3 Hello的编译结果解析

       3.3.1数据

C语言源代码:

 

 查看hello.s文件

1)字符串:

这两个字符串常量分别由.LC0和.LC1指示,均存放在只读数据段.rodata中。

2)局部变量

main函数声明了一个局部变量i。

编译器进行编译的时候将局部变量i会放在堆栈中。如图所示,局部变量i放在栈上-4(%rbp)的位置。

3)main函数

参数 argc 作为用户传给main的参数。也是被放到了堆栈中。通过阅读汇编代码我们发现此函数是一个全局函数。

 4)立即数

5)数组char *argv[]

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

3.3.2全局函数

 由hello.c可知,hello.c声明了一个全局函数int main(int argc,char *argv[])。

3.3.3赋值

程序中的赋值操作主要有:i=0这条赋值操作在汇编代码主要使用mov指令来实现,而根据数据的类型又有好几种不一样的后缀

movb:一个字节

movw:两个字节

movl:四个字节

movq:八个字节

3.3.4算数操作

hello.c中的算数操作主要有i++,i是int类型,在汇编代码中用addl实现此操作。

3.3.5关系操作

首先给出汇编中,我们需要使用的操作指令

 cmp和test指令并不修改寄存器的值,而是设置条件码。

 argc!=4;是在一条件语句中的条件判断:argc!=4,进行编译时,这条指令被编译为:cmpl $4,-20(%rbp),同时这条cmpl的指令还有设置条件码的作用,当根据条件码来判断是否需要跳转到分支中。

i<8,在hello.c作为判断循环条件,在汇编代码被编译为:cmpl $7,-4(%rbp),计算 i-7然后设置 条件码,为下一步 jle 利用条件码进行跳转做准备。 

3.3.6控制转移指令

在本程序中,涉及的控制转移部分主要有两处第一处是判断argc是否等于4,在第24行cmpl比较了argc与4的大小之后设置条件码,然后25行再判断是否跳转到L2,这个判断是基于条件码ZF位,如果为0则跳转,不为0则继续往下执行。

第二处是判断变量i是否满足循环条件i<8,首先在L2中第29行将i赋值为0,跳转到L3,开始判定是否进入循环,第51行cmpl变量i与7,设置条件码,52行再根据此判断是否跳转到L4执行循环体,如果i<=7则执行,如果>7则继续执行下一行,而不进入循环。 

3.3.7函数操作

函数是过程的一种形式,提供了一种封装代码的方式。用一组指定的参数和一个可选的返回值便实现了一种功能。调用函数时有以下操作:(假设函数P调用函数Q)

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

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

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

在hello.c程序中,所涉及的函数主要为main 、printf、exit、atoi、sleep、getchar,一共6个。

main函数,main函数被存储在.text节中,有两个参数,分别为命令行传入的argc和argv[],开始被保存在寄存器%rdi和%rsi

 

printf函数, 在hello.c程序中有两处调用了printf函数。第一处调用由于只是输入一串字符串,所以被系统优化成了puts函数,此时参数存放在%edi中。之后通过call来调用puts。 

exit函数,调用exit函数退出。 

atoi函数, 将参数存储在%rdi函数中,作为atoi函数的参数调用。 

3.3.8类型转换

hello.c中涉及的类型转换是:atoi(argv[3]),将字符串类型转换为整数类型其他的类型转换还有int、float、double、short、char之间的转换。

3.4 本章小结

本章主要介绍了编译的概念及其作用,以及在linux下编译的指令,最后我们根据hello.c文件的编译文件hello.s文件中的汇编代码详细的解析了数据类型,各类操作是如何实现的。经过这一章,最初的hello.c已经被转换成了更底层的汇编程序。

第4章 汇编

4.1 汇编的概念与作用

汇编:汇编器as,将.s文件翻译成机器指令,也即.o文件,这一过程称为汇编,同时这个.o文件也是可重定位目标文件。

作用:将编译器产生的汇编语言进一步翻译为计算机可以理解的机器语言,生成.o文件。

4.2 在Ubuntu下汇编的命令

命令:as hello.s -o hello.o

4.3 可重定位目标elf格式

ELF Header:用命令:readelf -h hello.o

ELF Header:以 16B 的序列 Magic 开始,Magic 描述了生成该文件的系统 的字的大小和字节顺序,ELF 头剩下的部分包含帮助链接器语法分析和解 ## 标题释目标文件的信息,其中包括 ELF 头的大小、目标文件的类型、机器类型、 字节头部表(section header table)的文件偏移,以及节头部表中条目的大 小和数量等信息。

根据头文件的信息,可以知道该文件是可重定位目标文件,有14个节。

Section Headers:命令:readelf -S hello.o

 

Section Headers:节头部表,包含了文件中出现的各个节的语义,包括节 的类型、位置和大小等信息。 由于是可重定位目标文件,所以每个节都从0开始,用于重定位。在文件头中得到节头表的信息,然后再使用节头表中的字节偏移信息得到各节在文件中的起始位置,以及各节所占空间的大小,同时可以观察到,代码是可执行的,但是不能写;数据段和只读数据段都不可执行,而且只读数据段也不可写。

查看符号表.symtab :命令readelf -s hello.o

.symtab: 存放程序中定义和引用的函数和全局变量的信息。name是符号名称,对于可冲定位目标模块,value是符号相对于目标节的起始位置偏移,对于可执行目标文件,该值是一个绝对运行的地址。size是目标的大小,type要么是数据要么是函数。Bind字段表明符号是本地的还是全局的。

.rela.text:存放着代码的重定位条目。当链接器把这个目标文件和其他文件组合时,会依据重定向节的信息修改.text节中的相应位置信息。如下图中的重定位信息分别对应.L0、puts函数、exit函数、.L1、printf函数、atoi函数、sleep函数、getchar函数。

 

Offset:该值给出了需要重定位的代码在各自节中的偏移量

Info:包括两部分,前四个字节为symbol,表示重定位到的目标在.symtab中的偏移量,而后四个字节为type,表示重定位的类型

Addend:指定常量加数,一些类型的重定位需要它来作重定位加数,做偏移调整

Type:重定位到的目标类型

Name:重定位到的目标的名称

4.4 Hello.o的结果解析

objdump -d -r hello.o  分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。

反汇编结果:

 

hello.s: 

通过反汇编的代码和hello.s进行比较,发现汇编语言的指令基本相同,只是反汇编代码所显示的不仅仅是汇编代码,还有机器代码,机器语言程序的是二进制机器指令的集合,是纯粹的二进制数据表示的语言,是电脑可以真正识别的语言。机器指令由操作码和操作数构成,汇编语言是人们比较熟悉的词句直接表述CPU动作形成的语言,是最接近CPU运行原理的语言。每一条汇编语言操作码都可以用机器二进制数据来表示,进而可以将所有的汇编语言(操作码和操作数)和二进制机器语言建立一一映射的关系,因此可以将汇编语言转化为机器语言,通过对机器代码的分析可以看出一下不同的地方。

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

说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。

4.5 本章小结

本章对hello.s进行了汇编,生成了hello.o可重定位目标文件,并且分析了可重定位文件的ELF头、节头部表、符号表和可重定位节,比较了hello.s和hello.o反汇编代码的不同之处,分析了从汇编语言到机器语言的映射关系。

5章 链接

5.1 链接的概念与作用

链接:链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载到内存并执行。

作用:链接器使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,更便于我们维护管理,可以独立的修改和编译我们需要修改的小的模块。

5.2 在Ubuntu下链接的命令

使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件

链接命令: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.3 可执行目标文件hello的格式

使用命令readelf -a -W hello查看hello的ELF格式,各节的信息基本均在节头表中。包括名称(Name),类型(Type),起始地址(Address),偏移量(OFF),大小(Size)等信息。

ELF Header:hello的文件头和hello.o文件头的不同之处:类型为可执行目标文件,有27个节。

节头部表Section Headers:Section Headers 对 hello中所有的节信息进行了声明,其 中包括大小 Size 以及在程序中的偏移量 Offset,因此根据 Section Headers 中的信息我们就可以用 HexEdit 定位各个节所占的区间(起始位置,大小)。其中 Address 是程序被载入到虚拟地址的起始地址。

5.4 hello的虚拟地址空间 

使用edb查看hello: 

 观察Data Dump窗口,发现虚拟地址从0x401000开始到0x401ff0结束。

5.5 链接的重定位过程分析

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

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

使用命令objdump -d -r hello > helloasm得到反汇编文件。

 

分析hello.asm与helloasm的区别:

在hello1.asm中,节的个数更多,多了比如.init和.plt等等。在hello.asm中只有.text,而hello1.asm中却有很多节。

两文件内容比较:

在hello1.asm中不再是相对偏移地址而是可以由CPU直接寻址的绝对地址

同时函数调用,跳转指令也不再是相对偏移地址,而是虚拟内存地址。并且许多调用的函数都是外部链接的共享库函数。

对于hello的重定位分析。利用在。rela.data和.rela.text节中保存的重定位信息,来修改对各个符号的引用,即修改他们的地址。

5.6 hello的执行流程

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

开始执行:_start、_libc_start_main

执行main:_main、_printf、_exit、_sleep、_getchar

退出:exit

程序名 :

_start

_libc_start_main

main

_printf

_exit

_sleep

_getchar

5.7 Hello的动态链接分析

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

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

在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。GNU编译系统使用延迟绑定(lazybinding),将过程地址的绑定推迟到第一次调用该过程时。

查看dl_init函数调用前后.got.plt节的变化

根据hello ELF文件可知,GOT起始表位置为0x404000

在dl_init调用之前,对于每一条PIC函数调用,调用的目标地址都实际指向PLT中的代码逻辑,初始时每个GOT条目都指向对应的PLT条目的第二条指令。

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

  在之后的函数调用时,首先跳转到PLT执行.plt中逻辑,第一次访问时,GOT地址为下一条指令,将函数序号压栈,然后跳转到PLT[0],在PLT[0]中将重定位表地址压栈,然后访问动态链接器,在动态链接器中使用函数序号和重定位表确定函数运行时地址,重写GOT,再将控制传递给目标函数。之后如果对同样函数调用,第一次访问跳转直接跳转到目标函数。

5.8 本章小结

本章主要介绍了链接的概念及作用,以及生成链接的命令,对hello的elf格式文件进行了深入的分析,同时也分析了hello的虚拟地址空间,重定位过程,遍历了整个hello的执行过程,并且比较了hello.o的反汇编和hello的反汇编。对于链接有了更加深入的理解。

6章 hello进程管理

6.1 进程的概念与作用

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

作用:进程提供给应用程序关键抽象:一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器。一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用内存系统。

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

shell的定义:

shell是系统的用户界面,提供了用户与内核进行交互操作的一种接口。它接收用户输入的命令并把它送入内核去执行。

shell的作用:

实际上shell是一个命令解释器,它解释由用户输入的命令并且把它们送到

内核。不仅如此,shell有自己的编程语言用于对命令的编辑,它允许用户编写由shell命令组成的程序。shell编程语言具有普通编程语言的很多特点,比如它也有循环结构和分支控制结构等,用这种编程语言编写的shell程序与其他应用程序具有同样的效果。

shell的处理流程:

shell首先检查命令是否是内部命令,若不是再检查是否是一个应用程序(这里的应用程序可以是Linux本身的实用程序,如ls和rm,也可以是购买的商业程序,如xv,或者是自由软件,如emacs)。然后shell在搜索路径里寻找这些应用程序(搜索路径就是一个能找到可执行程序的目录列表)。如果键入的命令不是一个内部命令并且在路径里没有找到这个可执行文件,将会显示一条错误信息。如果能够成功找到命令,该内部命令或应用程序将被分解为系统调用并传给Linux内核。

6.3 Hello的fork进程创建过程

终端程序通过调用fork()函数创建一个子进程,子进程得到与父进程完全相同但是独立的一个副本,包括代码段、数据段、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,父进程和子进程最大的不同时他们的PID是不同的。父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的逻辑控制流的指令。在子进程执行期间,父进程默认选项是显示等待子进程的完成。

我们在shell上输入./hello,由于这不是一个内置的shell命令,所以shell会认为hello是一个可执行目标文件,通过调用某个驻留在存储器中被称为加载器的操作系统代码来运行它。

6.4 Hello的execve过程

调用fork函数之后,子进程将会调用execve函数,来运行hello程序,如果成功调用则不再返回,若未成功调用则返回-1。

完整的加载运行hello程序,首先加载器会删除当前子进程虚拟地址端。,然后创建一组新的代码、数据、堆端,并初始化为0。接着映射私有区域和共享区域,将新的代码和数据段初始化为可执行文件中的内容最后设置程序计数器,使其指向代码区的入口,下一次调度这个进程时,将直接从入口点开始执行。

6.5 Hello的进程执行

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

上下文切换:操作系统内核使用一种称为上下文切换的较高层形式的异常控制流来实现多任务。上下文切换机制是建立在较低层异常机制之上的。内核为每个进程维持-一个.上下文。上下文就是内核重新启动一个被抢占的进程所需的状态。它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页表、包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。

调度:在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行时,我们说内核调度了这个进程。

如果系统调用因为等待某个事件发生而阻塞,那么内核可以让当前进程休眠,切换到另一个进程。比如,如果一个read系统调用需要访问磁盘,内核可以选择执行上下文切换,运行另外一个进程,而不是等待数据从磁盘到达。另一个示例是sleep系统调用,它显式地请求让调用进程休眠。

hello程序与操作系统其他进程通过操作系统的调度,切换上下文,拥有各自的时间片从而实现并发运行。hello在调用sleep函数时做了上下文切换。

hello初始运行在用户模式中,直到它通过执行系统调用sleep陷入到内核。内核处理休眠请求主动释放当前进程(hello),同时计时器开始计时,内核进行如上图所示的上下文切换,将当前进程的控制权交给其他进程,当进程达到sleep_secs的时间时,给其他进程发送中断信号,触发中断异常处理子程序,将hello进程从等待队列中移出,重新加入到运行队列。

6.6 hello的异常与信号处理

异常和信号异常可以分为四类:中断、陷阱、故障、终止

 

hello程序出现的异常可能有:

中断:在hello程序执行的过程中可能会出现外部I/O设备引起的异常。

陷阱:陷阱是有意的异常,是执行一条指令的结果,hello执行sleep函数的时候会出现这个异常。

故障:在执行hello程序的时候,可能会发生缺页故障。

终止:终止时不可恢复的错误,在hello执行过程可能会出现DRAM或者SRAM位损坏的奇偶错误。

在发生异常时会发出信号,比如缺页故障会导致OS发生SIGSEGV信号给用户进程,而用户进程以段错误退出。常见信号种类如下表所示。

hello执行过程中

键盘随机按键:如果按键过程中没有回车键,会把输入屏幕的字符串缓存起来;如果按键过程中有回车键,则当程序运行完成后,缓存区中的换行符前的字符串会被shell当作指令执行。

按Ctrl-Z键

输入Ctrl-Z键会发送一个SIGTSTP信号给前台进程组的每一个进程,故hello进程停止。

 

运行ps命令:显示当前进程的状态 

运行jobs命令:用于显示Linux中的任务列表及任务状态,包括后台运行的任务。 

 运行fg命令:用于将后台作业(在后台运行的或者在后台挂起的作业)放到前台终端运行。由于后台作业只有hello,于是hello被转到前台运行,继续循环输出字符串。

 pstree命令:将所有行程以树状图显示

kill命令

kill -9 <进程号> :杀死对应进程

 

按Ctrl-C键:输入Ctrl+C,会让内核发送一个SIGINT信号给到前台进程组中的每个进程,结果是终止前台进程。 

6.7本章小结

本章主要介绍了进程的概念及其作用,对shell的功能和处理流程也进行了介绍,然后详细分析了hello程序从fork进程的创建,到execve函数执行,最后具体执行过程以及出现异常的处理。对于整个进程管理有了更加深入的理解。

7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:在有地址变换功能的计算机中,访内指令给出的地址(操作数)叫逻辑地址,也叫相对地址。分为两个部分,一个部分为段基址,另一个部分为段偏移量。

线性地址:逻辑地址经过段机制后转化为线性地址,为描述符:偏移量的组合形式。分页机制中线性地址作为输入。

虚拟地址:CPU启动保护模式后,程序运行在虚拟地址空间中。与物理地址相似,虚拟内存被组织为一个存放在磁盘上的N个连续的字节大小的单元组成的数组,其每个字节对应的地址成为虚拟地址。虚拟地址包括VPO(虚拟页面偏移量)、VPN(虚拟页号)、TLBI(TLB索引)、TLBT(TLB标记)。

物理地址:CPU通过地址总线的寻址,找到真实的物理内存对应地址。CPU对内存的访问是通过连接着CPU和北桥芯片的前端总线来完成的。在前端总线上传输的内存地址都是物理内存地址。

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

在 Intel 平台下,逻辑地址(logical address)是 selector:offset 这种形式,selector 是 CS 寄存器的值,offset 是 EIP 寄存器的值。如果用 selector 去 GDT( 全局描述符表 ) 里拿到 segment base address(段基址) 然后加上 offset(段内偏移),这就得到了 linear address。我们把这个过程称作段式内存管理。

一个逻辑地址由两部份组成,段标识符: 段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节,表示具体的是代码段寄存器还是栈段寄存器抑或是数据段寄存器。

全局的段描述符,放在“全局段描述符表(GDT)”中,一些局部的段描述符,放在“局部段描述符表(LDT)”中。

给定一个完整的逻辑地址段选择符+段内偏移地址,看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,就得到了其基地址。Base + offset = 线性地址。

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

Linux采用了分页的方式来记录对应关系。所谓的分页,就是以更大尺寸的单位页来管理内存。在Linux中,通常每页大小为4KB。CPU中的一个控制寄存器,页表基址寄存器(Page Table Base Register, PTBR)指向当前页表。n位的虚拟地址包含两个部分:一个p位的虚拟页面偏移(Virtual Page Offsetm, VPO)和一个(n-p)位的虚拟页号(Virtual Page Number, VPN)。MMU利用VPN来选择适当的PTE。例如,VPN 0选择PTE 0,VPN 1选择PTE 1,以此类推。将页表条目中物理页号(Physical Page Number, PPN) 和虚拟地址中的VPO串联起来,就得到相应的物理地址。注意,因为物理和虚拟页面都是P字节的,所以物理页面偏移( Physical Page Offset,PPO)和VPO是相同的。下图展示了从虚拟地址到物理地址的基于页表的翻译过程:

当页命中时,CPU硬件执行的步骤: 

第1步:处理器生成一个虚拟地址,并把它传送给MMU。

第2步: MMU生成PTE地址,并从高速缓存/主存请求得到它。

第3步:高速缓存/主存向MMU返回PTE。

第4步:MMU构造物理地址,并把它传送给高速缓存/主存。

第5步:高速缓存/主存返回所请求的数据字给处理器

用来压缩页表的常用方法为使用层次结构的页表:

以二级页表为例:

第一级页表: 每个 PTE 指向一个页表 (常驻内存)

第二级页表: 每个 PTE 指向一页

 k级页表层次结构的地址翻译:

虚拟地址被划分成为k个VPN和1个VPO。每个VPN i都是一个到第i级页表的索引,其中1≤i≤k。第j级页表中的每个PTE, 1≤j≤k-1,都指向第j+1级的某个页表的基址。第k级页表中的每个PTE包含某个物理页面的PPN,或者一个磁盘块的地址。为了构造物理地址,在能够确定PPN。之前,MMU必须访问k个PTE。对于只有一级的页表结构,PPO和VPO是相同的。访问k个PTE,第一眼看上去昂贵而不切实际。然而,这里TLB能够起作用,正是通过将不同层次,上页表的PTE缓存起来。实际上,带多级页表的地址翻译并不比单级页表慢很多。

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

为了减少页表过大导致的空间浪费,我们采用多级页表来压缩大小

 

在四级页表层次结构的地址翻译中,虚拟地址被划分为4个VPN和1个VPO。每个第i个VPN都是一个到第i级页表的索引,第j级页表中的每个PTE都指向第j+1级某个页表的基址,第四级页表中的每个PTE包含某个物理页面的PPN,或者一个磁盘块的地址。为了构造物理地址,在能够确定PPN之前,MMU必须访问四个PTE。 

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

对于一个虚拟地址请求,CPU首先将去TLB寻找,看是否已经在TLB中缓存。如果命中的话就直接MMU获取,没有命中的话就先在结合多级页表,得到物理地址PA,L1 Cache对PA进行分解,将其分解为标记(CT)、组索引(CI)、块偏移(CO),检测物理地址是否L1 cache命中,若命中,则直接将PA对应的数据内容取出返回给CPU,若不命中则在下一级中寻找,并重复L1 cache中的操作。

 

7.6 hello进程fork时的内存映射

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

7.7 hello进程execve时的内存映射

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

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

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

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

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

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

缺页故障:当指令引用一个相应的虚拟地址,而与改地址相应的物理页面不再内存中,会触发缺页故障。通过查询页表PTE可以知道虚拟页在磁盘的位置。缺页处理程序从指定的位置加载页面 到物理内存中,并更新PTE。然后控制返回给引起缺页故障的指令。当指令再次执行时,相应的物理页面已经驻留在内存中,因此指令可以没有故障的运行完成。

 

7.9动态存储分配管理

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

分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显示地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显示地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显示执行的。要么是内存分配器自身隐式执行的。分配器有两种基本风格。两种风格都要求应用显示地分配块。他们的不同之处在于由哪个实体来负责释放已分配的块。

显示分配器:要求应用显示的释放任何已分配的块。例如C标准库提供一个叫做malloc程序包的显示分配器。

隐式分配器:要求分配器检测一个已分配块何时不再被程序使用,那么就释放这个块。隐式分配器也叫垃圾收集器。

隐式空闲链表的堆块格式

 

隐式空闲链表的带边界标记的堆块格式:

使用边界标记的堆块的格式其中头部和脚部分别存放了当前内存块的大小与是否已分配的信息。通过这种结构,隐式动态内存分配器会对堆进行扫描,通过头部和脚部的结构实现查找。

使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。维护链表的顺序有:后进先出(LIFO),将新释放的块放置在链表的开始处,使用LIFO的顺序和首次适配的放置策略,分配器会最先检查最近使用过的块,在这种情况下,释放一个块可以在线性的时间内完成,如果使用了边界标记,那么合并也可以在常数时间内完成。按照地址顺序来维护链表,其中链表中的每个块的地址都小于它的后继的地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。平衡点在于,按照地址排序首次适配比LIFO排序的首次适配有着更高的内存利用率,接近最佳适配的利用率。

7.10本章小结

本章主要介绍了hello的存储器的地址空间,介绍了四种地址空间的差别和地址的相互转换。同时介绍了hello的四级页表的虚拟地址空间到物理地址的转换。阐述了三级cashe的物理内存访问、进程 fork 时的内存映射、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。

8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

所有的I/O设备都被模型化为文件(甚至是内核),而所有的输入和输出都被当作相应文件的读和写来完成

设备管理:unix io接口

这种将设备优雅的映射为文件的方式,允许Linux内核引出一个简单的,低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。

8.2 简述Unix IO接口及其函数

Unix I/O 接口:

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

(2)Shell 创建的每个进程都有三个打开的文件:标准输入,标准输出,标 准错误。 (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 转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在 进程中当前没有打开的最小描述符,flags 参数指明了进程打算如何访 问这个文件,mode 参数指定了新文件的访问权限位。

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

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

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

8.3 printf的实现分析

首先来看看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需要做的事情是:接受一个fmt的格式,然后将匹配到的参数按照fmt格式输出。printf用了两个外部函数,一个是vsprintf,还有一个是write。

vsprintf函数作用是接受确定输出格式的格式字符串fmt(输入)。用格式字符串对个数变化的参数进行格式化,产生格式化输出。

write函数将buf中的i个元素写到终端。

write:

     mov eax, _NR_write

     mov ebx, [esp + 4]

     mov ecx, [esp + 8]

     int INT_VECTOR_SYS_CALL

从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall。字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

    sys_call:

     ;ecx中是要打印出的元素个数

     ;ebx中的是要打印的buf字符数组中的第一个元素

     ;这个函数的功能就是不断的打印出字符,直到遇到:'\0'

     ;[gs:edi]对应的是0x80000h:0采用直接写显存的方法显示字符串

     xor si,si

     mov ah,0Fh

     mov al,[ebx+si]

     cmp al,'\0'

     je .end

     mov [gs:edi],ax

     inc si

    loop:

     sys_call

    .end:

     ret

8.4 getchar的实现分析

int getchar(void)

{

    static char buf[BUFSIZ];//缓冲区

    static char* bb=buf;//指向缓冲区的第一个位置的指针

    static int n=0;//静态变量记录个数

    if(n==0)

    {

        n=read(0,buf,BUFSIZ);

        bb=buf;//并且指向它

    }

    return(--n>=0)?(unsigned char)*bb++:EOF;

}

getchar有一个int型的返回值。当程序调用getchar时,程序就等着用户按键,用户输入的字符被存放在键盘缓冲区中直到用户按回车为止(回车字符也放在缓冲区中)。

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

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

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

8.5本章小结

本章介绍了Linux的IO设备管理方法,Unix IO接口及其函数,分析了printf函数和getchar函数的实现。

结论

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

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

总结hello的一生。

预处理:hello.c预处理到hello.i文本文件;

编译:hello.i编译到hello.s汇编文件;

汇编:hello.s汇编到二进制可重定位目标文件hello.o;

链接:hello.o链接生成可执行文件hello;

创建子进程:bash进程调用fork函数,生成子进程;

加载程序:execve函数加载运行当前进程的上下文中加载并运行新程序hello;

访问内存:hello的运行需要地址的概念,虚拟地址是计算机系统最伟大的抽象;

交互:hello的输入输出与外界交互,与linux I/O息息相关;

终止:hello最终被shell父进程回收,内核会收回为其创建的所有信息。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值