计算机系统大作业:程序人生-Hello‘s P2P
Posted Sososoby
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了计算机系统大作业:程序人生-Hello‘s P2P相关的知识,希望对你有一定的参考价值。
计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机系
学 号 120L021303
班 级 2003002
学 生 苏泓斌
指 导 教 师 史先俊
计算机科学与技术学院
2021年5月
本文介绍了一个hello.c源程序在Linux系统中的一个完整的生命周期,从编译到运行到终止的全过程,并以此为依托,概述了计算机系统的一些重要机制,包含了计算机科学的一些经典思想。本文详细介绍了从.c源文件生成可执行文件的预处理、编译、汇编、链接四个阶段,同时介绍了计算机系统的两个及其重要的抽象机制:进程和虚拟地址空间,并简要的概述了I/O管理。
关键词:计算机系统;Linux;hello程序
目 录
1.1.1 P2P:From Program to Process. - 5 -
1.1.2 020:From Zero-0 to Zero-0. - 5 -
6.2 简述壳Shell-bash的作用与处理流程... - 31 -
6.3 Hello的fork进程创建过程... - 31 -
7.2 Intel逻辑地址到线性地址的变换-段式管理... - 37 -
7.3 Hello的线性地址到物理地址的变换-页式管理... - 38 -
7.4 TLB与四级页表支持下的VA到PA的变换... - 38 -
7.5 三级Cache支持下的物理内存访问... - 39 -
7.6 hello进程fork时的内存映射... - 39 -
7.7 hello进程execve时的内存映射... - 39 -
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
1.1.1 P2P:From Program to Process
·Program:在linux系统中,我们先打开vim等文本编辑器,在我们敲击键盘的过程中,字符被读入寄存器,再被存入内存中。当我们保存hello.c文件并退出,程序文本被交换到磁盘。
·Process:hello.c经过cpp的预处理、cc1的编译、as的汇编、ld的链接,最终成为可执行目标文件。然后在shell中键入运行命令,进程管理为其fork进程,hello.c就从程序变为了进程。
1.1.2 020:From Zero-0 to Zero-0
·为hello创建进程后,利用execve把hello的内容加载到子进程的地址空间中,映射虚拟内存,进入程序入口后开始载入物理内存。在这个过程中,涉及到虚拟内存:在fork时,操作系统仅仅给子进程复制各种数据结构,如页表等。进入 main 函数执行目标代码, CPU 为运行的 hello 分配时间片执行逻辑控制流。
·当子进程执行return语句后,它保持一种已经终止的状态,向shell发送SIGCHLD信号,等待shell对其进行回收。当shell调用waitpid指示操作系统将其回收,内核删除相关数据结构后,hello的生命周期便结束了。
1.2 环境与工具
1.2.1 硬件环境
X64 CPU(AMD Ryzen 7 4700U);2 GHz;16G RAM;256GHD Disk 以上
1.2.2 软件环境
Windows10 64位;Vmware 16pro;Ubuntu 20.04 LTS 64位
1.2.3 开发与调试工具
Visual Studio 2010 64位以上;CodeBlocks 64位;vi/vim/gedit+gcc;gcc;gdb;edb
1.3 中间结果
文件 | 作用 |
hello.i | hello.c预处理之后的文本文件 |
hello.s | hello.i编译后的汇编文件,文本文件 |
hello.o | hello.s汇编之后的可重定位目标文件,二进制文件 |
hello | 链接之后的可执行目标文件,二进制文件 |
hello.out | hello反汇编之后的可重定位文件 |
表1-1 中间结果文件
1.4 本章小结
本章简要介绍了hello的P2P和020的整个过程,写出了本文所用到的软硬件环境和开发与调试工具,列出了整个过程所产生的中间结果。本章是全文的概述,接下来的章节则是对各部分详细的论述。
第2章 预处理
2.1 预处理的概念与作用
预处理(preprocessing)指的是预处理器(cpp)根据以字符#开头的命令(如#include、#define、#pragma等),修改原始的C程序,最后生成.i文本文件的过程。C预处理器扩展源代码,插入所有用#include命令指定的文件,并扩展所有用#define声明指定的宏[1]。
预处理能够帮助程序员节省工作量、使程序更易读、便于维护。
1. 处理头文件包含指令头文件包含指令如#include "FileName"或者#include 等。该指令将头文件中的内容直接插入到hello.i中,以供编译程序对之进行处理。
2. 处理条件编译指令如#ifdef,#ifndef,#else,#elif,#endif等。预编译程序将根据有关的文件,将那些不必要的代码过滤掉。
3. 处理宏定义指令。用实际值替换用#define 定义的字符串。
4. 预编译程序可以识别一些特殊的符号如LINE、FILE等。预编译程序对于在源程序中出现的这些串将用合适的值进行替换。
2.2在Ubuntu下预处理的命令
命令:cpp hello.c hello.i
由图2-1可以看出,预处理过后,文件夹中产生了一个名为hello.i的新文件。
图 2-1 预处理命令
2.3 Hello的预处理结果解析
可以发现整个程序已经拓展为3060行,原来hello.c的程序出现在3046行及之后。在这之前出现的是头文件 stdio.h unistd.h stdlib.h 的依次展开,其中包括大量的typedef,如typedef unsigned char __u_char;还包括600多行的枚举(enum)类型,以及标准C的函数原型,如extern int printf (const char *__restrict __format, …);标准输入输出和错误也在这里被定义(extern FILE *stdin;extern FILE *stdout;extern FILE *stderr;)。
cpp 对头文件中define宏定义递归展开。所以最终的.i文件中是没有#define的;发现其中使用了大量的#ifdef #ifndef条件编译的语句,cpp会对条件值进行判断来决定是否执行包含其中的逻辑。预编译程序可识别一些特殊的符号,预编译程序对在源程序中出现的这些串将用合适的值进行替换。
图2-2 hello.i文件部分内容截图
2.4 本章小结
本章主要介绍了预处理的概念及其作用(包括头文件的展开、宏替换、去掉注释、条件编译),并在linux系统下以hello.c为例子,使用命令生成了hello.i文本文件,通过对hello.i内容的解析,进一步了解了预处理的内涵和流程。
第3章 编译
3.1 编译的概念与作用
编译的概念:编译器将文本文件 hello.i 翻译成文本文件 hello.s,它包含一个汇编语言程序。其以高级程序设计语言书写的源程序作为输入,而以汇编语言或机器语言表示的目标程序作为输出。这个过程称为编译,同时也是编译的作用。
汇编语言为不同种类的语言提供了相同的形式,其指令与处理器的指令集类似,更贴近底层,便于汇编器将其转换为机器码供机器执行。编译程序的基本功能是把源程序(高级语言)翻译成目标程序。除了基本功能之外,编译程序还具备语法检查、调试措施、修改手段、覆盖处理、目标程序优化、不同语言合用以及人际联系等重要功能。
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.i -o hello.s
由图3-1可以看出,编译过后,文件夹中产生了一个名为hello.s的新文件。
图3-1 编译命令
3.3 Hello的编译结果解析
3.3.1 数据
1、常量
在if (argc != 4)条件语句中,常量4保存在.text段中,作为指令的一部分,如图3-2所示。同理可得,.L4的常量0 8 1 2 3亦如此。
图3-2 常量4的调用
而在下述的printf函数中
printf("用法: Hello 学号 姓名 秒数!\\n");
printf(), scanf()中的字符串则被存储在.rodata节中。
图3-3 只读数据区的字符串常量
2、变量
main函数声明了一个局部变量i,i被初始化为0后,该局部变量存储在地址为栈上%rbp-4的位置。图3-4为对应的汇编代码。
图3-4 局部变量初始化的汇编代码
3.3.2 赋值
程序中的赋值操作:i = 0,此赋值操作在汇编代码中使用mov指令来实现,又根据数据的类型可以分为movb movw movl movq。由图3-4可以看出此赋值操作将一个四个字节大小的数据赋值给变量i。
3.3.3算术操作
在循环体中,每次循环都i++,使用++自加算术操作符
for(i=0;i<8;i++)
体现在汇编代码中,则是每次循环结束都使用addl指令,使存储在栈上的变量i的值加1。
图3-5 循环体中的算术操作
3.3.4 关系操作
1、argc != 4;是条件语句中的条件判断:argc != 4,进行编译时,这条指令被编译为:cmpl $4, -20(%rbp),同时这条cmpl的指令还有设置条件码的作用,当根据条件码来判断是否需要跳转到分支中。
图3-6 !=关系操作
2、i < 8,作为判断循环条件,在汇编代码被编译为:cmpl $7, -4(%rbp),计算 i-7然后设置条件码,为下一步 jle 利用条件码进行跳转做准备。
图3-7 <关系操作
3.3.5 数组操作
hello.c中唯一的数组char *argv[]是作为main函数的第二个参数,数组的每个元素都是一个指向字符类型的指针。数组的起始地址存放在栈中-32(%rbp)的位置,因为char* 数据类型占8个字节,前两次调用 argv[1] (%rbp - 8)和 argv[2] (%rbp - 16)传给printf,第三次调用传最后一个参数给sleep函数。图3-8用三种不同的颜色分别标出了对数组的三次引用。
图3-8 数组操作
3.3.6 控制转移
1、if/else
判断argc是否为4,如果不等于则执行if语句,否则跳转到.L4循环体执行代码。
图3-9 if条件控制转移C语言代码 图3-10 if条件控制转汇编代码
2、for
for(i=0;i<8;i++),通过每次判断i是否满足小于8来判断是否需要跳转至循环语句.L4中。
图3-11 for控制转移C语言代码 图3-12 for控制转移汇编代码
3.3.7 函数操作
X86-64中,过程调用传递参数规则:第1~6个参数一次储存在%rdi、%rsi、%rdx、%rcx、%r8、%r9这六个寄存器中,剩下的参数保存在栈当中。
1、main函数
分析图3-13的汇编代码:
参数传递:传入参数argc和argv[],分别用寄存器%rdi和%rsi存储。
函数调用:被系统启动函数调用。
局部变量:循环变量i。
函数返回:设置%eax为0并且返回,对应return 0 。
图3-13 main函数和.L3的汇编代码
2、printf函数:
hello.c共有两处调用了printf()函数。
(1) 第一次是在if条件控制语句中,如果参数个数不是4,则从只读数据区中取字符串输出。
参数传递:call puts只传入了只读数据区的字符串参数首地址。
函数调用:if判断满足条件后调用。
函数返回:无。
图3-14 第一次调用printf函数的汇编代码
(2) 第二次是在for循环体中,每次循环调用一次printf函数。
参数传递:for循环中call printf时传入了 argv[1]和argc[2]的地址。
函数调用:for循环中被调用。
函数返回:无。
图3-15 第二次调用printf函数的汇编代码
3、exit函数:
参数传递:传入的参数为1,再执行退出命令。
函数调用:if判断条件满足后被调用。
函数返回:无。
图3-16 exit函数调用汇编代码
4、sleep函数:
参数传递:传入参数atoi(argv[3])。
函数调用:for循环下被调用,call sleep。
函数返回:无。
图3-17 sleep函数调用汇编代码
5、getchar函数:
函数调用:在main函数最后被调用,call getchar。
3.4 本章小结
本章主要介绍了编译的概念以及过程和作用。同时通过示例函数表现了C语言中各种类型和操作所对应的的汇编代码。对我们的源程序hello.c编译后得到的汇编语言程序中的各种数据、操作做了详细的解析。介绍了汇编代码如何实现变量、常量、传递参数以及分支和循环。
第4章 汇编
4.1 汇编的概念与作用
汇编器as接受.s作为输入,以可重定位目标文件.o作为输出。即驱动程序运行汇编器as,将汇编语言(这里是hello.s)翻译成机器语言(hello.o)的过程称为汇编。
可重定位目标文件包含二进制代码和数据,其形式可以在编译时与其他可重定位目标文件合并起来,创建一个可执行目标文件,从而被加载到内存并执行[1]。所以汇编的作用就是将汇编语言转换成机器可以理解的机器语言指令。
4.2 在Ubuntu下汇编的命令
命令:as hello.s -o hello.o
由图4-1可以看到,经过as汇编后,文件夹中出现了名为hello.o的新文件。
图4-1 as汇编命令
4.3 可重定位目标elf格式
命令:readelf -a hello.o
-a显示全部信息,也可以换成其他选项,如-h只显示ELF头的信息,-S只显示节头部表的信息。
4.3.1 ELF头
ELF头描述生成该文件的系统的字的大小和字节顺序、帮助链接器语法分析和解释目标文件的信息。Magic 描述了生成该文件的系统的字的大小和字节顺序,ELF 头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括 ELF头的大小、目标文件的类型、机器类型、字节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量等信息。比如ELF64(ELF 64位的可执行程序);2’s complement,little endian即补码表示,小端法;REL(Relocatable file即可重定位目标文件);运行机器为AMD x86-64;节头开始为文件开始处的1240字节偏移处。
图4-2 可重定位目标文件的ELF头
4.3.2 节头部表
节头部表用于描述不同节的位置和大小,图4-3的属性分别是节名、类型、地址(此时暂时未被分配均为0)、偏移量(节相对于文件开始的偏移)、节大小、表项(Entry)大小、flags(节属性)、(与其他节的)关联、附加节信息、对齐(2的Align次方)。由于是可重定位目标文件,所以每个节都从0开始,用于重定位。
图4-3 可重定位目标文件的节头部表
4.3.3 重定位节
一个.text节中位置的列表,当连接器把这个目标文件和其他文件组合时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。另一方面,调用本地函数的指令则不需要修改。
本程序需要重定位的信息有:.rodata中的模式串,puts,exit,printf,slepsecs,sleep,getchar这些符号。
图4-4 可重定位目标文件的重定位节
4.3.4 符号表
.symtab是一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。name是符号名称,对于可冲定位目标模块,value是符号相对于目标节的起始位置偏移,对于可执行目标文件,该值是一个绝对运行的地址。size是目标的大小,type要么是数据要么是函数。Bind字段表明符号是本地的还是全局的。
图4-5 可重定位目标文件的符号表
4.4 Hello.o的结果解析
命令:objdump -d -r hello.o
图4-6 objdump反汇编结果
分析hello.o的反汇编,并与第3章的 hello.s进行对照分析:
操作数的表示:hello.o反汇编代码中的操作数是十六进制的,hello.s中的操作数是十进制的。
分支转移:跳转语句之后,hello.s中是.L2和.LC1等段名称,而反汇编代码中跳转指令之后是相对偏移的地址,即间接地址,更适合被加载到内存中工作。
函数调用:hello.s中,call指令使用的是函数名称,而反汇编代码中call指令使用的是main函数的相对偏移地址。要留到链接阶段进行重定位符号引用,才会填上相对偏移量。此外,由于在编译阶段没有保留符号的名字,函数调用都被写为了<main+offset>的形式。
4.5 本章小结
本章简述了汇编的概念,介绍了汇编的命令行,分析了ELF可重定位目标文件的格式,解析了对汇编后的文件hello.o进行反汇编之后的内容,并与hello.s文件进行了对比,分析了从汇编语言到机器语言的一一映射关系。
第5章 链接
5.1 链接的概念与作用
链接器将可重定位目标代码文件和一些库函数的代码合并,产生最终的可执行目标文件hello,其可以直接被复制到内存执行。链接可以执行于编译时,也可以执行于加载时,也可以在运行时由应用程序来执行。
链接将多个重定位目标文件(或静态/动态库)整合到一起,并且修改符号引用,输出一个可执行目标文件。链接使得一个较大的程序可以被分解成许多模块来编写,并最终合并为一个可执行程序。
5.2 在Ubuntu下链接的命令
命令:
ld -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 /usr/lib/gcc/x86_64-linux-gnu/9/crtbegin.o hello.o -lc /usr/lib/gcc/x86_64-linux-gnu/9/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -z relro
由图5-1可以看出,链接命令之后,文件夹出现了一个名为hello的新文件。
图5-1 链接命令
5.3 可执行目标文件hello的格式
命令:readelf -a hello
-a显示全部信息,也可以换成其他选项,如-h只显示ELF头的信息,-S只显示节头部表的信息。
5.3.1 ELF头
将图5-2与链接前的ELF头作比较,发现Type由REL(可重定位目标文件)变为EXEC(可执行目标文件),Entry point address即程序入口点由0x0(未确定)变为了0x4010f0,程序和节的起始位置和大小也均有改变,节个数由14个变为31个。
图5-2 可执行目标文件的ELF头
5.3.2 节头部表
节头描述了各个节的大小、偏移量和其他属性。链接器链接时,会将各个文件的相同段合并成一个大段,并且根据这个大段的大小以及偏移量重新设置各个符号的地址。由图5-3的节头信息可以看出,第一个特点就是节的数目有了显著增加,由14个变为31个,增加了许多具有其他作用的节,这里就不一一赘述了。
图5-3 可执行目标文件的节头部表
5.3.3 符号表
可执行文件的符号表(图5-5)中多了很多符号,而且额外有一张动态符号表(.dynsym),如图5-4所示。printf puts exit getchar等C标准库函数在动态符号表和符号表中都有表项。此外一个与可重定向目标文件的不同是,这些符号已经确定好了运行时位置。
图5-4 动态符号表
图5-5 符号表
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息。
通过查看edb,看出hello的虚拟地址空间开始于0x401000,结束于0x402000。
图5-6 Linux进程的虚拟地址空间布局
查看 ELF 格式文件中的 Program Headers,它告诉链接器运行时加载的内容,并提供动态链接的信息。每一个表项提供了各段在虚拟地址空间和物理地址空间的各方面的信息。根据5.3.2的节头部表的信息,可以通过edb找到各个节的信息,比如.txt节虚拟地址开始于0x4010f0,可以在Data Dump中寻找其位置如图5-6所示。
图5-7 .text的位置
5.5 链接的重定位过程分析
命令:objdump -d -r hello
分析hello与hello.o的不同:
(1) 地址访问。hello.o中的相对偏移地址变成了hello中的虚拟内存地址。而hello.o文件中对于某些地址的定位是不明确的,其地址也是在运行时确定的,因此访问也需要重定位,在汇编成机器语言时,将操作数全部置为0,并且添加重定位条目。hello反汇编的代码有确定的虚拟地址,也就是说已经完成了重定位。
(2) hello反汇编的代码中多了很多的节以及很多函数的汇编代码,增加了.init和.plt节,和一些节中定义的函数。图5-8展示了部分新增的节,这些节都具有一定的功能和含义,在5.3节已做过陈述。
图5-8 新增节的部分截图
(3) 函数调用。hello中无hello.o中的重定位条目,并且跳转和函数调用的地址在hello中都变成了虚拟内存地址。对于hello.o的反汇编代码,函数只有在链接之后才能确定运行执行的地址,因此在.rela.text节中为其添加了重定位条目。
链接过程:
根据hello和hello.o的不同,分析出链接就是链接器(ld)将各个目标文件(各种.o文件)组装在一起,文件中的各个函数段按照一定规则累积在一起。
(1) 符号解析:将每个符号引用正好和一个符号定义关联起来;
(2) 重定位:所有相同类型的节合并成同一类型的新的聚合节,链接器将运行时内存地址付赋给每个节、每个符号,此时程序中每条指令、每个符号都有唯一的运行时内存地址了;再依据重定位条目,修改对每个符号的引用。
此重定位条目会告诉链接器修改偏移量为0x1b处的相对引用。此时链接器已知main的地址为0x4010c1,puts函数的地址为0x401030,则引用处地址为0x4010c1+0x1b=0x4010dc,相对偏移为:puts的地址+addend-引用处地址,即0x401030-0x4-0x4010dc=-0xb0,转换成补码表示就是ff ff ff 50,小端模式填入即可。绝对寻址极其简单,直接填入地址即可。
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。下面列出其调用与跳转的各个子程序名。
加载hello到_start之间调用_dl_start和_dl_init函数;main函数之前会调用_init函数初始化代码;main调用一些库函数如puts;程序终止调用exit函数。
加载hello:_dl_start、_dl_init;
main函数之前:_start、__libc_start_main、__cxa_aexit、_init、_setjmp、_sigsetjmp;
main函数:main、puts、exit、printf、sleep、getchar;
main函数之后:exit。
5.7 Hello的动态链接分析
(以下格式自行编排,编辑时删除)
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
动态链接会把库函数的文本和代码加载进来,hello程序是在加载时进行动态链接。在加载,_dl_init之前,文件中并没有库函数的代码、数据。_dl_init之后,库函数的代码、数据被加载进文件。
由图5-9得,.got起始表的位置是0x404000。
图5-9 hello的节头部表
使用edb查看Data Dump,发现GOT表位置在调用dl_init之前0x601008后的16个字节均为0。
图5-10 edb运行init之前.got节的内容
点击edb的运行按钮后,可以看到之前空白的字节变成了0x7f6a98c54190和0x7f6a98c3dbb0,其中GOT[0]~GOT[2]是动态链接器相关的地址,GOT[3]开始为调用函数的地址,而每个表项为8字节。在symbol viewer中查找该地址,发现正好就是puts在内存中的位置。依次可以验证后面的几项是printf、getchar等。这说明在hello的_start开始前调用了动态链接的相关函数,修改GOT表的内容,将其指向库中函数的运行时位置。
图5-11 edb运行程序之后.got的内容
5.8 本章小结
本章介绍链接阶段,介绍了链接的概念、命令行,介绍了可执行目标文件的ELF格式和各个节的含义,分析了hello的虚拟地址空间、各段的地址,详细介绍了链接的重定位过程,分析了hello的从加载到终止的流程,分析了动态链接前后哪些项目发生了变化。
第6章 hello进程管理
6.1 进程的概念与作用
进程就是一个执行中程序的实例。这是一种非正式的说法。进程不只是程序代码,还包括当前活动,如PC、寄存器的内容、堆栈、数据段,还可能包含堆[2]。
进程提供了两个关键的抽象:1、独立的逻辑控制流,好像程序在独占地使用处理器和内存;2、私有的地址空间,处理器好像是无间断的执行我们程序中的指令,好像程序独占的使用内存。
6.2 简述壳Shell-bash的作用与处理流程
Linux系统中,Shell是一个交互型应用级程序,代表用户运行其他程序,解释并运行用户的指令,连接用户和操作系统以及内核。
重复如下处理过程:
(1) 终端进程读取用户由键盘输入的命令行。
(2) 调用parseline函数,分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量。
(3) 调用builtin_command函数,检查第一个命令行参数是否是一个内置的shell命令,如果是则立刻解释这个命令。
(4) 如果不是内部命令,调用fork( )创建新的子进程。
(5) 在子进程中,用步骤2获取的参数,调用execve( )执行指定程序。
(6) 如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid(或wait)等待作业终止后返回。
6.3 Hello的fork进程创建过程
shell解析命令行,发现第一个参数是./hello而不是内置命令,就调用fork创建子进程,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。
fork创建一个子进程,子进程与父进程除了PID几乎完全相同。fork函数只会被调用一次,但会返回两次,在父进程中返回子进程的pid,子进程中返回0;子进程与父进程有完全相同的虚拟地址空间,但是是独立的,对一个进程私有的变量做的改变不会反映到另一个进程之中;子进程与父进程并发执行;子进程继承父进程打开的文件。
6.4 Hello的execve过程
在shell为hello创建子进程后,shell调用execve函数。execve在当前进程的上下文中加载并运行一个新的程序。步骤如下所示:
(1) execve调用启动加载器,加载器删除子进程现有的虚拟内存段。
(2) 映射私有区:创建一组新的代码、数据、堆栈段,新的代码、堆栈段被初始化为可执行文件的内容,堆栈段被初始化为0。
(3) 映射共享区:比如Hello程序与标准C库libc.so链接,这些对象都是动态链接到程序的,然后再映射到用户虚拟地址空间中的共享区域内。
(4) 设置程序计数器PC:exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。下一次调用这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
6.5 Hello的进程执行
进程时间片:一个进程执行它的控制流的一部分的每一时间段。
上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。
图6-1 上下文切换示例
用户模式和内核模式:处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
调度的过程:当进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策叫做调度。通过上下文切换的机制来转移控制:1、保存当前进程的上下文;2、回复即将调度的进程的上下文;3、将控制转移给被调度的进程。
以hello.c为例,hello并不是操作系统中运行的唯一进程。为了最大化CPU利用率,需要进行进程调度。当一个进程等待时,操作系统从该进程接管CPU控制,并将CPU交给另一进程。进行上下文切换时,陷入内核态,内核会将旧进程状态保存在其进程管理块中,然后加载经调度而要执行的新进程的上下文,并将控制重新转移给用户态。上下文切换的典型速度为几毫秒。
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
6.6.1 异常
异常可以分为四类:中断、陷阱、故障、终止,各自的属性如表6-1所示。
表6-1 异常类型
hello程序出现的异常可能有:
中断:在hello程序执行的过程中可能会出现外部I/O设备引起的异常。
陷阱:陷阱是有意的异常,是执行一条指令的结果,hello执行sleep函数的时候会出现这个异常。
计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机
学 号 120L021716
班 级 2003005
学 生 蔡泽栋
指 导 教 师 吴锐
计算机科学与技术学院
- 2021年5月
本文介绍了hello.c文件编写完成后在Linux下运行的完整生命历程,对预处理、编译、汇编、链接、进程管理、存储管理、I/O管理这些hello程序的生命历程进行详细、清楚地解释。通过运用一些工具清晰地观察hello程序完整的周期,直观地表现了程序从开始到结束的生命历程。
关键词:预处理;编译;汇编;链接;进程;存储;I/O
-
目录
第1章 概述
1.1 Hello简介
P2P: From program to process,即从程序到进程。在Linux中,hello.c经过cpp预处理、ccl编译、as汇编、ld链接最终成为可执行目标程序hello,在shell中键入启动命令后,shell为其fork产生一个子进程,然后hello便从程序变为了进程。
020: shell为此子进程execve,映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入 main函数执行目标代码,CPU为运行的hello分配时间片执行逻辑控制流。当程序运行结束后,shell父进程负责回收hello进程,内核删除相关数据结构。
1.2 环境与工具
1.2.1 硬件环境
i7-X64 CPU;2.30GHz;2G RAM;256GHD Disk
1.2.2 软件环境
Windows 10 64 位 VitualBox 15.1.0 Ubuntu 1 8 .04 LTS
1.2.3 开发工具
Visual Studio 2022 64位;CodeBlocks 64位;vi/vim/gedit+gcc
1.3 中间结果
文件名称 | 说明 |
hello.c | hello源文件 |
hello.i | 预处理后文件 |
hello.s | 编译得到的汇编文件 |
hello.o | 汇编后的可重定位目标文件 |
hello | 链接后可执行文件 |
hello.elf | hello的elf文件 |
-
1.4 本章小结
本章主要介绍了hello的P2P,020过程,给出了本次实验的环境。也列出了为编写本论文生成的中间结果文件并解释了其作用。
第2章 预处理
2.1 预处理的概念与作用
预处理的概念:预处理是计算机对一个程序处理的第一步,对.c文件初步处理成一个.i文件。预处理器根据以字符#开头的命令,修改原始的C程序:是指在进行编译的第一遍扫描之前所做的工作。当对一个c源文件进行预处理时,系统自动引用预处理程序以解析以字符#开头的预处理命令,比如#include<stdio.h> 等命令来修改原始的C程序,待预处理进行完毕之后自动进入对源程序的编译。
预处理的作用:
删除宏定义“#define”展开并解析所定义的宏,处理所有条件预编译指令。插入include后面的文件到“#include”处。删除所有的注释。最后将处理过后的新的文本保存在hello.i中。
2.2 在Ubuntu下预处理的命令
命令 gcc -E hello.c -o hello.i
结果如下图所示:
2.3 Hello的预处理结果解析
hello.i文件的内容增加到3000多行,预处理器对源文件中的宏进行了宏展开,对#define相应的符号进行了替换,同时也将系统头文件中的内容直接插入到了程序文本中。
2.4 本章小结
预处理是计算机对程序进行操作的第一个步骤,预处理器会对hello.c文件进行初步的解释,对头文件、宏定义和注释进行操作,将程序中涉及到的库中的代码补充到程序中,将注释这个对于执行没有用的部分删除。
第3章 编译
3.1 编译的概念与作用
概念:编译器将文本文件hello.i翻译成hello.s,它包含一个汇编语言程序。它是以高级程序设计语言书写的源程序作为输入,以汇编语言或者机器语言表示的目标程序作为输出。
作用:进行词法分析、语法分析和目标代码的生成,检查无误后生成汇编语言。
3.2 在Ubuntu下编译的命令
命令 gcc -S hello.i -o hello.s
结果如下图所示:
3.3 Hello的编译结果解析
3.3.1 汇编指令
.file:声明源文件
.text:代码节
.rodata:只读代码段
.align:数据或者指令的地址对其方式
.string:声明一个字符串(.LC0,.LC1)
.global:声明全局变量(main)
.type:声明一个符号是数据类型还是函数类型
3.3.2 数据
1. 字符串
第一个字符串“用法: Hello 学号 姓名 秒数!\\n”存放在只读数据段.rodata中,被编码成utf-8格式,其中一个汉字占三个字节。
第二个字符串"Hello %s %s\\n",输出传入的格式化参数,存放在只读数据段.rodata中。
2. main函数的参数argc
用户传递给main函数的参数argc被放到了堆栈。19行将栈地址保存在%rbp中,第22行%edi保存函数传入的第一个参数即argc,存放在-20(%rbp)的位置。
3. main函数的参数argv数组
argv每个元素char*大小为8字节,指针指向已分配好存放字符指针的连续空间,起始地址为argv。第23行%rsi保存函数传入的第二个参数即argv数组的首地址,存放在-32(%rbp)的位置。
访问数组元素argv[1],argv[2]和argv[3]时,按照起始地址argv大小8位计算数据地址取数据。
4. 临时变量int i
main函数内声明的局部变量i编译的时会放在堆栈中,即栈上-4(%rbp)的位置。
3.3.3 操作
1. 算术操作
for循环中临时变量i++,通过add指令实现。
2.赋值操作
for循环中对i赋初值的操作通过mov指令来进行实现。
3.关系操作
if语句判断argc!=4,设置条件码,为之后je跳转做准备。
for循环的条件判断i<8,比较i是否小于等于7,被编译为cmpl $7,-4(%rbp)。
4.数组操作
访问数组元素argv[1],argv[2]和argv[3]时,按照起始地址argv大小8位计算数据地址取数据。argv[]先被存在用户栈中,再用基址加偏移量寻址访问argv[1],argv[2]和argv[3]在第34、37和44行使用三次%rax取出其值。
5.控制转移操作
控制转移在本程序中包括if条件分支引起的跳转以及for循环分支引起的跳转。通过关系操作cmpl进行比较设置条件码,之后根据条件码进行跳转。
6.函数操作
main函数:
传递数据:外部调用过程向main函数传递参数argc和argv,分别使用%edi和%rsi存储,将%eax设置0返回。
printf函数:
传递数据:第一次printf将%rdi设为“用法: Hello 学号 姓名 秒数!\\n”字符串的首地址。第二次printf将 %rdi设为“Hello %s %s\\n”的首地址,设置%rdx为argv[1],%rsi为argv[2]。
控制传递:第一次printf有一个字符串参数, call puts@PLT,第二次printf使用call printf@PLT。
exit函数:
传递数据:将%edi设置为1。
控制传递:call exit@PLT。
sleep函数:
控制传递:call sleep@PLT。
atoi函数:
控制传递:call atoi@PLT。
getchar函数:
控制传递:call gethcar@PLT
3.4 本章小结
本章介绍了linux环境下对C语言程序进行预处理之后的文件进行编译的命令,用hello程序实际演示对编译结果hello.s的简单分析,通过源程序与汇编语言程序的对比,说明了编译器是怎么处理C语言的各个数据类型以及各类操作的,分数据,赋值,算数操作,关系操作,数组,控制转移,函数操作等方面按照类型和操作进行了分析。
第4章 汇编
4.1 汇编的概念与作用
汇编指的是汇编器将hello.s翻译成机器语言指令,把这些指令打包成可重定位目标文件,并将结果保存在目标文件hello.o中。
hello.o是一个二进制文件,包含hello程序执行的机器指令。汇编的作用是将汇编语言翻译成机器可以直接读取分析的机器指令。
4.2 在Ubuntu下汇编的命令
命令 gcc -c hello.s -o hello.o
结果如下图所示:
4.3 可重定位目标elf格式
1. 查看ELF头
命令:readelf -h hello.o
以16字节的序列Magic即魔数开始,描述了生成该文件的系统的字的大小和字节顺序。剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括 ELF 头的大小、目标文件的类型、机器类型、字节头部表的文件偏移,以及节头部表中条目的大小和数量等信息。
2. 查看节头部表
命令:readelf -S hello.o
以16字节的序列Magic即魔数开始,描述了生成该文件的系统的字的大小和字节顺序。剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括 ELF 头的大小、目标文件的类型、机器类型、字节头部表的文件偏移,以及节头部表中条目的大小和数量等信息。
2. 查看节头部表
命令:readelf -S hello.o
ELF文件中共有13个节,包含了文件中出现的节的类型、位置和大小等信息。每个节都从0开始,用于重定位。在文件头中得到节头部表的信息,再使用字节偏移信息得到各节在文件中的起始位置,以及各节所占空间的大小等。
部分节的名称及内容如下:
节名称 | 包含内容 |
.text | 已编译程序的机器代码 |
.rela.text | 一个.text节中位置的列表,链接器链接其他文件时,需修改这些内容 |
.data | 已初始化的全局和静态C变量 |
.bss | 未初始化的全局和静态C变量和所有被初始化为0的全局或静态变量 |
.rodata | 只读数据段 |
.comment | 包含版本控制信息 |
.symtab | 符号表,存放程序中定义和引用的函数和全局变量信息 |
3. 查看符号表
命令:readelf -s hello.o
符号表用来存放程序中定义和引用的函数和全局变量的信息,重定位需要引用的符号在其中声明。
4. 查看重定位条目
命令:readelf -r hello.o
描述了需要进行重定位的各种信息,包括需要进行重定位符号的位置、重定位的方式、名字。在hello.o中,对printf,exit等函数的未定义的引用替换为该进程的虚拟地址空间中机器代码所在的地址。
4.4 Hello.o的结果解析
objdump -d -r hello.o分析hello.o的反汇编。
机器语言是二进制机器指令的集合,而机器指令由操作码和操作数构成。
机器语言与汇编语言的映射关系:每一条汇编语言操作码都可以用机器二进制数据来表示,所有的汇编语言和二进制机器语言是一一映射关系。
分支转移:
反汇编代码跳转指令的操作数使用的不是段名称如.L3,段名称只是在汇编语言中便于编写的助记符。而在机器语言反汇编程序中,分支转移命令是直接跳转入目的地址。
函数调用:
在.s文件中,函数调用之后直接跟着函数名称,而在反汇编程序中,call的目标是相对地址。因为.c文件中调用的函数都是共享库中的函数,最终需要通过动态链接器才能确定函数的运行时执行地址,反汇编的代码已经知道了相对位置。在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其call指令后的相对地址设置为全0。
4.5 本章小结
本章介绍了hello从hello.s 到hello.o 的汇编过程,通过查看hello.o的ELF格式和使用objdump得到反汇编代码与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的格式
读取hello的ELF, 可以看到保存了可执行文件hello中的各个节的信息。如图5-2所示。hello文件中的节的数目比hello.o中多了很多,说明在链接过后有新文件添加进来。
hello的ELF头和hello.o的ELF头大体一致,但是类型从REL (可重定位文件)变为了EXEC (可执行文件),增加程序头起点,节头和字符串表索引节头的数量变多。
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息。
可以看出.interp段在虚拟地址0x4002e0处,.dynstr段在0x400398处,.init段在0x401000处等。
同时在Data Dump中查看hello的虚拟地址空间开始处为0x401000。
5.5 链接的重定位过程分析
objdump -d -r hello得到hello的反汇编文件:
hello.o的重定位项目:
从这段汇编代码中可以看到:
hello.o中的je,call,jmp后面跟的操作数是全0,而hello中是已经计算出来的相应段或函数的地址。根据这个不同可以分析出hello.o链接成为hello的过程中需要对重定位条目进行重定位,对相应的条目进行计算得到地址。并且hello的反汇编代码还有很多其他的函数,例如puts,printf,getchar等,从这个不同可以分析出链接会将共享库中函数的汇编代码加入hello.o中。
hello重定位的过程:
(1)重定位节和符号定义链接器将所有类型相同的节合并在一起后作为可执行目标文件的节。然后链接器把运行时的内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号,当这一步完成时,程序中每条指令和全局变量都有唯一运行时的地址。
(2)重定位节中的符号引用这一步中,连接器修改代码节和数据节中对每个符号的引用,使他们指向正确的运行时地址。执行这一步,链接器依赖于可重定位目标模块中称为的重定位条目的数据结构。
(3)重定位条目当编译器遇到对最终位置未知的目标引用时,它就会生成一个重定位条目。代码的重定位条目放在.rela.txt
重定位算法:
foreach section s
foreach relocation entry r
refptr = s + r.offset;/*ptr to reference to be relocated*/
if(r.type == R_X86_64_PC32)//PC相对寻址的引用
refaddr = ADDR(s) + r.offset;/*ref's run-time address*/
*refptr = (unsigned) (ADDR(r.symbol) + r.addend - refaddr);
/*Relocate an absolute reference*/
if( r.type == R_X86_64_32)//使用32位绝对地址
*refptr = (unsigned)(ADDR(r.symbol) + r.addend);
重定位地址计算公式为:
refaddr = ADDR(s) + r.offset
*refptr = (unsigned) (ADDR(r.symbol) + r.addend - reffaddr)
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。
程序名称 | 程序地址 |
ld-2.27.so!_dl_start | 0x7ffff7a03b00 |
ld-2.27.so!_dl_init | 0x7ffff7de37d0 |
libc-2.27.so!__libc_start_main | 0x7ffffc827ab0 |
-libc-2.27.so!__cxa_atexit | 0x7ffffc849430 |
libc-2.27.so!_setjmp | 0x7ffffc844c10 |
libc-2.27.so!exit | 0x7ffffc849128 |
5.7 Hello的动态链接分析
动态链接就是要将程序拆成几个独立的部分,在运行的时候将它们连接起来,与静态链接把所有模块都链接成一个可执行文件不同。
在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。GNU编译系统使用延迟绑定,将过程地址的绑定推迟到第一次调用该过程时。
动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。如果一个目标模块调用定义在共享库中的任何函数,那么就有自己的GOT和PLT。
PLT是一个数组,其中每个条目是16字节代码。每个库函数都有自己的PLT条目,PLT[0]是一个特殊的条目,跳转到动态链接器中。从PLT[2]开始的条目调用用户代码调用的函数。
GOT也是一个数组,每个条目是8字节的地址,和PLT联合使用时,GOT[2]是动态链接在ld-linux.so模块的入口点,其余条目对应于被调用的函数,在运行时被解析。每个条目都有匹配的PLT条目。
在elf文件中找到.got的地址0x403ff0
5.8 本章小结
本章介绍了链接的概念和作用,详细介绍了hello.o如何成为可执行的目标文件,详细介绍了hello.o的ELF形式和各节的意义,分析了hello的虚拟地址空间、重置进程、运行进程和动态链接过程。
第6章 hello进程管理
6.1 进程的概念与作用
进程是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
进程的作用:
1.每次用户向shell输入一个可执行目标文件的名字运行时,shell就会创建一个新的进程,然后在这个进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,并且在新进程的上下文中运行它们自己的代码或其他应用程序。
2.给应用程序两种关键抽象:
一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器;一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用内存系统。
6.2 简述壳Shell-bash的作用与处理流程
作用:
Shell是一个用C语言编写的程序,他是用户使用Linux的桥梁。Shell是一个交互型应用级程序,代表用户运行其它程序。
处理流程:
Shell首先检查命令是否是内部命令,若不是再检查是否是一个应用程序(可以是Linux本身的实用程序,如ls和rm,也可以是购买的商业程序,或者是自由软件)。然后Shell在可执行程序的目录列表里寻找这些应用程序。如果键入的命令不是一个内部命令并且在列表里没有找到这个可执行文件,将会显示一条错误信息。如果能够成功找到命令,该内部命令或应用程序将被分解为系统调用并传给Linux内核。
6.3 Hello的fork进程创建过程
Shell通过fork创建一个新的子进程。新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库和用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。子进程与父进程最大的区别就是有不同的pid。
fork被调用一次,返回两次。在父进程中fork返回子进程的pid,在子进程中fork返回0。父进程与子进程是并发运行的独立进程。
6.4 Hello的execve过程
execve函数加载并运行可执行目标文件hello,且带参数列表argv和环境变量envp。只有当出现错误时,例如找不到hello,execve才会返回到调用程序。所以,与fork一次调用返回两次不同,execve调用一次并从不返回。
加载并运行hello需要以下几个步骤:
1.删除已存在的用户区域。删除当前进程虚拟地址的用户部分存在的区域结构。
2. 映射私有区域。新建应用程序的代码、数据、bss和堆放区域的新区域结构。所有这些区域结构都是私人的,写的时候都是复印的。虚拟地址空间的代码和数据区域将被映射到Hello文件的.txt和.data区域。bss区域请求二进制0。 地图匿名文件。那个大小包含在Hello文件里。堆栈和堆栈区域也请求初始长度为0的二进制数。
3. 映射共享区域。如果hello程序与共享对象相连,例如标准C库libc.so所有对象都将被动态地连接到该程序中,并反映到用户的虚拟中。地址空间的共享区域。
4.设置程序计数器。exceve最后做的就是将当前进程上下文中的程序柜台设置为指向代码区域的入口。调用此进程从这个入口开始实行。Linux可根据需要更换代码和数据页面。
6.5 Hello的进程执行
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
上下文信息:
上下文是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。
进程时间片:
一个进程执行它的控制流的一部分的每一时间段叫做时间片。
上下文切换:
当内核选择一个新的进程运行时,则内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程:
1.保存以前进程的上下文。
2.恢复新恢复进程被保存的上下文。
3.将控制传递给这个新恢复的进程 ,来完成上下文切换。
用户模式和内核模式:
处理器通常是用某个控制寄存器中的一个模式位提供两种模式的区分功能,该寄存器描述了进程当前享有的特权。当设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据。
hello程序的运行:
进程调用execve函数,为hello分配好了虚拟地址空间,将代码段和数据段映射为可执行文件hello中的相应内容。假设hello进程在sleep之前一直在顺序执行。在执行到sleep函数的时候,切换到内核模式,将hello进程挂起,然后切换到用户模式执行其它进程。一段时间后发生中断,切换到内核模式,继续运行之前被挂起的进程。最后切换回用户模式,继续运行hello进程。
6.6 hello的异常与信号处理
hello执行过程中会出现的异常:
中断:信号SIGTSTP,默认行为是 停止直到下一个SIGCONT
终止:信号SIGINT,默认行为是 终止
程序正常运行的结果如下图所示,程序执行完后进程被回收,再按回车键退出程序。
运行时乱按时的结果如下图所示,乱按的输入并不会影响进程的执行,当按到回车键时,getchar会读入回车符,并且后面的字符串会当作shell的命令行输入。
运行时按Ctrl+Z后结果如下图所示。按下Ctrl+Z后父进程收到SIGTSTP信号,将hello进程挂起,ps命令列出当前系统中的进程。运行jobs命令列出当前shell环境中已启动的任务状态。
运行pstree命令,以树状图显示进程间的关系:
运行时按Ctrl+C后结果如下图所示。父进程收到SIGINT信号,终止hello进程,并且回收hello进程。
6.7本章小结
本章介绍了进程的概念及作用,shell的作用及其处理流程,并分析了hello的fork进程创建过程、execve过程和进程执行过程,最后根据不同情况分析了hello运行过程中的异常和信号处理。
第7章 hello的存储管理
7.1 hello的存储器地址空间
1.逻辑地址:
逻辑地址是指由程序产生的与段相关的偏移地址部分。逻辑地址由一个段(segment)和偏移量(offset)组成,偏移量指明了从段开始的地方到实际地址之间的距离。
2.线性地址:
线性地址是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。
3.虚拟地址:
虚拟地址是程序保护模式下,程序访问存储器所使用的逻辑地址称为虚拟地址,与实地址模式下的分段地址类似,虚拟地址也可以写为“段:偏移量”的形式,这里的段是指段选择器。就是hello里面的虚拟内存地址。
4.物理地址:
CPU通过地址总线的寻址,找到真实的物理内存对应地址。在前端总线上传输的内存地址都是物理内存地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
在保护模式下,段描述符占8个字节,无法直接存放在段寄存器中(段寄存器只有2字节)。X86的设计是段描述符存放在GDT或LDT中,而段寄存器存放的是段描述符在GDT或LDT内的索引值。一个逻辑地址由两部份组成,段标识符: 段内偏移量。
逻辑地址到线性地址的变换方法:
1. 给定一个完整的逻辑地址[段选择符:段内偏移地址],首先根据T1的值,确定当前要转换是GDT中的段,还是LDT中的段,再依据对应寄存器,得到其地址和大小。
2. 根据段选择符中前13位,在数组中查找到相应的段描述符,获得基地址。
3. 将基地址加上偏移量得到要转换的线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址(即虚拟地址VA)到物理地址(PA)之间的转换通过分页机制完成。而分页机制是对虚拟地址内存空间进行分页。
Linux系统有自己的虚拟内存系统。Linux将虚拟内存组织成一些段的集合,段之外的虚拟内存不存在因此不需要记录。内核为hello进程维护一个段的任务结构即图中的task_struct,其中条目mm指向一个mm_struct,它描述了虚拟内存的当前状态,pgd指向第一级页表的基地址(结合一个进程一串页表),mmap指向一个vm_area_struct的链表,一个链表条目对应一个段,所以链表相连指出了hello进程虚拟内存中的所有段。
系统将每个段分割为被称为虚拟页(VP)的大小固定的块来作为进行数据传输的单元,在linux下每个虚拟页大小为4KB,类似地,物理内存也被分割为物理页(PP/页帧)。虚拟内存系统中MMU负责地址翻译,MMU使用存放在物理内存中的被称为页表的数据结构将虚拟页到物理页的映射,即虚拟地址到物理地址的映射。
不考虑TLB与多级页表,虚拟地址分为虚拟页号VPN和虚拟页偏移量VPO,根据位数限制分析可以确定VPN和VPO分别占多少位是多少。通过页表基址寄存器PTBR+VPN在页表中获得条目PTE,一条PTE中包含有效位、权限信息、物理页号,如果有效位是0+NULL则代表没有在虚拟内存空间中分配该内存,如果是有效位0+非NULL,则代表在虚拟内存空间中分配了但是没有被缓存到物理内存中,如果有效位是1则代表该内存已经缓存在了物理内存中,可以得到其物理页号PPN,与虚拟页偏移量共同构成物理地址PA。
7.4 TLB与四级页表支持下的VA到PA的变换
Intel i7的地址翻译用到了TLB与四级页表的技术,其中虚拟地址48位,物理地址52位,页表大小为4KB,页表为4级。
由于页表大小是4KB,所以VPO为12位,VPN就是36位。每一个PTE条目是8字节,所以每一个页表就有512个条目,那么VPN中每一级页表对应的索引就有9位。TLB有16组,所以TLBI就是4位,TLBT就是32位。
CPU产生虚拟地址VA,传给MMU,MMU使用前36位作为VPN,在TLB中搜索,如果命中,就得到其中的40位物理页号PPN,与12位VPO合并成52位的物理地址PA。
如果TLB没有命中,MMU就向内存中的页表请求PTE。CR3是一级页表的起始地址,VPN1是第一级页表中的偏移量,用来确定二级页表的起始地址,VPN2又作为第二级页表的偏移量,以此类推,最后在第四级页表中找到对应的PPN,与VPO组合成物理地址PA。
7.5 三级Cache支持下的物理内存访问
L1 Cache是8路64组相连高速缓存。块大小64B。因为有64组,所以需要6 位CI进行组寻址,共有8路,块大小为64B,所以需要6位CO表示数据偏移位置,VA共52位,所以CT共40位。
在上一步中已经获得了物理地址VA,使用CI进行组索引,每组8路,对8路的块分别匹配CT(前40位)。如果匹配成功且块的valid标志位为1,则命中,根据数据偏移量CO(后六位)取出数据返回。
如果没有匹配成功或者匹配成功但是标志位是0,则不命中,向下一级缓存中查询数据(L2 Cache->L3 Cache->主存)。查询到数据之后,一种简单的放置策略如下:如果映射到的组内有空闲块,则直接放置,否则组内都是有效块,产生冲突(evict),则采用最近最少使用策略LFU进行替换。
7.6 hello进程fork时的内存映射
fork函数为新进程创建各种数据结构,并给它分配一个唯一的PID。为了给新的hello进程创建虚拟内存,它创建了当前进程的的mm_struct, vm_area_struct和页表的原样副本,两个进程中的每个页面都标记为只读,两个进程中的每个区域结构都标记为私有的写时复
以上是关于计算机系统大作业:程序人生-Hello‘s P2P的主要内容,如果未能解决你的问题,请参考以下文章
哈工大计算机系统大作业 程序人生-Hello‘s P2P 020
哈工大 计算机系统大作业 程序人生-Hello’s P2P From Program to Process