哈工大 计算机系统 大作业 程序人生-Hello’s P2P

2022-11-16,,,,

计算机系统

作业

题 目 程序人生-Hello’s P2P

专 业 计算机

学   号

班   级

学 生  

指 导 教 师

计算机科学与技术学院

2021年6月

摘 要

本文主要通过分析hello这个程序的一生,回顾了这学期计算机系统这门课的几乎所有知识。在分析过程中使用ubuntu作为操作系统,并使用了一些工具辅助完成,目的是对于计算机系统的工作与原理有更深的了解。

关键词:计算机系统;程序的一生;P2P;O2O;

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

目 录

第1章 概述 - 5 -

1.1 HELLO简介 - 5 -

1.2 环境与工具 - 5 -

1.3 中间结果 - 5 -

1.4 本章小结 - 6 -

第2章 预处理 - 7 -

2.1 预处理的概念与作用 - 7 -

2.2在UBUNTU下预处理的命令 - 7 -

2.3 HELLO的预处理结果解析 - 7 -

2.4 本章小结 - 8 -

第3章 编译 - 9 -

3.1 编译的概念与作用 - 9 -

3.2 在UBUNTU下编译的命令 - 9 -

3.3 HELLO的编译结果解析 - 9 -

3.3.1 数据 - 10 -

3.3.2 赋值 - 12 -

3.3.3 类型转换 - 12 -

3.3.4 算术操作 - 13 -

3.3.5 关系操作 - 13 -

3.3.6数组/指针/结构操作 - 14 -

3.3.7函数调用 - 14 -

3.4 本章小结 - 15 -

第4章 汇编 - 17 -

4.1 汇编的概念与作用 - 17 -

4.2 在UBUNTU下汇编的命令 - 17 -

4.3 可重定位目标ELF格式 - 17 -

4.3.1 命令 - 17 -

4.3.2 ELF头 - 17 -

4.3.3 节头表 - 18 -

4.3.4 重定位节 - 18 -

4.3.5 符号表 - 19 -

4.4 HELLO.O的结果解析 - 19 -

4.5 本章小结 - 21 -

第5章 链接 - 22 -

5.1 链接的概念与作用 - 22 -

5.2 在UBUNTU下链接的命令 - 22 -

5.3 可执行目标文件HELLO的格式 - 22 -

5.3.1 ELF头 - 22 -

5.3.1节头 - 23 -

5.4 HELLO的虚拟地址空间 - 23 -

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

5.6 HELLO的执行流程 - 25 -

5.7 HELLO的动态链接分析 - 26 -

5.8 本章小结 - 27 -

第6章 HELLO进程管理 - 28 -

6.1 进程的概念与作用 - 28 -

6.2 简述壳SHELL-BASH的作用与处理流程 - 28 -

6.3 HELLO的FORK进程创建过程 - 29 -

6.4 HELLO的EXECVE过程 - 29 -

6.5 HELLO的进程执行 - 29 -

6.6 HELLO的异常与信号处理 - 30 -

6.7本章小结 - 34 -

第7章 HELLO的存储管理 - 35 -

7.1 HELLO的存储器地址空间 - 35 -

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

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

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

7.5 三级CACHE支持下的物理内存访问 - 37 -

7.6 HELLO进程FORK时的内存映射 - 38 -

7.7 HELLO进程EXECVE时的内存映射 - 38 -

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

7.9动态存储分配管理 - 39 -

7.10本章小结 - 40 -

第8章 HELLO的IO管理 - 41 -

8.1 LINUX的IO设备管理方法 - 41 -

8.2 简述UNIX IO接口及其函数 - 41 -

8.3 PRINTF的实现分析 - 42 -

8.4 GETCHAR的实现分析 - 43 -

8.5本章小结 - 43 -

结论 - 44 -

附件 - 45 -

参考文献 - 46 -

第1章 概述

1.1 Hello简介

根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。

P2P:首先,Hello的开始是一段储存在磁盘上的程序文本(Program),在需要使用这一个代码文件的时候,用预处理器处理hello.c文件,生成一个hello.i文件,也就是修改了的源程序,之后,hello.i输入编译器,编译器将生成一个hello.s文件,在这一步之前所有的文件都还是文本形式,还没有转换为二进制机器码格式。生成的hello.s文件将输入汇编器,产生一个hello.o,也就是可重定位程序,可重定位文件经过链接器的链接将生成可执行目标程序hello,此时在shell中调用相关命令将为其创建进程(Process),执行程序。

O2O:在shell中输入相关命令后,shell将调用fork函数为这一程序创建进程,之后将通过exceve在进程的上下文中加载并运行hello,将进程映射到虚拟内存空间,并加载需要的物理内存。执行时,在CPU的分配下,指令进入CPU流水线执行。当执行结束后父进程将回收这一进程,内核将清除这一进程的相关信息,这一进程就结束了。

1.2 环境与工具

列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。

硬件环境:处理器:Intel Core i7-9750H CPU @ 2.60GHz

RAM:32.00GB

系统:64位操作系统,基于x64的处理器

软件环境:Windows10 64位;Ubuntu 20.04

开发与调试工具:gcc,as,ld,vim,edb,readelf,gedit,gdb

1.3 中间结果

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

1.4 本章小结

这一章主要堆hello在执行的过程中的总体流程进行了简要的概述,以及在实验中使用的软硬件条件以及实验中产生的文件。

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

概念:预处理器(cpp)根据以字符#开头的命令,修改原始的c程序。

