计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机系
学 号 1190500812
班 级 1903005
学 生 吴宇辰
指 导 教 师 史先俊
摘要是论文内容的高度概括,应具有独立性和自含性,即不阅读论文的全文,就能获得必要的信息。摘要应包括本论文的目的、主要内容、方法、成果及其理论与实际意义。摘要中不宜使用公式、结构式、图表和非公知公用的符号与术语,不标注引用文献编号,同时避免将摘要写成目录式的内容介绍。
关键词:helloworld程序;Ubuntu;进程;
目 录
2.1 预处理的概念与作用................................................................................ - 5 -
2.2在Ubuntu下预处理的命令..................................................................... - 5 -
2.3 Hello的预处理结果解析......................................................................... - 5 -
3.2 在Ubuntu下编译的命令........................................................................ - 6 -
3.3 Hello的编译结果解析............................................................................. - 6 -
4.2 在Ubuntu下汇编的命令........................................................................ - 7 -
4.3 可重定位目标elf格式............................................................................ - 7 -
5.2 在Ubuntu下链接的命令........................................................................ - 8 -
5.3 可执行目标文件hello的格式................................................................ - 8 -
5.4 hello的虚拟地址空间............................................................................. - 8 -
5.5 链接的重定位过程分析............................................................................ - 8 -
5.7 Hello的动态链接分析............................................................................. - 8 -
第6章 hello进程管理............................................................................... - 10 -
6.2 简述壳Shell-bash的作用与处理流程................................................ - 10 -
6.3 Hello的fork进程创建过程................................................................. - 10 -
6.6 hello的异常与信号处理....................................................................... - 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 -
第8章 hello的IO管理............................................................................ - 13 -
8.1 Linux的IO设备管理方法..................................................................... - 13 -
8.2 简述Unix IO接口及其函数.................................................................. - 13 -
第1章 概述
1.1 Hello简介
1. P2P简介
使用编译器编写hello.c程序,再经过C预处理器得到中间文件hello.i,接着通过C编译器得到汇编语言文件hello.s,然后通过汇编器得到可重定位目标文件hello.o,最后通过链接器得到可执行文件hello。在shell中输入./hello启动程序,shell将调用fork函数产生子进程,此时hello便成为了进程
2. O2O简介
shell调用execve函数在子进程中运行hello,CPU需要为其分配内存、时间片等资源。系统的进程管理将帮助hello切换上下文,同时信号处理程序将帮助hello处理各种信号,当我们按下ctrl+z或者hello自行运行结束,将由shell将其回收,内核将打扫干净其所有痕迹
1.2 环境与工具
硬件环境:Inter® Core™ i7-9750H CPU;16G RAM;1T SSD
软件环境:Windows 10 64位;Vmware 15.5.0;Ubuntu 18.04 64位
开发与调试工具:gcc;edb; readelf;objdump;gedit;hexedit;
1.3 中间结果
hello.c——原文件
hello.i——预处理得到的中间文件
hello.s——汇编语言文件
hello.o——as之后的可重定位目标文件
hello——ld之后的可执行目标文件
hello.elf——hello.o的elf文件,可以查看文件的信息
hello.asm——hello.o的反汇编文件,可以查看汇编器翻译后的汇编代码
hello1.asm——hello的反汇编文件,可以查看链接器链接后的汇编代码
1.4 本章小结
本章主要介绍了hello.c的P2P和O2O,接着列举了整个大作业中所需的实验环境与工具,最后展示了完成过程中所需要的的中间文件及其作用
第2章 预处理
2.1 预处理的概念与作用
(以下格式自行编排,编辑时删除)
预处理的概念:
预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。典型地,由预处理器(preprocessor) 对程序源代码文本进行处理,得到的结果再由编译器核心进一步编译,预处理中会展开以#起始的行,修改原始的C程序。将所引用的所有库展开,处理所有的条件编译,并执行所有的宏定义,得到另一个通常是以.i作为文件扩展名的C程序。
预处理的作用:
- 宏的替换。将宏名(#define定义的字符串)替换为实际值(可以是字符串、代码等)。
- 文件包含。将c程序中所有#include声明的头文件复制到新的程序中。将头文件的内容插入到该命令所在的位置,从而把头文件和当前源文件连接成一个源文件。
- 条件编译。根据#if以及#endif和#ifdef以及#ifndef来判断是否处理之后的代码。
2.2在Ubuntu下预处理的命令
命令:cpp hello.c>hello.i
图2.2 对hello.c进行预处理
2.3 Hello的预处理结果解析
首先展示hello.i文件部分内容
可以看到经过预处理,hello.c已经被扩展到了3121行。仔细观察hello.i文件,可以发现主要是头文件的展开占据了大量篇幅。头文件stdio,从第12行展开到第850行才结束;下一个头文件unistd展开于853行,到2059行结束;最后一个头文件stdlib展开于2062行,到3102行结束。这些头文件的展开占据了大量的篇幅,我们的程序主题部分只占了一小部分,从3108行开始到结尾,同时可以发现预处理删除了我们的注释内容,但主体部分并没有什么差别。
图2.3.1 stdio.h结尾
图2.3.2 unistd.h结尾
图2.3.3 stdlib.h结尾
2.4 本章小结
本章主要介绍了预处理的概念及其作用,给出了在Linux下预处理的指令,接着给出了hello.i文件的分析,了解了实际上一个预处理文件是怎样的,有了更加深入的理解
第3章 编译
3.1 编译的概念与作用
编译的概念
编译就是将源语言经过词法分析、语法分析、语义分析以及经过一系列优化后生成汇编代码的过程。这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
编译的作用
编译程序把一个源程序翻译成目标程序的工作过程分为五个阶段:词法分析;语法分析;语义检查和中间代码生成;代码优化;目标代码生成。
- 词法分析的任务是对由字符组成的单词进行处理,从左至右逐个字符地对源程序进行扫描,产生一个个的单词符号,把作为字符串的源程序改造成为单词符号串的中间程序。
- 编译程序的语法分析器以单词符号作为输入,分析单词符号串是否形成符合语法规则的语法单位,如表达式、赋值、循环等,最后看是否构成一个符合要求的程序,按该语言使用的语法规则分析检查每条语句是否有正确的逻辑结构,程序是最终的一个语法单位。编译程序的语法规则可用上下文无关文法来刻画。
- 中间代码是源程序的一种内部表示,或称中间语言。中间代码的作用是可使编译程序的结构在逻辑上更为简单明确,特别是可使目标代码的优化比较容易实现中间代码,即为中间语言程序,中间语言的复杂性介于源程序语言和机器语言之间。
- 代码优化是指对程序进行多种等价变换,使得从变换后的程序出发,能生成更有效的目标代码。所谓等价,是指不改变程序的运行结果。所谓有效,主要指目标代码运行时间较短,以及占用的存储空间较小。这种变换称为优化。
- 目标代码生成是编译的最后一个阶段。目标代码生成器把语法分析后或优化后的中间代码变换成目标代码。
3.2 在Ubuntu下编译的命令
编译命令:gcc -S hello.i -o hello.s
图3.2 编译生成.s文件
3.3 Hello的编译结果解析
3.3.1 数据类型(整数)
在hello.s中用到的数据类型有整数,数组,字符串,我们首先研究整数
在程序中可见有整型变量有int i和int argc
(1) 对于i,我们可知局部变量通常是会被保存在栈或者寄存器中,在本程序中,我们可以看到i被存储在栈上-4(%rbp)处,由于是int类型,故占据4个字节。在图中可见在49行处,将i的值+1,对应了原代码for循环中的操作
图3.3.1.1 int i
(2)而对于int argc。这个参数为main函数的第一个形式参数。正是由于它是第一个参数,所以由寄存器%edi保存,通过分析文件,我们可以发现我们将argc的值赋给了-20(%rbp)。可见在第20,执行了该操作
图 3.3.1.2 int argc
(3)此外程序中还出现了一些常数,对于这些作为立即数出现的常数,他们是直接出现的,以$常数形式。例如0,1等,可见在第26行就对1进行了直接操作
图3.3.1.3 常数
3.3.2 数据类型(数组)
对于数组char *argv[],是hello.c中的第二个形式参数,由我输入的数据所决定,是一个字符指针数组,由于其为main函数的第二个参数,所以被保存在寄存器%rsi中。由图中第21行可见 ,它随便被保存在栈的-32(%rbp)位置
图3.3.2.1 argv被保存在栈中
argv数组中一个元素的大小为8个字节,所以想要访问argv数组中的内容时,首先需要获取数组的首地址,然后计算偏移量,来访问后一个元素。例如第33和36行,就用不同的偏移量访问argv数组。
图3.3.2.2 访问argv数组
3.3.3 数据类型(字符串)
在我们的程序中,可以看出用到的字符串为“用法: Hello 学号 姓名 秒数!\n”和“Hello %s %s\n”。一般而言,字符串是被存储在.rodata节中,如下图所示。
图3.3.3 存储的两个字符串
在图中,可见\347\224\250\346\263\225等,这些其实是第一个字符串中的汉字被编码成UTF-8格式,一个汉字占据3个字节,每个字节用\分割
3.3.4 赋值操作
在hello.c程序中的赋值操作仅有i=0这一条,在汇编中用mov实现,具体语句为movl $0, -4(%rbp),将立即数0赋给int i,因为是int类型,所以占据4个字节,用字母l作为后缀
图3.3.4 为i赋值
3.3.5 类型转换
在我们的程序中,用到的类型转换仅有一处,为原代码的19行处,用atoi函数将argv[3]由字符串转换为了整型
图3.3.5.1 原程序
图3.3.5.2 汇编中的实现
3.3.6 算术操作
汇编语言的算术操作如下
指令 | 效果 | 描述 |
1eaq S,D | D←&S | 加载有效地址 |
INC D DEC D NEG D NOT D | D←D十1 D←D-1 D←一D D←一D | 加1 减l 取负 取补 |
ADD s,D | D←D十s | 加 |
SUB S,D | D←D-S | 减 |
IMUL S,D | D←D*S | 乘 |
XOR S,D | D←D^S | 异或 |
OR S,D | D←D|S | 或 |
AND S,D | D←D&S | 与 |
SAL k,D | D←D<<k | 左移 |
SHL k,D | D←D<<k | 左移 |
SAR k,D | D←D>>Ak | 算术右移 |
SHR k,D | D←D<<Lk | 逻辑右移 |
(1)具体到我们的程序中,首先可以想到的便是i++的算术操作,这句通过addl $1, -4(%rbp)语句实现,-4(%rbp)中存储着原来的i=0,所以可以对此不断加1达到循环的目的。
图3.3.6.1 i++操作
(2)除此之外还有语句subq $32, %rsp,作用是开辟新的栈空间,此处为32个字节,具体操作如图19行
图3.3.6.2 开辟新空间
(3)最后是相似语句addq $16, %rax,addq $8, %rax和addq $24, %rax,这些语句是为了取出argv数组中的内容而计算偏移量。如图33,36,43行
图3.3.6.3 计算偏移量
3.3.7 关系操作
首先给出汇编中,我们需要使用的操作指令
指令 | 基于 | 描述 |
CMP S1,S2 | S2-Sl | 比较 |
TEST S1, S2 | S1&S2 | 测试 |
set D |
|
|
Jmp等 |
|
|
cmp和test指令并不修改寄存器的值,而是设置条件码。
(1)具体到我们的函数中去,可见第一条为cmpl $4, -20(%rbp),对应的原程序语句为if(argc!=4)。第22行先进行计算,然后设置条件码,再由23行根据条件码进行判断是否跳转。
图3.3.7.1 argc!=4的实现
(2)第二条语句为cmpl $7, -4(%rbp),对应的原程序语句为for(i=0;i<8;i++),这里可以发现我们原本是比较i<8,而在汇编中实现则是i<=7,有细微的差别。
图3.3.7.2 i<8的实现
3.3.8 数组/指针/结构操作
在hello.s中访问argv数组的操作通常由mov指令实现。如图第35行,取到argv数组的首地址,然后对首地址加8字节得到argv[1]的地址,然后再通过argv[1]中的内容找到对应的字符串,存储在寄存器%rax之中,对于argv[2]的处理也类似于此
图3.3.8 取出argv[1]所指的字符串
3.3.9控制转移
在本程序中,涉及的控制转移部分主要有两处
(1)第一处是判断argc是否等于4,在第22行cmpl比较了argc与4的大小之后设置条件码,然后23行再判断是否跳转到L2,这个判断是基于条件码ZF位,如果为0则跳转,不为0则继续往下执行
图3.3.9.1 处理if(argc!=4)
(2)第二处是判断变量i是否满足循环条件i<8,首先在L2中第29行将i赋值为0,跳转到L3,开始判定是否进入循环,第51行cmpl变量i与7,设置条件码,52行再根据此判断是否跳转到L4执行循环体,如果i<=7则执行,如果>7则继续执行下一行,而不进入循环。
图3.3.9.2 处理for循环
3.3.10 函数操作
函数是过程的一种形式,提供了一种封装代码的方式。用一组指定的参数和一个可选的返回值便实现了一种功能。在程序的各个不同的地方都可以调用,例如P在调用Q时,有以下行为
传递控制:开始执行函数时,首先得将PC设置为代码的起始地址,而返回时也要将PC设置为P调用完Q后的下一条语句
参数传递:P必须向Q提供一个或多个参数,而Q也能向P返回0个或者1个参数。
分配和释放内存:在Q执行前为其分配合适的空间,而Q返回后释放为其分配的空间。
在我们的程序中,所涉及的函数主要为main,printf,exit,atoi,sleep,getchar,一共6个。下面将逐个分析
- main函数
main函数被存储在.text节中,有两个参数,分别为命令行传入的argc和argv[],开始被保存在寄存器%rdi和%
图3.3.10.1 main函数
- printf函数
在我们的程序中有两处调用了printf函数。第一处调用由于只是输入一串字符串,所以被系统优化成了puts函数,此时参数存放在%edi中。之后通过call来调用puts
图3.3.10.2.1 第一处printf
而第二处的printf,有了三个参数,首先我们需要取出参数,如图中第32,36和37行,将参数分别保存在%rdx,%rsi和%edi寄存器中,之后通过call调用printf
图3.3.10.2.2 第二处printf
- exit函数
如果我们输入的参数并不是4个,程序将会调用exit函数结束程序,具体过程如图24行,先将1传给%edi,然后调用exit函数退出。
图3.3.10.3 exit函数
- atoi函数
在我们的程序 ,需要调用atoi函数来将我们输入的第四个参数从字符串转化为整型,具体操作如图43行,将第四个参数存储在%rdi函数中,作为atoi函数的参数,然后调用atoi函数
图3.3.10.4 atoi函数
- sleep函数
在我们的程序中sleep函数的参数就是atoi的返回值,所以在atoi被调用完后,紧接着会将其返回值从%eax传给%edi作为sleep函数的参数。
图3.3.10.5 sleep函数
- getchar函数
getchar函数没有参数,故不需要传递参数,可以直接调用
图3.3.10.6 getchar函数
3.4 本章小结
本章主要介绍了编译的概念及其作用,以及在linux下编译的指令,最后我们根据hello.c文件的编译文件hello.s文件中的汇编代码详细的解析了数据类型,各类操作是如何实现的。经过这一章,最初的hello.c已经被转换成了更底层的汇编程序。
第4章 汇编
4.1 汇编的概念与作用
汇编的概念
汇编器as,将.s文件翻译成机器指令,也即.o文件,这一过程称为汇编,同时这个.o文件也是可重定位目标文件。
汇编的作用
将编译器产生的汇编语言进一步翻译为计算机可以理解的机器语言,生成.o文件。
4.2 在Ubuntu下汇编的命令
命令:as hello.s -o hello.o
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
使用命令readelf -a -W hello.o > hello.elf得到hello.elf
图4.3 生成的elf文件
接着列出ELF文件的各节内容
- ELF头:由一个16字节的Magic序列开始,此序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助连接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件类型、机器类型、字节头部表的文件偏移,以及节头部表中条目的大小和数量等信息。
图4.3.1 ELF头
2. 节头部表:包含了文件中出现的各个节的名称,类型,属性,地址,偏移量,大小等信息。
图4.3.2 节头部表
3. .rela.text节
在此节中存放着代码的重定位条目。当链接器把这个目标文件和其他文件组合时,会依据重定向节的信息修改.text节中的相应位置信息。如下图中的重定位信息分别对应.L0、puts函数、exit函数、.L1、printf函数、atoi函数、sleep函数、getchar函数。
接下来对表中各个信息进行补充说明,
- Offset:该值给出了需要重定位的代码在各自节中的偏移量
- Info:包括两部分,前四个字节为symbol,表示重定位到的目标在.symtab中的偏移量,而后四个字节为type,表示重定位的类型
- Addend:指定常量加数,一些类型的重定位需要它来作重定位加数,做偏移调整
- Type:重定位到的目标类型
- Name:重定位到的目标的名称
如果我们想要计算一个R_X86_64_32类型重定位目标地址,例如上图中第一行。r.offset=0x16,r.sympol=.rodata,r.type=R_X86_64_32,r.addend=0。首先我们计算引用运行时地址,refaddr=ADDR(s)+r.offset,然后将其更新为运行时指向真正的内容*refptr = (unsigned)(ADDR(r.sympol) + r.addend - refaddr)。最后在可执行目标文件中,我们便可以得到正确的引用地址了
4. .rela.eh_frame节
其中包含eh_frame节的重定位信息
- symtab节
一个符号表,存放在程序中定义和引用的函数和全局变量的信息。重定位需要引用的符号都在其中声明。接下来介绍其中包含的信息
name:记录字符串的便宜
value:符号的地址。对于可重定位的模块来说,value是距定义目标的节的起始位置的偏移。对于可执行目标文件来说,该值是一个绝对运行时地址
size:目标的大小
type:目标的类型,是数据或者是函数
blind:表述该符号是本地变量还是全局变量
section:表示四种情况,节索引号、ABS、UNDEF、COM,分别对应
- 说明符号所对应的空间在哪个节里面。
- 表示该符号不需要被“链接程序”处理。
- 表示这个符号,只是在本模块中被引用了,这个符号并不是由本模块定义的,在本某块找不到定义
- 表示还未被分配空间(位置)的未初始化的数据目标,比如未初始化的全局变量。
4.4 Hello.o的结果解析
(以下格式自行编排,编辑时删除)
输入命令:objdump -d -r hello.o >hello.asm
图4.4 objdump -d -r hello.o >hello.asm
- 机器语言的构成
机器语言是机器能直接识别的程序语言或指令代码,无需经过翻译,每一操作码在计算机内部都有相应的电路来完成它,或指不经翻译即可为机器直接理解和接受的程序语言或指令代码。机器语言使用绝对地址和绝对操作码。不同的计算机都有各自的机器语言,即指令系统。从使用的角度看,机器语言是最低级的语言。而汇编语言的主体是汇编指令,是机器指令便于记忆的表示形式,为了方便程序员读懂和记忆的语言指令。主要与机器指令在指令的表示方法上有所不同。
一条机器指令必须包含以下内容
- 操作码,它具体说明了操作的性质和功能,每一条指令都有一个相应的操作码,计算机通过识别该操作码来完成不同的操作;
- 操作数的地址,CPU通过地址取得所需的操作数;
- 操作结果的存储地址,把对操作数的处理所产生的结果保存在该地址中,以便再次使用。
- 下条指令的地址。执行程序时,大多数指令按顺序依次从主存中取出执行,只有在遇到转移指令时,程序的执行顺序才会改变。
- 接下来分析hello.o的反汇编代码与hello.s文件的区别
仔细观察可以看出,大部分区别不大,只在一些地方有些许不同,下面做更详细的分析
- 分支转移
反汇编得到的代码中,跳转指令不再依靠段名称,而是直接通过地址进行跳转。这些段名称在汇编成机器语言后变消失了,故反汇编后不会再出现,具体实现如下图,在反汇编代码第14,23,47行均有类似
图4.4.2.1 分支跳转
- 函数调用
在hello.s文件中,call指令后通常为函数名称,而在反汇编文件hello.o中,call指令后为下一条指令的地址。而又因为在本程序中调用的函数都是共享库中的函数,此时无法确认地址,所以所有的相对地址均设为0,在.rela.text节中为其加入重定位信息,等连接后再进一步确认
具体如下图,在反汇编代码第20,34,40,43和48行处均有类似
图4.4.2.2 函数调用
- 全局变量访问
在hello.s文件中,全局变量由类似movl $.LC0, %edi的形式来访问,而在反汇编文件中则是类似mov $0x0,%edi的形式。这是因为.rodata节中的地址还没有确定,所以先设为0,并在.rela.text节中添加重定位条目。等链接之后再确定
具体如下图,在反汇编代码第15和31行处均有类似
图4.4.2.3 全局变量访问
图4.4.2 hello.s与hello.asm比较
4.5 本章小结
本章主要介绍了汇编文件hello.o,以及汇编的概念与作用,如何得到汇编文件的操作命令。同时对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.3 可执行目标文件hello的格式
使用命令readelf -a -W hello查看hello的ELF格式,各节的信息基本均在节头表中。包括名称(Name),类型(Type),起始地址(Address),偏移量(OFF),大小(Size)等信息
图5.3 hello的ELF格式
5.4 hello的虚拟地址空间
1. 使用edb查看hello
图5.4.1 打开edb
2. 观察Data Dump窗口,发现虚拟地址从0x400000开始到0x400fff结束
我们可以看到开头是ELF头的部分
图5.4.2.1 查看Data Dump开头
接着我们可以发现这整个空间内之间的每一节都对应5.3的节头表中的内容,例如下图存放着.interp节,保存Linux动态共享库的路径
图5.4.2.2 查看.interp节
如果查看edb的Symbols窗口,更是一目了然,从0x400000开始与5.3的节头表中内容一一对应
图5.4.2.3 Symbols窗口
- 而.dynamic节往后的内容,根据节头表的信息可知被存放在0x600790地址处,在Data Dump中查看可得.dynamic之后的节
图5.4.3 .dynamic节
5.5 链接的重定位过程分析
- 使用命令objdump -d -r hello > helloasm得到反汇编文件。
图5.5.1 得到反汇编文件
- 分析hello.asm与helloasm的区别
- 节的个数
在hello1.asm中,节的个数更多,多了比如.init和.plt等等
在hello.asm中只有.text,而hello1.asm中却有很多节
图5.5.2.1 两文件内容比较
- 在hello1.asm中不再是相对偏移地址而是可以由CPU直接寻址的绝对地址
- 同时函数调用,跳转指令也不再是相对偏移地址,而是虚拟内存地址。并且许多调用的函数都是外部链接的共享库函数,例如下图
- 对于hello的重定位分析。利用在。rela.data和.rela.text节中保存的重定位信息,来修改对各个符号的引用,即修改他们的地址。
5.6 hello的执行流程
调用命令edb –run Hello Wu 1来查看执行流程
下面列出所有过程
ld-2.27.so!_dl_start
ld-2.27.so!_dl_init
Hello!_start
libc-2.27.so!__libc_start_main
Hello!main
Hello!printf@plt
hello!atoi@plt
Hello!sleep@plt
hello!getchar@plt
libc-2.27.so!exit
5.7 Hello的动态链接分析
1. 对于动态共享链接库中PIC函数,编译器没有办法预测函数的运行时地址,所以需要添加重定位记录,等待动态链接器处理,为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。
2. 接着我们查看dl_init函数调用前后.got.plt节的变化
图5.7.2.1 调用前
图5.7.2.2 调用后
可发现填入了两个地址
3.我们查看这两个地址的内容
可以发现第一个地址指向重定位表
图5.7.3.1 重定位表
而第二个地址指向动态连接器
5.8 本章小结
本章主要介绍了链接的概念及作用,以及生成链接的命令,对hello的elf格式文件进行了深入的分析,同时也分析了hello的虚拟地址空间,重定位过程,遍历了整个hello的执行过程,并且比较了hello.o的反汇编和hello的反汇编。对于链接有了更加深入的理解
第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
进程的作用:
它提供一个假象,好像我们的程序独占地使用内存系统,处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
shell-bash的作用:是一种交互型的应用级程序,是Linux的外壳,提供了一个界面,用户可以通过这界面访问操作系统内核。
shell-bash的处理流程:
(1)读取用户输入的命令行。
(2)分析命令行字符串,若是内置命令,则立即执行
(3)如果不是内置命令,则调用fork()创建新子进程,再调用execve()执行指定程序
6.3 Hello的fork进程创建过程
(以下格式自行编排,编辑时删除)
- 首先我们输入命令 ./hello 190500812 吴宇辰 1
- 接着shell会分析该命令,由于这并非是一条内置命令,所以shell会认为是要执行当前目录下的可执行目标文件hello
- 此时shell会调用fork函数,创建一个子进程,值得注意的是子进程会得到与父进程完全相同的数据空间,栈,堆等资源,但是并非是共享而是独立的。
- 此时我们的hello程序便有了独立的地址空间开始执行
图6.3 进程图
6.4 Hello的execve过程
调用fork函数之后,子进程将会调用execve函数,来运行hello程序,如果成功调用则不再返回,若未成功调用则返回-1。
完整的加载运行hello程序需要以下几个步骤
- 首先加载器会删除当前子进程虚拟地址端。,然后创建一组新的代码、数据、堆端,并初始化为0。
- 接着映射私有区域和共享区域,将新的代码和数据段初始化为可执行文件中的内容
- 最后设置程序计数器,使其指向代码区的入口,下一次调度这个进程时,将直接从入口点开始执行
6.5 Hello的进程执行
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
- 进程上下文切换
在进程执行的某些时刻,内核可以决定抢占当前进程,重新开始一个先前被强占的进程,这个决定便是调度,而在调度的过程中,则需要上下文切换来控制转移到新的进程,上下文切换的具体操作如下
- 保存当前进程的上下文
- 恢复某个先前被强占的进程被保存的上下文
- 将控制传递给这个新恢复的进程
- 进程时间片
一个进程执行它的控制流的一部分的每一时间段叫做时间片。
- 具体程序分析
当hello程序开始时,内核将为其保存一个上下文,而当其调用函数sleep时,系统将陷入内核,而内核处理完成系统函数调用后,将执行上下文切换,将控制返回给hello程序中的下一条语句
6.6 hello的异常与信号处理
1. 可能出现的异常
(1) 中断:来自I/O设备的信号。比如输入CTRL -C或者CTRL-Z
(2) 陷阱:有意的异常,是执行一条指令的结果,调用后也会返回到下一条指令,用来调用内核的服务进行操作。
(3)故障是由错误情况引起的,它可能能够被故障处理程序修正。如果修正成功,则将控制返回到引起故障的指令,否则将终止程序。
(4)终止是不可恢复的致命错误造成的结果,通常是一些硬件的错误,处理程序会将控制返回给一个abort例程,该例程会终止这个应用程序。
2. 可能产生的信号
SIGINT,SIGSTP,SIGCONT,SIGWINCH等等
3.各种处理的分析
(1)正常运行,程序结束后,被正常回收
图6.6.3.1 正常运行
(2)运行过程中按下CTRL-C,此举会给进程发送SIGINT信号,程序将被终止回收
图6.6.3.2 按下CTRL-C
(3)运行过程中按下CTRL-Z,此举会给进程发送SIGSTP信号,hello程序将被挂起,用ps命令可以看到hello进程并没有回收,其进程号为6433,再使用jobs命令可以查看job的ID为1,状态为已停止,最后用fg 1命令,即可让其回到前台继续运行。
图6.6.3.3.1 按下CTRL-Z
接着我们继续将其挂起,用命令pstree查看进程,找到hello进程的位置
图6.6.3.3.2 hello进程的位置
最后输入kill – 9 6433终止进程,再用jobs命令查看,可见已被终止,最后用ps命令查看,也再也没有hello进程了,说明进程已经彻底终止被回收
图6.6.3.3.3 程序终止
6.7本章小结
本章主要介绍了进程的概念及其作用,对shell的功能和处理流程也进行了介绍,然后详细分析了hello程序从fork进程的创建,到execve函数执行,最后具体执行过程以及出现异常的处理。对于整个进程管理有了更加深入的理解。
第7章 hello的存储管理
7.1 hello的存储器地址空间
1. 逻辑地址:
在有地址变换功能的计算机中,访内指令给出的地址(操作数)叫逻辑地址,也叫相对地址。分为两个部分,一个部分为段基址,另一个部分为段偏移量。
2. 线性地址:
逻辑地址经过段机制后转化为线性地址,为描述符:偏移量的组合形式。分页机制中线性地址作为输入。
3. 虚拟地址:
CPU启动保护模式后,程序运行在虚拟地址空间中。与物理地址相似,虚拟内存被组织为一个存放在磁盘上的N个连续的字节大小的单元组成的数组,其每个字节对应的地址成为虚拟地址。虚拟地址包括VPO(虚拟页面偏移量)、VPN(虚拟页号)、TLBI(TLB索引)、TLBT(TLB标记)。
- 物理地址:
CPU通过地址总线的寻址,找到真实的物理内存对应地址。CPU对内存的访问是通过连接着CPU和北桥芯片的前端总线来完成的。在前端总线上传输的内存地址都是物理内存地址。
结合hello1.asm文件来理解,例如下图第81行的起始地址为0x40054a,这就是逻辑地址,这部分再加上段地址便可以得到虚拟地址,而虚拟地址经过MMU处理后便可以得到物理地址
7.2 Intel逻辑地址到线性地址的变换-段式管理
在段式管理系统中,整个进程的地址空间是二维的,即其逻辑地址由段号和段内地址两部分组成。为了完成进程逻辑地址到物理地址的映射,处理器会查找内存中的段表,由段号得到段的首地址,加上段内地址,得到实际的物理地址。这个过程也是由处理器的硬件直接完成的,操作系统只需在进程切换时,将进程段表的首地址装入处理器的特定寄存器当中。这个寄存器一般被称作段表地址寄存器。
图7.2 地址变换
7.3 Hello的线性地址到物理地址的变换-页式管理
在页表的地址变换中
MMU首先利用VPN选择PTE,将列表条目中的PPN和虚拟地址的VPO链接起来,这样便能得到对用的物理地址
图7.3 页表的地址变换
页表各个项的含义如下
(1)P:1表示页表或页在主存中;P=0表示页表或页不在主存,即缺页,此时需将页故障线性地址保存到CR2。
(2)R/W:0表示页表或页只能读不能写;1表示可读可写。
(3)U/S:0表示用户进程不能访问;1表示允许访问。
(4)PWT:控制页表或页的cache写策略是全写还是回写(Write Back)。
(5)PCD:控制页表或页能否被缓存到cache中。
(6)A:1表示指定页表或页被访问过,初始化时OS将其清0。利用该标志,OS可清楚了解哪些页表或页正在使用,一般选择长期未用的页或近来最少使用的页调出主存。由MMU在进行地址转换时将该位置1。
(7)D:修改位(脏位dirty bit)。页目录项中无意义,只在页表项中有意义。初始化时OS将其清0,由MMU在进行写操作的地址转换时将该位置1。
(8)高20位是页表或页在主存中的首地址对应的页框号,即首地址的高20位。每个页表的起始位置都按4KB对齐。
7.4 TLB与四级页表支持下的VA到PA的变换
为了减少页表过大导致的空间浪费,我们采用多级页表来压缩大小
图7.4.1 多级页表
在四级页表层次结构的地址翻译中,虚拟地址被划分为4个VPN和1个VPO。每个第i个VPN都是一个到第i级页表的索引,第j级页表中的每个PTE都指向第j+1级某个页表的基址,第四级页表中的每个PTE包含某个物理页面的PPN,或者一个磁盘块的地址。为了构造物理地址,在能够确定PPN之前,MMU必须访问四个PTE。
图7.4.2 四级页表翻译
7.5 三级Cache支持下的物理内存访问
此时我们已经得到了物理地址,首先我们利用其中的CI进行组索引,得到匹配的组后,按照标志位CT的内容进行匹配,如果匹配成功并且有效位为1,则命中,即可按照偏移量取出数据。如果不命中,就要向下一级的cache寻找数据,三级都没有,则要向内存中寻找。找到之后更换cache中的空闲块,若没有空闲块,则需要根据自己的策略来驱逐一个块来更新
图7.5 3级cache
7.6 hello进程fork时的内存映射
当fork函数为hello进程创建虚拟内存时,它创建了当前进程的mm_struct区域结构和页表的原样副本,并且将这两个进程的每个页面标记为只读,以及两个两个进程中每个区域结构都标记为私有的写时复制。在hello进程中返回时,hello进程拥有与调用fork进程相同的虚拟内存。
图7.6 fork时的内存映射
7.7 hello进程execve时的内存映射
execve函数在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。
这一过程需要下面几步
(1)删除已存在的用户区域
(2)映射私有区域
(3)映射共享区域
(4)设置程序计数器(PC)
这些步骤在上面已经详细介绍过,这里不再赘述
图7.7 映射空间
7.8 缺页故障与缺页中断处理
指令引用一个虚拟地址,传给MMU,MMU在查找页表时,发现对应的物理地址并不在内存中,此时便发生了异常,内核中的缺页异常处理程序会先选择一个牺牲页面,如果这个页面已经牺牲过了则会把它换到磁盘,换入新的页面并更新页表。之后返回原来的进程,再次执行引起缺页的命令,此时已经可以正常运作了
图7.8 缺页异常处理
7.9动态存储分配管理
动态内存分配器动态内存分配器维护者一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护,每个块要么是已分配的,要么是空闲的。分配器主要分为显式分配器和隐式分配器。
下面将介绍一些基本方法与策略
(1)显式空间链表的堆块结构:将空闲块组织成链表形式的数据结构。堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个pred(前驱)和succ(后继)指针。
图7.9.1 显式空间链表
(2)带边界标记的隐式空闲链表:一个块是由一个字的头部、有效载荷、可能的一些额外的填充,以及在块的结尾处的一个字的脚部组成的。头部编码了这个块的大小(包括头部和所有的填充),以及这个块是已分配的还是空闲的。
图7.9.2 带边界标记的隐式空闲链表
(3)适配块策略有以下三种
首次适配:从头开始搜索空闲链表,选择第一个合适的空闲块:可以取总块数(包括已分配和空闲块)的线性时间,但是会在靠近链表起始处留下小空闲块的“碎片”。
下一次适配:和首次适配相似,只是从链表中上一次查询结束的地方开始,优点是比首次适应更快:避免重复扫描那些无用块。但是一些研究表明,下一次适配的内存利用率要比首次适配低得多。
最佳适配:查询链表,选择一个最好的空闲块适配,剩余最少空闲空间,优点是可以保证碎片最小——提高内存利用率,但是通常运行速度会慢于首次适配。
(4)合并策略则分为4种
在情况1中,两个邻接的块都是已分配的,因此不可能进行合并。所以当前块的状态只是简单地从已分配变成空闲。在情况2中,当前块与后面的块合并。用当前块和后面块的大小的和来更新当前块的头部和后面块的脚部。在情况3中,前面的块和当前块合并。用两个块大小的和来更新前面块的头部和当前块的脚部。在情况4中,要合并所有的三个块形成一个单独的空闲块,用三个块大小的和来更新前面块的头部和后面块的脚部。
(5)链表的维护方式则有两种
一是后进先出的顺序,而是按照地址顺序来维护
(6)最终我们采取策略的首要目的就是保证吞吐率和内存使用率的最大化
7.10本章小结
本章主要介绍了hello的存储地址空间,段式管理,页表管理,TLB与四级页表支持下的VA到PA的变换,三级cache支持下的物理内存访问,hello进程fork和execve时的内存映射,缺页故障与缺页中断处理和动态存储分配管理等内容。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
(以下格式自行编排,编辑时删除)
设备的模型化:文件
所有的I/O设备都被模型化为文件(甚至是内核),而所有的输入和输出都被当作相应文件的读和写来完成
设备管理:unix io接口
这种将设备优雅的映射为文件的方式,允许Linux内核引出一个简单的,低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。
8.2 简述Unix IO接口及其函数
1. 接口的操作
(1)打开文件:程序要求内核打开文件,内核返回一个小的非负整数(描述符),用于标识这个文件。程序在只要记录这个描述符便能记录打开文件的所有信息。
(2)shell在进程的开始为其打开三个文件:标准输入、标准输出和标准错误。
(3)改变当前文件的位置:对于每个打开的文件,内核保存着一个文件位置k,初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek操作显式地设置文件的当前位置为k。
(4)读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的文件,当k>=m时执行读操作会出发一个称为EOF的条件,应用程序能检测到这个条件,在文件结尾处并没有明确的EOF符号。
(5)关闭文件:内核释放打开文件时创建的数据结构以及占用的内存资源,并将描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
2. 函数
(1)打开文件:int open(char* filename,int flags,mode_t mode)
flag: O_RDONLY(只读),O_WRONLY(只写),O_RDWR(可读写)
mode: 指定新文件的访问权限位
返回值:成功则为文件描述符,失败则返回-1
(2)关闭文件:int close(fd)
返回值:成功则返回0,失败则返回-1
(3)读文件ssize_t read(int fd,void *buf,size_t n)
read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。
返回值:成功则返回读的字节数,出错则返回-1,EOF返回0
(4)写文件ssize_t wirte(int fd,const void *buf,size_t n)
write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。
返回值:成功则返回读的字节数,出错则返回-1
8.3 printf的实现分析
1. 首先查看printf函数体
图8.3.1 prinf函数体
观察可知首先arg将获得第二个不定长参数,即输出的时候格式化串对应的值。接着函数调用了vsprintf函数,我们查看其代码
2. 查看vsprintf函数
图8.3.2 vsprintf函数
观察可知次函数的作用为格式化,按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。最后在printf函数中调用write函数,将buf输出
3.查看write函数
图8.3.3 write函数
int INT_VECTOR_SYS_CALL语句表示要通过系统来调用sys_call函数。
4.查看sys_call函数
图8.3.4 sys_call函数
此函数将字符串从寄存器中通过总线复制到显卡的显存中去,显存中存储的是字符的ASCII码。字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。于是我们的打印字符串“Hello 1190500812 吴宇辰”就显示在了屏幕上。
8.4 getchar的实现分析
1.getchar函数运行时,控制权会交给os,用户按键,输入的内容便会显示在屏幕上。按下回车键表示输入完成,这时控制权将被交还给程序。
2.异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
3.getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章介绍了Linux的IO设备管理方法,Unix IO接口及其函数,分析了如何实现printf和getchar函数。对文件的操作有了更深入的理解
结论
用计算机系统的语言,逐条总结hello所经历的过程。
hello的一生波澜不惊却又十分精彩,下面将总结它的人生经历
- 编写得到hello.c程序
- hello.c经过预处理得到hello.i
- hello.i经过编译得到汇编文件hello.s
- hello.s经过汇编得到可重定位目标文件hello.o
- hello.o经过链接得到可执行文件hello
- 输入命令 ./hello 1190500812 吴宇辰 1 运行程序
- shell调用fork函数创建子进程,并调用execve函数加载运行hello程序
- cpu将为其分配时间片,在自己的时间片里,hello顺序执行自己的逻辑控制流
- hello执行过程中,会访问内存,请求一个虚拟地址,再由MMU将其转化为物理地址,通过cache来访问
- 运行过程中,同时也会调用一些函数,例如printf函数,这些函数与linux I/O的设备模拟化密切相关
- 运行过程中,还会遇到各种各样的信号,shell为其准备了各种的信号处理程序
- 最后hello程序结束,被父进程回收。内核会收回它的所有信息。由此,hello结束了它的一生
感悟:
一个简简单单的hello程序背后,是设计者庞大而缜密的设计实现。一个个的步骤是如此的精密而准确,不得不让人去感叹计算机的精巧,同时通过这次作业也更加加深了我对计算机系统的理解,让我大受震撼。
附件
1.hello.c——原文件
2.hello.i——预处理后所得文本文件
3.hello.s——编译后所得的汇编文件
4.hello.o——汇编后所得可重定位目标执行文件
5.hello——链接后所得可执行目标文件
6.hello.elf——hello.o的ELF格式,可以查看hello.o的各节信息
7.hello.asm——hello.o反汇编得到的文件,用来看汇编后的汇编代码
8.hello1.asm——hello的反汇编得到的文件,用来看链接器链接后的汇编代码
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 林来兴. 空间控制技术[M]. 北京:中国宇航出版社,1992:25-42.
[2] 辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.
[3] 赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).
[4] 谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.
[5] KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.
[6] CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.
[7] printf 函数实现的深入剖析 https://www.cnblogs.com/pianist/p/3315801.html
[8] 虚拟地址、逻辑地址、线性地址、物理地址 https://blog.csdn.net/rabbit_in_android/article/details/49976101