计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术
学 号 2021
班 级 2103
学 生 高
指 导 教 师 刘
计算机科学与技术学院
2022年5月
经过一学期对计算机系统的课程,对《深入理解计算机系统》一书的前八章进行了系统的学习,并且通过四次实验巩固了学习内容,提高了实践能力。通过本文,将借助对hello程序在Linux下生命周期的分解,回顾课程知识,深化学习内容,增强知识连贯性,加强实践能力。
关键词:计算机系统;Ubuntu;程序的生命周期;操作系统;编译;链接;进程
目 录
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
1. P2P(From Program to Process)
程序员通过编辑器编写高级语言程序得到.c文件,经过cpp预处理得到.i文件,经过ccl编译器编译得到.s汇编语言文件,利用汇编器(as)获得重定位的.o目标文件,最后经链接器(ld)与库函数链接得到可执行文件hello。Linux>./hello执行此文件,shell会调用fork函数为其fork产生子进程,再调用execve函数加载进程。于是hello从程序转变为了进程。
2.020(From Zero-0 to Zero-0)
子进程调用execve加载hello,创建新的区域结构以提供给hello的代码、数据、bss和栈区域,映射虚拟内存,载入物理内存,然后进入程序入口处开始执行,然后进入main函数,CPU为hello分配时间片执行逻辑控制流。
为让hello程序能够调用硬件进行从键盘读入字符,操作系统将I/O设备都抽象为文件,进而实现向屏幕输入输出。hello执行完成后操作系统回收hello进程,内核从系统中删除hello所有相关数据,hello一生结束。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件:
CPU:AMD Ryzen 7 5800H with Radeon Graphics
显卡:NVIDIA GeForce RTX 3060 Laptop GPU 6 GB
硬件平台:Intel X86-64
软件:Linux: Vmware虚拟机; Ubuntu 18.04 LTS 64位/优麒麟 64位
开发与调试工具:gcc,as,ld,vim,edb,readelf
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
文件名 文件功能
hello.c 源程序
hello.i 预处理之后得到的文本文件
hello.s 编译得到的汇编文件
hello.o 汇编得到的可重定位目标文件
hello 链接得到的可执行目标文件
hello1.elf hello.o经readelf输出的elf文件
hello2.elf hello经readelf输出的elf文件
说明:由于hello(1).c出现bash:未预期的符号’(’附近有语法错误,因此改名为hello.c
1.4 本章小结
本章对hello程序进行了概括,介绍了P2P,020过程,列出了本次作业的软硬件环境及开发工具以及从hello.c到hello的中间文件。
第2章 预处理
2.1 预处理的概念与作用
概念:
在程序设计中,预处理是在编译之前调用预处理器进行的处理。一般指的是在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程,预处理器(cpp)会展开以#起始的行,试图解释为预处理指令(preprocessing directive) ,其中ISO C/C++要求支持的包括#if/#ifdef/#ifndef/#else/#elif/#endif(条件编译)、#define(宏定义)、#include(源文件包含)、#line(行控制)、#error(错误指令)、#pragma(和实现相关的杂注)以及单独的#(空指令) 。预处理指令一般被用来使源代码在不同的执行环境中被方便的修改或者编译。
作用:
1. 预处理可以扩展源代码,插入所有用#include命令指定的文件。
2. 扩展所有用#define声明指定的宏,又称宏展开。
3. 根据#if以及#endif和#ifdef以及#ifndef后面的条件决定需要编译的代码
2.2在Ubuntu下预处理的命令
图一.输入预处理命令
输入命令:gcc -E hello.c -o hello.i
2.3 Hello的预处理结果解析
预处理得到了3105行的.i文件
图二.头文件的宏展开
图三.hello.i中的源代码
可以发现,hello.i相较hello.c已经扩展了三千余行,原来的代码出现在hello.i的最后,main函数主体内容没有什么变化,注释部分删除。头文件stdio.h unistd.h stdlib.h展开成为三个头文件的源码,且对 stdio 中的define 宏定义递归展开,此外,预编译程序可识别一些特殊的符号,预编译程序对在源程序中出现的这些串将用合适的值进行替换。
2.4 本章小结
本章主要介绍了预处理(包括头文件的展开、宏替换、去掉注释、条件编译)的概念和功能,同时在Ubuntu上输入命令进行预处理并作结果解析。
第3章 编译
3.1 编译的概念与作用
概念:
编译过程是整个程序构建的核心部分,编译成功,会将源代码由文本形式转换成机器语言汇编程序。编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析以及优化后生成相应的汇编代码文件,将文本文件 hello.i 翻译成汇编程序hello.s。
作用:
把源程序(高级语言)翻译成汇编语言或机器语言。其包括以下几个步骤:
1、词法分析:词法分析是使用一种叫做lex的程序实现词法扫描,它会按照用户之前描述好的词法规则将输入的字符串分割成一个个记号。产生的记号一般分为:关键字、标识符、字面量(包含数字、字符串等)和特殊符号(运算符、等号等),然后他们放到对应的表中。方法分为两种:自上而下分析法和自下而上分析法。
2、语法分析:语法分析器根据用户给定的语法规则,将词法分析产生的记号序列进行解析,然后将它们构成一棵语法树,即在词法分析的基础上将单词序列组合成各类语法短语,如“程序”,“语句”,“表达式”等等。对于不同的语言,只是其语法规则不一样。
3、语义分析:即静态语法检查,对结构上正确的源程序进行上下文有关性质的审查,进行类型审查。语义分析是审查源程序有无语义错误,为代码生成阶段收集类型信息。
4、中间代码:源程序的一种内部表示,或称中间语言。中间代码的作用是可使编译程序的结构在逻辑上更为简单明确,特别是可使目标代码的优化比较容易实现中间代码。
5、目标代码生成与优化:代码生成器将中间代码转成机器代码,这个过程是依赖于目标机器的,因为不同的机器有着不同的字长、寄存器、数据类型等。最后目标代码优化器对目标代码进行优化,比如选择合适的寻址方式、使用唯一来代替乘除法、删除出多余的指令等,即对程序进行多种等价变换,使得从变换后的程序出发,能生成更有效的目标代码
3.2 在Ubuntu下编译的命令
图四.输入编译命令
输入命令:gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
3.3.1汇编指令介绍
图五.汇编指令
.file:声明源文件
.text:代码节
.section:
.rodata:只读代码段
.align:数据或者指令的地址对齐方式
.string:声明一个字符串(.LC0,.LC1)
.global:声明全局变量(main)
.type:声明一个符号是数据类型还是函数类型
3.3.2数据
1.常量
图六.常量4
图七.常量4的直接引用
常量4被储存在寄存器%rbq中,也就是在.text中,作为指令的一部分
- .字符串
图八.字符串
字符串作为printf函数的参数,保存在只读数据段中。
- 变量
全局变量:储存在.data或.bss节中。初始化的全局变量储存在.data节,它的初始化不需要汇编语句,而是直接完成的。
局部变量:在main函数中定义了一个局部变量i。
图九.局部变量i的赋值
编译器编译时将局部变量i放在堆栈中,如图九所示,本程序中局部变量i存放在栈-4(%rbp)中。
3.3.3赋值
如上图九,编译器通过mov语句将局部变量i赋值为0。
3.3.4算术操作
图十.实现i++
在每次循环执行内容结束后,利用addl算术操作对i进行+1。
3.3.5关系跳转与控制转移
图十一.源程序
图十二.if判断语句的实现
图十三.for语句的实现
汇编语言中首先设置条件码,然后根据条件码来进行控制转移。
在图十一,在13行和17行分别出现了if(argc!=4)和for(i=0;i<8;i++)两处关系操作,前者通过图十二中的cmpl指令设置的条件码来判断是否需要跳转到分支中,后者通过图十三中的cmpl指令设置的条件码(即如果小于等于8就跳转)来判断是否跳转到指定地址。
3.3.6数组/指针/结构体
主函数main的参数中有指针数组char *argv[],数组的每个元素都是一个指向字符类型的指针。
图十四.main函数定义
图十五.argc和argv的存储位置
argc存储在%edi中,argv存储在%esi中
图十六.对argv数组成员取值
用movq指令获取argv[1]和argv[2]的地址
3.3.7函数操作
X86-64中,过程调用传递参数规则:第1~6个参数一次储存在%rdi、%rsi、%rdx、%rcx、%r8、%r9这六个寄存器中,剩下的参数保存在栈当中。
main函数:
参数传递:传入参数argc和argv[],分别用寄存器%rdi和%rsi存储。
局部变量:创建了一个int类型的局部变量i。
函数调用:被系统启动函数调用。
函数返回:设置%eax为0并且返回,对应return 0 。
图十七.printf函数调用
printf函数:
参数传递:call puts时只传入了字符串参数首地址;for循环中call printf时传入了 argv[1]和argc[2]的地址。
函数调用:if判断满足条件后调用,与for循环中被调用。
exit函数:
参数传递:传入的参数为1,再执行退出命令。
函数调用:if判断条件满足后被调用。
sleep函数:
参数传递:传入参数atoi(argv[3])。
函数调用:for循环下被调用,call sleep。
getchar函数:
函数调用:在main中被调用,call getchar。
3.4 本章小结
本章内容围绕编译展开,介绍了编译的概念以及作用,说明了ubuntu下的编译命令,对hello.i程序进行编译获得了hello.s程序,并对hello.s程序进行了解析,分析了其数据、操作、函数调用等,加深了对汇编语言的理解。
第4章 汇编
4.1 汇编的概念与作用
汇编器(as)将.s 汇编程序翻译成等价的二进制机器语言指令,汇编输入的是汇编语言,输出的是机器语言。此机器语言是可重定位目标程序。
作用:
汇编就是将高级语言转化为机器可直接识别执行的代码文件的过程,汇编器将.s 汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在.o 目标文件中,.o 文件是一个二进制文件,它包含程序的指令编码。
4.2 在Ubuntu下汇编的命令
图十八.输入汇编命令
输入命令:gcc -C hello.s -o hello.o
4.3 可重定位目标elf格式
1.ELF Header
图十九.ELF Header
ELF头:以 16B 的序列 Magic 开始,Magic 描述了生成该文件的系统的字的大小和字节顺序,余下部分指明了类别,数据,版本,OS/ABI,ABI,ELF头的大小、目标文件的类型、机器类型、字节头部表的文件偏移,以及节头部表中条目的大小和数量等信息。
- Section Headers
图二十.1.Section Headers
图二十.2.Section Headers
节头表:节头表包括节名称,节的类型,节的属性(读写权限),节在ELF文件中所占的长度以及节的对齐方式和偏移量。
- 符号表.symtab
图二十一..symtab
符号表.symtab:存放在程序中定义和引用的函数和全局变量的信息,但它不包含局部变量的条目。
- 重定位节
图二十二.重定位节
重定位节:,包含.text 节和.eh_frame中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。
4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
图二十三.hello.o反汇编
hello.o的反汇编相较于hello.s多了机器代码。机器指令由操作码和操作数构成,每一条汇编语言操作码都可以用机器二进制数据来表示,进而可以将所有的汇编语言(操作码和操作数)和二进制机器语言建立一一映射的关系。
分支转移:hello.s文件中分支转移使用段名称进行跳转,hello.o文件中通过地址进行直接跳转。
函数调用:因为 hello.c 中调用的函数都是共享库中的函数,最终都需要通过动态链接器才能确定函数的运行时执行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其 call 指令后的相对地址设置为全 0,然后在.rela.text 节中为其添加重定位条目,等待静态链接的进一步确定。所以在.s 文件中,函数调用之后直接跟着函数名称,而在反汇编程序中,call 的目标地址是当前下一条指令。
(3)全局变量:hello.s文件中,全局变量通过语句“段地址+%rip”完成,在于hello.o的反汇编后生成的代码中是通过“0+%rip”实现,因为.rodata节中的数据是在运行时确定的,也需要重定位,现在填0占位,并为其在.rela.text节中添加重定位条目。
4.5 本章小结
本章围绕汇编展开,阐述了汇编的概念以及作用,指出了在ubuntu下的汇编指令,利用指令查看了hello.o的可重定位目标文件的格式,体分析elf格式的可重定位目标文件的各个部分(如ELF Header等等)。最后使用反汇编查看了hello.o经过反汇编后生成的代码并与hello.s进行了对比分析,更加深入地理解了汇编。
第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.3 可执行目标文件hello的格式
ELF Header:
图二十五.ELF Header
其与.o文件的差别仅仅是类型(可执行文件)与节数(25)不同
Section Headers:
图二十六.Section Headers
各节的基本信息均在节头表(描述目标文件的节)中进行了声明。节头表(包括名称,大小,类型,全体大小,地址,旗标,偏移量,对齐等信息)。
符号表.dynsym和.symtab
图二十七.符号表.dynsym和.symtab
重定位节:.rela.dyn和.rela.plt
图二十八.重定位节:.rela.dyn和.rela.plt
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
1. 在data dump中查看加载到虚拟地址情况:
图二十九.data dump
- 各节位置和名称:地址和名称与5.3相对应
图三十.各节位置和名称
- 利用节头表可以去到各个节,比如下图为.rodata节:
图三十一..rodata节
5.5 链接的重定位过程分析
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
图三十二.Hello的部分反汇编代码
1、对比可以发现hello反汇编的代码有确定的虚拟空间绝对地址并在后面标出执行函数的地址和函数名,也就是说已经完成了重定位,而hello.o反汇编代码中代码的虚拟地址均为0,未完成重定位的过程,0只是起到了占位的作用。由此,hello.o文件中有重定位条目而hello文件中没有。
2、hello相较于hello.o增加了新的节(.init和.plt)和函数(如exit、printf等库函数)。
hello重定位过程:
1、链接器将所有相同类型的节合并成同一类型的新的聚合节。然后将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。这时,程序中的每条指令和全局变量都有唯一的运行时内存地址。
2、链接器利用可重定位目标模块中称为重定位条目的数据结构,修改节代码和数据节中对每个符号的引用,使得它们指向正确的运行时地址。
3、重定位条目:汇编器遇到对最终位置未知的目标引用时,就会生成一个重定位条目,告诉链接器如何修改这个引用。代码的重定位条目放在.rel.text中,已初始化数据的重定位条目放在.rel.data中。
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
ld-2.27.so!_start@0x7f99:35f0e090
ld-2.27.so!_dl_start_user@0x7f99:35f0e098
hello!_start@0x400550
hello!_main@0x400582
5.7 Hello的动态链接分析
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。虽然动态链接把链接过程推迟到了程序运行时,但是在形成可执行文件时,还是需要用到动态链接库。
在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。GNU编译系统使用延迟绑定(lazybinding),将过程地址的绑定推迟到第一次调用该过程时,以此防止运行时修改调用模块的代码段。
动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。
PLT:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。
GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。
如果一个目标模块调用定义在共享库中的任何函数,那么就有自己的GOT和PLT。前者是数据段的一部分,后者为代码段的一部分。PLT是一个数组,其中每个条目是16字节代码。每个库函数都有自己的PLT条目,PLT[0]是一个特殊的条目,跳转到动态链接器中。从PLT[2]开始的条目调用用户代码调用的函数。GOT同样是一个数组,每个条目是8字节的地址,和PLT联合使用时,GOT[2]是动态链接在ld-linux.so模块的入口点,其余条目对应于被调用的函数,在运行时被解析。每个条目都有匹配的PLT条目。当某个动态链接函数第一次被调用时先进入对应的PLT条目例如PLT[2],然后PLT指令跳转到对应的GOT条目中例如GOT[4],其内容是PLT[2]的下一条指令。然后将函数的ID压入栈中后跳转到PLT[0]。PLT[0]通过GOT[1]将动态链接库的一个参数压入栈中,再通过GOT[2]间接跳转进动态链接器中。动态链接器使用两个栈条目来确定函数的运行时位置,用这个地址重写GOT[4],然后再次调用函数。经过上述操作,再次调用时PLT[2]会直接跳转通过GOT[4]跳转到函数而不是PLT[2]的下一条地址。
图三十三..GOT起始地址
由图三十三知GOT起始表位置为0x601000
图三十四.edb执行init之前的地址
在调用dl_init之前0x601008后的16个字节均为0,对于每一条PIC函数调用,调用的目标地址都实际指向PLT 中的代码逻辑,GOT存放的是PLT中函数调用指令的下一条指令地址。
图三十五.edb执行init之后的地址
在之后的函数调用时,首先跳转到PLT执行.plt中逻辑,第一次访问跳转时,GOT 地址为下一条指令,将函数序号压栈,然后跳转到PLT[0],在 PLT[0]中将重 定位表地址压栈,然后访问动态链接器,在动态链接器中使用函数序号和重定位 表确定函数运行时地址,重写 GOT,再将控制传递给目标函数。之后如果对同样函数调用,第一次访问跳转直接跳转到目标函数。
5.8 本章小结
本章围绕链接展开,阐述了链接的概念以及作用,详细阐述了hello.o是怎么链接成为一个可执行目标文件的过程,并对hello的ELF格式各个部分进行了分析,还使用edb加载hello,查看本了进程的虚拟地址空间各段信息,并通过反汇编hello文件,将其与hello.o反汇编文件进行了对比,详细了解了重定位过程;此外,还遍历了整个hello的执行过程,在最后对hello进行了动态链接分析。极大加深了对链接的理解。
第6章 hello进程管理
6.1 进程的概念与作用
狭义定义:进程就是一个执行中程序的实例。系统中每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态所组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
进程的概念主要有两点:第一,进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。第二,进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时(操作系统执行之),它才能成为一个活动的实体,我们称其为进程。
进程是操作系统中最基本、重要的概念。是多道程序系统出现后,为了刻画系统内部出现的动态情况,描述系统内部各道程序的活动规律引进的一个概念,所有多道程序设计操作系统都建立在进程的基础上。
作用:
进程为应用程序提供两个抽象,一是独立的逻辑控制流,一个是私有的地址空间。提高CPU的执行效率,减少因为程序等待带来的CPU空转以及其它计算机软硬件资源浪费。在现代计算机中,进程为用户提供了以下假象:我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存,处理器好像是无间断的执行 我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
作用:Shell是用户与操作系统之间完成交互式操作的一个接口程序,它为用户提供简化了的操作,它代表用户运行其他程序,解释命令,连接用户和操作系统以及内核。
处理流程:
1、读取用户由键盘输入的命令行。
2、分析命令,以命令名作为文件名,并将其它参数改造为系统调用execve( )内部处理所要求的形式。
3、检查第一个(首个、第0个)命令行参数是否是一个内置的shell命令,是则立即执行,如果不是,则用fork创建子进程。
4、终端进程本身调用waitpid()来等待子进程完成(如果是后台命令,则不等待)。当子进程运行时调用execve(),子进程根据文件名到目录中查找有关文件,调入内存,执行这个程序。
5、如果命令末尾有&,则终端进程不用执行系统调用waitpid(),立即发提示符,让用户输入下一条命令;否则终端进程会一直等待,当子进程完成工作后,向父进程报告,此时中断进程醒来,作必要的判别工作后,终端发出命令提示符,重复上述处理过程。
6.3 Hello的fork进程创建过程
终端程序通过调用fork()函数创建一个子进程,子进程得到与父进程完全相同但是独立的一个副本,包括代码段、段、数据段、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,父进程和子进程最大的不同时他们的PID是不同的。父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的 逻辑控制流的指令。在子进程执行期间,父进程默认选项是显示等待子进程的完成。fork函数只会被调用一次,但会返回两次,在父进程中,fork返回子进程的PID,在子进程中,fork返回0。
6.4 Hello的execve过程
Execve函数加载并运行可执行目标文件hello,且包含相对应的一个带参数的列表argv和环境变量的列表exenvp,,只有当出现错误时,例如找不到hello文件时,execve才会返回-1到调用程序,execve调用成功则不会产生返回。
1. 为子进程调用函数fork之后,子进程调用execve函数(传入命令行参数)在当前进程的上下文中加载并运行一个新程序hello。
2. 为执行hello程序加载器、删除子进程现有的虚拟内存段,execve 调用驻留在内存中的、被称为启动加载器的操作系统代码,并创建一组新的代码、数据、堆和栈段。
3. 新的栈和堆段被初始化为零,通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容。最后加载器设置PC指向_start 地址,_start 最终调用 hello中的 main 函数。
4. 除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制。直到 CPU 引用一个被映射的虚拟页时才会进行复制,这时,操作系统利用它的页面调度机制自动将页面从磁盘传送到内存。
6.5 Hello的进程执行
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
Linux 系统中的每个程序都运行在一个进程上下文中,有自己的虚拟地址空间。当shell 运行一个程序时,父进程生成一个子进程,它是父进程的一个复制。子进程通过execve 函数系统调用启动加载器。加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零。通过将虚拟地址空间中的页映射到可执行文件的页大小的片(chunk), 新的代码和数据段袚初始化为可执行文件的内容。最后,加载器跳转到_start地址,它最终会调用应用程序的main 函数。进程的执行并不一定是完整、连续地执行完成,每一个进程在执行时都会有其对应的时间片。
进程提供给应用程序的抽象:
(1) 一个独立的逻辑控制流,它提供一个假象,好像我们的进程独占的使用处理器。
(2) 一个私有的地址空间,它提供一个假象,好像我们的程序独占的使用CPU内存。
- 逻辑控制流:一系列程序计数器 PC 的值的序列叫做逻辑控制流,在同一个处理器核心中,每个进程执行它的流的一部分后被抢占(暂时挂起),然后轮到其他进程,进程轮流使用处理器。
- 并发流:一个逻辑流的执行在时间上与另一个流重叠,称为并发流,这两个流被称为并发地运行。多个流并发地执行的一般现象被称为并发。
- 时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
- 上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态。它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。
- 私有地址空间:进程为每个流都提供一种假象,好像它是独占的使用系统地址空间。一般而言,和这个空间中某个地址相关联的那个内存字节是不能被其他进程读或者写的,在这个意义上,这个地址空间是私有的。
- 用户模式和内核模式:处理器通常使用一个寄存器描述了进程当前享有的特权,对两种模式区分。设置模式位时,进程处于内核模式,该进程可以访问系统中的任何内存位置,可以执行指令集中的任何命令;当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据。
- 上下文切换:当内核选择一个新的进程运行时,则内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程:
- 保存以前进程的上下文;
- 恢复新恢复进程被保存的上下文;
- 将控制传递给这 个新恢复的进程 ,来完成上下文切换。
下面来看hello的进程执行。最初hello运行在用户模式下,输出edb --run ./hello 2021112655 gzb 3,然后hello调用sleep函数之后进程陷入内核模式,内核处理休眠请求主动释放当前进程,并将hello进程从运行队列中移出加入等待队列,定时器开始计时,内核进行上下文切换将当前进程的控制权交给其他进程,当定时器发送一个中断信号,此时进入内核状态执行中断处理,将hello进程从等待队列中移出重新加入到运行队列,hello进程继续进行自己的控制逻辑流。
图三十六.调用edb及程序执行结果
图三十七.sleep进程上下文切换
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
异常就是控制流中的突变,用来响应处理器状态中的某些变化,它可以分为四类:
1、中断:中断是处理来自I/O设备的信号的结果,是异步发生的,总是返回到下一条指令。
2、陷阱:它是有意的异常,是执行一条指令的结果,它是同步发生的,总是返回到下一条指令。
3、故障:是由错误情况引起,它可能能够被故障处理程序修正。它是同步发生的,并且可能返回到当前指令。
4、终止:是不可恢复的致命错误造成的结果,通常是一些硬件错误。它也是同步的,且不会返回。
信号是更高层的软件形式的异常,它就是一条小消息,通知进程系统中发生了一个某种类型的事件。它提供了一种机制,通知用户进程发生了异常。Linux信号主要有以下几种:
图三十八.Linux信号
键入ctrl-z:
图三十九.键入ctrl-z
ps查看后台:
图四十.ps查看后台
jobs查看后台job号:
图四十一.jobs查看后台job号
fg调到前台:
图四十二.fg调到前台
由上可知:键入ctrl-z的默认结果是挂起前台的作业,hello进程并没有回收,而是运行在后台下。
图四十三.键入crtl-c及后续验证
由图四十三可知:在键盘上输入ctrl-c会导致内核发送一个SIGINT信号到前台进程组的每个进程,默认情况是终止前台作业。
kill指令:挂起的进程被终止,在ps中无法查到到其PID。
图四十四.kill命令
6.7本章小结
本章围绕hello的进程管理展开,简述了进程的概念与作用、shell-bash的作用与处理流程,着重分析了调用 fork 创建新进程,调用 execve函数执行hello,hello的进程执行,以及hello的异常与信号处理,加深了对异常与信号处理的理解。
结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
hello的一生经历了被编写成代码、预处理、编译、汇编、链接、运行、创建子进程、加载、执行指令、访问内存、动态内存分配、发送信号、终止。
hello程序虽然简单,但其经历的一生丰富而精彩,极大加深了我们对计算机系统的理解。
预处理:从hello.c生成.i文件,将hello.c中调用的外部库展开合并到.i中
编译:由hello.i生成hello.s汇编文件
汇编:将hello.s文件翻译为机器语言指令,并打包成可重定位目标程序hello.o
链接:将hello.o可重定位目标文件和动态链接库链接成可执行目标程序hello
运行:在shell中输入命令
创建子进程:shell用fork为hello程序创建子进程
加载:shell调用execve函数,将hello程序加载到该子进程,映射虚拟内存
执行指令:CPU为进程分配时间片,加载器将计数器预置在程序入口点,则hello可以顺序执行自己的逻辑控制流
访问内存:MMU将虚拟内存地址映射成物理内存地址,CPU通过其来访问
动态内存分配:根据需要申请动态内存
信号:shell的信号处理函数可以接受程序的异常和用户的请求
终止:执行完成后父进程回收子进程,内核删除为该进程创建的数据结构
计算机系统的设计与实现非常精妙,应用了大量数学理论,一些结构设计也体现了许多哲学思想,仅仅是简略的学习,就已让我感到无比的惊讶并产生了浓厚的兴趣。然而,如此系统一定还有前进的空间,未来的计算机系统会是哪种样貌,我无比期待。
文件名 文件功能
hello.c 源程序
hello.i 预处理之后得到的文本文件
hello.s 编译得到的汇编文件
hello.o 汇编得到的可重定位目标文件
hello 链接得到的可执行目标文件
hello1.elf hello.o经readelf输出的elf文件
hello2.elf hello经readelf输出的elf文件