作用:

1.处理头文件:比如hello.c的第一行的#include<stdio.h>命令告诉预处理器读取系统有文件stdio.h的内容,并把它直接插入程序文本中。

2.处理宏定义:对于#define指令,进行宏替换,对于代码中所有使用宏定义的地方使用符号表示的实际值替换定义的符号

3.处理条件编译:根据可能存在的#ifdef来确定程序需要执行的代码段。

4.处理特殊符号:例如#error等,预编译程序可以识别一些特殊的符号,并在后续过程中进行合适的替换。

5.删除c语言源程序中的注释部分。

2.2在Ubuntu下预处理的命令

cpp hello.c > hello.i

图2.1 ubuntu下的预处理命令

2.3 Hello的预处理结果解析

可以发现,原本的源代码文件只有28行,预处理后的文件为3074行,原本的源代码部分在3055行之后,在这之前是hello引用的所有的头文件stdio.h, unistd.h , stdlib.h内容的展开。而很显然我们发现插入的部分不止有这三个头文件的内容,还出现了其他的头文件,这是以为这三个头文件中同样使用#include命令引入了其他的头文件,这些头文件同样出现在了hello.i文件中。插入的库文件的具体信息如下图所示:

图2.2 头文件信息

可以观察如下两张图,我们发现在源代码头部出现的注释在预处理之后的源代码部分已经不可见,因此这一点就印证了我们上面说的在预处理过程中预处理器将删除源代码中的注释部分。由于源代码中不存在宏定义与#ifdef等部分,因此这一部分无法展示。

图2.3 预处理后的源代码部分

图2.4 源代码文件

2.4 本章小结

这一部分介绍了在预处理过程中预处理器的工作(头文件展开,宏替换,删除注释,条件替换等),同时使用ubuntu系统展示了对于hello.c文件的预处理过程与预处理结果。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

概念:编译器(ccl)将文本文件hello.i翻译成文本文件hello.i,它包含一个汇编语言程序。

作用:编译的主要作用可以分为如下几个部分:

    扫描(词义分析):将源代码程序输入扫描器,将源代码中的字符序列分割为一系列c语言中的符合语法要求的字符单元,这一部分可以分为自上而下的分析和自下而上的分析两种方式。
    语法分析:基于词法分析得到的字符单元生成语法分析树。
    语义分析:在语法分析完成之后由语义分析妻进行语义分析,主要就是为了判断指令是否是合法的c语言指令,这一部分也可以叫做静态语义分析,并不判断一些在执行时可能出现的错误,例如如果不存在IDE优化,这一步对于1/0这种只有在动态类型检查的时候才会发现的错误,代码将不会报错。
    中间代码:中间代码的作用是可使使得编译程序的逻辑更加明确,主要是为了下一步代码优化的时候优化的效果更好。
    代码优化:根据用户指定的不同优化等级对代码进行安全的、等价的优化,这一行为的目的主要是为了提升代码在执行时的性能。
    生成代码:生成是编译的最后一个阶段。在经过上面的所有过程后,在这一过程中将会生成一个汇编语言代码文件,也就是我们最后得到的hello.s文件,这一文件中的源代码将以汇编语言的格式呈现。

3.2 在Ubuntu下编译的命令

gcc -S hello.i -o hello.s

图3.1 ubuntu下编译的命令

3.3 Hello的编译结果解析

3.3.1 数据

①常量

数字常量:通过观察我们可以发现在源代码中使用的数字常量都是储存在.text段的,包括在比较的时候使用的数字变量3,在循环的时候使用的循环比较变量等数字常量都是储存在.text节的,具体情况可以见如下截图:

图3.2 数字变量储存情况

字符串常量:可以发现在printf等函数中使用的字符串常量是储存在.rotate段的,具体储存情况可以见如下截图:

图3.3 字符常量储存情况

②变量

全局变量:在代码中存在一个全局变量,也就是在sleep中需要使用的sleepsecs变量,通过观察编译后的结果我们可以发现这一个全局变量放在了.data段,且大小被设置为4个字节,这一变量的初始化不需要其他的汇编语句,在刚开始的时候就初始化好了,详细情况可见如下截图:

图3.4全局变量储存情况

局部变量:可以发现局部变量是储存在栈中的某一个位置的或是直接储存在寄存器中的,对于源代码中的每一个局部变量可以进行逐一分析。局部变量共有三个,一个是循环变量i,以及argc和argv,对于i,我们发现它储存在栈中地址为-4(%rbp)的位置,对于i的操作可见如下截图:

图3.5 局部变量i的储存情况

对于局部变量argc,标志的是在程序运行的时候输入的变量的个数,可以发现它储存在栈中地址为-20(%rbp)的位置,对于它的操作主要是与3比较之后确定有一部分代码是否执行,具体汇编代码如下截图:

图3.6 局部变量argc的储存情况

对于局部变量argv,是一个保存着输入变量的数组,观察发现它储存在栈中,具体汇编代码段如下:

图3.7 局部变量argv的储存情况

3.3.2 赋值

可以发现对于变量的赋值在代码中出现了两次,一次是对于全局变量sleepsecs的赋值,一次是对于循环变量i的在循环中的赋值,可以分别进行分析。对于全局变量的赋值正如我们在上面看到的图3.4中展示的一样,在编译完成的时候就已经完成了对于全局变量sleepsecs的赋值,因此不需要其他的赋值语句。对于局部变量i,每次循环结束的时候都对齐进行+1操作,具体的操作汇编代码如下:

