HIT CSAPP 程序人生-Hello’s P2P

计算机系统

大作业

题     目  程序人生-Hello’s P2P 

专       业           计算学部           

学    号        1234567890        

班   级           1234567           

学      生              csd             

指 导 教 师             吴锐          

计算机科学与技术学院

2021年5月

摘  要

本文章以一个简单的程序hello.c为例,在linux下,运用gcc、readelf、edb等工具,介绍了其From Program to Process、From Zero-0 to Zero-0的过程,让读者从中学到一个程序的预处理、编译、汇编、链接等过程,了解它的进程管理、存储管理和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的预处理结果解析... - 6 -

2.4 本章小结... - 6 -

第3章 编译... - 7 -

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

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

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

3.3.1数据... - 8 -

3.3.2赋值... - 8 -

3.3.3算术操作... - 8 -

3.3.4逻辑操作... - 9 -

3.3.5关系操作... - 9 -

3.3.6数组/指针/结构操作... - 9 -

3.3.7控制转移... - 10 -

3.3.8函数操作... - 10 -

3.4 本章小结... - 11 -

第4章 汇编... - 12 -

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

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

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

4.4 Hello.o的结果解析... - 15 -

4.5 本章小结... - 16 -

第5章 链接... - 17 -

5.1 链接的概念与作用... - 17 -

5.2 在Ubuntu下链接的命令... - 17 -

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

5.4 hello的虚拟地址空间... - 20 -

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

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

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

5.8 本章小结... - 23 -

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

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

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

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

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

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

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

6.7本章小结... - 29 -

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

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

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

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

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

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

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

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

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

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

7.10本章小结... - 36 -

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

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

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

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

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

8.5本章小结... - 39 -

结论... - 40 -

附件... - 41 -

参考文献... - 42 -

第1章 概述

1.1 Hello简介

P2P:一个程序 hello.c经过预处理得到hello.i,经过编译得到hello.s,经过汇编得到hello.o,经过链接得到hello,进程管理执行hello。

020:从0开始,shell使用fork形成子进程,调用execve加载hello,映射虚拟内存,进入 main函数执行代码。当程序运行结束后,shell回收hello进程,内核删除相关数据结构,再次回到0。

1.2 环境与工具

(1)硬件环境

X64 CPU;1.8GHz;8G RAM;256GHD Disk

(2)软件环境

Windows10 64位;VirtualBox;Ubuntu 20.04优麒麟 64位;

(3)开发工具

GCC;READELF;EDB

1.3 中间结果

hello.c——源程序

hello.i——预处理生成的文件

hello.s——编译生成的文件

hello.o——汇编生成的文件

hello_o.elf——用readelf生成可重定位目标文件hello.o的ELF格式文件

hello——链接生成的文件

hello.elf——用readelf生成可执行目标文件hello的ELF格式文件。

1.4 本章小结

本章从P2P、020简要介绍了hello的一生,列出了本实验中的环境与工具、中间生成的文件。

第2章 预处理

2.1 预处理的概念与作用

概念:

预处理是C语言的一个重要功能,它由预处理程序负责完成。当对一个源文件进行编译时,系统将自动引用预处理程序对源程序中的预处理部分作处理,处理完毕自动进入对源程序的编译。C语言提供多种预处理功能,主要处理#开始的预编译指令,如宏定义(#define)、文件包含(#include)、条件编译(#ifdef)等。合理使用预处理功能编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。

作用:

(1)将源文件中以“include”格式包含的文件复制到编译的源文件中。

(2)用实际值替换用“#define”定义的字符串。

(3)根据“#if”后面的条件决定需要编译的代码。

2.2在Ubuntu下预处理的命令

命令:gcc -E hello.c -o hello.i

图2-1 预处理过程

2.3 Hello的预处理结果解析

打开hello.i文件查看内容,可以发现hello.i从hello.c的23行变成了3060行。

