计算机系统大作业:程序人生-Hello‘s P2P

计算机系统

大作业

题 目 程序人生-Hello's P2P
专 业 计算机科学与技术
学   号 1180300302
班   级 1803003
学 生 地里沙·迪力木拉提
指 导 教 师 史先俊

计算机科学与技术学院

2019年12月
摘 要
本文通过对hello程序P2P和020的整体介绍,阐述了hello经过多种处理后成为Process,并且hello进程创建直到回收的全部过程。以这些过程的分析为例,我们更好地说明了计算机的底层实现,并且更深地阐明了整个程序的生命周期。
关键词: 编译、存储层次结构、链接、虚拟内存、操作系统、异常、信号;

目 录

第1章 概述- 4 -
1.1 Hello简介- 4 -
1.2 环境与工具- 4 -
1.3 中间结果- 4 -
1.4 本章小结 -
第2章 预处理 -
2.1 预处理的概念与作用 -
2.2在Ubuntu下预处理的命令 -
2.3 Hello的预处理结果解析 -
2.4 本章小结 -
第3章 编译 -
3.1 编译的概念与作用 -
3.2 在Ubuntu下编译的命令 -
3.3 Hello的编译结果解析 -
3.4 本章小结 -
第4章 汇编 -
4.1 汇编的概念与作用 -
4.2 在Ubuntu下汇编的命令 -
4.3 可重定位目标elf格式 -
4.4 Hello.o的结果解析 -
4.5 本章小结7 -
第5章 链接 -
5.1 链接的概念与作用8 -
5.2 在Ubuntu下链接的命令8 -
5.3 可执行目标文件hello的格式8 -
5.4 hello的虚拟地址空间 -
5.5 链接的重定位过程分析 -
5.6 hello的执行流程 -
5.7 Hello的动态链接分析 -
5.8 本章小结 -
第6章 hello进程管理 -
6.1 进程的概念与作用 -
6.2 简述壳Shell-bash的作用与处理流程-
6.3 Hello的fork进程创建过程 -
6.4 Hello的execve过程 -
6.5 Hello的进程执行 -
6.6 hello的异常与信号处理 -
6.7本章小结 -
第7章 hello的存储管理 -
7.1 hello的存储器地址空间 -
7.2 Intel逻辑地址到线性地址的变换-段式管理 -
7.3 Hello的线性地址到物理地址的变换-页式管理 -
7.4 TLB与四级页表支持下的VA到PA的变换 -
7.5 三级Cache支持下的物理内存访问 -
7.6 hello进程fork时的内存映射 -
7.7 hello进程execve时的内存映射 -
7.8 缺页故障与缺页中断处理 -
7.9动态存储分配管理 -
7.10本章小结 -
第8章 hello的IO管理 -
8.1 Linux的IO设备管理方法 -
8.2 简述Unix IO接口及其函数 -
8.3 printf的实现分析1 -
8.4 getchar的实现分析3 -
8.5本章小结 -
结论4 -
附件 -
参考文献 -

第1章 概述

1.1 Hello简介

P2P:From Program to Process
在编译器的处理下,hello.c文件经历预处理、编译、汇编、链接,四个步骤,变为可执行文件(program)然后由shell为其创建一个新的进程(process)并运行它。在编译器下的处理过程如图1所示。计算机系统大作业:程序人生-Hello‘s P2P_第1张图片
020:From Zero to Zero
shell通过execve加载并执行hello,映射虚拟内存,进入程序入口后程序开始载入物理内存,然后执行第一条指令,CPU为运行的hello分配时间片执行逻辑控制流。当程序运行结束后,shell父进程负责回收hello进程,内核删除相关数据结构。

1.2 环境与工具

1.2.1 硬件环境:
i7-8750H CPU;2.21GHz;8G RAM;256GHD Disk;
1.2.2 软件环境:
Windows10 64位;Vmware 10;Ubuntu 18.04 LTS 64位;
1.2.3 开发工具:
Visual Studio 2019 64位以上;CodeBlocks 64位;vi/vim/gedit+gcc;

1.3 中间结果

hello.i 预处理文本文件
hello.s 编译后的汇编文件
hello.o 汇编后的可重定位文件
hello 链接后的可执行文件
helloobjdump.txt hello的反汇编代码

1.4 本章小结

hello.c程序从编写、预处理、编译、汇编、链接再到执行,体现了计算机系统系统各部分的具体功能,以及它们之间的的协同合作。

第2章 预处理

2.1 预处理的概念与作用

概念 :预处理指在代码翻译的过程中生成二进制代码之前的过程。

预处理会展开以#起始的行,试图解释为预处理指令,其中ISO C/C++要求支持的包括#if/#ifdef/#ifndef/#else/#elif/#endif、#define、#include、#line、#error、#pragma以及单独的# [1]。
作用 :预处理指令一般被用来使源代码在不同的执行环境中被方便的修改或者编译。[1]可以让目标程序变小,提高运行速度。