图3.8 对局部变量i的赋值操作

3.3.3 类型转换

对于全局变量sleepsecs事实上是存在一个隐式类型转换的,对于int型的全局变量赋值为2.5,很显然最后sleepsecs的值将会是2,在这里发生了由float向int转换的隐式类型转换,这一点也可以在汇编代码中得到验证:

图3.9 sleepsecs发生的隐式类型转换

可以发现第9行中sleepsec被初始化为2,这也印证了类型转换的发生。

3.3.4 算术操作

对于局部变量i,由于其是循环变量,因此在每一轮的循环中都要修改这个值,对于这个局部变量的算术操作的汇编代码如下:

图3.10 对局部变量i的算术操作

3.3.5 关系操作

源代码中一共出现了两处关系操作,具体情况可以分别分析。第一处是对于argc的判断,当等于3的时候将进行条件跳转,其中源代码片段如下:

图3.11 关系操作1源代码

而对应的汇编代码如下:

图3.12 关系操作1汇编代码

另一处是在for循环中对于循环变量i的判断,这一段的汇编代码如下图所示,当循环变量i大于等于9的时候将进行条件跳转。

图3.13 关系操作2汇编代码

3.3.6数组/指针/结构操作

这一段代码中出现的数组操作只有一个,也就是对于argv数组的操作,观察汇编代码可以发现argv储存的两个值都存放在栈中,argv[1]的储存地址是-24(%rbp),而argv[1]的储存地址是-16(%rbp),对于数组操作的汇编代码如下截图:

图3.14 数组操作

3.3.7函数调用

在这一段代码中出现了几个函数调用的情况,首先明确在X86系统中函数参数储存的规则,第1~6个参数依次储存在%rdi、%rsi、%rdx、%rcx、%r8、%r9这六个寄存器中,其余的参数保存在栈中的某些位置。

Main函数:

参数:传入参数argc和argv,其中argv储存在栈中,argc储存在%rdi中

返回:在源代码中最后的返回语句是return 0,因此在汇编代码中最后是将%eax设置为0并返回这一寄存器。汇编代码如下:

图3.15 main函数汇编代码

Printf函数:

参数:第一次调用的时候只传入了字符串参数首地址;for循环中调用的时候传入了 argv[1]和argc[2]的地址。

调用:第一次是满足if条件的时候调用,第二次是在for循环条件满足的时候调用。

具体汇编代码如下:

图3.16 printf第一次调用

图3.17 printf第二次调用

Sleep函数:函数以全局变量sleepsecs为参数,这一参数储存在%edi中,这一函数在for循环的条件下被调用,详细汇编代码如下:

图3.18 sleep函数调用

Exit函数:

参数:传入的参数为1,执行退出命令。

调用:当if条件满足的时候调用这一函数。

具体汇编代码如下:

图3.19 exit函数的调用

3.4 本章小结

本章主要介绍了在将修改了的源程序文件转换为汇编程序的时候主要发生的变化以及汇编代码文件中主要存在的部分以及源代码中的一些主要的操作对应的汇编代码中的汇编代码的展现形式。总的来说,编译器做的就是在进行词义分析和语义分析之后判断源代码符合语法要求之后将其转换为汇编代码。

(第3章2分)

第4章 汇编

4.1 汇编的概念与作用

概念:汇编器as将hello.s翻译成机器语言指令,把这些指令打包成一种叫可重定位目标程序的格式,并将结果保存在目标文件hello.o中。(hello.o是一个二进制文件)。

作用:将汇编代码根据特定的转换规则转换为二进制代码,也就是机器代码,机器代码也是计算机真正能够理解的代码格式。

4.2 在Ubuntu下汇编的命令

命令:as hello.s -o hello.o

图4.1 ubuntu下的汇编命令

4.3 可重定位目标elf格式

4.3.1 命令

readelf -a hello.o > ./elf.txt 使用这一命令导出我们需要的elf的文件

图4.2 生成可重定位目标elf格式

4.3.2 ELF头

ELF头以一个16字节的序列开始,这个序列描述了生成该文件系统下的字的大小以及一些其他信息。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息:包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。具体ELF头的代码如下:

图4.3 ELF头

4.3.3 节头表

描述了.o文件中每一个节出现的位置,大小,目标文件中的每一个节都有一个固定大小的条目。具体内容如下图所示:

图4.4 节头表

4.3.4 重定位节

重定位节中包含了在代码中使用的一些外部变量等信息,在链接的时候需要根据重定位节的信息对这些变量符号进行修改。链接的时候链接器会根据重定位节的信息对外部变量符号决定选择何种方法计算正确的地址,通过偏移量等信息计算出正确的地址。

本程序需要重定位的信息有:.rodata中的模式串,puts,exit,printf,slepsecs,sleep,getchar这些符号同样需要与相应的地址进行重定位。具体重定位节的信息如下图所示:

图4.5 重定位节信息

4.3.5 符号表

.symtab是一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。例如本程序中的getchar、puts、exit等函数名都需要在这一部分体现,具体信息如下图所示:

图4.6 符号表内容

4.4 Hello.o的结果解析

命令:objdump -d -r hello.o > Disas_hello.s

反汇编代码如下:

hello.o: 文件格式 elf64-x86-64

Disassembly of section .text:

0000000000000000

:

0: 55 push %rbp

1: 48 89 e5 mov %rsp,%rbp