这是因为预处理将stdio.h、unistd.h、stdlib.h这三个系统头文件的内容直接插入hello.i中,最后紧跟源文件hello.c中main函数的内容。

图2-2 hello.c内容

 

图2-3 hello.i中main函数内容

2.4 本章小结

本章介绍了预处理的概念和作用,然后以hello.c为例,进行预处理得到hello.i文件,并对预处理的结果进行解析。

第3章 编译

3.1 编译的概念与作用

概念:

编译是指编译器(ccl)将文本文件hello.i翻译成文本文件hello.s的过程。

作用:

把预处理后的高级语言文本变成汇编语言文本。

3.2 在Ubuntu下编译的命令

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

图3-1 编译过程

3.3 Hello的编译结果解析

3.3.1数据

(1)局部变量i

int i是一个未初始化的局部变量,根据它的第一次使用赋值0,找到它存放的栈,位置是-4(%rbp)。

图3-2 局部变量i汇编代码

(2)局部变量argc

argc由寄存器%edi保存,然后又被存入-20(%rbp)。

图3-3 局部变量argc汇编代码

(3)字符串

"用法: Hello 学号 姓名 秒数!\n"和"Hello %s %s\n"存放在hello.s开头.rodata 节

图3-4 .rodata内容

(4)整数4、0、8

对于直接出现的整数,编译器处理成立即数出现在汇编代码中。

3.3.2赋值

i=0:对i赋值用了movl操作,将立即数0传给i。

图3-5  i赋值操作汇编代码

3.3.3算术操作

i++:在for循环中的i++操作,用addl将-4(%rbp)中的值加1,再存入其中。

图3-6  i++算术操作汇编代码

3.3.4逻辑操作

argc!=4:在if语句中判断argc是否不等于4,用cmpl比较argc与立即数4的大小,跳转指令je表示相等则跳转到.L2(即if语句之外的操作),若不相等则执行je下一行程序(即if语句中的程序)。

图3-7 if语句汇编代码

3.3.5关系操作

i<8:在for循环中的条件为i<8,用cmpl比较-4(%rbp)与立即数7的大小,跳转指令jle表示若小于等于7(即小于8)则跳转到.L4(即for循环中的操作),若大于则执行jle下一行程序(即for循环之后的程序)。

图3-8  i<8关系操作汇编代码

3.3.6数组/指针/结构操作

数组argv:

char *argv[], main函数执行时输入的命令行,在用到argv[1]、argv[2]、argv[3]等,需要用到movq、addq等操作,因为argv作为一个指针数组,每个地址是8位。

图3-9  数组argv操作汇编代码

3.3.7控制转移

(1)if语句

在3.3.4中已说明,if语句的控制转移用到了cmpl和je操作。

(2)for循环

在3.3.5中已说明,for循环的控制转移用到了cmpl和jle操作。

3.3.8函数操作

(1)main

main函数的调用是pushq寄存器%rbp,movq%rsp到%rbp,保存栈基地址,栈指针减32,开一个32的空间;

main函数的返回是将%eax设置为0,然后释放堆栈空间,返回%eax(即return 0)。

图3-10 main函数调用汇编代码

图3-11 main函数返回汇编代码

(2)printf

调用printf函数是将字符串传到%rdi,再调用puts打印出来。

图3-12 printf函数调用汇编代码

(3)exit

exit的退出方式是将立即数1传入%edi(传参),然后调用exit退出。

图3-13 exit函数调用汇编代码

(4)atoi

将argv[3]传给%rdi(传参),然后调用atoi函数得到整型数

图3-14 atoi函数调用汇编代码

(5)sleep

将得到的整型数给到%edi(传参),然后调用sleep函数是计算机暂时休眠所给的时间。

图3-15 sleep函数调用汇编代码

(6)getchar

直接调用getchar

图3-16 getchar函数调用汇编代码

3.4 本章小结

本章介绍了编译的概念和作用,然后以hello.i为例,进行编译得到hello.s文件,并对编译的结果从数据、赋值、算数操作、逻辑操作、关系操作、数组、控制转移、函数操作进行解析。

