计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术
学 号 2021113147
班 级 2103103
学 生 王继媛
指 导 教 师 刘宏伟
计算机科学与技术学院
2022年11月
从hello.c程序的编写,到经过编译系统的一步步处理,逐步形成可执行的目标文件,完成编译,再由进程控制hello的执行。
关键词:预处理;编译;汇编;链接;进程管理
目 录
第1章 概述................................................................................... - 6 -
1.1 Hello简介............................................................................ - 6 -
1.2 环境与工具........................................................................... - 7 -
1.3 中间结果............................................................................... - 7 -
1.4 本章小结............................................................................... - 7 -
第2章 预处理............................................................................... - 8 -
2.1 预处理的概念与作用........................................................... - 8 -
2.2在Ubuntu下预处理的命令................................................ - 8 -
2.3 Hello的预处理结果解析.................................................... - 9 -
2.4 本章小结............................................................................... - 9 -
第3章 编译................................................................................. - 10 -
3.1 编译的概念与作用............................................................. - 10 -
3.2 在Ubuntu下编译的命令.................................................. - 10 -
3.3 Hello的编译结果解析...................................................... - 10 -
3.4 本章小结............................................................................. - 15 -
第4章 汇编................................................................................. - 16 -
4.1 汇编的概念与作用............................................................. - 16 -
4.2 在Ubuntu下汇编的命令.................................................. - 16 -
4.3 可重定位目标elf格式...................................................... - 16 -
4.4 Hello.o的结果解析........................................................... - 18 -
4.5 本章小结............................................................................. - 19 -
第5章 链接................................................................................. - 20 -
5.1 链接的概念与作用............................................................. - 20 -
5.2 在Ubuntu下链接的命令.................................................. - 20 -
5.3 可执行目标文件hello的格式......................................... - 20 -
5.4 hello的虚拟地址空间....................................................... - 21 -
5.5 链接的重定位过程分析..................................................... - 22 -
5.6 hello的执行流程............................................................... - 24 -
5.7 Hello的动态链接分析...................................................... - 24 -
5.8 本章小结............................................................................. - 25 -
第6章 hello进程管理.......................................................... - 26 -
6.1 进程的概念与作用............................................................. - 26 -
6.2 简述壳Shell-bash的作用与处理流程........................... - 26 -
6.3 Hello的fork进程创建过程............................................ - 26 -
6.4 Hello的execve过程........................................................ - 27 -
6.5 Hello的进程执行.............................................................. - 27 -
6.6 hello的异常与信号处理................................................... - 28 -
6.7本章小结.............................................................................. - 30 -
参考文献....................................................................................... - 16 -
第1章 概述
1.1 Hello简介
Hello程序的生命周期是从一个由程序员编辑的高级c语言程序开始的,因为这种形式能够被人读懂,易于人的编辑和理解,而要想让其成功被机器识别和执行,还需要一步步将其转化为机器能够识别的机器语言,形成可执行目标文件,并以二进制磁盘文件的形式存放起来。
在Unix系统上,从源文件到目标文件的转化是由编译器驱动程序完成的:
Linux> gcc -o hello hello.c
这里gcc编译器驱动程序读取源程序文件hello.c,并将其翻译为一个可执行目标文件hello。这个翻译过程可分为以下四个阶段:
- 预处理阶段。预处理器cpp根据#开头的头文件,修改源程序,读取系统头文件的相应内容,并将其插入程序文本中,得到了一个.i文件。
- 编译阶段。编译器ccl将文本文件.i翻译为.s汇编程序。在这个过程中,高级语言被翻译为汇编语言,便于进一步向机器语言转化。
- 汇编阶段。汇编器as将.s文件转化为.o文件,生成了一个可重定位的目标程序,此时文件中已经是机器语言(二进制)。
- 链接阶段。链接器ld将.o的可重定位目标文件经过重定位生成真正的可执行目标程序。Hello程序调用了printf函数,链接器将它合并到我们的hello.o程序中。
经过这四个阶段,我们的可执行文件就生成了。在我们执行文件时,会有以下过程:
- 在shell命令行输入./hello
- Shell命令行解释器构造argv和envp
- 调用fork()函数创建子进程,其地址空间与shell父进程完全相同,包括只读代码段、读写数据段、栈及用户栈等
- 调用execve()函数在当前进程(新创建的子进程)的上下文中加载并运行hello程序。将hello中的.text节、.data节、.bss节等内容加载到当前进程的虚拟地址空间。
- 调用hello程序的main()函数,hello程序开始在一个进程的上下文中运行
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件环境:AMD Ryzen 5 4600U with Radeon Graphics;CPU2.10 GHz ;16GRAM
软件环境:Windows10 64位;Vmware 16以上;Ubuntu 20.04 LTS 64位
工具:codeblocks;gcc;Objdump;
1.3 中间结果
1.4 本章小结
本章简介了hello.c程序从编写到执行,编译系统如何让其一步步转化为可执行文件的过程,又介绍了生成可执行文件后,执行时操作系统的基本执行流程,下面我们将按顺序从预处理开始到进程管理,一步步进行更详细的介绍。
第2章 预处理
2.1 预处理的概念与作用
- 概念:在预处理阶段,预处理器根据以字符#开头的命令,修改原始的C程序,得到另一个C程序。
- 作用:
- 将源文件中以“include”格式包含的文件复制到编译的源文件中。
- 以实际值替换用#define定义的字符串
- 根据#if / #ifdef / #ifndef / #else / #elif / #endif(条件编译)等后面的条件决定需要编译的代码。
总之,处理带有#的代码,对源程序进行补充和修改。
2.2在Ubuntu下预处理的命令
首先用如下指令生成hello.i文件
生成部分内容如下的文件:
可以看到,hello.i文件中还是高级C语言,最下方还保留着源文件内容:
而上面插入了大量其他代码,包含了头文件的内容。
2.4 本章小结
预处理是将源程序的带有#的代码进行解析,将#include的文件复制,对#define进行替换等等,但是仍然还是高级语言程序。
第3章 编译
3.1 编译的概念与作用
- 概念:编译器将.i文件翻译为.s文件,即将修改了的源程序翻译为汇编程序。
- 作用:编译过程会进行语法分析和词法分析及检查,并进行一些基于编译器的优化。将C语言翻译为汇编语言。汇编语言为不同高级语言的不同编译器提供了通用的输出语言。
3.2 在Ubuntu下编译的命令
生成hello.s文件的命令如下:
3.3 Hello的编译结果解析
这里我们需要将源代码和汇编代码比对分析:
3.3.1关于伪指令
打开hello.s文件,我们可以看到开头部分有几行以‘.’开头的行,这些是直到汇编器和链接器工作的伪指令,大致作用如下:
节名称 作用
.file 声明源文件
.text 代码节
.section .rodata 只读数据段
.align 声明对指令或者数据的存放地址进行对齐的方式
.globl 声明全局变量
.type 声明一个符号是函数类型还是数据类型
.size 声明大小
.string 声明一个字符串
3.3.2数据
- 常量
从源代码中我们可以看出,常量有,这里的4,0,9,在汇编文件中以立即数的形式出现
这是将argc与4比较
这是给i赋初值0
这里是将i与8比较,在这里,汇编器将i<9转化为了i<=8的判断。
综上可以看出,对于常数,都是以立即数形式出现,执行赋值、比较或运算等操作的。它被存放于代码中,在.text节位置。
- 变量
在main函数中,定义了一个局部变量,
而在汇编文件中,我们看到栈为这个变量分配了空间,这个i存储在%rbp指向地址-4的位置。
- 类型
可以看到,定义的i和传入的参数argc都是整型数,4字节,而数组*argv[]是字符指针数组,每个指针占八个字节,指针指向的数据是字符串,它们分别存储于-4(%rbp),-20(%rbp),和以-32(%rbp)为起点的位置(存储的是指向字符的指针)。
- 字符串
两个存放在只读数据段的字符串:
3.3.3赋值
这里主要是i=0的赋值操作。
movl是对四字节数的操作,正好对应i的整型数类型。
3.3.4类型转换
hello.c中的atoi(argv[3])利用函数将字符串类型显式转换为整形。
3.3.5算术操作
程序涉及i++的算数操作。
前面我们已经分析过,i存储在栈中,在-4(%rbp)的位置,循环每次加一,addl是对四字节数做加法的操作。
3.3.8关系操作
主要有两个:
在汇编文件中对应代码如下:
这是一个条件判断语句,cmpl对argc和4进行比较,不改变寄存器和栈值,只设置条件码,后续根据条件码决定是否跳转。
在汇编语言中对应代码如下:
这里是将i与8比较,在这里,汇编器将i<9转化为了i<=8的判断。同样,也会设置条件码,用来判断后续是否跳转。
3.3.6数组/指针操作
参数中传入了一个字符指针数组。数组的起始地址存放在-32(%rbp)的位置,数组中的每一个元素都是一个指向字符类型的指针,在内存中被两次调用传给printf函数,还被函数atoi调用。
3.3.7控制转移
包括一个if语句和for循环
这是判断argc与4是否相等的操作。首先通过cmpl比较argc与4,设置条件码ZF,je通过ZF的值判断,如果ZF设置为1,则相等,跳转到L2,否则不跳转,向下执行(进入if)。
这是for循环中的赋初值操作,赋初值之后,jmp无条件跳转至L3
进入L3之后先进行比较,i与8,jle通过(SF^OF)|ZF判断是否满足i<=8,满足,则跳转至L4(继续循环),不满足则继续执行getchar()操作。由于L4在L3前面,即执行完L4就又会进入L3进行判断,不断跳转循环,直至i>8不再跳转。
3.3.8函数操作
- 在文件中我们定义了全局函数main(),其中有两个参数第一个为argc,第二个为*argv[],分别存放在寄存器rai和rsi中传递。
在执行完gerchar()之后,令返回值为0,结束程序。
- printf函数
一共调用了两次printf函数
第一次只对rdi赋值
第二次rdi,rsi,rdx都用来传递参数。
- exit函数
将edi设置为1,终止。
- sleep函数
类似的,将edi用来传参。而eax是atoi的返回值。
- atoi函数
类似的,将rdi用来传参,传递的是argv[3]。
- getchar函数
直接调用gerchar(),执行相应操作。
3.4 本章小结
本章主要介绍了编译器(ccl)处理C语言程序的基本过程,分别从数据,赋值语句,类型转换,算术操作,关系操作,数组/指针操作,控制转移与函数操作这几点进行分析,解析代码是如何从高级语言转化为汇编语言的。
第4章 汇编
4.1 汇编的概念与作用
概念:是指汇编器将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.c中的过程。
作用:将汇编语言翻译成机器语言(二进制),使之在链接后可以被机器识别并执行。
4.2在Ubuntu下汇编的命令
4.3 可重定位目标elf格式
根据所学,可知可重定位目标文件应主要包括以下内容:
ELF头:以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。
.text节:已编译程序的机器代码。
.rodata节:只读数据,如printf 格式串、switch 跳转表等。
.data节:已初始化的全局和静态C变量.局部C变量在运行时被保存在栈中,既不出先在.data节中,也不出现在.bss节中。
.bss节:未初始化全局变量,仅是占位符,不占据任何实际磁盘空间。区分初始化和非初始化是为了空间效率。
.symtab节:一个符号表,存放在程序中定义和引用的函数和全局变量的信息。.symtab符号表不包含局部变量的条目
.rel.text节:一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。另一方面,调用本地函数的指令则不需要修改。
.rel.data节的重定位信息,用于对被模块使用或定义的全局变量进行重定位的信息,一般而言,任何已经初始化的全局变量,如果它的初始值为一个全局变量地址或者外部定义函数的地址,都需要被修改。
.debug节:一个调试符号表,其条目是程序中定义的局部变量和类型定义,程序中定义和引用的全局变量,以及原始的C源文件。
.line:原始C源程序中的行号和.text节中机器指令之间的映射.
.strtab:一个字符串表,其内容包括.symtab和.debug中的符号表,以及节头部中的节名字。
现在我们用如下指令生成hello.o文件的elf格式
可以看到其ELF头如下
节头如下:
本文件中没有程序头
.symtab节
重定位节:
.rela.text
4.4 Hello.o的结果解析
- 分支转移
hello.s:
hello1.txt:
L2,L3,L4等替换为了确定的相对偏移地址,可见L2,L3,L4等这类只是助记符,在汇编后不存在了。
- 函数调用
hello.s:
hello1.txt:
在可重定位文件中call后面不再是函数的具体名称,而是一条重定位条目指引的信息。而在反汇编文件中可以看到,call后面直接加的是所调用函数在可重定位文件中的地址。
- 数据变化
hello.s:
hello1.txt:
可以发现在可重定位目标文件中,操作数都转化为了十六进制。
4.5 本章小结
本章主要是hello.s到hello.o的过程。我们查看了hello.o的可重定位目标文件,并且利用反汇编工具查看了反汇编代码,比较了hello.s与hello.o中代码的不同,从而了解了汇编器的工作内容。
第5章 链接
5.1 链接的概念与作用
概念:链接是将各种代码和数据片段收集并合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时,也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至执行于运行时,也就是由应用程序来执行。
作用:链接器在软件开发过程中扮演着一个关键的角色,因为它们使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其它文件。
5.2 在Ubuntu下链接的命令
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
5.3 可执行目标文件hello的格式
ELF头:描述文件的总体格式,还包括程序的入口点,也就是说当程序运行时要执行的第一条指令的地址。
节头部表:对 hello中所有的节信息进行了声明,包括大小和偏移量。
程序头部表:描述从可执行文件的连续的片到连续的内存段的映射。
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
5.4 hello的虚拟地址空间
使用edb加载程序,查看本进程的虚拟地址空间各段信息。
在0x401000~0x402000段中,程序被载入,虚拟地址0x401000开始,到0x401ff0结束,根据5.3中的程序头表,可以通过edb找到对应的信息。
比如PHDR从地址400040开始,大小为0x2a0;INTERP从4002e0开始,大小为ox1c。其他也是如此。
5.5 链接的重定位过程分析
- 比较发现,hello中除了main函数之外,多了很多节,而hello.o中只有main函数。
- hello中代码地址不是像 hello.o一样从0开始。
- hello中对部分.text和.data节重定位
链接的过程:
符号解析:链接器解析符号引用的方法是将每个引用与它输入的可重定位目标文件的符号表中的一个确定的符号定义关联起来。
重定位:链接器在完成符号解析以后,就把代码中的每个符号引用和正好一个符号定义(即它的一个输入目标模块中的一个符号表条目)关联起来。此时,链接器就知道它的输入目标模块中的代码节和数据节的确切大小。然后就可以开始重定位步骤了,在这个步骤中,将合并输入模块,并为每个符号分配运行时的地址。重定位由两部组成。首先是重定位节和符号定义,链接器将所有输入到hello中相同类型的节合并为同一类型的新的聚合节。例如,来自所有的输入模块的.data节被全部合并成一个节,这个节成为hello的.data节。然后,链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每一个符号。当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。然后是重定位节中的符号引用,链接器会修改hello中的代码节和数据节中对每一个符号的引用,使得它们指向正确的运行时地址。
5.6 hello的执行流程
程序名 程序地址
401000 <_init>
401020 <.plt>
401090 <puts@plt>
4010a0 <printf@plt>
4010b0 <getchar@plt>
4010c0 <atoi@plt>
4010d0 <exit@plt>
4010e0 <sleep@plt>
4010f0 <_start>
401120 <_dl_relocate_static_pie>
401125 <main>
4011c0 <__libc_csu_init>
401230 <__libc_csu_fini>
401238 <_fini>
5.7 Hello的动态链接分析
got是数据段的一部分,而plt是代码段的一部分。plt初始存的是一批代码,它们跳转到got所指示的位置,然后调用链接器。初始时got里面存的都是plt的第二条指令,随后链接器修改got,下一次再调用plt时,指向的就是正确的内存地址。plt就能跳转到正确的区域。
.got.plt的起始位置为0x404000。
进入edb查看,调用前:
调用后:
从图中可以看到.got.plt的条目已经发生变化。
5.8 本章小结
本章我们深入了解了链接的基本步骤,查看了hello的可执行文件格式和虚拟地址空间,并对其重定位、执行和动态链接过程进行分析。
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程是一个执行中的程序的实例。系统中每一个程序都运行在某个进程的上下文中。上下文是程序正确运行所需的状态的组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
作用: 提供给应用程序两个关键抽象:一个独立的逻辑控制流,它提供一个假象,好像我们运行的每个程序独占的使用处理器。一个私有的地址空间,它提供一个假象,含香我们的程序独立的使用内存系统。
6.2 简述壳Shell-bash的作用与处理流程
Shell是一个交互级的应用级程序,它代表用户运行其他程序。
处理流程:
- 在shell命令行输入./hello
- shell命令行解释器构造argv和envp
- 调用fork()函数创建子进程,其地址空间与shell父进程完全相同,包括只读代码段、读写数据段、栈及用户栈等
- 调用execve()函数在当前进程(新创建的子进程)的上下文中加载并运行hello程序。将hello中的.text节、.data节、.bss节等内容加载到当前进程的虚拟地址空间。
- 调用hello程序的main()函数,hello程序开始在一个进程的上下文中运行
6.3 Hello的fork进程创建过程
用户输入./hello 学号 姓名 时间 后,shell命令行解释器构造argv和envp。父进程通过调用fork函数创建一个新的运行的子进程。新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时。子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程最大的区别在于他们有不同的PID。
特点
- 调用一次,返回两次。在父进程中fork会返回子进程的PID(总为非零值),在子进程中fork会返回0。
- 并行执行。父进程与子进程是并发运行的独立进程。内核能够以任何方式交替执行他们逻辑控制流中的指令。
- 相同但是独立地址空间。父进程与子进程的地址空间是相同的,但它们都有自己的私有地址空间。
- 共享文件。子进程继承父进程所有的打开文件。
6.4 Hello的execve过程
当创建了一个子进程之后,子进程调用exceve函数在当前子进程的上下文加载并运行hello程序,
- 删除已存在的用户区域。删除之前进程在用户部分中已存在的区域结构。
- 映射私有区域。创建新的代码、数据、堆和栈段。所有这些区域结构都是私有的,写时复制的。
- 映射共享区域。如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
- 设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。
6.5 Hello的进程执行
- 逻辑控制流和时间分片:一系列PC的值的序列叫做逻辑控制流,多个流并发的执行的一般现象被称为并发,一个进程和其他进程轮流运行的概念叫做多任务,一个进程执行它的控制流的一部分的每一时间段叫做时间片,因此,多任务也叫时间分片。
- 用户模式和内核模式:为了使操作系统内核提供一个无懈可击的进程抽象,处理器必须提供一种机制,限制一个应用可以执行的指令以及它可以访问的地址空间范围。通常处理器通过控制寄存器中的一个模式位来提供这种功能。设置了模式位,进程就运行在内核模式中,可以执行指令集中的任何指令,并且可以访问系统中的任何内存位置。没有设置,则在用户模式,不允许执行特权指令。
- 上下文切换:内核为每个进程维持一个上下文,上下文就是内核重新启动的一个被强占的进程所需的状态。
6.6 hello的异常与信号处理
主要包括四种:
按下ctrl+z,进程收到 SIGSTP 信号, hello 进程挂起。用ps,jobs,pstree可查看,调用fg 1可将其调回前台,kill可杀死进程。
不停乱按:被缓存到stdin,并随着printf指令被输出到结果
按下ctrl+c:在键盘上输入Ctrl+c会导致内核发送一个SIGINT信号到前台进程组的每个进程,默认情况是终止前台作业,用ps查看前台进程组发现没有hello进程,如图所示。
6.7本章小结
本章了解了进程的概念和作用,与进程控制相关的函数和信号,并进行实操,学习了异常的种类和处理。
结论
预处理阶段。预处理器cpp根据#开头的头文件,修改源程序,读取系统头文件的相应内容,并将其插入程序文本中,得到了一个.i文件。
编译阶段。编译器ccl将文本文件.i翻译为.s汇编程序。在这个过程中,高级语言被翻译为汇编语言,便于进一步向机器语言转化。
汇编阶段。汇编器as将.s文件转化为.o文件,生成了一个可重定位的目标程序,此时文件中已经是机器语言(二进制)。
链接阶段。链接器ld将.o的可重定位目标文件经过重定位生成真正的可执行目标程序。Hello程序调用了printf函数,链接器将它合并到我们的hello.o程序中。
在shell命令行输入./hello
Shell命令行解释器构造argv和envp
调用fork()函数创建子进程,其地址空间与shell父进程完全相同,包括只读代码段、读写数据段、栈及用户栈等
调用execve()函数在当前进程(新创建的子进程)的上下文中加载并运行hello程序。将hello中的.text节、.data节、.bss节等内容加载到当前进程的虚拟地址空间。
调用hello程序的main()函数,hello程序开始在一个进程的上下文中运行
计算机对于程序的编译和运行设计的如此巧妙,让我深刻体会到了计算机之美,希望在未来的学习中能更加深入的学习,体会计算机的科学之美。
附件
参考文献
[1] 深入理解计算机系统原书第3版-文字版.pdf
[2] fork()创建子进程步骤、函数用法及常见考点(内附fork()过程图)_小岳王子的博客-CSDN博客_fork函数流程图 fork创建子进程