4: 48 83 ec 20 sub $0x20,%rsp

8: 89 7d ec mov %edi,-0x14(%rbp)

b: 48 89 75 e0 mov %rsi,-0x20(%rbp)

f: 83 7d ec 03 cmpl $0x3,-0x14(%rbp)

13: 74 16 je 2b <main+0x2b>

15: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 1c <main+0x1c>

18: R_X86_64_PC32 .rodata-0x4

1c: e8 00 00 00 00 callq 21 <main+0x21>

1d: R_X86_64_PLT32 puts-0x4

21: bf 01 00 00 00 mov $0x1,%edi

26: e8 00 00 00 00 callq 2b <main+0x2b>

27: R_X86_64_PLT32 exit-0x4

2b: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)

32: eb 3b jmp 6f <main+0x6f>

34: 48 8b 45 e0 mov -0x20(%rbp),%rax

38: 48 83 c0 10 add $0x10,%rax

3c: 48 8b 10 mov (%rax),%rdx

3f: 48 8b 45 e0 mov -0x20(%rbp),%rax

43: 48 83 c0 08 add $0x8,%rax

47: 48 8b 00 mov (%rax),%rax

4a: 48 89 c6 mov %rax,%rsi

4d: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 54 <main+0x54>

50: R_X86_64_PC32 .rodata+0x21

54: b8 00 00 00 00 mov $0x0,%eax

59: e8 00 00 00 00 callq 5e <main+0x5e>

5a: R_X86_64_PLT32 printf-0x4

5e: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 64 <main+0x64>

60: R_X86_64_PC32 sleepsecs-0x4

64: 89 c7 mov %eax,%edi

66: e8 00 00 00 00 callq 6b <main+0x6b>

67: R_X86_64_PLT32 sleep-0x4

6b: 83 45 fc 01 addl $0x1,-0x4(%rbp)

6f: 83 7d fc 09 cmpl $0x9,-0x4(%rbp)

73: 7e bf jle 34 <main+0x34>

75: e8 00 00 00 00 callq 7a <main+0x7a>

76: R_X86_64_PLT32 getchar-0x4

7a: b8 00 00 00 00 mov $0x0,%eax

7f: c9 leaveq

80: c3 retq

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

可以发现有如下几点不同:

    进制不同:hello.s反汇编之后对于数字的表示是十进制的,而hello.o反汇编之后数字的表示是十六进制的。
    分支转移:对于条件跳转,hello.s反汇编中给出的是段的名字,例如.L2等来表示跳转的地址,而hello.o由于已经是可重定位文件,对于每一行都已经分配了相应的地址,因此跳转命令后跟着的是需要跳转部分的目标地址。
    函数调用:hello.s中,call指令后跟的是需要调用的函数的名称,而hello.o反汇编代码中call指令使用的是main函数的相对偏移地址。同时可以发现在hello.o反汇编代码中调用函数的操作数都为0,即函数的相对地址为0,因为再链接生成可执行文件后才会生成其确定的地址,所以这里的相对地址都用0代替。

    4.5 本章小结

    本章对汇编过程进行了一个简单但是完整的叙述。经过汇编器之后,生成了一个可重定位的文件,为下一步链接做好了准备。通过与hello.s的反汇编代码的比较,更加深入的理解了在汇编过程中发生的变化,这些变化都是为了链接做准备的。

    (第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.1 ubuntu下的链接命令

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

命令:readelf -a hello > hello1.elf

5.3.1 ELF头

包含内容与汇编中4.3.2节展示的类似,详细内容截图如下:

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

图5.2 ELF头

5.3.1节头

描述了各个节的大小、偏移量和其他属性。链接器链接时,会将各个文件的相同段合并成一个大段,并且根据这个大段的大小以及偏移量重新设置各个符号的地址。详细内容如下:

图5.3 节头表部分内容

5.4 hello的虚拟地址空间

使用edb打开hello可执行文件,可以在edb的Data Dump窗口看到hello的虚拟地址空间分配的情况,具体内容截图如下:

图5.4 edb中的data dump视图

可以发现这一段程序的地址是从0x401000开始的,并且该处有ELF的标识,可以判断从可执行文件时加载的信息。接下来可以分析其中的一些具体的内容:其中PHDR保存的是程序头表;INTERP保存了程序执行前需要调用的解释器;LOAD记录程序目标代码和常量信息;DYNAMIC储存了动态链接器所使用的信息;NOTE记录的是一些辅助信息;GNU_EH_FRAME保存异常信息;GNU_STACK使用系统栈所需要的权限信息;GNU_RELRO保存在重定位之后只读信息的位置。

5.5 链接的重定位过程分析

命令:objdump -d -r hello > hello_objdump.s

图5.5 hello反汇编代码部分

hello与hello.o的不同:

1.在链接过程中,hello中加入了代码中调用的一些库函数,例如getchar,puts,printf,等,同时每一个函数都有了相应的虚拟地址。例如exit函数的虚拟地址如下图:

图5.6 exit链接后虚拟地址展示

2. 对于全局变量的引用,由于hello.o中还未对全局变量进行定位,因此hello.o中用0加上%rip的值来表示全局变量的位置,而在hello中,由于已经进行了定位,因此全局变量的的值使用一个确切的值加上%rip表示全局变量的位置。

图5.7 hello中全局变量的表示

3. hello中增加了.init和.plt节,和一些节中定义的函数。

4. hello中无hello.o中的重定位条目,并且跳转和函数调用的地址在hello中都变成了虚拟内存地址。这是由于hello.o中对于函数还未进行定位,只是在.rel.text中添加了重定位条目,而hello进行定位之后自然不需要重定位条目。

5.地址访问:在链接完成之后,hello中的所有对于地址的访问或是引用都调用的是虚拟地址地址。例如下图中84行条件跳转代码所示:

图5.8 hello中的地址访问

链接的过程:

链接主要分为两个过程:符号解析和重定位。

符号解析:目标文件定义和引用符号,符号解析将每个符号引用和一个符号定义关联起来。

重定位:编译器和汇编器生成从0开始的代码和数据节。链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使得它们指向这个内存位置。链接器使用汇编器产生的重定位条目的详细指令,不加甄别地执行这样的重定位。

5.6 hello的执行流程

根据反汇编代码可以看出执行函数及虚拟内存地址如下:

401000 <_init>

401020 <.plt>

401030 puts@plt

401040 printf@plt

401050 getchar@plt

401060 atoi@plt

401070 exit@plt

401080 sleep@plt

401090 <_start>

4010c0 <_dl_relocate_static_pie>

4010c1

401150 <__libc_csu_init>

4011b0 <__libc_csu_fini>

4011b4 <_fini>

图5.9 使用edb执行hello过程截图

5.7 Hello的动态链接分析

当程序调用一个由共享库定义的函数时,由于编译器无法预测这时候函数的地址是什么,因此这时,编译系统提供了延迟绑定的方法,将过程地址的绑定推迟到第一次调用该过程时。通过GOT和过程链接表PLT的协作来解析函数的地址。在加载时,动态链接器会重定位GOT中的每个条目,使它包含正确的绝对地址,而PLT中的每个函数负责调用不同函数。那么,通过观察edb,便可发现dl_init后.got.plt节发生的变化。

首先可以观察elf中.got.plt节的内容

图5.10 elf中.got.plt的内容

使用edb查看时有如下发现:

图5.11 执行init之前的地址

图5.12 执行init之后的地址

5.8 本章小结

在链接过程中,各种代码和数据片段收集并组合为一个单一文件。利用链接器,分离编译称为可能,我们不用将应用程序组织为巨大的源文件,只是把它们分解为更小的管理模块,并在应用时将它们链接就可以完成一个完整的任务。

经过链接,已经得到了一个可执行文件,接下来只需要在shell中调用命令就可以为这一文件创建进程并执行该文件。

(第5章1分)

第6章 hello进程管理

6.1 进程的概念与作用

概念:进程是执行中程序的抽象。

作用:在现代系统上运行一个程序时,我们会得到一个假象,好像我们的程序是系统中唯一运行的程序一样。我们的程序好像独占处理器和内存。处理器好像无间断地一条接一条执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。这些假象是通过进程的概念提供的。进程提供给应用程序的关键抽象:1)一个独立的逻辑控制流,提供一个程序独占处理器的假象;2)一个私有的地址空间,提供一个程序独占地使用内存系统的假象。

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

