摘 要
本文通过对hello程序运行的整个过程的详细分析,具体阐述了代码文件经过预处理,编译,汇编,链接后形成可执行目标文件,在shell中创建一个子进程执行该可执行目标文件直到执行完成退出的这一整个过程。
关键词:计算机系统;编译;链接;异常处理;系统信号;虚拟内存;进程管理;存储管理;IO管理。
目 录
第1章 概述
1.1 Hello简介
P2P:From Program to Process。程序员编写的hello.c文件是Program,电脑最终运行的hello是Process。hello.c文件经过预处理,编译,汇编,链接后形成可执行目标文件,在shell中创建一个子进程执行该可执行目标文件这一整个过程中,hello从Program变为Process。
020:From Zero-0 to Zero-0。当hello要运行时,shell为创建一个子进程,内核会在虚拟内存中加载并运行hello的相关代码和数据。当hello结束运行后,shell回收hello进程,内核删除相关数据。在这过程中,hello从零到有,又从有到零。
1.2 环境与工具
硬件环境:X64 CPU;2.60GHz;16.0G RAM;1024GHD Disk
软件环境:Windows10 64位;VirtualBox;Ubuntu 16.04 LTS 64位
开发工具与调试工具:Visual Studio 2010 64位;CodeBlocks 64位;vi/vim/gedit+gcc/gdb
1.3 中间结果
文件 | 作用 |
hello.c | hello程序的源代码文件 |
hello.i | hello程序经过预处理后产生的代码文件 |
hello.s | hello程序编译产生的汇编代码文件 |
hello.o | hello程序汇编产生的可重定位目标文件 |
hello | 经过链接后的可执行文件 |
hello.o_asm | hello.o的反汇编文件 |
hello_asm | hello的反汇编文件 |
表 1.3.1 中间结果
1.4 本章小结
简述了Hello的P2P,020的整个过程。清晰列出了实验环境与工具以及实验的中间结果。
第2章 预处理
2.1 预处理的概念与作用
预处理的概念:预处理器根据预处理指令(一般为代码中以#开头的指令)修改代码中的内容, 生成一个处理完毕并且删除了预处理指令的代码文件。
预处理的作用:
- 处理头文件。将源文件中以”include”格式包含的文件复制到编译的源 文件中;
2、处理宏。用实际值替换用“#define”定义的字符串;
3、处理条件指令。根据“#if”后面的条件决定需要编译的代码。
2.2在Ubuntu下预处理的命令
预处理的命令:gcc -E hello.c -o hello.i
图 2.2.1 预处理的命令
图 2.2.2 预处理的命令
2.3 Hello的预处理结果解析
结果解析:hello.i比hello.c多了非常多行代码,预处理器根据预处理指令在hello.i中添加了许多代码,并且删除了预处理指令,hello.i最后部分保留了hello.c的代码。
图 2.3.1 预处理结果解析
2.4 本章小结
本章介绍了预处理的概念与作用,并实现了对hello.c的预处理,比较了hello.i和hello.c两者之间的不同。
第3章 编译
3.1 编译的概念与作用
编译的概念:利用编译程序将源语言编写的源程序转换为汇编语言程序的过程。在该实验中编译器把hello.i文件编译为汇编语言的hello.s文件。
编译的作用: 将高级语言转变为接近计算机底层的汇编语言。
3.2 在Ubuntu下编译的命令
编译的命令:gcc -S hello.i -o hello.s
图 3.2.1 编译命令
图 3.2.2 编译命令
3.3 Hello的编译结果解析
3.3.1伪指令
图 3.3.1 编译结果解析
.file:指示汇编器该汇编程序的逻辑文件名(hello.c);
.text:将接下来的代码汇编链接到.text段;
.section .rodata:将接下来的代码汇编链接到.rodata段;
.align:将当前PC地址推进到“2的integer(8)次方个字节”对齐的位置;
.string:声明一个字符串(.LC0,.LC1);
.global:声明一个全局符号(main);
.type:声明一个符号是数据类型还是函数类型。
3.3.2数据
3.3.2.1字符串
图 3.3.2 编译结果解析
.LC0存储的是程序第一条printf语句所打印的字符串“用法: Hello 学号 姓名 秒数!\n”。
.LC1存储的是程序第二条printf语句所打印的字符串“Hello %s %s\n”。
这两个字符串都存在.rodata段中。
3.3.2.2main函数
图 3.3.3 编译结果解析
由上述伪指令可知,main在伪指令中被声明为全局函数。
图 3.3.4 编译结果解析
分析上述汇编指令可知,main函数的第一个参数argc存在-20(%rbp)的位置,第二个参数argv[]存在-32(%rbp)的位置。
3.3.2.3局部变量i
图 3.3.5 编译结果解析
图 3.3.6 编译结果解析
分析上述汇编指令可知,main函数中的负责计数的局部变量i,存放在-4(%rbp)的位置。
3.3.3操作
3.3.3.1赋值操作
利用mov指令赋值。
图 3.3.7 编译结果解析
上述汇编指令将0存储到栈里局部变量i所对应的位置。
mov后紧跟的b,w,l,q对应着1/2/3/4个字节操作数。
3.3.3.2算术操作
为了实现函数功能,函数中往往会用到多种算术操作,汇编指令中常用的的算数操作指令如下表所示。
图 3.3.8 编译结果解析
3.3.3.3关系操作
利用cmp指令进行关系操作。
图 3.3.9 编译结果解析
上述汇编指令将存储在栈里的局部变量i与7进行比较。
cmp后紧跟的b,w,l,q对应着1/2/3/4个字节操作数。
cmp b,a计算b-a但不改变目的操作数,仅用结果设置条件码:
▪ CF=1 如果最高有效位有借位(无符号数比较)
▪ ZF=1 如 a== b
▪ SF=1 如 (a-b) < 0 (有符号数比较)
▪ OF=1 如补码 (有符号数)溢出
3.3.3.4数组/指针/结构操作
数组的地址存储在栈中,通过栈指针的偏移来读取数组数据。
图 3.3.10 编译结果解析
上述汇编指令读取了argv[1](存储到rdx寄存器)与argv[2](存储到rsi寄存器)中的数据。
3.3.3.5控制转移
利用jX指令根据条件码进行控制转移。
jX指令如下图所示。
图 3.3.11 编译结果解析
3.3.3.6函数操作
利用call,ret指令来进行函数操作。
call指令先将返回地址入栈,再跳转到对应的函数。
图 3.3.12 编译结果解析
上述汇编指令调用了getchar@PLT函数。
ret指令先将返回地址出栈,再跳转到返回地址。
3.4 本章小结
本章介绍了编译的概念与作用,并从伪指令、数据与操作三个方面详细分析了hell.s文件。
第4章 汇编
4.1 汇编的概念与作用
汇编的概念:利用汇编器将汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在.o 目标文件中的过程。.o 文件是一个二进制文件,它包含程序的指令编码。
汇编的作用:将接近计算机底层的汇编语言转变为机器能够理解的二进制文件。
4.2 在Ubuntu下汇编的命令
汇编的命令:gcc -c hello.s -o hello.o
图 4.2.1 汇编命令
4.3 可重定位目标elf格式
4.3.1ELF文件头
图 4.3.1 1ELF文件头
ELF头是以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序,剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括了 ELF 头的大小、目标文件的类型、机器类型、 字节头部表的文件偏移,以及节头部表中条目的大小和数量等信息。
4.3.2节信息
图 4.3.2节头部表
节头部表包含文件中各个节的语义,包括节的类型、位置、大小等,每个节都从零开始因为是可重定位目标文件,根据节头表中的字节偏移信息可知各节的起始位置以及所占空间的大小。
4.3.3重定位节区信息
图 4.3.3重定位节
重定位节包含.text 节中需要进行重定位的信息,包括需要被修改的引用节的偏移量、重定位的类型、重定向的目标的名称、一个有符号常数用来对被修改引用的值做偏移调整。当链接器把这个目标文件和其他文件组合时,需要修改这些位置。
4.3.4符号表
图 4.3.4符号表
符号表中包含符号的大小、类型、相对于目标节的起始位置偏移、全局或者本地变量等信息。
4.4 Hello.o的结果解析
4.4.1机器语言的构成
hello.o反汇编的一条语句:
图 4.4.1Hello.o的结果解析
与之对应的hello.s中的语句:
图 4.4.2Hello.o的结果解析
由此可见,hello.o反汇编文件中增加了机器码,由操作码和操作数构成。增加了重定向语句,方便重定向操作。
4.4.2与汇编语言的映射关系
4.4.2.1分支转移
hello.o反汇编的一条分支转移语句:
图 4.4.3Hello.o的结果解析
与之对应的hello.s中的语句:
图 4.4.4Hello.o的结果解析
在hello.s中,跳转语句跳转到对应的伪节,而在hello.o反汇编中跳转语句跳转到相应的地址。
4.4.2.2函数调用
hello.o反汇编的一条函数调用语句:
图 4.4.5Hello.o的结果解析
与之对应的hello.s中的语句:
图 4.4.6Hello.o的结果解析
在hello.s中,call指令之后直接写函数名称,而在hello.o反汇编中call指令之后写的地址,并且是下一条指令的地址,因为还没有进行重定向,不能确定跳转到的函数位置。
4.5 本章小结
本章介绍了汇编的概念与作用,利用readelf命令查看了hello.o的ELF格式,分析了其中的ELF文件头、节头部表、重定位节、符号表,并将hello.o反汇编与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.2.1链接的命令
5.3 可执行目标文件hello的格式
5.3.1ELF文件头
图 5.3.11ELF文件头
5.3.2节信息
图 5.3.2节头部表
5.3.3重定位节区信息
图 5.3.3重定位节
5.3.4符号表
图 5.3.4符号表
5.4 hello的虚拟地址空间
图 5.4.1虚拟地址空间
查看本进程的虚拟地址空间可知程序起始虚拟地址是0x400000。
5.4.1.text节
图 5.4.2虚拟地址空间
根据用readelf命令得到的ELF目标文件信息,.text节偏移量为0x10f0,大小为0x145,虚拟地址空间信息如上图所示。
5.4.2.rodata节
图 5.4.3虚拟地址空间
根据用readelf命令得到的ELF目标文件信息,.rodata节偏移量为0x2000,大小为0x3b,虚拟地址空间信息如上图所示。
5.5 链接的重定位过程分析
5.5.1hello与hello.o的不同
hello.o:
图 5.5.1重定位过程分析
hello:
图 5.5.2重定位过程分析
不同:
- hello.o的反汇编文件中只有.text段,该段中也只有main函数的汇编代 码。而hello的反汇编文件中有.init段、.plt段(动态链接)、.text段 等,每段里也都有需要用到的所有汇编代码;
- hello.o的反汇编文件中运用到是相对地址,而hello的反汇编文件中运 用到是虚拟地址;
- hello.o的反汇编文件中的可重定向条目在hello的反汇编文件中得到重 定向;
5.5.2重定向过程
hello.o:
图 5.5.3重定位过程分析
hello:
图 5.5.4重定位过程分析
以上述重定向操作为例。
重定向信息:
▪ offset = 1c
▪ r.symbol = .rodata
▪ r.type = R_X86_64_PC32
▪ r.addend = -0x4
refaddr = 0x401125+0x1c = 0x401141
*refptr=(unsigned)(ADDR(r.symbol)+r.addend-refaddr)=(unsigned)(0x402000-0x4-0x401141) = 0xec3
得到的0xec3就是重定向的值。
5.6 hello的执行流程
通过edb使用step into、step over、step out调试hello,可以看到调试栏左边显示的程序名和汇编窗口的绿色箭头所指的程序名和程序地址
libc-2.31.so!__libc_start_main | 0x7efcd5195fc0 |
libc-2.31.so!__cxa_atexit | 0x7efcd51b8e10 |
hello!__libc_csu_init | 0x4011c0 |
hello!_init | 0x401000 |
libc-2.31.so!_setjmp | 0x7efcd51b4cb0 |
hello!main | 0x401125 |
hello!puts@plt | 0x401030 |
hello!.plt | 0x401020 |
libc-2.31.so!puts | 0x7efcd51f6440 |
表 5.6.1执行流程表
5.7 Hello的动态链接分析
图 5.7.1动态链接分析
.got:全局偏移表,是数据段的一部分,是链接器在执行链接时实际上要填充的部分,保存了所有外部符号的地址信息。
.plt:进程链接表,是代码段的一部分。该表包含的代码有两个功能:用来调用链接器来解析某个外部函数的地址,并填充到.got.plt中,然后跳转到该函数;如果已经填充过,则直接在.got.plt中查找并跳转到对应外部函数。
.got.plt:是.plt的.got全局偏移表,如果在之前查找过该符号,则其中已经填充了外部函数的具体地址,否则要跳转回.plt的代码执行查找。
通过节信息可以知道这些与动态链接相关的节的地址位置。
在执行dl_init之前,观察虚拟地址空间中.got.plt段的内容,发现从0x404003到0x404017都为00,如下图所示。
图 5.7.2动态链接分析
在执行dl_init之后,观察虚拟地址空间中.got.plt段的内容,发现之前为00的地址存储了数据,如下图所示。
图 5.7.3动态链接分析
根据上述对.got.plt节的描述可以得知,其中存储的是外部函数的具体地址。
5.8 本章小结
本章介绍了链接的概念与作用,利用ld命令生成了hello文件,利用readelf命令分析hello的ELF格式,使用edb查看了hello运行时虚拟地址空间各段信息并理清了hello的执行流程,据此,还分析了链接的重定位过程和动态链接过程。
第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:是正在运行的程序的实例,也就是一段程序的执行过程。系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
进程的作用:进程为程序提供了一个独立的逻辑控制流以及一个私有地址空间。使得我们感觉程序好像是独占的使用处理器和内存,处理器好像是无间断地一条接一条地执行我们程序中的指令。
6.2 简述壳Shell-bash的作用与处理流程
壳Shell-bash的作用:Shell是指为使用者提供操作界面的软件(命令解析器)。它接受用户命令,然后调用相应的应用程序。Linux系统中所有的可执行文件都可以作为Shell命令来执行。
处理流程:
- 调用parseline函数解析以空格分隔的命令行参数,并构造传递给execve的argv向量,返回值存储到bg用于之后判断该命令行指令是否是前台运行;
- 调用builtin_cmd函数判断第一个命令行参数是否是一个内置的shell指令。如果是,builtin_cmd函数会识别该命令行指令并执行。否则调用fork创建子进程,在子进程中根据argv向量调用execve函数,读取相应参数,执行相应的程序;
- 在父进程中将创建的子进程加入作业集合;
- 通过bg判断是否是前台进程,如果是前台进程,则调用waitpid函数显式等待进程运行结束,否则将其放入后台并返回。
6.3 Hello的fork进程创建过程
父进程通过调用fork函数创建一个新的、处于运行状态的子进程。fork函数被调用一次,却返回两次,子进程返回0,父进程返回子进程的PID。
子进程与父进程的比较:
▪ 新创建的子进程几乎但不完全与父进程相同;
▪ 子进程得到与父进程虚拟地址空间相同的(但是独立的)一份副本;
▪ 子进程获得与父进程任何打开文件描述符相同的副本;
▪ 子进程有不同于父进程的PID。
Hello的fork进程创建:在shell中输入命令./hello 120L021326 史子琦 5。
图 6.3.1fork进程创建
6.4 Hello的execve过程
调用fork创建子进程后,在子进程中根据argv向量调用execve函数,读取相应参数,执行相应的程序。execve函数覆盖当前进程的代码、数据、栈,但保留相同的PID,继承已打开的文件描述符和信号上下文。
execve函数说明:在当前进程中载入并运行程序
int execve(char *filename, char *argv[], char *envp[])
▪ filename:可执行文件
目标文件或脚本(用#!指明解释器,如 #!/bin/bash)
▪ argv:参数列表
惯例:argv[0]==filename
▪ envp:环境变量列表
图 6.4.1进程栈结构
图 6.4.2execve示例
6.5 Hello的进程执行
6.5.1进程调度的过程
多重处理:
图 6.5.1多重处理
单处理器在并发地执行多个进程:地址空间由虚拟内存系统管理,未执行进程的寄存器值保存在内存中。当处理器在进程之间交替执行时,先将寄存器当前值保存到内存,再调度下一个进程执行,装载保存的寄存器、切换地址空间。
图 6.5.2多重处理
多核处理器在并发地执行多个进程:当单个芯片有多个CPU时,则这些CPU共享主存,有的还共享cache ,每个核可以执行独立的进程,kernel负责处理器的内核调度。
并发进程:
图 6.5.3并发进程
每个进程是个逻辑控制流,如果两个逻辑流在时间上有重叠,则称这两个进程是并发的(并发进程),否则他们是顺序的。并发进程的控制流,在时间上是物理不相交的。然而,可将并发的进程看做是并行运行的。
6.5.2用户态与核心态转换
图 6.5.4用户态与核心态转换
进程由常驻内存的操作系统代码块(称为内核)管理,内核不是一个单独的进程,而是作为现有进程的一部分运行。通过上下文切换,控制流通从一个进程传递到另一个进程。
上下文切换步骤:
- 保存当前进程的上下文;
- 恢复某个先前被抢占的进程被保存的上下文;
- 将控制权传递给这个新恢复的进程。
shell刚开始运行hello程序时,hello进程在用户模式,在调用了sleep函数之后,进程从用户模式进入内核模式,使程序休眠,内核执行上下文切换执行其他进程。当sleep函数结束后,发送信号给内核,hello进程回到用户模式。当hello进程调用getchar函数时,执行系统调用read,等待键盘缓冲区到内存的数据传输,这时内核执行上下文切换执行其他进程,当完成键盘缓冲区到内存的数据传输时,发出一个信号,内核再次执行上下文切换接着执行hello进程。
6.6 hello的异常与信号处理
6.6.1异常类型
hello执行过程中会出现四类异常:中断、陷阱、故障、 终止。
图 6.6.1异常类型
中断:
图 6.6.2中断
处理器外部I/O设备引起,由处理器的中断引脚指示,中断处理程序返回到下一条指令处
陷阱:
图 6.6.3陷阱
陷阱是有意的,是执行指令的结果,所以其发生时间是可预知的,陷阱处理程序将控制返回到下一条指令。
故障:
图 6.6.4故障
故障不是有意的,如果能被修复,处理程序重新执行引起故障的指令(已修复),如果不能修复则终止。
终止:
图 6.6.5终止
终止是非故意的,由不可恢复的致命错误造成,会导致当前程序中止。
6.6.2信号处理
不停乱按:
图 6.6.6信号处理
不带回车时,shell只是将我们乱打的乱码显示出来。
图 6.6.7信号处理
带回车时,shell会在hello运行结束后尝试执行以回车结尾的乱码。
Ctrl-C:
图 6.6.8信号处理
Ctrl-C给前台的所有作业发送SIGINT结束前台作业。
Ctrl-Z:
图 6.6.9信号处理
Ctrl-Z给前台的所有作业发送SIGSTP暂停前台作业。
ps:
图 6.6.10信号处理
ps命令查看进程信息。
jobs:
图 6.6.11信号处理
jobs命令查看作业信息。
pstree:
图 6.6.12信号处理
pstree命令查看进程树
fg:
图 6.6.13信号处理
fg命令将后台程序转到前台运行。
kill:
图 6.6.14信号处理
kill命令发送信号9(SIGKILL)给进程3261。
6.7本章小结
本章介绍了进程的概念与作用,简述了壳Shell-bash的作用与处理流程,梳理了hello的fork进程创建过程和execve过程。通过Hello的进程执行解释了进程调度和上下文切换。最后通过对hello的异常与信号处理。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:程序编译后程序汇编代码中的地址,用来指定一个操作数或一条指令的地址,由段标识符加上偏移量表示,要经过寻址方式的计算或变换才能转化为内存储器中的物理地址。
图 7.1.1逻辑地址
线性地址:线性地址是逻辑地址和物理地址变换的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。
虚拟地址:CPU启动保护模式后,程序运行在虚拟地址空间中的地址。
图 7.1.2虚拟地址
物理地址:物理内存中放在寻址总线上的地址。
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
7.2 Intel逻辑地址到线性地址的变换-段式管理
全局描述符表(GDT)包含:
- 操作系统使用的代码段、数据段、堆栈段的描述符
- 各任务、程序的LDT(局部描述符表)段
每个程序的描述符表(LDT)包含:
- 对应任务/程序私有的代码段、数据段、堆栈段的描述符
- 对应任务/程序使用的门描述符:任务门、调用门等。
段式管理:一个程序分成若干个段进行存储和管理。
过程:
1. 通过判断段选择符的T1是0还是1,判断当前要转换是GDT中的段还是LDT中的段,再根据相应寄存器,得到其地址和大小;
2. 查找对应的段描述符,获得基地址;
3. 基地址加上偏移量,得到要转换的线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
虚拟页面的三种状态:
- 缓存:已经缓存在物理内存中的已分配页;
- 未缓存:未缓存在物理内存中的已分配页;
- 未分配/创建。
图 7.3.1页式管理
地址翻译:命中
图 7.3.2页式管理
- 处理器生成一个虚拟地址,并将其传送给MMU
- MMU生成PTE地址(PTEA),并从高速缓存/主存请求得到PTE
- 高速缓存/主存向MMU返回PTE
- MMU 将物理地址传送给高速缓存/主存
- 高速缓存/主存返回所请求的数据字给处理器
地址翻译:未命中
图 7.3.3页式管理
- 处理器生成一个虚拟地址,并将其传送给MMU
- MMU生成PTE地址(PTEA),并从高速缓存/主存请求得到PTE
- 高速缓存/主存向MMU返回PTE
- PTE的有效位为零, 因此 MMU 触发缺页异常
- 缺页处理程序确定物理内存中的牺牲页 (若页面被修改, 则换出到磁盘——写回策略)
- 缺页处理程序调入新的页面,并更新内存中的PTE
- 缺页处理程序返回到原来进程,再次执行导致缺页的指令
7.4 TLB与四级页表支持下的VA到PA的变换
首先要了解四级页表的格式。
1-3级页表条目格式:
图 7.4.1 1-3级页表条目格式
每个条目引用一个4KB子页表:
P: 子页表在物理内存中 (1)不在 (0)
R/W: 对于所有可访问页,只读或者读写访问权限
U/S: 对于所有可访问页,用户或超级用户 (内核)模式访问权限
WT: 子页表的直写或写回缓存策略
A: 引用位 (由MMU 在读或写时设置,由软件清除)
PS: 页大小为4 KB 或 4 MB (只对第一层PTE定义)
Page table physical base address: 子页表的物理基地址的最高40位 (强制页表
4KB 对齐)
XD: 能/不能从这个PTE可访问的所有页中取指令
第 4 级页表条目格式:
图 7.4.2 4级页表条目格式
每个条目引用一个 4KB子页表:
P: 子页表在物理内存中 (1)不在 (0)
R/W: 对于所有可访问页,只读或者读写访问权限
U/S: 对于所有可访问页,用户或超级用户 (内核)模式访问权限
WT: 子页表的直写或写回缓存策略
A: 引用位 (由MMU 在读或写时设置,由软件清除)
D: 修改位 (由MMU 在读和写时设置,由软件清除)
Page table physical base address: 子页表的物理基地址的最高40位 (强制页表
4KB 对齐)
XD: 能/不能从这个PTE可访问的所有页中取指令
虚拟地址到物理地址过程示意图:
图 7.4.3 虚拟地址到物理地址过程
CR3寄存器中存储L1页表的物理地址。VPN1提供到一个L1 PTE的偏移量,这个PTE包含L2页表的基地址。VPN2提供到一个L2 PTE的偏移量,以此类推。
7.5 三级Cache支持下的物理内存访问
存储器(三级Cache)层次结构:
图 7.5.1存储器(三级Cache)层次结构
通用的高速缓存存储器组织结构:
图 7.5.2高速缓存存储器组织结构
读取缓存数据:
图 7.5.3读取缓存数据
先定位组,再定位行,检查集合中的任何行是否有匹配的标记,如果有匹配的标记并且行有效则命中,最后根据块偏移定位数据。
两种方式:
1、直接映射高速缓存
每一组只有一行
图 7.5.4直接映射高速缓存
2、E-路组相联高速缓存
当每组两行时
图 7.5.5 2-路组相联高速缓存
7.6 hello进程fork时的内存映射
在分析hello进程fork时的内存映射之前要了解内存映射和共享对象。
内存映射:Linux通过将虚拟内存区域与磁盘上的对象关联起来以初始化这个虚拟内存区域的内容。
共享对象:
进程1和2映射了同一个对象,该对象称之为共享对象。
私有的写时复制对象是共享对象的一种
图 7.6.1 共享对象
两个进程都映射了私有的写时复制对象,区域结构被标记为私有的写时复制。私有区域的页表条目都被标记为只读。当输入写私有页的指令时触发保护故障,故障处理程序创建这个页面的一个新副本,故障处理程序返回时重新执行写指令。在这过程中要尽可能地延迟拷贝。
图 7.6.2 私有的写时复制对象
fork时的内存映射:
- 为新进程创建虚拟内存;
▪ 创建当前进程的mm_struct、vm_area_struct和页表的原样副本。
▪ 两个进程中的每个页面都标记为只读
▪ 两个进程中的每个区域结构(vm_area_struct)都标 记为私有的写时复制(COW)
- 在新进程中返回时,新进程拥有与调用fork进程相同的虚拟内存;
- 随后的写操作通过写时复制机制创建新页面。
7.7 hello进程execve时的内存映射
execve函数在当前进程中加载并运行新程序hello.out的步骤:
- 删除已存在的用户区域
- 创建新的区域结构
▪ 代码和初始化数据映射到.text和.data区(目标文件提供)
▪ .bss和栈映射到匿名文件
- 设置PC,指向代码区域的入口点
▪ Linux根据需要换入代码和数据页面
图 7.7.1 内存映射
注:将图中的a.out看作hello.out
7.8 缺页故障与缺页中断处理
图 7.8.1 缺页中断处理
缺页处理程序检查:
检查地址是否合法:
搜索区域链表,确认地址是否在合法的某个区域内。如果不是则地址非法,触发段错误。
检查访问是否合法:
是否有读、写或执行区域内页面的权限。如果没有则违反许可,触犮保护异常。处理程序返回,如果是正常缺页,内核会选择牺牲页,换入新的页面并进行页表更新,完成缺页处理。
7.9动态存储分配管理
图 7.9.1 堆
在程序运行时程序员使用动态内存分配器获得虚拟内存。动态内存分配器维护着进程的一个虚拟内存区域,称为堆。分配器将堆视为一组不同大小块的集合,每个块要么是已分配的,要么是空闲的。
分配器的类型:
▪ 显式分配器: 要求应用显式地释放任何已分配的块。
▪ 隐式分配器: 应用检测到已分配块不再被程序所使用,就释放这个块。
记录空闲块的四种方法:
- 隐式空闲链表通过头部中的长度字段—隐含地链接所有块;
图 7.9.2隐式空闲链表
图 7.9.3隐式空闲链表
隐式空闲链表中每个块都需要记录块的大小和分配状态,分配是线性时间的。带边界标记的隐式空闲链表在块的“脚部(footer)”标记 “大小/已分配”可以反查 “链表”,允许在常数时间进行对前面块的合并,但需要额外的空间。
- 显式空闲链表在空闲块中使用指针;
图 7.9.4显式空闲链表
图 7.9.5显式空闲链表
显式空闲链表中维护空闲块链表, 而不是所有块 ,“下一个” 空闲块可以在任何地方,因此需要存储前/后指针,而不仅仅是大小,还需要边界标记,用于块合并。与隐式链表相比较,分配时间从块总数的线性时间减少到空闲块数量的线性时间,但是空闲块需要更大的最小块大小,潜在地提高了内部碎片的程度。
- 分离的空闲列表按照尺寸大小size分类/组,每个类/组使用一个空闲链表;
- 块按大小排序使用平衡树(如红黑树),在每个空闲块中保存指针,并用长度(块大小)作为key值。
7.10本章小结
本章介绍了hello的存储器地址空间,简述了hello从逻辑地址到线性地址,再到物理地址的变换过程,梳理了在TLB与四级页表支持下的虚拟地址到物理地址的变换过程,以及在三级Cache支持下的物理内存访问过程,分析了hello进程fork和execve时的内存映射,并说明了缺页故障与缺页中断处理。最后,介绍了动态存储分配管理。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
一个 Linux 文件 就是一个 m 字节的序列: B0 , B1 , .... , Bk , .... , Bm-1。
设备管理:unix io接口
将设备映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。
8.2 简述Unix IO接口及其函数
8.2.1接口
打开文件:通知内核访问文件,内核返回一个小的描述符数字——文件描述符。返回的描述符总是在进程中当前没有打开的最小描述符。
Linux内核创建的每个进程都以与一个终端相关联的三个打开文件开始:
▪ 0: 标准输入 (stdin)
▪ 1: 标准输出 (stdout)
▪ 2: 标准错误 (stderr)
关闭文件:通知内核不再访问文件。
关闭文件时,内核的操作:
▪ 内核释放文件打开时创建的结构体
▪ 内核将描述符释放给可用描述符池
▪ 下次打开某个文件时,从该池中分配一个最小可用的描述符
读文件:读文件从当前文件位置复制字节到内存位置,然后更新文件位置。返回值表示的是实际传送的字节数量。
写文件:写文件从内存复制字节到当前文件位置,然后更新文件位置。返回值表示的是从内存向文件fd实际传送的字节数量。
文件位置:每个打开的文件,内核保持一个文件位置k,初始为0,表示从文件开头起始的字节偏移量。
8.2.2函数
打开和关闭文件: open()and close()
读写文件:read() and write()
改变当前的文件位置:seek()
指示文件要读写位置的偏移量:lseek()
获取文件状态:stat()
读取目录函数:readdir()
8.3 printf的实现分析
图 8.3.1 printf函数
va_list arg = (va_list)((char*)(&fmt) + 4);
在该语句中,va_list是一个字符指针, (char*)(&fmt) + 4) 表示的是输入的可变形参中的第一个参数,将可变形参中第一个参数的地址赋值给arg。
i = vsprintf(buf, fmt, arg);
在该语句中,vsprintf函数返回的是字符串长度,它的作用是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出存放进buf。
write(buf, i);
理解write函数首先要知道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函数实现的功能是显示格式化了的字符串。存储在寄存器ecx中的数据是要打印出的元素个数,存储在寄存器ebx中的数据是要打印的buf字符数组中的第一个元素。syscall将字符串中的字节从寄存器中复制到显卡的显存中,显存中存储的是字符的ASCII码。字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量),从而将字符串显示在屏幕上。
8.4 getchar的实现分析
图 8.4.1 getchar函数
在getchar程序中,buf表示缓冲区,BUFSIZ表示缓冲区的最大长度,bb表示指针指向缓冲区的首地址。getchar程序调用read函数,将缓冲区读入到buf中,将返回值存入n,将buf指针赋值给bb,如果长度n < 0,则返回EOF错误,否则返回buf中的第一个字符。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章介绍了Linux的IO设备管理方法,简述了Unix IO接口及其函数并且详细分析了printf的实现和getchar的实现。
结论
程序员编写完hello.c的代码后,要将运行hello程序时,需要进行如下操作:
- 首先i预处理器根据预处理指令修改代码中的内容, 生成一个处理完毕并且删除了预处理指令的hello.i;
- 之后编译器把hello.i文件编译为汇编语言的hello.s文件;
- 汇编器又把汇编程序hello.s翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在hello.o 目标文件中;
- 链接器将hello.o需要的各种目标文件与hello.o进行链接,形成了可执行目标文件hello;
- 在shell中输入命令,通过shell执行hello;
- shell通过fork函数创建一个子进程,使用execve函数加载并运行hello程序。在运行中涉及到了hello的进程管理、存储管理和IO管理;
- 最后,hello运行结束被父进程回收。
感悟:
- 实践是巩固所学知识的最好方法,在大作业中,通过对hello的折腾,我理清了计算机系统所学的知识,并将这些知识串联了起来;
- 计算机程序的执行过程我认为是一个从抽象到具体实现的过程,在最顶层的几行代码,是最底层一系列操作的复杂实现;
- 理清计算机系统内部的关系有助于我们学习这门课程。
附件
文件 | 作用 |
hello.c | hello程序的源代码文件 |
hello.i | hello程序经过预处理后产生的代码文件 |
hello.s | hello程序编译产生的汇编代码文件 |
hello.o | hello程序汇编产生的可重定位目标文件 |
hello | 经过链接后的可执行文件 |
hello.o_asm | hello.o的反汇编文件 |
hello_asm | hello的反汇编文件 |
参考文献
[1] 靖哥哥编程. RISC-V汇编程序伪操作 摘抄[EB/OL]. 2020-09-10[2022-05-20]. https://www.jianshu.com/p/f404ffb69024.
[2] wipping的技术小栈. linux应用程序——ELF查看工具[EB/OL]. 2020-02-22[2022-05-20]. https://www.jianshu.com/p/d9489aba95a9.
[3] 有价值炮灰. 深入了解GOT,PLT和动态链接[EB/OL]. 2018-04-09[2022-05-20]. https://www.cnblogs.com/pannengzhi/p/2018-04-09-about-got-plt.html.
[4] Timmy_Handsome_Cheng. 学习笔记——异常控制流(一)[EB/OL]. 2018-01-21[2022-05-20]. https://blog.csdn.net/Timmy_attack/article/details/79119855.
[5] Pianistx. printf 函数实现的深入剖析[EB/OL]. 2013-09-11[2022-05-20]. https://www.cnblogs.com/pianist/p/3315801.html.
[6] 梦小冷. 存储管理-段式管理[EB/OL]. 2019-11-24[2022-05-20]. https://www.cnblogs.com/mengxiaoleng/p/11921912.html.