计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 航天与自动化
学 号 7203610713
班 级 2036016
学 生 郭松言
指 导 教 师 史先俊
计算机科学与技术学院
2022年5月
本文以一个简单程序hello从编写到执行,最后终止,被回收的全过程为例,详细介绍了如预处理、编译、汇编、链接生成可执行文件的全过程,并详略得当地分析了各个过程当中计算机的软件和硬件如何有机地配合。通过这样的方式,详细阐述我们的计算机系统是如何对hello进行进程管理、存储管理和I/O管理,通过对hello一生周期的探索,让我们对计算机系统有更深的了解,最终总结出了有关hello运行的总体性过程。
关键词:操作系统;预处理;编译;汇编;进程;I/O ;
目 录
第1章 概述
1.1 Hello简介
P2P的过程:
预处理: 预处理器(cpp)根据字符#开头的命令,修改原始的程序员用键盘输入的.c后缀的c程序(Program),并把它直接插入程序文本当中,得到了另一个以.i作为文件拓展名的c程序。
编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,其包含了一个汇编语言程序,该程序还包含main函数的定义。汇编器(as)将hello.s翻译成积极语言指令,并且把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目文件hello.o中,其是一个二进制文件。链接器负责处理如printf等存在于其他.o目标文件中的文件,将他们与main.o文件合并在一起,修改重定向条目,得到一个hello文件,它是一个可执行文件,可以被加载到内存之中,由系统进行执行。在得到可执行目标文件hello之后,其被存放在磁盘上,我们可以将其文件名输入到shell中,shell将通过fork的方式,生成一个新的子进程(Process),并且在子进程的上下文中加载并运行这个程序文件。
020的过程:
shell为此子进程execve,映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入 main函数执行目标代码,CPU为运行的hello分配时间片执行逻辑控制流。当程序运行结束后,shell父进程负责回收hello进程,内核删除相关数据结构。
1.2 环境与工具
硬件环境:X64 CPU;3.8 GHz;16G RAM;1T Disk
软件环境:Windows10 64位;Vmware 11;Ubuntu 20.04 LTS 64位/
开发与调试工具:gcc,vim,edb,readelf,HexEdit
1.3 中间结果
文件名称 | 文件作用 |
hello.i | hello.c预处理之后文本文件 |
hello.s | hello.i编译后的汇编文件 |
hello.o | hello.s汇编之后的可重定位目标文件 |
hello | 链接之后的可执行目标文件 |
hello.out | hello反汇编之后的可重定位文件 |
hello_elf | hello的elf格式文件 |
Hello_objdump | hello的反汇编文件 |
Hello_o_objdump | hello.o的反汇编文件,带有重定位条目标注信息 |
1.4 本章小结
本章以hello.c为例,主要简单介绍了 hello 的 p2p,020 过程,列出了本次实验信息:环境、中间结果,并且大致简介了hello程序从c程序hello.c到可执行目标文件hello的大致经过的历程以及所需要的运行环境和工具,以及产生的中间文件名和作用。
第2章 预处理
2.1 预处理的概念与作用
2.1.1预处理概念:
预处理器cpp根据以字符#开头的命令(宏定义、条件编译),修改原始的C程序,将引用的所有库展开合并成为一个完整的文本文件。
2.1.2预处理阶段作用:
1.处理宏定义指令预处理器根据#if和#ifdef等编译命令及其后的条件,将源程序中的某部分包含进来或排除在外,通常把排除在外的语句转换成空行。
2. 处理条件编译指令
条件编译指令如#ifdef,#ifndef,#else,#elif,#endif等。 这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉。
3.处理头文件包含指令头文件包含指令如#include "FileName"或者#include 等。 该指令将头文件中的定义统统都加入到它所产生的输出文件中,以供编译程序对之进行处理。
4.处理特殊符号
预编译程序可以识别一些特殊的符号。 例如在源程序中出现的LINE标识将被解释为当前行号(十进制数),FILE则被解释为当前被编译的C源程序的名称。预编译程序对于在源程序中出现的这些串将用合适的值进行替换。
2.1.3总结
预处理阶段,程序将所有的#define删除,并展开所有的宏定义;处理所有的条件预编译指令,比如#if, #ifdef, #elif等;处理#include预编译指令,将被包含的文件插入到该预编译指令所在的位置,这个过程是递归进行的;删除所有的注释;添加行号和文件名标识,以便于编译时编译器产生条实用的行号与代码段的映射信息;保留所有的#pragma编译器指令。
当我们无法确定宏定义是否正确,或者包含的头文件是否正确的时候,可以查看预编译后的文件来定位问题所在。
2.2在Ubuntu下预处理的命令
命令:gcc hello.c -E -o hello.i
2.3 Hello的预处理结果解析
处理前:
处理后:
被展开成3000余行
这是因为源程序hello.c中有如下三条预处理指令:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
这三条预处理指令告诉预处理器读取系统头文件stdio.h、unistd.h、stdlib.h的内容,并把它们插入到程序文本中,一般包括结构体的定义(typedef struct)、函数声明(extern)、对引用目录的标注(# 28 “/usr/include/stdio.h” 2 3 4)等内容。若源程序中有#define预处理指令还会对相应的符号进行替换,或者其他类型的预处理指令,预处理器也会执行对于的操作。
我们再看源程序的其他部分,预处理得到的hello.i中的main函数和源程序保持一致,因为预处理器不会对其他部分进行修改。
2.4 本章小结
本章主要介绍了预处理的概念与作用,执行预处理的命令,并结合hello.i文件,解析了hello的预处理结果
预处理过程是之后所有操作的基础,是不可或缺的重要过程
第3章 编译
3.1 编译的概念与作用
编译的概念:编译器将文本文件 hello.i 翻译成文本文件 hello.s,它包含一个汇编语言程序。其以高级程序设计语言书写的源程序作为输入,而以汇编语言或机器语言表示的目标程序作为输出。 这个过程称为编译,同时也是编译的作用。
编译的作用就是将高级语言源程序翻译成等价的目标程序,并且进行语法检查、调试措施、修改手段、覆盖处理、目标程序优化等步骤。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
3.3.0汇编指令的介绍
.file:声明源文件
.text:代码节
.section:
.rodata:只读代码段
.align:数据或者指令的地址对其方式
.string:声明一个字符串(.LC0,.LC1)
.global:声明全局变量(main)
.type:声明一个符号是数据类型还是函数类型
3.3.1 数据
1.字符串
按照文件顺序,首先是:
程序中有两个字符串,由上图可知,这两个字符串都在只读数据段中,分别如图所示。这两个字符串作为printf函数的参数。
2.局部变量i:
3.main函数
参数 argc 作为用户传给main的参数。也是被放到了堆栈中。
4.各种立即数
立即数直接体现在汇编代码中
5.数组:char *argv[]
hello.c中唯一的数组是作为main函数的第二个参数,数组的每个元素都是一个指向字符类型的指针。数组的起始地址存放在栈中-32(%rbp)的位置,被两次调用找参数传给printf
红色方框标注的是:数组argv的存放的位置:-32(%rbp)
蓝色方框标注的是:分别获取argv[1]和argv[2]的地址
3.3.2.全局函数
由hello.c可知,hello.c声明了一个全局函数int main(int argc,char *argv[]),经过编译之后,main函数中使用的字符串常量也被存放在数据区。
这段汇编代码说明main函数是全局函数
3.3.3赋值操作
程序中的赋值操作主要有:i=0这条赋值操作在汇编代码主要使用mov指令来实现,而根据数据的类型又有好几种不一样的后缀
movb:一个字节
movw:两个字节
movl:四个字节
movq:八个字节
3.3.4算数操作
hello.c中的算数操作有:i++,由于是i是int类型的,因此汇编代码只用addl就能实现其他的操作有
指令 | 效果 |
leaq S,D | D=&S |
INC D | D+=1 |
DEC D | D-=1 |
NEG D | D=-D |
ADD S,D | D=D+S |
SUB s,D | D=D-S |
3.3.5关系操作
- argc!=3;是在一条件语句中的条件判断:argc!=3,进行编译时,这条指令被编译为:cmpl $3,-20(%rbp),同时这条cmpl的指令还有设置条件码的作用,当根据条件码来判断是否需要跳转到分支中。
- i<8,在hello.c作为判断循环条件,在汇编代码被编译为:cmpl $7,-4(%rbp),计算 i-7然后设置 条件码,为下一步 jle 利用条件码进行跳转做准备。
3.3.6控制转移指令
汇编语言中首先设置条件码,然后根据条件码来进行控制转移,在hello.c中,有以下控制转移指令:
(1)判断i是否为3,如果i等于3,则不执行if语句,否则执行if语句,对应的汇编代码为
(2)for(i=0;i<8;i++),通过每次判断i是否满足小于8来判断是否需要跳转至循环语句中,对应的汇编代码为:
第一个红色方框:首先i赋初值0,然后无条件跳转至判断条件的代码中,即.L3.
第二个红色方框:判断i是否符合循环的条件,符合直接跳转至.L4,也就是循环体的内部.
3.3.7函数操作
调用函数时有以下操作:(假设函数P调用函数Q)
(1)传递控制:进行过程 Q 的时候,程序计数器必须设置为 Q 的代码的起始 地址,然后在返回时,要把程序计数器设置为 P 中调用 Q 后面那条指令的 地址。
(2)传递数据:P 必须能够向 Q 提供一个或多个参数,Q 必须能够向 P 中返回 一个值。
(3) 分配和释放内存:在开始时,Q 可能需要为局部变量分配空间,而在返回 前,又必须释放这些空间。
hello.C涉及的函数操作有:
main函数,printf,exit,sleep ,getchar函数
main函数的参数是argc和argv;两次printf函数的参数恰好是那两个字符串
exit参数是1,sleep函数参数是atoi(argv[3]),函数的返回值存储在%eax寄存器中。
3.3.8类型转换
hello.c中涉及的类型转换是:atoi(argv[3]),将字符串类型转换为整数类型其他的类型转换还有int、float、double、short、char之间的转换
3.4 本章小结
本章介绍了编译的概念与作用,以及hello.i文件转化为hello.s文件的过程。同时还对其中的汇编代码中C语言的数据和操作进行了解析。
第4章 汇编
4.1 汇编的概念与作用
汇编的概念:汇编器(as)将.s汇编程序翻译成机器语言,把这些机器语言指令打包成可重定位目标程序的格式,并将结果保存在.o目标文件中.
汇编的作用:翻译生成机器语言,因为机器语言是计算机能直接识别和执行的一种语言.
4.2 在Ubuntu下汇编的命令
命令gcc -c hello.s -o hello.o
4.3 可重定位目标elf格式
4.3.1Elf头
描述生成该文件的系统的字的大小和字节顺序
命令:readelf -h hello.o
以 16B 的序列 Magic 开始,Magic 描述了生成该文件的系统的字的大小和字节顺序,ELF 头剩下的部分包含帮助链接器语法分析和解 ## 标题释目标文件的信息,其中包括 ELF 头的大小、目标文件的类型、机器类型、 字节头部表(section header table)的文件偏移,以及节头部表中条目的大 小和数量等信息。根据头文件的信息,可以知道该文件是可重定位目标文件,有13个节。
4.3.2节头部
表描述了不同节的位置和大小,目标文件中每个节都有固定大小的条目。
命令:readelf -S hello.o
节头部表,包含了文件中出现的各个节的语义,包括节的类型、位置和大小等信息。由于是可重定位目标文件,所以每个节都从0开始,用于重定位。在文件头中得到节头表的信息,然后再使用节头表中的字节偏移信息得到各节在文件中的起始位置,以及各节所占空间的大小,同时可以观察到,代码是可执行的,但是不能写;数据段和只读数据段都不可执行,而且只读数据段也不可写。
4.3.3符号表
存放程序中定义和引用的函数和全局变量的信息。命令readelf -s hello.o
name是符号名称,对于可冲定位目标模块,value是符号相对于目标节的起始位置偏移,对于可执行目标文件,该值是一个绝对运行的地址。size是目标的大小,type要么是数据要么是函数。Bind字段表明符号是本地的还是全局的。
4.3.4重定位节
节包含了要在链结中重定位的项目
命令:objdump -r hello.o
Offset:需要被修改的引用节的偏移Info:包括symbol和type两个部分,symbol在前面四个字节,type在后面四个字节。
symbol:标识被修改引用应该指向的符号
type:重定位的类型
Type:告知链接器应该如何修改新的应用
Attend:一个有符号常数,一些重定位要使用它对被修改引用的值做偏移调整Name:重定向到的目标的名称
4.4 Hello.o的结果解析
命令:objdump -d -r hello.o
分析hello.o的反汇编,并与第3章的 hello.s进行对照分析。
Hello.s
通过反汇编的代码和hello.s进行比较,发现汇编语言的指令并没有什么不同的地方,只是反汇编代码所显示的不仅仅是汇编代码,还有机器代码,机器语言程序的是二进制机器指令的集合,是纯粹的二进制数据表示的语言,是电脑可以真正识别的语言。机器指令由操作码和操作数构成,汇编语言是人们比较熟悉的词句直接表述CPU动作形成的语言,是最接近CPU运行原理的语言。每一条汇编语言操作码都可以用机器二进制数据来表示,进而可以将所有的汇编语言(操作码和操作数)和二进制机器语言建立一一映射的关系,因此可以将汇编语言转化为机器语言,通过对机器代码的分析可以看出一下不同的地方。
(1)分支转移:反汇编的跳转指令用的不是段名称比如.L3,二是用的确定的地址,因为,因为段名称只是在汇编语言中便于编写的助记符,所以在汇编成机器语言之后显然不存在,而是确定的地址。
(2)函数调用:在.s 文件中,函数调用之后直接跟着函数名称,而在反汇编程 序中,call的目标地址是当前下一条指令。这是因为 hello.c 中调用的函数 都是共享库中的函数,最终需要通过动态链接器才能确定函数的运行时执 行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其call指令后的相对地址设置为全0(目标地址正是下一条指令),然后在.rela.text 节中为其添加重定位条目,等待静态链接的进一步确定。
4.5 本章小结
本章经历了hello从hello.s到hello.o的汇编过程,查看了hello.o的elf,使用objdump工具得到反汇编代码,和之前的汇编代码进行比较,通过寻找不同之处分析了从汇编语言到机器语言的映射关系 。
第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.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
5.3.1 .ELF Header
命令:readelf -a hello > 5.3helloelf.txt
ELF 头:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
类别: ELF64
数据: 2 补码,小端序 (little endian)
Version: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
类型: EXEC (可执行文件)
系统架构: Advanced Micro Devices X86-64
版本: 0x1
入口点地址: 0x4010f0
程序头起点: 64 (bytes into file)
Start of section headers: 14200 (bytes into file)
标志: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 12
Size of section headers: 64 (bytes)
Number of section headers: 27
Section header string table index: 26
hello的文件头和hello.o文件头的不同之处如上述加粗所示,Type类型为EXEC表明hello是一个可执行目标文件,有27个节
5.3.2 Section Headers
节头:
[号] 名称 类型 地址 偏移量
大小 全体大小 旗标 链接 信息 对齐
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 00000000004002e0 000002e0
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.gnu.propert NOTE 0000000000400300 00000300
0000000000000020 0000000000000000 A 0 0 8
[ 3] .note.ABI-tag NOTE 0000000000400320 00000320
0000000000000020 0000000000000000 A 0 0 4
[ 4] .hash HASH 0000000000400340 00000340
0000000000000038 0000000000000004 A 6 0 8
[ 5] .gnu.hash GNU_HASH 0000000000400378 00000378
000000000000001c 0000000000000000 A 6 0 8
[ 6] .dynsym DYNSYM 0000000000400398 00000398
00000000000000d8 0000000000000018 A 7 1 8
[ 7] .dynstr STRTAB 0000000000400470 00000470
000000000000005c 0000000000000000 A 0 0 1
[ 8] .gnu.version VERSYM 00000000004004cc 000004cc
0000000000000012 0000000000000002 A 6 0 2
[ 9] .gnu.version_r VERNEED 00000000004004e0 000004e0
0000000000000020 0000000000000000 A 7 1 8
[10] .rela.dyn RELA 0000000000400500 00000500
0000000000000030 0000000000000018 A 6 0 8
[11] .rela.plt RELA 0000000000400530 00000530
0000000000000090 0000000000000018 AI 6 21 8
[12] .init PROGBITS 0000000000401000 00001000
000000000000001b 0000000000000000 AX 0 0 4
[13] .plt PROGBITS 0000000000401020 00001020
0000000000000070 0000000000000010 AX 0 0 16
[14] .plt.sec PROGBITS 0000000000401090 00001090
0000000000000060 0000000000000010 AX 0 0 16
[15] .text PROGBITS 00000000004010f0 000010f0
0000000000000145 0000000000000000 AX 0 0 16
[16] .fini PROGBITS 0000000000401238 00001238
000000000000000d 0000000000000000 AX 0 0 4
[17] .rodata PROGBITS 0000000000402000 00002000
000000000000003b 0000000000000000 A 0 0 8
[18] .eh_frame PROGBITS 0000000000402040 00002040
00000000000000fc 0000000000000000 A 0 0 8
[19] .dynamic DYNAMIC 0000000000403e50 00002e50
00000000000001a0 0000000000000010 WA 7 0 8
[20] .got PROGBITS 0000000000403ff0 00002ff0
0000000000000010 0000000000000008 WA 0 0 8
[21] .got.plt PROGBITS 0000000000404000 00003000
0000000000000048 0000000000000008 WA 0 0 8
[22] .data PROGBITS 0000000000404048 00003048
0000000000000004 0000000000000000 WA 0 0 1
[23] .comment PROGBITS 0000000000000000 0000304c
0000000000000023 0000000000000001 MS 0 0 1
[24] .symtab SYMTAB 0000000000000000 00003070
00000000000004c8 0000000000000018 25 30 8
[25] .strtab STRTAB 0000000000000000 00003538
0000000000000158 0000000000000000 0 0 1
[26] .shstrtab STRTAB 0000000000000000 00003690
00000000000000e1 0000000000000000 0 0 1
5.3.3 重定位节.rela.text
重定位节 '.rela.dyn' at offset 0x500 contains 2 entries:
偏移量 信息 类型 符号值 符号名称 + 加数
000000403ff0 000300000006 R_X86_64_GLOB_DAT 0000000000000000 __libc_start_main@GLIBC_2.2.5 + 0
000000403ff8 000500000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
重定位节 '.rela.plt' at offset 0x530 contains 6 entries:
偏移量 信息 类型 符号值 符号名称 + 加数
000000404018 000100000007 R_X86_64_JUMP_SLO 0000000000000000 puts@GLIBC_2.2.5 + 0
000000404020 000200000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0
000000404028 000400000007 R_X86_64_JUMP_SLO 0000000000000000 getchar@GLIBC_2.2.5 + 0
000000404030 000600000007 R_X86_64_JUMP_SLO 0000000000000000 atoi@GLIBC_2.2.5 + 0
000000404038 000700000007 R_X86_64_JUMP_SLO 0000000000000000 exit@GLIBC_2.2.5 + 0
000000404040 000800000007 R_X86_64_JUMP_SLO 0000000000000000 sleep@GLIBC_2.2.5 + 0
The decoding of unwind sections for machine type Advanced Micro Devices X86-64 is not currently supported.
5.3.4 符号表.symtab
Symbol table '.dynsym' contains 9 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5 (2)
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@GLIBC_2.2.5 (2)
3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND getchar@GLIBC_2.2.5 (2)
5: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
6: 0000000000000000 0 FUNC GLOBAL DEFAULT UND atoi@GLIBC_2.2.5 (2)
7: 0000000000000000 0 FUNC GLOBAL DEFAULT UND exit@GLIBC_2.2.5 (2)
8: 0000000000000000 0 FUNC GLOBAL DEFAULT UND sleep@GLIBC_2.2.5 (2)
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
通过查看edb,看出hello的虚拟地址空间开始于0x401000,结束与0x401ff0,如下图
根据下图的节头部表,可以通过edb找到各个节的信息,比如.txt节,虚拟地址开始于0x4010f0,大小为0x132.
5.5 链接的重定位过程分析
命令:objdump -d -r hello
5.5.1 生成的反汇编代码
Disassembly of section .init:
0000000000401000 <_init>:
401000: f3 0f 1e fa endbr64
401004: 48 83 ec 08 sub $0x8,%rsp
401008: 48 8b 05 e9 2f 00 00 mov 0x2fe9(%rip),%rax # 403ff8 <__gmon_start__>
40100f: 48 85 c0 test %rax,%rax
401012: 74 02 je 401016 <_init+0x16>
401014: ff d0 callq *%rax
401016: 48 83 c4 08 add $0x8,%rsp
40101a: c3 retq
Disassembly of section .plt:
0000000000401020 <.plt>:
401020: ff 35 e2 2f 00 00 pushq 0x2fe2(%rip) # 404008 <_GLOBAL_OFFSET_TABLE_+0x8>
401026: f2 ff 25 e3 2f 00 00 bnd jmpq *0x2fe3(%rip) # 404010 <_GLOBAL_OFFSET_TABLE_+0x10>
40102d: 0f 1f 00 nopl (%rax)
401030: f3 0f 1e fa endbr64
401034: 68 00 00 00 00 pushq $0x0
401039: f2 e9 e1 ff ff ff bnd jmpq 401020 <.plt>
40103f: 90 nop
401040: f3 0f 1e fa endbr64
401044: 68 01 00 00 00 pushq $0x1
401049: f2 e9 d1 ff ff ff bnd jmpq 401020 <.plt>
40104f: 90 nop
401050: f3 0f 1e fa endbr64
401054: 68 02 00 00 00 pushq $0x2
401059: f2 e9 c1 ff ff ff bnd jmpq 401020 <.plt>
40105f: 90 nop
401060: f3 0f 1e fa endbr64
401064: 68 03 00 00 00 pushq $0x3
401069: f2 e9 b1 ff ff ff bnd jmpq 401020 <.plt>
40106f: 90 nop
401070: f3 0f 1e fa endbr64
401074: 68 04 00 00 00 pushq $0x4
401079: f2 e9 a1 ff ff ff bnd jmpq 401020 <.plt>
40107f: 90 nop
401080: f3 0f 1e fa endbr64
401084: 68 05 00 00 00 pushq $0x5
401089: f2 e9 91 ff ff ff bnd jmpq 401020 <.plt>
40108f: 90 nop
Disassembly of section .plt.sec:
0000000000401090 <puts@plt>:
401090: f3 0f 1e fa endbr64
401094: f2 ff 25 7d 2f 00 00 bnd jmpq *0x2f7d(%rip) # 404018 <puts@GLIBC_2.2.5>
40109b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
00000000004010a0 <printf@plt>:
4010a0: f3 0f 1e fa endbr64
4010a4: f2 ff 25 75 2f 00 00 bnd jmpq *0x2f75(%rip) # 404020 <printf@GLIBC_2.2.5>
4010ab: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
00000000004010b0 <getchar@plt>:
4010b0: f3 0f 1e fa endbr64
4010b4: f2 ff 25 6d 2f 00 00 bnd jmpq *0x2f6d(%rip) # 404028 <getchar@GLIBC_2.2.5>
4010bb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
00000000004010c0 <atoi@plt>:
4010c0: f3 0f 1e fa endbr64
4010c4: f2 ff 25 65 2f 00 00 bnd jmpq *0x2f65(%rip) # 404030 <atoi@GLIBC_2.2.5>
4010cb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
00000000004010d0 <exit@plt>:
4010d0: f3 0f 1e fa endbr64
4010d4: f2 ff 25 5d 2f 00 00 bnd jmpq *0x2f5d(%rip) # 404038 <exit@GLIBC_2.2.5>
4010db: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
00000000004010e0 <sleep@plt>:
4010e0: f3 0f 1e fa endbr64
4010e4: f2 ff 25 55 2f 00 00 bnd jmpq *0x2f55(%rip) # 404040 <sleep@GLIBC_2.2.5>
4010eb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
Disassembly of section .text:
00000000004010f0 <_start>:
4010f0: f3 0f 1e fa endbr64
4010f4: 31 ed xor %ebp,%ebp
4010f6: 49 89 d1 mov %rdx,%r9
4010f9: 5e pop %rsi
4010fa: 48 89 e2 mov %rsp,%rdx
4010fd: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp
401101: 50 push %rax
401102: 54 push %rsp
401103: 49 c7 c0 30 12 40 00 mov $0x401230,%r8
40110a: 48 c7 c1 c0 11 40 00 mov $0x4011c0,%rcx
401111: 48 c7 c7 25 11 40 00 mov $0x401125,%rdi
401118: ff 15 d2 2e 00 00 callq *0x2ed2(%rip) # 403ff0 <__libc_start_main@GLIBC_2.2.5>
40111e: f4 hlt
40111f: 90 nop
0000000000401120 <_dl_relocate_static_pie>:
401120: f3 0f 1e fa endbr64
401124: c3 retq
0000000000401125 <main>:
401125: f3 0f 1e fa endbr64
401129: 55 push %rbp
40112a: 48 89 e5 mov %rsp,%rbp
40112d: 48 83 ec 20 sub $0x20,%rsp
401131: 89 7d ec mov %edi,-0x14(%rbp)
401134: 48 89 75 e0 mov %rsi,-0x20(%rbp)
401138: 83 7d ec 04 cmpl $0x4,-0x14(%rbp)
40113c: 74 16 je 401154 <main+0x2f>
40113e: 48 8d 3d c3 0e 00 00 lea 0xec3(%rip),%rdi # 402008 <_IO_stdin_used+0x8>
401145: e8 46 ff ff ff callq 401090 <puts@plt>
40114a: bf 01 00 00 00 mov $0x1,%edi
40114f: e8 7c ff ff ff callq 4010d0 <exit@plt>
401154: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
40115b: eb 48 jmp 4011a5 <main+0x80>
40115d: 48 8b 45 e0 mov -0x20(%rbp),%rax
401161: 48 83 c0 10 add $0x10,%rax
401165: 48 8b 10 mov (%rax),%rdx
401168: 48 8b 45 e0 mov -0x20(%rbp),%rax
40116c: 48 83 c0 08 add $0x8,%rax
401170: 48 8b 00 mov (%rax),%rax
401173: 48 89 c6 mov %rax,%rsi
401176: 48 8d 3d b1 0e 00 00 lea 0xeb1(%rip),%rdi # 40202e <_IO_stdin_used+0x2e>
40117d: b8 00 00 00 00 mov $0x0,%eax
401182: e8 19 ff ff ff callq 4010a0 <printf@plt>
401187: 48 8b 45 e0 mov -0x20(%rbp),%rax
40118b: 48 83 c0 18 add $0x18,%rax
40118f: 48 8b 00 mov (%rax),%rax
401192: 48 89 c7 mov %rax,%rdi
401195: e8 26 ff ff ff callq 4010c0 <atoi@plt>
40119a: 89 c7 mov %eax,%edi
40119c: e8 3f ff ff ff callq 4010e0 <sleep@plt>
4011a1: 83 45 fc 01 addl $0x1,-0x4(%rbp)
4011a5: 83 7d fc 07 cmpl $0x7,-0x4(%rbp)
4011a9: 7e b2 jle 40115d <main+0x38>
4011ab: e8 00 ff ff ff callq 4010b0 <getchar@plt>
4011b0: b8 00 00 00 00 mov $0x0,%eax
4011b5: c9 leaveq
4011b6: c3 retq
4011b7: 66 0f 1f 84 00 00 00 nopw 0x0(%rax,%rax,1)
4011be: 00 00
00000000004011c0 <__libc_csu_init>:
4011c0: f3 0f 1e fa endbr64
4011c4: 41 57 push %r15
4011c6: 4c 8d 3d 83 2c 00 00 lea 0x2c83(%rip),%r15 # 403e50 <_DYNAMIC>
4011cd: 41 56 push %r14
4011cf: 49 89 d6 mov %rdx,%r14
4011d2: 41 55 push %r13
4011d4: 49 89 f5 mov %rsi,%r13
4011d7: 41 54 push %r12
4011d9: 41 89 fc mov %edi,%r12d
4011dc: 55 push %rbp
4011dd: 48 8d 2d 6c 2c 00 00 lea 0x2c6c(%rip),%rbp # 403e50 <_DYNAMIC>
4011e4: 53 push %rbx
4011e5: 4c 29 fd sub %r15,%rbp
4011e8: 48 83 ec 08 sub $0x8,%rsp
4011ec: e8 0f fe ff ff callq 401000 <_init>
4011f1: 48 c1 fd 03 sar $0x3,%rbp
4011f5: 74 1f je 401216 <__libc_csu_init+0x56>
4011f7: 31 db xor %ebx,%ebx
4011f9: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
401200: 4c 89 f2 mov %r14,%rdx
401203: 4c 89 ee mov %r13,%rsi
401206: 44 89 e7 mov %r12d,%edi
401209: 41 ff 14 df callq *(%r15,%rbx,8)
40120d: 48 83 c3 01 add $0x1,%rbx
401211: 48 39 dd cmp %rbx,%rbp
401214: 75 ea jne 401200 <__libc_csu_init+0x40>
401216: 48 83 c4 08 add $0x8,%rsp
40121a: 5b pop %rbx
40121b: 5d pop %rbp
40121c: 41 5c pop %r12
40121e: 41 5d pop %r13
401220: 41 5e pop %r14
401222: 41 5f pop %r15
401224: c3 retq
401225: 66 66 2e 0f 1f 84 00 data16 nopw %cs:0x0(%rax,%rax,1)
40122c: 00 00 00 00
0000000000401230 <__libc_csu_fini>:
401230: f3 0f 1e fa endbr64
401234: c3 retq
Disassembly of section .fini:
0000000000401238 <_fini>:
401238: f3 0f 1e fa endbr64
40123c: 48 83 ec 08 sub $0x8,%rsp
401240: 48 83 c4 08 add $0x8,%rsp
401244: c3 retq
5.5.2 hello的反汇编代码中不同节的含义:
5.5.3 hello重定位的过程:
(1)重定位节和符号定义链接器将所有类型相同的节合并在一起后,这个节就作为可执行目标文件的节。然后链接器把运行时的内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号,当这一步完成时,程序中每条指令和全局变量都有唯一运行时的地址。
(2)重定位节中的符号引用这一步中,连接器修改代码节和数据节中对每个符号的引用,使他们指向正确的运行时地址。执行这一步,链接器依赖于可重定位目标模块中称为的重定位条目的数据结构。
(3)重定位条目当编译器遇到对最终位置未知的目标引用时,它就会生成一个重定位条目。代码的重定位条目放在.rela.txt(在上一节)。
5.5.4 hello的反汇编代码和hello.o反汇编代码的区别
hello的反汇编代码从.init节开始,而hello.o的反汇编代码从.text节开始。
hello的反汇编代码中导入了puts、printf、atoi、getchar、sleep等在主程序中使用过的函数,而hello.o的反汇编代码中不包含这些函数。
hello的反汇编代码中函数的调用方法同hello.s,使用call + 函数名直接调用,而hello.o的反汇编代码使用call指向下一条语句,并未直接调用函数。
5.6 hello的执行流程
子程序名 | 程序地址 |
init; | 0x401430 |
puts@plt; | 0x401460 |
printf@plt; | 0x401470 |
__libc_start_main@plt; | 0x401480 |
getchar@plt; | 0x401490 |
exit@plt; | 0x4014a0 |
sleep@plt; | 0x4014b0 |
start; | 0x4014d0 |
main; | 0x4014fe |
__libc_csu_init; | 0x401580 |
__libc_csu_fini; | 0x4015f0 |
_fini; | 0x4015f4 |
5.7 Hello的动态链接分析
5.7.1分析
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。虽然动态链接把链接过程推迟到了程序运行时,但是在形成可执行文件时(注意形成可执行文件和执行程序是两个概念),还是需要用到动态链接库。比如我们在形成可执行程序时,发现引用了一个外部的函数,此时会检查动态链接库,发现这个函数名是一个动态链接符号,此时可执行程序就不对这个符号进行重定位,而把这个过程留到装载时再进行。
在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。GNU编译系统使用延迟绑定(lazybinding),将过程地址的绑定推迟到第一次调用该过程时。
延迟绑定是通过GOT和PLT实现的。GOT是数据段的一部分,而PLT是代码段的一部分。两表内容分别为:
PLT:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。
GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。
5.7.2
根据hello ELF文件可知:
[21] .got.plt PROGBITS 0000000000404000 00003000
0000000000000048 0000000000000008 WA 0 0 8
GOT表位置在调用dl_init之前0x601008后的16个字节均为0:
调用_start之后发生改变,0x404008后的两个8个字节分别变为:0x7f6f8dc46170、0x7f6f8da34750,其中GOT[O](对应0x404e28)和GOT[1](对应0x7fb06087e168)
包含动态链接器在解析函数地址时会使用的信息。GOT[2](对应0x7fb06066e870)是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,改变后的GOT表如下:
GOT[2]对应部分是共享库模块的入口点,如下:
举例puts函数在调用puts函数前对应GOT条目指向其对应的PLT条目的第二条指令,如图puts@plt指令跳转的地址
调用puts函数前(链接前)PLT函数:
可以看出其对应GOT条目初始时指向其PLT条目的第二条指令的地址。puts函数执行后在查看此处地址:
可以看出其已经动态链接,GOT条目已经改变。
5.8 本章小结
在本章中主要介绍了链接的概念与作用,并且详细阐述了hello.o是怎么链接成为一个可执行目标文件的过程,详细介绍了hello.o的ELF格式和各个节的含义,并且分析了hello的虚拟地址空间、重定位过程、执行流程、动态链接过程。
第6章 hello进程管理
6.1 进程的概念与作用
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
6.2 简述壳Shell-bash的作用与处理流程
为使用者提供操作界面,具备命令行解释器的作用,作为系统和用户之间的接口
流程:
获取命令行用户输入的命令,若是内置命令,执行内置命令
若非内置命令,则为可执行程序
创建子进程,运行该可执行程序
若在前台运行,等待程序在前台运行完毕,回收
若在后台运行,不等待程序运行,运行结束时回收
6.3 Hello的fork进程创建过程
在终端中键入./hello 118030XXXX XX 3,运行的终端程序会对输入的命令行进行解析,因为hello 不是一个内置的shell 命令。所以解析之后终端程序判断./hello 的语义为执行当前目录下的可执行目标文件hello,之后终端程序首先会调用fork 函数创建一个新的运行的子进程,新创建的子进程几乎但不完全与父进程相同,子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,这就意味着,当父进程调用fork 时,子进程可以读写父进程中打开的任何文件。父进程与子进程之间最大的区别在于它们拥有不同的PID。
6.4 Hello的execve过程
子进程调用execve函数,加载并运行hello程序,加载器删除子进程所有的内存段,并且创建新的代码、数据、堆和栈段,它们被初始化为0
6.5 Hello的进程执行
上下文信息:上下文程序正确运行所需要的状态,包括存放在内存中的程序的代码和数据,用户栈、用寄存器、程序计数器、环境变量和打开的文件描述符的集合构成
用户模式和内核模式:处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,这个模式中,硬件防止特权指令的执行,并对内存和I/O空间的访问操作进行检查.设置模式位时,进程处于内核模式,一切程序都可运行.任务可以执行特权级指令,对任何I/O设备有全部的访问权,还能够访问任何虚地址和控制虚拟内存硬件
进程执行分析:Hello起初在用户模式下运行,在hello进程调用sleep之后转入内核模式,内核休眠,并将hello进程从运行队列加入等待队列,定时器开始计时2s,当定时器到时,发送一个中断信号,此时进入内核状态执行中断处理,将hello进程从等待队列中移出重新加入到运行队列, hello进程继续执行。
6.6 hello的异常与信号处理
6.6.1异常和信号异常
(1)异常和信号异常可以分为四类:中断、陷阱、故障、终止,各自的属性如下:
类别 | 原因 | 异步\同步 | 返回行为 |
中断 | 来自I/O设备的信号 | 异步 | 总是返回到下一条指令 |
陷阱 | 有意的异常. | 同步 | 总是返回到下一条指令 |
故障 | 潜在可恢复的错误 | 同步 | 可能返回到当前指令 |
终止 | 不可恢复的错误 | 同步 | 不会返回 |
hello程序出现的异常可能有:
中断:在hello程序执行的过程中可能会出现外部I/O设备引起的异常。
陷阱:陷阱是有意的异常,是执行一条指令的结果,hello执行sleep函数的时候会出现这个异常。
故障:在执行hello程序的时候,可能会发生缺页故障。
终止:终止时不可恢复的错误,在hello执行过程可能会出现DRAM或者SRAM位损坏的奇偶错误。
在发生异常时会发出信号,比如缺页故障会导致OS发生SIGSEGV信号给用户进程,而用户进程以段错误退出。常见信号种类如下表所示。
ID | 名称 | 默认行为 | 相应事件 |
2 | SIGINT | 终止 | 来白键盘的中断 |
9 | SIGKILL | 终止 | 杀死程序(该信号不能被捕获不能被忽略) |
11 | SIGSEGV | 终止 | 无效的内存引用(段故障) |
14 | SIGSEGV | 终止 | 来自alarm函数的定时器信号 |
17 | SIGSEGV | 忽略 | 一个子进程停止或者终止 |
6.6.2键盘上各种操作导致的异常
1.正常执行hello程序的结果:
输入:./hello 7203610713 郭松言 0.5
- 按下 ctrl-z 的结果:
输入ctrl-z默认结果是挂起前台的作业,hello进程并没有回收,而是运行在后台下:
如图所示用ps命令可以看到,hello进程并没有被回收,此时他的后台 job 号是 1:
调用 fg 1 将其调到前台,此时 shell 程序首先打印 hello 的命令行命令, hello 继续运行打印剩下的 8 条 info,之后输入字串,程序结束,同时进程被回收:
- 下图是按下Ctrl+c的结果,在键盘上输入Ctrl+c会导致内核发送一个SIGINT信号到前台进程组的每个进程,默认情况是终止前台作业:
用ps查看前台进程组发现没有hello进程。:
- 程序运行过程中按键盘,不停乱按,结果如图,可以发现,乱按只是将屏幕的输入缓存到 stdin,当 getchar 的时候读出一个’\n’结尾的字串(作为一次输入),其他字串会当做 shell 命令行输入:
6.7本章小结
在本章中,阐述进程的定义与作用,同时介绍了 Shell 的一般处理流程和作用,并且着重分析了调用 fork 创建新进程,调用 execve函数 执行 hello,hello的进程执行,以及hello 的异常与信号处理。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:在有地址变换功能的计算机中,访内指令给出的地址 (操作数) 叫逻辑地址,也叫相对地址。要经过寻址方式的计算或变换才得到内存储器中的实际有效地址,即物理地址。是hello.o中的相对偏移地址。
线性地址:线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。
虚拟地址:程序访问存储器所使用的逻辑地址称为虚拟地址。是hello里的虚拟内存地址。
物理地址:在存储器里以字节为单位存储信息,为正确地存放或取得信息,每一个字节单元给以一个唯一的存储器地址,称为物理地址。是hello里虚拟内存地址对应的物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址=段选择符 + 偏移地址
线性地址 = 段基址 + 偏移地址
逻辑地址到线性地址的变换——段式管理的关键在于;段选择符à段基址
变换在处理器的两种寻址模式——实模式和保护模式下有所不同:
实模式:
段基址 = 段选择符 * 16
线性地址 = 段选择符 * 16 + 偏移地址
保护模式:
保护模式是现代计算机常用的寻址模式。保护模式下,段选择符作为一个索引,到一个称为描述符表的数据结构中读取段基址。此时,16位的段选择符也被划分成了几个部分,予以不同的解释:
TI=0,选择全局描述符表(GDT),TI=1,选择局部描述符表(LDT)。
RPL=00,为第0级,位于最高级的内核态,RPL=11,为第3级,位于最低级的用户态。
高13位(8K个索引):用来确定当前使用的段描述符在描述符表中的位置。
计算机维护一系列表,称为描述符表。描述符表分为三种:全局描述符表(GDT)、局部描述符表(LDT)、中断描述符表(IDT)。描述符表的每个表项,大小为8个字节,称为段描述符。段描述符的内容如下图。
BASE:段基址
Limit:指出这个段有多大。
DPL:描述符的特权级(privilege level),其值从0(最高特权,内核模式)到3(最低特权,用户模式),用于控制对段的访问。
下图详细显示了一个逻辑地址是怎样转换成相应线性地址的,逻辑地址转换为线性地址的一般步骤:
首先,给定一个完整的逻辑地址[段选择符:段内偏移地址],
1、看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。
2、拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。
3、把Base + offset,就是要转换的线性地址了。
7.3 Hello的线性地址到物理地址的变换-页式管理
每个页表实际上是一个数组,数组中的每个元素称为页表项(PTE, page table entry),每个页表项负责把虚拟页映射到物理页上。在 DRAM 中,每个进程都有自己的页表,具体如下
因为有一个表可以查询,就会遇到两种情况,一种是命中(Page Hit),另一种则是未命中(Page Fault)。
命中的时候,即访问到页表中蓝色条目的地址时,因为在 DRAM 中有对应的 数据,可以直接访问。
不命中的时候,即访问到 page table 中灰色条目的时候,因为在 DRAM 中 并没有对应的数据,所以需要执行一系列操作(从磁盘复制到 DRAM 中), 具体为:
触发 Page fault,也就是一个异常,Page fault handler 会选择 DRAM 中需要被置换的 page,并把数据从磁盘复制到 DRAM 中,重新执行访问指令,这时候就会是 page hit。复制过程中的等待时间称为 demand paging。
仔细留意上面的页表,会发现有一个条目是 null,也就是没有分配。具体的分配过程(比方说声明了一个大数组),就是让该条目指向虚拟内存(在磁盘上)的某个页,但并不复制到 DRAM,只有当出现 page fault 的时候才需要拷贝数据。
7.4 TLB与四级页表支持下的VA到PA的变换
TLB:页表常驻内存,地址翻译时MMU需要去内存中访问,为减少这样的开销,MMU中包含了一个关于PTE的小缓存,称为翻译后备缓冲器(TLB)。
多级页表:将页表分为多个层级,以减少内存要求。
CPU将VPN进一步划分为TLBT(TLB标记)和TLBI(TLB索引)。
由TLBI,访问TLB中的某一组,遍历该组中的所有行,若找到一行的tag等于TLBT,且有效位valid为1,,则缓存命中,该行存储的即为PPN;否则缓存不命中,需要到页表中找到被请求的块替换原TLB表项中的块。
多级页表的访问:缓存不命中后,VPN被解释成从低位到高位的等长的4段,从高地址开始,第一段VPN作为第一级页表的索引,用以确定第二级页表的基址;第二段VPN作为第二级页表的索引,用以确定第三级页表的基址;第三段VPN作为第三级页表的索引,用以确定第四级页表的基址;第四段VPN作为第四级页表的索引,若该位置的有效位为1,则第四级页表中的该表项存储的是所需要的PPN的值。
7.5 三级Cache支持下的物理内存访问
我们只讨论Cashe1的物理内存访问,Cashe2,Cashe3原理相同。
由于L1Cashe有64组,所以组索引位s为6,每组有8个高速缓存行,由于每个块的大小为64B,所以块偏移为为6,因此标记位为52-6-6=40位。
因此L1Cashe的物理访存大致过程如下:
(1) 组选择取出虚拟地址的组索引位,将二进制组索引转化为一个无符号整数,找到相应的组
(2) 行匹配把虚拟地址的标记为拿去和相应的组中所有行的标记位进行比较,当虚拟地址的标记位和高速缓存行的标记位匹配时,而且高速缓存行的有效位是1,则高速缓存命中。
(3) 字选择一旦高速缓存命中,我们就知道我们要找的字节在这个块的某个地方。因此块偏移位提供了第一个字节的偏移。把这个字节的内容取出返回给CPU即可
- 不命中如果高速缓存不命中,那么需要从存储层次结构中的下一层取出被请求的块,然后将新的块存储在组索引位所指示的组中的一个高速缓存行中。一种简单的 放置策略如下:如果映射到的组内有空闲块,则直接放置,否则组内都是有效块, 产生冲突(evict),则采用最近最少使用策略 LFU 进行替换。
7.6 hello进程fork时的内存映射
当 fork 函数被 shell 进程调用时,内核为新进程创建各种数据结构,并分配给 它一个唯一的 PID,为了给这个新进程创建虚拟内存,它创建了当前进程的 mm_struct、区域结构和页表的原样副本。它将这两个进程的每个页面都标记为只 读,并将两个进程中的每个区域结构都标记为私有的写时复制。
7.7 hello进程execve时的内存映射
execve 函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运 行包含在可执行目标文件 hello 中的程序,用 hello 程序有效地替代了当前程序。 加载并运行 hello 需要以下几个步骤:
1)删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存 在的区域结构。
2)映射私有区域,为新程序的代码、数据、bss 和栈区域创建新的区域结 构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为 hello 文件中的.text 和.data 区,bss 区域是请求二进制零的,映射到匿名 文件,其大小包含在 hello 中,栈和堆地址也是请求二进制零的,初始长 度为零。
3)映射共享区域, hello 程序与共享对象 libc.so 链接,libc.so 是动态链 接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
- 设置程序计数器(PC),execve 做的最后一件事情就是设置当前进程 上下文的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
缺页故障是一种常见的故障,当指令引用一个虚拟地址,在MMU中查找页表时发现与该地址相对应的物理地址不在内存中,因此必须从磁盘中取出的时候就会发生故障。
缺页中断处理:
CPU 首先把虚拟地址发送给 MMU,MMU 检查缓存,发现没有对应的页,于是触发异常,异常处理器会负责从磁盘中找到对应页面并与缓存/内存中的页进行置换,置换完成后再访问同一地址,并把从页表中得到对应的物理地址,接着 MMU 把物理地址发送给缓存/内存,最后从缓存/内存中得到数据。
7.9动态存储分配管理
1 堆:动态存储的分配管理是由动态内存分配器完成的,动态内存分配器维护着一个进程的虚拟内存区域(称为堆),堆是请求二进制零的区域,它季节再未初始化的数据区域后面,并向上增长。对于每个进程,内核维护一个brk变量,它指向堆顶。
2 堆结构:分配器把堆看成一组大小不同的块的集合来维护。其中每一个块就是一个连续虚拟内存片,它的状态有两种,已分配或者空闲。已分配块显式地保留为应用程序所使用,空闲块可被分配,保持空闲直到显式地被应用程序分配。已分配块保持分配状态,直到被释放。
3 分配器的实现:
1 分配器的分类:
显示分配器:要求应用显式地释放任何已分配块
隐式分配器:要求分配器检测一个已分配块何时不再被程序使用,自动地释 放已分配块
2 隐式空闲链表:
带边界标签的隐式空闲链表将这些信息嵌入块本身,在块的前四个字节的头 部和最后四个字节的尾部中嵌入表示块状态的信息,最低位用来表示块的分配 状态,1表示已分配,0表示未分 配;整个四字节的值用来表示块的大小;在 空闲块进行合并时,通过前一个块的脚部和后一个块的尾部就能知道新释 放的空闲块该如何合并。
3.显式空闲链表:
将堆组织成一个双向空闲链表,在每一个空闲块中,都包含一个pred(前驱) 和succ(后继)指针,释放一个块可以采用后进先出(LIFO)的顺序或者按 照地址顺序来维护链表,前者释放块可以在常数时间内完成,后者需要线性的 时间,但具有更高的内存利用率。
7.10本章小结
本章主要介绍了hello的存储器地址空间、intel的段式管理、页式管理,TLB与四级页表支持下的VA到PA的变换、三级cache支持下物理内存访问, hello进程fork时的内存映射、execve时的内存映射、缺页故障与缺页中断处理、动态存储分配管理等内容。
在hello 的运行中,它需要与其他进程共享主存,而对于有限的主存空间,如果太多的进程需要太多的内存,这就可能会使程序没有空间可用。而虚拟内存的概念很好地解决了这一点。他是硬件异常,地址翻译,主存,磁盘和内核软件的完美交互,它为每个进程提供了大的私有的地址空间。在这个机制下,hello可以通过malloc从堆中申请内存,也可以通过free释放掉使用结束的内存。可以说,虚拟内存及其管理机制为hello提供了一个自由正确运行的广阔平台。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
所有的I/ O 设备都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux 内核引出一个简单、低级的应用接口,称为Unix I/O(即unix io接口),这使得所有的输入和输出都能以一种统一且一致的方式来执行。
设备管理:所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备映射为文件的方式,允许Linux内核引出一个简单,低级的应用接口,称为Unix I/O。
这便是Linux的IO设备管理方法。
8.2 简述Unix IO接口及其函数
Unix I/O接口:
1.打开文件:一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。
2.Linux shell创建的每个进程开始时都有三个打开的文件:标准输入,标准输出,标准错误。
3.改变当前文件位置:对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。
4.读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时执行读操作时触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
5.关闭文件:当应用完成了访问,它就通知内核关闭这个文件,并释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。
6.Unix I/O函数:
int open(char* filename,int flags,mode_t mode) :进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
返回:若成功则为新文件描述符,若出错为-1。
7.int close(fd),fd是需要关闭文件的描述符。
返回:若成功则为新文件描述符,若出错为-1。
8.ssize_t read(int fd,void *buf,size_t n),该函数从描述符为fd的当前位置最多赋值n个字节到内存buf的位置,返回值为实际传送的字节数量。返回:若成功则为新文件描述符,若出错为-1。
9.ssize_t wirte(int fd,const void *buf,size_t n),write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。
返回:若成功则为新文件描述符,若出错为-1。
8.3 printf的实现分析
分析首先查看printf函数的函数体:
printf程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。
接下来是write函数:
在printf中调用系统函数write(buf,i)将长度为i的buf输出,在write函数中,将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址,
int INT_VECTOR_SYS_CALLA代表通过系统调用syscall。
查看syscall函数体:
syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。
字符显示驱动子程序将通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中。
显示芯片会按照一定的刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
于是我们的打印字符串就显示在了屏幕上。
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
getchar 的源代码为:
异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码转换成 ASCII 码,保存到系统的键盘缓冲区之中。
getchar 函数落实到底层调用了系统函数 read,通过系统调用read读取存储在 键盘缓冲区中的ASCII码直到读到回车符然后返回整个字串,getchar进行封装, 大体逻辑是读取字符串的第一个字符然后返回。
8.5本章小结
了解了系统级的IO过程,分析了getchar和printf函数的实现
结论
1.预处理阶段,预处理器将hello.c include的外部的库取出合并到hello.i文件中
2.编译阶段,文本文件hello.i翻译成文本文件hello.s
3.汇编:将.s汇编程序翻译成机器语言指令,hello.s会变成为可重定位目标文件hello.o
4.链接:可重定位目标文件和动态链接库链接,生成可执行目标程序hello
5.运行:在shell中输入./hello 并跟上3个参数1190201015 guyuxiao 2
6.运行程序:shell收到运行./hello.c命令后,调用fork创建子进程,并调用execve启动加载器,加映射虚拟内存,创建新的内存区域,并将hello存入内存与CPU
7.执行指令:CPU为其分配时间片,在一个时间片中,hello有对CPU的控制权,顺序执行自己的代码
8.访问内存:MMU将程序中使用的虚拟内存地址通过页表映射成物理地址
9.动态申请内存:printf会调用malloc向动态内存分配器申请堆中的内存
10.异常处理:如果运行途中键入ctr-c指令,则程序停止,如果运行途中键入ctr-z指令,则程序挂起
- 结束:shell父进程回收子进程的资源,hello的一生就此终结。
我的感悟:沿着一个简单的HELLO程序从代码到执行进行了一次细致地梳理,才发现在初学时就可以随手敲出来的代码背后有多少智慧的火花和天才的构想。这也是我第一次仔细的研究一个程序的执行,对我以后编写硬件友好的程序有很大的帮助。我想,随着这一次美妙的旅程。增长的不仅是对于计算机的理解,还有对系统这个软件层面上的科技黑箱一个剖析,一次由高级语言到电子信号的切身感受。以后如果还有机会,我会选择更深入的探究计算机的奥秘。
附件
hello.c :hello源代码
hello.i :预处理后的文本文件
hello.s :hello.i编译后的汇编文件
hello.o :hello.s汇编后的可重定位目标文件
Hello:可执行目标程序,分析链接器行为
5.3helloelf.txt:hello的elf格式,分析汇编器和链接器行为
5.5helloobjdump.txt:可执行hello的反汇编,作用是重定位过程分析
elf.txt:hello.o的elf格式,分析汇编器和链接器行为
objdump.txt:hello.o的反汇编,主要是为了分析hello.o
参考文献
[1] https://blog.51cto.com/u_15443779/4718537
[2] https://www.cnblogs.com/diaohaiwei/p/5094959.html
[3] https://www.cnblogs.com/pianist/p/3315801.html
[5] 深入理解计算机系统原书第3版
[8] 详解重定位目标文件 - 简书
[9] HIT-CSAPP大作业_东百月的博客-CSDN博客
[10] CSAPP 大作业程序人生 - 哔哩哔哩
[11] https://www.likecs.com/show-203699927.html