作用:shell执行一系列的读/求值步骤,然后终止。读步骤读取来自用户的一个命令行,求值步骤解析命令行,并根据解析结果运行程序。

处理流程:

1.Shell首先从命令行中找出特殊字符(元字符),在将元字符翻译成间隔符号。元字符将命令行划分成小块tokens。Shell中的元字符如下所示:SPACE , TAB , NEWLINE , & , ; , ( , ) ,< , > , |

2. 程序块tokens被处理,检查看他们是否是shell中所引用到的关键字。

3. 当程序块tokens被确定以后,shell根据aliases文件中的列表来检查命令的第一个单词。如果这个单词出现在aliases表中,执行替换操作并且处理过程回到第一步重新分割程序块tokens。

4.Shell对~符号进行替换。

5.Shell对所有前面带有\(符号的变量进行替换。
6.Shell将命令行中的内嵌命令表达式替换成命令;他们一般都采用\)(command)标记法。

7.Shell计算采用$(expression)标记的算术表达式。

8.Shell将命令字符串重新划分为新的块tokens。这次划分的依据是栏位分割符号,称为IFS。缺省的IFS变量包含有:SPACE , TAB 和换行符号。

9.Shell执行通配符* ? [ ]的替换。

10.shell把所有从处理的结果中用到的注释删除,并且按照下面的顺序实行命令的检查:

A. 内建的命令

B. shell函数(由用户自己定义的)

C. 可执行的脚本文件(需要寻找文件和PATH路径)

11.在执行前的最后一步是初始化所有的输入输出重定向。

12.最后,执行命令。[1]

6.3 Hello的fork进程创建过程

父进程通过调用fork函数创建一个新的运行的子进程。调用fork函数后,新创建的子进程几乎但不完全与父进程相同:子进程得到与父进程虚拟地址空间相同的(但是独立的)一份副本,包括代码、数据段、堆、共享库以及用户栈,子进程获得与父进程任何打开文件描述符相同的副本,这意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。fork被调用一次,却返回两次,子进程返回0,父进程返回子进程的PID。父进程和新创建的子进程之间最大的区别在于它们有不同的PID。

6.4 Hello的execve过程