第4章 汇编

4.1 汇编的概念与作用

概念:

汇编是指汇编器(as)将文本文件hello.s翻译成二进制文件hello.o的过程。

作用:

将编译后的汇编代码翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中。

4.2 在Ubuntu下汇编的命令

命令:gcc -c hello.s -o hello.o

图4-1 汇编过程

4.3 可重定位目标elf格式

(1)分析hello.o的ELF格式

图4-2 典型的ELF可重定位目标文件格式

(2)用readelf列出其各节的基本信息

用readelf -a hello.o > hello_o.elf命令生成可重定位目标文件hello.o的ELF格式文件hello_o.elf。

图4-3 readelf使用过程

①ELF头

ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表(section header table) 的文件偏移,以及节头部表中条目的大小和数量。

图4-4 ELF头内容

②节头部表

不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的条目(entry)。

图4-5 节头部表内容

③重定位节rel.text

一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。另一方面,调用本地函数的指令则不需要修改。注意,可执行目标文件中并不需要重定位信息,因此通常省略,除非用户显式地指示链接器包含这些信息。

.rela.text中有8条重定位信息,分别是对字符串.L0内容、puts函数、exit函数、字符串.L1内容、printf函数、atoi函数、sleep函数、getchar函数的重定位声明。.rela.eh_frame中是对代码段的重定位声明。

图4-6 重定位节rel.text内容

④.symtab

它是一个符号表,存放在程序中定义和引用的函数和全局变量的信息。每个可重定位目标文件在. symtab中都有一张符号表(除非程序员特意用STRIP命令去掉它)。和编译器中的符号表不同,symtab符号表不包含局部变量的条目。

从图中我们看到开始的10个条目没有显示出来,它们是链接器内部使用的局部符号。第94行全局符号main定义的条目,它是一个位于.text节中偏移量为0(即value值)处的146字节函数。其后跟随着的是全局符号_GLOBAL_OFFSET_TABLE、puts、exit、printf、atoi、sleep、getchar等外部符号的引用。

readelf用一个整数索引来标识每个节。Ndx=1表示.text节,而Ndx=UND表示来自对外部符号的引用。

图4-7 符号表.symtab内容

4.4 Hello.o的结果解析

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

图4-8 hello.o反汇编过程

机器语言是二进制机器指令的集合。机器指令展开来讲就是一台机器可以正确执行的命令。机器指令由操作码和操作数组成。汇编语言是一种用于计算机或其他可编程器件的低级语言,亦称为符号语言。是通俗的比较容易理解的语言。在不同的设备中,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令。机器语言与汇编语言具有一一对应的映射关系,一条机器语言程序对应一条汇编语言语句。

通过对照,可以发现在分支转移、函数的调用中机器语言与汇编语言在操作数上的不一致:

①机器语言是用“函数名+偏移量”表示分支转移和函数的调用

图4-9 分支转移的机器语言

图4-10 函数调用的机器语言

②汇编语言直接将跳转目标用“.L2”等助记符表示分支转移

图4-11 分支转移的汇编语言

用“函数名@PLT”表示函数的调用          

图4-12 函数调用的汇编语言

4.5 本章小结

本章介绍了汇编的概念和作用,然后以hello.s为例,进行汇编得到hello.o文件,分析了hello.o的可重定位目标elf格式,并将hello.o的反汇编结果与第3章的 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-2 执行过程

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

(1)分析hello的ELF格式

图5-3 典型的ELF可执行目标文件格式

(2)用readelf列出其各段的基本信息,包括各段的起始地址,大小等信息

用readelf -a hello > hello.elf命令生成可执行目标文件hello的ELF格式文件hello.elf。

图5-4 readelf使用过程

①ELF头

图5-5  ELF头内容

②节头部表

图5-6 节头部表内容(1)

图5-7 节头部表内容(2)

③程序头

图5-8 程序头内容

5.4 hello的虚拟地址空间