2.2在Ubuntu下预处理的命令

图 2.1-Ubuntu下预处理命令
计算机系统大作业:程序人生-Hello‘s P2P_第2张图片

2.3 Hello的预处理结果解析

经过预处理之后,hello.c文件转化为hello.i文件。原文件中的宏进行了宏展开,头文件中的内容被包含进该文件中,形成一个完备的程序文本。hello.c程序被拓展成3113行,但依旧是c语言文本文件,从3100行开始就是源代码。
计算机系统大作业:程序人生-Hello‘s P2P_第3张图片

2.4 本章小结

本阶段完成了对hello.c的预处理工作。使用Ubuntu下的预处理指令将.c文件转换为.i文件。完成该阶段转换后,下一阶段就要进行汇编处理。

第3章 编译

3.1 编译的概念与作用

概念 :1)利用编译程序从源语言编写的源程序产生目标程序的过程。 2)用编译程序产生目标程序的动作。[2]

作用 :编译就是把高级语言变成计算机可以识别的2进制语言,计算机只认识1和0,编译程序把人们熟悉的语言换成2进制的。

编译程序把一个源程序翻译成目标程序的工作过程分为五个阶段:词法分析;语法分析;语义检查和中间代码生成;代码优化;目标代码生成。主要是进行词法分析和语法分析,又称为源程序分析,分析过程中发现有语法错误,给出提示信息。[2]

3.2 在Ubuntu下编译的命令

图 3.1-Ubuntu下的编译命令

计算机系统大作业:程序人生-Hello‘s P2P_第4张图片

3.3 Hello的编译结果解析

3.3.1数据:两个字符串

"用法: Hello 学号 姓名 秒数!\n",printf的输出格式化参数,其中编码为utf8

"Hello %s %s\n",printf传入的输出格式参数
计算机系统大作业:程序人生-Hello‘s P2P_第5张图片

3.3.2赋值

程序中涉及的赋值是在for循环中对i赋值为0;如下图6.
图 3.3.2

3.3.3关系操作:两处判断

第一处判断argc是否等于4

图 3.3.3.1
图 3.3.3.2- -20(%rbp)对应argc

第二处判断i是否小于8

图 3.3.3.3

图 3.3.3.4- -4(%rbp)对应i

3.3.4算术操作:对i进行++
图3.3.4.1
图3.3.4.1
图3.3.4.2- -4(%rbp)对应i,对它进行加1操作

3.3.5数组操作

程序中的数组操作主要是对于输入参数的输出
图 3.3.5.1

计算机系统大作业:程序人生-Hello‘s P2P_第6张图片

3.3.6控制转移:程序包含两处控制转移

第一处是判断输入参数个数是否等于4,如果等于4才进行跳转
图 3.3.6.1

图 3.3.6.2

第二处是判断循环体是否结束,若循环体未结束(即i<=7),跳到.L4,继续输出
图 3.3.6.3

3.3.7类型转换

int atoi(char *argv[]),atoi函数把字符串转换成整型数。[3]
图 3.3.7

3.3.8函数操作:程序包括六个函数

1)main函数,参数为int argc , char * argv [],其中argc为输入参数的个数,argv为字符串数组,存放输入的参数,在这里就是输入的学号、姓名、秒数。call指令将下一条指令压栈,跳转main,并将参数argc和argv,分别使用%rdi和%rsi存储,使用%rbp记录栈帧的底,分配和释放内存

2)printf函数,传递argv的数据,%rdi设置为第一个字符串的首地址,第二次是第二个字符串首地址,%rsi为argv[1],%rdx为argv[2]

3)exit函数参数值为1,exit(1)表示异常退出,在退出前可以给出一些提示信息,或在调试程序中察看出错原因

4)sleep函数中,call指令将下一条指令压栈,跳转到atoi函数;

5)atoi函数,参数为argv[3]。把字符串转换成整型数。

6)getchar函数,读取标准输入流的一个字符

3.4 本章小结

本阶段完成了对hello.i的编译工作。使用Ubuntu下的编译指令可以将其转换为.s汇编语言文件。此外,本章通过与源文件C程序代码进行比较,完成了对汇编代码的解析工作。完成该阶段转换后,可以进行下一阶段的汇编处理。

第4章 汇编

4.1 汇编的概念与作用

汇编程序是把汇编语言书写的程序翻译成与之等价的机器语言程序的翻译程序。汇编程序输入的是用汇编语言书写的源程序,输出的是用机器语言表示的目标程序。[4]汇编语言的指令与机器语言的指令大体上保持一一对应的关系,汇编算法采用的基本策略是简单的。通常采用两遍扫描源程序的算法。第一遍扫描源程序根据符号的定义和使用,收集符号的有关信息到符号表中;第二遍利用第一遍收集的符号信息,将源程序中的符号化指令逐条翻译为相应的机器指令。