exceve函数在当前进程的上下文中加载并运行一个新程序。exceve函数加载并运行可执行目标文件,并带参数列表和环境变量列表。只有当出现错误时,exceve才会返回到调用程序。所以,与fork一次调用返回两次不同,在exceve调用一次并从不返回。当加载可执行目标文件后,exceve调用启动代码,启动代码设置栈,将可执行目标文件中的代码和数据从磁盘复制到内存中,然后通过跳转到程序的第一条指令或入口点来运行该程序,由此将控制传递给新程序的主函数。

6.5 Hello的进程执行

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

进程调度:即使在系统中通常有许多其他程序在运行,进程也可以向每个程序提供一种假象,好像它在独占地使用处理器。如果想用调试器单步执行程序,我们会看到一系列的程序计数器(PC)的值,这些值唯一的对应于包含在运行时动态链接到程序的共享对象中的指令。这个PC的序列叫做逻辑控制流,或者简称逻辑流。进程是轮流适用处理器的,每个进程执行它的流的一部分,然后被抢占,然后轮到其他进程。

在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行,我们说内核调度了这个进程。在内核调度了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。

内核模式转变到用户模式:操作系统内核使用上下文切换来实现多任务。内核为每个进程维持一个上下文,它是内核重启被抢占的进程所需的状态,包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构的值。

进程执行到某些时刻,内核可决定抢占该进程,并重新开启一个先前被抢占了的进程,这种决策称为调度。内核调度一个新的进程运行后,通过上下文切换机制来转移控制到新的进程:1)保存当前进程上下文;2)恢复某个先前被抢占的进程被保存的上下文3)将控制转移给这个新恢复的进程。当内核代表用户执行系统调用时,可能会发生上下文切换,这时就存在着用户态与核心态的转换。如下图所示:

图6.1 上下文切换

6.6 hello的异常与信号处理

正常执行状态:

图6.2 程序正常执行状态

异常类型:

处理方式:

图 6.3中断处理方式

图 6.4 陷阱处理方式

图 6.5 故障处理方式

图 6.6 终止处理方式

不停乱按:将屏幕的输入缓存到缓冲区。乱码被认为是命令,不影响当前进程的执行。

图6.7 运行时不停乱按

按下Ctrl-Z:程序运行时按Ctrl-Z,这时,产生中断异常,它的父进程会接收到信号SIGSTP并运行信号处理程序,然后便发现程序在这时被挂起了,并打印了相关挂起信息。

图6.8 运行时按下Ctrl-Z

Ctrl-Z后运行ps,打印出了各进程的pid,可以看到之前挂起的进程hello。

图6.9 挂起hello后执行ps

Ctrl-Z后运行jobs,打印出了被挂起进程组的jid,可以看到之前被挂起的hello,以被挂起的标识Stopped。

图6.10 挂起hello后执行jobs

Ctrl-Z后运行pstree,可看到它打印出的信息:

图6.11 挂起hello后执行pstree

Ctrl-Z后运行fg:因为之前运行jobs是得知hello的jid为1,那么运行fg 1可以把之前挂起在后台的hello重新调到前台来执行,打印出剩余部分,然后输入hello回车,程序运行结束,进程被回收。

图6.12 挂起hello后执行fg

Ctrl-Z后运行Kill:重新执行进程,可以发现hello的进程号为34230,那么便可通过kill -9 34230发送信号SIGKILL给进程34230,它会导致该进程被杀死。然后再运行ps,可发现已被杀死的进程hello。

图6.13 挂起hello后执行kill

按下Ctrl-C:进程收到 SIGINT 信号,结束 hello。在ps中查询不到其PID,在job中也没有显示,可以看出hello已经被彻底结束。

图6.14 运行hello时按下Ctrl-C

6.7本章小结

本章主要介绍了hello可执行文件的执行过程,包括进程创建、加载和终止,以及通过键盘输入等过程。从创建进程到进程并回收进程,这一整个过程中需要各种各样的异常和中断等信息。程序的高效运行离不开异常、信号、进程等概念,正是这些机制支持hello能够顺利地在计算机上运行。

(第6章1分)

第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:逻辑地址指由程序产生的与段相关的偏移地址部分,也叫相对地址。要经过寻址方式的计算或变换才得到内存储器中的实际有效地址,即物理地址。从hello的反汇编代码中看到的地址,它们需要通过计算,通过加上对应段的基地址才能得到真正的地址,这些便是hello中的逻辑地址。

线性地址:是逻辑地址到物理地址变换之间的中间层。程序hello的代码会产生逻辑地址,hello的反汇编文件中看到的地址(即逻辑地址)中的偏移量,加上对应段的基地址,便得到了hello中内容对应的线性地址。

虚拟地址:有时我们也把逻辑地址称为虚拟地址。因为与虚拟内存空间的概念类似,逻辑地址也是与实际物理内存容量无关的,是hello中的虚拟地址。

物理地址:是指出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。在hello的运行中,在访问内存时需要通过CPU产生虚拟地址,然后通过地址翻译得到一个物理地址,并通过物理地址访问内存中的位置。

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

逻辑地址由段选择符和偏移量组成,线性地址为段首地址与逻辑地址中的偏移量组成。其中,段首地址存放在段描述符中。而段描述符存放在描述符表中,也就是GDT(全局描述符表)或LDT(局部描述符表)中。

图7.1 段式管理示意图

段式管理特点:

1.段式管理以段为单位分配内存,每段分配一个连续的内存区。

2.由于各段长度不等,所以这些存储区的大小不一。

3.同一进程包含的各段之间不要求连续。

4.段式管理的内存分配与释放在作业或进程的执行过程中动态进行。

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

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

