2021春深入理解计算机系统大作业——程序人生
Posted lgxo
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了2021春深入理解计算机系统大作业——程序人生相关的知识,希望对你有一定的参考价值。
计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算学部
学 号 1190200608
班 级 1903004
学 生 琚晓龙
指 导 教 师 史先俊
计算机科学与技术学院
2021年5月
文章以最简单的hello程序为例,跟踪一个程序从以C语言形式创建,到一步一步生成可执行文件,然后被运行的过程。第一章简单介绍了用到的环境与工具。第二章到第五章详细介绍了hello程序如何从源代码经历预处理、编译、汇编、链接成为一个可执行文件。后几章详细介绍了hello程序是如何加载到内存中并被计算机执行的,包括进程创建、环境创建、内存访问、异常处理、I/O系统交互等。通过hello程序的一生,窥视了计算机系统执行一个程序的基本流程与原理,对我们深入理解计算机系统又很大的帮助。
关键词:预处理;编译;汇编;重定位;链接;进程;存储;数据访问;I/O;虚拟地址;异常与信号。
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
5.3.4 Section to Segment mapping. - 32 -
6.2 简述壳Shell-bash的作用与处理流程... - 41 -
6.3 Hello的fork进程创建过程... - 41 -
7.2 Intel逻辑地址到线性地址的变换-段式管理... - 50 -
7.3 Hello的线性地址到物理地址的变换-页式管理... - 51 -
7.4 TLB与四级页表支持下的VA到PA的变换... - 52 -
7.5 三级Cache支持下的物理内存访问... - 53 -
7.6 hello进程fork时的内存映射... - 54 -
7.7 hello进程execve时的内存映射... - 54 -
第1章 概述
1.1 Hello简介
- P2P:
Hello程序的生命周期是从一个高级的C语言程序开始的。GCC编译器驱动程序读取源程序文件hello.c(Program),并通过预处理、编译、汇编、链接四个阶段将它翻译成一个可执行目标文件。当在shell中输入这个可执行文件的名字时(由于该名字不是一个内置的shell命令,shell会假设这是一个可执行文件的名字,并将加载与运行这个文件),shell会调用fork函数为hello程序创建一个新进程(Process)。
- 020:
Hello程序最初并不存在(0),因为程序员的编写而诞生,生成一个C语言程序,由此开始,经历上边P2P的过程,得到一个执行hello程序的进程。之后hello程序执行完毕后,hello进程会终止,并由shell回收(0)。
1.2 环境与工具
硬件环境:X64 CPU;2.40 GHz;16G RAM;716G
系统环境:Windows10 专业版;最新Vmware; Ubuntu 20.04
开发与调试工具:gcc; objdump; readelf; edb; hexedit; 文本编辑器
1.3 中间结果
中间结果文件名 | 文件的作用 |
hello.c | hello C程序文本(源代码) |
hello.i | hello.c预处理后生成的文件 |
hello.s | hello.i编译后生成的文件 |
hello.o | hello.s汇编后生成的文件 |
hello_o_readelf.txt | 存放使用readelf工具查看hello.o文件的结果的文件 |
hello_o_objdump.txt | 存放使用objdump工具查看hello.o文件的结果的文件 |
hello(.out) | Hello程序的可执行文件 |
hello_readelf.txt | 存放使用readelf工具查看hello文件的结果的文件 |
hello_objdump.txt | 存放使用objdump工具查看hello文件的结果的文件 |
1.4 本章小结
本章对hello进行了简介;列出了编写本文所用的硬件软件系统以及开发工具;列出了编写本文是生成的中间结果的文件名以及这些文件各自的作用。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理工作(如图2-1-1)是在程序被编译前进行的,修改原始的C程序。可能处理的工作有:将其他文件包含到即将被编译的文件中来,定义符号常量(Symbol constant)和宏(Macro),程序代码的条件编译(Conditional compilation)和有条件地执行预处理命令(Conditional execution of preprocessor directive)。所有的预处理命令都是以#开头的。在同一行中,只有空格和注释可以出现在预处理命令之前。
如图2-1-2,hello.c中第6行的#include <stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中。结果就得到了另一个C程序,通常是以 .i 作为文件拓展名。
图2-1-1
图2-1-2
2.2在Ubuntu下预处理的命令
命令: gcc -E hello.c -o hello.i
如图2-2-1:
图2-2-1
2.3 Hello的预处理结果解析
打开生成的文件可以看到内容明显增多(如图2-3-1所示)。
图2-3-1
(由23行变为3069行)
具体增加的具体内容如下,举例见图2-3-2:
对原文件中的宏进行了展开。
将头文件中的内容添加到进该文件中。例如声明的函数、定义的结构体、定义的变量、定义的宏等内容。
将代码中有#define命令对应的符号进行了替换。
(a)
(b)
(c)
图2-3-2
2.4 本章小结
本章介绍了预处理的概念与作用,例如将头文件中的内容添加到C程序问文件,替换掉宏常量等。对如何使用gcc对c程序文件进行预处理进行了示范。对生成的预处理后的文件进行了简单的解析。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译器 (ccl) 将文本文件 hello.i 翻译成文本文件hello.s ,它包含一个汇编语言程序。这个过程叫做编译,如图3-1-1所示。
编译程序把源程序(高级语言)翻译成一个包含汇编语言的程序问文件,同时,还可以进行语法检查、优化源程序、分配寄存器的使用、将程序中的文件输出到汇编语言,确保每个模块中每个局部符号只有一个定义、唯一的名字,对无法解析的全局符号生成一个连接器符号表条目等。
图3-1-1
3.2 在Ubuntu下编译的命令
命令: gcc -S hello.i -o hello.s
如图3-2-1:
图3-2-1
3.3 Hello的编译结果解析
打开生成的文件可以看到该文件中都是汇编指令(如图3-3-1)。
图3-3-1
具体解析如下:
指导汇编器和链接器工作的伪指令及作用见图3-2-2和表3-2-1.
图3-3-2
.file | 声明源文件 |
.text | 代码段 |
.section | 定义内存段 |
.rodata | 只读数据段 |
.align | 数据或者指令的地址对齐方式 |
.string | 声明一个字符串(.LC0,.LC1) |
.global | 声明全局变量(main) |
.type | 声明一个符号是数据类型还是函数类型 |
表3-2-1
3.3.1数据
对于C语言中的常量,编译器会根据它们对应的类型进行编码:
1)对于有符号整型数据,编译器会将其转化为该数据对应的十六进制补码形式[1];(如图3-3-1-1)
2)对于无符号整数,编译器会将其转化为该数据对应的十六进制的源码形式。[2]
3)对于浮点类型,编译器会根据 IEEE754 标准将其转化为对应的十六进制数;
4)对于字符类型,汇编器会根据字符的ASII编码或是Unicode编码生成十六进制数。[3](如图3-3-1-2)
图3-3-1-1
图3-3-1-2
对于C语言中的全局变量,编译器会将其符号放在伪指令 .global后。(如图3-3-1-3),并将该数据存储到内存中。
对于C语言中的局部变量,编译器有两种处理方式:1)将该变量的值存放在一个寄存器中(如图3-3-1-4)。2)分配新的栈帧,并将局部变量存放在内存(栈)中,下面几种情况必须将变量数据存储在内存中:
- 寄存器不够存放所有的本地数据。
- 对一个局部变量使用地址运算符’&’,因此必须能够为它产生一个地址。
- 某些局部变量是数组或结构,因此必须能够通过数组或结构引用被访问到。
对于C语言中的静态变量,编译器将该变量存储在栈中。不会释放直到程序退出或主函数返回零。
图3-3-1-3
图3-3-1-4(此处%edi存放argc)
[1]、[2]对于有些(绝对值)比较小的整数,编译器会直接使用十进制数编码。
[3]对于字符串来说,则是一个编码序列,最后以空字符编码结尾。
对于C语言中的表达式,编译器会通过一系列指令对其进行处理,然后将结果存放到寄存器或是内存中。
对于C语言中宏,由于在预处理阶段就已经处理,所以编译器只是把相对应的数据进行编码即可。
3.3.2赋值
对编译器来说,赋值操做就是将数据从一个位置复制到另一个位置。进行这些操做的指令叫做数据传送指令。逗号操作符会将一行代码分割成几个独立的赋值操做。对于赋初始值的变量编译器会在首次出现这个变量时为其赋相应值。对于没有赋初始值的变量,编译器有两种处理方式:1)相当于赋初始值为0,即在首次出现这个变量时为其赋值0;2)不进行处理,直到为其赋值时再申请内存并赋值。
数据传输指令可分为三种。
最简单形式的数据转送指令——MOV类,如图3-3-2-1.
图3-3-2-1
零扩展数据传送指令,如图3-3-2-2.
图3-3-2-2
符号扩展数据传送指令,如图3-3-2-3.
图3-3-2-3
Hello程序中的赋值操做在hello.s中的指令如图3-3-2-4:
图3-3-2-4(i = 0;语句)
3.3.3类型转换(隐式或显式)
对于整型与整型、整型与字符型之间的类型转换,直接按照位级表示截断(字长大的像字长小的转换,截断高位)或者扩展(字长小的像字长大的转换,分为有符号扩展和无符号扩展)即可。其中截断只需要在使用数据传输指令时将后缀减小,而扩展有具体的扩展指令,见图3-3-2-2和图3-3-2-3.
对于整型与浮点类型之间的转换,由编译器根据其表示的值来进行转换。
类型转换分为显式和隐式转换,其中显式转换需要需要程序员显式地利用 () 在代码中表示出来,而隐式转换由编译器编译时自行完成,不需要程序员的参与。
3.3.4Sizeof
Sizeof的结果由编译器根据源代码中的数据类型得出,作为一个常量使用。如图3-3-4-1,指针类型的大小为8,编译器在编译阶段确认类型后直接将其表示为常数。
图3-3-4-1
3.3.5算术操作&逻辑/位操作
图3-3-5-1列出了一些整数和逻辑操做。大部分操做都被分成了指令类,这些指令类有各种带不同大小的操作数的变种(leaq没有)。
图3-3-5-1(>>A为算数右移)
如图3-3-5-2,addl 指令实现了源代码中的 i++ (i的值保存在%ebp中)
图3-3-5-2
3.3.6关系操作&控制转移
关系操做通过CMP或TEST指令设置条件码,例如CF、ZF、SF、OF。注意,关系操做并不会改变寄存器或内存中的值。这些指令如图3-3-6-1所显示。
图3-3-6-1
图3-3-6-2显示了编译器是怎么实现源代码中的条件判断的:
a.(判断argc!=4)
b.(判断i<8)
图3-3-6-2
条件码不会直接读取,常用的使用方法有三种:1)可以根据条件码的某种组合,将一个字节设置成0或者1,set类指令;2)可以条件跳转到程序的某个其他的部分,jmp类指令;3)可以有条件的传送数据,条件传送指令。
后两种使用方式可以实现控制转移,jmp指令如图3-3-6-3.
图3-3-6-3
如图3-3-6-4,编译器通过jmp指令实现了控制转移。
(a) (b)
图3-3-6-4
3.3.7数组/指针/结构操作
对数组、指针、结构的操做通过使用基址加变址的方式访问操做,根本上都是对指针的操做。注意指针的算数操做是以它们指向的对象的大小为单位进行操作的(即伸缩),而这种大小并不一定是一个字节。
最长用的内存访问形式如 Imm(rb, ri, s) ,其中Imm是立即数偏移,是基址寄存器,是变址寄存器,s是比例因子(必须是1、2、4、8),在对数组、指针、结构访问时s往往是指针指向的对象大小。
图3-3-7-1显示了编译器是怎么实现对main函数的参数char *argv[]这一数组进行访问的(该数组的元素为一个指向字符的指针,大小为8字节,寄存器rbx中存放的是argv数组的首地址)
图3-3-7-1
3.3.8函数操作
call指令用来进行过程调用(即调用其他函数),ret指令用来进行从过程中调用返回(即return)。
在进行过程调用时需要设置被调用过程的参数。参数小于6个时,依次使用%rdi, %rsi, %rdx, %rcx, %r8, %r9传递参数;参数个数大于六个则需要使用栈进行参数传递,栈中存放参数的位置如图3-3-8-1所示。
图3-3-8-1
图3-3-8-2显示了printf函数调用时参数传递过程。(%rdi存放格式化用的字符串,%rsi与%rdx分别存放着两个要打印的字符串的首地址。)
图3-3-8-2
图3-3-8-3显示了atoi函数调用时参数传递过程。(%rdi中存放字符串的首地址)
图3-3-8-3
图3-3-8-4显示了sleep函数调用时参数传递过程。(%edi中存放所需要sleep的秒数)
图3-3-8-4
图3-3-8-5显示了exit函数调用时参数传递过程。(%edi中存放了退出状态)
图3-3-8-5
图3-3-8-6显示了main函数的返回。
图3-3-8-6
对于其他函数的调用这里不再赘述。
3.4 本章小结
本章简述了编译的概念与作用;示范了如何使用gcc工具对源程序进行编译操做;针对生成的汇编代码文件(.s后缀),在数据、赋值、类型转换、sizeof、算数操做&逻辑/位操做、关系操作&控制转移、数组/指针/结构操做、函数操做共8个方面进行了解析。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中。这个过程便是汇编,如图4-1-1所示。
汇编将机器不可识别的指令转化成机器可识别的指令的同时还会进行构造符号表等工作。
图4-1-1
4.2 在Ubuntu下汇编的命令
命令: gcc -c hello.s -o hello.o
如图4-2-1.
图4-2-1
4.3 可重定位目标elf格式
可重定位目标文件的典型格式如图4-3-1.
图4-3-1
如图4-3-2,我通过输出重定向把readelf读出的信息存入了文件hello_o_readelf.txt中。之后我们可以直接在hello_o_readelf.txt中获取信息,也可以通过其他途径获取信息。
图4-3-2
获取的信息如下:
4.3.1ELF头
ELF头(如图4-3-1-1所示)以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序(小端序)。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(REL可重定位文件)、机器类型(x86-64)、节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量。
图4-3-1-1
4.3.2节头部表
不同节的位置和大小是由节头部表(如图4-3-2-1)描述的,其中目标文件中每个节都有一个固定大小的条目(entry)。通过节头部表,我们可以直到各节的名称、类型、地址、偏移量、地址、对齐信息等。以.text节为例,我们可以得知其类型为PROGBITS,地址为0x0,偏移量为0x40字节,大小为0x41字节,全体大小为0字节,旗标为AX,链接标志为0,信息标志为0,对齐要求为1字节。
图4-3-2-1
4.3.3符号表
符号表(如图4-3-3-1)中存放着程序中定义和引用的函数和全局变量的信息。每个可重定位目标文件在. symtab 中都有一个符号表(除非程序员特意用STRIP命令去掉它)。和编译器中的符号表不同,.symtab符号表不包含局部变量的条目。通过符号表,我们可以得出该符号名字(通过其在字符串表中的字节偏移量),定义它的节,它在定义它的节中的距起始位置的偏移,大小,类型,源文件路径名,符号是本地还是全局的等信息。
图4-3-3-1
4.3.4重定位节
当汇编器生成一个目标模块时,它并不知道数据和代码最终将放在内存中的什么位置。它也不知道这个模块引用的任何外部定义的函数或者全局变量的位置。所以,无论何时汇编器遇到对最终位置未知的目标引用,它就会生成一个重定位条目,告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。代码的重定位条目放在.rel.text中。已初始化数据的重定位条目放在.rel.data中。这些重定位条目都放在重定位节(如图4-3-4-1)中。
图4-3-4-1
其中偏移量是需要被修改的引用的节偏移。符号名称标识被修改引用应该指向的符号。类型告知链接器如何修改新的引用。加数是一个有符号常数,一些类型的重定位要使用它对被修改引用的值做偏移调整。
我们只关心其中两种最基本的ELF的重定义类型:
- R_X86_64 PC32。重定位一个使用32位PC相对地址的引用。当CPU执行一条使用PC 相对寻址的指令时,它就将在指令中编码的32位值加上PC的当前运行时值,得到有效地址(如call指令的目标),PC值通常是下一条指令在内存中的地址。
- R _X86_ 64_ 32。 重定位一个使用32位绝对地址的引用。通过绝对寻址,CPU直接使用在指令中编码的32位值作为有效地址,不需要进一步修改。
链接器的重定位算法的伪代码如图4-3-4-2所示。
图4-3-4-2
对于该重定位节中的R_X86_64_PLT32类型,PLT 是一个新的函数入口表的格式,目前不必深究。
4.3.5其他节
如图4-3-5-1所示,文件中没有section groups, 程序头和dynamic section.
图4-3-5-1
4.4 Hello.o的结果解析
如图4-4-1,我通过输出重定向将调用objdump -d -r hello.o命令生成的反汇编存储到文件hello_o_objdump.txt中。
图4-4-1
反汇编内容如图4-4-2.
图4-4-2
hello.s的内容如图4-4-3.
图4-4-3
对照分析汇编代码和反汇编代码得:
- 两者的指令并没有什么不同的地方。
- 汇编语言(操作码和操作数)和二进制机器语言存在双射的关系。
- 对分支转移,反汇编的跳转指令的操作数不是段名称,而是下一条指令的相对地址的偏移量。
- 对函数调用,原汇编(.s)文件中函数调用指令的操作数是函数名称,而反汇编函数调用指令的操作数是下一条指令的相对地址的偏移量。
4.5 本章小结
本章简述了汇编的概念与作用;示范了如何用gcc工具进行汇编操做;简单分析了可重定位目标文件elf格式中的ELF头、节头部表、符号表,重定位节中的内容;对hello.o文件的反汇编结果与源汇编(.s)文件进行了对照分析,得出了一些两者之间的共同之处与一些差别,以及汇编语言与二进制机器语言之间的关系。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
链接(linking)是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时(compile time),也就是在源化码被翻译成机器代码时;也可以执行于加载时(load time), 也就是在程序被加载器(loader)加载到内存并执行时;甚至执行于运行时(run time),也就是由应用程序来执行。
这里的链接是指将hello.o与其依赖的模块中的各种代码和数据片段收集并组合为一个可执行文件hello的过程。以printf.o为例展示链接过程如图5-1-1.
图5-1-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-2-1
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
可执行目标文件的典型格式如图5-3-1.
图5-3-1
通过输出重定向将readelf读出的信息存放到hello_readelf.txt文件中,如图5-3-2所示。
图5-3-2
readelf读出的信息如下。(各段的起始地址,大小等信息存放在程序头部表中)
5.3.1ELF头
如图5-3-1-1所示。
程序人生HIT计算机系统大作业——Hello的自白