hello的一生

计算机系统大作业
题 目 程序人生-Hello’s P2P
计算机科学与技术学院
2018年12月

目 录

第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简介
1.1.1 From Program to Process
首先,在codeblocks中敲入hello的代码,得到了hello.c的文件,保存后便得到了hello的最初形态——program。此时它只是一个静态的,毫无生命可言的。然后在linux中利用gcc,將hello.c通过cpp预处理得到hello.i,然后通过ccl编译得到hello.s,再通过as汇编器得到hello.o,然后通过ld连接器与c标准库链接最终变成hello可执行文件。
在shell中键入命令./hello。当检测到hello是可执行文件时,shell便会调用fork函数,建立一个新的子进程,然后子进程调用execve函数启动加载器将hello的内容加载到内存中去,覆盖原先子进程的内存段。然后就可以通过一系列的调用来运行hello的程序了,此时便是一个进程process了。
1.1.2 From Zero-0 to Zero-0
首先,我们得到的hello执行文件只是存放在硬盘上的二进制文件,相当于还是死的。当我们在执行程序时,hello的内容才会从磁盘加载到内存,被CPU调用。此时的hello已然有了生命。
当程序运行结束后,shell回收hello进程,然后由操作系统删除hello的驻留,然后操作系统将控制权又返回给shell,就好像hello从未发生过一样。
1.2 环境与工具
1.2.1硬件环境
X64 CPU;2.8GHz;8G RAM;931GHD Disk;
1.2.2 软件环境
Windows10 64位;Vmware 14.13;Ubuntu 18.04 LTS 64位;
1.2.3 开发工具
CodeBlocks;gcc+gpedit
1.3 中间结果
文件的名字 文件的作用
hello.i 预处理后得到的文件
hello.s 编译后得到的文件
hello.o 汇编后得到的文件
hello 链接后的可执行文件
hello.txt hello的反汇编代码
helloobjdump.txt hello.o的反汇编代码

1.4 本章小结
刚开始hello.c文件只是称为程序的字符串,只有经过预处理,编译,汇编,链接才能变成可执行文件。然后通过shell的调用,使hello变成一个进程,占有内存和CPU等资源。最后运行结束被父进程shell回收,被内核删除所有状态。这就是hello完整的一生,也应该是每个细心的程序员必须知道的事。
(第1章0.5分)

第2章 预处理
2.1 预处理的概念与作用
概念:由预处理器cpp在源程序hello.c中搜索#开头的字符串(头文件,宏定义,条件编译等),并将其展开,修改源程序。
作用:预处理器根据以字符#开头的命令,修改原始的C程序,它总共完成4步:
(1) 展开头文件:拓展源代码,插入所有头文件中的所有文件。
(2) 替换宏定义:扫描程序中所有的符号,将其替换成宏定义的内容。
(3) 去掉注释:去掉程序中所有的注释性语句。
(4) 处理条件编译。
2.2在Ubuntu下预处理的命令
使用gcc预处理命令(gcc -m64 -no-pie -fno-PIC -E hello.c -o hello.i)直接预处理hello.c,得到hello.i的预处理文件:
在这里插入图片描述
图.2.2.1预处理命令
(其中,-E表示只进行预处理,不编译。-o表示输出成指定文件名。)
然后我们就得到hello.i这个预处理文件。
在这里插入图片描述
图2.2.2得到预处理文件
2.3 Hello的预处理结果解析
由上面的命令,我们得到hello.i文件:

打开hello.i发现里面已经有很多行奇怪的代码,我们慢慢分析:
在这里插入图片描述
图2.3.1 hello.i内容
如上图,首先hello.i文件里包含了各个要插入头文件的文件目录,打开stdio.h文件,我发现这些都是stdio.h, unistd.h, stdlib.h内又引用的头文件,如下:
在这里插入图片描述
图2.3.2 hello.i内容
接着继续分析hello.i文件内容:
在这里插入图片描述
图2.3.3 hello.i内容
在这里插入图片描述
图2.3.4 hello.i内容
在这里插入图片描述
图2.3.5 hello.i内容
在这里插入图片描述
图2.3.6 hello.i内容
通过上面的截图,我发现hello.i文件里主要做了如下事情:
(1).对变量进行了大量的重命名typedef
(2).定义了大量的外部函数extern
(3).定义了大量的结构体struct和少量的枚举类enum
最后,让我们跳到hello.i的最后几行:
在这里插入图片描述
图2.3.7 hello.i内容
哈哈,终于不再是陌生的东西,而是我们的老朋友hello.c了,不过仔细对比源代码发现,好像是hello.c剃了头一样,各种#include不见了,仔细一想,是预处理将头文件内容加载了进来,那么也就不需要include头文件了。
2.4 本章小结
预处理是对.c源文件的简单加工,是将.c中的头文件的所有内容(包括头文件调用的头文件的内容)插入到.c文件中,并将宏定义变量名替换成数据,还将.c中的各种注释去掉,最终得到了.i文件。
在.i文件中包含所有头文件目录(包括头文件调用的头文件的目录),包含头文件所有内容(包括头文件调用的头文件的内容),最后包括.c源代码去掉#include,#define,并已经替换宏定义量之后的代码。