线性地址(虚拟地址)由虚拟页号VPN和虚拟页偏移VPO组成。首先,MMU从线性地址中抽取出VPN,并且检查TLB,看他是否因为前面某个内存引用缓存了PTE的一个副本。TLB从VPN中抽取出TLB索引和TLB标记,查找对应组中是否有匹配的条目。若命中,将缓存的PPN返回给MMU。若不命中,MMU需从页表中的PTE中取出PPN,若得到的PTE无效或标记不匹配,就产生缺页,内核需调入所需页面,重新运行加载指令,若有效,则取出PPN。最后将线性地址中的VPO与PPN连接起来就得到了对应的物理地址。

图7.2 Core i7 地址翻译

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

每次CPU产生一个虚拟地址,MMU(内存管理单元)就必须查阅一个PTE(页表条目),以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会从内存多取一次数据,代价是几十到几百个周期。如果PTE碰巧缓存在L1中,那么开销就会下降1或2个周期。然而,许多系统都试图消除即使是这样的开销,它们在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓存器(TLB)。

虚拟地址VA虚拟页号VPN和虚拟页偏移VPO组成。若TLB命中时,所做操作与7.3中相同;若TLB不命中时,VPN被划分为四个片,每个片被用作到一个页表的偏移量,CR3寄存器包含L1页表的物理地址。VPN1提供到一个L1 PTE的偏移量,这个PTE包含L2页表的基地址。VPN2提供到一个L2 PTE的偏移量,依次类推。最后在L4页表中对应的PTE中取出PPN,与VPO连接,形成物理地址PA。

图7.3 多级页表管理

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

MMU将物理地址发给L1缓存,缓存从物理地址中取出缓存偏移CO、缓存组索引CI以及缓存标记CT。若缓存中CI所指示的组有标记与CT匹配的条目且有效位为1,则检测到一个命中条目,读出在偏移量CO处的数据字节,并把它返回给MMU,随后MMU将它传递给CPU。若不命中,则在下一级cache或是主存中寻找需要的内容,储存到上一级cache后再一次请求读取。

图7.4 存储器层次结构

7.6 hello进程fork时的内存映射

当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给他一个唯一的pid。

为了给这个新进程创建虚拟内存,系统创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有写时复制。

当fork从新进程返回,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,也就为每个进程保持了私有地址空间的抽象概念。

7.7 hello进程execve时的内存映射

execve函数在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要:

(1)删除已存在的用户区域

(2)映射私有区域:为新程序hello的代码、数据、bss 和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。

(3) 映射共享区域:如果hello程序与共享对象(或目标)链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。

(4) 设置程序计数器(PC) ,指向代码的入口点。

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

页面命中完全是由硬件完成的,而处理缺页是由硬件和操作系统内核协作完成的:

    处理器生成一个虚拟地址,并将它传送给MMU
    MMU生成PTE地址,并从高速缓存/主存请求得到它
    高速缓存/主存向MMU返回PTE
    PTE中的有效位是0,所以MMU出发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序。
    缺页处理程序确认出物理内存中的牺牲页,如果这个页已经被修改了,则把它换到磁盘。
    缺页处理程序页面调入新的页面,并更新内存中的PTE
    缺页处理程序返回到原来的进程,再次执行导致缺页的命令。CPU将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面已经换存在物理内存中,所以就会命中。

图7.5 缺页操作示意图

7.9动态存储分配管理

定义:一种内存管理方法。对内存空间的分配、回收等操作在进程执行过程中进行,以便更好地适应系统的动态需求,提高内存利用率。

分配器的基本风格:

    显示分配器:要求应用显示地释放任何已分配的块。
    隐式分配器:要求分配器检测一个已分配的块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器。

    基本方法与策略:

    1.带边界标签的隐式空闲链表分配器管理

    带边界标记的隐式空闲链表的每个块是由一个字的头部、有效载荷、可能的额外填充以及一个字的尾部组成的。

    当一个应用请求一个k字节的块时,分配器搜索空闲链表,查找一个符合大小的空闲块来放置这个请求块。分配器有三种放置策略:首次适配、下一次适配合最佳适配。在释放一个已分配块的时候需要考虑是否能与前后空闲块合并,减少系统中碎片的出现。

    2.显示空间链表管理

    显式空闲链表是将空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。如,堆可以组织成一个双向链表,在每个空闲块中,都包含一个前驱与一个后继指针。放置策略与上述放置策略一致。

    7.10本章小结

    本章主要介绍了hello进程在执行的过程中的虚拟内存与物理内存之间的转换关系,以及一些支持这些转换的硬件或软件机制。同时介绍了在发生缺页异常的时候系统将会如何处理这一异常。最后介绍了动态内存分配的作用以及部分方法与策略。

(第7章 2分)

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件。所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入输出都被当做对相应文件的读和写来执行。

设备管理:unix io接口。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。

8.2 简述Unix IO接口及其函数

Unix IO接口:

打开文件:一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。

Linux shell创建的每个进程开始时都有三个打开的文件:标准输入(文件描述符0)、标准输出(描述符为1),标准出错(描述符为2)。头文件<unistd.h>定义了常量STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO,他们可用来代替显式的描述符值。

改变当前的文件位置,文件开始位置为文件偏移量,应用程序通过seek操作,可设置文件的当前位置为k。

读写文件,读操作:从文件复制n个字节到内存,从当前文件位置k开始,然后将k增加到k+n;写操作:从内存复制n个字节到文件,当前文件位置为k,然后更新k。