4.2 在Ubuntu下汇编的命令

图 4.1-Ubuntu下汇编命令

4.3 可重定位目标elf格式

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

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

  1. 1)ELF头。它以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息:包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。
    计算机系统大作业:程序人生-Hello‘s P2P_第7张图片

  2. 2)节头部表。它描述了不同节的位置和大小,目标文件中的每一个节都有一个固定大小的条目。
    计算机系统大作业:程序人生-Hello‘s P2P_第8张图片

  3. 3)可重定位项目

    其中.rela.text节是一个.text节中位置的列表。当连接器把扎个目标文件和其他文件组合时,需要修改这些位置。
    
    .real.eh\_frame节包含了对en\_frame节的重定位信息。
    
    其中,Offset是需要被修改的引用的字节偏移(在代码节或数据节的偏移),Info指示了重定位目标在.symtab中的偏移量和重定位类型,Type表示不同的重定位类型,例如图中的R\_X86\_64\_PC32就表示重定位一个使用32位PC相对地址的引用。.Sym.Name表示被修改引用应该指向的符号,Append用于一些类型的重定位要使用它对被修改引用的值做偏移调整。
    

可见下图中,在链接时需要对.rodata中的两个字符串常量(用于printf函数中),以及函数puts,exit,printf,sleep,atoi,getchar进行重定位。
计算机系统大作业:程序人生-Hello‘s P2P_第9张图片

  1. 4)符号表。它存放了程序中定义和引用的函数的信息(不包含局部变量的条目)。
    计算机系统大作业:程序人生-Hello‘s P2P_第10张图片

4.4 Hello.o的结果解析

反汇编命令:objdump -d -r hello.o

机器语言程序的是二进制的机器指令序列集合,是纯粹的二进制数据表示的语言,是电脑可以真正识别的语言。机器指令由操作码和操作数组成。汇编语言是以人们比较熟悉的词句直接表述CPU动作形成的语言,是最接近CPU运行原理的较为通俗的比较容易理解的语言。在不同的设备中,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令。机器语言与汇编语言具有一一对应的映射关系,一条机器语言程序对应一条汇编语言语句,但不同平台之间不可直接移植。

对比hello.s文件和反汇编代码,主要有以下的差别:

1)操作数:hello.s中的操作数时十进制,hello.o反汇编代码中的操作数是十六进制。

2)分支转移:跳转语句之后,hello.s中是.L2和.L3等段名称,而反汇编代码中跳转指令之后是相对偏移的地址。

3)函数调用:hello.s中,call指令之后直接是函数名称,而反汇编代码中call指令之后是函数的相对偏移地址。因为函数只有在链接之后才能确定运行执行的地址,因此在.rela.text节中为其添加了重定位条目。

  1. 对栈的使用:反汇编代码的栈空间利用率较高,而.s文件中对栈的使用有一定的浪费。
    计算机系统大作业:程序人生-Hello‘s P2P_第11张图片

计算机系统大作业:程序人生-Hello‘s P2P_第12张图片

4.5 本章小结

通过汇编操作,汇编语言转化为机器语言,hello.o可重定位目标文件为后面的链接做了准备。通过对比hello.s和反汇编代码的区别,更深刻地理解了汇编语言到机器语言实现地转变,和这过程中为链接做出的准备(设置重定位条目等)。

第5章 链接

5.1 链接的概念与作用

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

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

5.2 在Ubuntu下链接的命令

ld链接命令:

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
计算机系统大作业:程序人生-Hello‘s P2P_第13张图片

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

使用readelf –h hello.out查看文件头信息。根据文件头的信息,可以知道该文件是可执行目标文件,有25个节,如图5.3.1。
计算机系统大作业:程序人生-Hello‘s P2P_第14张图片

使用readelf –S hello.out查看节头表。从而得知各节的大小,以及他们可以进行的操作,如图5.3.2。
计算机系统大作业:程序人生-Hello‘s P2P_第15张图片计算机系统大作业:程序人生-Hello‘s P2P_第16张图片

使用readelf –s hello.out可以查看符号表的信息,如图5.3.3。
计算机系统大作业:程序人生-Hello‘s P2P_第17张图片

根据图5.3.2可以得到各段的基本信息。由于是可执行目标文件,所以每个段的起始地址都不相同,它们的起始地址分别对应着装载到虚拟内存中的虚拟地址。这样可以直接从文件起始处得到各段的起始位置,以及各段所占空间的大小。同时可以观察到,代码段是可执行的,但是不能写;数据段和只读数据段都不可执行,而且只读数据段也不可写。

5.4 hello的虚拟地址空间

通过readelf查看hello的Program Headers,可发现其中列出的虚拟地址(VirtAddr)在edb的Data Dump中都能找到对应的位置。
计算机系统大作业:程序人生-Hello‘s P2P_第18张图片
图 5.4.1-程序表头
计算机系统大作业:程序人生-Hello‘s P2P_第19张图片
图 5.4.2-edb加载hello并查看Data Dump