使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。

我们先用edb打开hello,通过查看5.3中的节头部表得知各段的地址,然后在edb中找到它们的具体信息。

例如在5.3中找到.interp段,它的地址位于0x4002e0

图5-9 节头部表中.interp段内容

用edb查看,.interp段确实位于0x4002e0处

图5-10 edb查看各段地址信息

查看其他段,同样都符合5.3中节头部表的地址声明。

5.5 链接的重定位过程分析

先用objdump反汇编hello,查看反汇编内容。

图5-11 objdump反汇编过程

通过观察分析,可以看出hello相比hello.o多了许多节:.interp(保存ld.so的路径)、.rela.plt(.plt的重定位项目)、.init(初始化代码)、.plt(动态链接过程链接表)、.got(动态链接全局偏移量表,用于存放变量)、.got.plt(动态链接全局偏移量表,用于存放函数)

并且hello.o的反汇编中对全局变量的引用地址均为0,函数调用的地址也只是当前指令的下一条指令的地址,而hello中有函数的地址。

图5-12 hello中main函数调用printf函数汇编代码

我们知道重定位记录有两种,分别是PC相对地址的引用和绝对地址的引用。进行重定位时,hello根据.rela.text和.rela.data中的重定位记录,在.symtab中查找需要修改的记录的符号,并结合符号与重定位记录中的位置信息对目标位置进行修改。如果需要修改的符号是本地符号,则计算偏移量并修改目标位置;如果是共享库中的符号,则创建.got表项(如果是函数还需创建.plt项),并创建新的重定位记录指向.got表项。这就是hello的重定位过程。

5.6 hello的执行流程

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

hello执行过程中调用的函数如下:

_dl_start、_dl_init、_start、__libc_start_main@plt、__libc_csu_init、init、main、__GI_exit、__run_exit_handlers、_dl_fini、_IO_cleanup、_IO_flush_all_lockp。

5.7 Hello的动态链接分析

无论在内存中的何处加载一个目标模块,数据段与代码段的距离总是保持不变。因此,代码段中任何指令和数据段中任何变量之间的距离都是一个运行时常量,与代码段和数据段的绝对内存位置是无关的。

而要生成对全局变量PIC引用的编译器利用了这个事实,它在数据段开始的地方创建了一个表,叫做全局偏移量表(GOT)。在GOT中,每个被这个目标模块引用的全局数据目标(过程或全局变量)都有一个8字节条目。编译器还为GOT中每个条目生成 一个重定位记录。在加载时,动态链接器会重定位GOT中的每个条目,使得它包含目标的正确的绝对地址。

先查看.got.plt段的地址信息,在0x404000处,用edb找到该段内容。

图5-13 .got段信息

在0x404000后面可以看到地址0x7f07510d9190。在0x404010前可以看到地址0x7f075100c2ae0。这两个地址就是我们分别要找的GOT[1]和GOT[2]地址。

图5-14 GOT[1]与Got[2]地址信息

GOT[2]动态链接在ld-linux.so中的入口点,我们用edb找到动态链接函数。动态链接器使用两个栈条目来确定puts的运行时位置,用这个地址重写puts的GOT项,再把控制传递给puts。

在下一次执行到puts对应的PLT条目时,GOT项已经被修改,因此利用GOT项进行的间接跳转会直接跳转到puts函数。

图5-15 动态链接器汇编代码

5.8 本章小结

本章介绍了链接的概念和作用,然后以hello.o为例,进行汇编得到hello文件,分析了hello的可执行目标elf格式,并使用edb加载hello,查看本进程的虚拟地址空间各段信息,对hello进行反汇编,对链接的重定位过程进行了分析,更加深入地认识到链接的重定位过程和各个表的作用。

6章 hello进程管理

6.1 进程的概念与作用

概念:

进程的经典定义就是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文(context)中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。

作用:

每次用户通过向shell 输人一个可执行目标文件的名字,运行程序时,shell 就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,并且在这个新进程的上下文中运行它们自己的代码或其他应用程序。

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