关闭文件:当应用完成对文件的访问后,通知内核关闭这个文件。内核会释放文件打开时创建的数据结构,将描述符恢复到描述符池中

Unix IO函数:

(1).打开文件:int open(char *filename, int flags, mode_t mode);

Open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程当中没有打开的最小描述符。Flags参数指明了进程打算如何访问这个文件,同时也可以是一个或者更多为掩码的或,为写提供给一些额外的指示。Mode参数指定了新文件的访问权限位。

(2).关闭文件:int close(int fd);

调用close函数,通知内核结束访问一个文件,关闭打开的一个文件。成功返回0,出错返回-1。

(3).读文件:ssize_t read(int fd, void *buf, size_t n);

调用read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示错误,返回值0表示EOF,否则返回值表示的是实际传送的字节数量。

(4).写文件:ssize_t write(int fd, const void *buf, size_t n);

调用从内存位置buf复制至多n个字节到描述符fd的当前文件位置。返回值-1表示出错,否则,返回值表示内存向文件fd输出的字节的数量。

8.3 printf的实现分析

printf函数:

int printf(const char *fmt, ...)
{
int i;
va_list arg = (va_list)((char *)(&fmt) + 4);
i = vsprintf(buf, fmt, arg);
write(buf, i);
return i;
}

可以发现printf的输入参数是fmt,但是后面是不定长的参数,同时在printf内存调用了两个函数,一个是vsprintf,一个是write。

int vsprintf(char *buf, const char *fmt, va_list args)
{
char *p;
chartmp[256];
va_listp_next_arg = args;
for (p = buf; *fmt; fmt++)
{
if (*fmt != '%')
{
*p++ = *fmt;
continue;
}
fmt++;
switch (*fmt)
{
case 'x':
itoa(tmp, *((int *)p_next_arg));
strcpy(p, tmp);
p_next_arg += 4;
p += strlen(tmp);
break;
case 's':
break;
default:
break;
}
return (p - buf);
}
}

Printf执行流程:

vsprintf函数将所有的参数内容格式化之后存入buf,然后返回格式化数组的长度。write函数将buf中的i个元素写到终端。从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

getchar是读入函数的一种。它从标准输入里读取下一个字符,相当于getc(stdin)。返回类型为int型,为用户输入的ASCII码或EOF。getchar可用宏实现:#define getchar() getc(stdin)。getchar有一个int型的返回值。当程序调用getchar时.程序就等着用户按键。用户输入的字符被存放在键盘缓冲区中。直到用户按回车为止(回车字符也放在缓冲区中)。当用户键入回车之后,getchar才开始从stdin流中每次读入一个字符。getchar函数的返回值是用户输入的字符的ASCII码,若文件结尾(End-Of-File)则返回-1(EOF),且将用户输入的字符回显到屏幕。 [2]

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。

getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

本章主要介绍了linux系统中的I/O设备基本概念和管理方法,同时简单介绍了printf和getchar函数的实现。

(第8章1分)

结论

用计算机系统的语言,逐条总结hello所经历的过程。

你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。

Hello的一生是简单的但是又蕴含着每一个c语言程序执行前的必经之路:

    预处理,hello.c文件通过cpp的预处理,得到了扩展后的源程序文件hello.i
    编译,hello.i通过编译器的处理,被翻译成了汇编语言程序hello.s
    汇编,在汇编器as的处理下,hello.s生成了可重定位文件hello.o
    链接,链接器将重定位目标文件链接为可执行目标文件hello
    生成子进程,在shell中输入指定命令shell调用fork函数为hello生成进程。
    Execve加载并运行hello程序,将它映射到对应虚拟内存区域,并依需求载入物理内存。
    I/O设备,在hello程序中存在输入与输出,这些部分与printf,getchar函数有关,这些函数与linux系统的I/O设备密切相关。
    Hello将在cpu流水线中执行每一条指令
    程序运行结束后,父进程会对其进行回收,内核把它从系统中清除。

    这样,hello就结束了它的一生。

    在计算机系统的设计与实现过程中所必须要满足的就是准确,程序的执行必须能输出准确的结果,在这一基础上进行一定的优化能够让程序执行的更快,包括cache,流水线,超标量等设计都是基于这些的。在完成大作业的过程中相当于回顾了一遍这学期的学习内容,对于计算机系统设计与实现也有了更深切的感悟。

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

附件

列出所有的中间产物的文件名,并予以说明起作用。

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

参考文献

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

[1]伍之昂. Linux Shell编程从初学到精通 [M]. 北京:电子工业出版社

[2]https://baike.baidu.com/item/getchar/919709?fr=aladdin

[3]《深入理解计算机系统》 Randal E.Bryant David R.O’Hallaron 机械工业出版社

[4] https://blog.csdn.net/wang13342322203/article/details/80862382

[5]https://docs.microsoft.com/zh-cn/cpp/build/creating-precompiled-header-files?view=msvc-160

[6] CSDN博客 ELF可重定位目标文件格式

[7] https://www.cnblogs.com/knife-king/p/11090029.html

[8] https://baike.baidu.com/item/ELF/7120560?fr=aladdin

[9] http://www.elecfans.com/emb/20190402898901.html

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

哈工大 计算机系统 大作业 程序人生-Hello’s P2P的相关教程结束。

《哈工大 计算机系统 大作业 程序人生-Hello’s P2P.doc》

下载本文的Word格式文档,以方便收藏与打印。