还可发现Data Dump是从地址0x400000开始的,并且该处有ELF的标识,可以判断从可执行文件时加载的信息(只读代码段,读/写段)是从地址0x400000处开始的。

根据5.3节的信息,可以找到各节的二进制信息。代码段的信息如下所示。代码段开始于0x400500处。
计算机系统大作业:程序人生-Hello‘s P2P_第20张图片

5.5 链接的重定位过程分析

反汇编命令:objdump -d -r hello(>helloobjdump.txt)
计算机系统大作业:程序人生-Hello‘s P2P_第21张图片

hello与hello.o主要有以下的不同:

1)链接增加新的函数:在hello中链接加入了在hello.c中用到的函数,如exit、printf、sleep、atoi、getchar等函数。

2)增加的节:hello中增加了.init和.plt节,和一些节中定义的函数。

3)函数调用:hello中无hello.o中的重定位条目,并且跳转和函数调用的地址在hello中都变成了虚拟内存地址。对于hello.o的反汇编代码,函数只有在链接之后才能确定运行执行的地址,因此在.rela.text节中为其添加了重定位条目。

4)地址访问:hello.o中的相对偏移地址变成了hello中的虚拟内存地址。而hello.o文件中对于.rodata的访问,是$0x0,是因为它的地址也是在运行时确定的,因此访问也需要重定位,在汇编成机器语言时,将操作数全部置为0,并且添加重定位条目。

根据hello和hello.o的不同,分析出链接的过程为:链接就是链接器ld将各个目标文件组装在一起,就是把.o文件中的各个函数段按照一定规则累积在一起,比如规则:解决符号依赖,库依赖关系,并生成可执行文件。

重定位:编译器和汇编器生成从0开始的代码和数据节。链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使得它们指向这个内存位置。链接器使用汇编器产生的重定位条目的详细指令,不加甄别地执行这样的重定位。

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

5.6 hello的执行流程

Hello调用与跳转的各子程序或程序地址如下:

0x4004c0 _init

0x4004e0 .plt

0x4004f0 puts@plt

0x400500 printf@plt

0x400510 getcgar@plt

0x400520 atoi@plt

0x400530 exit@plt

0x400540 sleep@plt

0x400550 _start

0x400580 _dl_relocate_static_pie

0x400582 main

0x400610 __libc_csu_init

0x400680 __libc_csu_fini

0x400684 _fini

5.7 Hello的动态链接分析

当程序调用一个由共享库定义的函数时,编译器无法预测这个函数运行时的地址,因为定义它的共享模块在运行时可以加载到任何位置。这时,编译系统提供了延迟绑定的方法,将过程地址的绑定推迟到第一次调用该过程时。他通过GOT和过程链接表PLT的协作来解析函数的地址。在加载时,动态链接器会重定位GOT中的每个条目,使它包含正确的绝对地址,而PLT中的每个函数负责调用不同函数。那么,通过观察edb,便可发现dl_init后.got.plt节发生的变化。

通过readelf可以发现.got.plt节在地址为0x601000的地方开始。而它后面的.data节从地址0x601048开始。那么中间部分便是.got.plt的内容。
计算机系统大作业:程序人生-Hello‘s P2P_第22张图片
用edb观察.got.plt,发现在dl_init前后.got.plt的第8到15个字节发生了变化。

在这里,它们对应GOT[1]和GOT[2]的位置。其中,这时GOT[1]包含动态链接器在解析函数地址时使用的信息,而GOT[2]是动态链接器ld-linux.so模块中的入口点。加载时,动态链接器重定位GOT中的这些条目,使它们包含正确的绝对地址。

计算机系统大作业:程序人生-Hello‘s P2P_第23张图片

5.8 本章小结

在链接过程中,各种代码和数据片段收集并组合为一个单一文件。利用链接器,分离编译称为可能,我们不用将应用程序组织为巨大的源文件,只是把它们分解为更小更模管理的模块,并在应用时将它们链接就可以完成一个完整的任务。

经过链接,hello.o与其他一些所需部分结合起来,生成可执行目标文件,这时,hello便可以开始在shell中运行了,正式开始它的程序人生。

第6章 hello进程管理

6.1 进程的概念与作用

概念 :进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。[5]

作用 :在现代系统上运行一个程序时,我们会得到一个假象,好像我们的程序是系统中唯一运行的程序一样。我们的程序好像独占处理器和内存。处理器好像无间断地一条接一条执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。这些假象是通过进程的概念提供的。进程提供给应用程序的关键抽象:1)一个独立的逻辑控制流,提供一个程序独占处理器的假象;2)一个私有的地址空间,提供一个程序独占地使用内存系统的假象。[6]

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