作用:

Shell是指UNIX系统下的一个命令解析器;主要用于用户和系统的交互。UNIX系统上有很多种Shell。bash是一个为GNU项目编写的Unix shell。bash脚本功能非常强大,尤其是在处理自动循环或大的任务方面可节省大量的时间。bash是许多Linux平台的内定Shell。

处理流程:

(1) 从终端读入输入的命令。

(2) 将输入字符串切分获得所有的参数。

(3) 如果是内置命令则立即执行。

(4) 否则调用相应的程序为其分配子进程并运行。

(5) shell 应该 接受键盘输入信号,并对这些信号进行相应处理。

6.3 Hello的fork进程创建过程

(1) 首先在终端输入./hello。它不是shell的内置命令,shell会从文件系统中找到当前目录下的hello文件并执行。

(2) Shell调用fork函数,创建一个子进程。子进程除了进程号之外与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,当父进程调用fork时,子进程可以读写父进程中的任何文件。

(3) hello将在fork创建的子进程中执行。内核能够以任意方式交替执行父子进程的逻辑控制流的指令,父进程与子进程是并发运行而独立的。在子进程执行期间,父进程等待子进程的完成。

6.4 Hello的execve过程

execve函数在当前进程的上下文中加载并运行一个新的程序,函数声明如下:

int execve(const char *filename, const char *argv [],const char *envp[]) ;

(1) execve函数加载并运行可执行目标文件filename,且带参数列表argv 和环境变量列表envp。

(2) 只有当出现错误时,例如找不到filename,execve才会返回到调用程序。

(3)在execve加载了filename之后,它调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数。

图6-1 一个新程序开始时,用户栈的典型组织结构

6.5 Hello的进程执行

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

Hello的进程执行:

(1) 在终端输入命令行“./hello 1190201925 蔡思娣 1”,内核调度hello的进程开始执行,输出“Hello 1190201925 蔡思娣”,然后执行sleep函数,显示地请求让调用进程休眠。

(3) 在休眠时,内核转而执行其他进程,这时就会发生一个从用户到内核再到用户的转换。休眠结束后恢复hello进程的上下文,继续执行hello进程。重复8次这个过程。

(4) 输出8行字符串后,执行到getchar函数,需要读取数据,所以会发生一个上下文切换执行其他进程。当数据已经被读取到缓存区中,将会发生一个中断,使内核发生上下文切换,重新执行hello进程。

6.6 hello的异常与信号处理

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

中断是异步发生的,是来自处理器外部的I/O设备的信号的结果。硬件中断不是由任何一条专门的指令造成的,从这个意义上来说它是异步的。硬件中断的异常处理程序常常称为中断处理程序(interrupt handler)。

陷阱是有意的异常,是执行- -条指令的结果。就像中断处理程序一样,陷阱处理程序将控制返回到下一条指令。陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。

故障由错误情况引起,它可能能够被故障处理程序修正。当故障发生时,处理器将控制转移给故障处理程序。如果处理程序能够修正这个错误情况,它就将控制返回到引起故障的指令,从而重新执行它。否则,处理程序返回到内核中的abort例程,abort例程会终止引起故障的应用程序。

终止是不可恢复的致命错误造成的结果,通常是一些硬件错误,比如DRAM或者SRAM位被损坏时发生的奇偶错误。终止处理程序从不将控制返回给应用程序。

一个信号就是一条小消息,它通知进程系统中发生了一个某种类型的事件。图6-2展示了Linux系统上支持的30种不同类型的信号。每种信号类型都对应于某种系统事件。低层的硬件异常是由内核异常处理程序处理的,正常情况下,对用户进程而言是不可见的。信号提供了一种机制,通知用户进程发生了这些异常。

图6-2 Linux下执行过程中可能产生的信号

(1)执行hello,按下回车。hello进程被回收。

 图6-3 回车命令的运行结果

