计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 未来技术
学 号 XXXXXX
班 级 XXXXXXx
学 生 xxxxxxxxx
指 导 教 师 郑贵滨
计算机科学与技术学院
2023年5月
本文通过分析hello程序从出现到运行结束所经历的步骤,概述了所有程序在计算机中处理和运行所经历的过程,为计算机系统知识的概述。
关键词:计算机系统;hello程序;
目 录
6.2 简述壳Shell-bash的作用与处理流程... - 22 -
6.3 Hello的fork进程创建过程... - 22 -
7.2 Intel逻辑地址到线性地址的变换-段式管理... - 26 -
7.3 Hello的线性地址到物理地址的变换-页式管理... - 26 -
7.4 TLB与四级页表支持下的VA到PA的变换... - 27 -
7.5 三级Cache支持下的物理内存访问... - 27 -
7.6 hello进程fork时的内存映射... - 28 -
7.7 hello进程execve时的内存映射... - 28 -
第1章 概述
Hello的P2P过程:
编写hello程序的程序员在编写hello的c代码后,系统会对c代码进行预处理,编译,汇编,链接形成可执行文件,再由shell创建fork子杀进程,得到了一个运行的进程,完成了从程序到进程的过程,即从program到process。
Hello的020过程:
Hello的020过程是内存中原无hello文件的内容,再到有文件的内容,再到无文件的时候。
系统通过调用excave对hello程序进行加载,为hello创建虚拟内存,并进行虚拟内存映射载入物理内存,进入main函数执行代码,cpu分配时间片,期间可能有异常信号,对异常信号进行处理,待程序运行终止后,删除创建的虚拟内存及相关数据,即完成了from zero to zero。
1.2 环境与工具
处理器:11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz 2.80 GHz
机带RAM:16.0 GB (15.8 GB 可用)。
软件:Windows11 64位;VMware Workstation 17 Player;Red Hat Enterprise Linux 7 64 位。
调试工具:Visual Studio 2022;Codeblocks
1.3 中间结果
来源/内容 | |
hello.c | 原始的c语言代码 |
hello.i | 预处理后的文件 |
hello.s | 编译之后的汇编文件 |
hello.o | 汇编后的可重定位文件 |
hello | 可执行文件 |
hello.text | 反汇编后的文件 |
1.4 本章小结
本章大致介绍了hello的P2P和020过程,并简单介绍了hello从无到执行完经历的文件过程。
第2章 预处理
2.1 预处理的概念与作用
预处理指的是在程序编译之前,根据以字符#开头的命令,修改原始的c程序。
预处理首先将源文件中以"include"格式包含的文件复制到编译的源文件中。如c程序中存在#include<stdio.h>则将stdio.h文件的全部内容复制到c程序中。并将实际值替换用"#define"定义的字符串,如#define Constant 1 则将c程序中所有Constant替换为1。并根据"#if"后面的条件决定需要编译的代码。除此之外,c程序预处理还会删除空白字符和所有注释,处理结束后会得到.i的文件。
2.2在Ubuntu下预处理的命令
使用gcc -E hello.c命令进行预处理
2.3 Hello的预处理结果解析
hello.c头文件的信息】、
hello的定义的一些别名
hello.c真正内容
2.4 本章小结
本章主要提到了预处理的概念,实现预处理的命令,以及进行预处理后hello.i中主要内容。
预处理看似简单,实则有着许多的内容,预处理要将人们编写的c程序补充完整,并将一些与程序无关的内容删掉,保证c程序在后续的处理中正确无误的运行,是程序运行的第一步。
第3章 编译
3.1 编译的概念与作用
编译是编译器将预处理后(.i)的文件变为汇编语言的过程。
编译将高级语言(本文为c语言)转化为更加底层的汇编语言,是高级语言到机器语言之间的媒介,同时汇编可以对程序代码进行优化,保证程序能高效快速的运行。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
3.3.1数据
1.常量:字符串:
c程序中的字符串被存储在下面
printf("用法: Hello 学号 姓名 秒数!\n");
printf("Hello %s %s\n",argv[1],argv[2]);
2.全局变量:
c中只有一个全局函数,在汇编中呈现如下:
.globl main
3.局部变量
局部变量i存储在栈中
argv被存在%edi中后又被压入栈中,后与4进行比较
movl %edi, -20(%rbp)
cmpl $4, -20(%rbp)
3.3.2赋值
c中存在的赋值操作为在循环中将i赋值为1,其存在栈中,汇编代码如下:
movl $0, -4(%rbp)
3.3.3算术运算
加法:c程序中i++为加法,通过add实现,汇编代码如下:
addl $1, -4(%rbp)
3.3.4关系操作
1.不等于!=:
汇编中的!=通过cmp以及je实现c程序中代码为:
if(argc!=4)
汇编实现的代码为:
cmpl $4, -20(%rbp)
je .L2
2.小于:
汇编中的小于通过cmp和jle(小于或等于)实现的
c程序代码:
for(i=0;i<8;i++)
汇编代码:
cmpl $7, -4(%rbp)
jle .L4
3.3.5数组/结构/指针操作
argv数组:argv[1],argv[2]存储的为两个字符串,存储在栈中,又存在%rsi中。实现汇编代码:
movq -32(%rbp), %rax
addq $16, %rax
movq (%rax), %rdx
movq -32(%rbp), %rax
addq $8, %rax
movq (%rax), %rax
movq %rax, %rsi
3.3.6控制转移
1.if操作:通过cmp以及je实现。
汇编代码:如果-20(%rbp)中的值等于4则执行.L2(主程序)中的内容,不等则继续执行(if中的程序)
cmpl $4, -20(%rbp)
je .L2
2.for操作:通过赋值累加比较实现
汇编代码:
movl $0, -4(%rbp) //赋值i=0
addl $1, -4(%rbp) //累加i++
cmpl $7, -4(%rbp)
jle .L4 //比较i<8
3.3.7函数操作
1.main函数:
参数传递:argc,argv[],分别存在%edi和%rsi中。
movl %edi, -20(%rbp)
movq %rsi, -32(%rbp)
函数调用:程序在初始化后被调用。
函数返回:%eax的值存为0后返回。
2.printf函数:
参数传递:leaq .LC0(%rip), %rdi将%rsp的值加上.LC0的值相加送入%rdi寄存器传入参数。
函数调用:再main函数中通过call调用call puts@PLT
参数传递:movq %rax, %rsi
leaq .LC1(%rip), %rdi
movl $0, %eax
函数调用:call printf@PLT
3.exit函数:
参数传递:movl $1, %edi 传入参数1存在%edi中
函数调用:call exit@PLT
4.sleep函数:
参数传递:movl %eax, %edi将上一步操作的返回值作为参数
函数调用:call sleep@PLT
5.getchar函数
参数传递:无
函数调用:call getchar@PLT
3.4 本章小结
在本章中主要涉及了编译的概念及其作用,及编译实现的命令,以及对hello编译结果进行较为细致的分析。
编译是程序实现的重要一步,同时由于编译形成的汇编语言直接影响了后面机器语言的形成,所以了解汇编的知识对程序的优化,程序的设计,一些特殊bug的修改都有着重要的意义,同时我们应该了解不同的编译器可能对相同的c代码有着不同的编译结果。这就导致了可能会出现这样一个现象,相同程序在一个编译器上成功编译,在另一个编译器可能会出现报错的现象,因此了解编译器的工作原理是很有必要的。
通过编译实现了从hello.i(预处理程序)到hello.s(汇编语言)的过程。
第4章 汇编
4.1 汇编的概念与作用
汇编是将汇编语言通过汇编器翻译为机器语言,由编译文件(.s)变为可重定位文件(.o)文件的过程。
汇编是机器语言形成的步骤之一,是初步的机器语言,翻译成机器语言后,在经链接步骤便可形成完整的机器语言。
4.2 在Ubuntu下汇编的命令
gcc -c hello.s -o hello.o
4.3 可重定位目标elf格式
命令:readelf -a hello.o
ELF头:
ELF头中包含一些系统信息,数据的存储方式,版本等等,同时包含了节的数量和大小。
节头:包含了节的大小偏移量等信息。
重定位节:包含了重定位节的偏移量,定位类型等信息。
符号表:包含了一些符号的类型等信息
4.4 Hello.o的结果解析
命令:objdump -d -r hello.o > text
机器语言(反汇编)的构成:地址 操作数 汇编代码
- 操作数
汇编语言中的操作数为10进制,而反汇编中的操作数为16进制。对于相同的减法操作内容:
汇编:subq $32, %rsp
机器语言(反汇编):sub $0x20,%rsp
- 分支转移
对于分支的转移操作,汇编代码为转移到段名称(如.L2),而反汇编为转移到一个特定的地址(如2f)并包含跳转位置与主函数起始位置的距离。
cmpl $4, -20(%rbp)
je .L2
13: 83 7d ec 04 cmpl $0x4,-0x14(%rbp)
17: 74 16 je 2f <main+0x2f>
- 函数调用
函数调用与分支转移类似,汇编代码利用call函数转移到函数名称,而反汇编转移到特定地址,并表明了特定地址与主函数起始位置的距离。
对于getchar函数的调用:
汇编:
call getchar@PLT
反汇编:
86: e8 00 00 00 00 callq 8b <main+0x8b>
- 代码位置
反汇编中包含了代码的位置,而汇编中不存在
如 86(调用getchar函数的位置)
5.机器语言
反汇编代码中存在该操作的机器语言,16进制表示,而汇编代码中不存在。
如:e8 00 00 00 00(调用getchar函数的机器语言)
4.5 本章小结
本章主要讲述汇编的概念及作用,可重定位目标的elf格式,以及对机器语言进行反汇编的分析,
汇编已经开始初步形成机器语言,除了一些未定位的地址信息外,其他机器语言已全部形成,包含了机器语言的大部分内容,在机器语言的反汇编中,我们可以看到函数调用时具体代码位置的变化,可以查看汇编指令的每一条机器语言,有助于使我们对程序有着更好的了解。
汇编是将汇编文件(.s)转变为可重定位文件(.o)。
第5章 链接
5.1 链接的概念与作用
链接是将各种不同文件的代码和数据部分收集并组合成一个单一文件的过程。
链接将所有可重定位文件(hello.o)进行链接定位形成可执行文件(hello),链接将各个可重定位文件的代码段,数据段合并为一个统一的整体。链接这一过程使得不同的文件可以分开编译,使得人们可以将一个任务分割开来,分别完成,节省了大量的工作时间。
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 > elf
- ELF头:
ELF头中包含一些系统信息,数据的存储方式,版本等等,同时包含了节的数量和大小。同时包含程序的节的数量及大小
- 节头:
节头中包含节的名称,大小,起始地址以及偏移量。
- 程序头:
程序头中同样包含了程序的类型,大小,分批的虚拟空间地址,实际地址及对齐所用的偏移量。
在程序头中我们可以看到程序的起始地址为0x40040。
- 符号表
符号表中包含符号的名称类型等信息。
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
5.4 hello的虚拟地址空间
elb中详细标注了程序的虚拟空间的起始与结束地址,并表明了操作类型。
而程序头中没有操作类型,但是有着程序的实际地址。程序头中标明的虚拟地址与实际地址都为0x40040,而elb中标明的起始地址为0x40000,是由于存在偏移量所以两者有着差距。
5.5 链接的重定位过程分析
命令:objdump -d -r hello > hello.text
- 重定位过程
在可重定位文件中包含着函数或者数据的重定位类型,同时未定位的地址位置用0替代,程序会根据重定位类型,函数起始地址,偏移量,在链接之时将定位的地址计算出来。例如调用puts,可重定位文件中地址位置为00000000,重定位类型为PLT(延迟重定位),可执行文件中地址为46 ff ff ff(小端序,实际地址为0xffffff46),未标注重定位类型,直接标注了调用的函数的地址。
可重定位文件:
可执行文件:
- 地址:
可执行文件中包含各代码具体的地址,而可重定位文件中显示的为代码的偏移量。如调用puts,可执行文件前为具体地址0x401145,可重定位文件为偏移量0x20。
- 内容
可重定位文件中包含程序用到的全部函数,包括hello.c中未定义的,而重定位文件中只有关于main函数的内容
例可执行文件包含puts函数以及printf函数的代码,而可重定位文件中没有涉及。
5.6 hello的执行流程
程序的起始点为0x7f7330073100, 这里是hello使用的动态链接库ld-2.31.so的入口点_dl_start:
然后,程序进入_start,地址为0x4010f0.
然后程序通过一个间接call指令跳到动态链接库ld-2.31.so的__libc_start_main处,这个函数会进行一些初始化,并负责调用main函数。程序地址为0x00007f732fe8bfc0。
紧接着通过call指令跳转到libc-2.31.so地址为0x7f732fe8c00f。
然后返回到__libc_start_main继续,然后调用hello可执行文件中的__libc_csu_init函数,这函数是由静态库引入的,也是做一些初始化的工作,
然后返回_start,
然后进入main函数,地址为0x401125。
在经历exit函数过后,程序退出,exit地址为0x4010d0。
5.7 Hello的动态链接分析
通过hello的ELF文件,可以看到GOT和PLT节的起始地址:
在dl_init之前地址字节都为0。
在dl_init之后显示的为正确的地址。
5.8 本章小结
本章主要提及了链接的概念与作用,在Ubuntu下链接的命令,hello的重定位,执行流程,动态链接过程。
链接是程序运行前的最后一步,运行时的关键一步,静态链接与动态链接有着不同方法,不同的优点,静态链接在运行前链接完毕,动态链接在运行时参与链接,二者共同的作用才使程序能够正确,快速的运行下去。
第6章 hello进程管理
6.1 进程的概念与作用
进程是运行在执行中的一个实例。进程中通常包括指令代码,程序运行时的堆栈,数据,所使用的寄存器等。
进程为程序提供了应用程序的关键抽象,逻辑控制流以及私有地址空间,通过这两个抽象,使得程序有着独占内存独占cpu的假象,使得程序更好管理,能够更加准确的运作
6.2 简述壳Shell-bash的作用与处理流程
shell可以接受用户的指令,送入内核,调用程序进行处理。
1.Shell首先从命令行中找出特殊字符(元字符),在将元字符翻译成间隔符号。元字符将命令行划分成小块tokens。Shell中的元字符如下:SPACE , TAB , NEWLINE , & , ; , ( , ) ,< , > , |
2.程序块tokens被处理,检查看他们是否是shell中所引用到的关键字。
3.当程序块tokens被确定以后,shell根据aliases文件中的列表来检查命令的第一个单词。如果这个单词出现在aliases表中,执行替换操作并且处理过程回到第一步重新分割程序块tokens。
4.Shell对~符号进行替换。
5.Shell对所有前面带有$符号的变量进行替换。
6.Shell将命令行中的内嵌命令表达式替换成命令;他们一般都采用$(command)标记法。
7.Shell计算采用$(expression)标记的算术表达式。
8.Shell将命令字符串重新划分为新的块tokens。这次划分的依据是栏位分割符号,称为IFS。缺省的IFS变量包含有:SPACE , TAB 和换行符号。
9.Shell执行通配符* ? [ ]的替换。
10.shell把所有从处理的结果中用到的注释删除,並且按照下面的顺序实行命令的检查:A. 内建的命令B. shell函数(由用户自己定义的)C. 可执行的脚本文件(需要寻找文件和PATH路径)
11.在执行前的最后一步是初始化所有的输入输出重定向。
12.最后,执行命令。
6.3 Hello的fork进程创建过程
shell为hello进行加载,调用fork函数创建子程序,fork在子程序中返回0,在父程序中返回非零值,fork创建的子程序几乎与父程序几乎完全相同,有着相同数据段,栈,堆代码,节表等。但是父程序与子程序有着不同的pid,父程序与子程序并发运行,没有固定的先后顺序。父进程会将新创建的子进程放在一个新的进程组中,这个进程组对应./hello这个作业,shell可以通过向进程组中的所有进程发信号的方式管理作业。
6.4 Hello的execve过程
在fork子程序调用结束后,子程序会调用execve函数加载hello,eaecve会根据虚拟内存映射规则,会将hello的代码段,数据段映射到对应的代码段,数据段加载hello程序新的用户区域。在加载完成后,execve会运行hello,进入主函数并给予main函数所附带的参数。
6.5 Hello的进程执行
单处理器在看似在并发地执行,是因为多个进程进程交错执行并且地址空间由虚拟内存系统管理同时未执行进程的寄存器值保存在内存中。进程A,B,C,在运行时A进行先执行一段时间,然后转换到B进程执行与此同时保存A暂停时的参数,以便能够再次执行。在运行C,A,C…最后完成所有进程。
在进程切换的过程中,会进行2次用户态与核心态之间的转换,在进程A切换到B时会先从用户态转变为核心态,再由核心态转变为用户态,由B-A的过程中同样会经历两次转变。
6.6 hello的异常与信号处理
1.按普通字符
在程序执行运行过程中按普通字符(包括回车),会引起键盘的中断信号异常程序会执行中断程序(最终字符会被getchar()吸收),在中断程序执行完成后会返回下一条指令。
在程序运行期间,会出现输入Ctrl Z会引起进程暂停,进程挂起可以恢复。
3.Ctrl-C
在程序运行期间,输入Ctrl-C,会出现中断现象,程序立刻终止。
4.在Ctrl-Z输入ps:
在Ctrl-Z输入ps后,程序会显示进程类型。
5.在Ctrl-Z后输入jobs:
在Ctrl-Z后输入jobs会显示暂停的进程。
6. 在Ctrl-Z后输入pstree:
在Ctrl-Z后输入pstree会显示进程树。
7. 在Ctrl-Z后输入fg:
在Ctrl-Z后输入fg 会使暂停的进程回复运行。
8. 在Ctrl-Z后输入kill:
在Ctrl-Z后输入kill会使程序终止。
6.7本章小结
本章主要涉及了进程的概念,shell运行过程,子进程的创建,进程加载,进程执行,异常与信号处理等过程。
进程是程序运行的重要步骤,进程涉及了程序如何在系统中运行,何时在内核与用户之间切换。进程同时涉及了异常处理过程,使得进程在运行过程中,面对不管是来自系统还是I/O设备的异常信号都能从容的面对,保证了程序能够正常的运行。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:逻辑地址是由段加上偏移量,代表了代码在居段首的位置,hello.o中的地址为逻辑地址。
线性地址:程序所构建的,在重定位后,将所有代码顺序排列每行代码的地址都为第一行地址加上该行代码之前的代码所占空间之和。地址空间中的整数是连续的,所以称为线性地址。
虚拟地址:由系统所创建的,跟线性地址相同。N = 2n个虚拟地址的集合 {0, 1, 2, 3, …, N-1}。
物理地址:代码实际在内存中存储的位置。M = 2m个物理地址的集合 {0, 1, 2, 3, …, M-1}。
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址的形式为段加偏移地址:首先系统会通过段选择子确定为GDT还是LDT;寻找对应类型的段描述符,得到段描述符得到段的基址,将基址与段内偏移地址相加得到的64位整数就是线性地址。其中LDT/GDT是由内核管理的。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址到物理地址的变换是通过页表实现的,页表是一个页表条目 (PTE)的数组,将虚拟页地址映射到物理页地址。
每个线性地址对应这一个页表条目,在该页表中存在标志位以及物理地址,若标志位1,则物理地址有效,页表中存的物理地址即为虚拟地址所对应的物理地址。若标志位为0,则需要去磁盘中寻找对应的地址。
7.4 TLB与四级页表支持下的VA到PA的变换
在计算机中为了方便存储,节约存储空间,会采用多级页表的方法,L1页表中每个有效的页表条目会对应一整页L2页表,L2页表中每个有效的页表条目会对应一整页L3页表,L4页表中每个有效的页表条目会对应一整页L5页表.虚拟地址在寻找页表时VPN(虚拟页号)会分为四部分,每一部分与每一级页表相对应,第一部分对应找到有效的地址后,第二部分则会在L2中寻找对应位置,以此类推,最后在L4中找到物理地址。
7.5 三级Cache支持下的物理内存访问
得到PA物理地址后,会根据Cache分为CT,CI,CO,利用标记位在一级Cache中寻找,若命中则直接采用,否则去二级Cache中寻找,去三级Cache中寻找。
7.6 hello进程fork时的内存映射
在创建fork后父进程与子进程的数据,堆栈,代码等完全相同,只有fork返回值与pid不同,对于应用的内存,父进程与子进程都会标记为私有,在要修改内存时,进程会将修改的内存复制,进程在复制的内存中修改。这样看上去内存在每个进程上看都是私有的。
7.7 hello进程execve时的内存映射
Execve在进行内存映射时会经历以下几个过程:
1.首先删除当前进程虚拟地址的用户部分中的已存在的区域结构。
2.为新程序的代码、数据栈等区域创建新的区域结构,所有这些新的区域都是私有的在写时复制的。代码和数据区域被映射为 hello 文件中的.text 和.data 区,bss 区域是请求二进制零的,映射到匿名文件,其大小包含在 hello 中,栈和堆地址也是请求二进制零的,初始长度为零。
3.hello 程序与共享对象 libc.so 链接,libc.so 是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
4.设置程序计数器(PC):
execve 做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
首先在接收到缺页信号时,会先检查地址是否合法(通过搜索区域链表确认地址是否在合法的地址),其次检查访问是否合法,是否有读写权限。若出现不合法的现象,则执行终止程序,若合法则执行缺页程序,将页面从磁盘拷贝至内存返回重新执行指令。
本章主要阐述了hello储存管理的内容,虚拟地址,物理地址之间的映射关系,四级页表的访问,cache的访问,fork,execve内存映射,缺页故障处理。
计算机所设计的多种内存是为了用户能更好的使用计算机,进程能够快速运行,计算机能够更好管理…本章通过简单的梳理,可以使我们更好的了解计算机的内存的原理。
结论
- 预处理:系统将hello.c的c语言代码所调用的所有库导入到内存中,并修改了一些必要的内容和删除了一些与程序运行无关的内容,转变文件为hello.i。
- 编译:通过编译器将c代码翻译为汇编语言,hello.i->hello.s。
- 汇编:将汇编语言转变为机器语言(可重定位文件),hello.s->hello.o。
- 链接:将所有可重定位文件和动态链接库进行综合处理,hello.o->hello。
- 运行:shell通过fork创建子程序,通过exceve加载,并对异常信号进行处理。
- 内存映射:进程运行时使用虚拟内存,通过页表将虚拟内存映射为物理内存,通过cache访问内存中的内容,并对fork,exceve进行内存映射。
- 结束:shell正常退出,系统回收进程,删除有关的内容。
总结:
系统通过复杂的过程实现hello一个小小的程序,说明计算机存在着非常复杂的机制,看似简单实则非常复杂,涉及了计算机非常的知识。计算级系统这门课让我们了解掌握程序背后机制,对我们理解计算机有着非常大的帮助
附件
文件名 | 来源/内容 |
hello.c | 原始的c语言代码 |
hello.i | 预处理后的文件 |
hello.s | 编译之后的汇编文件 |
hello.o | 汇编后的可重定位文件 |
hello | 可执行文件 |
hello.text | 反汇编后的文件 |
hello.elf | 可执行文件的头文件 |
参考文献
- Computer Systems:A Programmer’s Perspective
- https://blog.csdn.net/qq_53314152/article/details/124620978
- https://blog.csdn.net/tangodope/article/details/124869956