作用** :**实际上Shell是一个命令解释器,它解释由用户输入的命令并且把它们送到内核。不仅如此,Shell有自己的编程语言用于对命令的编辑,它允许用户编写由shell命令组成的程序。Shell编程语言具有普通编程语言的很多特点,比如它也有循环结构和分支控制结构等,用这种编程语言编写的Shell程序与其他应用程序具有同样的效果.[7]

处理流程** :**shell打印一个命令行提示符,等待用户在stdin上输入命令行,然后对命令行求值,即解析以空格分隔的命令行参数,第一个参数被假设为要么是一个内置的shell命令名,马上就会解释这个命令并执行相应操作;要么是一个可执行目标文件,会通过fork创建一个新的子进程,并在新的子进程的上下文中通过execve加载并运行这个文件。如果用户要求在后台运行该程序,那么shell返回到循环的顶部,等待下一个命令行。否则,shell使用waitpid函数等待作业终止并回收。当作业终止时,shell开始下一轮的迭代。
计算机系统大作业:程序人生-Hello‘s P2P_第24张图片

6.3 Hello的fork进程创建过程

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

6.4 Hello的execve过程

exceve函数在当前进程的上下文中加载并运行一个新程序。exceve函数加载并运行可执行目标文件,并带参数列表和环境变量列表。只有当出现错误时,exceve才会返回到调用程序,否则,exceve调用依一次且从不返回。在exceve加载了可执行目标文件后,他调用启动代码,启动代码设置栈,将可执行目标文件中的代码和数据从磁盘复制到内存中,然后通过跳转到程序的第一条指令或入口点来运行该程序,由此将控制传递给新程序的主函数。

6.5 Hello的进程执行

系统中通常有许多程序在运行,那么进程会为每个程序提供一个好像它在独占地使用处理器的假象。这时依赖于进程提供的独立的逻辑控制流(由上下文切换机制提供)。如一个系统运行着多个进程,那么处理器的一个物理控制流就被分成了多个逻辑控制流,每个进程1个。这些逻辑流的执行是交错的,它们轮流使用处理器,会存在并发执行的现象。其中,一个进程执行它的控制流的一部分的每一时间段叫做时间片。这样的机制使进程在执行时仿佛独占了处理器。

处理器用某个控制寄存器中的一个模式位来限制一个应用可以执行的指令以及它可以访问的地址空间范围。没有设置模式位时,进程运行在用户模式中,它必须通过系统调用接口才可间接访问内核代码和数据;而设置模式位时,它运行在内核模式中,可以执行指令集中的任何指令,访问系统内存的任何位置。异常发生时,控制传递到异常处理程序,由用户模式转变到内核模式,返回至应用程序代码时,又从内核模式转变到用户模式。

操作系统内核使用上下文切换来实现多任务。内核为每个进程维持一个上下文,它是内核重启被抢占的进程所需的状态,包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构的值。

进程执行到某些时刻,内核可决定抢占该进程,并重新开启一个先前被抢占了的进程,这种决策称为调度。内核调度一个新的进程运行后,通过上下文切换机制来转移控制到新的进程:1)保存当前进程上下文;2)恢复某个先前被抢占的进程被保存的上下文3)将控制转移给这个新恢复的进程。当内核代表用户执行系统调用时,可能会发生上下文切换,这时就存在着用户态与核心态的转换。如图所示:

图 6.5-进程的上下文切换[6]

6.6 hello的异常与信号处理

hello执行过程中会出现的异常:

1)中断:信号SIGTSTP,默认行为是 停止直到下一个SIGCONT

2)终止:信号SIGINT,默认行为是 终止

下面演示程序运行时各命令情况:

  1. 1)运行程序(正常)

在终端运行程序,打印八次Hello 姓名 学号后,输入回车,程序执行完成,进程被回收。
计算机系统大作业:程序人生-Hello‘s P2P_第26张图片

  1. 2)运行时不停乱按(包括回车)

发现它会把乱按的字符打印出来,按回车它会换一行,但是这些都不影响程序的正常执行,因为当程序执行时他不会受到外部输入的影响,它会阻塞这些操作产生的信号,而因为之前将大量字符(包括回车)输入到了屏幕上,所以最后不用自己再输入字符来结束程序,而是直接读取之前的输入。
计算机系统大作业:程序人生-Hello‘s P2P_第27张图片

  1. 3)运行程序时按Ctrl-Z

程序运行时按Ctrl-Z,这时,产生中断异常,它的父进程会接收到信号SIGSTP并运行信号处理程序,然后便发现程序在这时被挂起了,并打印了相关挂起信息。
计算机系统大作业:程序人生-Hello‘s P2P_第28张图片

Ctrl-Z后运行ps,打印出了各进程的pid,可以看到之前挂起的进程hello。
计算机系统大作业:程序人生-Hello‘s P2P_第29张图片

Ctrl-Z后运行jobs,打印出了被挂起进程组的jid,可以看到之前被挂起的hello,以被挂起的标识Stopped。
图 6.6.5-Ctrl-Z后运行jobs