(2)执行hello,按下Ctrl-Z。hello进程被停止。

图6-4 Ctrl-Z 命令的运行结果

(3)Ctrl-Z后输入ps,显示当前各进程信息。

图6-5 ps命令的运行结果

(4)Ctrl-Z后输入jobs,显示执行hello的命令行及其状态。

 图6-6 jobs命令的运行结果

(5)Ctrl-Z后输入pstree,显示当前各进程的树形结构。

图6-7 pstree命令的运行结果

(6)Ctrl-Z后输入fg,显示当前前台进程。

图6-8 fg命令的运行结果

(7)Ctrl-Z后输入kill -9 2420,hello进程被杀死。

图6-9 kill命令的运行结果

(8)执行hello,按下Ctrl-C,hello进程被终止。

 图6-10 Ctrl-C命令的运行结果

6.7本章小结

本章介绍了进程的概念与作用,然后简述了壳Shell-bash的作用与处理流程,以hello为例,描述了fork进程创建过程、execve过程以及进程执行,最后通过Ctrl-Z、Ctrl-C、ps、jobs、fg、kill等指令对hello的异常与信号处理进行分析。更加深入地认识到异常、进程和信号等知识。

7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址

逻辑地址指的是机器语言指令中,用来指定一个操作数或者是一条指令的地址,是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 [段标识符:段内偏移量]。

线性地址、虚拟地址

跟逻辑地址类似,也是一个不真实的地址,如果逻辑地址是对应的硬件平台段式管理转换前地址的话,那么线性地址则对应了硬件页式内存的转换前地址。

物理地址

用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。

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

段式内存管理方式就是直接将逻辑地址转换成物理地址,也就是CPU不支持分页机制。其地址的基本组成方式是段号+段内偏移地址。

在x86保护模式下,段的信息占8个字节,段信息无法直接存放在段寄存器中。Intel的设计是段描述符集中存放在GDT或LDT中,而段寄存器存放的是段描述符在GDT或LDT内的索引值。

首先给定一个完整的逻辑地址[段标识符:段内偏移量],

(1)看段选择描述符中的T1字段是0还是1,可以知道当前要转换的是GDT中的段,还是LDT中的段,再根据指定的相应的寄存器,得到其地址和大小,我们就有了一个数组了。

(2)拿出段选择符中的前13位,可以在这个数组中查找到对应的段描述符,这样就有了Base,即基地址就知道了。

(3)把基地址Base+Offset,就是要转换的下一个阶段的地址。

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

分页的基本原理是把内存划分成大小固定的若干单元,每个单元称为一页,每页包含4k字节的地址空间。这样每一页的起始地址都是4k字节对齐的。为了能转换成物理地址,我们需要给CPU提供当前任务的线性地址转物理地址的查找表,即页表。

CPU中的一个控制寄存器,页表基址寄存器指向当前页表。n位的虛拟地址包含两个部分:一个p位的虚拟页面偏移(VPO)和一个(n-p)位的虚拟页号(VPN)。 MMU利用VPN来选择适当的PTE。例如,VPN0选择PTE 0,VPN 1选择PTE1,以此类推。将页表条目中物理页号(PPN) 和虚拟地址中的VPO串联起来,就得到相应的物理地址。注意,因为物理和虚拟页面都是P字节的,所以物理页面偏移(PPO)和VPO是相同的。

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

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

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

(3)高速缓存/主存向MMU返回PTE。

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

(5)高速缓存/主存返回所请求的数据字给处理器。

处理缺页:

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

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

(3)高速缓存/主存向MMU返回PTE。

(4)PTE中的有效位是零,所以MMU触发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序。

(5)缺页处理程序确定出物理内存中的牺牲页,如果这个页面已经被修改了,则把它换出到磁盘。

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

(7)缺页处理程序返回到原来的进程,再次执行导致缺页的指令。CPU将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面现在缓存在物理内存中,所以就会命中,在MMU执行了图9-13b中的步骤之后,主存就会将所请求字返回给处理器。

