计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 未来技术
学 号 2021113562
班 级 21WL023
学 生 朱虹臻
指 导 教 师
计算机科学与技术学院
2023年5月
本文主要讲述hello所经历的过程,hello.c程序的一生需要经历预处理、 编译、汇编、链接、进程管理、存储管理、IO管理,下面分别对这些阶段进行分析,明白hello的一生经历了什么。
关键词:预处理;编译;汇编;链接;进程管理;存储管理;IO管理
目 录
9结论
第1章 概述
(0.5分)
1.1 Hello简介
1.P2P(from Program to Process)
Program指的是源程序,是在编译器(例如VS,Code:Blocks)中进行代码的编写,获得的源文件(hello.c)。Process指的是进程,是指根据写好的代码生成的运行进程。从Program到Process一共需要两步:
(1)从源代码到可执行文件
Linux下使用命令行:
gcc -m64 -no-pie -fno-PIC hello.c -o hello
源代码经过预处理阶段,编译阶段,汇编阶段,链接阶段得到可执行文件。过程如下图。
图1.1 hello的一生
(2)从可执行文件到进程
通过shell对可执行文件进行编译
./hello (学号) (姓名)
shell获取可执行文件后,加载和运行过程调用fork函数创建进程、execve函数运行函数,通过内存映射、分配空间等手段让hello拥有自己的空间和时间,与其他程序并发地运行。
2. O2O(from 0-Zero to 0-Zero):
O2O也分为两个阶段,从无到有,再从有到无。
(1)从无到有
Linux加载器execve()将程序计数器置为程序入口点,首先初始化执行函数,调用用户层的main函数,处理main函数的返回值,并在需要的时候把控制返回给内核。CPU为执行文件hello分配时间周期,执行逻辑控制流,每条指令通过流水线取值、译码、执行、访存、写回、更新PC。编译器会对程序代码进行优化,高速缓存让存取数据更加方便快捷,多级页表在物理内存中存取数据、指令,遇到异常会发送信号,I/O系统负责输入输出。
(2)从有到无
当程序运行结束时(例如通过键盘中断Ctrl+C手段),shell回收进程,释放hello的内存并且删除有关进程上下文。
1.2 环境与工具
硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上
软件环境:Windows10 64位;VMware Workstation Pro15.5.1;Ubuntu 20.04.4
开发和调试工具:gdb;edb;readelf;objdump;Code::Blocks20.03
1.3 中间结果
hello.i:hello.c预处理后的文件。
hello.s:hello.i编译后的文件。
hello.o:hello.s汇编后的文件。
hello:hello.o链接后的文件。
hello1asm.txt:hello.o反汇编后代码。
hello2asm.txt:hello反汇编后代码。
hello.o_elf:hello.o用readelf -a hello.o指令生成的文件。
hello_elf:hello用readelf -a hello指令生成的文件。
1.4 本章小结
本章根据hello的自白,概括介绍了hello的P2P和O2O的过程。此外,还介绍了本实验用到的硬软件环境和开发调试工具。
第2章 预处理
(0.5分)
2.1 预处理的概念与作用
2.1.1预处理的概念:
当对一个源文件进行编译时,系统将自动引用预处理程序对源程序中的预处理部分作处理,处理完毕自动进入对源程序的编译。C语言提供多种预处理功能,主要处理#开始的预处理指令,如宏定义(#define)、文件包含(#include)、条件编译(#ifdef)等。
2.1.2预处理的作用:
所有的预处理器(cpp)命令都是以井号(#)开头。它必须是第一个非空字符,为了增强可读性,预处理器指令应从第一列开始。
(1)添加对应的头文件在#include处
(2)删除#define并展开对应的宏,#undef取消已经定义的宏。
(3)#ifdef, 若宏已经定义,则返回真;#ifndef, 若宏没有定义,则返回真
(4)处理所有的条件预编译指令,例如#if#endif,根据“#if”后面的条件决定需要编译的代码。
(5) #if,如果给定条件为真,则编译下面代码;#else是#if 的替代方案;#elif,如果前面的#if给定条件不为真,当前条件为真,则编译下面代码;#endif,结束一个 #if……#else 条件编译块。
(6)#error,当遇到标准错误时,输出错误信息。
(7)#pragma,使用标准化方法,发布特殊的命令到编译器中。
2.2在Ubuntu下预处理的命令
打开终端,输入gcc –E hello.c –o hello.i 或 cpp hello.c > hello.i ,即可生成文本文件hello.i。
图2.2Ubuntu下预处理的命令
2.3 Hello的预处理结果解析
打开hello.i程序,程序是可读的文本文件,总共有3060行。观察发现,其中的注释已经消失,前一部分的代码为被加载到程序中的头文件;程序的最后一部分与hello.c中的main函数完全相同。
2.4 本章小结
第二章主要介绍了预处理,说明了预处理的过程,以hello.c为例,在Ubuntu下通过预处理生成了hello.i文件,并对预处理的结果进行了简析。
第3章 编译
(2分)
3.1 编译的概念与作用
3.1.1编译的概念:
编译器(cc1)将预处理之后生成的文本文件hello.i翻译成文本文件hello.s,它通常包含一个汇编语言程序。
3.1.2编译的作用:
将高级语言书写的源程序转换为一条条机器指令,机器指令和汇编指令一一对应,使机器更容易理解,为汇编做准备。
3.2 在Ubuntu下编译的命令
打开终端,输入cc1 hello.i -o hello.s 或 gcc -S hello.c -o hello.s
图3.2Ubuntu下编译的命令
3.3 Hello的编译结果解析
3.3.1数据:
Hello.c的代码如下:
图3-3-1 hello.c文件内容
1)常量
Hello.c的main函数中常量只有:用法: Hello 学号 姓名 秒数!\n
Hello %s %s\n
在hello.s文件中.LC0处.LC1处变成了ascii码,在main函数中调用常量的位置写成了LC0。其中两个字符串均存放在.rodata段中。
图3-3-2 hello.s文件内容
2)变量(全局/局部/静态)
代码中有局部变量i,argc,argv。
argc是函数参数,存储在寄存器%edi中。代码先放入了帧指针%rbp-20的位置。argv本来放在了寄存器%rsi中,使用时放在帧指针%rbp-32。
图3-3-3 hello.s文件内容
局部变量i作为循环计数器,先初始化为0,然后迭代每次与8比较判断循环是否结束。
图3-3-4 hello.s文件内容
3.3.2赋值
Hello.c中进行了一次赋值是在main函数中对int类型局部变量i进行赋值。
3.3.3算术操作
对于变量i来说进行了循环的加操作,对应汇编语句中的add指令。
3.3.4关系操作
一共有两次关系操作,第一次是比较argc与4是否不等;第二次是循环的判断条件i是否小于8。对应语句:
if(argc != 4);
for(i=0;i<8;i++);
3.3.5数组/指针/结构操作
数组:参数列表argv[]。
argv地址放在了寄存器RSI中,使用时放在栈中,帧指针RBP-32的过程。
当argc==3是在循环中引用了argv数组内容。
3.3.6控制转移
有一条if语句,判断argv是否等于4。还有一条for语句,用来进行循环。argc存在栈-20(%rbp)中与4比较,若是不相等继续顺序执行,调用exit;相等则跳转至.L2执行for循环。这一过程主要通过je进行:
图3-3-5 hello.s文件内容
for循环在.L2初始化,迭代结束后每次判断在.L3所示部分通过cmpl比较,jle比较,若i<=7跳转至.L4执行循环中的迭代,否则继续顺序执行printf、sleep函数.
3.3.7函数操作
图3-3-6 hello.s文件内容
调用了printf函数。将.rodata节的.LC0的printf格式串的地址存进EDI,作为第一个参数方便函数printf调用;调用了exit函数,将1存进EDI作为exit函数的第一参数以供调用;调用了sleep函数。
3.4 本章小结
编译过程将.i文件转换为.s文件。以hello.c为例,通过分析汇编代码简析了变量,赋值,类型转换,算术操作,关系操作,数组/指针/结构操作,相关转移函数操作,目的是理解高级语言的底层表示方法。
第4章 汇编
(2分)
4.1 汇编的概念与作用
4.1.1汇编的概念
汇编器(as)将汇编程序翻译为机器语言指令,然后把这些指令打包成可重定位目标程序的格式,并将结果保存在目标文件中(该文件是个二进制文件,文本编译器打开会乱码)。
4.1.2汇编的作用
生成机器指令,方便机器直接分析。
4.2 在Ubuntu下汇编的命令
打开终端输入 as hello.s -o hello.o 或 gcc -c hello.s -o hello.o
图4-2-1 Ubuntu下汇编as和gcc
4.3 可重定位目标elf格式
在终端输入命令行readelf -a hello.o > hello1elf.tx
4.3.1 ELF头
ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。
图4.3.1 readelf -h指令查看ELF头
hello.o的ELF以一个16进制序列:7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
作为ELF头的开头。这个序列描述了生成该文件的系统的字的大小为8字节和字节顺序为小端序。
ELF头的大小为64字节,目标文件的类型为REL(可重定位文件)、机器类型为Advanced Micro Devices X86-64即AMD X86-64、节头部表的文件偏移为0,以及节头部表中条目的大小,其数量为14。
4.3.2 节头部表(section header table)
图4.3.2 readelf -S指令查看节头表
.text节:以编译的机器代码,类型为PROGBITS,意为程序数据,旗标为AX,即权限为分配内存、可执行
.rela.text节:一个.text节中的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。
.data节:已初始化的静态和全局C变量。类型为PROGBITS,意为程序数据,旗标为WA,即权限为可分配可写。
.bss节:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量。在目标文件中这个节不占据实际的空间,它仅仅是一个占位符。类型为NOBITS,意为暂时没有存储空间,旗标为WA,即权限为可分配可写。
.rodata节:存放只读数据,例如printf中的格式串和开关语句中的跳转表。类型为PROGBITS,意为程序数据,旗标为A,即权限为可分配。
.comment节:包含版本控制信息。 .note.GNU_stack节:用来标记executable stack(可执行堆栈)。
.eh_frame节:处理异常。 .rela.eh_frame节:.eh_frame的重定位信息。
.shstrtab节:该区域包含节区名称。 .symtab节:一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。
.strtab节:一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部的节名字。
4.3.3 符号表
一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。符号表有汇编器构造,使用编译器输出到汇编语言.s文件中的符号。每个符号表是一个条目的数组hello.o程序的符号表包含编号Num、Value、Size、Type、Bind、Vis、Ndx、Name字段:
图4-3-2-4 readelf -s指令查看符号表
main是一个位于.text节(Ndx=1)偏移量(value)为0,大小为146字节的全局符号,类型为函数。
puts、exit、printf、sleep、getchar为NOTYPE未知类型,未定义(UND)符号。hello.c为文件,ABS表示不该被重定位的符号。
4.3.4 .rela.text节和.rela.eh_frame节
R_X86_64_32:重定位绝对引用。重定位时使用一个32位的绝对地址的引用,通过绝对寻址,CPU直接使用在指令中编码的32位值作为有效地址,不需要进一步修改。
R_X86_64_PC32:重定位PC相对引用。重定位时使用一个32位PC相对地址的引用。一个PC相对地址就是据程序计数器的当前运行值的偏移量。
(1)相对引用重定位算法:
refaddr = ADDR(s) + r.offset;
*refptr = (unsigned) (ADDR(r.symbol) + r.addend – refaddr);
(2)重定位绝对引用重定位算法:
*refptr = (unsigned) (ADDR(r.symbol) + r.addend);
4.4 Hello.o的结果解析
在终端输入objdump -d -r hello.o查看hello.o的反汇编,结果如下:
通过将反汇编与hello.s比较发现,汇编指令代码几乎相同,反汇编代码除了汇编代码之外,还显示了机器代码,在左侧用16进制表示。机器指令有操作码和操作数组成,和汇编指令一一对应。最左侧为相对地址。
其中跳转指令和函数调用等指令,在反汇编代码中表示为对应地址的偏移,而在hello.s中直接表示为函数名或定义的符号。在反汇编代码中,立即数是16进制显示的,而在hello.s中立即数是以十进制显示的。
4.5 本章小结
本章首先介绍了汇编的概念和作用,接着通过实操,对hello.s文件进行汇编,生成ELF可重定位目标文件hello.o,接着使用readelf工具,通过设置不同参数,查看了hello.o的ELF头、节头表、可重定位信息和符号表等,通过分析理解可重定位目标文件的内容。最后将其与hello.s比较,分析不同,并说明机器语言与汇编语言的一一对应关系。
第5章 链接
(1分)
5.1 链接的概念与作用
5.1.1链接的概念
链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可以被加载到内存并执行。
5.1.2链接的作用
链接使得分离编译,一个大的应用程序可以被分解为更小、更好管理的模块,可以独立地修改和编译这些模块。
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 -o hello
图5-2-1 Ubuntu下链接ld指令
5.3 可执行目标文件hello的格式
图5-3-1 可执行目标文件格式
可执行目标文件与可重定位文件稍有不同,ELF头中字段e_entry给出执行程序时的第一条指令的地址,而在可重定位文件中,此字段为0。可执行目标文件多了一个程序头表,也成为段头表,是一个结构数组。还多了一个.init节,用于定义init函数,该函数用来执行可执行目标文件开始执行时的初始化工作。因为可执行目标文件不需要重定位,所以比可重定位目标文件少了两个.rel节。
查看hello的ELF头:发现hello的ELF头中Type处显示的是EXEC,表示时可执行目标文件,这与hello.o不同。hello中的节的数量为30个。
图5-3-2 readelf -h查看hello的ELF头
图5-3-3 readelf -S查看hello的节头表
发现刚才提到的30个节的具体信息,在节头表中都有显示,包括大小Size,偏移量Offset,其中Address是程序被载入虚址地址的起始地址。
查看hello的程序头表,首先显示这是一格可执行目标文件,共有12个表项,其中有4个可装入段(Type=LOAD),VirtAddr和PhysAddr分别是虚拟地址和物理地址,值相同。Align是对齐方式,这里4个可装入段都是4K字节对齐。以第一个可装入段为例,表示第0x00000~0x005bf字节,映射到虚拟地址0x400000开头的长度为0x5c0字节的区域,按照0x1000=4KB对齐,具有只读(Flags=R)权限,是只读代码段。
图5-3-4 readelf -l查看hello的程序头表
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。由下图,可以看到虚拟地址空间的起始地址为0x400000。
图5-4-1 edb查看hello的虚拟地址空间
由图可以知道,.inerp段的起始地址为04002e0可以在edb中可以找到:
图5-4-2 edb查看.inerp段信息
text段的起始地址为0x4010f0,.rodata段的起始地址为0x402000,可以在edb中找到:
图5-4-3 edb查看.text段信息
图5-4-4 edb查看.rodata段信息
5.5 链接的重定位过程分析
objdump -d -r hello > hello2.txt
图5-5-1 objdump查看hello反汇编结果
hello的反汇编代码多了很多节,并且发现每条数据和指令都已经确定好了虚拟地址,不再是hello.o中的偏移量。通过链接之后,也含有了库函数的代码标识信息。
接着,我们具体比较分析一下hello和hello.o的反汇编结果,下面两个图分别为hello.o和hello的反汇编的部分截图,其余同理。
图5-5-2 objdump查看hello反汇编结果
图5-5-3 objdump查看hello反汇编结果
在hello.o中跳转指令和call指令后为绝对地址,而在hello中已经是重定位之后的虚拟地址。
接下来,以0x4011f6出的call指令为例,说明链接过程:
图5-5-4 readelf查看hello.o的重定位信息
查看该图可知,此处应该绑定第0xc个符号,同时链接器知道这里是相对寻址。接着查看hello.o的符号表,找到第12个符号puts,此处绑定puts的地址。
图5-5-5 readelf查看hello.o的符号表
在hello中找到puts的地址为0x401090。
图5-5-6 readelf查看hello.o的符号表
当前PC的值为call指令的下一条指令的地址,也就是0x4011fb。而我们要跳转到的地方为0x401090,差0x16b,因此PC需要减去0x16b,也就是加上0xff ff fe 95,由于是小端法,因此重定位目标处应该填入 95 fe ff ff。
图5-5-7 hello.o反汇编
5.6 hello的执行流程
子程序名 | 子程序地址 |
hello!_start | 0x00000000004010f0 |
hello!__libc_csu_init | 0x0000000000401270 |
hello!_init | 0x0000000000401000 |
hello!frame_dummy | 0x00000000004011d0 |
hello!register_tm_clones | 0x0000000000401160 |
hello!main | 0x00000000004011d6 |
hello!printf@plt | 0x0000000000401040 |
hello!atoi@plt | 0x0000000000401060 |
hello!sleep@plt | 0x0000000000401080 |
hello!getchar@plt | 0x0000000000401050 |
hello!exit@plt | 0x0000000000401070 |
hello!__do_global_dtors_aux | 0x00000000004011a0 |
hello!deregister_tm_clones | 0x0000000000401130 |
hello!_fini | 0x00000000004012e8 |
5.7 Hello的动态链接分析
动态链接项目中,查看dl_init前后项目变化。对于动态共享链接库中PIC函数,编译器加重定位记录,等待动态链接器处理,为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略,将过程地址的绑定推迟到第一次调用该过程。动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。
在dl_init调用之前,对于每一条PIC函数调用,调用的目标地址都实际指向PLT中的代码逻辑,初始时每个GOT条目都指向对应的PLT条目的第二条指令。
图5-7-1 查看.got的地址
在dl_init调用之后, 0x6008c0和0x6008c0处的两个8字节的数据分别发生改变。
和PLT联合使用时,GOT[0]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。其中GOT[1]指向重定位表(依次为.plt节需要重定位的函数的运行时地址)用来确定调用的函数地址, GOT[2]是动态链接器ld-linux.so模块中的入口点。
在之后的函数调用时,首先跳转到PLT执行.plt中逻辑,第一次访问时,GOT地址为下一条指令,将函数序号压栈,然后跳转到PLT[0],在PLT[0]中将重定位表地址压栈,然后访问动态链接器,在动态链接器中使用函数序号和重定位表确定函数运行时地址,重写GOT,再将控制传递给目标函数。之后如果对同样函数调用,第一次访问跳转直接跳转到目标函数。
5.8 本章小结
本章首先介绍了链接的概念和作用,详细说明了可执行目标文件的结构,及重定位过程。并且以可执行目标文件hello为例,具体分析了各个段、重定位过程、虚拟地址空间、执行流程等。
第6章 hello进程管理
(1分)
6.1 进程的概念与作用
6.1.1 进程的概念:
进程是指计算机中已运行的程序, 是系统进行资源分配和调度的基本单位, 是操作系统结构的基础。
6.1.2 进程的作用:
提供给应用程序的关键抽象:
(1)一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器。
(2)一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用内存系统。
6.2 简述壳Shell-bash的作用与处理流程
在计算机科学中,Shell俗称壳(用来区别于核),是指“为使用者提供操作界面”的软件(命令解析器)。它类似于DOS下的command.com和后来的cmd.exe。它接收用户命令,然后调用相应的应用程序。同时它又是一种程序设计语言。作为命令语言,它交互式解释和执行用户输入的命令或者自动地解释和执行预先设定好的一连串的命令;作为程序设计语言,它定义了各种变量和参数,并提供了许多在高级语言中才具有的控制结构,包括循环和分支。
Bash是一个命令处理器,通常运行于文本窗口中,并能执行用户直接输入的命令。Bash还能从文件中读取命令,这样的文件称为脚本。和其他Unix shell 一样,它支持文件名替换(通配符匹配)、管道、here文档、命令替换、变量,以及条件判断和循环遍历的结构控制语句。包括关键字、语法在内的基本特性全部是从sh借鉴过来的。其他特性,例如历史命令,是从csh和ksh借鉴而来。总的来说,Bash虽然是一个满足POSIX规范的shell,但有很多扩展。
处理流程:
第一步:用户输入命令。
第二步:shell对用户输入命令进行解析,判断是否为内置命令。
第三步:若为内置命令,调用内置命令处理函数,否则调用execve函数创建一个子进程进行运行。
第四步:判断是否为前台运行程序,如果是,则调用等待函数等待前台作业结束;否则将程序转入后台,直接开始下一次用户输入命令。
第五步:shell应该接受键盘输入信号,并对这些信号进行相应处理
6.3 Hello的fork进程创建过程
pid_t fork(void):创建的子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程fork时,子进程可以读取父进程中打开的任何文件。父进程与创建的子进程之间最大的区别在于它们有不同的PID。子进程中,fork返回0,父进程中,fork返回子进程PID。
6.4 Hello的execve过程
execve函数在当前进程的上下文中加载并运行一个新程序。execve函数加载并运行可执行文件filename(hello),且带参数列表argv和环境变量envp。只有当出现错误时,例如找不到filename,execve才会返回到调用程序。
当加载器运行时,它创建一个类似与图 6-2 的内存映像。在程序头部表的引导下,加载器将可执行文件的片复制到代码段和数据段,接下来,加载器跳转到程序的入口,_start函数的地址,这个函数是在系统目标文件ctrl.o中定义的,对所有的c程序都一样。_start函数调用系统启动函数,_libc_start_main,该函数定义在libc.so里,初始化环境,调用用户层的main函数,处理main函数返回值,并且在需要的时候返回给内核。
6.5 Hello的进程执行
1、上下文信息:操作系统使用一种称为上下文切换的较高层次的异常控制流来实现多任务。其实上下文就是进程自身的虚拟地址空间,分为用户级上下文和系统及上下文。每个进程的虚拟地址空间和进程本身一一对应(因此和PID一一对应)。由于每个CPU只能同时处理一个进程,而很多时候系统中有很多进程都要去运行,因此处理器只能一段时间就要切换新的进程去运行,而实现不同进程中指令交替执行的机制称为进程的上下文切换。
2、进程时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
图6-5-1 上下文切换机制
如上图所示,为进程A与进程B之间的相互切换。处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,为用户模式;设置模式为为内核模式。用户模式就是运行相应进程的代码段的内容,此时进程不允许运行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;而内核模式中,进程可以运行任何指令。
6.6 hello的异常与信号处理
1、异常和信号异常分为四类:中断、陷阱、故障、终止,属性如下:
图6-6 四种异常的属性
2、hello执行中可能出现的异常:
(1)中断:异步发生的。在执行hello程序的时候,由处理器外部的I/O设备的信号引起的。I/O设备通过像处理器芯片上的一个引脚发信号,并将异常号放到系统总线上,来触发中断。这个异常号标识了引起中断的设备。在当前指令完成执行后,处理器注意到中断引脚的电压变高了,就从系统总线读取异常号,然后调用适当的中断处理程序。在处理程序返回前,将控制返回给下一条指令。结果就像没有发生过中断一样。
(2)陷阱:陷阱是有意的异常,是执行一条指令的结果,hello执行sleep函数的时候会出现这个异常。
(3)故障:由错误引起,可能被故障处理程序修正。在执行hello时,可能出现缺页故障。
(4)终止:不可恢复的致命错误造成的结果,通常是一些硬件错误,比如DRAM或者 SRAM位被损坏时发生的奇偶错误。
3、键盘上操作导致的异常:
(1)运行时输入回车:
图6-6-1 运行hello时输入回车
- 运行时输入Ctrl+C
图6-6-2 运行hello时输入Ctrl+C
- 运行时输入Ctrl+Z
图6-6-3 运行hello时输入Ctrl+Z
(4)输入ps,监视后台程序
图6-6-4 输入ps
(5)输入jobs,显示当前暂停的进程
图6-6-5 输入jobs
(6)输入pstree,以树状图形式显示所有进程
图6-6-6 输入pstree
(7)输入fg,使停止的进程收到SIGCONT信号,重新在前台运行。
图6-6-7 输入fg
(8)输入kill,-9表示给进程34698发送9号信号,即SIGKILL,杀死进程。
图6-6-8 输入kill信号,杀死hello进程
6.7本章小结
本章介绍了进程的概念和作用,描述了shell如何在用户和系统内核之间建起一个交互的桥梁。讲述了shell的基本操作以及各种内核信号和命令,还总结了shell是如何fork新建子进程、execve如何执行进程、hello进程的上下文切换。
第7章 hello的存储管理
( 2分)
7.1 hello的存储器地址空间
1、逻辑地址:程序经过编译后出现在汇编代码中的地址。
2、线性地址:逻辑地址向物理地址转化过程中的一步,逻辑地址经过段机制后转化为线性地址,为描述符:偏移量的组合形式,分页机制中线性地址作为输入。
3、虚拟地址:也就是线性地址。
4、物理地址:CPU通过地址总线的寻址,找到真实的物理内存对应地址
7.2 Intel逻辑地址到线性地址的变换-段式管理
Intel平台下,逻辑地址的格式为段标识符:段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节。分段机制将逻辑地址转化为线性地址的步骤如下:
1、使用段选择符中的偏移值在GDT或LDT表中定位相应的段描述符。
2、利用段选择符检验段的访问权限和范围,以确保该段可访问。
3、把段描述符中取到的段基地址加到偏移量上,最后形成一个线性地址。
图7.2段式管理
7.3 Hello的线性地址到物理地址的变换-页式管理
概念上而言,虚拟内存被组织为一个由存放在磁盘上的N个连续字节大小的单元组成的数组。如下图,虚拟内存被分为一些固定大小的块,这些块称为虚拟页块。这些页块根据不同的映射状态也被划分为三种状态:未分配、为缓存、已缓存。
未分配:虚拟内存中未分配的页
未缓存:已经分配但是还没有被缓存到物理内存中的页
已缓存:分配后缓存到物理页块中的页
图7.3 页式管理
7.4 TLB与四级页表支持下的VA到PA的变换
页表是 PTE(页表条目)的数组,它将虚拟页映射到物理页,每个 PTE 都有一个有效位和一个 n 位地址字段,有效位表明该虚拟页是否被缓存在 DRAM 中。虚拟地址分为两个部分,虚拟页号(VPN)和虚拟页面偏移量(VPO)。其中VPN需要在PTE中查询对应,而VPO则直接对应物理地址偏移(PPO)。
多级页表:将虚拟地址的VPN划分为相等大小的不同的部分,每个部分用于寻找由上一级确定的页表基址对应的页表条目。如下图,VPN被分为k个部分,第一级VPN结合基址寄存器得到一个页表条目,其中存放下一级页表的基址,再结合VPN2,得到第三级页表基址,继续寻找,以此类推,直到最后确定对应的物理页号,与VPO结合,得到由PPN与PPO结合成的物理地址,用于物理地址寻址。
每次CPU产生一个虚拟地址,MMU(内存管理单元)就必须查阅一个PTE(页表条目),以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会从内存多取一次数据,代价是几十到几百个周期。如果PTE碰巧缓存在L1中,那么开销就会下降1或2个周期。然而,许多系统都试图消除即使是这样的开销,它们在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓存器(TLB)。Core i7是四级页表进行的虚拟地址转物理地址。48位的虚拟地址的前36位被分为四级VPN区。结合存放在CR3的基址寄存器,由前面多级页表的知识,可以确定最终的PPN,与VPO结合得到物理地址。
图7.4 Core i7地址翻译过程
7.6 hello进程fork时的内存映射
当fork 函数被shell调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID。为了给hello进程创建虚拟内存,它创建了hello进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。当fork 在hello进程中返回时,hello进程现在的虚拟内存刚好和调用fork 时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve 函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运 行包含在可执行目标文件 hello 中的程序,用 hello 程序有效地替代了当前程序。 加载并运行 hello 需要以下几个步骤:
1、删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存 在的区域结构。
2、映射私有区域,为新程序的代码、数据、bss 和栈区域创建新的区域结 构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为 hello 文件中的.text 和.data 区,bss 区域是请求二进制零的,映射到匿名 文件,其大小包含在 hello 中,栈和堆地址也是请求二进制零的,初始长 度为零。
3、映射共享区域, hello 程序与共享对象 libc.so 链接,libc.so 是动态链 接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
4、设置程序计数器(PC),execve 做的最后一件事情就是设置当前进程 上下文的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
缺页其实就是DRAM缓存未命中。当我们的指令中对取出一个虚拟地址时,若我们发现对该页的内存访问是合法的,而找对应的页表项式发现有效位为0,则说明该页并没有保存在主存中,出现了缺页故障。
此时进程暂停执行,内核会选择一个主存中的一个牺牲页面,如果该页面是其他进程或者这个进程本身页表项,则将这个页表对应的有效位改为0,同时把需要的页存入主存中的一个位置,并在该页表项储存相应的信息,将有效位置为1。然后进程重新执行这条语句,此时MMU就可以正常翻译这个虚拟地址了。
7.9本章小结
本章主要介绍了hello的存储器地址空间,逻辑地址到线性地址、线性地址到物理地址的变换,接着介绍了四级页表下的线性地址到物理地址的变换,分析了hello的内存映射,及缺页故障与缺页中断处理和动态存储分配管理。
结论
一开始,hello.c被程序员一字一键地敲进电脑,轻松地点击运行后,hello world!就出现在了屏幕上。虽然看起来简单,但是在深入了解后才发现,这一切并没有那么简单。最开始,hello.c程序安静地呆在磁盘里,等待着被执行。终于,hello.c经过了预处理,头文件被引入、宏被展开等,变成了hello.i文件。接着经过编译器的处理,变成了hello.s文件后,又经过汇编器的转换,变成了只有机器认识的二进制代码文件hello.o。但是这是还没有结束,hello.o文件还需经过链接才能最终成为可执行文件hello。可执行文件成功生成后,为了执行它,程序员在终端输入./hello。虽然结果瞬间就出来了,但是中间还是经历了很多。shell解析命令行输入的命令,然后调用fork创建子进程,并用execve映射到虚拟内存中。当CPU执行到hello时,开始读取对应的虚拟内存地址,通过缺页异常将hello放入主存中。之后通过四级页表、一层层缓存……终于hello被加载到了处理器内部。然后,再通过I/O包装的I/O函数,终于结果被输出到终端。最后hello.c程序被回收,重新进入硬盘……虽然hello的一生如此短暂,但是却坎坷而精彩!计算机系统这门课整体来说,感觉知识很多,难度也不小,不过很有趣。计算机系统远比我想象中的更复杂得多得多,其中的奥秘也很多。通过学习这门课,通过实际做实验、上网查资料等,我也了解到了许多新知识,提高了学习能力。总之,计算机系统这门课让我受益颇多,我之后也将继续学习相关知识。
附件
hello.i:hello.c预处理后的文件。
hello.s:hello.i编译后的文件。
hello.o:hello.s汇编后的文件。
hello:hello.o链接后的文件。
hello1asm.txt:hello.o反汇编后代码。
hello2asm.txt:hello反汇编后代码。
hello.o_elf:hello.o用readelf -a hello.o指令生成的文件。
hello_elf:hello用readelf -a hello指令生成的文件。
参考文献
为完成本次大作业你翻阅的书籍与网站等
[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分,缺失 -1分)