Ctrl-Z后运行pstree,可看到它打印出的信息。
计算机系统大作业:程序人生-Hello‘s P2P_第30张图片
计算机系统大作业:程序人生-Hello‘s P2P_第31张图片计算机系统大作业:程序人生-Hello‘s P2P_第32张图片计算机系统大作业:程序人生-Hello‘s P2P_第33张图片

因为之前运行jobs是得知hello的jid为1,那么运行fg 1可以把之前挂起在后台的hello重新调到前台来执行,打印出剩余部分,然后输入hello回车,程序运行结束,进程被回收。
计算机系统大作业:程序人生-Hello‘s P2P_第34张图片

重新开始hello,按Ctrl-Z后通过ps得知hello的进程号为3701,那么便可通过kill -9 8675发送信号SIGKILL给进程8675,它会导致该进程被杀死。然后再运行ps,可发现已被杀死的进程hello。
计算机系统大作业:程序人生-Hello‘s P2P_第35张图片

  1. 4)运行程序按Ctrl-C

运行hello时按Ctrl-C,会导致一个中断异常,从而内核产生信号SIGINT,父进程受到它后,向子进程发生SIGKILL来强制终止子进程hello并回收它。这时在运行ps,可以发现并没有进程hello,可以说明他已经被终止并回收了。
计算机系统大作业:程序人生-Hello‘s P2P_第36张图片

6.7本章小结

hello开始真正在系统上运行时,离不开shell给它提供的平台,也离不开进程机制的支持和各种信号的通知。从创建进程,到在进程中加载程序,信号以及上下文切换使其可以自如的运行在计算机中,就好像独占了整个CPU。而当hello的进程生命结束,同样需要各种信号与系统的配合来对它进行终止,回收。程序的高效运行离不开异常、信号、进程等概念,正是这些机制支持hello能够顺利地在计算机上运行。

第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址 :包含在机器语言中用来指定一个操作数或一条指令的地址。每一个逻辑地址都由一个段和偏移量组成,偏移量指明了从段开始的地方到实际地址之间的距离。就是hello.o里相对偏移地址。

线性地址 :逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。是hello中的虚拟内存地址。

虚拟地址 :一个带虚拟内存的系统中,CPU从一个有N=2^n个地址空间中生成虚拟地址。虚拟地址其实就是线性地址。

物理地址 :用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。地址翻译会将hello的一个虚拟地址转化为物理地址。

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

一个逻辑地址由两部分组成,段标识符和段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节。可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。一些全局的段描述符,就放在"全局段描述符表(GDT)"中,一些局部的,例如每个进程自己的,就放在所谓的"局部段描述符表(LDT)"中。[8]

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

CPU的页式内存管理单元,负责把一个线性地址,最终翻译为一个物理地址。从管理和效率的角度出发,线性地址被分为以固定长度为单位的组,称为页(page),例如一个32位的机器,线性地址最大可为4G,可以用4KB为一个页来划分,这页,整个线性地址就被划分为一个tatol_page[2^20]的大数组,共有2的20个次方个页。这个大数组我们称之为页目录。目录中的每一个目录项,就是一个地址一一对应的页的地址。另一类"页",我们称之为物理页,或者是页框、页桢的。是分页单元把所有的物理内存也划分为固定长度的管理单位,它的长度一般与内存页是一一对应的。[8]

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

计算机系统大作业:程序人生-Hello‘s P2P_第37张图片

如果TLB中没有命中,MMU向页表中查询,CR3确定第一级页表的起始地址,VPN1(9bit)确定在第一级页表中的偏移量,查询出PTE,确定第二级页表的起始地址,VPN2(9bit)确定在第一级页表中的偏移量,查询出PTE,确定第三级页表的起始地址,VPN3(9bit)确定在第一级页表中的偏移量,查询出PTE,确定第四级页表的起始地址,VPN4(9bit)确定在第一级页表中的偏移量,查询出PTE,如果在物理内存中且权限符合,确定第二级页表的起始地址,以此类推,最终在第四级页表中查询到PPN,与VPO(12bit)组合成物理地址

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

在上一步中我们已经获得了物理地址,使用CI进行组索引,每组8路,对8路的块分别匹配CT,如果匹配成功且块的valid标志位为1,则命中,根据数据偏移量CO取出数据返回。

如果没有匹配成功或者匹配成功但是标志位是1,则不命中(miss),向下一级缓存中查询数据。查询到数据之后,一种简单的放置策略如下:如果映射到的组内有空闲块,则直接放置,否则组内都是有效块,产生冲突(evict),则采用最近最少使用策略进行替换。

7.6 hello进程fork时的内存映射