图7-1 使用页表的地址翻译

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

一个运行Linux的Intel Core i7实现支持48位虚拟地址空间和52位物理地址空间。

Corei7内存系统的重要部分:处理器封装包括四个核、一个大的所有核共享的L3高速缓存,以及一个DDR3内存控制器。每个核包含一个层次结构的TLB、一个层次结构的数据和指令高速缓存,以及一组快速的点到点链路,这种链路基于QuickPath技术,是为了让一个核与其他核和外部I/O桥直接通信。TLB是虚拟寻址的,是四路组相联的。L1、L2和L3高速缓存是物理寻址的,块大小为64字节。L1和L2是8路组相联的,而L3是16路组相联的。页大小可以在启动时被配置为4KB或4MB。Linux 使用的是4KB的页。

Core i7采用四级页表层次结构。每个进程有它自己私有的页表层次结构。当一个Linux进程在运行时,虽然Core i7体系结构允许页表换进换出,但是与已分配了的页相关联的页表都是驻留在内存中的。CR3控制寄存器指向第一级页表(L1)的起始位置。CR3的值是每个进程上下文的一部分,每次上下文切换时,CR3的值都会被恢复。

图7-2 Core i7 的内存系统

易知48位VA中VPN占36位(TLBT(前32位)+TLBI(后4位)),VPO占12位。如图,CPU产生虚拟地址VA传送给MMU,MMU使用前36位VPN作为向TLB中匹配。若命中,则得到PPN(40位)与PPO(12位)组合成PA(52位);若不命中,MMU向页表中查询,CR3确定第一级页表的起始地址,VPN1(9位)确定在第一级页表中的偏移量,查询出PTE,若在物理内存中且权限符合,确定第二级页表的起始地址,以此类推,最终在第四级页表中查询到PPN,与PPO组合成PA,并且向TLB中添加条目。若查询PTE不在物理内存中,则引发缺页故障。若权限不够,则引发段错误。

图7-3 Core i7 地址翻译的概况

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

(1)先访问一级缓存;

(2)若访问一级缓存不命中则访问二级缓存;

(3)若访问二级缓存再不命中则访问三级缓存;

(4)若访问三级缓存再不命中则访问主存;

(5)若主存缺页则访问硬盘。

7.6 hello进程fork时的内存映射

当fork函数被当前进程(hello)调用时

(1)内核为新进程创建各种数据结构,并分配给它一个唯一的PID。

(2)为了给这个新进程创建虚拟内存,它创建了当前进程的mm_ struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。

(3)当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,为每个进程保持了私有地址空间的抽象概念。

7.7 hello进程execve时的内存映射

当进程(hello)执行了execve调用,会执行以下步骤:

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

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

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

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

图7-4 加载器映射用户地址空间的区域

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

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

(1)判断虚拟地址A是否合法。缺页处理程序搜索区域结构的链表,把A和每个区域结构中的vm_start和vm_end做比较。如果这个指令是不合法的,那么缺页处理程序就触发一个段错误,从而终止这个进程。

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

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

7.9动态存储分配管理

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

策略:

(1)记录空闲块:隐式空闲链表,显示空闲链表,分离的空闲链表和按块大小排序建立平衡树。

(2)放置策略:首次适配,下一次适配,最佳适配。

(3)合并策略,立即合并,延迟合并。

7.10本章小结

本章介绍了逻辑地址、线性地址、虚拟地址和物理地址的概念,分析了Intel逻辑地址到线性地址的变换、线性地址到物理地址的变换以及TLB与四级页表支持下的VA到PA的变换描述了三级Cache支持下的物理内存访问。列出了hello进程fork时、execve时的内存映射,以及缺页故障与缺页中断处理。概括了动态存储分配管理的步骤,从抽象到具体的阐述了hello的存储管理。

8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:

一个Linux文件就是一个m个字节的序列:B1,B2,…,Bk,…,Bm-1

所有的1/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输人和输出都被当作对相应文件的读和写来执行。

