计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 数据科学与大数据
学 号 2021113529
班 级 2103501
学 生 刘渝
指 导 教 师 史先俊
计算机科学与技术学院
2023年4月
在本学期的学习中,我们深入的学习了计算机系统,对于计算机系统的软硬件等方面有了更深的理解。在学习中,我们也学到了当计算机想要运行一个程序时所要经历的各个过程,那么在本文中,我们以hello.c为例,在虚拟机Ubuntu下,利用gcc等工具去领略程序的一生,从而深化我们对于计算机系统的理解。
关键词:Ubuntu,hello.c,链接,进程,编译
目 录
6.2 简述壳Shell-bash的作用与处理流程 - 10 -
7.2 Intel逻辑地址到线性地址的变换-段式管理 - 11 -
7.3 Hello的线性地址到物理地址的变换-页式管理 - 11 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 11 -
7.7 hello进程execve时的内存映射 - 11 -
1.1 Hello简介
P2P:From Program to Process,即从程序到进程,当我们在电脑中敲下程序形成hello.c文件,一个程序就生成了,而系统为了运行这个程序(Program),会一步一步将其进行预处理,编译,汇编,链接,从而形成一个可执行文件。接着在bash中,进程管理系统为hello程序创建进程(Process),然后调用execve启动程序,利用mmap将hello映射进内存,划分时间片,最终使hello在流水线上取指译码执行。
020:From Zero-0 to Zero-0,开始时内存中并没有hello程序,即从0开始,在运行结束之后,hello进程回收,内核数据也删除,最终将hello在内存中存在的痕迹全部去掉,即以0结束,故为020.
1.2 环境与工具
硬件环境:AMD Ryzen 7 5800H with Radeon Graphics 3.20 GHz;16.0GB RAM;512GB SSD
软件环境:Windows10,Vmware workstation 16.2.2,Ubuntu18.04.6
开发调试工具:vim,gcc,edb
文件名 | 作用 |
hello.c | 源文件 |
hello.i | 预处理后文件 |
hello.s | 编译后文件 |
hello.o | 可重定位目标文件 |
hello | 可执行目标文件 |
hello.elf | hello.o的elf文件 |
hello2.elf | hello的elf文件 |
hello.fan | hello.o反汇编得到文件 |
hello2.fan | hello反汇编得到文件 |
在本节中,简要介绍了hello的P2P以及020,另外介绍了在完成本文的整个过程中所处的环境及使用的工具,还列出了在过程中所生成的文件及其功能。
2.1 预处理的概念与作用
概念:程序的预处理过程是指程序在编译之前所做的工作,在这个过程中,源文件hello.c变为预处理后的程序hello.i,这个过程通过cpp来完成。
作用:在这个过程中,cpp根据以字符#开头的命令对原始C程序进行修改,以hello.c为例,在预处理时,第一行的#include<stdio.h>告诉预处理器读取头文件stdio.h的内容并将其插入到程序的文本文件中,得到一个拓展的,以.i命名的文件。除此之外,cpp会拓展以define所定义的宏。
预处理命令:gcc -E hello.c -o hello.i
图1 预处理过程
由以下图片可看出生成了.i文件
图2 生成.i文件
预处理之后得到了这样一个大约有着3000行的文件(由于篇幅问题,本文仅截取了hello.i文件的一部分)
图3 hello.i文件
由图片中可以看出,相比于.c文件,.i文件仅仅是将.c文件中的三个以#开头的头文件stdio.h,unistd.h,stdlib.h进行了拓展,将三个头文件中的内容插到了.i文件之中,而主函数main并未发生变化。
在本章中介绍了预处理的概念及作用,Ubuntu进行预处理的命令以及预处理后得到文件的解析,通过这几个方面详细的了解了程序的预处理过程,知道了预处理之后的.i文件是什么样子的。
概念:编译是指将某一种程序语言写的程序翻译成另一种语言写的程序,以hello程序为例,编译器将文本文件hello.i翻译成文本文件hello.s的过程,它包含一个汇编语言程序。
作用:将hello.i文件翻译为hello.s文件,将高级语言转化为了汇编语言,在.s文件中每一行语句都对应了一条低级机器语言指令,该过程为不同的高级语言的不同的编译器提供了通用的输出语言,该过程通过一系列的词法分析,语法分析,语义分析以及代码优化完成。词法分析的任务是对由字符组成的单词进行处理,从左至右逐个字符地对源程序进行扫描,产生一个个的单词符号,把作为字符串的源程序改造成为单词符号串的中间程序;对于语法分析,编译程序的语法分析器以单词符号作为输入,分析单词符号串是否形成符合语法规则的语法单位,如表达式、赋值、循环等,最后看是否构成一个符合要求的程序,按该语言使用的语法规则分析检查每条语句是否有正确的逻辑结构,程序是最终的一个语法单位;中间代码是源程序的一种内部表示,或称中间语言。中间代码的作用是可使编译程序的结构在逻辑上更为简单明确,特别是可使目标代码的优化比较容易实现中间代码,即为中间语言程序,中间语言的复杂性介于源程序语言和机器语言之间;代码优化指对程序进行多种等价变换,使得从变换后的程序出发,能生成更有效的目标代码。所谓等价,是指不改变程序的运行结果。所谓有效,主要指目标代码运行时间较短,以及占用的存储空间较小。这种变换称为优化。
命令:gcc -S hello.i hello.s
图4 输入编译命令
由下图知hello.s文件已生成
图5 生成hello.s文件
生成hello.s文件如图所示
图6 hello.s文件
数据及赋值操作
赋值操作利用mov,主要应用后缀为l与q,l表示四字节,q表示八字节
全局变量main,类型function,保存在.text
图7 全局变量main
字符串,以string标注
图8 字符串
局部变量int i:movl $0,-4(%rbp)指将0赋给局部变量i,将i保存在地址%rbp-4处
图9 局部变量i
数组*argv[],整型数据argc:movq %rsi ,-32(%rbp),将argv首地址保存在%rbp-32处,movl %edi ,-20(%rbp),将argc保存在%rbp-20处
图10 char *argv[]和int argc
算数操作
(1)加法:add a,b等价于b=a+b;hello中主要应用如下:
图11 加法
(2)减法:sub a,b等价于b=b-a;hello主要应用如下
图12 减法
(3)地址之间的赋值leaq a,b等价于b=&a,具体应用如下:
图13 地址赋值
关系操作及控制转移
关系操作指cmp a,b;比较a与b的大小,通常与控制转移语句一起使用,hello中出现的控制转移语句由je,a和b相等则跳转;jle,b小于等于a则跳转;以及jmp,无条件跳转,具体如下:
(1)je:比较argc和4,等于跳转到L2
图14 je跳转
(2)jle:比较i和4,i<=4则跳转到L4
图15 jle跳转
(3)jmp:直接跳转到L3
图16 jmp跳转
函数操作
调用函数时用指令call,hello中一共调用了以下函数:
Puts
图16 puts函数
exit
图17 exit函数
printf
图18 printf函数
atoi
图19 atoi函数
sleep
图20 sleep函数
getchar
图21 getchar函数
本章中主要讲了编译的概念及过程,作用,以及编译时所输入的命令,然后解析了编译过程中产生的hello.s中出现的各个数据与操作的含义,从而加深了对于编译的理解
概念:汇编指汇编器将.s文件翻译成机器语言指令,并将这些指令打包成一种叫做可重定位目标程序的格式,并将结果保留在目标文件hello.o,它是一个二进制文件,包含的字节是main的编码
作用:将汇编语言转化为机器语言,并将其保存在可重定位目标文件中。
指令:gcc -c hello.s -o hello.o
图22 汇编指令
由下图,已生成hello.o文件
图23 生成hello.o文件
输入指令:readelf -a hello.o >hello.elf
图24 输入指令,生成文件
打开所生成的文件:
首先是elf头:
图25 elf头
首先以一个16字节序列开始,描述了生成该文件的系统的字的大小和字节顺序。剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小,目标文件的类型(可重定位,可执行,共享),机器类型(X86-64等,本例中为虚拟机unix),节头部表的文件偏移,以及节头部表中条目的大小和数量。
节头
图26 节头
包含各节的名称,大小,类型,地址,偏移量
重定位节.rela.text
图27 .rela.text
节中包含了该程序所调用的函数puts,exit,printf,atoi,sleep,getchar,另外还包含着两处字符串(L0,L1)(用.rodata表示),这一节中写明了它们的名称,寻址类型(绝对,PC),偏移量,以及符号值,信息。
重定位节.rela.eh_frame
图28 .rala.eh_frame
符号表
图29 符号表
表中包含所有需要重定位引用的符号的信息
利用指令objdump -d -r hello.o > hello.fan生成反汇编文件
图30生成反汇编文件
打开反汇编文件,比较hello.fan与hello.s文件
共有以下几处不同:
首先调用函数时
在hello.s文件中,直接利用语句call后加所调用的函数名进行调用
图31 hello.s文件中调用函数
而在hello.fan中,则利用重定位符号引用的方式调用函数,需要与动态链接器作用才能确定函数运行时地址
图32 hello.fan文件中调用函数
第二进行跳转语句时
hello.s中jxx后直接跟所要跳转的段如L1,L2等
图33 hello.s文件跳转语句
hello.fan中jxx后跟所要跳转地址相对于main的偏移量,即(例如main+0x7c,即为main所在地址加上0x7c后的地址)如图:
图34 hello.fan中跳转语句
访问全局变量时
hello.s中通过段地址加%rip完成
图35 hello.s中全局变量访问
hello.fan中通过0+%rip实现
图36 hello.fan中全局变量访问
除此之外其余基本相同
本章中主要介绍了汇编过程的概念及作用,另外生成hell.o文件后由它生成hello.elf文件及hello.fan文件,对两个文件均进行了分析,并将hello.fan与hell.s相比较
概念:链接是指将各种代码与数据片段收集并组合成为一个单一文件的过程,这个文件可被加载到内存并执行。
作用:将一个大型的应用程序分解为更小,更好管理的模块,从而可以独立的修改和编译这些模块,当我们改变这些模块中的一个时,只需简单的重新编译它,并重新链接应用,而不用重新编译其他文件。
命令为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
图37 链接
输入指令readelf -a hello >hello2.elf将其信息存到hello2.elf中
首先是elf头
图38 elf头
与上一章hello.elf大同小异,分析详见上一章,可以看到节头变为25个
节头
图39 节头
(3)程序头
图40 程序头
该节为可执行文件相比与可重定位文件新增的,包含了偏移量,虚拟地址及物理地址,根据查阅资料得知:
PHDR:指定程序头表在文件及程序内存映像中的位置和大小
INTERP:指定要作为解释程序调用的以空字符结尾的路径名的位置和大小。
LOAD:指定由p_filesz和p_memsz描述的可装入段。
DYNAMIC:指定动态链接信息。
NOTE:指定辅助信息的位置和大小。
GNU_STACK:标志栈是否可执行
GNU_RELRO:重定位后需被设定为只读内存区域
(4)重定位节
图41 重定位节
(5)符号表
图42 符号表
输入命令objdump -d -r hello >hello2.fan将hello反汇编并保存在hello2.fan中
图43 反汇编hello
不同之处:
(1)增加了许多函数:在hello中多了许多函数如printf等,其原因为链接时将库中所用代码加入hello
图44 新增函数
地址:hello.o中无确切地址,未进行重定位,而hello中已有了确定的的虚拟地址,说明已完成重定位
图45 hello
图46 hello.o
如下表所示
子程序名 | 程序地址 |
<_init> | 4004c0 |
<.plt> | 4004e0 |
<puts@plt> | 4004f0 |
<printf@plt> | 400500 |
<getchar@plt> | 400510 |
<atoi@plt> | 400520 |
<exit@plt> | 400530 |
<sleep@plt> | 400540 |
<_start> | 400550 |
<_dl_relocate_static_pie> | 400580 |
<main> | 400582 |
<__libc_csu_init> | 400610 |
<__libc_csu_fini> | 400680 |
<_fini> | 400684 |
在hello2.elf中找到了:
图47 hello2.elf
在调用前:
图48 调用前
调用后:
图49 调用后
可以看到GOT发生了改变,指向了正确的内存地址
本章主要分析了链接的过程,通过对hello生成的hello2.elf和hello2.fan的分析,以及对hello执行流程及动态链接的分析,加深了对链接的理解。
概念:进程的经典定义就是一个执行中程序的实例,系统中每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的,这个状态包括存放在内存中的程序的代码和数据,它的栈,通用寄存器,程序计数器,环境变量,打开文件描述符的集合。
作用:使用户得到一种假象,就好像我们的程序是系统当前唯一运行的程序一样,我们的程序好像独占的使用处理器和内存。处理器就好像是无间断的一条一条执行我们程序中的指令,最后我们的代码和数据好像是系统内存中唯一的对象。
作用:shell是由C语言编写的交互性应用程序,他代表用户运行程序。同时shell提供一个界面,使得用户能够对系统进行基本操作,访问操作系统内核的服务。
处理流程:如下
终端进程读取用户由键盘输入的命令行。
分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量
检查第一个命令行参数的是否是一个内置的shell命令
如果不是内部命令,调用fork创建新进程或子进程
在子进程中用步骤二获取的参数,调用execve()执行指定程序
如果用户没要求后台进行,否则shell使用waipid等待作业终止后返回
如果用户要求后台运行,则shell返回。
用户通过向shell输入一个可执行目标文件的名字hello,运行程序时,shell就会创建一个新进程,并为其分配资源,复制父进程父进程地址空间里的内容,并将其加入就绪队列,等待CPU调度。
Execve函数加载并运行可执行文件hello,且带参数列表argv和环境变量列表envp。该函数要删除已有的用户区域,加载目标文件,调用启动代码,启动代码设置新的栈,并将控制传递给新程序的主函数main,之后与库相链接并设置PC,若执行成功则不返回,只有出现错误时才返回。
进程上下文:上下文就是内核重新启动一个被抢占的进程所需的状态,上下文切换机制是建立在较低层异常机制之上的。
进程时间片:一个进程执行它的控制流的一部分每一时间段叫做时间片
用户态与核心态:为了能让处理器安全运行,需要限制应用程序可执行指令所能访问的地址范围。因此划分了用户态与核心态。核心态可以说是拥有最高的访问权限,处理器以一个寄存器当做模式位来描述当前进程的特权。进程只有故障、中断或陷入系统调用时才会得到内核访问权限,其他情况下始终处于用户权限之中,保证了系统的安全性。
系统在执行进程时,会反复横跳,一段时间执行一个程序,每个程序只执行一段时间,系统以时间片轮转调度算法来实现进程之间的调度,这种系统以该方法实现“同时”执行多个程序的过程叫做并发
1.异常种类:
图50 异常种类
2.处理方法:
图51 处理方法
3.发送信号
(1)不停乱按
图52 不停乱按
由图中可以看到乱按并未使程序停止运行,实际上乱按的输入被当成命令存放在缓冲区中
(2)回车
图53 按回车
输入回车也并未使程序运行收到影响,输入的回车也被加入到了缓冲区,在结束之后被命令行录入
(3)Ctrl+C
图54 Ctrl+C
由图可知按下Ctrl+C后程序运行直接停止,接下来利用ps与jobs命令查看
图55 输入命令查看
由图中,ps无相应pid,jobs与fg无结果可知,hello进程已被结束。
(4)Ctrl+Z
图56 Ctrl+Z
由图可知,在输入Ctrl+Z之后,程序运行停止了,但经过对ps及jobs的查看可知,该进程时被挂起而未结束,输入fg后继续进行。
Kill结束进程:
图57 kill
由图可知,利用kill可使进程终止。
利用pstree可查看所有指令
图58 pstree
在本章中,讲述了进程的概念及作用,另外讲述了创建进程,execve以及执行的过程,分析了信号与异常的处理。
预处理:将hello.c外部库拓展到hello.i中
编译:根据hello.i文件进行编译形成汇编文件hello.s
汇编:经过汇编将汇编语言转化为机器语言存在二进制文件hello.o中
链接:将库中函数以某种方式合在hello.o中形成可执行目标文件。
fork:调用fork创建进程
execve:调用execev进行加载
执行CPU为其分配资源
结束:子进程被父进程回收,资源空间被回收
感悟:通过大作业将所学知识重新梳理了一遍,对于知识有了更深的掌握