计算机系统
计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术
学 号 2021113000
班 级 2103101
学 生 吕彦锐
指 导 教 师 刘宏伟
计算机科学与技术学院
2022年5月
Hello是大多数程序员学习的第一个程序,这个报告从预处理开始一步步研究这个小程序被处理的过程,结合CSAPP课程所学,覆盖到信号处理章节(之后的没有学)。
关键词:进程;反汇编;
目 录
2.2在Ubuntu下预处理的命令.......................................................................... - 5 -
3.2 在Ubuntu下编译的命令............................................................................. - 6 -
4.2 在Ubuntu下汇编的命令............................................................................. - 7 -
5.2 在Ubuntu下链接的命令............................................................................. - 8 -
5.3 可执行目标文件hello的格式.................................................................... - 8 -
6.2 简述壳Shell-bash的作用与处理流程..................................................... - 10 -
6.3 Hello的fork进程创建过程..................................................................... - 10 -
6.6 hello的异常与信号处理............................................................................ - 10 -
7.1 hello的存储器地址空间............................................................................ - 11 -
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.1 Linux的IO设备管理方法.......................................................................... - 13 -
8.2 简述Unix IO接口及其函数....................................................................... - 13 -
第1章 概述
1.1 Hello简介
P2P的意思是from program to process,由程序到进程。Hello文件起初只是一个.c程序,经过预处理、编译、汇编、链接生成hello可执行文件,之后在shell中输入./hello,shell调用加载器创建这个进程,先fork一个子进程,再在这个子进程执行execve,使子进程执行hello程序,就这样,hello进程创建完毕,实现了由程序到进程的转变。
020的意思是from zero to zero。从一开始什么都没有,到.c程序被写出来、被编译成可执行文件、被创建进程,直到最后进程结束,生成SIGCHLD信号,被父进程bash回收,又再次回归0。
1.2 环境与工具
1.2.1 硬件环境
操作系统:Windows11家庭中文版
处理器:AMD Ryzen 5 4600U with Radeon Graphics 2.10 GHz
1.2.2 软件环境
VMware workstation;
Visual studio
1.2.3 开发工具
gcc
1.3 中间结果
(图1.1)全部创建出的文件
预处理阶段创建了hello.i文件
编译阶段创建了hello.s文件
汇编阶段创建了hello.o文件
链接阶段创建了hello可执行文件
反汇编hello.o文件,将反汇编的结果重定位创建了helloobjdump.txt
利用readelf读取hello.o文件的ELF信息,重定位输出生成helloelf.txt文件
反汇编hello可执行文件,将反汇编结果重定位创建了excutabledump.txt文件
利用readelf读取hello可执行文件的ELF信息,重定位输出到excutable.txt文件
1.4 本章小结
Hello从编写到编译到执行到回收,走完了全部的一生。
第2章 预处理
2.1 预处理的概念与作用
概念:预处理是指在进行编译的第一遍扫描(词法扫描和语法分析) 之前所作的工作。
作用:预处理是在编译之前进行的操作,预处理会删除所有的注释,并且处理#开头的内容,比如说对库的引用操作,以及宏定义#define。对库的引用操作会直接把库的内容复制过来。
2.2在Ubuntu下预处理的命令
gcc -E hello.c -o hello.i
(图2.1)预处理阶段执行的指令
2.3 Hello的预处理结果解析
(图2.2)预处理生成的.i文件的部分信息
(图2.3)预处理生成的.i文件中的Main函数部分
预处理之后,程序开头的注释被全部删除,除此之外,生成的.i文件相较于原来的.c文件多出了许多的内容,这些内容都是对#include的头文件的复制。
2.4 本章小结
预处理阶段执行的内容相对简单,程序将注释删去,并且处理所有的以#开头的语句,将所有程序引用的.h文件复制到include的位置。
第3章 编译
3.1 编译的概念与作用
概念:编译是指将代码转变为汇编指令或机器指令的过程。
作用:编译将高级语言翻译为汇编语言程序,这使得高级语言文件具有可跨平台的属性,不同的系统可以部署不同的编译器,将同样的高级语言文件编译成不同的适应相应系统的汇编语言文件。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
(图3.1)编译过程指令
3.3 Hello的编译结果解析
(3.2).s文件部分内容
(3.3.1)函数操作
首先是main函数,main函数有参数argc以及argv[],前者是int类型的数据,存储在rdi寄存器中,后者是char*类型数组,存储在rsi寄存器中。可以看见,在main函数其实,创建了main的栈帧并且修改了rbp以及rsp的值。之后就将rdi压栈到rbp-20的地址去,并且将rsi压栈到rbp-32的地址,这里就是对函数参数的保存操作。
第二处函数操作就是对printf函数的调用了。
(3.3).c文件中有关printf函数的输出模式串
(3.4)printf段在.s文件中被翻译成的汇编指令
(3.5)printf输出模式串在.s文件中的体现
调用printf函数时,首先把.LC0的地址传递给rax寄存器,之后将rax寄存器的内容复制给rdi寄存器,之后call调用puts@PLT,这里就是汇编代码中对printf函数的调用了。可以看到图有给出.LC0段的内容,这里就是printf要输出的语句格式串,可以看到hello以及:因为是ASCII字符的原因,所以可以直接显示出来,而中文则必须转为编码。
(3.6)exit函数在.s汇编文件中的体现
紧接着,在输出之后,还执行了exit(1)操作,这一部分的汇编代码就比较容易了,直接将立即数1移到edi寄存器中作为调用函数的参数,然后call调用exit@PLT就好。
(3.7)hello.c中循环输出源代码
(3.8)for循环在.s文件中的体现
第三处函数操作对应于for循环内部的printf函数以及sleep函数。在.s文件中对应于.L4部分。这里首先将rbp-32部分的内容取出存放到rax寄存器(这里的栈存放着argv指令),并且给rax加上了16,由于argv是一个char*类型的字符型指针数组,所以说数组内部每一个元素都占用8个字节,这里给rax加上16就指向了argv[2]。之后把这个字符串指针取址赋值给rdx寄存器,之后依靠同样的方法把argv[1]赋值给rsi寄存器。之后又是将.LC1的地址传递给rax寄存器rdi。之后将rax赋值为0,call函数printf。这里可以看见,调用printf中参数的设置是321,首先把arv[2]也就是名字参数传递给printf函数的第三个参数,将argv[1]也就是学号传递给第二个参数,最后将printf的模式串传递给printf的第一个字符串。
之后对于sleep函数的调用就相对来说简单多了,将argv[3]传递给rdi寄存器并call调用atoi@pLT函数,并将函数的返回值作为sleep函数的参数调用sleep函数,在完成这一切后还会给rbp-4地址的值加一(循环计数)。
(3.3.2)关系操作
(3.9)if循环在原本.c文件中的内容
源文件中比较输入的参数是不是有四个,在汇编语言中体现为cmpl操作,将立即数4与argc比较,如果相等就跳转到L2.代码段,如果不等就继续下面的操作。这里值得注意的是,jmp不是同.c文件一样判断不等于,而是等效的转换为je判断,如果相等就跳转到L2,而非不等时跳转。
(3.10)if条件判断在汇编文件中的体现
(3.11)for循环在.c文件中的内容
(3.12)for循环在汇编文件中的循环条件判断片段
第二处关系操作在于for循环的输出,这里将用户输入的第一个以及第二个命令行参数循环打印9次。跟踪这一部分的操作,可以看见程序将立即数8与rbp-4地址处的值比较(初值为0),如果小于等于就跳转到.L4部分执行for语句内部的内容。可以看见,从0到8正好循环进行9次,满足原本.s文件的循环要求,但是又不是完全的相同,常数相关的循环次数,编译器明确知道循环要进行的次数,因此可以自己指定循环的递加方式,只要不改变语义都可以。
(3.3.3)控制转移
首先是if条件判断,以argc不等于4为判断条件,在.s文件中翻译为如下语句:
(3.13)if条件判断在汇编文件中的全内容
通过条件判断,依靠je语句跳转到L2语句段,完成控制的转移,即改变程序计数器的值。此处L2语句段对应的是源程序中if没有执行时的情况。
(3.3.4)数组、指针操作
这个.s文件中对指针的调用都是先将地址存放到rax寄存器中,再调用这个地址的,具体方式如下:
(3.14).s文件中的取址操作
而对于数组的操作(argv数组),就是首先把数组的第一个元素的地址取出,然后直接对这个地址加上一个数字,用来搜索数组的不同位置的元素。本程序中argv是字符指针数组,是8字节的元素,因此搜索索引时都是以8为倍数加减的。
(3.3.5)数据
程序中运用到了许多数据,首先是字符串,这里主要指printf调用时使用的模式串,这些字符串主要被存放在.rodata段中,在main函数书写之前就出现在.s文件中,对这些字符串的调用也是直接将字符串对应段落的地址传递到寄存器中使用。
循环计数器i,程序中的循环计数器并没有被编译器设置为一个指针,而是在栈中(rbp - 4)开辟了一个4字节空间来存放这个参数,并且在每次循环结束时增加这个参数的值。
命令行输入argc与argv。argc是一个int类型的参数,在.s文件中体现出,main函数与一般的函数相同,也是将这两个作为普通参数存放在rdi与rsi中。由于这两个参数要经常使用,所以一开始就把这两个参数存放到栈中。
3.4 本章小结
.s文件中中有很多内容令人困惑,在上网查阅后,我总结所获大致如下:
(1.)汇编代码中有很多伪指令(汇编器指令),这时汇编语言中使用的操作符和助记符,还包括一些宏指令。这些指令告诉汇编程序如何进行汇编,既不控制机器的操作也不被汇编成机器代码,只能为汇编程序所识别并指导汇编如何进行。[2]
汇编指令以’.’开口,以’;’或者换行来结尾。常用的伪指令有:
- .global symbol 声明symbol为全局变量。
- .lobal symbol 声明symbol为局部变量。作用范围仅在当前文件内。
- .extern symbol 声明一个外部符号。
- .set symbol ,expression常量设置
“.set mark,0x3”设定常数,类似c语言中的宏定义。[2]
以上对符号的声明可以在.s文件中看到。
(3.15)在.s文件中出现的伪代码
main函数被声明为全局变量,函数只要没有被定义为static都是全局的。
- .byte 定义一个字节(8位)的地址空间。类似的指令还有.int、.long 、.word。空间大小依赖具体系统。
- .string “STR”也是用于字符串定义。但是有尾端区分。
这些指令也在.s文件中有也体现。同样是上一张图片,这里的.string就是声明了printf的输出格式串。比较令人疑惑的是,同样是.string字符串,前一个提示字符串(.LC0段)就没有像此处这样有众多标识。
- .text 告诉汇编器,把此后产生的代码放到目标文件中的“.text”段。
- .section name 告诉汇编器,把此后产生的代码放到目标文件中的name段。
- .section .text 表明接下来的这段代码放到目标文件的.text段(代码段)。
- .section .data 表明接下来的这段代码放到目标文件的.data段(数据段)。
- .ent 标识函数的起始点。
- .end 标识函数的结尾,和.ent一样仅仅用于调试。
- .size 表示在函数表中,function和所用指令的字节数一同列出。
- .align n对指令或者数据的存放地址指定对齐方式。n取值因系统而异。
- .type symbol @function 标识symbol 为函数名称。
这里有关段的各种设定与之后生成可执行文件有着重要意义,可以发现其中的内容与所学ELF文件的格式是相符合的。
(3.16)在.s文件中出现的伪代码
文件一开头就指定,之后的代码全部都是放在.text段,然后再用.section .rodata指定.LC0段应该存放在.rodata段,并且以8字节对齐。而.LC1则因为指定了.text所以直接存放在.text段中,并且这里声明了全局变量main,并且依靠.type指定这时一个函数。
第4章 汇编
4.1 汇编的概念与作用
汇编是将.s文件转变为.o文件的过程,即将汇编语言文件转变为机器语言的过程。
机器语言可以被计算机理解并且直接执行,不过.s文件尚未完成链接,没有解析符号以及重定位,因此不能被直接加载到内存执行。
4.2 在Ubuntu下汇编的命令
(4.1)汇编指令
4.3 可重定位目标elf格式
(4.2)使用readelf的代码
执行命令如上,将readelf获得的全部信息输出到helloelf文件中。
(4.3)readelf的结果:ELF头
首先是ELF头,根据CSAPP,ELF头应该有一个16字节的开头用来指示程序的字节信息(大端还是小段),可以看见这里Magic恰有16个字节。ELF头中标识了一些系统信息,例如是UNIX操作系统的虚拟机,给出了系统架构。并且给出处理的hello.o是一个可重定位文件,给出了入口点地址、程序头起点等各种信息,还指出了这个ELF头的大小是64个字节,没有程序头表,总共14个节头,节头表索引在第十三位。
(4.4)readelf结果:节信息表
恰如ELF头所述,共计14个节,节头部表的索引是13。
(4.5)readelf结果:.rela.text节内容
(4.6)反汇编文件中的重定位条目信息
对于重定位节.rela.text,这里存储着各种各样的重定位符号的信息,与反汇编文件结合可以看见这里的信息正是.text段中各种等待被重定位的内容,.rodata-4,.puts-4,.exit-4。出现的顺序正如反汇编程序中重定位条目的出现顺序。
(4.7)readelf结果:.symtab
.symtab节中给出了一些符号的信息,指明了它们是函数抑或是其他的什么内容,并且给出他们的作用域。
4.4 Hello.o的结果解析
(4.8)反汇编结果
将hello.o反汇编的结果重定位生成helloobjdump.txt文件,比较与hello.s的区别。①首先最直观的区别在于许多.s文件中的汇编器指令被删除了。
②.o文件为各个段分配了虚拟的地址,现在的main被设置为起始地址0,其他代码行也都有自己的相对于main函数开头的偏移量地址。
③文件被翻译为了二进制,原本的hello.o文件完全是二进制文件,不可以被文本编辑器直接打开,但是被objdump处理过之后我们可以看到反汇编得到的汇编语言代码。不过即使是汇编语言,数字也被翻译为了16进制而非原来的十进制,可以看出就是这一步常数变为二进制的。
④由于被分配了地址,所以.s文件中的.L2之类的标识不再存在,同样的有call函数调用的地址也不再是exit@PLT这样的标识了,取而代之的是实际的地址,如.L2那里的代码在反汇编文件中具有地址0x32。
⑤由于这里objdump是根据.o文件反汇编的,所以代码没有被分配到实际的运行时的虚拟内存,因此main函数的起始地址是0,除此之外,各种跳转还没有被重定位,因此je跳到的地址都是0,并且生成了一个标记条目用来辅助链接过程的进行。
⑥指令的翻译结果稍有不同,.s文件中指令都有操作的位数,例如subq到反汇编中就直接是sub
以上就是我所观察到的.s文件与反汇编文件的区别了。可以看到除了细节上有所不同之外,反汇编结果同.s文件在程序主题上近乎是相同的,我一条条指令对比下来,发现都是一一对应的。
机器语言是完全的二进制字符串,是处理器的指令集以及使用它们编写程序的规则,因此对常数的表示由反汇编的结果看都是16进制的串。
汇编语言是用助记符表示的指令以及使用他们编写程序的规则。因此在函数调用操作方面,汇编语言使用助记符来表示跳转的位置,而机器语言直接使用地址来表示。汇编语言的操作数仍是程序原本书写的格式,十进制就是十进制,十六进制就是十六进制,但是汇编语言的操作数全部都是01字符串。
4.5 本章小结
由于对文件的分析需要经常用到objdump以及readelf(这些都是GNU二进制工具),所以我大致了解了一下他们的信息。
以下内容是我关于部分参考文件的总结。[3]
(1.)objdump:这个工具用来显示二进制文件的信息,就是以一种可阅读的格式更多的了解二进制文件可能带有的附加信息。
常用参数有:-f显示文件头信息;
-D反汇编所有section(-d反汇编特定section)
-h显示目标文件各个section的头部摘要信息
-x显示所有可用的头部信息,等价于-a -f -h -r -t同时指定
-r显示文件的重定位入口,与-d或者-D同时使用,重定位部分以反汇编后的格式显示出来
-R显示文件的动态重定位入口,仅对动态目标文件有用,如共享库。
-S京肯反汇编处源代码,隐含了-d参数
-t显示文件的符号表入口。类似于nm -s提供的信息
(2.)readelf命令[4]
一般用于查看ELF格式的文件信息,常见的文件如linux下的可执行文件,动态库或者静态库等包含ELF格式的文件。
常用的命令有:
-h :--file-header 显示elf文件开始的文件头信息.
-l :--program-headers ;--segments 显示程序头(段头)信息(如果有的话)。
-S :--section-headers ;--sections 显示节头信息(如果有的话)。
-g :--section-groups 显示节组信息(如果有的话)。
-t :--section-details 显示节的详细信息(-S的)。
-s :--syms ;--symbols 显示符号表段中的项(如果有的话)。
-e :--headers 显示全部头信息,等价于: -h -l -S
-n :--notes 显示note段(内核注释)的信息。
-r :--relocs 显示可重定位段的信息。
-u :--unwind 显示unwind段信息。当前只支持IA64 ELF的unwind段信息。
-d :--dynamic 显示动态段的信息。
-V :--version-info 显示版本段的信息。
-A :--arch-specific 显示CPU构架信息。
-D :--use-dynamic 使用动态段中的符号表显示符号,而不是使用符号段。
-x <number or name> :--hex-dump=<number or name> 以16进制方式显示指定段内内容。number指定段表中段的索引,或字符串指定文件中的段名。
-I :--histogram 显示符号的时候,显示bucket list长度的柱状图。
-v :--version 显示readelf的版本信息。
-H :--help 显示readelf所支持的命令行选项。
-W :--wide 宽行输出。
第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.1)链接指令
(链接的命令查了快两个小时,没人介绍需要哪些库……)
5.3 可执行目标文件hello的格式
(5.2)readelf查看指令
使用readelf解析hello的ELF格式,将输出信息重定位到excutable.txt文件中。
(5.3)readelf结果:ELF头
hello的ELF格式与hello.o的ELF格式明显不同,首先对比二者的magic,是相同的,之后关于操作系统、版本之类的信息,都是相同的。再之后是程序入口点地址以及程序头起点,可以发现因为可执行程序经过链接之后已经被重定位了,函数被映射到了虚拟空间地址,所以入口点地址不再是0,并且有了程序头。
之后是节头起点,变为了13560字节,相较于hello.o而言增大了很多。在标志中,新增了程序头,并且节的数量增加了。
(5.4)readelf结果:节信息
(5.5)readelf结果:节信息
对比原来的节信息,多出来.init段,.rela.text段消失,这与课本的描述是吻合的,重定位完成之后不再需要.rela.text指示重定位信息。另外,可执行文件的ELF头中没有.bss段。
ELF中值得注意的几个段就是.text,ELF中指出.text段的地址在4010f4,大小为101f字节,以16字节为对齐标准。.rodata段在402000地址,大小2000字节,以8字节为对齐标准。.data在404048地址,大小为3048字节,以一个字节为对齐标准。
5.4 hello的虚拟地址空间
(5.6)edb调试界面
进入程序,执行完init加载之后,程序就进入了main函数,可以看出地址为4010f0,正是ELF中指出的.text的地址。
(5.7)虚拟内存信息
可以发现.text段在401ff0结束,紧接着应该就是402000的.rodata段,而.text段也恰如ELF中指出的那样占用1ff0字节的单位。
(5.8).rodata段信息
在402000存放的就是.rodata的内容了,可以看见里面有Hello的printf字符串信息,还有Hello %s %s,都在这个段中。
5.5 链接的重定位过程分析
(5.9)反汇编过程指令
(5.10)反汇编结果.plt.sec段
反汇编hello可执行程序,首先注意到的就是这次反汇编的产物多出了许多函数。在.plt.sec段中,将许多调用到的函数都存放了进来,例如printf,例如puts,例如getchar,都可以在这里找到。
(5.11)反汇编结果:main函数部分
再看main函数,首先关注的就是main函数的起始地址是401125而不再是0,这里说明main函数被重定位了,找到了它在虚拟内存中的地址。之后就是关注之前.o文件中的重定位条目被怎样处理了,可以看见40113e地址的lea此时已经有了明确的地址,不过取到的地址在.rodata段中。再看call函数调用,位于401148地址处对于puts函数的调用,这时已经将地址指向了401090,正好是上一张图中给出的puts的位置。而且看到这条call指令编码为e8 43 ff ff ff,很明显是PC寻址方式,而在.o文件的反汇编中它的重定位条目也确实是记录为PC寻址方式。
5.6 hello的执行流程
(5.12)edb调试界面
使用edb,一开始是这个样子的,这时地址空间非常奇怪,与平时看见的400000开头的地址完全不同。在执行了一系列操作之后,jmp到了r12,也就是_start函数的地址,这也是第一个可以辨识的函数。
(5.13)edb调试界面
执行了一系列操作之后跳转到4010f0即_start的地址,在这里执行了一系列操作,改变了寄存器的值。并且最后依靠call调用了Main函数,地址为401125。
(5.14)edb调试界面
(5.15)edb调试界面(寄存器内容)
(5.16)edb调试界面(栈信息)
等真正进入到main函数,通过观察寄存器以及stack,可以发现这时rdi和rsi存放着的正是argc的数量4以及各种字符串的首地址。
(5.17)edb调试界面(虚拟内存信息)
程序一路运行,因为输入的参数数量正好是3个,所以可以正常运行循环的过程,因此程序第一个调用的就是Printf函数,地址位于4010a0。
(5.18)edb调试界面(程序输出结果)
程序循环运行,不断输出信息。
(5.19)edb调试界面
程序执行完循环体之后,紧接着就调用4010b0地址的getchar函数
(5.20)edb调试界面
输入一个数字,结果程序到这里应该就运行完毕了,但是实际上还有一步ret没有执行,而且观察地址可以发现,这时地址又变回7f开头的地址了。
5.7 Hello的动态链接分析
(这个部分实在是没有弄清楚是什么目的,将动态链接的时候没有将的这么细致,又少学了两个单元的内容,不知道这里的要求来自那一部分)
5.8 本章小结
链接过程处理了.o文件中的重定位条目,将每一个符号与一个具体的定义相对应了,之后又将整个程序映射到了虚拟内存中,给各个内容分配了其具体的运行空间。
第6章 hello进程管理
6.1 进程的概念与作用
进程是正在运行的程序和实例,进程提供了两个虚拟,一个是独立的逻辑控制流,还有一个就是私有的内存空间地址。正是因为有了进程的概念,我们每次加载程序,看到的虚拟空间地址都是相近的。
6.2 简述壳Shell-bash的作用与处理流程
Bash是shell的一个早期版本,其作用是读取用户输入的每一行指令,并且依照指令调用不同的程序。
Bash循环的读取指令,首先判断这条指令是不是内置指令,如果是的话就调用内置的函数处理这些指令,例如fg,bg,quit这一类指令。如果不是内置指令,就调用特定的程序来执行指令。另外,如果程序没有指定后台运行,就默认前台运行,这样的话bash要暂停前台读指令对进程的影响,直到进程被执行完毕。前台进程执行完毕之后,就开始继续等待下一条指令的输入。
6.3 Hello的fork进程创建过程
Bash进程依靠fork创建一个子进程,与bash在同一个进程组里,fork得到与bash相同的上下文,但是他们的pid不同,映射到的物理地址也不同。
6.4 Hello的execve过程
Bash在fork创建一个子进程成功之后,就在这个子进程执行execve,这个函数调用一次,不会返回,它会完全改变这个进程的上下文,代码段、数据段、堆栈等等全部被替换,执行一个全新的程序(hello),不过新的程序(hello)的pid与原进程一样。
6.5 Hello的进程执行
进程提供了两种抽象:独立的逻辑控制流、私有的地址空间,制造出hello进程独占CPU以及内存系统。在bash完成fork以及execve之后,就创建了一个子进程来执行hello程序。在完成fork的时候内核已经将控制转移到了新创建的hello进程中,不过当fork调用sleep函数时,内核又会调度。
调度是指内核抢占当前进程,并重新开始一个先前被抢占的进程。原本的bash进程可能再次被执行,也有可能其他进程被执行,这个时候就会发生上下文切换,控制流原本在hello进程中,但是要转移到别的进程中,首先内核保存hello进程的上下文,之后恢复先前被抢占的进程的保存过的上下文,最后将控制传递给新恢复的进程。如果遇到了异常,控制会传递给异常处理程序,这个时候内核会控制将控制由用户模式转变为内核模式。
6.6 hello的异常与信号处理
Hello在执行的过程四种异常都有可能出现,至于会产生的信号,由于hello中没有fork任何子进程,所以不会产生任何SIGCHLD信号,但是别的信号就有可能会产生了,由于源程序中没有使用signal函数人为的设定信号处理程序,所以系统会依照接受到信号时默认的行为处理这些信号。
(6.1)在实行程序时输入ctrl + c
在运行的时候输入ctrl+c,程序接收到SIGINT信号立刻就停止了。
(6.2)在执行程序时胡乱输入
在程序循环输出的过程中胡乱输入,结果发现对程序的执行没有任何影响,输入的所有内容都被缓存了起来,包括换行符,在程序结束之后一条一条的运行。
(6.3)在执行程序时开启另一个终端执行ps
尝试开两个终端,在正常源程序运行时查看那个程序的信息,结果发现连pid都查询不到。
(6.4)在执行程序时输入ctrl + z挂起进程
使用ctrl+z暂停了进程的执行,这个时候调用ps,发现可以看到进程hello被暂停了,不过在另一个终端依旧没有任何关于hello的信息。
(6.5)在挂起进程时执行pstree
运行pstree,在gnome-terminal中找到了hello。
(6.6)同时在两个终端执行pstree
联想到之前在运行过程中开两个终端,相互间没有影响,我这时重新打开了一个终端,同样运行pstree指令,发现了问题所在,虽然同样都叫bash,但是二者实际上时terminal的不同子进程。
(6.7)在两个终端执行ps
实际打印出来也可以看到两个bash的pid根本不同。
(6.8)在进程被挂起时执行jobs
输入jobs可以发现有一个已经被停止的job,就是hello进程。
(6.9)在进程被刮起时传递SIGCHLD信号
发送SIGCHLD信号给hello进程,没有任何效果,因为默认对SIGCHLD的应对就是ignore。
(6.10)在进程被挂起时传递SIGCONT信号
重新执行hello程序,输入ctrl+z挂起程序,使用ps获取到hello进程的pid之后传输SIGCONT信号给hello进程,结果它又能继续运行了。进程接收到SIGCONT之后默认行为继续运行,结束了挂断状态。
(6.11)在进程被挂起时传递SIGALRM信号
尝试将SIGALRM信号传递给hello进程,之后再让其继续运行,结果没有反应,输入ps,发现hello进程的确被设定了闹钟。
以上的各种尝试说明了hello程序再接收到不同信号时的默认行为,由于没有设定信号处理函数,所以一直都是以默认状态应对各种信号。
6.7本章小结
Hello程序中没有设定信号处理函数,因此对信号的回应都是默认的状态,所以SIGCHLD信号会被忽略,而想要尝试将不同的信号传递给hello进程,因为hello进程在前台运行,所以只能先用ctrl + z挂起进程,再利用kill传输不同信号。
开启多个终端从属于不同的bash,相互之间没有影响。
结论
Hello被程序员编译出.c文件,这是它的前身,它的生命由这里开始,被预处理后,它引用的文件被复制进来,这是它的第一次完善,之后它被编译器处理成汇编语言文件,这使得它能被作用于某一个特定的系统。而后,汇编器将他翻译为机器语言文件,但仅就此,它还不能被实际运行,直到链接器完成重定位工作,它才真正成为可以被加载到内存中执行的程序。
之后,在shell窗口中,他被bash父进程fork出子进程,并execve,在这里它完成了它的使命,并在这里结束生命,在它结束时传递SIGCHLD信号并被父进程回收,这就是hello的一生。
学习计算机系统,是我对计算机底层的第一次探索,在学习过程中最让我欣喜的莫过于第一次了解处理器的原理和设计、第一次了解内存的发展以及组织逻辑,对于硬件知识的向往是我选择计算机专业的原因,计算机系统课程第一次让我感受到收获。除此之外,汇编语言的学习也让我觉得收获良多,尤其是和链接结合,利用objdump尝试分析一个程序到底是如何被组织起来的。
附件
全部创建出的文件
预处理阶段创建了hello.i文件
编译阶段创建了hello.s文件
汇编阶段创建了hello.o文件
链接阶段创建了hello可执行文件
反汇编hello.o文件,将反汇编的结果重定位创建了helloobjdump.txt
利用readelf读取hello.o文件的ELF信息,重定位输出生成helloelf.txt文件
反汇编hello可执行文件,将反汇编结果重定位创建了excutabledump.txt文件
利用readelf读取hello可执行文件的ELF信息,重定位输出到excutable.txt文件
参考文献
[1] (24条消息) 汇编语言笔记-汇编文件(.s文件)介绍_凯之~的博客-CSDN博客_.s文件
(汇编语言笔记-汇编文件(.s文件)介绍_凯之~的博客-CSDN博客_.s文件)
[2] https://blog.csdn.net/weixin_38669561/article/details/105184760
[3] objdump 二进制文件分析 — Linux - 结巴练朗读_哔哩哔哩_bilibili
[4] (24条消息) readelf命令使用说明_木虫下的博客-CSDN博客_readelf
https://blog.csdn.net/yfldyxl/article/details/81566279