计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 未来技术
学 号 2021113669
班 级 21wl024
学 生 王元
指 导 教 师 郑贵滨
计算机科学与技术学院
2023年5月
主要内容是展示hello程序的一生。
将源文件到应用程序的过程拆分成预处理、汇编、编译、链接生成可执行文件几个步骤进行逐步分析,再通过shell实际运行hello程序,研究开始运行到进程被回收这个过程,来对计算机系统的运行原理进行说明。
关键词:计算机系统;编译;反汇编;进程管理;内存管理。
目 录
2.2在Ubuntu下预处理的命令............................................................................. - 5 -
5.3 可执行目标文件hello的格式....................................................................... - 8 -
6.2 简述壳Shell-bash的作用与处理流程........................................................ - 10 -
6.3 Hello的fork进程创建过程........................................................................ - 10 -
7.2 Intel逻辑地址到线性地址的变换-段式管理............................................... - 11 -
7.3 Hello的线性地址到物理地址的变换-页式管理.......................................... - 11 -
7.4 TLB与四级页表支持下的VA到PA的变换................................................ - 11 -
7.5 三级Cache支持下的物理内存访问............................................................. - 11 -
7.6 hello进程fork时的内存映射..................................................................... - 11 -
7.7 hello进程execve时的内存映射................................................................. - 11 -
7.8 缺页故障与缺页中断处理.............................................................................. - 11 -
8.2 简述Unix IO接口及其函数.......................................................................... - 13 -
第1章 概述
(0.5分)
1.2 环境与工具
硬件环境:CPU:AMD Ryzen 7 5800H(8核16线程)
内存:2x8GB
显卡Nvidia Geforce RTX 3060laptop
开发工具:VScode gcc edb objdump VMware
1.3 中间结果
hello.c 源文件
hello.i 经过预处理的源程序
hello.s 编译后的程序文件
hello.o 汇编器汇编后的可重定位文件
hello(.exe for windows) 程序文件
elf_o.elf 可重定位目标文件的ELF文件格式
elf_out.elf 可执行文件的ELF文件格式
1.4 本章小结
hello.c在编译过程中的各个步骤及中间产物,
hello的可执行文件执行和回收的过程,
第2章 预处理
(0.5分)
2.1 预处理的概念与作用
即替换代码中特定文本的步骤
如处理以#开头的指令,典型的有#include <stdio.h>,以及#define 宏定义的替换 , 条件编译等,就是为编译做的预备工作的阶段。
该过程不对程序的源代码进行解析,而是直接往c程序中插入文本片段,得到另一个c程序,通常以.i为后缀。
2.2在Ubuntu下预处理的命令
gcc -m64 -no -pie -fno -PIC -E hello.c -o hello.i
2.3 Hello的预处理结果解析
预处理后的文件变大了很多,其内容文本量变大,可以看到所有编译需要的内容都在其中,包括头文件的替换等等。
而在文本的末尾,我们可以找到原本hello.c的部分:
2.4 本章小结
本章主要是对预处理部分进行了一定的阐述,预处理后的文件hello.i比源文件要大很多。通过hello.i这一例子展示了预处理如何对待程序源代码,说明出预处理后的文件更加、直接、全面,,利于编译器理解,有利于进一步编译,说明了预处理的好处和重要性。
第3章 编译
(2分)
3.1 编译的概念与作用
C语言程序编译过程内的“编译”步骤,是指将预处理后形成的.i文件经过分析并优化,翻译成一份汇编语言代码,得到的结果为.s文件。,使其更加接近机器二进制语言,更符合机器运作规律的逻辑,方便下一步的汇编步骤。
3.2 在Ubuntu下编译的命令
gcc -m64 -no-pie -fno-PIC -S hello.i -o hello.s
3.3 Hello的编译结果解析
3.3.1常量
从上到下依次表示:
文件名
已编译程序的机器代码存放位置
只读数据
空行
只读数据1(LC0):
只读数据2(LC2)
…
…
函数声明
main的类型
.section 和.rodata段表明下面的数据为只读数据,即当作文本看,.rodata段中存储了两个字符串常量,即.LC0和.LC1里面的内容,分别对应源程序的两个printf.
3.3.2 变量
%edi和%rsi中分别存放着输入的相关参数,%edi表示第一个参数,即变量int argc;%rsi 表示第二个参数,即变量char ** argv
3.3.3 赋值
使用mov进行赋值,上述代码表示将栈顶元素取出存入%rax中,即给寄存器赋值
3.3.4 算数运算
可表示为:x=x+24 (%rax存x)
3.3.5 关系操作
使用cmpl将立即数4与-20(%rbp)比较,即为一个关系操作,也可使用条件码进行判断。
3.3.6 函数操作
上图中使用了call来调用exit()函数,电脑的%rip能够通过call后面的相对位置找到sleep()函数的所在位置,将当前地址压入栈中后将rip放置在指定位置运行函数。
3.3.7 控制转移
跳转指令为je能一系列指令,能将程序当前执行的部分跳转到指定处,上图即为.L2处,而跳转所需判断条件在flag寄存器中。
3.3.8 数组操作
虽然汇编中没有数组的概念,但却使用了数组的本质,即一段连续的存储空间,要想访问数组中的元素,就是用首地址加上偏移量来搜寻。
上图中-32(%rbp)为首地址,立即数16为偏移量
3.4 本章小结
在本章我们分析了在汇编后的文件的各个函数内元素的表示方式和执行过程, 了解了hello.c程序是如何在汇编代码层面上执行的,包括使用的寄存器,传递参数的方法,条件跳转,数组等一系列的程序实现问题。也为接下来的深入探讨做出了很重要的铺垫。
第4章 汇编
(2分)
4.1 汇编的概念与作用
汇编是将汇编语言代码翻译成二进制机器语言,并生成可重定位目标文件的过程。汇编器(as)将 hello.s 翻译成机器语言指令,打包成可重定位目标程序,将结果保存在目标文件hello.o中。hello.o文件是一个二进制文件,它包含的是函数main的指令编码。得到的二进制机器语言是机器可以直接理解并运行的,只要再经过链接就可以得到能够运行的完整程序了。
4.2 在Ubuntu下汇编的命令
gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o
4.3 可重定位目标elf格式
4.3.1 ELF头
ELF header以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。其余部分帮助链接器语法分析和解释目标文件的信息。
4.3.2 节头部表
包含了不同节的类型,地址,偏移大小等基本信息
4.3.3 程序头
4.3.4 重定位条目
文件中有一些内存地址或引用在链接前是待定的,需要视链接的情况指定确切的地址。因此,需要对这些地址进行重定位。因此需要一个重定位表对每个代码段或数据段进行定位,方便对它们进行查找和操作。
4.3.5符号表
.symtab是符号表,它列举了程序中用到的函数和全局变量。
4.4 Hello.o的结果解析
objdump -d -r hello.o >hello.txt
1.进制从十进制转化为了十六进制:
2.寻址方式改变:
直接使用内存地址而不是.L2等来寻址,增加运行效率
4.5 本章小结
本章介绍了汇编的概念和作用,通过生成的ELF程序了解了在执行编译时使用的表的结构,同时也用反汇编工具对生成的汇编代码进行分析,认识理解到了汇编过程和编译过程中编译器做出的工作,也了解了重定位策略
第5章 链接
(1分)
5.1 链接的概念与作用
链接是结合多个不同的可重定位目标文件,得到具有统一内存地址,能够运行的可执行程序的过程。链接将不同文件中的数据和程序段结合统一起来,在编程时方便由各个小文件组成大型程序,条理清晰,方便模块化编程。
5.2 在Ubuntu下链接的命令
注意:前一步汇编时若使用命令gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o会导致这一步链接时报错:cannot use executable file,因此要使用命令:gcc -m64 -no-pie -fno-PIC -c hello.s -s hello.o生成的hello.o文件进行链接。
5.3 可执行目标文件hello的格式
打开后发现大致结构与之前一样,不同部分有:
ELF头的大部分内容相同,一些参数发生变化。
节头部表:节头部表的节的数量相较之前的.o文件增加了很多,增加了一些可执行文件所特有的段比如.init等。.test为程序代码,.data是初始化的全局变量,.bss是未初始化的全局变量,.rodata是只读数据节,.symtab是符号节,.strtab是字符串节
符号表增添了一些需要的变量名与函数名,是导入的库内部内容。
5.4 hello的虚拟地址空间
程序内存起始位置与.init位置相同,因为.init段在该虚拟地址的段最开始处出现,所以.init是程序最开始数据内容的地址;已经转换成机器代码的代码部分转载在.text文件内。
5.5 链接的重定位过程分析
不同:
- 反汇编后的call使用了实际的地址
- 反汇编多了很多链接后加入的节与函数。
重定位相关:
重定位节和符号定位:链接器将所有相同类型的节合并为同一类型的新的节。例如,来自所有输入模块的.data 节被全部合并成一个节,这个节成为输出的可执行目标文件的.data 节。然后,链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。使程序中每条指令或变量都有唯一的运行时地址。
重定位节中的符号引用:通过前文提到的可重定位目标模块中的重定位条目,链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。
5.6 hello的执行流程
运行调用的函数名 | 函数名对应的地址 |
<_init> | 401000 |
<.plt> | 401020 |
<puts@plt> | 401090 |
<printf@plt> | 4010a0 |
<getchar@plt> | 4010b0 |
<atoi@plt> | 4010c0 |
<exit@plt> | 4010d0 |
<sleep@plt> | 4010e0 |
<_start> | 4010f0 |
<_dl_relocate_static_pie> | 401120 |
<main> | 401125 |
<__libc_csu_init> | 4011c0 |
<__libc_csu_fini> | 401230 |
<_fini> | 401238 |
5.7 Hello的动态链接分析
Plt相关:
运行前.plt及.plt.sec段:
运行后:
无变化,即不会被修改
运行前.got.plt:
运行后:
出现改动,说明完成动态链接过程。
5.8 本章小结
本章对链接的过程进行了拆分,将可执行文件的elf内容与第四部分的elf内容进行比较,了解了可执行程序的特殊之处;观察hello的虚拟地址空间内容,了解到各个数据段的存放方式;同时分析可重定位文件的内容;通过edb,了解了生成可执行文件的动态链接过程。
第6章 hello进程管理
(1分)
6.1 进程的概念与作用
一个执行中的程序实例,包括代码与当前的活动。
进程的作用:
进程提供给了我们假象,好像我们的程序是系统当前运行的唯一的程序一样,我们的程序好像是独占地使用内存和处理器,处理器就好像是无间断地一条一条执行我们的指令,我们的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
shell是用户和Linux内核之间的接口程序。提示符内输入的每个命令都由shell先解释然后传给Linux内核,shell提供了一个界面,用户通过它来与内核进行交互。shell 也是一个命令语言解释器。拥有自己内建的 shell 命令集,因此也可以将shell成为一种程序设计语言。
流程:
(1)终端进程读取用户输入的命令行。
(2)判断命令是否合法,分析获取命令行参数,改造后传递给execve的argv向量
(3)终端调用fork( )创建新进程/子进程
(4)在子进程中,用步骤2获取的参数,调用execve( )执行指定程序。
(5)如果用户没要求后台运行(命令末尾没有&号)则shell使用wait等待子进程终止后返回。
(6)如果用户要求后台运行(有&号),则shell立即返回;
6.3 Hello的fork进程创建过程
父进程通过shell函数创建一个新的运行的子进程hello。Hello进程几乎但不完全和父进程相同,hello进程得到与父进程用户级虚拟空间相同但是独立的一个副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,即当父进程调用fork时,子进程可以读写父进程的内容,但是它们有着不同的pid,但在父进程中,fork返回子进程的pid,在子进程中,fork返回0.
6.4 Hello的execve过程
调用函数fork创建新的子进程之后,子进程会调用execve函数,在当前进程的上下文中加载并运行一个新程序hello。execve 函数只在运行错误时返回,它将删除该进程的代码和地址空间内的内容并将其初始化,然后通过跳转到程序的第一条指令或入口点来运行该程序。它调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数,进入新程序。
6.5 Hello的进程执行
进程上下文:
当一个进程在执行时,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容被称为该进程的上下文。
进程时间片:
一个进程执行它的控制流的一部分的每一时间段叫做时间片。
逻辑控制流:
即使在系统中有许多其他程序在运行,进程也可以向每个程序提供一种假象,好像它在独占地使用处理器。如果想用调试器单步执行程序,我们会看到一系列的程序计数器(PC)的值,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或是包含在运行时动态链接到程序的共享对象中的指令。这个 PC值的序列叫做逻辑控制流,或者简称逻辑流。
并发流:
如果一个逻辑流的执行在时间上与另一个流重叠,则称为并发流。
用户态:
执行在用户空间中,不能直接执行系统调用。必须先切换到内核态,也就是系统调用的相关数据信息必须存储在内核空间中,然后执行系统调用,操作系统将线程分为了内核态和用户态,当用户线程调用了系统调用的时候,需要将线程从用户态切换到内核态。
用户模式和内核模式的切换:
进程从用户模式变为内核模式的唯一方法是通过诸如中断、故障或者陷入系统调用这样的异常。当异常发生时,控制传递到异常处理程序,处理器将模式从用户模式变为内核模式。处理程序运行在内核模式中,当它返回到应用程序代码时,处理器就把模式从内核模式改回到用户模式。
6.6 hello的异常与信号处理
(以下格式自行编排,编辑时删除)
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
异常种类:中断、陷阱、故障、终止。
信号:使用命令kill -l查看:
正常运行:
Ctrl+Z(挂起)
Ctrl+C(结束进程):
Ps(查看子进程):
Pstree(进程树):
Jobs(展示作业):
Fg(继续前台程序):
6.7本章小结
本章主要是讲了进程管理,包括shell、异常、信号以及进程的创建和执行过程。
第7章 hello的存储管理
( 2分)
7.1 hello的存储器地址空间
逻辑地址:逻辑地址是用户编程时使用的地址,分为段地址和偏移地址两部分。
线性地址:虚拟地址到物理地址变换的中间层,如果地址空间中的整数是连续的,那么我们说他是一个线性地址空间。如hello中代码的存储是从0x400000地址一个一个字节往上增加的。
虚拟地址:是一种虚拟的地址,是由CPU生成的用来访问主存的中间地址,虚拟地址还需由MMU硬件翻译成物理地址。
物理地址:指内存中物理单元的集合,他是地址转换的最终地址,进程在运行时执行指令和访问数据最后都要通过物理地址来存取主存。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由段标识符和段内偏移量组成:
段标识符:一个16位长的字段,其前13位可用于在段描述符表中找到一个具体的段描述符,段描述符分为全局段描述符(GDT)及局部段描述符(LDT),区分方法为段标识符的第14位,0为GDT,1为LDT。再根据相应寄存器,取得其地址,找到对应的段描述符,得到其基地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
首先通过页表基址寄存器找到查询的页表,在虚拟地址内找到虚拟页号(VPN),接着查询有效位是否有效,若有效,则查到对应的物理页号(PPN),。虚拟页偏移量与物理页偏移量完全相同,物理页号和偏移量结合得到物理地址,即可以实现虚拟地址到物理地址的转化。若无效则触发缺页中断,从低一级的缓存中查找并映射到这一级中。
7.4 TLB与四级页表支持下的VA到PA的变换
CPU生成一个虚拟地址。MMU用虚拟地址中的虚拟页号向TLB请求对应的PTE,如果PTE不在TLB中,则MMU将转而查询四级页表,原本的VPN被分为4部分,分别对应4个等级的页表,称为一/二/…级索引。我们通过四级索引找到的四级页表的对应内容,即一张三级页表,在通过三级索引在其中找到对应的二级页表,以此类推,直到在一级页表中找到物理页号(PPN),加上偏移量得到最终的物理地址。
7.5 三级Cache支持下的物理内存访问
由于cpu访问cache的速度更快,于是系统会将一些内存块缓存到三级cache中,访问内存时,会逐级查找三级缓存,若都没有则在内存中找,并将对应内存块存入cache中(替换原则:最近最少使用)
7.6 hello进程fork时的内存映射
Fork为hello创建虚拟内存空间,分配PID,并创建当前进程的mm_struct、区域结构和页表的原样副本,将两个进程的所有页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制,新进程最开始的虚拟内存与调用fork的进程相同,但发生写操作时,会通过写时复制机制创建新页面,使得两个进程的地址私有。
7.7 hello进程execve时的内存映射
Execve函数运行新程序的步骤:删除现有的用户区域,创建新区域结构,这些区域是私有的,写时复制的,代码和数据区映射到.text和.data节,bss区域和堆栈映射到匿名文件,栈和堆被初始化为空,hello所需共享对象由动态链接映射到本进程共享区域,最后设置PC,指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
缺页故障:虚拟内存中的字不在物理内存中 (DRAM 缓存不命中)
处理:
- cpu发送虚拟地址给MMU
- MMU通过页表生成PTE
- 有效位为零,触发缺页异常
- 处理程序确定物理内存中牺牲页
- 替换成所需页面,更新内存中的PTE
- 返回到原来的进程,再次执行引发缺页的指令
7.9本章小结
本章我们了解了hello程序运行的背后复杂的内存管理机制,更加深入地了解了虚拟内存到物理内存的转换以及多级页表,cache等相关知识,以及fork及execve函数的实现原理。
结论
- 开始:使用高级语言书写成的hello.c文件。
- 预处理阶段:hello.c文件进行宏替换,并把调用的库中的函数定义添加到hello.c文件中,成为hello.i文件,(仍是高级语言)。
- 汇编阶段:hello.i被翻译为汇编语言,同用数据段来表示不同的数据结构,生成hello.s文件。
- 编译阶段:生成hello.o文件(机器语言文件),此时暂时没有放进一个虚拟地址内,还需等待链接步骤。
- 链接:生成可执行程序hello文件,此时的hello文件是由hello.o文件和其他可重定位的.o文件共同生成的。
- 通过shell运行hello程序: shell通过调用fork函数创建一个子进程并通过execve来运行hello这个程序,创建出一个执行hello程序的进程。
- 为hello创建虚拟内存空间,并映射到物理内存当中.
- 在执行sleep函数的过程中发生异常(ctrl+z等等),导致hello的上下文进行切换。
- 最后:程序将被父进程回收(wait函数)。
附件
中间产物名 | 作用 |
hello.i | 预处理后的文件 |
hello.s | 汇编操作后的文件 |
hello.o | 可重定位目标文件,用于执行链接 |
hello | 可执行目标文件 |
Elf.txt | 可重定位文件的elf格式的文本形式 |
Elf1.txt | 可执行文件的elf格式的文本形式 |
参考文献
[1] csdn官网
[2]《深入理解计算机系统》,Bryant,R.E. ,机械工业出版社,2016.11.15
[3] 知乎
[4] github