计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术
学 号 2021113667
班 级 2103103
学 生 陈云丹
指 导 教 师 刘宏伟
计算机科学与技术学院
2022年5月
本文以Hello为例,叙述了程序从.c文件开始,经历预处理、编译、汇编、链接,生成可执行目标文件的过程。人们在Shell中执行一系列指令加载并运行Hello,本文便接着阐述了系统如何对hello进行进程管理,用各种工具观察了一个程序从“出生”到“死亡”的精彩一生,从而更加深入地理解程序如何在计算机系统中运行,感悟一代又一代计算机人的智慧。
关键词:预处理;编译;汇编;链接;进程管理
目 录
2.2在Ubuntu下预处理的命令.......................................................................... - 6 -
3.2 在Ubuntu下编译的命令............................................................................. - 8 -
4.2 在Ubuntu下汇编的命令........................................................................... - 18 -
5.2 在Ubuntu下链接的命令........................................................................... - 24 -
5.3 可执行目标文件hello的格式.................................................................. - 25 -
5.5 链接的重定位过程分析............................................................................... - 28 -
6.2 简述壳Shell-bash的作用与处理流程..................................................... - 29 -
6.3 Hello的fork进程创建过程..................................................................... - 29 -
6.6 hello的异常与信号处理............................................................................ - 29 -
第7章 hello的存储管理............................................................................... - 30 -
7.1 hello的存储器地址空间............................................................................ - 30 -
7.2 Intel逻辑地址到线性地址的变换-段式管理............................................ - 30 -
7.3 Hello的线性地址到物理地址的变换-页式管理...................................... - 30 -
7.4 TLB与四级页表支持下的VA到PA的变换............................................. - 30 -
7.5 三级Cache支持下的物理内存访问.......................................................... - 30 -
7.6 hello进程fork时的内存映射.................................................................. - 30 -
7.7 hello进程execve时的内存映射.............................................................. - 30 -
7.8 缺页故障与缺页中断处理........................................................................... - 30 -
8.1 Linux的IO设备管理方法.......................................................................... - 32 -
8.2 简述Unix IO接口及其函数....................................................................... - 32 -
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P(From Program to Process):Hello程序的生命周期是从一个高级C语言程序(program)开始的。GCC编译器驱动程序读取源程序文件Hello.c,并把它翻译成一个可执行目标文件Hello。这个翻译的过程可以分为四个阶段:预处理、编译、汇编、链接。如图1所示。
图1 编译系统
接着,外壳将加载并运行Hello程序。在Bash中,OS通过fork创建一个子进程,Hello便在其进程上下文中运行,成为一个进程(Process)。
020(From Zero-0 to Zero-0):Hello程序在运行之前不占用系统资源(Zero)。经过P2P的过程后,execve映射虚拟内存,mmap将用户空间的虚拟内存地址与文件进行映射(绑定),CPU为其分配时间片执行逻辑控制流,I/O管理与信号处理软硬结合,直至Hello程序运行结束,它的父进程将会回收Hello程序,内核删除相关数据结构(Zero)。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
软件环境:Windows11 64位以上;VirtualBox/Vmware 11以上;Ubuntu 18.04.5 LTS 64位
硬件环境:x64 CPU;2.10GHz;16G RAM;256GHD Disk
开发与调试工具:Visual Studio 2015 64位以上;vi/vim/gedit+gcc;gdb;redelf;objdump
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
hello.c:源程序(文本)
hello.i:hello.c预处理生成的被修改的源程序(文本)
hello.s:hello.i编译生成的汇编程序(文本)
hello.o:hello.s汇编生成的可重定位目标程序(二进制)
hello:hello.o链接生成的可执行目标程序(二进制)
hello.elf:hello.o的ELF文件
hello2.elf:hello的ELF文件
hello.txt:hello.o的反汇编文件
hello2.txt:hello的反汇编文件
1.4 本章小结
本章对Hello程序P2P和020的特点进行阐述,列写了实验的软硬件环境与开发调试工具,罗列了中间结果文件,为后续实验打下基础。
第2章 预处理
2.1 预处理的概念与作用
概念:在编译之前进行的处理。C语言的预处理主要有三个方面的内容:宏定义、文件包含、条件编译。
作用:预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如#include<stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入到程序文本中。结果就得到了另一个C程序,通常是以.i作为文件拓展名。
2.2在Ubuntu下预处理的命令
图2 Ubuntu下预处理的命令
2.3 Hello的预处理结果解析
①处理文件包含。惊讶的发现文件内容真的是增加了非常多。处理文件包含,即把指定的文件插入命令行位置取代该命令行,从而把指定的文件和当前的源程序文件连成一个文件- 35 -[1]。故增加的内容应为头文件stdio.h,unistd.h和stdlib.h的具体内容。
②删除代码注释。注释是为了提高晦涩难懂的代码的可读性,如帮助开发者在没有仔细阅读代码的情况下能快速了解一个函数的功能,但对于编译器来说这部分就是无用的代码,将被移除。
③不对源程序代码解析。在最后找到了Hello.c的代码,可以看出这部分变化不是很大。
图3 Hello的预处理结果
2.4 本章小结
本章介绍了预处理的概念、作用,并在Ubuntu上用gcc -E -m64 -no-pie -fno-PIC hello.c -o hello.i命令进行了实验,分析了预处理生成的hello.i文件,得出预处理将处理文件包含、删除代码注释、不对源程序代码解析等结论。从现在开始,Hello迈出了它一生中的第一步。
第3章 编译
3.1 编译的概念与作用
概念:从预处理后的文件(.i)生成汇编语言程序(.s)的过程。也就是将一段程序转换为指令集的过程。
作用:编译器(ccl)将文本文件Hello.i翻译成文本文件Hello.s,它包含一个汇编语言程序。汇编语言程序中的每条语句都以一种标准的文本格式确切地描述了一条低级机器语言指令。汇编语言是非常有用的,因为它为不同高级语言的不同编译器提供了通用的输出语言。
3.2 在Ubuntu下编译的命令
图4 Ubuntu下编译的命令
3.3 Hello的编译结果解析
图5 编译结果
图6 编译结果
3.3.1 常量
程序中包含两个字符串常量:
图7 字符串常量
这是printf语句中的格式化控制串,即"用法: Hello 学号 姓名 秒数!\n" 与 "Hello %s %s\n"。带有“L”的被认为是“本地标签”,编译器使用它来引用特定目标文件专用的符号[2]。它们被放在只读数据.rodata中。
3.3.2 变量
①局部变量int i:
图8 局部变量i的处理
保存在-4(%rbp)中,即栈中%rbp-4的内存地址中(并赋初值为0,)。
②main函数参数int argc:
图9 main函数第一个参数argc的处理
由于argc是main函数的第一个参数,所以先保存在寄存器%edi中,随后,又被传送到栈中%rbp-20的内存地址中。
3.3.3 数组
main函数的第二个参数char *argv[],先保存在寄存器%rsi中(保存的是64位地址),随后,又被传送到栈中%rbp-32的内存地址中。
图10 main函数第二个参数argv的处理
3.3.4 赋值(=)
图11 对局部变量i的赋值
对于局部变量i的赋值语句,使用movl指令(双字传送,因为i为int型)为其赋初值为0。
3.3.5 类型转换
图12 atoi类型转换处理
程序调用了atoi函数,将第四个命令行参数(argv[3])由字符串转为int型整型数。
3.3.6 算术操作
每次循环,局部变量i增加1,直至等于9时跳出循环。
图13 局部变量i的加法操作
3.3.7 关系操作
①将argc与4进行比较,等于时(je)跳转到.L2语句段。否则,输出.LC0中字符串"用法: Hello 学号 姓名 秒数!\n"并调用exit(1)。
图14 argc和4的比较操作
②每次循环将局部变量i与8进行比较,小于等于8时(jle)则跳转到.L4执行循环体内语句,否则结束循环。
图15 i和8的比较操作
3.3.8 数组/指针操作
图16 argv[3]的引用
图中movq -32(%rbp),%rax命令使%rax寄存器存放char *argv[](地址)。addq $24,%rax命令使%rax中存放的地址值增加了24。于是此时,由%rax保存的内存地址中存放的值相当于argv[3]。
依次类推,图17表示的是对argv[2]和argv[1]的引用(数值又将分别存放于%rdx和%rsi中)。
3.3.9 控制转移
图18 控制转移语句
- (je)为相等时跳转(ZF等于0时进行跳转)。
- (call)为puts函数调用。
- (call)为exit函数调用。
-
图19 控制转移语句
- (jmp)为无条件跳转(直接跳转进入循环体内)。
-
图20 控制转移语句
- (call)为printf函数调用。
- (call)为atoi函数调用。
- (call)为sleep函数调用。
图21 控制转移语句
- (call)为getchar函数调用。
- (ret)为子程序的返回指令。
3.3.10 函数操作
本程序用到的函数有:main,printf,puts,exit,sleep,atoi,getchar。
其中,main函数是主函数,是程序的入口。如图22所示,.globl将main声明为全局符号。使用.type伪指令定义main标签的类型是一个函数(function)。在执行程序时,由系统调用main函数,并传递参数int argc(存放于%edi中)和char * argv[](存放于%rsi中)。
图22 main函数
而对于其他函数(printf,puts,exit,sleep,atoi,getchar)的调用则使用call指令。分别如图23、图24、图25、图26、图27、图28所示。
3.4 本章小结
本章介绍了编译的概念与作用,并在Ubuntu下输入命令gcc -S -m64 -no-pie -fno-PIC hello.i -o hello.s进行实验,将高级语言编译成较低级的汇编语言,此时更加接近于机器能读懂的语言。本章对生成的汇编结果进行解析,并说明了编译器是如何处理C语言的各个数据类型以及各类操作的。至此,Hello的“人生”又迈出了更大的一步。
第4章 汇编
4.1 汇编的概念与作用
概念:从编译后的文件(.s)到生成机器语言二进制程序(.o)的过程。
作用:汇编器(as)将Hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件Hello.o中。Hello.o文件是一个二进制文件,它的字节编码是机器语言指令而不是字符。(如果我们在文本编辑器中打开Hello.o文件,看到的将是一堆乱码。)
4.2 在Ubuntu下汇编的命令
图29 Ubuntu下汇编的命令
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
图30 获取可重定位目标elf格式的指令
- ELF头。如图31所示,ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表的文件偏移,以及节头部表中条目的大小和数量。
图31 Hello.o的elf头
- 节头部表。不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的条目。如图32所示。
图32 Hello.o的节头部表
- .rela.text节。一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置(如Hello.o中对puts,exit,printf,atoi,sleep,getchar等的重定位信息)。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。另一方面,调用本地函数的指令则不需要修改。
图33 .rela.text节
如图33所示,它包含了:
偏移量:需要被修改的引用的节偏移。
信息。
类型:告知链接器如何修改新的引用。图中重定位类型为R_X86_64_PC32(重定位一个使用32位PC相对地址的引用)和R_X86_64_32(32位绝对地址引用)。
符号值。
符号名称+加数:其中加数为一个有符号常数,一些类型的重定位要使用它对被修改引用的值做偏移调整。
- .rela.eh_frame节。它包含的是.eh_frame的重定位信息。而.eh_frame内部存放的是以DWARF格式保存的一些调试信息[3]。
图34 .rela.eh_frame节
- .symtab节。一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。但.symtab符号表不包含局部变量的条目。
图35 .symtab节
4.4 Hello.o的结果解析
objdump -d -r hello.o分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
图36 hello.o的反汇编结果
如图36是objdump -d -r hello.o > hello.txt命令生成的文本文件。
- 操作数:hello.s中为十进制数,而hello.o的反汇编文件中为十六进制数。(16进制转换为2进制比十进制更方便。)
- 分支转移:hello.s使用.L2等标签进行跳转,而hello.o的反汇编代码使用目标地址进行跳转。
- 函数调用:hello.s使用call+函数名,而hello.o的反汇编代码中使用call+偏移量调用函数。但由于未链接,此时偏移量均为0,并留下了重定位条目。
4.5 本章小结
本章介绍了汇编的概念和作用,并使用gcc -c -m64 -no-pie -fno-PIC hello.s -o hello.o命令生成了可重定位目标文件,由于此时hello.o文件是一个二进制文件,它的字节编码是机器语言指令而不是字符,所以如果我们在文本编辑器中直接打开hello.o文件,看到的将是一堆乱码。故笔者使用objdump -d -r hello.o > hello.txt命令进行反汇编,并将反汇编结果存入文本文件中以便阅读。接着,本章分析了可重定位目标elf格式,并对hello.o(反汇编)的结果进行解析。此时,hello的一生走向了机器语言,做好了“在台上表演”的充分准备。
第5章 链接
5.1 链接的概念与作用
概念:指从 hello.o 到hello生成过程。链接器(ld)负责合并被调用的标准库函数的预编译好的目标文件。
作用:以hello程序调用了printf函数为例,printf是每个C编译器提供的标准C库中的一个函数。printf函数存放于一个名为printf.o的单独的预编译好了的目标文件中,而这个文件必须以某种方式合并到我们的hello.o程序中。链接器(ld)就负责处理这种合并。结果就得到hello文件,它是一个可执行目标文件(或者简称为可执行文件),可以被加载到内存中,由系统执行。
5.2 在Ubuntu下链接的命令
图37 Ubuntu下链接的命令
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
图38 获取hello的ELF格式的命令
如图38,将hello的ELF格式存入hello2.elf文件中。
图39 hello的ELF头
图39是hello的ELF头,它以一个16字节的序列开始。Hello的ELF头与hello.o的ELF头的不同之处在于:从hello的类型为EXEC可以看出hello已经是一个可执行文件了。
图40 hello的ELF格式
图40是hello的elf节头。从中我们可以看到各段的名称、类型、大小、起始地址、偏移量、对齐等基本信息。
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
图41 Data Dump
图42 .text段
如图41图42是hello的Data Dump的截图。可以看到,hello虚拟地址空间起于0x00400000,结束于0x00401000。与5.3节头部表对照,可以发现,通过edb可以找到各个段的位置,例如.text的地址是0x400550,大小为0x132,即可以在edb中地址0x400550~0x400682找到相应信息,如图42所示。其余段的查看方法与.text同理。
5.5 链接的重定位过程分析
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
5.5.1 不同之处
图43 hello反汇编命令
图43是hello的反汇编命令,并把反汇编结果存入hello2.txt中。对比发现hello与hello.o的不同之处如下:
- Hello加入了程序中调用的库函数,如puts,printf,getchar,atoi等,在链接时完成了符号解析和重定位。如图44:
图44 截图1
- hello实现了调用函数时的重定位,即call+偏移量。其中,偏移量=目标地址-当前PC存放的地址。图45就是函数调用的例子。此外,jmp指令后紧跟着的也由相对地址变成了确定的虚拟地址。
图45 截图2
- hello.o的反汇编代码虚拟地址从0开始,hello的反汇编代码虚拟地址从0x00000000004004c0开始,如图46所示:
图46 截图3
5.5.2 链接过程
以hello程序调用的printf函数为例,printf是每个C编译器提供的标准C库中的一个函数。printf函数存放于一个名为printf.o的单独的预编译好了的目标文件中,链接器(ld)负责将这个文件合并到我们的hello.o程序中,结果就得到hello文件,它是一个可执行目标文件(或者简称为可执行文件),可以被加载到内存中,由系统执行。
链接器通过解析符号引用完成链接,具体方法是将每个引用与它输入的可重定位目标文件的符号表中的一个确定的符号定义关联起来。对于那些引用定义在相同模块中的局部符号的引用,符号解析是非常简单明了的。而对于全局符号的引用,编译器会假设该符号是在其他模块中定义的,生成一个链接器符号表条目并把它交给链接器处理。如果链接器在它的任何输入模块中都找不到这个被引用符号的定义,就输出一条错误信息并终止。
5.5.3 重定位步骤
链接器将需要调用的库函数文件合并到我们的hello.o程序中,重定位符号引用时,类型为R_X86_64_PC32的,根据
refaddr = ADDR(s) + r.offset;
*refptr = (unsigned)(ADDR(r.symbol) + r.addend – refaddr);
修改32位PC相对引用,这样在运行时它就会指向被调用的函数例程。而类型为R_X86_64_32的,根据
*refptr = (unsigned) (ADDR(r.symbol) + r.addend)
绝对寻址。
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
子程序名 | 程序地址 |
ld-2.27.so!_dl_start | 0x7fe930791ea0 |
ld-2.27.so!_dl_init | 0x7fe9307a07d0 |
libc-2.27.so!_init | 0x7f6fecfe8a10 |
libc-2.27.so!__libc_start_main | 0x7f54eee11ba0 |
hello!_init | 0x7ffd5bd27e68 |
hello!_start | 0x400550 |
Hello!main | 0x400582 |
hello!puts@plt | 0x4004f0 |
hello!printf@plt | 0x400500 |
hello!getchar@plt | 0x400510 |
hello!atoi@plt | 0x400520 |
hello!sleep@plt | 0x400540 |
hello!exit@plt | 0x400530 |
5.7 Hello的动态链接分析
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
5.7.1 动态链接项目分析
动态链接指在可执行文件装载或运行时,由操作系统的装载程序加载库。动态链接库中的函数在程序执行时才会确定地址,故编译器采用延迟绑定[4]的策略,通过过程链接表(PLT)和全局偏移量(GOT)实现。
PLT是一个数组,每个条目是16字节代码,PLT[0]是一个特殊条目,它跳转到动态链接器中。PLT[1]调用系统启动函数,初始化执行环境, PLT[2]开始的条目是调用用户代码调用的函数。
GOT是一个数组,每个条目是8字节地址。和PLT联合使用。GOT[0]与 GOT[1]包含动态链接器在解析函数地址时的会使用的信息。GOT[2]是动态链接器在 ld-linux.so 模块的入口点,其余条目对应一个函数,在第一次调用时进行解析,结束后将其指向正确的函数运行时地址。
5.7.2 dl_init前后项目内容变化
图47
如图47,在节头中找到.got.plt的地址为0x601000,大小为0x48。
图48 dl_init前
图49 dl_init后
从图48、图49的对比中,可以发现dl_init前后,项目的内容发生了变化。在调用dl_init后,新增了两个地址:0x7f165x44b170和0x7f165c2377a0,即动态链接后函数的地址。
5.8 本章小结
本章介绍了链接的概念和作用,并在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
进行实验,接着对可执行目标文件HELLO的格式、HELLO的虚拟地址空间、链接的重定位过程、HELLO的执行流程、HELLO的动态链接进行了分析。此时,链接器将库函数合并到Hello.o中,使之真正成为一个可以运行的程序Hello。Hello的“人生”进入了一个崭新的阶段。
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程的经典定义就是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文中。
作用:进程提供给应用程序两个关键抽象:
1、一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器。
2、一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用内存系统。
6.2 简述壳Shell-bash的作用与处理流程
作用:Linux系统中,Shell是一个交互型应用级程序,代表用户运行其他程序(是命令行解释器,以用户态方式运行的终端进程)。其基本功能是解释并运行用户的指令。
(1)终端进程读取用户由键盘输入的命令行。
(2)分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量。
(3)检查第一个(首个、第0个)命令行参数是否是一个内置的shell命令。如果不是内部命令,调用fork( )创建新进程/子进程。
(4)在子进程中,用步骤2获取的参数,调用execve( )执行指定程序。
(5)如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid(或wait...)等待作业终止后返回。
(6)如果用户要求后台运行(如果命令末尾有&号),则shell返回。
6.3 Hello的fork进程创建过程
父进程通过调用fork函数创建一个新的运行的子进程hello。fork调用一次,返回两次。新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大的区别在于它们有不同的PID:在父进程中,fork返回子进程的PID;在子进程中,fork返回0。
6.4 Hello的execve过程
execve函数在当前进程的上下文中加载并运行可执行目标文件hello,且带参数列表argv和环境变量envp。execve调用一次并从不返回。在execve加载了hello后,它调用启动代码设置栈,并将控制传递给新程序的主函数。当main开始执行时,用户栈的组织结构如图50所示。在栈的顶部是系统启动函数libc_start_main的栈帧。
图50 一个新程序开始时,用户栈的典型组织结构
6.5 Hello的进程执行
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
上下文:上下文就是内核重新启动一个被抢占的进程所需要的状态。它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构。
进程时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
进程调度的过程:在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程。这种决策就叫做调度,是由内核中称为调度器(scheduler)的代码处理的。当内核选择一个新的进程运行时,我们说内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程,上下文切换1)保存当前进程的上下文,2)恢复某个先前被抢占的进程被保存的上下文,3)将控制传递给这个新恢复的进程。
用户态和内核态:处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。图51展示了一对进程A和B之间上下文切换的实例。与这个例子同理,进程hello初始运行在用户模式中,直到它通过执行系统调用exit,sleep和getchar时便陷入内核。内核中的陷阱处理程序完成对系统函数的调用。之后,内核执行上下文切换,将控制返回给hello紧随系统调用之后的那条语句。
图51 进程上下文切换的剖析
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行截屏,说明异常与信号的处理。
异常:
- 中断:在hello程序执行的过程中可能会出现外部I/O设备引起的异常,造成中断;
- 陷阱:在hello程序调用并执行sleep函数的时候会出现陷阱;
- 故障:执行hello程序的时候,可能会发生缺页故障;
- 终止:hello程序执行过程中可能出现DRAM或SRAM位损坏的奇偶错误,引起终止。
信号:SIGINT、SIGTSTP、SIGKILL等等。具体如下:
- 不停乱按没有影响。如图52所示。
图52 不停乱按
- 输入ctrl+c导致内核发送一个SIGINT信号到前台进程组中的每个作业,SIGINT默认情况是终止前台作业。如图53。
图53 ctrl+c
- 输入ctrl+z导致内核发送一个SIGTSTP信号到前台进程组中的每个作业,SIGTSTP默认情况是停止(挂起)前台作业。
图54 ps
如图54,使用ps命令查看当前运行的进程。可以得到hello的PID为4382,并且没有终止。
图55 ctrl+z
如图55,输入jobs命令显示当前shell环境中已启动的作业状态。可以看到hello的作业号为1。再输入fg %1指令,将hello调回前台运行。
- 然后输入kill %1指令,发送SIGKILL信号将hello进程杀死,此时输入jobs发现hello进程已经消失了。如图56所示。
图56 kill
- 输入pstree命令,可以得到所有进程的树状图。如图57所示。
图57 pstree
6.7本章小结
本章介绍了进程的概念与作用、shell的功能与处理流程,分析了hello的fork进程创建过程、execve过程,从进程调度的过程和用户态与核心态转换阐述了hello进程的执行过程。本章还介绍了hello运行过程中可能出现的异常和信号处理。此刻,我们目睹了hello的完美谢幕,它挥了挥手,不带走一片云彩。
结论
本文利用多种工具观察了hello的一生:从最开始的hello.c源程序,经过预处理器预处理得到被修改的源程序hello.i,再由编译器翻译为hello.s,此时hello程序已经摇身一变,变为更接近计算机底层的汇编语言。接着,汇编器将hello.s汇编生成二进制可重定位目标程序hello.o,此时,hello已经逐渐让程序员读不懂了,但是却更能让机器读懂了。最后,链接器将hello.o与各个库函数合并、重定位,结果就得到可执行目标程序hello,它可以被加载到内存中,由系统执行。当我们在shell中输入命令,它便由Program成为Process,在Hardware(CPU/RAM/IO)上驰骋,在键盘、主板、显卡、屏幕间游刃有余。运行结束后,它被父进程回收,它的一生从有到无,又从无到有。源程序中看似简单的代码,可以追根溯源到系统的最底层,我们这时才感叹:原来hello的一生是如此丰富多彩!Hello“人生”中的每一步都凝聚着一代又一代计算机人智慧的结晶,让人震撼,又回味无穷。
附件
hello.c:源程序(文本)
hello.i:hello.c预处理生成的被修改的源程序(文本)
hello.s:hello.i编译生成的汇编程序(文本)
hello.o:hello.s汇编生成的可重定位目标程序(二进制)
hello:hello.o链接生成的可执行目标程序(二进制)
hello.elf:hello.o的ELF文件
hello2.elf:hello的ELF文件
hello.txt:hello.o的反汇编文件
hello2.txt:hello的反汇编文件