(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
概念:编译器将hello.i翻译成hello.s,即得到一个汇编语言的文本文件。
作用:将.o文件的内容按照一定的语法规则翻译成汇编代码输出到.s文件中。
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序。
3.2 在Ubuntu下编译的命令
同样,使用gcc编译命令(gcc -m64 -no-pie -fno-PIC -S hello.i -o hello.s)对hello.i进行编译,得到hello.s的汇编文件:
在这里插入图片描述
图3.2.1编译命令
(其中,-S表示只编译,不汇编。-o表示输出成指定文件名。)
然后我就得到了hello.s这个文件。
在这里插入图片描述
图3.2.2得到编译文件
3.3 Hello的编译结果解析
打开hello.s文件,先看main之前的内容:
在这里插入图片描述
图3.3.1 hello.s内容main前
这是对代码中各种字符的声明,声明如下:
.file 对源程序的声明
.text 对代码段的声明
.globl 对全局变量的声明
.data 对数据段的声明
.align 对对齐方式的声明
.type 对函数或对象的声明
.size 对大小的声明
.long 对long类型的声明
.section .rodata 对只读数据段的声明
.string 对string类型的声明

下面对hello.s出现的各种数据和操作进行详细的声明:
3.3.1 数据
(1)整型变量sleepsecs
在这里插入图片描述
图3.3.2 hello.s中变量
由hello.s中的描述可以看见,它是全局变量,已经赋初值,存放在数据段.data。大小为4字节,以四字节对齐方式对齐。赋值为2。在只读数据段。
(2)字符串类型
在这里插入图片描述
图3.3.3 hello.s中字符串
在hello.s中的描述可以看见,它是存放在代码段.text中。
(3)函数类型
在这里插入图片描述
图3.3.4 hello.s中main函数
我们可以看到,main是一个函数类型,是全局已赋值的类型。
3.3.2操作
(1)赋值操作
赋值操作包括int sleepsecs = 2.5和i = 0;
对sleepsecs赋值操作汇编代码如下:
在这里插入图片描述
图3.3.5 hello.s中sleepsecs变量
对i赋值为0的汇编代码如下:
在这里插入图片描述
图3.3.6 hello.s中i=0
(2)类型转换
hello.s中存在一个隐式类型转换,即sleepsecs声明的是int型,但赋值时为2.5。而在汇编代码里面赋值为2。
在这里插入图片描述
图3.3.7 hello.s中强制转换
(3)算术运算
算术运算包括i++和计算有效地址。
对i++的操作如下:
在这里插入图片描述
图3.3.8 hello.s中i++
对有效地址的计算:
在这里插入图片描述
图3.3.9 hello.s中地址计算
(4)关系运算
关系运算包括argc!=3和i<10。
对argc的判断如下:
在这里插入图片描述
图3.3.10 hello.s中argc != 3
对i<10的判断如下:
在这里插入图片描述
图3.3.11 hello.s中i<10
这里编译器将<10,翻译成了<=9,二者等价。
(5)控制转移
主要包含两个部分:
a) if(argc != 3)编译如下:
在这里插入图片描述
图3.3.12 hello.s中if(argc != 3)
b) for(i = 0;i<10;i++)编译如下:
在这里插入图片描述
图3.3.13 hello.s中i<10
(6)函数调用
hello.s中主要有5个函数:
a) main函数
在这里插入图片描述
图3.3.14 hello.s中main函数
b) puts函数
在这里插入图片描述
图3.3.15 hello.s中puts函数
c) exit函数
在这里插入图片描述
图3.3.16 hello.s中exit函数
d) printf函数
在这里插入图片描述
图3.3.17 hello.s中printf函数
e) getchar函数
在这里插入图片描述
图3.3.18 hello.s中getchar
3.4 本章小结
通过编译器ccl的编译,将hello.i变成了hello.s,主要工作就是将源程序的各种数据进行分类,将源程序的各种操作与对应的汇编代码对应起来,以寄存器和系统函数的调用这种微操作来完成源hello.c要实现的操作。最终形成了比c更低级的汇编语言形式的hello。这种形式是直接操作CPU,使得它的执行速度和效率远大于高级程序语言。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
概念:汇编是将.s汇编程序翻译成.o可重定位的目标文件。.o文件是一个二进制文件,它包含了机器代码。
作用:汇编器按照汇编语句与机器指令的一一对应关系直接将.s中的汇编代码翻译成机器指令。
4.2 在Ubuntu下汇编的命令
我们使用gcc汇编命令(gcc -c -m64 -no-pie -fno-PIC hello.s -o hello.o)对hello.s进行汇编,得到hello.o的可重定位文件:
在这里插入图片描述
图4.2.1汇编命令
(其中,-no-pie -fno-PIC 是为了为了简化)
然后我们就得到了可重定位的hello.o
在这里插入图片描述
图4.2.2当前得到文件
4.3 可重定位目标elf格式
用readelf打开hello.o文件:
我们便得到了hello.o的ELF格式。
首先是elf的头部:
在这里插入图片描述
图4.3.1 hello.o elf头
它包括Magic,用来声明生成该文件的字大小和字节顺序。
包括elf类别,数据表示形式,版本,系统架构等与操作系统有关的东西。
包括本头大小,节头数量,字符串节头索引等用于连接时解释目标文件的信息。