当fork函数被系统调用时,内核会为hello创建子进程,同时会创建各种数据结构并分配给hello唯一的PID。为了给hello创建虚拟内存,内核创建了当前进程的mm_struct、vm_area_struct和页表的原样副本,并将两个进程中的每个页面都标记为只读。将两个进程中的每个区域结构都标记为写时复制。在新进程中返回时,新进程拥有与调用fork进程相同的虚拟内存,随后的写操作通过写时复制机制创建新页面。

7.7 hello进程execve时的内存映射计算机系统大作业:程序人生-Hello‘s P2P_第38张图片

execve函数在当前进程中加载并运行新程序的步骤:[9]
1)删除已存在的用户区域
2)创建新的区域结构
(1)私有的、写时复制
(2)代码和初始化数据映射到.text和.data区(目标文件提供)
(3).bss和栈堆映射到匿名文件 ,栈堆的初始长度0
3)共享对象由动态链接映射到本进程共享区域
4)设置pc,指向代码区域入口点
(1)linux根据需要换入代码和数据页面

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

计算机系统大作业:程序人生-Hello‘s P2P_第39张图片
缺页中断处理 :缺页处理程序是系统内核中的代码,选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令再次发送VA到MMU,MMU就能正常翻译VA.

7.9动态存储分配管理

在程序运行时程序员使用动态内存分配器(如malloc)获得虚拟内存。动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护,每个块要么是已分配的,要么是空闲的。分配器的类型包括显式分配器和隐式分配器。前者要求应用显式地释放任何已分配的块,后者在检测到已分配块不再被程序所使用时,就释放这个块。

动态内存管理的策略包括首次适配、下一次适配和最佳适配。首次适配会从头开始搜索空闲链表,选择第一个合适的空闲块。搜索时间与总块数(包括已分配和空闲块)成线性关系。会在靠近链表起始处留下小空闲块的"碎片"。下一次适配和首次适配相似,只是从链表中上一次查询结束的地方开始。比首次适应更快,避免重复扫描那些无用块。最佳适配会查询链表,选择一个最好的空闲块,满足适配,且剩余最少空闲空间。它可以保证碎片最小,提高内存利用率。

7.10本章小结

本章主要介绍了hello的存储器地址空间、intel的段式管理、hello的页式管理,以intel Core7(i7)在指定环境下介绍了虚拟地址到物理地址的变换、物理内存访问,还介绍了hello进程fork时的内存映射、execve时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件
设备管理:unix io接口
一个linux文件就是一个m个字节的序列:B0 , B1 , … , Bk , … , Bm-1
所有的I/ O 设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux 内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。

8.2 简述Unix IO接口及其函数

Unix IO接口
打开文件,内核返回一个非负整数的文件描述符,通过对此文件描述符对文件进行所有操作。
Linux shell创建的每个进程开始时都有三个打开的文件:标准输入(文件描述符0)、标准输出(描述符为1),标准出错(描述符为2)。头文件<unistd.h>定义了常量STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO,他们可用来代替显式的描述符值。
改变当前的文件位置,文件开始位置为文件偏移量,应用程序通过seek操作,可设置文件的当前位置为k。
读写文件,读操作:从文件复制n个字节到内存,从当前文件位置k开始,然后将k增加到k+n;写操作:从内存复制n个字节到文件,当前文件位置为k,然后更新k
关闭文件。当应用完成对文件的访问后,通知内核关闭这个文件。内核会释放文件打开时创建的数据结构,将描述符恢复到描述符池中
Unix IO函数
1)open()函数
功能描述:用于打开或创建文件,在打开或创建文件时可以指定文件的属性及用户的权限等各种参数。
函数原型:int open(const char *pathname,int flags,int perms)
参数:pathname:被打开的文件名(可包括路径名如"dev/ttyS0")flags:文件打开方式,
返回值:成功:返回文件描述符;失败:返回-1
2)close()函数
功能描述:用于关闭一个被打开的的文件
所需头文件: #include <unistd.h>
函数原型:int close(int fd)
参数:fd文件描述符
函数返回值:0成功,-1出错
3)read()函数
功能描述: 从文件读取数据。
所需头文件: #include <unistd.h>
函数原型:ssize_t read(int fd, void *buf, size_t count);
参数:fd:将要读取数据的文件描述词。buf:指缓冲区,即读取的数据会被放到这个缓冲区中去。count: 表示调用一次read操作,应该读多少数量的字符。
返回值:返回所读取的字节数;0(读到EOF);-1(出错)。
4)write()函数
功能描述: 向文件写入数据。
所需头文件: #include <unistd.h>
函数原型:ssize_t write(int fd, void *buf, size_t count);
返回值:写入文件的字节数(成功);-1(出错)
5)lseek()函数
功能描述: 用于在指定的文件描述符中将将文件指针定位到相应位置。
所需头文件:#include <unistd.h>,#include <sys/types.h>
函数原型:off_t lseek(int fd, off_t offset,int whence);
参数:fd;文件描述符。offset:偏移量,每一个读写操作所需要移动的距离,单位是字节,可正可负(向前移,向后移)
返回值:成功:返回当前位移;失败:返回-1

