HITCSAPP大作业——程序人生

 

摘  要

本文基于linux系统,利用gcc,edb,objdump等工具,研究hello.c源程序是如何一步步变为可执行程序hello的。结合课内外知识。将计算机系统软硬件方面的知识与实际的程序生成,编译,链接,运行等结合起来,分析hello的虚拟地址空间,进程的创建与回收等,将整本书的内容融会贯通。通过分析hello这一个程序的生命周期,构建完整的计算机体系结构的知识。

关键词:Linux系统;计算机体系结构;程序的生命周期;

(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分

 

 

 

 

 

 

 

目  录

 

第1章 概述... - 4 -

1.1 Hello简介... - 4 -

1.2 环境与工具... - 4 -

1.3 中间结果... - 4 -

1.4 本章小结... - 5 -

第2章 预处理... - 6 -

2.1 预处理的概念与作用... - 6 -

2.2在Ubuntu下预处理的命令... - 6 -

2.3 Hello的预处理结果解析... - 7 -

2.4 本章小结... - 7 -

第3章 编译... - 8 -

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

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

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

3.3.1 hello.s文件开头部分解析... - 9 -

3.3.2 hello.s文件C数据解析... - 9 -

3.3.3 hello.s中的赋值解析... - 11 -

3.3.4 hello.s中的类型转换解析... - 11 -

3.3.5 hello.s中的算术操作解析... - 12 -

3.3.6 hello.s中的关系操作解析... - 12 -

3.3.7 hello.s中的控制转移操作的解析... - 14 -

3.3.8 hello.s中的函数操作解析... - 15 -

3.4 本章小结... - 17 -

第4章 汇编... - 18 -

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

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

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

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

4.5 本章小结... - 24 -

第5章 链接... - 25 -

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

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

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

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

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

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

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

5.8 本章小结... - 33 -

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

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

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

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

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

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

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

6.7本章小结... - 38 -

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

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

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

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

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

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

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

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

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

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

7.10本章小结... - 45 -

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

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

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

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

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

8.5本章小结... - 47 -

结论... - 47 -

附件... - 49 -

参考文献... - 50 -

 

第1章 概述

1.1 Hello简介

P2P(From Program to Process):

在Ubuntu的终端下输入命令gcc -m64 -Og -no-pie -fno-PIC hello.c -o hello

hello.c将通过如下图示的过程,一步步转化为可执行程序hello

图1.1

hello执行时将通过调用fork为其创建子进程完成From Program to Process的过程。

O2O(From Zero-0 to Zero-0):

上述过程结束后,execve为其加载到内存空间,进入主函数后CPU为hello分配时间片,然后通过内存管理机制和各类存储器联动,hello执行控制流,输出内容。程序结束后shell调用waitpid将其回收,删除了他来过的所有痕迹,正如自白中说的那样:“我朝 CS挥一挥手,不带走一片云彩!”。

完成了从无到有再到无的From Zero-0 to Zero-0过程。

1.2 环境与工具

Intel(R) Core(TM) i5-9300H CPU @ 2.40GHz

Ubuntu20.04

Windows10 64位

Gcc,edb,objdump,vim等

1.3 中间结果

列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。

文件名称

文件作用

hello.i

预处理后的文本文件

hello.s

编译后汇编程序文本文件

hello.o

汇编后的可重定位目标程序

hello

链接后的可执行目标文件

hello.txt

hello.o的ELF格式文件

helloall.txt

hello的ELF格式文件

hello_o_obj.txt

hello.o的反汇编文件

hello_obj.txt

hello的反汇编文件

1.4 本章小结

本章简述了hello程序的一生,P2P,020的整个过程,介绍了大作业中需要的环境和工具以及生成的中间文件。

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

预处理的概念:在编译之前进行的处理。

预处理命令是由ANSI C统一规定的[1],但是它不是C语言本身的组成部分,不能直接对它进行编译,因为编译器无法识别它,必须在对程序进行通常的编译之前,先对程序中这些特殊的命令进行“预处理”。

C提供的预处理功能主要有以下三种:1.宏定义 2.文件包含 3.条件编译

预处理器cpp根据以字符#开头的命令,修改原始的C程序,读取系统头文件,并把它直接插入程序文本中。[2]

预处理的作用:处理源文件中以“#”开头的命令。包括但不限于:

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

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

3.根据“#if”,“#ifdef”后面的条件决定需要编译的代码

4.删除所有的注释“//”和“/* */”

5.添加行号

2.2在Ubuntu下预处理的命令

gcc -E hello.c -o hello.i生成预处理文件hello.i(一般情况)

本程序要求使用gcc –m64 –no-pie –fno-PIC指令

故由gcc -m64 -no-pie -fno-PIC -E hello.c -o hello.i生成hello.i文件

图2.1

生成的预处理文件hello.i

图2.2

2.3 Hello的预处理结果解析

图2.3

上图是hello.i文件中最后几十行的内容,我们可以在里面看到源程序中int main()中的内容,此文件在这之前的内容是头文件stdio.h,unistd.h,stdlib.h被插入到相应的位置。由预处理的作用我们可以知道预处理文件中应该替换了所有的宏定义“#define”

图2.4

所以我们在hello.i中搜索不到此关键字

源程序.c经过cpp的预处理后生成.i文件,由于复制了头文件中的内容,故它扩展了很多内容,仍然是一个可读的文本文件,所有宏定义都被替换。

2.4 本章小结

本章先是从理论上分析预处理的概念和作用,然后对hello.c的预处理,生成hello.i文件,结合此前的理论知识,可以对生成的.i文件有一个较清楚的结果解析。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

(注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序)

编译,就是把代码转化为汇编指令的过程,汇编指令只是CPU相关的

在这一操作时,程序完成了复杂的过程。一个程序的编译,需要完成词法分析,用于将字符串转化成内部的表示结构;语法分析,将词法分析得到的标记流生成一棵语法树;中间代码生成,此中间代码是汇编代码,但是还不可执行;代码优化;目标代码生成等。

3.2 在Ubuntu下编译的命令

Ubuntu下编译命令有

gcc –S hello.c –o hello.s   从.c文件到.s文件

gcc –S hello.i –o hello.s   从.i文件到.s文件

我在此处加上要求的编译选项 gcc -m64 -no-pie -fno-PIC

图3.1

编译后产生的.s文件

图3.2

3.3 Hello的编译结果解析

此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析

3.3.1 hello.s文件开头部分解析

编译后的hello.s仍然是一个可读文件

打开看到文件开头部分为如下图

图3.3

.file 源文件名称 这里是hello.c

.text 代码段

.section .rodata 节区包含只读数据,包含我们需要打印的字符串

.align 声明对指令或者数据的存放地址进行对齐的方式 本程序为8

.globl main 声明全局变量 main(其实就是主函数)

.type 用来指定是函数类型或是对象类型 本处main @function,意味着main就是函数

3.3.2 hello.s文件C数据解析

此文件中用到的C数据类型有整数,数组和字符串。

1.整数

main函数中的参数 int argc,此参数是后面指针数组argv中元素的个数(即输入的参数个数)

在main函数中定义了一个int i,此参数是作为循环变量使用

本程序中i存储在栈中-4(%rbp)的位置上

图3.4

图3.5

还有就是.s文件中的立即数,直接在汇编指令中出现

2.数组

指针数组char *argv[],存放的参数值,char *大小为8字节

argv[0]指向输入的程序路径及名称,从argv[1]开始存放参数值,也就是说argv[1]的地址为argv的起始地址+8处,argv[2]为+16处

在此hello.s文件中,调用两次%rax+字节数来取出参数值

图3.6

3.字符串

程序中的字符串

图3.7

在源程序中是这两处,如下图

图3.8

在第一个printf函数中,输出的格式化字符串,但是我们无法在hello.s中直接看到汉字字符,因为此输出在hello.s中声明为.LC0下的被编码为UTF-8格式的形式,一个汉字在UTF-8编码用三个字节来表示,用\区分每个字节。

第二个printf函数中传入Hello %s %s,后面两个参数是从main函数中传入的前两个参数,只读。

3.3.3 hello.s中的赋值解析

程序中赋值一般使用mov指令实现,根据数据长度的不同,有不一样的后缀

b,w,l,q分别代表1,2,4,8个字节

在此程序中出现的赋值语句为i = 0

汇编语言实现如下图

图3.9

i是int类型的,4个字节,所以mov后缀是l

3.3.4 hello.s中的类型转换解析

类型转换大概有以下几种类型:

1.从int转换为float

这种情况不会发生溢出,但可能有数据被舍入

2.从int或 float转换为double

因为double的有效位数更多,所以能够保留精确值

3.从double转换为float和int

由于有效位数变少,所以可能被舍入;也有可能发生溢出

4.从float 或double转换为int

因为int没有小数部分,所以数据可能会向0方向被截断

此程序没有隐式的类型转换

但是使用了一个转换函数atoi,将字符串转换为整数

sleep(atoi(argv[3]));

此函数的作用就是将我们在命令行上键入的argv[3],即第三个参数,给转换成int类型值传递给sleep函数,也就是把秒数传递给sleep函数。

int atoi(const char *nptr)

它的头文件包含在#include 中

功能:将字符串转换成整型数;atoi()会扫描参数nptr字符串,跳过前面的空格字符,直到遇上数字或正负号才开始做转换,而再遇到非数字或字符串时('\0')才结束转化,并将结果返回。

3.3.5 hello.s中的算术操作解析

本程序中的算术操作有 i++,计数器i的自增操作,汇编指令为

addl $1, -4(%rbp)

后缀l表示i四字节数据

还有就是汇编代码中对栈指针%rsp和帧指针%rbp的add和sub操作,我认为这不是严格意义上的算术操作

3.3.6 hello.s中的关系操作解析

从《CSAPP》上我们可以找到汇编代码中跳转指令的解析,如下图

图3.10[2]

本程序只用到了上图中的对jle,jmp和je的条件码判断,set和cmov没有用到

图3.11

1.jmp直接跳转

对应源程序中for循环刚开始,将i赋值为0之后的操作,跳转到.L3,判断i与7的大小,若i<=7则执行循环体中的内容

2.je跳转

对应源程序中

if(argc!=4)

前面的cmpl  $4, -20(%rbp)用来判断argc是否等于4,然后设置条件码,为下一步je利用条件码ZF进行跳转作准备。

即argc = 4则跳转到.L3

3.jle跳转

对应源程序中

for(i=0;i<8;i++)

cmpl  $7, -4(%rbp)

判断i是否<=7,计算i-7然后设置条件码,为下一步jle利用条件码(SF^OF)|ZF进行跳转做准备。

即i<=7时跳转到.L4

3.3.7 hello.s中的控制转移操作的解析

此程序中涉及到的控制转移有:for循环和if判断,没有用到用条件传送cmov进行的判断

1.if判断

if(argc!=4)

当argv不等于4的时候执行程序段if中的代码。

对于if判断,编译器使用跳转指令实现,首先cmpl比较argv和4,对于比较操作,是用减法实现的,并设置条件码,使用je判断ZF标志位,如果为0,说明 argv-4=0即argv==4,则不执行if中的代码直接跳转到.L2顺序执行下一条语句。

图3.12

2.for循环

for(i=0;i<8;i++)

计数变量为i,跳转循环8次。首先将i赋值为0后无条件跳转到.L4的比较代码。用cmpl进行比较,如果i<=7,则跳转到.L4循环体执行,否则说明循环结束,顺序执行for循环体之后的代码,即getchar及其之后的部分。

图3.13

3.3.8 hello.s中的函数操作解析

过程提供了一种封装代码的方式,用一组指定的参数和可选的返回值实现某种功能。函数是一种过程,善于利用函数,可以减少重复编写程序段的工作量。

程序是按顺序执行的指令流水线 (PipeLine)。分支和循环逻辑,可以通过在流水线中往后跳或往前跳实现。其实,函数调用也不过是在跳转。调用某个函数,就跳转到那个函数的指令流的开始位置,函数执行完成后,再跳转回来。函数能获取外部写入的数据(输入),能持有自己独特的数据(本地状态),还能向外部写数据(输出)。[3]

例如过程P调用过程Q,Q执行后返回到P,这些动作包含下面一个或多个机制:[2]

1.     传递控制:进行过程 Q 的时候,程序计数器必须设置为 Q 的代码的起始地址,然后在返回时,要把程序计数器设置为 P 中调用 Q 后面那条指令的地址。

2.     传递数据:P 必须能够向 Q 提供一个或多个参数,Q 必须能够向 P 中返回一个值。分配和释放内存:在开始时,Q 可能需要为局部变量分配空间,而在返回前,又必须释放这些存储空间。

3.     分配和释放内存:在开始时Q可能需要为局部变量分配空间,而在返回时,必须释放这些存储空间。

本程序中涉及的函数操作有:

1.main函数(主函数)

main 函数被系统启动函数__libc_start_main调用,call指令将下一条指令的地址压栈然后跳转到main函数,即传递控制。

main函数中的参数为argc和argv,分别使用%rdi和%rsi存储,返回时将%eax 赋为0,也就是return0。

图3.14

最后这个leave指令,相当于将栈恢复为调用main函数之前的状态,然后ret,即从栈中pop出调用main函数之前压入栈中的下一条指令的地址,也就是返回地址。

2.printf函数

第一处printf函数

图3.15

将%edi设置为.LC0的首地址,这个就是("用法: Hello 学号 姓名 秒数!\n")的首地址。然后call puts,而不是call printf。这有点出人意料,不过不用担心,这是编译器对printf的一种优化。实践证明,对于printf的参数如果是以'\n'结束的纯字符串,printf会被优化为puts函数,而字符串的结尾'\n'符号被消除。除此之外,都会正常生成call printf指令。[4]调用puts函数的好处是puts函数可以立即刷新输出缓冲区。

第二处printf函数

图3.16

将%edi赋为.LC1的首地址,即为字符串"Hello %s %s\n"的首地址,设置%rsi为argv[1](%rbp-24),%rdx为argv[2](%rbp-16)。由于次字符串不是纯字符串,故call printf。

3.exit函数

图3.17

将%edi设置为1,然后退出。

4.sleep函数

图3.18

将%eax赋给%edi,即将%edi设置为atoi(argv[3])。然后call sleep,进行休眠

5.atoi函数

参数为argv[3],利用%rdi传参,通过一系列的mov和add指令,将(%rbp-8)处的数据赋给%edi

然后call atoi,调用此函数

atoi函数作用见3.3.4节

6.getchar函数

无数据传递,有控制传递call getchar

3.4 本章小结

本章通过编译hello.i文件,获得hello.s文件,结合编译的知识,分析了此过程发生的相应处理,并详细的阐述了数据、赋值、类型转换、算术操作、逻辑/位操作、关系操作、数组/指针/结构操作、控制转移、函数操作的过程,对hello.s文件的上述过程进行了相应的解析。

(第32分)

第4章 汇编

4.1 汇编的概念与作用

(注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。)

把汇编语言翻译成机器语言的过程称为汇编。

汇编器(as)将hello.s翻译成机器语言指令,并把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在二进制目标文件hello.o中。hello.o文件是一个二进制文件。若用文本编辑器打开此.o文件将会看到一堆乱码。[2]

4.2 在Ubuntu下汇编的命令

应截图,展示汇编过程!

Ubuntu下汇编指令有

gcc –c hello.c –o hello.o

gcc –c hello.s –o hello.o

as hello.s -o hello.o

此处我使用gcc –m64 –no-pie –fno-PIC –c hello.s –o hello.o指令,如下图

图4.1

生成的.o文件如下

图4.2

4.3 可重定位目标elf格式

分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。

用vim文本编辑D器打开此.o文件如下图

图4.3

是一堆乱码。.o文件是不能用文本显示的

下面利用readelf分析此.o文件

下图是典型的ELF可重定位目标文件的格式,以及各个节包含的内容

 

图4.4

1.ELF头

利用readelf工具可以查看到此hello.o文件的ELF头内容

图4.5

ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。[2]

由于我的Ubuntu版本是中文,所以在此文件中已给出了部分信息的中文(如上图)在此就不赘述了。值得注意的是第19行是节头部表数量,这里是14,第20行是字符串表在节头部表中的索引,这里是13。

2.节头部表分析

利用readelf工具可以看到hello.o文件的可重定位目标文件的Section Herders如下图所示,在下图中我们可以看见各个节的名称,及此节的大小,地址,偏移量等信息。

图4.6

其中重定位项目有

.rela.text:可重定位代码

.rela.eh_frame

内容如下图所示

图4.7

可以在.rela.text重定位节中看到此节的偏移量,类型等信息

从类型栏中可以看见.rela.text节是使用的绝对地址寻址,而.rela.eh_frame节使用PC相对地址寻址。

ELF重定位条目的格式如下图所示

图4.8

Offset:需要进行重定向的代码在.text或.data节中的偏移位置。

Symbol标识被修改引用应该指向的符号

Type告知链接器如何修改新的引用

Addend是一个有符号常数,一些类型的重定位要使用它对被修改引用的值做偏移调整。[2]

重定位算法[2]算法如下图示

当生成可执行目标文件时,链接器会对这些重定位项目进行引用,重新确定其在可执行目标文件中的地址。

图4.9

3.符号表.symbol

图4.10

符号表,用来存放程序中定义和引用的函数和全局变量的信息。重定位需要引用的符号都在其中声明

4.4 Hello.o的结果解析

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

说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。

图4.11

对照分析:

1. .s汇编代码会有一些不知意义的伪代码,如下图示

图4.12

这些可能是用来标识一些信息

2.分支转移,.s汇编代码是跳到.LX位置,这个是段名称,这个段名称只是在汇编语言中便于编写的助记符,而hello.o反汇编的文件中则不再出现类似符号,跳转地址则是类似的相对寻址方式

3.函数调用,在.s 汇编代码中,函数调用之后直接跟着函数名称,如下图示

图4.13

而在hello.o的反汇编文件中call后跟的都是地址,而不是函数名称,但是值得注意的是,在相应的机器指令代码中call后的指令全为0,如下图示,这是因为hello.o文件还没有与共享库链接,此时无法确定函数的最终地址,即还需要重定位。

图4.14

1f: R_X86_64_PLT32这样的信息表示重定位信息在.rela.text节中,等待链接的时候再确定它们的具体信息,填进去。

4.反汇编器objdump使用的指令命名规则与GCC生成的.s汇编代码使用的有些细微的差别,hello.o的反汇编代码省略了很多指令结尾的q,这些后缀是大小指示符,而又给ca11和ret指令添加了‘q’后缀。

4.5 本章小结

本章对hello.s文件进行汇编得到hello.o文件,分析了hello.o文件的可重定位ELF格式,节头部表的各部分详细含义,并对重定位项目进行了刨析。利用反汇编工具objdump反汇编hello.o文件,与hello.s文件进行对比,了解到从汇编语言映射到机器语言时汇编器需要实现的一些转换,并对结果进行了相应的解析。

(第41分)

第5章 链接

5.1 链接的概念与作用

注意:这儿的链接是指从 hello.o 到hello生成过程。

链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载到内存并执行。链接可以执行于编译时,也就是在源代码被编译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至于运行时,也就是由应用程序来执行。[2]

链接使分离编译成为可能。

5.2 在Ubuntu下链接的命令

使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件

链接指令:

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

生成hello可执行文件

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

分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。

使用指令readelf -a hello > helloall.txt生成helloall.txt文件

按顺序写出ELF格式的可执行目标文件的各类信息,如下图

图5.2

先看ELF头,如下图示

图5.3

相应信息在图中已表示出

ELF Header:以16B的序列Magic开始,Magic描述了生成该文件的系统的字的大小和字节顺序,ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型、机器类型、字节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量等信息。

然后我们查看节头部表信息,如下图

图5.4

包含了文件中出现的各个节的语义,包括节的类型、位置和大小等信息。

我们将两部分一起来分析一下

ELF头中信息确定了节头部表的开始和大小,如下图

图5.5

在可重定位目标文件的elf中,入口点地址和程序头起点是0,当程序被链接后生成的可执行文件中的elf中都被填入了正确的地址,并且也增加了许多节

和可重定位目标文件相比,可执行文件增加了一个Dynamic section,如下图

图5.6(a)是可重定位目标文件的elf,(b)是可执行文件中的(注意这里提到了本文件中没有程序头,下文会对可执行文件中的程序头进行分析)

图5.6(a)

图5.6(b)

不再有.rela.text和.rela.eh_frame节

图5.7

符号表中也增加了许多内容

图5.8

Value是字符串在strtab节中的偏移量

Size是目标字节数

Type是类型:数据、函数、源文件、节、未知

Bind代表绑定的属性:全局符号、局部符号、强弱符号

Ndx符号对应目标所在的节,或其他情况,比如UND节

接下来我们分析可执行文件中特有的部分,程序头,如下图

图5.9

程序头表描述可执行文件中的节与虚拟空间中的存储段之间的映射关系

图4.4给出了程序头表包含的内容。

5.4 hello的虚拟地址空间

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

edb的Data Dump可以看到hello程序的虚拟地址

图5.10

由图可知,虚拟空间从0x400000开始。这与书中给出的结论一致

利用edb工具我们查看Loaded Symbols,如下图

图5.11

对照图5.9

每一个表项提供了各段在虚拟地址空间和物理地址空间的大小、位置、标志、访问权限和对齐方面的信息。

5.5 链接的重定位过程分析

(objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。

结合hello.o的重定位项目,分析hello中对其怎么重定位的。)

objdump -d -r hello内容如下

图5.12

对比之后可以发现,hello和hello.o反汇编生成的代码完全相同,二者的区别在于:后者地址是相对偏移,前者地址是可由CPU直接访问的虚拟地址。hello中不存在类似R_X86_64_32.rodata这样的利用相对偏移求地址的字段,这说明在链接时,链接器把hello.o中的相对偏移的寻址加上程序在虚拟内存中的起始地址得到了可直接访问的地址。

5.6 hello的执行流程

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

ld-2.27.so!_dl_start

ld-2.27.so!_dl_setup_hash

ld-2.27.sodl_sysdep_start

ld-2.27.so!_dl_init

libc-2.27.so!_cxa_atexit

libc-2.27.so!_new_nextfn

hello!main

hello!puts@plt

hello!exit@plt

hello! printf@plt

hello!sleep@plt

hello!getchar@plt

5.7 Hello的动态链接分析

(分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。)

dl_init前PIC函数调用的目标地址都实际指向PLT中的代码逻辑,GOT部分采取了延迟绑定[5],GOT保存下一命令的地址。dl_init执行后,重定位确定函数地址。

GOT表位置如下图示

图5.13

5.8 本章小结

本部分对hello.o进行链接,获得可执行文件hello,查看了hello的elf格式并对其进行了分析,查看了hello的虚拟空间地址的各段信息,利用objdump查看hello.o与hello的不同,由此分析说明了在链接过程中的重定位,查看了hello的执行流程,都经历了哪些函数。最后对hello的动态链接进行了简要的分析。

(第51分)

第6章 hello进程管理

6.1 进程的概念与作用

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

在现代系统上运行一个程序时,我们会得到一个假象,就好像我们的程序是系统中当前运行的唯一的程序一样。我们的程序好像是独占地使用处理器和内存。处理器就好像是无间断地一条接一条地执行我们程序中的擅令。最后,我们程序中的代码和数据好像是系统内存中唯一的对象。这些假象都是通过进程的概念提供给我们的。

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

Linux系统中,Shell是一个交互型应用级程序,代表用户运行其他程序(是命令行解释器,以用户态方式运行的终端进程)。

其基本功能是解释并运行用户的指令,重复如下处理过程:

(1)终端进程读取用户由键盘输入的命令行。

(2)分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量

(3)检查第一个(首个、第0个)命令行参数是否是一个内置的shell命令

(3)如果不是内部命令,调用fork( )创建新进程/子进程

(4)在子进程中,用步骤2获取的参数,调用execve( )执行指定程序。

(5)如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid(或wait...)等待作业终止后返回。

(6)如果用户要求后台运行(如果命令末尾有&号),则shell返回;

6.3 Hello的fork进程创建过程

在终端中输入命令,shell会先解析是否为内置命令,若是则执行内置命令,否则将会此命令解析为可执行文件,在当前目录下寻找此文件,./hello被shell解析为可执行程序。

执行hello如下图示

图6.1

之后shell会调用fork函数创建一个新的子进程,此子进程和父进程即shell进程拥有完全相同的地址空间包括只读代码段、读写数据段、堆及用户栈等。新创建的子进程几乎但不完全与父进程相同,子进程得到与父进程用户级虚拟地址空间相同但是独立的一份副本,这就意味着,当父进程调用fork时,子进程可以读写父进程中打开的任何文件。[2]父子进程最大的区别就是他们拥有不同的PID。

6.4 Hello的execve过程

调用execve()函数在当前进程(新创建的子进程)的上下文中加载并运行hello程序。将hello中的.text节、.data节、.bss节等内容加载到当前进程的虚拟地址空间。

书上[2]给出了如下例子,一个新程序开始时,用户栈的典型组织结构,此hello程序的栈帧与此类似。

图6.2

6.5 Hello的进程执行

(结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。)

上下文切换的流程:1.保存当前进程的上下文。2.恢复某个先前被抢占的进程被保存的上下文。3.将控制传递给这个新恢复的进程。

hello的进程执行:刚开始运行时内核为其保存一个上下文,进程在用户状态下运行。循环结束后,hello调用getchar函数,进入内核模式,当前时间片用尽,执行上下文切换,把控制转移给其他进程。完成键盘输入后,内核从其他进程切换回hello进程,最终hello执行return终止进程,hello程序的时间片完全结束。

过程与下图类似

图6.3

6.6 hello的异常与信号处理

(hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。

 程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps  jobs  pstree  fg  kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。)

会出现的异常有:故障和终止;信号有:SIGINT,SIGSTP

键入CTRL-Z后shell父进程会受到SIGSTP信号,调用信号处理子程序将hello进程挂起,即停止hello进程;CTRL-C会调用信号处理子程序结束hello进程

1.ctrl-c,进程结束

图6.4

2.运行时按回车,空格和乱按键盘,可以看到不会打断程序运行

图6.5

3.在挂起下运行ps等指令

图6.6

6.7本章小结

本章主要介绍了hello在shell中是如何运行的,较详细的分析了hello执行过程中的进程管理。通过进程的概念和shell的工作流程,分析shell是如何通过调用fork函数为hello创建子进程,execve函数加载hello函数的,利用时间片的概念,分析的内核的进程调度过程,用户态和内核态的转换。最后分析了hello执行过程中遇到的异常和信号。

(第61分)

第7章 hello的存储管理

7.1 hello的存储器地址空间

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

虚拟地址(virtual memory):虚拟地址即线性地址,虚拟地址是对物理地址的映射。

逻辑地址(logical address):Intel为了兼容,将远古时代的段式内存管理方式保留了下来。逻辑地址指的是机器语言指令中,用来指定一个操作数或者是一条指令的地址。

线性地址(linear address):线性地址或也叫虚拟地址,逻辑地址经过段机制后转化为线性地址,是一个不真实的地址,线性地址对应硬件页式内存的转换前地址。[6]

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

段式管理(segmentation)方式就是直接将逻辑地址转换成物理地址,是指把一个程序分成若干个段(segment)进行存储,每个段都是一个逻辑实体(logical entity),程序员需要知道并使用它。它的产生是与程序的模块化直接有关的。段式管理是通过段表进行的,它包括段号或段名、段起点、装入位、段的长度等。此外还需要主存占用区域表、主存可用区域表。

为了进行段式管理,除了系统需要为每道程序分别设置段映象表外,还得由操作系统为整个主存系统建立一个实主存管理表,它包括占用区域表和可用区域表两部分。占用区域表的每一项(行)用来指明主存中哪些区域已被占用,被哪道程序的哪个段占用以及该段在主存的起点和长度。此外,还可以设置诸如该段是否进入主存后被改写过的字段,以便该段由主存中释放时,决定是否还要将其写回到辅存中原先的位置来减少辅助操作。可用区域表的每一项(行)则指明每一个未被占用的基地址和区域大小。当一个段从辅存装入主存时,操作系统就在占用区域表中增加一项,并修改可用区域表。而当一个段从主存中退出时,就将其在占用区域表的项(行)移入可用区域表中,并进行有关它是否可与其它可用区归并的处理,修改可用区域表。当某道程序全部执行结束或者是被优先级更高的程序所取代时,也应将该道程序的全部段的项从占用区域表移入可用区域表并作相应的处理。[8]

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

将各进程的虚拟空间划分成若干个长度相等的页(page),页式管理把内存空间按页的大小划分成片或者页面(page frame),然后把页式虚拟地址与内存地址建立一一对应页表,并用相应的硬件地址变换机构,来解决离散地址变换问题。页式管理采用请求调页或预调页技术实现了内外存存储器的统一管理。

图7.1页表示意图

任何页表都被分为如下三种状态之一:

未分配的:系统还未分配(或创建)的页。未分配的页没有任何数据与他们相关联,因此也就不占用任何磁盘空间。

缓存的:当前已缓存在物理内存中的已分配页。

未缓存的:未缓存在物理内存中的已分配页。

现代CPU一般采用多级(4级)页表的方式,如下图

图7.2 多级页表示意图

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

Intel Core i7采用如下图示的地址翻译方案

图7.3

VA48位,其中0-11位是VPO,12-47位是VPN,被分为4个9位,分别对应于每一级页表的偏移,最后在四级页表中找到PPN,VPO = PPO,所以最后的PA = PPN + PPO(VPO),得到52位的PA

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

Cache被组织和划分为了如下图示的组织结构

高速缓存是一个高速缓存组的数组,每个组包含一个或多个行,每行包含一个有效位,一些标记位,以及一个数据块,高速缓存的结构将m个地址划分为了t个标记位,s个组索引位,b个块偏移位

图7.4

Intel Corei7 CPU采用的就是三级Cache。虚拟地址通过MMU翻译为物理地址。

PA有52位,0~5位是块偏移量(即CO),6~11位是组索引(即CI),12~51位是标记(即CT),先用CI进行组索引找到对应的组,然后在组中找到标记与CT匹配的行,如果这个行存在且有效位为1,则缓存命中,取出块偏移量为CO的字节,并传递给CPU。如果缓存未命中,则继续到L2中寻找,L2未命中到L3中,L3未命中到主存中寻找。

7.6 hello进程fork时的内存映射

Hello进程fork时的内存映射与下图类似

图7.5

也就是说当fork函数被调用时,内核为新进程创建各种数据结构,并给他分配了一个唯一的PID。新进程是父进程的一个副本,两个进程的虚拟内存映射的是同一块物理内存,只不过是写时复制的。也就是说当新进程的某一虚拟页发生了更改,与父进程的虚拟页不同时,再将新进程被更改的虚拟页映射到另一个块物理页上。

7.7 hello进程execve时的内存映射

加载器映射用户地址空间区域如下图示

图7.6

加载并运行hello程序需要以下几个步骤:

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

2.     映射私有区域

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

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

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

图7.7缺页操作图

CPU硬件执行步骤:

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

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

3.高速缓存/主存向MMU返回PTE

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

5.缺页处理程序确定出物理内存中牺牲页,若页面被修改,则把它换出到磁盘

6.缺页处理程序调入新的页面,并更新内存中的PTE

7.缺页处理程序返回到原来进程,再次执行缺页的指令

7.9动态存储分配管理

程序中的printf会调用malloc函数,一个显式分配器。程序通过调用malloc的函数来从堆中分配块。

动态存储分配管理由动态内存分配器完成。动态内存分配器维护着一个进程的虚拟内存区域,称为堆。堆是一个请求二进制零的区域,它紧接在未初始化的数据区后开始,并向上生长(向更高的地址)。分配器将堆视为一组不同大小的块的集合来维护。

每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可以用来分配。空闲块保持空闲,直到它显示地被应用程序所分配。

一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。

动态内存分配器从堆中获得空间,将对应的块标记为已分配,回收时将堆标记为未分配。而分配和回收的过程中,往往涉及到分割、合并等操作。

动态内存分配器的目标是在对齐块的基础上,尽可能地提高吞吐率及空间占用率,即减少因为内存分配造成的碎片。其实现常见的数据结构有隐式空闲链表、显式空闲链表、分离空闲链表,常见的放置策略有首次适配、下一次适配和最佳适配。[2]

7.10本章小结

本章通过介绍段式管理,页式管理的概念,来分析hello的存储空间,介绍了VA到PA的转换过程,hello进程时fork,execve的内存映射,和缺页故障及缺页处理子程序。

(第7 2分)

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

普通文件(包含任意数据的文件)、目录(文件夹,包含一组链接的文件,每个链接都将一个文件名映射到一个文件)、套接字(用来与另一个进程进行跨网络通信的文件)、命名通道、符号链接以及字符和块设备。[2]

设备管理:unix io接口

打开关闭文件,读写文件,改变文件的位置

8.2 简述Unix IO接口及其函数

Unix IO接口统一执行方式:

(1)    打开文件

(2)    Linux Shell 创建的每个进程都有三个打开的文件:标准输入,标准输出,标准错误。

(3)    改变当前文件位置

(4)    读写文件

(5)    关闭文件

1. int open(char* filename,int flags,mode_t mode)

open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。

2. int close(fd),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的实现过程调用了vsprintf和write函数,接受一个格式串之后将匹配到的参数按照格式串的形式输出。而vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。vsprintf函数生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall。字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点。[7]

8.4 getchar的实现分析

getchar()函数返回原理

当程序调用getchar()时,程序就等着用户按键。用户输入的字符被存放在键盘缓冲区中,直到用户按回车为止(回车字符也放在缓冲区中)。

当用户键入回车之后,getchar()才开始从输入流中每次读入一个字符,getchar()函数的返回值是用户输入的第一个字符的ASCII码,如出错返回EOF。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,,待后续getchar()调用读取。也就是说,后续的getchar()调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完为后,才等待用户按键。

getchar函数通过调用read函数返回字符。read函数通过sys_call中断来调用内核中的系统函数。键盘中断处理子程序会接受按键扫描码并将其转换为ASCII码后保存在缓冲区。然后read函数调用的系统函数可以对缓冲区ASCII码进行读取,直到接受回车键返回。

8.5本章小结

本章详细的分析了Unix IO及其函数,对标准printf和getchar函数进行了分析,C标准IO函数都调用了UnixIO函数,但是比其安全性更高,IO函数通过中断指令函数调用系统函数进而调用相应的中断处理程序,完成相应的操作。

(第81分)

结论

Hello一生所经历的过程如下:

1.编写,程序员通过编辑器等,将hello的源代码写入,存为.c文件

2.预处理,.c文件通过预处理器cpp将调用的库,所有的宏替换掉,合并到hello.i的文本文件中

3.编译,hello.i文件通过编译器ccl成为可读的汇编文件hello.s

4.汇编,hello.s文件通过汇编器as成为可重定位目标文件hello.o,此文件无法直接打开

5.链接,.o文件通过链接器ld与外部库链接成为可执行文件hello

6.运行,在shell中输入命令

7.创建子进程,shell调用fork函数为hello创建子进程(hello终于有生命啦)

8.hello的进程进入OS下的进程群(hello进入社会了),有了自己的唯一标识PID(有了身份证号),shell调用了execve函数为其加载内存空间,此空间是父进程的一个副本(有父母的基因),hello改变的是自己私有空间的量(但是也有自己独特的地方)。

9.执行指令,CPU为hello进程分配时间片,在自己的时间片中,hello享有CPU资源,顺序执行自己的逻辑控制流,但是hello进程也不能完全占用着CPU,他也会被挂起(社会不会就围着他自己一个人转)

10.访问内存,MMU将虚拟地址翻译为物理地址,malloc动态申请内存

11.信号,hello在运行中可以收到来自键盘输入的信号,比如CTRL-C,CTRL-Z等(hello去休息或者出来意外去世)

12.结束,hello子进程收到终止信号后就死亡了,在父进程没有调用waitpid函数将其回收前,他一直是僵死进程

13.回收,shell父进程调用waitpid函数将其回收,内核删除了为hello进程创建的所有数据结构(hello烟消云散,注销了所有)

感想:计算机系统庞大而复杂,即使是一个简单的hello程序,当我们在IDE中按下“运行”键后,计算机后台发生了很多我们很难想象的过程。通过学习这门课,初步了解了计算机系统,但是仍然需要在以后的学习中加深对其的理解和感悟。

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

附件

hello.i        预处理后的文本文件

hello.s        编译后汇编程序文本文件

hello.o        汇编后的可重定位目标程序

hello          链接后的可执行目标文件

hello.txt       Hello.o的ELF格式文件

helloall.txt     Hello的ELF格式文件

hello_o_obj.txt  hello.o的反汇编文件

hello_obj.txt    hello的反汇编文件

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

参考文献

为完成本次大作业你翻阅的书籍与网站等

[1]  https://wenku.baidu.com/view/61feeec60708763231126edb6f1af00bed57000.html

[2]  Randal E. Bryant, David R. O'Hallaron.深入理解计算机系统(原书第3版) [M]. 龚奕利,雷迎春译北京:机械工业出版社,2016

[3]  函数调用详解(函数状态保存参数传递和返回值)_WHOAMIAnony的博客-CSDN博客

[4]  printf背后的故事 - Florian - 博客园 (cnblogs.com)

[5]  https://blog.csdn.net/shenhuxi_yu/article/details/71437167

[6]  https://blog.csdn.net/zsl091125/article/details/52556766

[7]  https://www.cnblogs.com/pianist/p/3315801.html

[8]  段式存储管理_百度百科 (baidu.com)

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值