其次是节头信息:
在这里插入图片描述
图4.3.2 hello.o elf节头
它包括对各种节的大小,类型,地址,偏移量,对齐的声明。以及对其操作权限的规定如下:
在这里插入图片描述
图4.3.3 hello.o标志

然后是对重定位节的详细信息:
在这里插入图片描述
图4.3.4 hello.o 重定位节
它包括. .rela.text节和.rela.eh_frame节的信息,里面包括偏移量,信息,类型,符号名称+加数。
偏移量 各个符号离所在节首地址的偏移
类型 重定位到的目标的类型
符号值 此时全0
符号名称 需要重定位的符号
加数 计算重定位时的辅助信息

以分析.rela.text节的信息为例:
在这里插入图片描述
图4.3.5 重定位节
里面包含了8个符号,包括.rodata,puts,exit,.rodata,printf,sleepsecs,sleep,getchar。
对于.puts,假设我们已经有ADDR(.text),和ADDR(r.symbol),首先链接器计算引用的运行地址:
refaddr = ADDR(.text) + offset。
*refptr=(unsigned)(ADDR(r.symbol)+r.addend -refaddr)
我们就得到到了PC相对偏移地址,然后PC只需加上refptr即可引用puts的数据了。

4.4 Hello.o的结果解析
为了对比hello.o和原汇编代码区别,我们使用objdump -d -r hello.o > helloobjdump.txt命令将hello.o反汇编,并写入到helloobjdump.txt中,内容大致如下:
在这里插入图片描述
图4.4.1 hello.o elf格式
将其与hello.s中汇编语句比较有如下区别:
(1) 每行操作最前面多了16进制数,那是当前操作的地址偏移,用于链接时计算地址所用,由于main还未链接,故地址为0,。
(2) 每个汇编语句前面对应一段机器代码,而且每段机器包括汇编操作和对应的数据,寄存器代号。
(3) 函数调用(以printf函数为例):
在这里插入图片描述
图4.4.2 重定位反汇编printf函数
在这里插入图片描述
图4.4.3 可执行反汇编printf函数
上面的汇编代码就直接call printf,而下面的反汇编代码则call的是一个地址,是相对于main的偏移,这个地址是需要重定位的,在链接时需要对其进行修改使之指向动态库中printf函数的实际地址。
(4)分支转移
在这里插入图片描述
图4.4.4 重定位反汇编cmpl参数
在这里插入图片描述
图4.4.5可执行反汇编cmpl参数
上面的汇编语句jump的参数是.L2,即要跳转到的地方前面的标识符。而下面的反汇编代码则不是,由于反汇编代码中每行唯一对应偏移量,故jump时只需jump到跳转行的偏移地址即可。
(4) 在汇编代码中立即数都是十进制的,而反汇编代码中都是十六进制的。就像(4)中的20和0x14
(5) 对全局变量的访问
在这里插入图片描述
图4.4.6重定位访问全局变量
在这里插入图片描述
图4.4.7可执行访问全局变量
在汇编代码中对全局变量的访问是.LC1(%rip)的形式,而在反汇编代码中访问形式为0x0(%rip)的形式。这是因为在链接前,全局变量的地址无法确定,只有链接时修改0x0,使其指向全局变量的实际地址才能正确访问。
4.5 本章小结
汇编是将汇编语言转化为机器语言的过程,首先将每个汇编语句转化为对应的机器代码,然后计算出每条语句,每个符号的段内偏移,并将分支转移,函数调用,全局变量访问的形式由原来的行标记确认符号位置改为由地址偏移量进行访问。这些所有都是为了能让链接器更好的工作。
(第4章1分)

