程序人生-Hello’s P2P

程序人生-Hello’s P2P

目录

摘 要

本文通过对hello.c程序的生命周期进行探讨,从预处理、编译、汇编、链接、生成可执行文件几个方面了解计算机对源程序的处理,之后通过进程管理、存储管理以及IO管理,分析了hello是如何在计算机系统中执行的。

关键词:计算机系统;P2P;020;hello;

第一章 概述

1.1 Hello简介

首先我们来介绍p2p的概念,p2p是from program to process,这是一个从高级语言的.c文件到最后产生可执行文件并产生进程的过程。首先从hello.c的高级语言文件由编译器预处理之后得到hello.i的文本文件,之后计算机将其编译成hello.s的汇编文件,之后通过将指令打包变成可重定位的hello.o的可重定位目标文件,再经过链接器与库函数链接得到可执行文件hello,系统会执行该文件并调用fork为其产生子进程,最后调用execve加载进程。
之后我们来介绍020的概念,020是from zero to zero,即execve将程序加载到新创建的进程后,通过虚拟内存映射将程序从磁盘载入物理内存中执行,然后载入物理内存,进入 main函数执行hello目标代码(功能),CPU为hello程序执行逻辑控制流。shell父进程调用waitpid,待hello运行结束后回收hello子进程。这是一个从0到1到0(从无到有到无)的过程。

1.2 环境与工具

(1)硬件环境:X64 CPU;2GHz;4GRAM;256Disk
(2)软件环境:Windows11 64位;Vmware 10;Ubuntu 20.04 LTS 64位
(3)使用工具:Visual studio 2022 64位;Objdump;Edb.

1.3 中间结果

hello.c: 原C语言文件
hello.i:hello.c经预处理生成的文本文件。
hello.s:hello.i经编译生成的汇编语言文件。
hello.o:hello.s经汇编生成的可重定位目标文件。
hello.out:hello.o经链接生成的可执行目标文件。

本章小结

本章主要介绍了hello的P2P,020过程,以及进行实验时的软硬件环境及开发与调试工具和在本论文中生成的中间结果文件。

第二章 预处理

2.1 预处理的概念与作用

