计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术
摘 要
本文详细介绍了hello程序的一生,在Linux环境下借助gcc,cpp,cll,as,ld,vim,objdump,edb等工具,分析 hello程序如何从一个文本文件源程序hello.c经过预处理、编译、汇编、链接成为可执行目标文件,还分析了进程管理、异常与信号处理。通过分析hello的一生,复习了本课程的所有内容。
关键词:编译;预处理;汇编;链接;进程;异常与信号;
计算机科学与技术学院
2022年10月
目录
- 摘 要
- 第1章 概述
- 1.1Hello简介
- 1.2环境与工具
- 1.3中间结果
- 1.4本章小结
- 第2章 预处理
- 2.1预处理的概念与作用
- 2.2在Ubuntu下预处理的命令
- 2.3Hello的预处理结果解析
- 2.4本章小结
- 第3章 编译
- 3.1编译的概念与作用
- 3.2在Ubuntu下编译的命令
- 3.3Hello的编译结果解析
- 3.4本章小结
- 第4章 汇编
- 4.1汇编的概念与作用
- 4.2在Ubuntu下汇编的命令
- 4.3可重定位目标ELF格式
- 4.4Hello.o的解析结果
- 4.5本章小结
- 第5章 链接
- 5.1链接的概念与作用
- 5.2在Ubuntu下链接的命令
- 5.3可执行目标文件目标ELF格式
- 5.4Hello的虚拟地址空间
- 5.5链接的重定位过程分析
- 5.6Hello的执行流程
- 5.7Hello的动态链接分析
- 5.8本章小结
- 第6章 HELLO进程管理
- 6.1进程的概念与作用
- 6.2简述壳SHELL-BASH的作用与处理流程
- 6.3HELLO的FORK进程创建过程
- 6.4HELLO的EXECVE过程
- 6.5HELLO的进程执行
- 6.6HELLO的异常与信号处理
- 6.7本章小结
- 结论
- 附件
- 参考文献
第1章 概述
1.1Hello简介
P2P过程:(From Program to Process)通过编译器的处理,hello.c文件经历预处理、编译、汇编、链接,四个步骤,从源程序文件变为可执行目标文件,然后由shell为其创建一个新的进程并运行它,此时hello就从program变成了process。
020过程:(From 0 to 0)刚开始程序还不在内存空间中,所以是0,shell通过execve和fork加载并执行hello,映射虚拟内存,进入程序入口后程序开始载入物理内存,然后执行第一条指令,CPU为运行的hello分配时间片执行逻辑控制流。当程序运行结束后,shell父进程负责回收hello进程,内核删除相关数据结构,此时又变成了0。
1.2环境与工具
硬件环境:AMD Ryzen 7 5800H; 16G RAM;512G SSD
软件环境:Windows 10 64位;VirtualBox6.1;Ubuntu 20.04 LTS 64;
开发工具: gcc, objdump, vim
1.3中间结果
名称 | 作用 |
---|---|
hello.c | hello程序c语言源文件 |
hello.i | hello.c预处理生成的文本文件 |
hello.s | 由hello.i编译得到的汇编文件 |
hello.o | hello.s汇编得到的可重定位目标文件 |
elf.txt | readelf生成的hello.o的elf文件 |
hello | hello.o和其他文件链接得到的可执行目标文件 |
hello1.elf | readelf生成的hello的elf文件 |
注反汇编结果只是命令执行后查看过,并未保存文件。
1.4本章小结
本章介绍了hello的P2P和020过程,分析hello一生需要的软硬件环境和开发工具,还罗列出了中间结果文件的名字及其作用。
第2章 预处理
2.1预处理的概念与作用
预处理是指程序在编译一个c文件之前,预处理器cpp根据以字符#开头的命令(头文件、宏等),修改原始的c程序。比如hello.c中的#include 命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中,结果就得到了另一个C程序,通常是以.i作为文件扩展名。
2.2在Ubuntu下预处理的命令
gcc -m64 -no-pie -fno-PIC -E hello.c -o hello.i
2.3Hello的预处理结果解析
如图展示了部分结果,预处理器cpp处理以#开头的语句,因为,,都是标准文件库,所以它会在linux系统中的环境变量下寻找这三个库,它们在/usr/include下,然后cpp将这三个库直接插入代码中。因为这三个库中还有#define #ifdef等,所以cpp还需要将这些展开,所以hello.i最后没有#define
2.4本章小结
本章讲述了预处理的概念和作用,并且分析了hello.c经过预处理后生成hello.i的过程,对hello.c来说是cpp读取了这三个系统头文件的内容,并把它插入此程序文本中,然后得到了另一个C程序hello.i。
第3章 编译
3.1编译的概念与作用
编译过程是指编译器ccl做一些语法语义分析,如果没有错误,它就会把文本文件hello.i翻译成ASCII汇编语言文件hello.s,它包含一个汇编语言程序,翻译后的汇编语言程序中,每一条语句都以一种文本格式描述一条低级机器语言指令,为不同高级语言的不同编译器提供了通用的输出语言。
3.2在Ubuntu下编译的命令
gcc -m64 -no-pie -fno-PIC -S hello.i -o hello.s
3.3Hello的编译结果解析
3.3.1数据
1.立即数
汇编语言中立即数是以$开头的。hello.c中的整型常量是以数字出现的,对应了hello.s中的立即数,如图红框中的数字就是hello.c中的整型常量。分别是4与argc比较,exit(1)参数为1,给i赋初值1,i与9比较,i加1,输出argv[1],argv[2]时的下标1,2,以及argv[3]中的下标3。
它们与hello.s中的对应关系如图所示:
2.字符串:
字符串有红框中的两个,即”用法: Hello 学号 姓名 秒数!\n”和格式串”Hello %s %s\n”它们都作为为源程序中printf函数中直接输出的部分,存放在了.rodata中。
3.int
a.int i是局部变量存储在寄存器或者栈中,hello.c程序中i是int型4字节局部变量,在程序中声明,存放在-4(%rbp)中。
b. int argc是main中的第一个参数,根据参数传递规则,不需要声明,直接调用就可以。如图,将edi的值传给了-20(%rbp)
4.数组
char *argv[]是指向char型变量的指针, 没有单独声明,在函数执行时在命令行进行输入。argc指向已经分配好连续的连续空间,起始地址为argv,它保存在%rsi中,如图,将rsi的值传给了-32(%rbp)。
3.3.2赋值
对局部变量i赋值即i=0;因为i是双字,所以用的是movl
3.3.3类型转换
atoi(argv[3])用到了强制转换,它把字符串转换成整型数。
3.3.4算术操作
1. 加法:ADD S, D (D <- D + S)
2. 减法:SUB S, D (D <- D – S)
3.3.5关系操作
CMP S1, S2
1. 判断argc是否等于4,使用cmp S1,S2根据S2-S1的值设置条件码,等于0则ZF=1,否则ZF=0,为接下来的跳转做准备。
2.判断i是否小于等于8,将条件码设置为i – 8,为下一句跳转指令做准备
3.3.6数组指针操作
printf中用了argv[1]、argv[2],atoi中用了argv[3]对应在汇编中的操作是寻址后把argv[1]传给%rsi,argv[2]传给%rdx完成printf的参数传递,寻址后把argv[3]传给%rdi,将参数传给atoi。
3.3.7控制转移
1.第一处是判断argc是否等于4,若不等于,则继续执行,若等于,则跳转至L2处继续执行。
2.第二处是无条件跳转,以跳到L4,即循环部分代码。
3.第三处是判断是否达到循环终止条件(i<9),hello.s中是比较i和8,若小于等于则跳回L4重复循环,否则顺序执行。
3.3.8函数调用
1.第一处是调用printf(),输出一个字符串常量,参数存在%rdi中。
2.第二处是调用printf()输出字符串常量和两个char指针argv[1],argv[2]指向的字符串,字符串常量作为参数1在%rdi中,两个指针作为参数2、3分别存在%rsi和%rdx中。
3.第三处是调用atoi(),将%rdi设置为argv[3],call调用atoi函数进行字符串到整型数的转换。
4.第四处是调用sleep(),首先是将atoi转换后的值保存到%edi中,然后调用sleep。
5.第五处是调用exit,参数是1,将立即数1保存到%edi,然后调用exit
6.第六处是调用getchar(),没有参数。
3.4本章小结
本章先分析了从hello.i到hello.s的过程,然后分析了hello.s文件的程序代码,包括C语言中的各种数据类型以及各种操作指令。它比原来更加接近于底层,更加接近于机器。
第4章 汇编
4.1汇编的概念与作用
概念:汇编器as将hello.s翻译成机器语言指令,把这些指令打包成一种叫可重定位目标程序的格式,并将结果保存在目标文件hello.o中。其中hello.o是一个二进制文件。
作用:产生机器语言指令,使得机器能够识别。
4.2在Ubuntu下汇编的命令
gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o
4.3可重定位目标ELF格式
4.3.1.ELF头
ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序,剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中ELF头的大小,目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如X86-64)、字节头部表的文件偏移,以及节头部表中条目的大小和数量。
4.3.2节头部表
节头部表中描述其他节的位置和大小,还包括包括节的名称、类型、地址、偏移量、对齐等。
4.3.3.rel.text和.rel.data
.rel.text(重定位节) 一个.text节中位置的列表,包含了.text节中需要进行重定位的信息。.rel.data是被模块引用或定义的所有全局变量的重定位信息。链接时,需要重定位函数位置(exit, printf, atoi, sleep, getchar)和.rodata中LC0和LC1两个字符串中的信息。
4.3.4符号表
.symtab一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。Name是字符串中的字节偏移,指向符号的以null结尾的字符串名字,value是据定义目标的节的起始位置偏移,size是目标的大小(以字节为单位)。Type是符号的种类,有函数、数据、文件等,Binding简写为bing,表示符号是本地的还是全局的,符号表还可以包含各个节的条目,以及对应原始源文件的路径名的条目。
4.4Hello.o的解析结果
反汇编结果:
hello.s:
反汇编和hello.s在代码段很相像,但是反汇编左侧增加了汇编语言对应的机器语言指令。机器语言是由0/1所构成的序列,在终端显示为16进制表示的。
1.分支转移:
hello.s分支转移目标位都是使用.L*表示,hello.o反汇编之后,目标位置变成具体的地址。段名称在hello.s只是助记符,在hello.o机器语言中不存在。
2.函数调用:
hello.s中函数调用是call+函数名,在反汇编文件中目标地址变成了当前的PC值,因为都调用外部函数,所以需要在链接阶段重定位。
3.操作数:
hello.s中立即数是十进制的,反汇编文件中都是二进制的,在终端显示的时候转换成了十六进制。
4.5本章小结
本章分析了从hello.s到hello.c的过程,分析了hello.s的ELF文件信息,对比hello.s和hello.o反汇编代码,分析了汇编指令和机器指令区别和相同点。下一步,hello.o就会通过链接,成为可执行文件。
第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可执行目标文件目标ELF格式
1.ELF头
hello的 ELF头和hello.o的包含的信息种类相同,但是它还包括程序的入口点,也就是当程序执行时要执行的第一条地址,程序头和节数量也有了增加。
2. 节头表
与之前的相比,因为邻接,所有的节都有了实际地址。它描述各个节的大小、偏移量和其他属性。链接器链接时,将文件的相同段合并成一个段,根据这个段的大小和偏移量重定位符号的地址。
3.重定位节
与之前的重定位节完全不同,这是链接重定位的结果。
4.符号表
链接进行符号解析后,不在main中定义的符号也有了类型(TYPE),符号表不需要加载到内存。
5.程序头
程序头部表描述了可执行文件的连续的片与连续内存段的映射,我们可以看到根据可执行目标文件的内容初始化两个内存段,代码段和数据段。
5.4Hello的虚拟地址空间
使用edb加载hello,Data Dump 窗口可以查看本进程的虚拟地址空间各段信息,程序从0x00400000处开始存放。再看程序头,它告诉链接器运行时需要加载的内容,它还提供动态链接的信息。每一个表项提供各段在虚拟地址空间和物理地址空间的各方面的信息。
其中phdr显示程序头表;interp必须调用的解释器。load表示需要从二进制文件映射到虚拟地址空间的段,保存常量数据、程序的目标代码等。dynamic 保存动态链接器使用的信息;note辅助信息。gnu_stack是权限标志,标志栈是否可执行。gnu_relro:重定位后内存中只读区域的位置。
5.5链接的重定位过程分析
命令:objdump -d -r hello
分析hello与hello.o的不同:
hello是符号解析和重定位后的结果,链接器会修改hello中数据节和代码节中对每一个符号的引用,使得他们指向正确的运行地址。
1.新的函数:
hello.o与其他库链接,hello.c中使用的库中的函数就被加载进来了,如exit、printf、sleep、getchar、atoi等函数。
2.条件控制和函数调用地址都有改变
3.hello中多了.init和.plt节。
链接的过程:链接就是链接器将各个目标文件组装在一起,文件中的各个函数段按照一定规则累积在一起。
hello重定位:有重定位PC相对引用和绝对引用,对于PC相对引用,将地址改为PC值-跳转目标位置地址。绝对引用则将地址改成该符号的第一个字节所在的地址。
5.6Hello的执行流程
通过edb的调试,逐步记录call命令调用的函数。
地址 | 名称 |
---|---|
0x401000 | |
0x401020 | |
0x401090 | puts@plt |
0x4010a0 | printf@plt |
0x4010b0 | getchar@plt |
0x4010c0 | atoi@plt |
0x4010d0 | exit@plt |
0x4010e0 | sleep@plt |
0x4010f0 | |
0x401120 | |
0x401125 | |
0x4011c0 | |
0x401230 | |
0x401238 |
5.7Hello的动态链接分析
从hello的elf文件可知,.got表的地址为0x0000000000403ff0,在edb中的Data Dump窗口跳转,定位到GOT表处。
调用_init后
初始地址0x00600ff0全为0。程序调用一个由共享库定义的函数,编译器没有办法预测这个函数的运行时地址,因为它定义它的共享模块在运行时可以加载到任意位置。链接器采用延迟绑定的策略解决。动态链接器使用过程链接表PLT + 全局变量偏移表GOT实现函数的动态链接,GOT中存放目标函数的地址,PLT使用该地址跳转到目标位置,其中GOT[1]指向重定位表,GOT[2]指向动态链接器ld-linux.so运行地址。
5.8本章小结
本章介绍了链接的定义和作用,分析了程序链接的过程,查看ELF的信息分析链接生成可执行文件过程中程序发生的变化。使用edb分析了动态链接。
第6章 HELLO进程管理
6.1进程的概念与作用
概念:进程时一个执行中程序的实例。
作用:系统中的每个程序都运行在某个进程的上下文中,进程提供一个假象,就好像我们的程序是系统中当前运行的唯一的程序的一样,我们的程序好像是独占地使用处理器和内存。处理器就好像是无间断地一条接着一条地执行我们程序中的指令。最后,我们程序中的代码和数据好像是系统内存中唯一的对象。
6.2简述壳SHELL-BASH的作用与处理流程
作用:Shell能解释用户输入的命令,将它传递给内核,还可以:调用其他程序,给其他程序传递数据或参数,并获取程序的处理结果。在多个程序之间传递数据,把一个程序的输出作为另一个程序的输入。Shell本身也可以被其他程序调用。
处理流程:
- 输出一个提示符,等待输入命令,读取从终端输入的用户命令。
- 分析命令行参数,构造传递给execve的argv向量。
- 检查第一个命令行参数是否是一个内置的shell命令,如果是立即执行。
- 否则用fork为其分配子进程并运行。
- 子进程中,进行步骤2获得参数,调用exceve()执行程序。
- 命令行末尾没有&,代表前台作业,shell用waitpid等待作业终止后返回。
- 命令行末尾有&,代表后台作业,shell返回。
6.3HELLO的FORK进程创建过程
输入命令执行hello后,父进程判断不是内部指令后,会通过fork创建子进程。子进程几乎但不完全与父进程相同,子进程得到与父进程用户级虚拟空间相同但独立的副本,包括代码和数据段、堆、共享库以及用户栈。子进程可以读写父进程中打开的任何文件,二者最大的区别在于它们有不同的PID。
fork函数只会被调用一次,但会返回两次,在父进程中,fork返回子进程的PID,在子进程中,fork返回0。
6.4HELLO的EXECVE过程
execve函数在加载并运行可执行目标文件hello,且带列表argv和环境变量列表envp。当出现错误时,例如找不到hello时,execve会返回到调用程序,与一次调用两次返回的fork不同。在execve加载了hello之后,它调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数,该主函数有如下的原型:int main(intargc , char **argv , char *envp);结合虚拟空间和内存映射过程,有删除已存在的用户区域,映射私有区,映射共享区,设置PC这四个过程,进程地址空间如图所示。
6.5HELLO的进程执行
1.逻辑控制流:一系列程序计数器 PC 的值的序列叫做逻辑控制流。这个序列每个PC值唯一对应于包含在程序的可执行目标文件中的指令,或是包含在运行时动态链接到程序的共享对象中的指令,一个进程有一个逻辑控制流,进程交错执行。
2.上下文:内核重新启动一个被抢占的进程所需要恢复的原来的状态,由寄存器、程序计数器、用户栈、内核栈和内核数据结构等对象的值构成。
3.用户模式和内核模式:处理器使用某个控制寄存器一个控制位提供两种模式的区分。用户模式的进程不允许执行特殊指令,不允许直接引用地址空间中内核区的代码和数据;内核模式进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
4.进程时间片:一个进程和执行它的控制流的一部分的每一时间段。
5.上下文切换:比如sleep函数,初始时,控制在hello中,处于用户模式,调用系统函数sleep后,转到内核模式,调用进程被挂起,经过设定秒数后,发送中断信号,转回用户模式,继续执行指令。如图是一个进程上下文切换的剖析。
调度过程:
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行,我们说内核调度了这个进程。在内核调度了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。
6.6HELLO的异常与信号处理
1.异常,如图所示,异常有以下四种,分别是中断、陷阱、故障、终止。它的种类和处理方式如图所示。
2.信号,信号有很多种,图中展示的是Linux上支持的30种不同类型的信号。
3.hello正常运行状态
4.Ctrl+Z
进程收到 SIGSTP 信号, hello 进程被挂起。ps查看它的进程PID,可知 hello的PID是3333; jobs查看hello的后台 job号是1,用调用 fg 1把它调回前台。
5. Ctrl+C:进程收到 SIGINT 信号,终止 hello。在ps中没有它的PID,在job中也没有,可以看出hello已经被永远地停止了。
6.运行中不停乱按,会将屏幕的输入缓存到缓冲区,乱码被认为是命令。
7.kill,杀死进程,hello被终止。
6.7本章小结
本章介绍了hello进程的执行过程。主要是hello的创建、加载和终止,通过键盘输入。在hello运行过程中,内核有选择地对其进行管理,决定何时进行上下文切换。在hello运行过程中,接受到不同的异常信号时,异常处理程序将对异常信号做出回应,执行对应指令,每种信号有不同的处理机制,对不同的异常信号,hello有不同的处理结果。
结论
hello经历的过程
1.源文件编写:用文本编辑器写出hello的源程序文件。
2.预处理:预处理器对hello.c进行预处理,生成hello.i文本文件,将源程序中使用到的外部库插入到文件中。
3.编译:编译器对hello.i进行语法分析、优化等操作生成hello.s汇编文件。
4.汇编:as将hello.s翻译机器更易读懂的机器代码hello.o,它是一个二进制文件。
5.链接:链接器ld将hello.o和其他用到的文件进行合并链接,生成可执行文件hello。
6.运行程序:在终端中输入运行命令,shell进程调用fork为hello创建子程序,然后调用execve启动加载器,加映射虚拟内存。
7.执行指令:CPU为程序分配时间片,在一个时间片中hello使用CPU资源顺序执行控制逻辑流。
8.异常处理:hello执行的过程中可能收到来自键盘输入的信号,调用信号处理程序进行处理。
9. 进程结束:shell作为父进程回收子进程,内核删除为这个进程创建的所有数据结构。
感悟:通过分析hello的一生,把本课程的知识联系到了一起,不再是以往零碎的知识点了,有了更加完整的知识体系。
附件
名称 | 作用 |
---|---|
hello.c | hello程序c语言源文件 |
hello.i | hello.c预处理生成的文本文件 |
hello.s | 由hello.i编译得到的汇编文件 |
hello.o | hello.s汇编得到的可重定位目标文件 |
elf.txt | readelf生成的hello.o的elf文件 |
hello | hello.o和其他文件链接得到的可执行目标文件 |
hello1.elf | readelf生成的hello的elf文件 |
参考文献
[1] 龚奕利. 深入理解计算机系统.北京:机械工业出版社,2016.