8.3 printf的实现分析[11]

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函数主要调用了vsprintf和write函数。
下面首先介绍vsprintf(buf, fmt, arg)是什么函数:

int vsprintf(char \*buf, const char \*fmt, va\_list args)
{char\* p;
char tmp[256];
va\_list p\_next\_arg = args;
for (p=buf;\*fmt;fmt++) {
if (\*fmt != '%') {
\*p++ = \*fmt;
continue;
}
fmt++;

switch (\*fmt) {
case 'x':
itoa(tmp, \*((int\*)p\_next\_arg));
strcpy(p, tmp);
p\_next\_arg += 4;
p += strlen(tmp);
break;
case 's':
break;
default:
break;
}
}
return (p - buf);
}

从上面vsprintf函数可以看出,这个函数的作用是将所有的参数内容格式化之后存入buf,然后返回格式化数组的长度。
对write进心追踪:
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
一个int INT_VECTOR_SYS_CALL表示要通过系统来调用sys_call这个函数。
sys_call的实现:
sys_call:
call save
push dword [p_proc_ready]
sti
push ecx
push ebx
call [sys_call_table + eax * 4]
add esp, 4 * 3
mov [esi + EAXREG - P_STACKBASE], eax
cli
ret
于是可以直到printf函数执行过程如下:
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析[12]

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由宏实现:#define getchar() getc(stdin)。getchar有一个int型的返回值。当程序调用getchar时.程序就等着用户按键。用户输入的字符被存放在键盘缓冲区中。直到用户按回车为止(回车字符也放在缓冲区中)。当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的字符的ASCII码,若文件结尾(End-Of-File)则返回-1(EOF),且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完后,才等待用户按键。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

本章通过介绍hello中包含的函数所对应的unix I/O,大致了解了I/O接口及其工作方式,同时也了解了硬件设备的使用和管理的技术方法。

结论

hello所经历的过程:
1)hello被I/O设备编写,以文件的方式储存在主存中。
2)hello.c被预处理hello.i文件
3)hello.i被编译为hello.s汇编文件
4)hello.s被汇编成可重定位目标文件hello.o
5)链接器将hello.o和外部文件链接成可执行文件hello
6)在shell输入命令后,通过exceve加载并运行hello
7)在一个时间片中,hello有自己的CPU资源,顺序执行逻辑控制流
8)hello的VA通过TLB和页表翻译为PA
9)三级cache 支持下的hello物理地址访问
10)hello在运行过程中会有异常和信号等
11)printf会调用malloc通过动态内存分配器申请堆中的内存
12)shell父进程回收hello子进程,内核删除为hello创建的所有数据结构
深切感悟:计算机系统的设计与实现,处处体现着抽象的含义。比如,程序的本质是01二进制码,也就是机器语言。而汇编语言实现了对机器语言的抽象,高级语言实现了对汇编语言的抽象。再比如,各种物理内存的实现方式各不相同,有磁盘、软盘等。使用虚拟内存的概念实现了对各种物理内存的抽象,而具体实现则交给I/O设备进行处理,使得上层在使用的时候非常方便。概念上的抽象使得对概念的使用变得简单,这就是我对计算机系统设计实现的一个感悟。

附件

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

文件名 文件作用
hello.i 预处理文本文件
hello.s 编译后的汇编文件
hello.o 汇编后的可重定位文件
hello 链接后的可执行文件
helloobjdump.txt hello的反汇编代码

参考文献

[1] 预处理_百度百科.https://baike.baidu.com/item/预处理/7833652?fr=aladdin
[2] 编译_百度百科.https://baike.baidu.com/item/编译/1258343?fr=aladdin
[3] atoi_百度百科.https://baike.baidu.com/item/atoi/10931331?fr=aladdin
[4] 汇编程序_百度百科.https://baike.baidu.com/item/汇编程序/298210?fromtitle=%E6%B1%87%E7%BC%96&fromid=627224&fr=aladdin
[5] 进程_百度百科.https://baike.baidu.com/item/进程
[6] HIT-ICS第8章 异常控制流 L1异常与进程—PPT
[7] shell(计算机壳层)_百度百科.
https://baike.baidu.com/item/shell/99702?fr=aladdin

  1. [8] 逻辑地址到线性地址的转换.
    https://blog.csdn.net/xuwq2015/article/details/48572421
    [9] HIT-ICS第9章 L2虚拟内存-系统new—PPT
    [10] HIT-ICS第9章 L1虚拟内存-概念—PPT
    [11]Printf函数实现的深入剖析.https://www.cnblogs.com/pianist/p/3315801.html
    [12]getchar计算机语言函数_百度百科.
    https://baike.baidu.com/item/getchar/919709?fr=aladdin

你可能感兴趣的:(计算机系统大作业:程序人生-Hello‘s P2P)