第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.2.1输入命令
在这里插入图片描述
图5.2.2得到hello可执行文件
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
5.3 可执行目标文件hello的格式
使用readelf打开hello可执行文件,其各段基本信息如下:

在这里插入图片描述
在这里插入图片描述
图5.3.1可执行文件hello节头
它包括各段的名称,类型,地址,偏移量,大小,旗标,链接,信息,对其等属性。其中,地址已经是对应段在虚拟地址中的地址了,可以通过hexedit进行追踪。
5.4 hello的虚拟地址空间
用edb打开hello可以看到各段详细信息如下:
其中可以在Data Dump中查看各段内容。首先通过Memory Regions定位到相应的段:
在这里插入图片描述
图5.4.1 检索地址
然后我们就可以在datadump中查看0x40000到0x400fff的节内容了。
比如找.interp节内容,我们只需定位到0x400200处:
在这里插入图片描述
图5.4.2 .interp节内容
同理可查看其他节内容,如下:
在这里插入图片描述
图5.4.3节内容
同理还可以查看到.data节的内容:
在这里插入图片描述
图5.4.4 .data节的内容
对于5.3中每个节,都可以通过edb来查看其节的十六进制内容。
5.5 链接的重定位过程分析
首先我们通过命令将hello的反汇编内容写入到hello.txt中:
在这里插入图片描述
图5.5.1 hello的反汇编内容写入到hello.txt
打开hello.txt:
在这里插入图片描述
图5.5.2 反汇编代码

与hello.o相比,主要区别如下:
首先,除了main函数,还多了许多新的节和函数:
(1).init段:包括_init,用于初始化程序
(2).plt段:包括.plt,puts@plt,printf@plt,getchar@plt,exit@plt,sleep@plt。
(3).text段:包括_start,_dl_relocate_static_pie,main,__libc_csu_init,__libc_csu_fini
(4).fini段:包括_fini终止程序时执行的代码
其次,由于已经重定位,每个操作最前面对应的不再是地址偏移,而是完整的虚拟地址,指向对应的机器代码和数据还有函数存放的地址。
再者,函数调用不在用地址偏移了,而是直接用绝对地址作为call参数,比如printf函数的调用:
在这里插入图片描述
图5.5.3 call printf函数
对重定位的分析我们以下面该语句为例:
在这里插入图片描述
图5.5.4重定位内容
在这里插入图片描述
图5.5.5 已重定位内容
利用4.3公式:
refaddr = ADDR(.text) + offset。
*refptr=(unsigned)(ADDR(r.symbol)+r.addend -refaddr)。
带入为:
refaddr = 0x400532 + 0x18 = 0x40054a
*refptr = 0x400644 - 0x4 – refaddr = 0xf6
在这里插入图片描述
图5.5.6 计算的PC偏移地址
5.6 hello的执行流程
使用edb单步执行hello程序,按照程序调用函数,写下函数名和地址如下:
地址 函数
0000000000400488 _init
0000000000400493 gmon_start
00000000004004a0 .plt
00000000004004b0 puts@plt
00000000004004c0 printf@plt
00000000004004d0 getchar@plt
00000000004004f0 sleep@plt
0000000000400500 _start
0000000000400530 _dl_ewlocate_static_pie
0000000000400530 Main
00000000004005c0 __libc_csu_init
0000000000400630 __libc_csu_fini
0000000000400634 _fini
下面是edb执行过程中调用的动态库
00007fee42162df0 _dl_fixup
00007fee4215e0b0 _dl_lookup_symbol_x
00007fee42164700 _dl_name_match_p
00007fee42170360 strcmp
00007fee42161ce0 __libc_memalign
……
5.7 Hello的动态链接分析
使用edb先在_dl_init_前后分别设置断点:
在这里插入图片描述
图5.7.1 设断点
然后在运行_dl_init_前查看.got.plt符号:
在这里插入图片描述
图5.7.2 .got.plt符号
在这里插入图片描述
图5.7.3运行_dl_init_前
在这里插入图片描述
图5.7.4运行_dl_init_后
5.8 本章小结
链接器完成链接过程,输入是可重定位文件,输出是已经包含虚拟地址的可执行文件。链接器主要任务是:(1)解析符号(2)重定位。符号解释将目标文件中的每个全局变量都绑定到一个唯一的定义,而重定位确定每个符号的最终内存地址,并修改对那些目标的引用。
然后加载器将可执行文件的内容映射到内存,并运行这个程序。共享库在任何时候都可以链接到主程序中
(第5章1分)

