计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术
学 号 2021112880
班 级 2103103
学 生 李龙泽汉
指 导 教 师 刘宏伟
计算机科学与技术学院
2022年5月
本文按照hello程序从源程序在从程序员编写的c源程序,到内存中的可执行文件,再到计算机中的进程这一过程中经历的若干步骤位顺序,对程序在计算机中程序的预处理、编译、汇编、链接、加载、在内核的控制下执行以及最后被内核回收的过程进行了大致介绍。
关键词:计算机系统;编译;进程;shell;
目 录
第1章 概述
1.1 Hello简介
P2P:
预编译器cpp修改原始的hello.c文件,将头文件内容插入其中,生成ASCII码的中间文件hello.i;
编译器cc1将hello.i中的内容翻译成汇编语言,生成ASCII码的汇编语言文件hello.s;
汇编器as将hello.s中的汇编指令翻译为机器语言指令并对其中的符号进行处理生成可重定位目标文件hello.o;
链接器ld将hello.o和其中调用的c标准库中的函数的实现链接为一个完整的可执行目标文件hello。
在shell中输入./hello,shell为hello生成一个子进程,子进程加载并执行hello,将hello的文件内容复制到内存中进行运行。
020:
OS为hello的进程分配独立的逻辑控制流和独立的地址空间。
OS的存储管理以及MMU解决VA到PA的转换,cache、TLB、页表等加速访问过程,IO管理与信号处理综合软硬件对信号等进行处理;程序结束时,shell回收hello进程,内核将其所有痕迹从系统中清除。
1.2 环境与工具
1.2.1 硬件环境
Intel X86-64
1.2.2 软件环境
Ubuntu 18.04 LTS 64;Windows11 64位
1.2.3 开发工具
GCC;vim;
1.3 中间结果
hello.c :hello源代码
hello.i :预处理后的文本文件
hello.s :hello.i编译后的汇编文件
hello.o :hello.s汇编后的可重定位目标文件
hello :链接后的可执行文件
1.4 本章小结
Hello.c程序通过预处理、编译、汇编、链接最终生成可供系统直接执行的目标文件,再由shell加载到内存,由操作系统对内存中的可执行文件进行逻辑流和存储空间的分配管理,实现进程的有序执行,执行结束后再由shell对其僵死进程进行回收。
第2章 预处理
2.1 预处理的概念与作用
C 预处理器不是编译器的组成部分,但是它是编译过程中一个单独的步骤。简言之,C 预处理器只不过是一个文本替换工具而已,它们会指示编译器在实际编译之前完成所需的预处理。cpp通过识别以“#”开头的预处理命令,对程序中的宏定义、头文件的添加以及一些其他的分支命令进行处理。
2.2在Ubuntu下预处理的命令
2.3 Hello的预处理结果解析
预处理将文件的大小从原.c文件的548b扩展到了64.7kb,向其中加入了许多内容。
上图这一部分用于识别哪个源文件和特定代码行来自哪个源代码的行标记。例如,它们可用于生成更准确的诊断消息。
上图这一部分为将头文件中大量的对数据类型的重命名以及对一些新数据类型(结构体、共用体等)的声明加入程序中。
上图这一部分为头文件中的大量对定义在动态库中的函数的声明,这些声明都加入到.i文件中。
.i文件的末尾是经过处理(如替换宏定义的符号)后的原.c文件中的内容。
2.4 本章小结
本章说明了预处理的过程,包括头文件插入、宏定义符号的替换以及条件编译,为下一步的编译做准备。
第3章 编译
3.1 编译的概念与作用
编译程序通过其内部算法将c语言的.i文件转换成等价的用汇编语言实现的.s文件,并在这个过程中利用编译器中的算法以及用户选择的优化选项对程序进行优化。
3.2 在Ubuntu下编译的命令
3.3 Hello的编译结果解析
hello.s文件的大小为1.3kb,在hello.i的基础上缩小了很多,这是由于hello.s的内容纯粹是由对程序执行的指令组成,不需要大量的定义、声明。
文件中存在部分以“.”开头的伪指令,用于对汇编过程进行控制,该类指令并不是可执行指令,没有机器代码,只用于汇编过程中为汇编程序提供汇编信息。
在main的16行将被调用者保存的寄存器rbp的内容压入栈中,之后用rbp存储初始栈指针。再将rbp减32,为main函数分配32字节运行时栈。再将edi中存储的argc和rsi中存储的argv存入栈中(rsi中存储的是argv的64位地址,其内容存储在该地址所指的内存中)。
24行将argc和4比较,25行进行条件跳转,如果相等则跳转至循环分支的条件判定部分;否则调用标准输出,将预先存储好的字符串常量输出到屏幕,之后将1作为exit()函数的参数存入edi中,之后调用exit()函数结束进程,参数为1。
如果跳转,则进入L2中:
L2中将0以一个双字存入rbp-4所指的地址中,作为c程序中的用于循环计数的局部变量i的初值,之后跳转至循环判定部分L3。
L3将栈中存储的局部变量i与8进行比较,如果i<=8则跳转至L4执行循环体内的内容。否则在55行调用getchar函数,之后将0以一个双字存入eax中作为main函数的返回值。
L4为循环体,34行将rbp-32中存储的数组argv首地址存入rax中,之后将其+16,即二倍指针型变量的大小,得到argv+2,再将*(argv+2)即argv[2]存入rdx中,后续同理操作将argv[1]存入rsi中,将这两个指针作为参数调用函数printf,输出内存中预先存好的字符串常量并用这两个指针对应的数组替代其中的占位符。
printf调用结束后,再重复之前的操作将argv[3]存入rdi中,作为参数调用函数atoi将其转化成整型数据,作为返回值存储在eax中,再将其作为参数存入edi中调用函数sleep。51行,即循环体末尾将存在rbp-4处的局部变量i加上1。
3.4 本章小结
本章主要详细介绍了编译的概念与作用,以及编译的指令,最后对编译文件hello.s进行了详细解释,包括各种指令的作用、存储资源的内容以及控制的转移。
在编译的过程中,编译器将高级语言编译生成了汇编语言,作为到机器语言的一个过渡,我们理解汇编语言可以理解编程的底层实现,对于代码优化、代码效率等方面都有帮助。
第4章 汇编
4.1 汇编的概念与作用
汇编器(as)将hello.s中的ASCII码汇编语言指令转换为可供计算机直接执行的二进制机器语言指令,并将程序中涉及的数据进一步处理生成可重定位目标文件hello.o,为后续的链接做准备。
4.2 在Ubuntu下汇编的命令
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
ELF头:
描述了声称该文件的系统的子的大小和字节顺序,ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移以及节头部表中条目的大小和数量。
重定位节:
.rela.text中为.text节中一些位置的列表。这些位置都调用了定义在外部的函数符号,当链接器将这个目标文件和其他文件组合时,需要修改这些位置的信息,以使程序再执行时可以正确跳转到外部定义的函数的实现代码段中。
符号表:
其中为程序中定义和引用的函数和全局变量的信息。如下图,Name项存储的是表示该符号的字符串,Bind用于标识该符号为本地的还是全局的,Size是目标的大小,Type用于标明目标是函数还是变量。
4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
与hello.s对比:
- hello.s中的函数调用和跳转均以函数名或是程序段的名称为目标,而反汇编生成的代码中的函数调用和跳转则以具体的代码地址为目标(相对寻址或绝对寻址)。
- hello.s中的立即数以十进制表示,而反汇编生成的代码中的立即数都以十六进制表示。
- 反汇编生成的代码中将hello.s中对外部符号的引用都转换成了对引用的占位符以及对应与该引用的重定位条目的信息。
机器语言是一种二进制语言,每一条指令、数据都由二进制来表示。汇编语言的指令用ASCII码的字符串表示。汇编语言可以根据特定的规则与其机器码形成一一对应的关系,即双射。
4.5 本章小结
本章介绍了汇编过程的概念,并对生成的可重定位目标文件进行了大致的介绍,总结了重定位目标文件各模块内容以及从原汇编文件转移到重定位目标文件过程的一些规则。
第5章 链接
5.1 链接的概念与作用
链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载到内存并执行。静态链接器以一组可重定位目标文件和命令行参数作为输入,生成一个完全链接的、可以加载和运行的可执行目标文件作为输出。
5.2 在Ubuntu下链接的命令
ld -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 /usr/lib/gcc/x86_64-linux-gnu/9/crtbegin.o hello.o
-lc /usr/lib/gcc/x86_64-linux-gnu/9/crtend.o /usr/lib/x86_64-
linux-gnu/crtn.o -z relro -o hello.out
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
5.4 hello的虚拟地址空间
edb的Data Dump窗口。窗口显示虚拟地址由0x400000开始,从开始到结束这之间的每一个节对应5.3中的每一个节头表的声明。
5.5 链接的重定位过程分析
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
Hello.out较之hello.o加入了除了.text节之外的许多文件节,例如.init和.plt,如下图。
Hello.out的代码对应的是可由操作系统直接映射到物理内存的虚拟地址,而hello.o中则是以0为起始的相对偏移地址,如下图。
Hello.out中将程序调用的动态库中的函数实现加入到了文件中,如下图。
5.6 hello的执行流程
hello调用与跳转的各个子程序名或程序地址如下:
0x400430 init;
0x400460 puts@plt;
0x400470 printf@plt;
0x400480 __libc_start_main@plt;
0x400490 getchar@plt;
0x4004a0 exit@plt;
0x4004b0 sleep@plt;
0x4004d0 _start;
0x4004fe main;
0x400580 __libc_csu_init;
0x4005f0 __libc_csu_fini;
0x4005f4 _fini;
5.7 Hello的动态链接分析
当程序调用一个由共享库定义的函数时,编译器无法预测这个函数运行时的地址,因为定义它的共享模块在运行时可以加载到任何位置。这时,编译系统提供了延迟绑定的方法,将过程地址的绑定推迟到第一次调用该过程时。他通过GOT和过程链接表PLT的协作来解析函数的地址。在加载时,动态链接器会重定位GOT中的每个条目,使它包含正确的绝对地址,而PLT中的每个函数负责调用不同函数。
在dl_init调用之前,对于每一条PIC函数调用,调用的目标地址都实际指向PLT中的代码逻辑,GOT存放的是PLT中函数调用指令的下一条指令地址。
5.8 本章小结
本章结合实验中的hello可执行程序依此介绍了链接的概念及作用以及命令,并对hello的elf格式进行了详细的分析对比。以及hello的虚拟地址空间知识,并通过反汇编hello文件,将其与hello.o反汇编文件对比,详细了解了重定位过程,遍历了整个hello的执行过程,整理了过程中的子函数,在最后对hello进行了动态链接分析,对链接有了更深的理解。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程的定义就是一个执行中程序的实例。系统每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的,包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
进程的概念让操作系统更加有序地管理和调度计算机中运行的各个程序。
6.2 简述壳Shell-bash的作用与处理流程
作用:Shell是一种命令行解释器, 其读取用户输入的字符串命令, 解释并且执行命令. 它是一种特殊的应用程序, 介于系统调用/库与应用程序之间, 其提供了运行其他程序的的接口。它可以是交互式的, 即读取用户输入的字符串;也可以是非交互式的, 即读取脚本文件并解释执行, 直至文件结束。
处理流程:shell先对以空格分割地命令行参数进行解析,并构造最终会传递给execve的argv向量。第一个参数被假设为要么是一个内置的的shell命令名,马上会解释这个命令并调用相应的函数进行执行;要么是一个可执行目标文件,会在一个新的子进程中通过execve函数加载并运行这个文件,并根据用户对前/后台运行的指示来决定shell是否等待该程序运行终止。
6.3 Hello的fork进程创建过程
用户在shell命令行中输入以“./hello”为起始的命令行,shell未找到与之匹配的内置命令,故将之解释成一个可执行文件名,通过fork()函数为shell进程创建一个子进程,这个子进程拥有与父进程除了PID和fork函数返回值之外完全相同的上下文信息,并根据fork的返回值进行分支,由子进程对hello进行加载执行。
6.4 Hello的execve过程
Shell通过fork创建的子进程,以用户输入的命令行为参数,调用execve函数加载并运行用户输入的可执行文件,如果成功则由该可执行文件产生的进程取代原子进程并不再返回,若失败则返回-1。execve加载了filename之后调用可执行目标文件中的启动代码,将控制传递给程序的主函数,主函数为如下原型:
int main(int argc, char **argv, char **envp);
其中argv和envp即为用户输入的以空格分隔的命令行,argc为argv的元素个数。
6.5 Hello的进程执行
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
在成功加载hello可执行文件之后,直接在原进程的上下文中执行新的程序。上下文是由程序正确运行所需的状态组成的,上下文是由程序正确运行所需的状态组成的,包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
同时,操作系统会为该进程分配独立的逻辑控制流和私有的地址空间保证该进程的独立正确运行。而为了与其它任务并行,系统会调度各进程轮流使用处理其,每个进程执行它的流的一部分,然后被抢占,系统将该进程的上下文保存,再调度另外被挂起的进程,载入其上下文并将控制传递给这个进程继续执行。这个过程由内核中成为调度器的代码处理其处理。一个进程执行它的控制流的一部分的每一时间段叫做时间片。
如果当前正在运行的进程发生异常,即出现中断、陷阱、错误或是终止,则系统会将控制转移给内核,即进入内核模式。处理器通过某个控制寄存器的模式位来提供这种功能,当设置了模式位时,便是当前进程运行在内核模式中,即可执行仅内核可执行的特权指令,否则则为用户模式,仅可执行部分指令。在hello进程中,每次循环都会调用sleep函数,sleep函数是系统调用函数,会将控制传递给内核,内核将当前进程挂起一段时间,并且在此期间内核可以决定执行进程的上下文切换让处理器再当前进程挂起期间执行其他的进程。
当hello执行结束之后会变为僵死进程,等待shell回收。
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
在hello运行过程中,键盘输入ctrl+z会将该进程挂起,键盘输入ctrl+c会终止进程。在键盘上乱按(输入无规则字符串)会被视作向shell输入命令行,在hello运行结束后被shell处理。
在hello被ctrl+z挂起时,命令行输入jobs可以查看当前被挂起的任务,输入ps可以查看当前运行的进程(包括被挂起等待调度的),输入pstree可以查看进程树。
6.7本章小结
本章介绍了hello从可执行程序到被系统加载并生成进程执行的过程,并对进程执行中的异常处理方法进行了说明。
第7章 hello的存储管理
7.1 hello的存储器地址空间
(以下格式自行编排,编辑时删除)
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
7.2 Intel逻辑地址到线性地址的变换-段式管理
(以下格式自行编排,编辑时删除)
7.3 Hello的线性地址到物理地址的变换-页式管理
(以下格式自行编排,编辑时删除)
7.4 TLB与四级页表支持下的VA到PA的变换
(以下格式自行编排,编辑时删除)
7.5 三级Cache支持下的物理内存访问
(以下格式自行编排,编辑时删除)
7.6 hello进程fork时的内存映射
(以下格式自行编排,编辑时删除)
7.7 hello进程execve时的内存映射
(以下格式自行编排,编辑时删除)
7.8 缺页故障与缺页中断处理
(以下格式自行编排,编辑时删除)
7.9动态存储分配管理
(以下格式自行编排,编辑时删除)
Printf会调用malloc,请简述动态内存管理的基本方法与策略。
7.10本章小结
(以下格式自行编排,编辑时删除)
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
(以下格式自行编排,编辑时删除)
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
(以下格式自行编排,编辑时删除)
8.3 printf的实现分析
(以下格式自行编排,编辑时删除)
[转]printf 函数实现的深入剖析 - Pianistx - 博客园
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
(以下格式自行编排,编辑时删除)
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
(以下格式自行编排,编辑时删除)
(第8章1分)
结论
hello作为最简单的程序,在从程序员编写的c源程序,到内存中的可执行文件,再到计算机中的进程,经历了预处理、编译、汇编、链接、加载、在内核的控制下执行以及最后被内核回收的漫长旅程,这个完整的过程体现了计算机对程序进行执行的大致流程,也在一定程度上反映了计算机系统的巧妙设计使得计算机可以有序、高效的并行执行各进程,体现出了计算机科学背后的伟大思想。
附件
hello.i:预编译器cpp修改原始的hello.c文件,将头文件内容插入其中,生成ASCII码的中间文件;
hello.s:编译器cc1将hello.i中的内容翻译成汇编语言,生成ASCII码的汇编语言文件;
hello.o:汇编器as将hello.s中的汇编指令翻译为机器语言指令并对其中的符号进行处理生成可重定位目标文件;
hello.out:链接器ld将hello.o和其中调用的c标准库中的函数的实现链接为一个完整的可执行目标文件;
hello.elf:用readelf基于hello.o的可重定位目标文件生成的.elf文件,用于对可重定位目标文件进行分析;
helloout.elf:用readelf基于hello.out的可执行目标文件生成的.elf文件,用于对可执行目标文件进行分析。
参考文献
- 兰德尔E.布莱恩特,大卫R.奥哈拉伦著. 深入理解计算机系统[M].北京:机械工业出版社,2016
[2] https://blog.csdn.net/weixin_43821874/article/details/86485888