目 录
第1章 概述................................................................. - 4 -
1.1 Hello简介.......................................................... - 4 -
1.2 环境与工具......................................................... - 6 -
1.3 中间结果............................................................. - 6 -
1.4 本章小结............................................................. - 6 -
第2章 预处理............................................................. - 7 -
2.1 预处理的概念与作用......................................... - 7 -
2.2在Ubuntu下预处理的命令.............................. - 7 -
2.3 Hello的预处理结果解析.................................. - 7 -
2.4 本章小结............................................................. - 9 -
第3章 编译............................................................... - 10 -
3.1 编译的概念与作用........................................... - 10 -
3.2 在Ubuntu下编译的命令............................... - 10 -
3.3 Hello的编译结果解析.................................... - 10 -
3.4 本章小结........................................................... - 13 -
第4章 汇编................................................................ - 14-
4.1 汇编的概念与作用........................................... - 14 -
4.2 在Ubuntu下汇编的命令............................... - 14 -
4.3 可重定位目标elf格式................................... - 14 -
4.4 Hello.o的结果解析......................................... - 16 -
4.5 本章小结........................................................... - 19 -
第5章 链接............................................................... - 24 -
5.1 链接的概念与作用........................................... - 24 -
5.2 在Ubuntu下链接的命令............................... - 24 -
5.3 可执行目标文件hello的格式....................... - 24 -
5.4 hello的虚拟地址空间.................................... - 24 -
5.5 链接的重定位过程分析................................... - 26 -
5.6 hello的执行流程............................................ - 28 -
5.7 Hello的动态链接分析.................................... - 28 -
5.8 本章小结........................................................... - 29 -
第6章 hello进程管理........................................ - 30 -
6.1 进程的概念与作用............................................ - 31-
6.2 简述壳Shell-bash的作用与处理流程.......... - 31-
6.3 Hello的fork进程创建过程........................... - 31-
6.4 Hello的execve过程...................................... - 32-
6.5 Hello的进程执行............................................ - 32 -
6.6 hello的异常与信号处理................................ - 32 -
6.7本章小结........................................................... - 35 -
结论............................................................................. - 36 -
附件.............................................................................. - 37 -
参考文献..................................................................... - 38 -
第1章 概述
1.1 Hello简介
Hello从程序到进程(P2P)的经历:如下图1-1,从程序员将源代码写入文件hello.c并保存,到hello.c经过预处理生成hello.i,再经过编译生成hello.s,再经过汇编生成hello.o,与C语言提供的标准C库中的printf.o一起链接成hello可执行文件。
图1-1 编译系统
Hello的020经历:需在命令行下输入./hello才能运行hello可执行文件,shell程序将字符逐一读入寄存器,再把它放入内存中。当我们在键盘上敲入回车键时,结束读入, 如下图1-2。
图1-2 从键盘上读取hello命令
shell判断命令行的第一个单词不是一个内置的shell命令,会将其看作地址来加载并执行这个文件,shell将调用fork函数为这一程序创建进程,之后通过exceve在进程的上下文中加载并运行hello,将进程映射到虚拟内存空间。Shell执行一系列指令将hello目标文件中的代码和数据从磁盘复制到主存(利用DMA技术),如下图1-3。
图1-3 从磁盘加载可执行文件到主存
之后,处理器就开始执行hello程序的main程序中的机器语言指令,将要打印的字符串中的字节从主存复制到寄存器文件,再从寄存器文件复制到显示设备最终显示到屏幕上,如图1-4。
图1-4 将输出字符串你从存储写到显示器
Hello程序在屏幕上输出它的消息,然后终止进程。它的父进程将回收这一进程,内核清理相关资源占用,进程最终消亡。
1.2 环境与工具
1.2.1 硬件环境
X64 CPU;2GHz;2G RAM;256GHD Disk 以上
1.2.2 软件环境
Windows11 64位;VirtualBox 6.1;Ubuntu 18.04 LTS 64位
1.2.3 开发工具
Visual Studio 2022 64位;CodeBlocks 64位;gedit+gcc
1.3 中间结果
中间结果文件名字 | 作用 |
hello.i | 预处理生成的文件 |
hello.s | 编译生成的文件 |
hello.o | 汇编生成的文件 |
hello.out | 链接生成的文件 |
hello | 从hello.c一步到位 |
elf.txt | hello.o的elf格式 |
elf2.txt | hello.out的elf格式 |
obj.txt | hello.o的反汇编代码 |
obj2.txt | hello.out的反汇编代码 |
1.4 本章小结
本章简要介绍了Hello的P2P,020过程,介绍了实验环境与工具以及中间结果文件及其作用。
第2章 预处理
2.1 预处理的概念与作用
2.1.1概念
预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。
2.1.2作用
1.宏定义#define 标识符 字符串
使用宏名代替一个字符串,预处理器处理时,会将这些宏定义名字换为相应的字符串的内容,可以减少程序中重复书写某些字符串的工作量。
2. 文件包含#include "文件名" 或 #include <文件名>
读取系统头文件并将它直接插入程序文本中,可以节省程序设计人员的重复劳动。
3. 条件编译
#ifdef 标识符
程序段1
#else
程序段2
#endif
对一部分内容指定编译的条件,可以提高C源程序的通用性。
4.C源文件中注释内容被删去
2.2在Ubuntu下预处理的命令
cpp hello.c > hello.i或gcc -E hello.c -o hello.i
结果如下图2-1
图2-1 预处理结果
2.3 Hello的预处理结果解析
经过预处理后的文件内容明显增加,行数由原来的23行变为3105行,文件前几行展示了插入的库的信息,如下图2-2:
图2-2 hello.i头文件信息
图2-3 hello.i中main函数内容
2.4 本章小结
本章主要介绍了预处理的概念和作用,以及对hello.c进行预处理的具体指令和结果。
第3章 编译
3.1 编译的概念与作用
3.1.1概念
编译器(cc1)将文本文件翻译为汇编语言的文本文件,其中每条语句都以一种文本格式描述了一条低级机器语言指令。
3.1.2作用
将预处理后得到的预处理文件(如hello.i)进行词法分析、语法分析、语义分析、优化后,生成汇编代码文件。
经过编译后,得到的汇编代码文件(如,hello.S)还是一个可读的文本文件。将源程序转换为更易于机器理解的汇编语言代码,为后续汇编语言程序铺垫。
3.2 在Ubuntu下编译的命令
/usr/lib/gcc/x86_64-linux-gnu/7/cc1 hello.i -o hello.s或gcc -S hello.c -o hello.s
图3-1 编译指令和结果
3.3 Hello的编译结果解析
3.3.1数据
1.常量
字符串常量如printf格式串在文件中的展示如下图3-2:
图3-2 字符串展示
已知字符串格式如下:
用法: Hello 学号 姓名 秒数!\n
Hello %s %s\n
可以看到汉字被编码成了三个字节值,而英文字符仍保留其原本格式;
数字常量仍为十进制整型数;
2.变量
无全局变量和静态变量;
i,argc,argv为局部变量,参数传递时用寄存器实现,如下图3-3,argc存放在%edi中,argv数组首地址放在%rsi中,内容存放在栈中,使用时通过寄存器间接寻址实现;变量i作为循环计数器,先初始化为0,再进行累加,比较来控制循环结束。
图3-3 局部变量展示
3.3.2赋值
赋值语句通过mov指令实现,如对变量i的赋值:movl $0, -4(%rbp)
而leaq指令是加载有效地址,时间有效地址写入数据,相当于指针。
3.3.3类型转换
在程序中用到了C标准库的atoi函数,把argv[3]中的字符串内容转换为一个整型数;
#include <unistd.h>
unsigned int sleep(unsigned int seconds);//睡眠秒
返回值:成功返回0,或者返回剩余的要睡眠的秒数(被signal中断后).
而传入的参数为整型,此时发生了隐式类型转换,有符号整型转换为无符号整型。
3.3.4算术操作
对i进行的加1操作:addl $1, -4(%rbp)
3.3.5关系操作
cmpl $4, -20(%rbp)
je .L2
比较argc和4的大小,如果相等,则跳转到.L2
cmpl $8, -4(%rbp)
jle .L4
比较i和8的大小,如果小于等于,则跳转到.L4
3.3.6数组/指针/结构操作
在for循环内需要对argv数组进行访问,如下图3-4:
图3-4 for循环实现
movq -32(%rbp), %rax
addq $16, %rax
表示argv[2]的地址
movq -32(%rbp), %rax
addq $8, %rax
表示argv[1]的地址
movq -32(%rbp), %rax
addq $24, %rax
表示argv[3]的地址
3.3.7控制转移
第一处:if
cmpl $4, -20(%rbp)
je .L2
比较argc和4的大小,如果相等,则跳转到.L2,对应着C语言中的如下语句:
if(argc!=4){
printf("用法: Hello 学号 姓名 秒数!\n");
exit(1);
}
第二处:for
cmpl $8, -4(%rbp)
jle .L4
比较i和8的大小,如果小于等于,则跳转到.L4,对应C语言中的如下语句:
for(i=0;i<9;i++){
printf("Hello %s %s\n",argv[1],argv[2]);
sleep(atoi(argv[3]));
}
3.3.8函数操作
本程序调用两个C标准库的函数printf,atoi和一个包含在unistd.h中的sleep函数;
如下图3-5,printf的传入参数有三个,%rdi是在.L1节的格式串,%rsi是argv[1]的地址,%rdx是argv[2]的地址;atoi传入的参数%rdi是argv[3]的值;sleep传入的参数是atoi的返回值。
图3-5 函数调用
3.4 本章小结
本章介绍了编译的概念和作用,编译的指令,重点分析了编译得到的结果文件的内容。
第4章 汇编
4.1 汇编的概念与作用
4.1.1概念
汇编器(as)将hello.s翻译为机器语言指令,并把这些指令打包成可重定位目标文件的过程;
4.1.2作用
将汇编语言文本文件转换为机器代码的二进制文件,汇编的结果是一个可重定位目标文件(如hello.o)其中包含的是不可读的二进制代码。
4.2 在Ubuntu下汇编的命令
as hello.s -o hello.o或gcc -c hello.s -o hello.o
图4-1 汇编指令及结果
4.3 可重定位目标elf格式
4.3.1.用指令readelf -a hello.o > ./elf.txt生成elf格式的文件;
4.3.2.分析hello.o的ELF格式
包括Elf头和节头部表(描述目标文件的节),简略介绍了各节的信息;
图4-2 elf文件信息
4.3.3.elf文件各部分详解
1.Elf头
以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。Elf头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小,目标文件的类型,机器类型,节头部表的文件偏移,以及节头部表中条目大小和数量。
由数据项看出是2、补码、小端机器,机器是Advanced Micro Devices X86-64,头的大小是64,节头部表偏移为0x0
2.节头部表
不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的条目。
.text节的类型为PROGBITS,标志为AX,对齐为1;
以及其它节的相关信息易看出。
3. .rela.text 节和.rela.eh_frame节
图4-3 .rela.text和.rela.eh_frame
.rela.tex节是.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。
.rela.eh_frame和.rela.tex都是重定位节,在其中有信息,偏移量,类型,符号值和符号名称。
可以看出hello.o在链接时需要修改的内容及其寻址方式(PC相对寻址或直接寻址)等。
4. .symtab节
图4-4 .symtab
一个符号表,存放在程序中定义和引用的函数和全局变量的信息,不包含局部变量的头目。
hello.o程序的符号表包含编号Num、Value、Size、Type、Bind、Vis、Ndx、Name字段。可以看出,main是一个全局的函数,偏移量为0,大小为142字节,同理可看出puts、exit、printf、atoi、sleep、getchar等信息。
4.4 Hello.o的结果解析
(以下格式自行编排,编辑时删除)
4.4.1.指令
用指令objdump -d -r hello.o > obj.txt生成反汇编代码的文本文件来分析hello.o的反汇编。结果如图4-5:
图4-5 obj.txt信息
4.4.2.分析
1.构成,与汇编语言的关系
机器语言由计算机直接识别的二进制代码构成,不同型号的计算机其机器语言是不相通的,按着一种计算机的机器指令编制的程序,不能在另一种计算机上执行。
机器语言与汇编语言是一一对应的关系,一条机器语言对应一条汇编指令。
汇编语言的操作数是十进制整型数,机器语言中的操作数是二进制数(有符号数为补码形式表示,无符号数用原码表示),机器语言全为0、1序列表示数据和指令,而汇编语言还有助记符帮助理解。
2.汇编反汇编对比
图4-6 汇编语言
由图4-5和图4-6对比易得,汇编代码与反汇编代码几乎一致
其中有以下几点不同:
1)操作数进制不同:汇编语言中的操作数是十进制数,而反汇编结果中的是十六进制数;
2)分支转移:汇编语言的分支跳转后跟的是段的名字,而由于以“. ”开头的行是指导汇编器和链接器工作的伪指令,在反汇编代码中没有这些,而且hello.o是可重定位文件,因此其跳转的时候用的是其要跳转的目的地址;
3)函数调用:汇编代码的函数调用时后面跟着函数名字,而反汇编代码调用函数时后面跟着的也是相对于main函数的偏移地址;
4)重定位条目:反汇编代码采用重定向的方式进行跳转,机器代码在此处留下一个地址以供链接时重定向。或者采用PC相对寻址,或者直接寻址,根据地址的更新和寻址的计算,实现跳转和调用。
4.5 本章小结
本章主要介绍了汇编的概念及作用,汇编和反汇编的指令,详细介绍了汇编后的文件进行反汇编得到的文件与汇编语言文件的对比。
第5章 链接
5.1 链接的概念与作用
5.1.1概念
链接(linking)是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行干编译时(compiletime),也就是在源代码被翻译成机器代码时;也可以执行干加载时(loadtime),也就是在程序被加载器(loader)加载到内存并执行时;甚至执行于运行时(runtime),也就是由应用程序来执行。
5.1.2作用
1.使分离编译成为可能:将巨大的源文件分解为更小、更好管理的模块,进行独立的编译和修改;
2.得到可执行程序
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.3.1指令
用readelf -a hello.out > elf2.txt得到hello.out的ELF格式。
图5-2
5.3.2分析
1.elf头
对hello.out的elf头,16字节的序列与hello.o相同,文件类型是可执行文件(EXEC),入口点地址为0x400550,字符串表索引节头为24。
图5-3 elf头
2.节头部表
相比于hello.o,增加了很多个段的信息,其中有几个重要的:
1).init段:定义了一个小函数init,程序的初始化代码会用到;
2).dynamic段:保存动态链接所需要的基本信息,存储动态链接会用到的所有表的位置信息;
3).dynsym段:动态符号表,存储与动态链接相关的导入导出符号,不包括模块内部的符号;
4).dynstr段:存储.dynsym段符号对应的符号名;
5).comment段:程序相关信息,用与定位符号地址。
图5-4 节头部表
3.程序头表
ELF可执行文件被设计得很容易加载到内存,可执行文件的连续的片(chunk)被映射到连续的内存段。程序头部表(programheadertable)描述了这种映射关系。
图5-5 程序头表
4. .dynamic 段 .dynsym
保存动态链接所需要的基本信息,存储动态链接会用到的所有表的位置信息。
图5-6 Dynamic section
动态符号表,存储与动态链接相关的导入导出符号,不包括模块内部的符号;
图5-7 .dynsym
5. .rela.dyn和 .rela.plt
图5-8 .rela.dyn和 .rela.plt
分别为对数据引用的修正和对函数引用的修正。
6. .symtab
图5-9 .symtab
.symtab 保存所有符号,包括 .dynsym 中的符号。
7. .gnu.version和.gnu.version_r
图5-10 .gnu.version和.gnu.version_r
版本信息
5.4 hello的虚拟地址空间
1.使用edb加载hello,查看本进程的虚拟地址空间各段信息,如图5-11:
易得虚拟地址空间从0x400000开始。
图5-11 edb载入信息
2.与5.3对照分析说明
使用symbol view易得hello.out各段加载的起始位置与elf中的记录相同,如.interp、.hash等等。
图5-12 symbol view
5.5 链接的重定位过程分析
objdump -d -r hello.out > obj2.txt 将hello.out反汇编结果定位到obj2.txt文件;
1.hello与hello.o的不同
图5-13 反汇编结果
1)hello.out添加了代码中调用的库,反汇编结果比hello.o内容要多,如printf,puts等;
2)hello.out反汇编地址从0x400000开始,而hello.o从0开始;
3)在hello.out没有重定位条目,全都是对应的虚拟地址,而hello.o中还有18: R_X86_64_PC32 .rodata-0x4类似的内容;跳转和调用的地址计算。
2.链接的过程
1)符号解析
目标文件定义和引用符号,每个符号对应于一个函数、一个全局变量或一个静态变量(即C语言中任何以static属性声明的变量)。符号解析的目的是将每个符号引用正好和一个符号定义关联起来。
在编译时,编译器向汇编器输出每个全局符号,或者是强(strong)或者是弱(weak),而汇编器把这个信息隐含地编码在可重定位目标文件的符号表里。函数和已初始化的全局变量是强符号,未初始化的全局变量是弱符号。
根据强弱符号的定义,Linux链接器使用下面的规则来处理多重定义的符号名:
规则1:不允许有多个同名的强符号;
规则2:如果有一个强符号和多个弱符号同名,那么选择强符号;
规则3:如果有多个弱符号同名,那么从这些弱符号中任意选择一个。
2)重定位
编译器和汇编器生成从地址0开始的代码和数据节。链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使得它们指向这个内存位置。链接器使用汇编器产生的重定位条目(relocationentry)的详细指令,不加甄别地执行这样的重定位。
重定位PC相对引用:
ADDR(s)=ADDR(text)
ADDR(r.symbol)=ADDR(sum)
链接器首先计算出引用的运行时地址:refaddr=ADDR(s)+r.offset
然后,更新该引用,使得它在运行时指向sum程序:
*refptr=(unsigned)(ADDR(rsymbol)+raddend-refaddr)
最后跳转时,PCßPC+*refptr
重定位绝对引用:
ADDR(r.symbol)=ADDR(array)
*refptr=(unsigned)(ADDR(rsymbol)+raddend)
最后跳转时,PCß*refptr
如调用atoi函数:
在hello.o中重定位条目如下:
6c: e8 00 00 00 00 callq 71 <main+0x71>
6d: R_X86_64_PLT32 atoi-0x4
易得r.offset=0x6d,r.addend=-4
4005ee: e8 2d ff ff ff callq 400520 <atoi@plt>
0000000000400520 <atoi@plt>:
已知ADD(main)=0x400582,ADD(atoi)=0x400520
则refsddr=0x400582+0x6d=0x4005ef
*refptr=0x400520-4-0x4005ef=0xffffff2d,PC=0x4005ee+0x400520刚好指向atoi
5.6 hello的执行流程
图5-14执行过程
子程序名 |
ld-2.27.so!_dl_start |
hello.out!_start |
ld-2.27.so!_dl_init |
libc-2.27.so!__libc_start_main |
hello.out!_init |
hello.out_main |
hello.out!puts@plt |
hello.out!exit@plt |
hello.out!printf@plt |
hello.out!atoi@plt |
hello.out!sleep@plt |
hello.out!getchar@plt |
libc-2.27.so!exit |
5.7 Hello的动态链接分析
动态链接是在程序加载时完成连接任务,将动态库的文本和数据重定位到某个段,重定位符号引用,最后动态链接器将控制传递给应用程序。
程序调用由共享库定义的函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。GNU 编译系统使用了延迟绑定(lazy binding),将过程地址的绑定推迟到第一次调用该过程时。在GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数,在加载时,动态链接器会重定位GOT中的每个条目,使得它包含目标的正确的绝对地址。
在dl_init调用之前,编译器产生对调用函数的直接PC相对引用,并增加一个重定位,让链接器在构造共享模块时解析它。PLT[1]调用系统启动函数初始化执行环境,调用main并处理其返回值,每个GOT条目都对应一条PLT条目,初始时,每个GOT条目都指向对应的PLT条目的第二条指令。
由elf头表易得got.plt位置为:
图5-15在dl_init前
在dl_init调用之后,可以看到.got节发生了明显变化。
图5-16 在dl_init后
动态链接器会重定位GOT中的每个条目,使它包含正确的绝对地址,而PLT中的每个函数负责调用不同函数。第1步,程序调用进入 PLT[2]。第2步。第一条PLT指令通过 GOT进行间接跳转。因为每个GOT条目初始时都指向它对应的PLT条目的第二条指令,这个间接跳转只是简单地把控制传送回PLT[2]中的下一条指令。第3步。在把调用函数的ID压人栈中之后,PLT[2]跳转到 PIT[O]。第4步。PLT[0]通过 GOT[1]间接地把动态链接器的一个参数压入栈中,然后通过GOT[2]间接跳转进动态链接器中。动态链接器使用两个栈条目来确定调用函数的运行时位置,用这个地址重写GOT[4],再把控制传递给调用函数。
在调用函数之后,可以看到.got.plt节之前的内容发生了改变,如下图5-17:
图5-17 在调用函数之后
5.8 本章小结
本章主要介绍了链接的概念及作用,链接的指令以及链接结果反汇编与hello.o对比,链接的执行过程分析以及动态链接。
第6章 hello进程管理
6.1 进程的概念与作用
1.概念
进程的经典定义就是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文(context)中。
2.作用
提供给应用程序两个关键抽象:
一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器;
一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用内存系统。
6.2 简述壳Shell-bash的作用与处理流程
1.作用
shell是一个命令行解释器,它输出一个提示行符,等待输入一个命令行,然后执行这个命令。如果该命令行的第一个单词不是一个内置的shell命令,那么shell就会假设这是一个可执行文件的名字,它将加载并运行这个文件。
2.处理流程
图6-1 处理流程
6.3 Hello的fork进程创建过程
父进程通过调用fork函数创建一个新的运行的子进程。
新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大的区别在于它们有不同的PID。fork被调用一次,返回两次:一次是在调用进程(公进程)中,一次是在新创建的子进程中,在父进程中,fork返回子进程的PID。在子进程中,fork返回0。
6.4 Hello的execve过程
execve函数在当前进程的上下文中加载并运行一个新程序。execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境变量列表envp。只有当出现错误时,execve才会返回到调用程序,否则execve调用一次并从不返回。
图6-2 新程序开始时的用户栈
6.5 Hello的进程执行
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策就叫做调度(scheduling),是由内核中称为调度器(scheduler)的代码处理的。
当内核选择一个新的进程运行时,我们说内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程,上下文切换1)保存当前进程的上下文,2)恢复某个先前被抢占的进程被保存的上下文,3)将控制传递给这个新恢复的进程。
操作系统的任务调度是采用时间片轮转的抢占式调度方式,也就是说一个任务执行一小段时间后强制暂停去执行下一个任务,每个任务轮流执行。任务执行的一小段时间叫做时间片,任务正在执行时的状态叫运行状态,任务执行一段时间后强制暂停去执行下一个任务,被暂停的任务就处于就绪状态等待下一个属于它的时间片的到来。这样每个任务都能得到执行,由于CPU的执行效率非常高,时间片非常短,在各个任务之间快速地切换,给人的感觉就是多个任务在“同时进行”,这也就是我们所说的并发。
比如,hello初始运行在用户模式,调用sleep之后陷入内核模式,内核处理休眠请求主动释放当前进程,并将hello进程从运行队列中移出加入等待队列,定时器开始计时,内核进行上下文切换将当前进程的控制权交给其他进程,定时器到时会发送一个中断信号,此时进入内核状态执行中断处理,将hello进程从等待队列中移出重新加入到运行队列,成为就绪状态,hello进程继续运行。
图6-3 进程切换
6.6 hello的异常与信号处理
常见异常:
图6-4 异常类别
6.6.1不停乱按,包括回车
图6-5 乱按
不停的按除回车之外的字符时,只是保存起来,并没发生什么异常,若输入了回车,之前输入的字符会被当作命令,因为shell无法处理这些,进程会终止并退出。
6.6.2Ctrl-Z
图6-5 Ctrl+Z
按下Ctrl+Z,父进程会收到此进程发来的SIGSTP信号并运行信号处理程序,程序会被挂起,并且输出了相关信息;
图6-6 ps
查看进程信息
图6-7 jobs
查看任务列表
图6-8 pstree
查看进程树
图6-9 fg
Fg会恢复前台运行
图6-10 kill
Ctrlz后运行kill:
Hello的进程号是16025,可通过kill -9 16025发送信号SIGKILL给进程16025,它会导致该进程被杀死。然后再运行ps,可发现已被杀死的进程hello。
6.6.3Ctrl-C
图6-10 Ctrl+C
键入Ctrl+C后,内核会发送一个SIGINT信号给此前台进程组的每个进程,进程终止。
6.7本章小结
本章主要介绍了进程的作用,shell的作用和处理流程以及hello运行和异常处理。
结论
Hello的一生包含着一个系统运行一个程序的基本流程。
- 预处理:经由cpp,变为hello.i;
- 编译:经由cc1,变为hello.s;
- 汇编:经由as,变为hello.o;
- 链接:经由ld,变为hello可执行程序;
- 在命令行输入执行指令,shell调用fork创建子进程;
- Execve加载并运行hello;
- 程序结束后,父进程回收hello,内核清除hello所占资源,hello短暂而又不平凡的一生到此结束;
通过体验hello的一生,我对计算机系统的各个环节更加熟悉,复习了计算机系统的相关知识,了解了计算机偏底层的运行,对今后的编程和调试以及大型程序项目的开发,提高程序的可移植性有莫大的帮助
附件
中间结果文件名字 | 作用 |
hello.i | 预处理生成的文件 |
hello.s | 编译生成的文件 |
hello.o | 汇编生成的文件 |
hello.out | 链接生成的文件 |
hello | 从hello.c一步到位 |
elf.txt | hello.o的elf格式 |
elf2.txt | hello.out的elf格式 |
obj.txt | hello.o的反汇编代码 |
obj2.txt | hello.out的反汇编代码 |
参考文献
[1] (美)布赖恩特(Bryant,R.E.)等.深入理解计算机系统(原书第3版). 机械工业出版社,2016.
[2] 预处理. https://baike.sogou.com/v581536.htm?fromTitle=%E9%A2%84%E5%A4%84%E7%90%86.
[3] 编译. https://baike.baidu.com/item/%E7%BC%96%E8%AF%91/1258343#1.
[4] linux 睡眠函数——sleep(),usleep() . https://blog.csdn.net/Gpengtao/article/details/7887293?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-7887293-blog-117064651.pc_relevant_3mothn_strategy_recovery&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-7887293-blog-117064651.pc_relevant_3mothn_strategy_recovery&utm_relevant_index=1.
[5] FastHook——实现.dynsym段和.symtab段符号查询. https://www.jianshu.com/p/b34f37715054.
[6] 进程,时间片.https://blog.csdn.net/qq_49425839/article/details/116107230.