第6章 hello进程管理
6.1 进程的概念与作用
概念:进程,是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竟争计算机系统资源的基本单位。
作用:进程相当于一小段正在执行的程序,通过进程的调用,操作系统可以方便的切换不同进程从而执行不同的程序,同时又给程序一种假象,就好像程序独自拥有cpu一样,这样可以更快捷方便的管理程序。
6.2 简述壳Shell-bash的作用与处理流程
作用:shell是一个应用程序,是用户和操作系统的桥梁,它为用户提供可视化界面,方便用户对操作系统的操作。
处理流程:
(1) 我们在shell中键入命令。
(2) shell解析我们的命令并保存相应的参数。
(3) 根据我们的命令执行相应的操作函数。
(4) 命令要么是shell内置的,要么是加载一个新程序。
(5) shell在处理命令时在接收到相应信号后应做相应处理。
6.3 Hello的fork进程创建过程
首先我们在shell中键入./hello ** **,此时shell发现它是执行程序hello的命令,于是调用fork函数马上创建一个新进程,该新进程虚拟地址内容与父进程极其相似,甚至虚拟地址空间也相同,但是是一个独立的副本,因此子进程拥有父进程的所有控制流信息,可以打开父进程打开过的所有文件。二者最大的区别在于拥有不同的PID。具体如下图:
在这里插入图片描述
图6.3.1 fork流程
6.4 Hello的execve过程
此时子进程拥有父进程几乎完全一样的虚拟内存内容。然后在子进程中调用execve函数来运行hello可执行文件,此时先删除子进程虚拟内存中所有内存段,然后将hello的对应段写入到子进程虚拟内存中。完成后加载器设置PC程序计数器内容为_start的地址,然后开始从_strat开始执行,最终会调用main函数,执行hello的内容。
在这里插入图片描述
图6.4.1 execve流程
6.5 Hello的进程执行
每个程序都运行在一个进程上下文中,有自己的虚拟地址空间。刚开始执行的是shell,操作系统处于用户模式,正在自己的时间片上一步一步执行操作。当要执行hello进程时,先进入shell的内核态,由内核保存shell的上下文信息,然后切换到hello进程的内核态,并构建该进程的上下文内容,然后进入该进程的用户态,当hello执行完毕后发送一个信号给内核,内核接收到信号以后,将hello用户态切换到hello内核态,然后保存当前上下文信息,并进入shell的内核态,恢复shell进程的上下文信息,再进入shell的用户态,恢复shell的执行。对于其他的函数调用或者切换其他程序样。操作图如下:
在这里插入图片描述
图6.5.1 进程切换示意图
其中在每个内核态进行上下文切换。
6.6 hello的异常与信号处理
6.6.1 异常种类
1.键入Ctrl-z中断:SIGSTP:挂起程序
内核收到信号后,将hello进程上下文保存,然后将控制权转给bash。
2.键入Ctrl-c终止:SIGINT:终止程序
内核收到信号后直接终止程序,由父进程bash回收hello的遗留物。
6.6.2 Ctrl—z后键入命令
在这里插入图片描述
图6.6.1键入Ctrl—z后程序停止
在这里插入图片描述
图6.6.2输入ps命令显示当前存在的所有进程。
在这里插入图片描述
图6.6.3输入jobs命令显示hello已停止。
在这里插入图片描述
图6.6.4输入pstree命令
在这里插入图片描述
图6.6.5输入fg命令,发现程序继续执行。
在这里插入图片描述
图6.6.6kill hello进程,将其杀死。
在这里插入图片描述
图6.6.7输入Ctrl-c直接终止进程,相当于为执行。
6.7本章小结
异常控制流发送在计算机系统各个层次,是计算机系统提供并发的基本机制。
当一个进程接收到信号时,进程就会暂时挂起,上下文被内核保存,然后去执行异常处理子程序,根据结果的不同,子程序可以返回到原程序当前执行语句,可以返回到下一条语句,设置直接退出。
进程则给每个程序提供一个假象:就好像程序单独占用着CPU等资源,多个程序再同时运行。
操作系统提供了大量的信号处理函数和进程操作函数,方便我们对应用程序的管理。
(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:逻辑地址是产生的可重定位文件中的段内偏移地址。即产生hello.o文件中的地址。
线性地址:地址空间(address space) 是一个非负整数地址的有序集合,如果地址空间中的整数是连续的,那么我们说它是一个线性地址空间(linear address space) 。即产生的hello可执行文件中对应的地址。
虚拟地址:就相当于线性地址。就是hello里面的虚拟内存地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址共16位长,其中前13位是一个索引号,后3位包含一些硬件细节。首先通过段标识符的前13位,在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段,每一个段描述符由8个字节组成,如下图:
在这里插入图片描述
图7.2.1段描述表
在这里插入图片描述
图7.2.2段描述符
获得段描述符之后,我们就得到了Base基地址。最后只需将Base和偏移地址相加即可。
总的来说如下:
(1)通过段选择符中T1段.是0还是1,可以知道当前要转换的是GDT中的段,还是LDT中的段,再根据指定的相应的寄存器,得到其地址和大小,我们就有了一个数组了。
(2)拿出段选择符中的前13位,可以在这个数组中查找到对应的段描述符,这样就有了Base,即基地址就知道了。
(3)3.把基地址Base+Offset,就是要转换的下一个阶段的物理地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
该过程主要通过MMU来翻译,它主要接收虚拟地址,然后通过页表来翻译成物理地址,具体过程如下:
在这里插入图片描述
图7.3.1页表地址翻译
(1) 首先,CPU会生成一个虚拟地址,并将它传送至MMU。
(2) MMU获取VA的前n-p位VPN,然后以他作为索引,在存在内存中页表中搜索对应的页表项,返回页表项。
(3) 如果不命中则发出缺页中断,然后从磁盘中加载缺失的页表项。
(4) 然后重新搜索VPN对应的PTE。
(5) MMU利用返回的页表项和虚拟地址的低p位VPO构成一个新的地址,得到了新的物理地址。
(6) 将物理地址返回给处理器处理。
7.4 TLB与四级页表支持下的VA到PA的变换
图7-5给出了Core i7 MMU 如何使用四级的页表来将虚拟地址翻译成物理地址。36位VPN 被划分成四个9 位的片,每个片被用作到一个页表的偏移量。CR3 寄存器包含Ll页表的物理地址。VPN 1 提供到一个Ll PET 的偏移量,这个PTE 包含L2 页表的基地址。VPN 2 提供到一个L2 PTE 的偏移量,以此类推。

在这里插入图片描述
图7.4.1 Corei7页表翻译
首先由CPU产生虚拟地址VA,有48位,然后将VA送至MMU来翻译地址。
MMU将VA的前36位TLBT索引,在TLB中寻找PPN,然后与VPO组合最终得到52位的PA物理地址。
如果在TLB中搜索时未命中,则引发缺页故障,从外存中取缺失的页加载到内存中,然后再从刚刚缺页的搜索重新搜索。
7.5 三级Cache支持下的物理内存访问
从CPU产生虚拟地址的时刻开始一直到来自内存数据字到达CPU。Corei7采用四级页表层次结构。
在这里插入图片描述
图7.5.1 Corei7内存系统
在这里插入图片描述
图7.5.2 Corei7地址翻译
具体流程如下:
首先,L1 Cache是8路64组,每块大小为64字节。
由于块大小为64,字节,故b为6位,因为有64组,故s为6位,而VA52bit,所以CT共40bit。
由7.4翻译方式,首先在L1cache中查找页表项,如果有则直接返回。如果不存在,则向下一个缓存搜索,即L2cache,如果有,加载到L1cache中,然后重新搜索页表项;如果没有,则继续在更低级的cache中查找。直到查找到为止。
7.6 hello进程fork时的内存映射
首先在shell中键入./hello。shell首先调用fork函数,copy父进程shell的虚拟内存段,包括当前进程的mm_struct、区域结构和页表等,同时将两个进程的每个页面都标记成可读,并将两个进程中的每个区域结构都标记为私有的写时复制。只不过子进程拥有唯一区别于父进程的PID。
7.7 hello进程execve时的内存映射
execve是在fork子进程后在子进程中调用,它会调用内核中的加载器,并将hello可执行文件的内容先由磁盘加载到内存并替换子进程的虚拟内存的过程。主要步骤如下:
(1) 删除已存在的用户区域。删除子进程虚拟地址的用户部分中的已存在的区域结构。
(2) 映射私有区域。为hello的代码、数据、bss 和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello 文件中的.text和.data 区。bss 区域是请求二进制零的,映射到匿名文件,其大小包含在hello 中。栈和堆区域也是请求二进制零的,初始长度为零。
(3) 映射共享区域。如果hello程序与共享对象(或目标)链接,比如标准C 库libc. so, 那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
(4) 设置程序计数器(PC),execve做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
缺页故障是一种十分常见的异常,是指MMU在翻译虚拟地址时,在页表中未能找到VPN对应的页表项(该页表项还在磁盘中未被加载进内存里),此时就会产生一个缺页信号中断,并且调用缺页中断处理子程序,缺页中断子程序是内核中的代码,会在暂停当前程序的时间里从磁盘中将缺的页加载进物理内存中,然后将控制权又返回给原先的程序,并重新将原先VPN发给MMU,重新查找页表项,最终返回正确的页表项。图示如下:
在这里插入图片描述
图7.8.1 TLB不命中
7.9动态存储分配管理
hello执行中调用的printf函数中就调用的malloc函数。
首先,动态内存分配器维护着一个进程的虚拟内存区域,称为堆。系统之间细节不同,但不失通用性。假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址)。对于每个进程,内核维护着一个变量brk, 它指向堆的顶部。
分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
总的内存管理基本方法与策略如下:
(1)系统调用sbrk函数得到一大段连续的内存空间。
(2)调用malloc从这段空间中分配。若sbrk的空间不够malloc用,则系统继续调用sbrk获取空间。
(3)由于malloc申请空间不一定是连续的,就需要将空闲快组织起来。组织方式用链表,分为显示空闲链表和隐式空闲链表。
(4)再次malloc时需要选择空闲快,有如下策略:
a) 首次适配,按地址遍历,选择第一个满足的空闲快。
b) 最佳适配,空闲链表按块大小组织空闲块,选择满足且最小的空闲块分配。
c) 下一次适配,从上次适配位置开始向后找满足的空闲块。
(5)free掉不用的空闲块。
(6)合并空闲块。共分四种情况:
d) 前后都已分配,则直接释放前块。
e) 前分配,后空闲,则和后合并。
f) 前空闲,后分配,则和前合并。
g) 前后都空闲,和前后一起合并。
分配器风格分为:显示分配器和隐式分配器。
显式分配器:要求应用显式地释放任何已分配的块。
隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。
块的结构又分为简单的堆块和待边界标记的块:
在这里插入图片描述
图7.9.1 简单堆组织
在这里插入图片描述
图7.9.2 带边界块标记
块的组织形式分别对应为隐式链表(使用简单的块格式)和显示链表(使用带边界标记的块格式)。、
匹配方面的策略又分为首次适配,下一次适配和最佳适配。
空闲链表的维护形式又分为:后进先出和按地址顺序访问。
7.10本章小结
虚拟内存能更加有效的管理内存,为系统提供一种有效的堆主存的抽象。它为每个进程提供了一个大的,一致的合私有的地址空间。
CPU产生虚拟地址,MMU将其翻译成物理地址,对虚拟地址空间的管理通动态内存分配器来管理,它像一个系统级程序的应用程序,直接操作内存,在内存需要时申请,在内存不用时释放,通过多种管理策略使内存的利用率和效率达到尽可能最大。
(第7章 2分)

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
所有的IO设备都被模型化为文件,而输入输出都模型化为对文件的读写操作。这样所有的IO设备的操作就只需对Linux内核引出一个简单,低级的应用接口,称为UnixI/O。
8.2 简述Unix IO接口及其函数
UnixI/O接口统一操作:
(1) 打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
(2) Linux shell 创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0) 、标准输出(描述符为1) 和标准错误(描述符为2) 。头文件< unistd.h> 定义了常量STDIN_FILENO 、STOOUT_FILENO 和STDERR_FILENO, 它们可用来代替显式的描述符值。
(3) 改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k, 初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek 操作,显式地设置文件的当前位置为K 。
(4) 读写文件。一个读操作就是从文件复制n>0 个字节到内存,从当前文件位置k 开始,然后将k增加到k+n 。给定一个大小为m 字节的文件,当k~m 时执行读操作会触发一个称为end-of-file(EOF) 的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF 符号” 。
(5) 关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
UnixI/O函数:
(1) int open (char* filename,int flags,mode_t mode) 进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
(2) int close(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函数的代码:
在这里插入图片描述
图8.3.1 printf函数实现
首先参数列表里有参数…:这是可变形参的一种写法,当参数不确定时,使用这种方法表示。
让我们看看va_list arg = (va_list)((char
)(&fmt) + 4);这句:
结果查找,va_list是char
类型,其中(char*)(&fmt) + 4) 表示的是…中的第一个参数。
再看vprintf函数:

  1. int vsprintf(char *buf, const char *fmt, va_list args)
  2. {   
    
  3.  char* p;   
    
  4.  char tmp[256];   
    
  5.  va_list p_next_arg = args;   
    
  6.  for (p=buf;*fmt;fmt++) {   
    
  7.  if (*fmt != '%') {   //先找到%
    
  8.  *p++ = *fmt;   
    
  9. continue;   
    
  10. }   
    
  11. fmt++;   
    
  12. switch (*fmt) {   
    
  13. case 'x':   //先处理x
    
  14. itoa(tmp, *((int*)p_next_arg));   
    
  15. strcpy(p, tmp);   
    
  16. p_next_arg += 4;   
    
  17. p += strlen(tmp);   
    
  18. break;   
    
  19. case 's':   处理s
    
  20. break;   
    
  21. default:   
    
  22. break;   
    
  23. }   
    
  24. }   
    
  25. return (p - buf);   //返回字符串长度
    
  26. }
    这是对printf中的内容进行格式化,最终返回要打印字符串的长度。
    然后看看write函数的汇编形式:
  27. write:
  28.   mov eax, _NR_write   
    
  29.   mov ebx, [esp + 4]   
    
  30.   mov ecx, [esp + 8]   
    
  31.   int INT_VECTOR_SYS_CALL   
    

