计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 网络空间安全
学 号 2021111747
班 级 2103901
学 生 秦浩伦
指 导 教 师 吴锐
计算机科学与技术学院
2023年5月
本文通过还原和研究hello.c这一基础c语言文件在Linux系统下的整个生命周期,从原始程序开始,依次深入编译、链接、加载、运行、终止、回收的过程,从而了解hello.c文件的“一生”。一屋不扫,何以扫天下。通过本文对这个具有特殊意义的简单程序hello的分析,我们可以以小见大,举一反三,对整个计算机系统的工作原理更加深入了解,并借此日后挑战更为复杂的程序。
关键词:计算机系统;程序生命;计算机底层;
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第1章 概述
1.1 Hello简介
介绍PPT里蓝字:
P2P是指hello.c文件从可执行程序(Program)变为运行时进程(Process)的过程。hello.c 文件依次经过cpp预处理、ccl编译、as 汇编、ld 链接最终成为可执行目标程序hello。打开shell,输入命令./hello后,shell 通过fork产生子进程,于是,hello 便从可执行程序(Program)变成为进程(Process)。
020是指“From 0 to 0”,初始时内存中并无hello文件,起初是“0”。通过在Shell下调用execve,系统会将hello文件载入内存并执行,程序运行结束后, hello进程被回收,并由内核删除hello相关数据,并无残留,又回到了“0”。
1.2 环境与工具
硬件:
处理器:AMD Ryzen 5 5600H with Radeon Graphics 3.30 GHz
RAM:32.0 GB (31.9 GB 可用)
CPU:AMD Ryzen 5 5600
软件:
Windows 11 家庭中文版 64位
Ubuntu 18.04.5 LTS 64位
工具:
Visual Studio 2022 64-bit;
gedit,gcc,notepad++,readelf, objdump, hexedit, edb等
1.3 中间结果
- Hello.i :Hello.c预处理得到的.i文件(文本文件)
- Hello.s :Hello.i编译后得到的.s文件(汇编语言文件)
- Hello.o :Hello.s汇编后得到的.o文件(可重定位文件)
- Hello.elf:用readelf读取hello.o得到的ELF格式信息
- Hello.asm:反汇编hello.o得到的反汇编文件
- Hello: Hello.o经链接后得到的可执行文件
- Hello.out: Hello经反汇编得到的可重定位文件
1.4 本章小结
本章介绍了Hello的P2P,020的含义,并列出了所用的环境与工具。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
(以下格式自行编排,编辑时删除)
总体来说,预处理主要分为两个部分:第一个是删除程序中的注释与空白字符,就像是做菜前先把菜洗干净。第二个作用是读取函数库,例如本程序中的#include命令,会直接让预处理器读取头文件stdio.h,unistd.h,stdlib.h的内容,并直接插入到程序文本中,进行一个简单粗暴的替换。最终得到以.i为拓展名的C程序。
预处理过程将#include后继的头文件内容直接插入程序文本中,完成字符串的替换,方便后续处理。预处理过程中并未直接解析程序源代码的内容,而是对源代码进行相应的分割、处理和替换。所以,综上所述,生成的hello.i文件仍然是文本文件。
2.2在Ubuntu下预处理的命令
(以下格式自行编排,编辑时删除)
cpp hello.c > hello.i
2.3 Hello的预处理结果解析
我们可以看到,前面原本只有三行的头文件引用已经被相当多的内容替换了,而我们的源主程序,则被挤到了将近4000行。
据此可以想到,如果那么多的函数没有被提前封装好,我们的编程会变得麻烦许多。
2.4 本章小结
本章介绍了linux系统下对Hello.c文件进行预处理的过程,解析与结果展示。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
是C编译器ccl通过词法分析和语法分析,将指令翻译成汇编代码的过程。据此,编译器将文本文件 hello.i 翻译成汇编语言文件 hello.s,在hello.s中,以文本的形式描述了一条条低级机器语言指令。作用是将文本文件翻译成汇编语言程序。以便下一步的处理。
3.2 在Ubuntu下编译的命令
命令为gcc -S hello.i -o hello.s -fno-PIC -no-pie -m64
3.3 Hello的编译结果解析
由于总体内容较多,我们分类分析。
3.3.1先头结构
我们打开hello.s文件,发现在文本开头是以上信息。表示的信息如下
.file:源文件
.text:全局变量
.data:存放已经初始化的全局和静态C 变量
.section .rodata:存放只读变量
.align:对齐方式
.type:表示是函数类型/对象类型
.size:表示大小
3.3.2数据类型
分析hello.c,很容易得出本程序涉及到的数据类型共有三种,整数,字符串和数组。
整数部分:
1.int sleepescs
从.c文件中,可以看到程序声明了一个全局变量 int sleepescs,而在.s文件中,我们可以找到有关于此的部分。
编译器在.text段中将sleepsecs声明为全局变量gloal,在.type段声明其为object类型,在.size段声明其长度为4,设置其值为2(long类型)。
2.int i,argc
i作为函数内的局部变量存储在寄存器或栈中,并不占用实际空间,。对于i的操作就是直接对寄存器或栈进行操作。
i占据了四字节。
而argc作为main函数的参数保存在堆栈中。
3.立即数3,10
作为条件判断用的立即数,在汇编语言的立即寻址中直接使用,并无存储位置。
字符串部分:
程序中共有两个字符串,也就是让我们输入的学号和密码。两者均为字符串常量,储存在.text数据段中
数组部分:
本程序涉及到的数组为argv[],首地址保存在栈中。访问时,通过寄存器寻址的方式访问。
3.3.3赋值
全局变量:int sleepsecs=2.5
将2.5赋值给一个int类型,结果为2。在.data节中将sleepsecs声明为值2的long类型数据。具体过程和详细已于上文数据类型部分给出。
局部变量:int i
对局部变量的赋值在汇编代码中通过mov指令完成。根据数据大小选择不同的mov指令后缀。
B:1 w:2 l:3 q:4
3.3.4 类型转换
包含了一个浮点数转为int
int sleepsecs=2.5
结果和过程已于上文给出。
3.3.5关系(条件)判断
1.argc
在汇编文件中,使用cmpl $3,-20(%rbp),比较 argc与3的大小并设置条件码,以便下一步je跳转。
2.i大小
使用cmpl $9, -4(%rbp),下一步是jle,原理同argc。
3.3.6转移
根据前一部分条件判断的结果,进行转移。
1.Argc
当argc不等于3时,开始执行下面代码,若相等,则跳转至.L2,不执行后续部分内容。
2.I
当i < 10时进行循环,每次循环i++。在i<=9时继续循环,进入.L4,i>9时跳出循环。
3.3.7算数指令
在本程序中,涉及到的有
subq $32, %rsp:开辟栈帧
addq $16, %rax:修改地址偏移量
addl $1, -4(%rbp):实现i++
3.3.8函数调用
在本程序hello.s中,调用了main 函数,标注为function函数类型。并调用 puts,printf,sleep,exit,getchar 函数,对函数的调用通过call指令进行。
3.4 本章小结
本章介绍了编译的概念与作用,并以hello.s文件为例,介绍了编译器如何处理各个数据类型以及各类操作。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
指汇编器将以.s结尾的汇编语言程序翻译成机器语言,并打包成可重定位目标程序格式,最终结果保存在.o 文件中。
4.2 在Ubuntu下汇编的命令
gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o
4.3 可重定位目标elf格式
输入readelf -a hello.o > hello.elf 指令获得 hello.o 文件的 ELF 格式。
打开后。
分析如下:
ELF头:
描述了生成该文件的系统的字的大小和字节顺序,ELF 头大小、目标文件类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量等相关信息。
节头;描述了文件中出现的各个节的意义,包括节的类型、位置和大小等信息
重定位节:
描述了.text 节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。
符号表:
描述了定位、重定位程序中符号定义和引用的信息,以及所有重定位需要引用的符号。
4.4 Hello.o的结果解析
(以下格式自行编排,编辑时删除)
objdump -d -r hello.o > hello.asm
对比可知,差异如下:
首先在hello.s文件中,call之后直接跟着函数名称,而在hello.asm中,call 的目标地址是下一条指令。因为 hello.c 中调用的函数都是共享库中的函数,需要动态链接器作用才能确定函数的运行地址。此外在hello.s中,跳转指令的目标地址可以直接记为段名称,如.L2,.L4等。而在hello.asm中,跳转的目标为具体的地址.
4.5 本章小结
介绍了汇编的概念与作用,介绍了ELF格式文件的具体结构。比较了hello.o的反汇编代码与hello.s中代码的差异,
(第4章1分)
第5章 链接
5.1 链接的概念与作用
指通过链接器,将程序编码与数据块收集整理为单一文件,生成可执行的目标文件(linux下无后缀名)。通过链接,可以将程序编写为一个较小的源文件的集合,实现了分开编译更改源文件,降低整体文件的复杂度,减小大小。
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 可执行目标文件hello的格式
输入命令 readelf -a hello > hello2.elf 生成 hello 程序的 ELF 格式文件
利用与上一部分同样的思路分析。
ELF头:
与hello.elf相比,hello2.elf中的基本信息未发生改变,但类型发生了改变,程序头大小和节头数量增加,并且获得了入口地址。
节头:
链接后的内容比hello.elf更多。
程序头:
描述了系统准备程序执行所需的段或其他信息。
段节与dynamic section:
5.4 hello的虚拟地址空间
使用edb加载hello,查看data dump。
分析可知,在地址0x400000~0x401000中,每个节的地址都与前一节中节对应的 Address 相同。而在地址空间0x400000~0x400fff中存放着与地址空间0x400000~0x401000相同的程序,在0x400fff之后存放的是.dynamic到.shstrtab节的内容。
5.5 链接的重定位过程分析
经过分析可以发现,相比较于hello.asm,函数数量增加了,因为动态链接器将共享库中hello.c用到的函数加入可执行文件中。而且函数调用指令call的参数也发生了变化。call之后的字节代码被链接器直接修改为目标地址与下一条指令的地址之差,指向相应的代码段,从而得到完整的反汇编代码。
5.6 hello的执行流程
由于数目较多,列出表格如下:
程序名称 | 程序地址 |
ld-2.27.so!_dl_start | 0x7fce8cc38ea0 |
ld-2.27.so!_dl_init | 0x7fce8cc47630 |
hello!_start | 0x400500 |
libc-2.27.so!__libc_start_main | 0x7fce8c867ab0 |
-libc-2.27.so!__cxa_atexit | 0x7fce8c889430 |
-libc-2.27.so!__libc_csu_init | 0x4005c0 |
hello!_init | 0x400488 |
libc-2.27.so!_setjmp | 0x7fce8c884c10 |
-libc-2.27.so!_sigsetjmp | 0x7fce8c884b70 |
--libc-2.27.so!__sigjmp_save | 0x7fce8c884bd0 |
hello!main | 0x400532 |
hello!puts@plt | 0x4004b0 |
hello!exit@plt | 0x4004e0 |
*hello!printf@plt | -- |
*hello!sleep@plt | -- |
*hello!getchar@plt | -- |
ld-2.27.so!_dl_runtime_resolve_xsave | 0x7fce8cc4e680 |
-ld-2.27.so!_dl_fixup | 0x7fce8cc46df0 |
--ld-2.27.so!_dl_lookup_symbol_x | 0x7fce8cc420b0 |
libc-2.27.so!exit | 0x7fce8c889128 |
5.7 Hello的动态链接分析
由于编译器无法预测函数运行地址,所以需要添加重定位记录,等待动态链接器处理,动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,在GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数,在加载时,动态链接器会重定位GOT中的每个条目,使得它包含目标的正确的绝对地址。
5.8 本章小结
介绍了链接的概念与作用,分析了hello2.elf与hello.elf的差异,比较了反汇编文件hello2.asm与hello.asm。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程是一个正在运行的程序的实例。作用是提供两个抽象:一个独立的逻辑控制流,假装独占处理器。一个私有地址空间,假装独占内存系统。
6.2 简述壳Shell-bash的作用与处理流程
是一个用C语言编写的交互型应用程序,代表用户运行其他程序。让用户通过一个界面进行系统的基本操作,访问操作系统内核。
处理流程:
1.从终端读入输入的命令。
2.切分输入字符串,获得并识别所有的参数
3.若输入参数为内置命令,则立即执行
4.若输入参数并非内置命令,则调用相应的程序为其分配子进程并运行
5.若输入参数非法,则返回错误信息
处理完当前参数后继续处理下一参数,直到处理完毕。
6.3 Hello的fork进程创建过程
带参执行当前目录下的可执行文件hello,父进程会通过fork函数创建一个新的运行的子进程hello。子进程获取了与父进程相同的副本。区别是有着跟父进程不一样的PID,子进程可以读取父进程打开的任何文件。当子进程运行结束时,父进程如果仍然存在,则执行对子进程的回收,否则就由init进程回收。
执行结果如下:
6.4 Hello的execve过程
调用函数fork创建新的子进程之后,子进程会调用execve函数,在当前进程的上下文中加载并运行一个新程序hello。execve 函数从不返回,它将删除该进程的代码和地址空间内的内容并将其初始化,然后通过跳转到程序的第一条指令或入口点来运行该程序。将私有的区域映射进来,例如打开的文件,代码、数据段,然后将公共的区域映射进来。后面加载器跳转到程序的入口点,即设置PC指向_start 地址。_start函数最终调用hello中的 main 函数,这样,便完成了在子进程中的加载。
6.5 Hello的进程执行
进程上下文,意思是可执行程序代码,是进程的重要组成部分。这些代码从可执行文件载入到进程的地址空间执行。一般程序在用户空间执行当一个程序调用了系统调用或者触发了某个异常,它就陷入了内核空间。此时,我们称内核“代表进程执行”并处于进程上下文。上下文保存的流程是:1.保存当前的上下文2.恢复某个先前被抢占资源的进程保存的上下文3.将控制传递给这个新恢复的进程。
时间片就是设立的一个时间界限,每次只要时间到,就换一个进程来执行。相当于一个定时闹钟的作用,从而实现似乎每个进程都在独占CPU的效果。
那么结合目前的hello程序来进行讲解:hello在刚开始运行时保存了一个上下文,其在用户模式下进行。如果没有任何中断或者信号发出,那么它将会一直正常运行下去,这中间它可能会因为进程时间片的缘故和其他进程交换,但是并不影响我们的“观感”,它看起来是一直在运行的。
如果我们在这个过程中使用键盘输入,那么当前的上下文就会被保存,接着切入到内核模式,交由操作系统来处理信号,或者是让用户自己定义的信号处理系统来处理。之后再根据信号种类来决定是继续执行还是停止。
当程序在执行sleep函数时,系统调用显式地请求让调用进程休眠,调度器抢占当前进程,记录下当前的上下文,并发生上下文切换,将控制转移到新的进程,此时计时器开始计时。当计时器达到我们输入的时间时,产生中断信号中断当前进程,控制回到hello进程。
当程序执行getchar函数时,发生上下文切换,进入内核模式请求来自键盘缓冲区的输入,并执行上下文切换使其他进程运行。获得输入之后,控制回到hello进程中。
6.6 hello的异常与信号处理
1.刚才上一小节运行后按下回车
按下后发现程序结束,进程回收。
2.重新运行,过程中按下回车(乱按)
发现程序正常结束,但多出了好几行。
3.过程中按下ctrl+c
发现立即停止了,进程回收,是因为Shell进程收到了SIGINT信号。
4.过程中按下ctrl+z
发现进程中止,但并未回收,使用jobs和ps查看。
输入pstree命令,将所有进程以树状图显示
5.输入kill命令,杀死指定(进程组的)进程
6.输入fg 1则命令将hello进程再次调到前台执行
Shell首先打印hello的命令行命令,hello再从挂起处继续运行。
程序正常结束,完成进程回收。
7.脸滚键盘
无事发生。
6.7本章小结
介绍了进程的概念与作用,Shell-bash的基本概念,研究了fork,execve函数的原理与执行过程,以及hello带参执行情况下各种异常与信号处理的结果
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
(以下格式自行编排,编辑时删除)
1.逻辑地址
逻辑地址是指由程序产生的与段相关的偏移地址部分,逻辑地址由选择符和偏移量两部分组成。具体而言,其为hello.asm中的相对偏移地址。
2.线性地址
逻辑地址经过段机制转化后为线性地址,其为处理器可寻址空间的地址,用于描述程序分页信息的地址。具体以hello而言,线性地址标志着 hello 应在内存上哪些具体数据块上运行。
3.虚拟地址
根据CSAPP教材,虚拟地址即为上述线性地址。
4.物理地址
CPU通过地址总线的寻址,找到真实的物理内存对应地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
每个程序在系统中都保存着一个段表,段表保存着该程序各段装入主存的状况信息,包括段号或段名、段起点、装入位、段的长度、主存占用区域表、主存可用区域表等,从而方便进行段式管理。
7.3 Hello的线性地址到物理地址的变换-页式管理
通过一个索引,可以定位到段描述符,进而通过段描述符得到段基址。段基址与偏移量结合就得到了线性地址,虚拟地址
7.4 TLB与四级页表支持下的VA到PA的变换
为了节省页表的空间,CPU一般采用多级页表的机制,即上一级页表中的条目指向下一级页表。在一般的x86-64模式下,CPU采用四级页表,线性地址被按位划分为5部分,前4个部分分别作为该级页表的索引,最低的12位作为页的偏移地址。CPU会逐级地查找到对应物理页的PTE,从而得到物理地址。
并且,为了优化对于页表的查找效率,CPU提供了专门用于页表的缓存TLB,即翻译后备缓存器,这样CPU可以将页表的PTE缓存到TLB中,从而减少对内存的访问
7.5 三级Cache支持下的物理内存访问
由于CPU对内存的访问较慢,CPU为内存访问提供了更快的三级Cache缓存,之前访问过的内存块将被暂存在缓存中。当CPU对一个物理地址进行访问的时候,首先去看L1 Cache里是否有对应内存块,若有则直接访问L1 Cache,否则去看L2 Cache……若三级Cache里都没有对应内存块,那么CPU将会直接访问物理内存,并将物理内存中的块加载到L3 L2 L1 Cache中,并使用最近最少访问策略替换掉Cache中的某个内存块。
7.6 hello进程fork时的内存映射
使用fork创建子进程时,内核为新的子进程创建各种数据结构,并分配给子进程一个唯一的PID,为了给它创建虚拟内存空间,内核创建了当前进程的mm_struct、区域结构和页表的原样副本,将两个进程的页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。这样,在新进程里,最开始的时候它的虚拟内存和原进程的虚拟内存映射相同,但当这两个进程中的任意一个进行写操作时,写时复制机制就会创建新页面,这样两个进程的地址空间就在逻辑上私有了
7.7 hello进程execve时的内存映射
先将当前进程虚拟内存空间中的用户部分的已存在的区域结构删除,再为新程序hello的代码、数据、bss段和栈段区域创建新的区域结构,这些新的区域都是私有且写时复制的。代码和数据区域被映射为hello可执行文件中的.text和.data节,bss区域请求二进制0故映射到匿名文件,栈和堆被初始化为空。然后execve会将hello链接的动态链接库(共享对象)映射到虚拟地址空间的共享区域内。最终跳转到hello的入口点。
7.8 缺页故障与缺页中断处理
如果页表中的PTE表明这个地址对应的页不在物理内存中,那么就会引发缺页故障,这使得跳转入内核态,执行操作系统提供的缺页中断处理程序,然后缺页中断处理程序能够将存在磁盘上的页使用一定的替换策略加载到物理内存,并更新页表。缺页故障处理完毕后,CPU重新执行该条指令
7.9动态存储分配管理
(本次大作业不包含此项内容)
7.10本章小结
介绍了hello 的存储器地址空间、intel 的段式管理、hello 的页式管理, VA 到PA 的变换、物理内存访问,hello进程fork、execve 时的内存映射、缺页故障与缺页中断处理。
(第7章 2分)
(本次大作业不包含此项内容)
8.1 Linux的IO设备管理方法
(以下格式自行编排,编辑时删除)
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
(以下格式自行编排,编辑时删除)
8.3 printf的实现分析
(以下格式自行编排,编辑时删除)
https://www.cnblogs.com/pianist/p/3315801.html
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
(以下格式自行编排,编辑时删除)
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
(以下格式自行编排,编辑时删除)
(第8章1分)
结论
一个简简单单的Hello,也要经历如此多的历程。预处理,编译,汇编,链接,加载运行,执行指令,访存,信号处理,回收......我之前在本文中曾用做菜的比喻形容过Hello的运行过程,但实际的复杂程度是难以想象的。就如同数万年前,我们的祖先茹毛饮血,而现在我们用上了先进的厨具,卫生方便的理念,更多的食材选择,更丰富的食谱,更多样的做饭技巧。很难想象,计算机从被发明出来,只能进行简单的运算,到如今,机器语言,汇编语言,高级语言....这么多层次,从无到有,被前人构建出来,此时完成本篇大作业的我,深刻理解了牛顿说的那句话,尽管我打了几行字编写出Hello这样的代码,也是站在了无数巨人的肩膀上的。也是这样的层次构造,才让程序员们可以专精某样,前端,后端,底层,硬件...但是,通过这样的经历,我也感觉到,如果想要成为更专业的程序员,是不可以对其他层次一无所知的。
附件
列出所有的中间产物的文件名,并予以说明起作用。
- Hello.i :Hello.c预处理得到的.i文件(文本文件)
- Hello.s :Hello.i编译后得到的.s文件(汇编语言文件)
- Hello.o :Hello.s汇编后得到的.o文件(可重定位文件)
- Hello.elf:用readelf读取hello.o得到的ELF格式信息
- Hello.asm:反汇编hello.o得到的反汇编文件
- Hello: Hello.o经链接后得到的可执行文件
- Hello.out: Hello经反汇编得到的可重定位文件
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] Randal E.Bryant, David O'Hallaron. 深入理解计算机系统[M]. 机械工业出版社.2018.4
[2] Pianistx.printf 函数实现的深入剖析[EB/OL].2013[2021-6-9].
https://www.cnblogs.com/pianist/p/3315801.html.
/