预处理的概念:预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。这个过程并不对程序的源代码进行解析,但它把源代码分割或处理成为特定的单位。
预处理的作用:通过预处理生成的预处理文件(hello.i)是一个可读但不包含任何宏定义(即以#开头的语句)的文件。其具体作用表现为:

  1. 删除“#define”并展开所定义的宏。
  2. 插入头文件到“#include”处,可以递归方式进行处理。
  3. 处理所有条件包含指令,如“#if”、“#ifdef”、“#endif”等。
  4. 添加行号和文件名标识,以便编译器产生调试用的行号信息。
  5. 删除所有的注释“//”和“/* */”。
  6. 保留所有的#pragma编译指令。

2.2 在Ubuntu下预处理的命令

预处理的命令如下:

gcc hello.c -E -o hello.i

在这里插入图片描述
图2.1 预处理指令

2.3 Hello的预处理结果解析

在这里插入图片描述
图2.2 预处理文件的部分展示

以上为hello.i的部分内容,我们可以发现hello.i文件有3000多行,文件大小大大增加,经过对比后我们发现,预处理对原文件中的宏进行了宏展开,头文件中的内容被包含进该文件中。例如声明函数、定义结构体、定义变量、定义宏等内容,而hello.i中main函数与源程序中main函数的部分相同,没有发生改变。最后hello.c文件变为hello.i文件。

2.4 本章小结

本章介绍了预处理的概念与作用,并且通过指令得到了hello.c文件的hello.i形式,即预处理之后的结果。

第三章 编译

3.1 编译的概念与作用

编译的概念:编译是将已经预处理好的C语言源代码翻译成汇编语言的过程,它以高级程序设计语言书写的源程序作为输入,而以汇编语言或机器语言表示的目标程序作为输出。
编译的作用:编译即是将预处理晚点程序编译成机器语言,也就是让计算机阅读读懂的语言,从而使得计算机能运行之前的高级语言程序,另外编译程序还具备语法检查、调试措施、修改手段、覆盖处理、目标程序优化以及不同语言合用等重要功能。
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序

3.2 在Ubuntu下编译的命令

编译的命令:

gcc hello.i -S -o hello.s

在这里插入图片描述
图3.1 编译的指令

3.3 Hello的编译结果解析

在这里插入图片描述
在这里插入图片描述
图3.2 汇编文件

下面我们来分析上述编译文件的具体内容。

3.3.1 汇编文件指令

.file “hello.c” :源文件名
.text :代码段

.section .rodata :下面是.rodata节
.align 8 :8字节对齐方式
.LC0: .string "\347\224\250\346\263\225: Hello \345\255\246\345\217\267 \345\247\223\345\220\215 \347\247\222\346\225\260\357\274\201"是“用法:Hello 学号 姓名 秒数!”的对应编码
.LC1: .string "Hello %s %s\n"是输入字符串
.text:是代码段
.globl main:是全局变量
.type main, @function: 是main函数

3.3.2 数据

在hello.s中出现的数据类型有全局变量,局部变量,指针数组与字符串。

  1. 常量
    .LC0: .string "\347\224\250\346\263\225: Hello \345\255\246\345\217\267 \345\247\223\345\220\215 \347\247\222\346\225\260\357\274\201"是“用法:Hello 学号 姓名 秒数!”的对应编码
    .LC1: .string "Hello %s %s\n"是输入字符串
    二者均为printf函数的参数,存放再内存中的.rodata节常量区中。
  2. 变量
    在hello.c中我们可以看到有3个变量,一个是i,一个是int argc,一个是char *argv[],其中i被存放在了-4(%rbp)的位置,因为在.L2段中将-4(%rbp)的位置中的数赋值为0,与c语言文件相吻合。此外,argc被存放在寄存器%edi中,指针数组argv[]被存放在%rsi中,这可以从编译文件的22与23行中得到验证。因此,argc被存储在了-20(%rbp)的位置,argv被存储在了-32(%rbp)的位置。
3.3.3 赋值

在hello.s文件中,-4(%rbp)在第31行被赋值为0,movl指令的意思是将前者的数赋给后者,而l是双字型的整数数据。

3.3.4 类型转换

在hello.c文件中atoi(argv[3])为类型转换,即将字符串型转换为整数,在hello.s中即在第48行call atoi@PLT

3.3.5 算术操作

在hello.c中涉及的算术操作为i++,在hello.s中实现指令为第51行addl $1, -4(%rbp),之后不断循环即可

3.3.6 关系操作

在hello.c中有两处关系操作。
(1) argc!=4 此语句用于判断变量argc是否等于4,在hello.s中该语句被编译为cmpl $4, -20(%rbp)。比较完成后会设置条件码,通过条件码判断跳转位置。
(2) i<8 该语句是作为之后循环的条件,即i<8时进入循环,在hello.s中该语句被编译为cmpl $7, -4(%rbp)。通过比较完成后的条件码确定跳转位置。

3.3.7 数组

在hello.c中的数组是argv[],此数组为指针数组,因此64位系统虚拟地址为8个字节,所以需要数组首地址加16(0x10)得到储存argv[2]的地址,首地址加8(0x8)得到储存argv[1]的地址。

3.3.8 控制转移

(1)if的条件语句,在hello.c中为判断argc是否等于4。若为4则执行if之后的语句,反之则不执行,对应的hello.s的代码第24,25行的cmpl与je。
(2)for的循环语句,在hello.c中是每次循环前判断i是否小于8,若满足则进入循环,反之则不进入。在hello.s中对应的代码为第53,54行的cmpl与jle。而进入循环后则执行第34至51行的.L4。

3.3.9 函数调用

函数调用的语句在hello.s即编译文件中为call <函数名>@PLT
(1)main函数
main函数的参数有aint argc与char *argv[]
(2)printf函数
hello.c中调用了两次printf函数,第一次参数是.LC0字段的字符串,而第二次的参数是.LC1的字符串以及argv[1]与argv[2]
(3)exit函数
此函数实现的是从main函数中退出,hello.c中是exit(1),表示为非正常退出
(4)atoi函数
atoi函数实现将字符串类型的数据转变成int类型的数据,参数为argv[3]。
(5)sleep函数
sleep函数实现程序休眠,参数为atoi(argv[3])(表示休眠秒数)
(6)getchar函数
getchar函数实现读取缓冲区字符。不需要传递参数,直接调用即可。

3.4 本章小结

本章主要介绍了编译的概念与作用,并且对hello.c的编译过程进行展示,在得出编译文件hello.s之后我们按照C语言的不同数据与操作类型,分析了源程序hello.c文件中的语句是怎样转化为hello.s文件中的语句的。其中数据类型包括数字常量、字符串常量和局部变量;操作类型包括赋值、类型转换、算术操作、关系操作、数组\指针\结构操作控制转移以及函数调用。

第四章 汇编

4.1 汇编的概念与作用

汇编的概念:汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程,将.s汇编程序翻译车工机器语言并将这些指令打包成可重定目标程序的格式存放在.o中。用汇编语言编写的程序,机器不能直接识别,要由一种程序将汇编语言翻译成机器语言,这种起翻译作用的程序叫汇编程序,汇编程序是系统软件中语言处理的系统软件。
汇编的作用:汇编过程将汇编代码转换为计算机能够理解并执行的二进制机器代码,这个二进制机器代码是程序在本机器上的机器语言的表示。

注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。

4.2 在Ubuntu下汇编的命令

汇编的命令如下:

gcc hello.s -c -o hello.o

在这里插入图片描述
图4.1 汇编的命令

4.3 可重定位目标elf格式

4.3.1 ELF头

用命令readelf -h hello.o查看。
ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。
在这里插入图片描述
图4.2 hello.o ELF头

上图为readelf -h hello.o查看ELF头的结果。由上图可知hello.o为64位文件,采用补码与小端序,是可重定位文件。

4.3.2 节头表

命令:

readelf -S hello.o

节头表描述了不同节的位置和大小,其中目标文件中每个节都有一个固定大小的条目。具体的描述包括节的名称、类型、地址和偏移量等。包含了文件中出现的各个节的语义,包括节的类型、位置和大小等信息。
在这里插入图片描述
图4.3 hello.o节头表

上图分析得出节头表有14项,除了第一项外对应于可重定位文件的每一节的相关信息,包括名字,类型,地址,在文件中的偏移量,大小,权限以及对齐方式。

4.3.3 符号表

命令:

readelf -s hello.o

符号表用于存放程序中定义的函数与全局变量的信息。
在这里插入图片描述
图4.4 hello.o符号表

上图展示了符号表中存放的函数名,全局变量名等,而每一个符号有其对应的值,大小,类型,名字等信息。

4.3.4 重定位节

命令:

readelf -r hello.o

重定位节是.text节中位置的列表,包含.text 节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。
在这里插入图片描述
图4.5 hello.o重定位节

上图展示了.rela.text节的内容,其中该节包括了很多项,offset是节的偏移,info包括symbol和type两个部分,symbol是标识被修改引用应该指向的符号,type是重定位的类型,Type是告知链接器应该如何修改新的应用,Addend是一个有符号常数,一些重定位要使用它对被修改引用的值做偏移调整,Name是重定向到的目标的名称。

4.4 Hello.o的结果解析

由于hello.o为机器语言我们无法阅读,因此我们选择将hello.o反汇编观察其程序。
在这里插入图片描述
图4.6 hello.o反汇编程序

通过对比反汇编得到的汇编代码和.i文件中的汇编代码,可得出如下结论:
(1)函数调用:hello.s中的函数调用只写了函数名称,hello.o的反汇编中则 是使用了当前指令的下一个字节(即下一条指令的地址)。
(2)分支转移部分:hello.s中对跳转指令使用.Lx这样的名称,而hello.o中
的反汇编中的跳转是直接使用跳转地址。
(3)立即数:hello.s中的立即数是十进制的,而hello.o的反汇编中则是十六进制的。
(4)汇编代码中有很多“.”开头的伪指令用来指导汇编器和链接器工作,而反汇编得到的代码中则没有。
(5)汇编中mov、push、sub等指令都有表示操作数大小的后缀,比如l\q等, 反汇编得到的代码中则不全有。

4.5 本章小结

在本章中,我们首先分析了汇编的含义与作用,并且利用指令分析了hello.o的elf文件格式信息,最后通过对不hello.o的反汇编代码从几个方面与hello.s进行比较,得出了二者的一些区别。

第五章 链接

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.1 链接的指令

5.3 可执行目标文件hello的格式

5.3.1 ELF头

在这里插入图片描述
图5.2 hello ELF头

由上图可见hello的ELF头中可以看出EXEC类型,即hello是可执行目标文件,且其入口地址不为0,则已重定位。并且可以看到hello文件中节头表的数目以及偏移量等信息。

5.3.2 节头表

在这里插入图片描述
图5.3 hello节头表

由上图可知hello的节头表数目有27个,以上为部分截图,并且每一个节都有实际地址,说明完成了重定位工作。

5.3.3 符号表

在这里插入图片描述
在这里插入图片描述
图5.4 hello 符号表

符号表是一个条目数组。这些条目包括目标位置的偏移量(Value),目标大小(Size),Type表示数据或者函数,Bind表示符号是local还是global的。

5.3.4 段头表

命令:

readelf -l hello

在这里插入图片描述
图5.5 hello 段头表

段头表描述了可执行目标文件的连续的片与连续的虚拟内存段之间的映射关系。从段头表中可以看到根据可执行目标文件的内容初始化为两个内存段,分别为只读内存段(代码段)和读写代码段(数据段)。

5.4 hello的虚拟地址空间

使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
首先我们通过./edb的指令打开edb并加载可执行程序hello,通过readelf查看hello的程序头,可以发现其中的虚拟地址在edb的Data Dump中都能找到相应的位置,且大小也相对应。
在这里插入图片描述
图5.6 edb中Data Dump关于程序头的内容

通过上图edb中的symbol窗口与5.3.2节头表中图片相对比,我们发现比如上图的.interp与前节头表的地址4002e0相对应,后续hash等也是一样。
在这里插入图片描述
图5.6 edb中Data Dump0x400000-0x401000

Data Dump是从地址0x400000开始,因为在该地址有ELF的标识。
在这里插入图片描述
图5.7 edb中Data Dump0x402000-0x403000

根据节头表信息可以找到各个节在内存中的位置,比如.rodata节的其实位置为0x402000,edb中结果如上图所示。

5.5 链接的重定位过程分析

命令:

objdump -d -r hello > hello1.txt

将得到hello文件的反汇编代码
在这里插入图片描述
图5.8 获得反汇编代码的指令

objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。

我们将hello的反汇编代码hello1.txt与hello2.txt(hello.o的反汇编代码)进行比较,可以得到如下几点不同。
(1) 代码数量增加,经过对比发现hello2.txt代码只有53行,而hello1.txt代码有192行。
(2) 在hello.o的反汇编代码中,仅有main函数,而在hello的反汇编代码中,即经过链接过程后,原来的C标准库中的内容均插入了代码中,每个函数都有各自分配的虚拟地址。

在这里插入图片描述
图5.9 hello1.txt的C库程序内容

如上图所示,在hello的反汇编文件中,出现了printf,exit,sleep等函数的内容。

(3) 在hello.o的反汇编文件中,main函数所有语句之前只有从0开始递增的地址,没有虚拟地址;而经过链接后,每个语句都被分配了虚拟地址。
在这里插入图片描述
图5.10 hello1.txt的语句虚拟地址

(4) 关于函数调用与跳转指令,在hello.o的反汇编文件中,调用函数的位置都用call加下一条指令地址来表示;而在hello的反汇编程序中,由于各函数已拥有了各自的虚拟地址,所以在call后加其虚拟地址来实现函数调用,并且跳转指令中,所有语句都有其各自的虚拟地址。
在这里插入图片描述
图5.11 hello1.txt的程序调用

在这里插入图片描述
图5.12 hello1.txt的语句的虚拟地址

(5)对于hello来说,链接器把hello中的符号定义都与一个虚拟内存位置关相关联,重定位了这些节,并在之后对符号的引用中把它们指向重定位后的地址。hello中每条指令都对应了一个虚拟地址,而且对每个函数,全局变量也都它关联到了一个虚拟地址,在函数调用,全局变量的引用,以及跳转等操作时都通过虚拟地址来进行,从而执行这些指令。

5.6 hello的执行流程

从加载hello到_start,到call main,以及程序终止的所有过程如下:
_dl_start 地址:0x7f5b162a0100
_dl_init 地址:0x7f5b162a0df0
_start 地址:0x4010f0
_libc_start_main 地址:0x403ff0
_cxa_atexit 地址:0x7f5b162a8a60
_libc_csu_init 地址:0x4011c0
_main 地址:0x401125
(若argc!=4则进入
puts 地址:0x401090
exit 地址:0x4010d0
此时输出窗口打印出“用法: Hello 学号 姓名 秒数!”,程序终止。)
print 地址:0x4010a0
sleep 地址:0x4010e0 (以上两个在循环体中执行8次)
我们在窗口打印8行“Hello 7203610120 xc”
getchar 地址:0x4004b0
等待用户输入回车,输入回车后:
_dl_runtime_resolve_xsave 地址:0x7f5b162428d0
_dl_fixup 地址:0x7f5b1623ace0
exit 地址:0x7f5b162c2650
程序终止。

5.7 Hello的动态链接分析

动态链接是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。
观察hello节头表中GOT的信息,我们发现got.plt从0x404000开始,大小为0x48,从而结束地址为0x404047.
在这里插入图片描述

图5.13 .got.plt在虚拟内存中的位置

之后我们在eata dump中观察此地址,即.got.plt节,发现在dl_init过程前后,.got.plt的字节发生了变化。
在这里插入图片描述
在这里插入图片描述
图5.14 dl_init前后.got.plt节的变化

如上图所示,和PLT联合使用时,GOT[0]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。其中GOT[1]指向重定位表(依次为.plt节需要重定位的函数的运行时地址)用来确定调用的函数地址, GOT[2]是动态链接器ld-linux.so模块中的入口点。

5.8 本章小结

本章主要对链接后的可执行文件进行了分析,利用objdump、edb等工具分析了重定位之后的代码。经过这个过程,可以清晰地看到代码在重定位前后发生的变化,在5.5、5.7节中对重定位的具体过程进行详细的剖析。

此外,5.6节中还探索了hello程序运行的具体流程,看到了许多在原始的C代码中看不到的函数调用,它们是执行一个程序必不可少的。

第六章 hello进程管理

6.1 进程的概念与作用

进程的概念:进程是一个执行中的程序的实例;是一个具有一定独立功能的程序关于某个数据集合的一次运动活动;是系统进行资源分配和调度运行的基本单位。 系统的每一个程序都是运行在某一个进程上下文中。上下文是由程序正确运行所需要的状态构成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及上下文描述符的集合。
进程的作用:向用户提供了一种假象。 我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存。处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。

6.2 简述壳Shell-bash的作用与处理流程

Shell的作用:Shell是一个交互型应用级程序,为用户提供命令行界面,使用户可以输入shell命令,然后调用相应的应用程序。
Shell的处理流程:首先其终端进程读取用户由键盘输入的命令行,然后对该命令进行解析,若该命令是shell的内置命令,则立刻执行此命令;否则shell创建会通过fork创建一个子进程,在子进程中,调用execve( )执行指定程序,并且使用waitpid等命令等待执行文件结束并回收,且删除,如果用户要求后台运行,则shell返回到循环的顶部,等待下一个命令行。

6.3 Hello的fork进程创建过程

父进程通过fork()函数创建一个新的子进程,调用fork()函数后,新创建的子进程几乎但不完全与父进程相同,包括代码、数据段、堆、共享库以及用户栈。父进程和新创建的子进程之间最大的区别在于他们有不同的PID。父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的逻辑控制流的指令。
例如对于hello的可执行程序,在shell中输入以下命令:./hello 7203610120宣传 2
此后,shell中就会调用fork()函数为hello创建一个shell的子进程。

6.4 Hello的execve过程

在shell给hello进行fork()函数创建子进程之后,会调用execve函数,在进程的上下文中加载并运行hello,调用_start创建新的且被初始化为0的栈等,随后将控制给主函数main,并传入参数列表和环境变量列表。只有当出现错误时,例如找不到filename,execve才会返回到调用程序。而加载运行时,首先会删除已存在的用户区域。删除当前进程虚拟地址的用户部分中已存在的区域结构然后会为新程序的代码、数据、bss和栈区域创建新的区域结构,最后能通过跳转到程序的第一条指令或入口点来运行该程序,由此将控制转移给新程序的主函数。

6.5 Hello的进程执行

Hello的进程执行有上下文信息、进程时间片、用户态和内核态转换、上下文切换等方面结合。
首先先来解释一下几个概念。
进程时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
用户态和内核态:处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
上下文切换:当一个进程正在执行时,内核调度了另一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程。在进行上下文切换时,需要保存以前进程的上下文,恢复新恢复进程被保存的上下文,将控制传递给这个新恢复的进程来完成上下文切换。
在执行hello时,内存给hello的可执行程序分配时间片,即逻辑流执行时交错的,一些程序轮流用处理器,出现并发现象。如果在此期间内发生了异常或系统中断,则内核会休眠该进程,并在核心态中进行上下文切换,控制将交付给其他进程。在hello进程调用sleep之后陷入内核模式,内核处理休眠请求主动释放当前进程以加载新的进程进行执行。同时将hello进程从运行队列中移入到等待队列,定时器开始计时,内核进行上下文切换将当前进程的控制权交给其他进程并执行,当定时器到时时发送一个中断信号,此时进入内核状态执行中断处理,将hello进程从等待队列中移出重新加入到运行队列,成为就绪状态,hello进程就可以继续进行自己的控制逻辑流了。
而在hello循环后,程序调用getchar()函数,hello进入核心态,次上下文切换,控制交付给其他进程。最终,内核从其他进程回到 hello 进程,在return后进程结束。
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。

6.6 hello的异常与信号处理

6.6.1 正常运行

在这里插入图片描述
图6.1 shell正常运行的结果

如上图所示,hello程序每隔两秒打印一行“Hello 7203610120 xc”,并循环8次,打印完后,等待用户输入回车后程序结束。

6.6.2 用户不停乱按键盘

在这里插入图片描述
图6.2 shell不断按键盘的结果

如上图所示,在不断乱按过程中,由于没有按回车,对程序运行没有影响。

6.6.3 按回车

在这里插入图片描述
在这里插入图片描述
图6.3 shell在程序运行时按回车的结果

如上图所示,在程序结束后,shell中多了9个空行,这是因为在程序运行过程中我们按了10下回车,从而排除了getchar()的回车,其余回车保留在shell中。

6.6.4 按ctrl-z

在运行过程中按ctrl-z,产生异常中断,此时hello的父进程会接受到SIGSTP并运行信号处理程序。
在这里插入图片描述
图6.4 shell在程序运行时按ctrl-z的结果

上图为按ctrl-z的结果,是hello被挂起并打印相关信息。

6.6.4.1 输入ps

在这里插入图片描述
图6.5 shell在程序运行时按ctrl-z后输入ps的结果

会打印各进程的pid,包括被挂起的hello

6.6.4.2 输入jobs

在这里插入图片描述
图6.6 shell在程序运行时按ctrl-z后输入jobs的结果

会打印被挂起的hello的jid和标识

6.6.4.3 输入pstree

在这里插入图片描述
图6.7 shell在程序运行时按ctrl-z后输入pstree的结果

查看进程树之间的关系,同时输出对应的进程pid。

6.6.4.4 输入fg

在这里插入图片描述
图6.8 shell在程序运行时按ctrl-z后输入fg的结果

被挂起在后台的hello进程被重新调到前台执行,打印出剩余部分,按回车后终止程序。

6.6.4.5 输入kill

在这里插入图片描述
图6.9 shell在程序运行时按ctrl-z后输入kill的结果

输入hello的pid8122,从而会将进程8122杀死

6.6.5 按ctrl-c

按ctrl-c后会导致中断异常,父进程收到信号SIGINT向子进程发生SIGKILL强制中止子进程hello并回收,我们如下图观察输入ps后的结果发现没有进程hello。
在这里插入图片描述
图6.10 shell在程序运行时按ctrl-c后查看pid的结果

hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。

6.7 本章小结

本章中主要讨论了hello进程管理的各个方面,包括从加载到运行再到运行时各种异常与信号处理的测试。
首先对进程的概念、作用进行了说明,然后详细剖析了加载运行程序时使用的fork、execve函数的作用以及工作流程。最后,在hello运行的过程中分别配合ps、fg等命令测试了ctrl-c、ctrl-z、乱按等情况下信号的接收与处理情况,展示了详细的测试结果,并分析了相应的处理机制。

第七章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:程序经过编译后出现在汇编代码中的地址或者说是由程序产生的与段有关的偏移地址。逻辑地址分为两个部分,一个部分为段基址,另一个部分为段偏移量。
线性地址:分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。
虚拟地址:虚拟地址是程序访问存储器所使用的逻辑地址。
物理地址:放在寻址总线上的地址。放在寻址总线上,如果是读,电路根据这个地址每位的值就将相应地址的物理内存中的数据放到数据总线中传输。如果是写,电路根据这个地址每位的值就在相应地址的物理内存中放入数据总线上的内容。物理内存是以字节(8位)为单位编址的。

7.2 Intel逻辑地址到线性地址的变换-段式管理

逻辑地址由两部分组成:段标识符和段内偏移量。段标识符是一个16位长的字段组成,其中前13位是一个索引号。后面三位包含一些硬件细节。
Base字段,表示的是包含段的首字节的线性地址,也就是一个段的开始位置的线性地址。只需要看段标识符的前13位,通过索引在段描述符表中找到对应的段描述符,从而可以得到Base字段,最后将开始位置的线性地址与段内偏移量相加,就能得到相应的线性地址。

在这里插入图片描述
图7.1 逻辑地址到线性地址变换的示意图

7.3 Hello的线性地址到物理地址的变换-页式管理

物理地址是内存储器中的实际有效地址,逻辑地址用是户程序中使用的地址,也就是访内指令给出的地址叫逻辑地址。计算机利用页表,通过MMU来完成从虚拟地址到物理地址的转换。
在这里插入图片描述
图7.2 线性地址到物理地址变换示意图

由虚拟地址到物理地址的变换通过以下步骤进行:首先从得到hello进程的页目录地址,将这个地址存储在相应的寄存器;同时根据线性地址前10位,即面目录索引,在数组中找到与之对应的索引项,即一个页表的地址,其中存储页的地址,然后根据线性地址的中间10位,即页表索引,在页表中找到页的起始地址,最后将页的起始地址与线性地址中最后12位,即偏移量,将它们相加,就可以得到物理地址。
而对于页式管理,分页存储管理允许把一个作业存放到若干不相邻、不连续的分区中,这样既可免去移动信息所造成的系统开销,又可尽量减少内存产生碎片。其具体步骤是:把物理地址空间分成大小相等的许多分区,每个分区 称为“块”或“帧”,每个块有一个编号,从“0”开始编号,块是存储分配的单元;按块的大小把逻辑地址空间分成许多“页”,从“0”开始编号;作业的逻辑地址与一个数对(页号,页内位移)一一对应;最后采用分页式存储管理,作业一次性全部装入内存,作业进入内存时其连续的页面可以装入内存中不相邻、不连续的块中,只要内存有空闲块,作业的某一页可以放到内存任一空闲块。

7.4 TLB与四级页表支持下的VA到PA的变换

每次CPU产生一个虚拟地址,MMU(内存管理单元)就必须查阅一个PTE(页表条目),以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会从内存多取一次数据,代价是几十到几百个周期。如果PTE碰巧缓存在L1中,那么开销就会下降1或2个周期。然而,许多系统都试图消除即使是这样的开销,它们在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓存器(TLB)。
首先,48位的虚拟地址将会被分为36位的VPN以及12位的VPO,前者按照9/9/9/9划分为四级页表的索引,而后者是页面内的位置(页面大小为4k,对应着12位)。
四级页表中每一级都对应着512个表项,其中只有第四级页表的表项中装着PPN。CR3指向的是第一级页表首地址,前9位VPN是一级页表的偏移量,用来索引一级页表中的表项,其表项内容为二级页表的首地址。第10-18位VPN是二级页表的偏移量……以此类推,最后9位VPN对应着第四级页表的偏移量,于是能找到对应的PPO。得到的40位PPO再与12位的VPO拼接,就得到了52位的物理地址。
如此操作,虽然经历的步骤很多,但是如果有一级页表的PTE为空,则其对应的二级页表不糊存在,这样可以节约大量空间。

7.5 三级Cache支持下的物理内存访问

在物理地址发给L1高速缓存后,缓存会从PA中得出其偏移、索引与标记,之后通过cache匹配,若命中则将数据返回给MMU;反之则按照L1-L2-L3-主存的顺序依次往低级别缓存寻找,当查找到后,则将块放置在cache中,若有此组内用空闲块,则直接放置;若没,即产生冲突,则按照规则进行替换即可。

7.6 hello进程fork时的内存映射

当fork()函数被父进程调用时,内核创建一个子进程,为新的子进程创建各种数据结构,并分配给子进程一个唯一的pid(与父进程不同)。为了给hello进程创建虚拟内存,它创建了hello进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork 在hello进程中返回时,hello进程现在的虚拟内存刚好和调用fork 时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面, 更新页表条目指向这个新的副本,然后恢复这个页面的可写权限。

7.7 hello进程execve时的内存映射

execve函数的区域映射包含两部分,分别是私有区域的映射和共享区域的映射。
映射私有区域:为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区被映射为hello文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。
映射共享区域:如果hello程序与共享对象链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
而加载运行hello需要几个步骤:(1)删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构。(2)映射私有区域,为新程序的代码、数据、bss和栈区域创建新的区域结构。(3)映射共享区域,如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域。(4)设置程序计数器(PC),exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。下一次调用这个进程时,它将从这个入口点开始执行。

7.8 缺页故障与缺页中断处理

缺页故障: DRAM缓存不命中称为缺页,当指令引用一个相应的虚拟地址,而与改地址相应的物理页面不再内存中,会触发缺页故障。
缺页中断处理:
1.判断虚拟地址A是否合法。也就是说,A在某个区域结构定义的区域内吗?却也异常处理程序搜索区域结构的链表,将A与每一个区域结构的头和尾相比较,如果没有匹配到任何结果,说明是不合法的,于是报出段错误。实际上系统采用更快的方式搜索链表,借助了树的数据结构。
2.判断进行的内存访问是否合法。也就是说进程是否有读、写或者执行这个区域内页面的权限?如果试图进行的访问是不合法的,那么缺页处理程序会触发一个保护异常,从而终止这个进程。
3.此时,如果已知这个缺页是由对合法的虚拟地址进行合法的操作造成的,就开始处理缺页。选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的命令,这条指令将再次发送A到MMU。这次MMU就能正常地翻译A,而不会再产生缺页中断了。

7.9动态存储分配管理

动态内存管理的基本方法:动态内存分配器是一种方便常用的方法。动态内存分配器维护着一个进程的虚拟内存区域,称为堆。假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向更高的地址延申。对于每个进程,内核维护着一个变量brk,它指向堆的顶部。分配器有两种基本风格:显式分配器与隐式分配器。

  1. 隐式空闲链表分配器:我们可以将堆组织为一个连续的已分配块和空闲块的序列,空闲块是通过头部中的大小字段隐含地连接着的,这种结构为隐式空闲表。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。一个块由一个字的头部、有效载荷、可能的填充和一个字的脚部组成,其中脚部就是头部的一个副本。头部编码了这个块的大小以及这个块是已分配还是空闲的。分配器就可以通过检查它的头部和脚部,判断前后块的起始位置和状态。
  2. 显示空闲链表分配器:将堆组成一个双向空闲链表,在每个空闲块中都包含一个pred(前驱)和succ(后继)指针。一种方法是用后进先出(LIFO)的顺序来维护链表,将新释放的块放置在链表开始处。使用LIFO的顺序和首次适配的放置策略,分配器会最先检查最近使用过的块。在这种情况下释放一个块可以在常数时间内完成。若使用了边界标记,那么合并也可以在常数时间内完成。另一种方法是按照地址顺序来维护链表,其中链表中每个块的地址都小于其后继地址,在这种情况下释放一个块需要线性时间的搜索来定位合适的前驱。平衡点在于,按照地址排序的首次适配比LIFO排序的首次适配有更高的内存利用率,接近最佳适配的利用。显式链表的缺点是空闲块必须足够大,以包含所有需要的指针,以及头部和可能的脚部。这就导致更大的最小块大小,亦潜在地提高了内部碎片的程度。

动态内存管理的策略:首先,对与堆中的块的组织,可以选择隐式、显示、分离空闲链表等。当分配器查找空闲块时,又可以采用不同的放置策略,如首次适配(从头开始搜索链表)、下一次适配(从上一次找到的空闲块的剩余块除法)以及最佳适配等。在分割空闲块时,又可以采用将剩余块分割为新的空闲块的策略。在合并时,可以采用带边界标记的合并,就像在上一段基本方法中所描述,通过边界标记来判断当前块周围是否也同样是空闲块,以此来判断是否需要合并。

7.10本章小结

本章主要讨论了hello的存储管理方式,详细地剖析了多种存储管理方式,包括段式管理、页式管理。此外,还非常明确详尽地讲述了多级页表机制下从虚拟地址转为物理地址的方式,以及利用cache访问数据的详细机制。

对于内存映射方式,本章展示了fork和evecve函数内存映射的方式;对于缺页异常的处理方式,本章也阐明了基于MMU的判断缺页异常原因的方式以及对相关页面进行调度的机制。最后,本章以最为主流的分离适配策略说明了动态内存分配管理机制,详细剖析了malloc、free、realloc函数配合空闲链表的运作机理。

结论

用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
首先编写hello.c的源程序,从此hello.c诞生。此时hello.c仍是一个文本文件,还没有变成二进制文件。之后hello.c经过预处理器(cpp)得到修改了的源程序hello.i。这时的hello.i代码量比hello.c大大增加,其中注释部分被删除了,引用的头文件被插入到了源代码中,除头文件与注释以外的源代码保持不变。接着,编译器(cc1)将hello.i翻译为汇编程序hello.s。在这个过程中,高级语言被翻译为机器逻辑下的汇编语言,编译器可能会根据编译选项的不同对程序进行一些优化。然后hello.s通过汇编器翻译为机器语言,并变成可重定位目标文件hello.o。这是hello.o是二进制文件。接下来通过链接器将嗲用的C库中函数与编译好的目标文件合并成了可执行目标程序hello,这是一个二进制文件,与hello.o比较发现所有内容都分配了虚拟地址。
当我们运行hello时,我们在shell中利用fork()创建子进程并分配虚拟内存空间,再用execve()加载hello程序,从而hello从程序变成了进程。然后hello在一个时间片中有自己的CPU资源,顺序执行逻辑控制流,且hello的VA通过TLB和页表翻译为PA,而三级cache支持下进行hello的物理地址访问,但是 hello在执行过程中会有异常和信号需要处理最后hello在Unix为I/O的帮助下与用户交互。

附件

列出所有的中间产物的文件名,并予以说明起作用。
hello.c: 原C语言文件
hello.i:hello.c经预处理生成的文本文件。
hello.s:hello.i经编译生成的汇编语言文件。
hello.o:hello.s经汇编生成的可重定位目标文件。
hello:hello.o经链接生成的可执行目标文件。
hello1.txt: hello的反汇编文件
hello2.txt: hello.o的反汇编文件

参考文献

[1] 林来兴. 空间控制技术[M]. 北京:中国宇航出版社,1992:25-42.
[2] 辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.
[3] 赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).
[4] 谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.
[5] KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.
[6] CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值