它首先将各种参数压栈,然后调用int INT_VECTOR_SYS_CALL,即通过系统调用syscall函数。
接着我们进入sys_call函数查看:

  1. sys_call:
  2.  call save   
    
  3.  push dword [p_proc_ready]   
    
  4.  sti   
    
  5.  push ecx   
    
  6.  push ebx   
    
  7. call [sys_call_table + eax * 4]   
    
  8. add esp, 4 * 3   
    
  9. mov [esi + EAXREG - P_STACKBASE], eax   
    
  10. cli   
    
  11. ret   
    

首先,该函数保存我们要输出的已格式化的字符串到显示器区域的缓存中,然后驱动程序在字模库中找到每个字符对应的vram。然后显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器输出每一个RGB分量。最终就在屏幕上显示带输出字符串。
8.4 getchar的实现分析
异步异常-键盘中断的处理:用户在键盘按键后,键盘发送一个中断请求,然后当前进程将控制权转给键盘处理子程序,键盘驱动程序会扫描按键位置,将其翻译成ASCII码,存储在系统的键盘缓冲区。
getchar函数主要调用了read函数,read函数读取键盘缓冲区内的ASCII字符,然后将完整字符串返回。
8.5本章小结
操作系统将输入输出设备抽象为文件,通过对文件的操作来操作输入输出设备,然后通过一些简单的函数统一管理,如:open,close,read,write等来对应用程序进行打开,关闭,读和写操作,以及执行I/O重定向。然后分析了printf函数和getcahr的底层实现。
(第8章1分)
结论
跟随hello的步伐,我们可以感同身受hello那辛苦艰难的一生:
(1)由键盘将hello的源代码键入hello.c的文件中形成原文件,hello的一生就此开始。
(2)hello.c经过预处理器被预处理为hello.i,形成了一个新生儿。
(3)hello.i又经过编译器的编译得到了hello.s这个汇编形式的青少年。
(4)hello.s在汇编器老师的教导下成为一个可以被计算机这个父亲认可的hello.o的二进制形式。
(5)最后由链接器来教会hello.o其他本领,将其与其他链接库链接并完成重定位和符号解析。
就此,hello已经步入了成年,马上就可以走上社会实现自己价值。
(6)hello来到shell这里工作,shell首先为fork一个新的工作空间。
(7)然后shell调用execve来调用加载器,让它加映射虚拟内存,然后调用系统函数来进入 main函数,hello终于进入了工作状态。
(8)在工作中,hello总会感觉自己一个人在独享公司唯一的CPU资源,这得益于策划者对进程概念的提出。
(9)在工作中,hello总会收到来自各个方面的信号,hello是个善良的小伙子,它总会停下手中的事,让别人先完成。
(10)就这样,hello在操作系统这样一个大公司中工作,它的主要任务无非就是调用printf来输出。
(11)当hello完成这个工作时,shell也为它准备好了后路,终止hello进程,调用内核删除掉hello的所有踪迹,让它好好养老。

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

附件
文件的名字 文件的作用
hello.c hello的源程序
hello.i 预处理后得到的文件
hello.s 编译后得到的文件
hello.o 汇编后得到的文件
hello 链接后的可执行文件
hello.txt hello的反汇编代码
helloobjdump.txt hello.o的反汇编代码

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

参考文献
[1] 深入理解计算机系统(原书第3版)/(美)兰德尔·E.布莱恩特等著;龚奕利,贺莲译.—北京:机械工业出版社,2016.7(2018.4重印)
[2] GCC编译详解:https://blog.csdn.net/junmuzi/article/details/50924233
[3] 进程线程详解:https://www.cnblogs.com/reality-soul/p/6397021.html
[4] 逻辑地址,线性地址和物理地址详解:https://blog.csdn.net/erazy0/article/details/6457626
[5] printf函数深度剖析:https://www.cnblogs.com/pianist/p/3315801.html
[6] fork()子进程深度剖析:https://blog.csdn.net/xuyuqingfeng953/article/details/51057685

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值