设备管理:

这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输人和输出都能以一种统一且一致的方式来执行:打开和关闭文件、改变当前文件位置、读和写文件。

8.2 简述Unix IO接口及其函数

(1)打开文件。函数:open

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

(2)Linux shell创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。头文件< unistd.h> 定义了常量STDIN_FILENO、STDOUT_ FILENO和STDERR_ FILENO,它们可用来代替显式的描述符值。

(3)改变当前的文件位置。函数:lseek

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

(4)读写文件。函数:read、write

一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的文件,当k≥m时执行读操作会触发一个称为end-of-file(EOF)的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF 符号”。类似地,写操作就是从内存复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。

(5)关闭文件。函数:close

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

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;

}

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

va_list是一个字符指针类型,((char*)(&fmt) + 4)表示的是“...”中的第一个参数。

(2)i = vsprintf(buf, fmt, arg);

vsprintf的作用是将参数内容格式化之后存入buf,然后返回格式化数组的长度,赋值给i。

(3)write(buf, i);

write是写操作,把buf中的i个元素的值写到终端。

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

8.4 getchar的实现分析

观察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;

}

①n = read(0, buf, BUFSIZ);

getchar函数调用read函数,将整个缓冲区都读到buf里,并将缓冲区的长度赋值给n。

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

若n>=0则返回buf的第一个元素,n<0,则返回EOF(EOF通常为-1)。

当程序执行调用getchar函数,按键时键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码转换成 ASCII 码,保存到系统的键盘缓冲区之中。

getchar 调用了 read 函数,read 函数通过sys_call调用内核中的系统函数,读取存储在键盘缓冲区中的 ASCII 码,直到读到回车符,然后返回整个字符串。 getchar函数只从中读取第一个字符,其他的字符被缓存在输入缓冲区。

8.5本章小结

本章介绍了Linux的IO设备管理方法,简述Unix IO接口及其函数,通过对printf的实现分析、getchar的实现分析,更深入地理解了write、read函数的操作,进一步深化了对Unix IO接口的认识。

结论

总结hello所经历的过程:

(1)预处理:预处理器将hello.c预处理成为文本文件hello.i。

(2)编译:编译器将hello.i翻译成汇编语言文件hello.s。

(3)汇编:汇编器将hello.s汇编成可重定位二进制代码hello.o。

(4)链接:链接器将外部文件和hello.o连接起来形成可执行二进制文件hello。

(5)进程管理:shell通过fork创建进程,execve加载hello。shell创建新的内存区域,并加载代码、数据和堆栈。hello在执行的过程中遇到异常,接受信号完成处理。hello执行结束后,shell回收僵尸进程,从系统中消失。

(6)内存管理:hello在执行的过程中需要使用内存,通过CPU和虚拟内存进行地址访问。

(7)I/O管理:hello.c通过键盘鼠标等I/O设备输入计算机,并存储在内存中。

感悟:

一个程序产生和运行在程序员眼中只是编写代码和运行程序,然而在机器中需要经过预处理、编译、汇编、链接等一系列复杂的过程,中间还可能会出现意想不到的异常。学习计算机系统能够帮助我们了解一个源代码在机器中从程序到过程,从无到有、从有到无的过程,了解在程序执行的过程中内存的分配、I/O对程序的管理。了解这些知识能够让程序员减少编程错误,编写出更优化的代码。

附件

hello.c——源程序

hello.i——预处理生成的文件

hello.s——编译生成的文件

hello.o——汇编生成的文件

hello_o.elf——用readelf生成可重定位目标文件hello.o的ELF格式文件

hello——链接生成的文件

hello.elf——用readelf生成可执行目标文件hello的ELF格式文件。

参考文献

[1]  CSDN专业开发者社区https://www.csdn.net/

[2]   (美)兰德尔E.布莱恩德等著;龚奕利,贺莲译. 深入理解计算机系统[M]. 北京:机械工业出版社,2016.7

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值