计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 信息安全
学 号 2021112228
班 级 2103202
学 生 宋明烨
指 导 教 师 刘宏伟
计算机科学与技术学院
2023年5月
Hello.c程序是每个Cser的初恋。对于每个初学者,第一次接触编程时都是小心翼翼地将字符一个个敲入到codeblocks,然后点击“运行”按钮,屏幕上打印出的“Hello world”便是进入计算机世界的开始……
从点击运行按钮到shell窗口打开,再到“Hello world”映入眼帘,这短短不到一秒钟的时间里却暗藏玄机——包含着预处理、编译、汇编、链接多个过程,包含着操作系统的进程管理、虚拟内存、I/O设备的管理等多方面过程。本文带大家细节性地回顾我们的初恋——Hello.c程序。
关键词:Hello.c程序;预处理;编译;汇编;链接;进程管理;虚拟内存;I/O;
目 录
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章 概述
1.1 Hello简介
本文所用Hello.c程序要求命令行内输入三个参数,分别对应着学号、姓名、以及程序每轮循环sleep的秒数,程序较为简单。
1.2 环境与工具
硬件环境:X86-64处理器、2G RAM、1T Disk。
软件环境:Windows 11、Unbuntu 20.04。
开发工具:vscode、vim、GCC、GDB等。
1.3 中间结果
Hello.c: 源c语言代码。
Hello.i:经过预处理器处理后的文件。
Hello.s:经过编译器处理后的文件。
Hello.o:经过汇编器处理后的文件。
Hello:经过链接器生成的可执行文件。
Hello1.s:可重定位文件hello.o反汇编生成的文件。
Hello2.s:可执行文件hello反汇编生成的文件。
Hello1.txt:可重定位文件hello.o用readelf格式查看的文本文件。
Hello2.txt:可执行文件hellp用readelf格式查看的文本文件。
1.4 本章小结
本章介绍了本次实验的软硬件环境、开发工具以及中间结果文件的含义。
第2章 预处理
2.1 预处理的概念与作用
概念:预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如第一行的#include<stdio.h>命令告诉预处理器读取stdio.h文件的内容,并把它直接插入到程序文本中,得到.i文件作为文件拓展名。
作用:将#include中的文件插入到源文件、将宏定义#define替换为文本……
2.2在Ubuntu下预处理的命令
gcc -E hello.c -o hello.i。
结果如图
图2.2.1 运行预处理器cpp
图2.2.2 hello.i文件视图
2.3 Hello的预处理结果解析
Hello.i文件相比较Hello.c文件内容扩充了非常多,其中包含了将近3000行的拓展stdio.h文件的内容,如图。
图2.3.1 Hello.i中stdio部分
但文件末尾的main函数并没有改变,如图。
图2.3.2 Hello.i中main部分
2.4 本章小结
本章介绍了Hello.c程序经过预处理Hello.i的简略过程以及结果,包含了实操截图。
第3章 编译
3.1 编译的概念与作用
概念:编译器(ccl)将文本文件Hello.i翻译成文本文件Hello.s,它包含一个汇编语言程序。
作用:将Hello.i文件翻译成机器语言的文本形式——汇编语言,其中可能包含优化过程。离机器又近了一步。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
图3.2.1 运行编译器ccl
3.3 Hello的编译结果解析
3.3.1 数据类型
为了展示全局变量以及类型转换,特地设定一个int类型的全局变量,并将初始值赋为1.5。
全局变量:全局变量x在编译后的文件中存储在.data节中,1.5向int类型转换后,x的值为1,如图:.align节表示为四字节对齐、.size字节表示为变量x的长度为4个字节。
图3.3.1.1 Hello.s中.data节
局部变量:局部变量i在编译后存储在main函数的栈帧中,其地址为-4(%rbp)中,作为循环变量,赋初始值0,如图。
图3.3.1.2 Hello.s中的局部变量i地址
3.3.2 函数操作
该程序十分简单只调用了一个main函数,首先将命令行参数argc存入到通用寄存器%rdi,将字符串数组地址argv存入到通用寄存器%rsi。然后将栈顶寄存器%rsp的值减32,为main函数分配栈帧。main函数同样要调用函数exit等,同样第一个参数用%rdi寄存器,并使用call指令调用函数,如图。
图3.3.2 Hello.s中的main函数栈帧
3.3.3 控制转移
该程序中仅用if做控制转移。开始时,将%rdi中保存的argc值与4做比较,即comq指令,若等于4,则跳转到.L2代码块,否则,顺序执行会调用exit函数,将%rdi置为1后,使用call指令调用exit(1)。
在.L2代码块中,将i赋值为0后,跳转到.L3代码块执行。
在.L3代码块中,用comq指令将i值与8比较,若i小于等于8则跳转到其上方的.L4代码块执行。若大于8则执行后续指令。
在.L4代码块中,每轮循环之后,都会将i值加1,然后顺序执行到,.L3代码块,重复步骤。
如图。
3.3.4 各种操作
赋值操作:赋值操作是由mov指令完成的,其后面的字符如q表示四字。
加法操作:加法操作是由add指令完成,结果保存在后操作数。
减法操作:减法操作是由sub指令完成,结果保存在后操作数。
比较操作:比较操作是由com指令完成,结果为0或1.
跳转指令:跳转指令为jmp分为条件跳转和非条件跳转,非条件跳转无需任何判断,程序直接跳转到相应的代码处执行。
调用函数指令:调用函数为call指令,在将参数放到对应寄存器后,调用call指令调用对应函数。
3.4 本章小结
本章介绍编译的概念及其作用,详细地介绍编译后的hello.s文件中汇编代码的执行过程和对应变量和指令的含义和作用。
第4章 汇编
4.1 汇编的概念与作用
概念:汇编器(as)将Hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在二进制文件Hello.o中,用于链接。
作用:将汇编代码翻译成二进制机器代码,使机器课读懂。
4.2 在Ubuntu下汇编的命令
gcc -c hello.s -o hello.o
图4.2 运行汇编器(as)生成可重定位文件
4.3 可重定位目标elf格式
生成hello.o可重定位目标文件后,由于文件格式是二进制格式,不能直接查看,利用readelf命令生成对应文本格式文件hello1.txt,后查看,如图。
图4.3.1 调用readelf命令生成可读的hello1.txt文件
观察hello1.txt文件,入口点地址为0,需要链接后才能分配虚拟地址空间,文件由.ELF头、.data、.rodata、.bss、.symtab等节组成。如图。
图4.3.2 .ELF头部分
图4.3.3 其他节部分
图4.3.4 符号表部分
.data节:已初始化的全局变量和静态变量
.rodata节:存放只读数据
.bss节:存放未初始化的静态和全局变量以及初始化为0的静态和全局变量
.symtab:符号表,存放程序中定义的函数和变量信息
4.4 Hello.o的结果解析
使用objdump -d hello.o > hello1.s命令,将生成的hello.o文件反汇编成hello.s。
结果是:
图4.4.1 使用反汇编objdump
立即数在原始hello.s文件中以十进制表示,在反汇编生成的hello1.s中以16进制格式表示。
Objdump生成的反汇编文件hello1.s中只有代码段.text部分。
Call指令和jmp指令,所要跳转的地址在hello.s文件中都是以代码段的形式出现,而在hello1.s文件中是以相对代码段.text的偏移量形式出现。
如图。
图4.4.2 hello.s文件
图4.4.3 反汇编生成的hello1.s文件
4.5 本章小结
本章详细介绍了汇编的概念和作用。利用汇编器将汇编文件生成可重定向文件,并通过反汇编工具objdump对比了.o与.s两文件内容的区别。包含了详细的实操和截图。
第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.2.1 运行链接器生成可执行文件
5.3 可执行目标文件hello的格式
利用readelf -a hello > hello2.txt指令,将可执行文件hello的elf格式输出到hello2.txt文件中,其中ELF头部分的程序入口点部分为0x4010f0,代表着连接器为程序分配虚拟地址,各种节和符号表部分都有所变化,如图。
图5.3.1 ELF头部分
图5.3.2 其他节部分
图5.3.3 符号表部分
5.4 hello的虚拟地址空间
通过使用edb –run hello指令,启动edb。
图5.4.1 EDB虚拟地址入口
程序入口点地址为0x401000。
其中.ELF节头部表为0x4010f0恰与edb中ELF的虚拟地址相对应。
图5.4.2 EDB ELF头部表虚拟地址
5.5 链接的重定位过程分析
图5.5.1 将hello反汇编输出到hello2.s
使用objdump -d hello > hello2.s 命令,将可执行文件hello的反汇编形式输出到hello2.s文件中,观察hello2.s与hello.o的反汇编hello1.s做对比:
hello1.s中只包含.text节,且节中只有main函数代码字段,且地址空间从0开始。而hello2.s包含.init、.text、.plt多个节,地址空间从0x401000开始,且.plt节中包含了其他函数的字段,并为其分配好地址,如图。
图5.5.2 hello2.s中的.init字段
图5.5.3 hello2.s中各种函数字段
图5.5.4 hello2.s中main函数字段
在条件跳转和函数调用指令中,hello1.s中用的是相对寻址方式,而hello反汇编生成的hello2.s中用的是绝对寻址,如图。
图5.5.5 hello1.s中的相对寻址
图5.5.6 hello2.s中的绝对寻址
5.6 hello的执行流程
使用edb –run hello命令运行hello程序,逐步运行调试,观察地址空间变化以及程序运行过程。
开始时地址在0x71f2b ,为shell执行有关指令为hello程序分配空间的地址。
逐步调试,程序跳到地址为0x4010f0处,即为函数_start地址,见图5.5.3处。
如图。
图5.6.1 用edb运行程序,开始时的地址空间
图5.6.2 用edb运行程序,跳转到_start执行
5.7 Hello的动态链接分析
PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责一个具体的函数。PLT[1]调用系统启动函数(__libc_start_main),它初始化执行环境,调用main函数并处理其返回值。从PLT[2]开始的条目调用用户代码调用的函数。
首先在elf格式文件中找到.Got地址。如图。
图5.7.1 .Got地址
调用.dl_init之前:对应地址内容为空。
图5.7.2 调用.dl_init前
调用.dl_init之后:对应地址内容发生变化。
图5.7.3 调用.dl_init后
5.8 本章小结
本章详细介绍了链接的概念和作用,以及对链接过程涉及到的细节都一一展示。对链接后的可执行文件通过反汇编与链接前比对,详细介绍了虚拟地址空间的变化和函数调用过程的变化。
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程即为运行起来的程序的一个实例。
作用:操作系统通过进程提供两种抽象:
- 一个独立的逻辑控制流,提供一种假象——程序独占处理器。
- 一个私有的地址空间,提供一种假象——程序独占内存系统。
6.2 简述壳Shell-bash的作用与处理流程
shell 是一个交互型应用级程序,它代表用户运行其他程序。它通过分析用户输入的命令行来执行不同的操作。
流程:首先shell接收到一串命令行,对它开始解析。如果argv[0]是个内置的命令,则调用内置命令函数,如果不是内置命令,那么就创建一个子进程,然后在子进程中用execve指令来运行这一文件。如果命令行中带有“&”,则表示在后台运行,否则我们要一直等待知道命令完成或者子进程中文件执行结束。
6.3 Hello的fork进程创建过程
Shell进程作为父进程,通过fork函数创建子进程,子进程得到与父进程用户级虚拟地址空间相同的一份副本,包括代码段、堆、共享库以及用户栈。但子进程与父进程有不同的PID。
6.4 Hello的execve过程
在通过fork创建完成子进程之后,便开始调用execve函数,将有关数据和代码段加载进对应的虚拟内存空间中。
- 删除已存在的用户区域,删除当前进程虚拟地址用户部分已存在的区域结构
- 映射私有区域,为新程序的代码、数据、bss和栈区创建新的区域结构,如代码和数据区域被映射到.text区和.data区
- 映射共享区域,若源程序与共享对象链接,则这些对象都是动态链接到该程序的,然后再映射到虚拟地址空间中的共享区域内。
- 设置程序计数器,execve函数做的最后一件事就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
6.5 Hello的进程执行
操作系统通过对逻辑控制流的上下文切换,进入内核模式,将PC转移到Hello进程的程序入口,再切换回用户模式,执行Hello程序。
在Hello程序中,包含了系统调用函数sleep,操作系统进入内核模式。在内核模式中,计时器开始计时,内核通过上下文切换,运行其他程序,切换到用户模式。当计时器超时后(也就是sleep中设定的秒数),向处理器发送中断信号,操作系统重新切换到内核模式,进行上下文切换,转移到Hello程序继续执行,进入用户模式。
6.6 hello的异常与信号处理
下面举几个异常例子:
- Ctrl C: 用户按下ctrl-c后,内核会向当前进程组中的每个进程发送SIGINT信号,将这个进程组中的进程全部终止。在接收到这个信号后,进程由用户模式变成内核模式,这个转送给信号处理程序,这个程序捕获SIGINT信号,并默认把当前进程终止。
- Ctrl Z:用户按下ctrl-z后,内核会向当前进程发送SIGTSTP信号,在接收到这个信号后,进程由用户模式变成内核模式,将这个信号转送给信号处理程序,这个程序捕获SIGTSTP,然后默认把当前进程停止直到下一个SIGCONT信号的到来。
- Jobs:可以查看任务列表。
- Kill:内置函数,可以向其他进程(包括自己)发送信号,kill [参数] [pid],其中kill -9是向进程发送SIGKILL信号,杀死该pid的进程。
6.7本章小结
本章简略介绍了进程的概念以及进程和虚拟内存之间的关系,并介绍了操作系统通过操作系统提供的异常处理机制。通过异常处理机制提供的内核模式与用户模式之间的转化,使得进程并行成为可能,并为进程之间相互交互提供了条件。虚拟内存机制则提供了“进程隔离”的机制,使得不同进程之间又有所保护。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:是指由程序产生的与段相关的偏移地址部分,又称为绝对地址,在hello中就是各部分在段内的偏移地址。
物理地址:是内存单元的绝对地址。在hello程序中就是虚拟内存地址经过翻译后获得的地址,也就是Hello程序所在的硬件地址。
线性地址:逻辑地址到物理地址变换之间的中间层。CPU在保护模式下,在分段部件中逻辑地址是段中的偏移地址,然后加上段的基址就称为了线性地址。即hello里面的虚拟内存地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段的概念:用户编制的程序可以由一个主程序、若干个子程序、符号表、栈以及数据等若干段组成,每一段都有独立、完整的逻辑意义,每一个段的长度可以不同。
地址变换操作:逻辑地址可以分成段选择符、段描述符的判别符以及地址偏移量的形式,首先通过判别福来确定这个段描述符是局部(LDT)还是全局(GDT),然后将其组合成段描述符+地址偏移量的形式,形成线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
页的概念:页式管理将内存空间划分为一个个页面,每个页面4k字节。对于虚拟地址,分为页号和页内地址两部分。每个进程都有一个页表,存放着该进程的虚拟页号和物理页号的对应映射。页表首地址存放在页表基址寄存器CR3中。由MMU(地址变换机构)完成地址转换。
地址变换操作:在获得线性地址(虚拟地址)后,我们将这个地址分成VPN和VPO,VPN表示虚拟页号,VPO表示虚拟页偏移量,我们可以通过VPN来获得PPN(物理页号),具体如下:在TLB(翻译后备缓冲器)中,将VPN分为TLBI,TLBT来寻找所求的物理页号;若不在TLB中,则去缓存中的或内存中的页表中寻找,若缺页,MMU触发一次异常,更新页表。最终将取得的PPN与VPO组合得到我们要的物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
在地址转换时,首先在TLB中寻找对应映射,若找不到则在页表中寻找 ,CR3寄存器中存储了一级页表的地址。
因此在四级页表中我们将VPN拆成4部分,在四级页表中从一级二级三级的在这三级页表目录中寻找我们的页表的地址,然后在四级的页表中,我们找到我们要的PPN,然后与VPO组合,得到PA。
7.5 三级Cache支持下的物理内存访问
获得物理地址PA后,PA分为三个字段,分别是标记、Cache字段和块内地址字段。
我们先通过组索引来确定我们的组,然后在这个组中找有效位为1的而且有对应标记的缓存行,若找到,则用块内偏移量锁定我们要的数据块,如果找不到,则到第二级(下一级)cache中去寻找,以此类推。
7.6 hello进程fork时的内存映射
mm_struct(内存描述符):描述了一个进程的整个虚拟内存空间。
vm_area_struct(区域结构描述符):描述了进程的虚拟内存空间的一个区间。
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页表都标记为只读,并将两个进程的每个区域结构都标记为私有的写时复制。
7.7 hello进程execve时的内存映射
- 删除已存在的用户区域,删除当前进程虚拟地址用户部分已存在的区域结构
- 映射私有区域,为新程序的代码、数据、bss和栈区创建新的区域结构,如代码和数据区域被映射到.text区和.data区
- 映射共享区域,若源程序与共享对象链接,则这些对象都是动态链接到该程序的,然后再映射到虚拟地址空间中的共享区域内。
设置程序计数器,execve函数做的最后一件事就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
当访问的页不在主存空间时,发出缺页中断,进入中断处理程序,操作系统切换上下文,去执行其他的程序。
在缺页中断处理程序中,若内存中有空页位,则将磁盘上相应的页面加载进内存,若内存中无空页位,则在内存中选取一个牺牲页,将其用所需的页面替换掉。
中断处理完成后,发送信号,处理器返回到引起缺页的指令继续执行。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆(heap) 。堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址) 。对于每个进程,内核维护着一个变量brk, 它指向堆的顶部。
分配器将堆视为一组不同大小的块 (blocks)的集合来维护,每个块要么是已分配的,要么是空闲的。分配器主要分为显式分配器和隐式分配器。
策略:显式空闲链表和隐式空闲链表。
隐式空闲链表:通过头部的大小字段隐式的连接。分配器可以通过遍历堆中的所有块,从而间接地遍历整个空闲块地集合。可以通过添加脚部的方式实现隐式双向链表。寻找空闲块时可使用首次适配、下一次适配、最佳适配和分离适配等分配策略;分配空闲块时,如果块较大且找不到更合适的,则可以进行分割;释放块时需要按照四种情况合并相邻空闲块。
显式空闲链表:通过某种数据结构来管理、分配空闲块,而不去管理已分配的块。
7.10本章小结
本章详细介绍了内存管理机制,详细阐述了虚拟地址与物理地址的关系以及二者之间的相互转换过程。在虚拟地址中,详细介绍了快表TLB以及多级页表。在物理地址中,介绍了Cache与主存间的交互关系以及映射方式。
结论
- 首先,我们从键盘上一个个字符地向计算机键入我们的代码,并将其以名为hello.c的名称保存在我们的本地磁盘上。
- 然后,我们打开shell,键入gcc -o hello.c hello,shell进程识别这个字符串形式的命令。
- 经过预处理器,编译器,汇编器,链接器,通过hello.c文件,生成了可执行文件hello(还有一些中间文件)。
- 在shell中键入./hello命令,shell进程创建创建一个新的进程作为hello的进程,并将可执行文件hello加载进内存对应位置中。
- Cpu逐条执行hello中的代码,在屏幕上打印相应的内容。
感悟:通过本学期对计算机系统课程的学习,使我对计算机软硬件之间的过程有了深刻的理解,而不是像学之前一样,蒙着一层纱,对我来说,是非常有意义的一门课。我对计算机系统的理解是这样的——计算机系统指的不是某个特定的物体,物品,它不是一个实物的存在,相反,它代表的更像是一种规则,一种设计模式,一种既基于软件与硬件之间,又分别在两者之中的各种交互与处理方式。理解计算机系统的程序员,才是好的程序员。
本人认为,计算机是21世纪人类智慧的最高结晶(敬礼)。
附件
Hello.c: 源c语言代码。
Hello.i:经过预处理器处理后的文件。
Hello.s:经过编译器处理后的文件。
Hello.o:经过汇编器处理后的文件。
Hello:经过链接器生成的可执行文件。
Hello1.s:可重定位文件hello.o反汇编生成的文件。
Hello2.s:可执行文件hello反汇编生成的文件。
Hello1.txt:可重定位文件hello.o用readelf格式查看的文本文件。
Hello2.txt:可执行文件hellp用readelf格式查看的文本文件。
参考文献
[1] 深入理解计算机系统. Randal E.Bryant,David R.O’Hallaron[M]. 北京:机械工业出版社
[2] 辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.
[3] 赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).
[4] 谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.
[5] KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.
[6] CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.