摘 要
经过一个学期的学习,再回顾初试编程时打出的hello
world程序,我们能够更加理解hello程序人生的所有关键节点。通过这种梳理,完成对整个学期的学习内容的回顾。我们运用了Ubuntu下的编程与调试工具,通过细致的分析,加深对与计算机系统知识的掌握。
关键词:hello world;程序人生;计算机系统;Ubuntu;
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第1章 概述
1.1 Hello简介
P2P:
即Program to Process:
-
Program: 在编辑器中编写代码得到的hello.c源程序
-
Process:
hello.c在Ubuntu系统中使用cpp进行与粗粒,使用ccl进行编译,使用as进行汇编,使用ld进行连接,最获得了可执行的目标文件hello。并可以通过使用shell键入文件名,执行程序获得进程。
020:
即在虚拟内存中申请并映射虚拟内存,最终释放所有资源的过程。
-
Shell获得了hello的文件名之后,调用fork函数,然后为hello进程执行execve,将执行文件内容映射到虚拟内存
-
进入程序的入口,载入物理内存,并进行时间片的分配,并执行逻辑控制流。
-
当程序运行结束之后,sell作为父进程回收hello进程所分配的资源,最后内核删除相关的数据结构。
1.2 环境与工具
硬件环境:
处理器:Intel® Core™ i5-8250U CPU @1.60GHz,1800 Mhz,4个内核,8个逻辑处理器
RAM:8.00GB
系统类型:64位操作系统,基于x64的处理器。
软件环境:Windows 10 64位,Ubuntu 20.04
开发与调试工具:gcc, as, ld, visual studio code, edb , readelf, objdump
1.3 中间结果
文件类型 | 文件名 |
---|---|
预处理之后得到的文件 | hello.i |
编译之后得到的文件 | hello.s |
汇编之后的可重定位目标文件 | hello.o |
链接之后的可执行目标文件 | Hello |
hello.o ELF格式代码 | Elf_hello.txt |
hello.o反汇编代码 | Disas_hello.s |
hello的ELF格式 | hello.elf |
hello的反汇编代码 | hello_objdump.s |
1.4 本章小结
本章介绍了P2P,020的含义和大致流程,介绍了大作业中所使用的硬件,软件环境和开发工具,最后简述了从.c文件到可执行文件还有调试过程中所经历的过程文件。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
概念:
预处理会解释预处理指令,即以#开始的行的内容,其中ISO
C/C++支持了#if,#ifdef,#ifndef,#else,#elif,#endif,#define,#include,#line,#error,#gragma,#。源代码中因为有了预处理指令,可以在不同的执行环境中被方便地兼容下来并且被修改和编译。
作用:
-
#include形式将后面声明地文件内容复制到新的程序中,hello.c中的#include<stdio.h>使得预处理器读取头文件stdio.h等内容,并将其插入到.c源程序代码文本之中。
-
在预处理阶段,#define后面的用空白字符分割的两个字符串中的前者,会在整个文件中的所有出现被后者代替。
-
会根据#if后面的条件限定后续需要编译的代码。
-
可以替换掉一些特殊的符号,并用合适的值进行替换。
2.2在Ubuntu下预处理的命令
运行的指令:
Linux>cpp hello.c > hello.i
图2-1
2.3 Hello的预处理结果解析
程序的长度被大大拓展了。头文件中的内容代替了原来的include预处理指令的位置。并且在这些头文件之中的预处理指令也被递归地进行了处理。最终的.i文件将不包含任何的include,define等预处理指令,预编译程序识别出了一些特殊符号,并对这些特殊符号的出现使用合适的只进行替换。
图2-2
本章小结
本章介绍了预处理的意义和过程,包括头文件的展开,注释的去掉,条件编译,宏替换等,以及Ubuntu下进行预处理的指令,最后我们对hello.c的文件的与处理结果hello.i进行了分析,更加透彻的了解了预处理究竟是在做什么。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
概念:
编译驱动程序的工作就是通过分析语言的词法和语法,判断所有的源程序内容是否符合c语言的标准,然后将其翻译成中间代码或是汇编代码,编译器在这一步会将hello.i文件翻译成文本文件hello.s。这个文件中的代码被翻译成汇编代码。
作用:
-
语法分析:
编译器的语法分析器以c源代码作为输入,分析其中的单词符号字符串是否形成符合规范的语法单位。
-
中间代码的生成:
源程序的一种编译内部的表示,被称作中间语言,中间语言存在的意义是是的被编译的程序的结构在逻辑上更加接近与机器指令,在逻辑上更加简单具体,并在这一步对于源程序的编译开始进行优化。对于程序进行各种在程序执行效果上的等价的优化,使得变换后的程序生成更有效率的执行代码。
-
目标代码:
在这里,中间代码被转换成目标代码,这里可以指汇编语言代码,经过汇编之后才可以成为能够执行的机器语言代码。
3.2 在Ubuntu下编译的命令
命令行输入:
Linux> gcc -S hello.i -o hello.s
图3-1
3.3 Hello的编译结果解析
3.3.1 文件头部
伪指令 | 含义 |
---|---|
.file | C文件声明 |
.text | 代码段 |
.section .rodata | 只读数据段 |
.align | 对指令或者数据的存放地址进行对齐的方式 |
.long .string | 声明long型、string型数据 |
.globl | 声明全局变量 |
.type | 指明函数类型或对象类型 |
.size | 声明变量大小 |
图3-2 hello.s头部段
数据
hello.c中有整数、数组和字符串
-
整数
-
int argc
argc作为第一个参数传入,图3-4结合汇编代码分析argc保存位置。图中的edi就是argc。
图3-3 argc保存位置
-
立即数常量
常量被保存在代码段,在图3-3中的立即数4就是以这种方式保存的。
-
int i
局部变量被编译器保存在栈中或是在寄存器当中,在hello.s中编译器将i存储在%rbp-4,即%rsp-4中,观察到栈增长了四个字节,这符合存储一个int型变量的要求。
图3-4 i保存位置
-
数组
char *argv[]
char*
类型数组的元素大小为8字节,argv指向对齐为8字节的一组这样的元素的起始位置。这段代码中的rsi存储的就是argv,则这一步中将rsi中的内容对放入了栈中。
图3-5 *argv保存位置
-
字符串
-
“用法: Hello 学号 姓名 秒数!\n”
这是一个字符串常量,是printf调用传入的参数。
图3-6 Usage: Hello 学号 姓名!\n保存位置
-
“Hello %s %s\n”
第二个printf传入的格式转换字符串,存放在只读数据段.rodata中。如图3-8所示。
图3-7 Hello %s %s\n保存位置
赋值
-
int i
i是栈中保存的局部变量,在这里使用mov指令对i进行赋值。这个指令为movl的原因是i是四个字节的。
图3-8 i赋值信息
类型转换
无
算数操作
-
for循环中i的自增
自增操作就是加一操作,采用add指令,addl是因为i是int型变量。
图3-9 i++指令解释
-
leaq计算LC1段地址
for循环中,需要对LC1中的字符串进行打印,计算了LC1的地址,使用的就是leaq指令,并且是通过相对寻址计算的。
图3-10 leaq取LC1段地址
关系操作
进行关系操作的指令如下:
指令 | 基于 | 解释 |
---|---|---|
CMP S1, S2 | S2-S1 | 比较设置条件码 |
TEST S1, S2 | S1&S2 | 测试设置条件码 |
SET** D | D=** | 按照**将条件码设置D |
J** | —— | 根据**与条件码进行跳转 |
程序中涉及的关系运算如下:
-
argc!=4
判断argc不等于4,hello.s中使用cmpl $4,
-20(%rbp),计算argc-4,但是不改变寄存器文件中的变量,只改变条件码寄存器中的内容,设置条件码,为之后je跳转做准备。
图3-11 argc!=3汇编解释
-
i<8
判断i<10,hello.s中使用cmpl $7,
-4(%rbp),计算i-7的值,设置条件码,为之后jle跳转做准备。
图3-12 i<10汇编解释
控制转移
程序中控制转移共有两处:
-
if (argv!=4)
和上面对关系的操作相关联,在进行判断之后,使用je判断ZF标志位,如果为0,说明argv-4=0,条件判断为else,直接跳转到.L2,否则顺序执行下一条语句,即执行if中的代码。
图3-13 if语句执行
-
for(i=0;i<8;i++)
使用计数变量i循环10次。首先无条件跳转到位于循环体.L4之后的比较代码,使用cmpl进行比较,如果i<=7,则跳入.L4
for循环体执行,否则说明循环结束,顺序执行for之后的逻辑。如图3-16所示。
图3-14 for语句执行
数组操作
在这个源程序中的循环内部,每一次循环都需要访问argv[1]、argv[2]和argv[3],首先将argc[1]和argv[2]作为printf的参数,然后将argv[3]作为atoi的参数。
-32(%rbp)存放着argv[1]中的内容,学号字符串。
-32(%rbp)+8存放着argv[2]中的内容,姓名。
-32(%rbp)+24即存放着argv[3]中的字符串首地址。
图3-15 argv[1]与argv[2]具体操作
函数操作
函数是一种过程,过程提供了一种封装代码的方式,用一组指定的参数和可选的返回值实现某种功能。P中调用函数Q包含以下步骤:
-
传递控制:
进行过程Q的时候,PC必须设置为Q的代码的起始地址,然后在返回时,要把PC设置为P中调用Q后面那条指令的地址。
-
传递数据:
P必须能够向Q提供一个或多个参数,Q能够向P中返回一个值。
-
分配和释放内存:
在开始时,Q可能需要为局部变量分配空间,而在返回前,又必须释放这些空间。
hello.c中涉及的函数操作有:
-
main函数:
-
传递控制:
main函数因为被调用call才能执行(被系统启动函数__libc_start_main调用),call指令将下一条指令的地址压栈,然后跳转到main函数。
1.2 传递数据:
外部调用过程向main函数传递参数argc和argv,分别使用%edi和%rsi存储,函数正常出口为return
0,将%eax设置0返回。 -
分配和释放内存:
使用%rbp记录栈帧的底,函数分配栈帧空间在%rbp之上,程序结束时,调用leave指令,leave相当于
mov %rbp,%rsp
pop %rbp
恢复栈空间为调用之前的状态,然后ret返回,ret相当pop IP。
-
-
printf函数:
2.1 传递数据:
第一次printf将%rdi设置为"用法: Hello 学号 姓名
秒数!\n"字符串的首地址。第二次printf设置%rdi为"Hello %s
%s\n"的首地址,设置%rdx为argv[1],%rsi为argv[2]。-
传递控制:
第一次printf因为只有一个字符串参数,所以call
puts@PLT;第二次printf使用call printf@PLT。
-
-
atoi函数:
3.1 传递数据:
将argv[3]的首地址通过%rdi传入这个函数。
3.2 传递控制:
call atoi@PLT
-
exit函数:
-
传递数据
将%edi设置为1。
4.2 传递控制
call exit@PLT。
-
-
sleep函数:
-
传递数据
将%edi设置为atoi返回值。
5.2 传递控制
call sleep@PLT。
-
-
getchar函数:
传递控制call getchar@PLT
3.4 本章小结
本章介绍了编译的概念意义,和执行过程,同时分析了生辰的汇编代码。介绍汇编代码怎样进行变量,常量,参数传递,分支,循环等操作。编译程序的工作是通过词法分析和语法分析,在判断好c语言源程序中的代码符合规则之后,将其翻译成抑郁变换的中间代码,并进行优化,得到汇编代码。经过了这一个步骤,我们理解了编译过程中如何将c语言源程序,转换成贴近计算机指令底层的汇编代码。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
概念:
编译器驱动程序运行汇编器as之后,汇编语言被翻译成机器语言,即由文本文件hello.s得到了二进制文件hello.o,这个机器语言文件也就是可重定位的目标文件。
作用:
将汇编语言转换成机器指令,这些指令可以被重定位,有着可重定位目标程序的格式,这些二进制指令所在的文件是一个.o文件,包含着源程序的机器指令编码。
4.2 在Ubuntu下汇编的命令
执行指令:
Linux> as hello.s -o hello.o
图4-1
4.3 可重定位目标elf格式
4.3.1 阅读elf格式的可重定向的目标文件:
Linux> readelf -a hello.o > Elf_hello.txt
图4-2
- ELF头:
包含了系统信息,编码的方式,ELF头的大小,节得的大小的一些列的信息:
图4-3
系统信息
- 节头部表:
这个部分,描述了.o文件中的各个接的类型位置所占的空间等信息。
图4-4
- 重定位节:
描述了引用的外部符号等信息,需要在连接过程中对重定向节的这些位置的地址继续宁修改,连接器通过重定向条目的类型使用不同的算法计算正确的重定向引用值。
在我们的例子中需要重定向:rodata段中的,puts,exit,printf, sleep,getchar。
图4-5
- 符号表
.symtab节是一个符号表,存放了程序中定义的函数的变量的信息。
图4-6
Hello.o的结果解析
执行指令:
Linux> objdump -d -r hello.o > Disas_hello.s
图4-7
分析:
-
在hello.s中存在一些数的表示仍为十进制的,但是在hello.o的汇编代码中的所有数的表示都被替换成了十六进制的表示。
-
分支跳转中,hello.s中存在一些伪指令,标记了跳转的位置,而在反汇编代码中这里被替换成了间接地址。
-
call在hello.s中都是后接函数的名称,而反汇编代码中使用的都是相对于main的偏移地址。产生这一去别的原因是在链接之后,运行时的地址才得以确定,因此重定向条目的引用值发生了改变。
4.5 本章小结
我们对汇编的结果进行了详细分析,编译器操作之后没汇编语言被转化为二进制的机器语言,hello.o可重定向目标文件后需要经过链接。对比hello.s和hello.o反汇编之后的代码,理解了汇编代码到机器语言发生的变化,在这个过程中已经开始为链接做出相应的准备。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
概念:
链接是指在电子计算机程序的各模块之间传递参数和控制命令,并把它们组成一个可执行的整体的过程。总之是把多个文件拼接合并成一个可执行文件。
作用:
链接可以在编译、汇编、加载和运行时执行。链接方便了模块化编程。
5.2 在Ubuntu下链接的命令
ld链接命令: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。
图5-1
5.3 可执行目标文件hello的格式
可执行目标文件hello的格式类似于可重定位目标文件的格式,ELF头描述文件的总体格式。它还包括程序的入口点,也就是当程序运行时要执行的第一条指令的地址。.text、.rodata和.data节与可重定位目标文件中的节是类似的,除了这些节已经被重定位到它们最终的运行时的内存地址外。.init节定义了一个小函数_init,程序初始化代码会调用它。因为可执行文件时完全连接的,所以无.rel节。
5.4 hello的虚拟地址空间
.interp节
图5-2
.init节
图5-3
.plot节
图5-4
.text节
图5-5
.data节
图5-6
.rodata节
图5-7
程序头部表,展示了ELF可执行文件的连续的片被映射到连续的内存段的映射关系。其中展示了目标文件中的偏移,内存地址,对其要求,目标文件中的段大小,内存中的段大小,运行时访问权限等信息。
图5-8
5.5 链接的重定位过程分析
使用objdump -d -r hello > asm1.txt
生成反汇编代码。观察asm.txt与asm1.txt分体hello与hello.o的不同主要有以下几点:
原先由0填充的跳转地址,已经由相对偏移地址,变成了该函数或者语句所在的虚拟内存地址。
hello中
图5-9
图5-10
这时的重定向引用还为0。
而且上面的地址重从零开始的相对位置修改成了绝对位置。
程序添加了许多动态链接库中的函数,程序原先调用的库函数都被复制到了程序的代码中来。
图5-11
原先存储在data及radata节中的信息,已经被程序放到了虚拟内存映射的空间中,再次调用时会直接从虚拟内存的相应位置读取(如sleepsecs及printf函数的格式串
5.6 hello的执行流程
使用edb单步调试执行观察到以下的结果。
载入:
_dl_start
_dl_init
开始执行:
__stat
_cax_atexit
_new_exitfn
_libc_start_main
_libc_csu_init
运行:
_main
_printf
_exit
_atoi
_sleep
_getchar
_dl_runtime_resolve_xsave
_dl_fixup
_dl_lookup_symbol_x
退出:
exit
5.7 Hello的动态链接分析
动态链接库中的函数在程序执行的时候才会确定地址,所以编译器无法确定其地址,在汇编代码中也无法像静态库的函数那样体现。
基于数据段与代码段相对距离不变这一个事实,代码段中任何指令和数据段中任何变量之间的距离都是一个运行时常量。
动态链接器使用过程链接表PLT+GOT(全局偏移量表)实现函数的动态链接,GOT中存放目标函数目标地址,PLT使用GOT中地址跳转到需要调用的目标函数。
PLT是一个数组,其中每个条目是16字节代码。每个库函数都有自己的PLT条目,PLT[0]是一个特殊的条目,跳转到动态链接器中。从PLT[2]开始的条目调用用户代码调用的函数。
GOT也是一个数组,每个条目是8字节的地址,和PLT联合使用时,GOT[2]是动态链接在ld-linux.so模块的入口点,其余条目对应于被调用的函数,在运行时被解析。每个条目都有匹配的PLT条目。
如图5-11、5-12所示,GOT表在动态链接前后的变化。
图5-12 do_init之前
图5-13 do_init之后
5.8 本章小结
本章介绍了hello在真正成为process之前的最后一步——链接。简单跟踪了hello的链接过程.重点分析了重定位和动态链接中的PIC调用和引用。
(以下格式自行编排,编辑时删除)
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程是一个执行中的程序的实例,每一个进程都有它自己的地址空间,一般情况下,包括文本区域、数据区域、和堆栈。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储区着活动过程调用的指令和本地变量。
进程为用户提供了以下假象:我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存,处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
作用:解释命令,连接用户和操作系统以及内核
流程:
分词,判断命令是否为内部命令,如果不是,则寻找可执行文件进行执行,重复这个流程:
-
Shell首先从命令行中找出特殊字符(元字符),在将元字符翻译成间隔符号。元字符将命令行划分成小块tokens。Shell中的元字符如下所示:SPACE
, TAB , NEWLINE , & , ; , ( , ) ,< , > , | -
程序块tokens被处理,检查看他们是否是shell中所引用到的关键字。
-
当程序块tokens被确定以后,shell根据aliases文件中的列表来检查命令的第一个单词。如果这个单词出现在aliases表中,执行替换操作并且处理过程回到第一步重新分割程序块tokens。
-
Shell对~符号进行替换。
-
Shell对所有前面带有$符号的变量进行替换。
-
Shell将命令行中的内嵌命令表达式替换成命令;他们一般都采用$(command)标记法。
-
Shell计算采用$(expression)标记的算术表达式。
-
Shell将命令字符串重新划分为新的块tokens。这次划分的依据是栏位分割符号,称为IFS。缺省的IFS变量包含有:SPACE
, TAB 和换行符号。 -
Shell执行通配符* ? [ ]的替换。
-
shell把所有從處理的結果中用到的注释删除,並且按照下面的顺序实行命令的检查:
-
内建的命令
-
shell函数(由用户自己定义的)
-
可执行的脚本文件(需要寻找文件和PATH路径)
-
-
在执行前的最后一步是初始化所有的输入输出重定向。
-
最后,执行命令。
6.3 Hello的fork进程创建过程
在终端中输入./hello 1190201818 lianchen 1,shell判断它不是内置命令,于是会加载并运行当前目录下的可执行文件hello。
此时shell通过fork创建一个新的子进程。新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库和用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。子进程与父进程最大的区别就是有不同的pid。
fork被调用一次,返回两次。在父进程中fork返回子进程的pid,在子进程中fork返回0父进程与子进程是并发运行的独立进程。
6.4 Hello的execve过程
execve函数在新创建的子进程的上下文中加载并运行hello程序。execve函数的功能是加载并运行可执行目标文件filename,且带参数列表argv和环境变量列表envp。只有发生错误时execve才会返回到调用程序。所以,execve调用一次且从不返回。
加载并运行hello需要以下几个步骤:
-
删除已存在的用户区域
删除当前进程虚拟地址的用户部分中已存在的区域结构。
-
映射私有区域
为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区被映射为hello文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。
- 映射共享区域
如果hello程序与共享对象链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址中的共享区域。
- 设置程序计数器
设置当前进程上下文中的程序计数器,使之指向代码段的入口点。下一次调度这个进程时,它将从这个入口点开始执行。
6.5 Hello的进程执行
逻辑控制流:
一系列程序计数器 PC
的值的序列叫做逻辑控制流。由于进程是轮流使用处理器的,同一个处理器每个进程执行它的流的一部分后被抢占,然后轮到其他进程。
用户模式和内核模式:
处理器使用一个寄存器提供两种模式的区分。用户模式的进程不允许执行特殊指令,不允许直接引用地址空间中内核区的代码和数据;内核模式进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
上下文:
上下文就是内核重新启动一个被抢占的进程所需要恢复的原来的状态,由寄存器、程序计数器、用户栈、内核栈和内核数据结构等对象的值构成。
示例:sleep进程的调度过程
图 6-1 进程上下文切换
初始时,控制流再hello内,处于用户模式
调用系统函数sleep后,进入内核态,此时间片停止。
2s后,发送中断信号,转回用户模式,继续执行指令。
调度的过程:
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行,我们说内核调度了这个进程。在内核调度了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。
以执行sleep函数为例,sleep函数请求调用休眠进程,sleep将内核抢占,进入倒计时,当倒计时结束后,hello程序重新抢占内核,继续执行。
用户态与核心态转换:
为了能让处理器安全运行,不至于损坏操作系统,必然需要先知应用程序可执行指令所能访问的地址空间范围。因此,就存在了用户态与核心态的划分,核心态可以说是“创世模式”,拥有最高的访问权限,处理器以一个寄存器当做模式位来描述当前进程的特权。进程只有故障、中断或陷入系统调用时才会得到内核访问权限,其他情况下始终处于用户权限之中,保证了系统的安全性。
6.6 hello的异常与信号处理
正常运行状态:
图 6-2 正常运行状态
异常类型:
类别 | 原因 | 同步/异步 | 返回行为 |
---|---|---|---|
中断 | 来自I/O设备的信号 | 异步 | 总是返回到下一条指令 |
陷阱 | 有意的异常 | 同步 | 重视返回到下一条指令 |
故障 | 潜在的可恢复的错误 | 同步 | 可能返回到当前指令 |
终止 | 不可恢复的错误 | 同步 | 不返回 |
处理方式:
图6-3 中断处理方式
图6-4 陷阱处理方式
图6-5 故障处理方式
图6-6 终止处理方式
**按下Ctrl+Z:**进程收到 SIGSTP 信号, hello
进程挂起。用ps查看其进程PID,可以发现hello的PID是2681;再用jobs查看此时hello的后台
job号是1,调用 fg 1将其调回前台。
图6-7 按下Ctrl+Z运行效果
**Ctrl+C:**进程收到 SIGINT 信号,结束
hello。在ps中查询不到其PID,在job中也没有显示,可以看出hello已经被彻底结束。
图6-8 按下Ctrl+C运行效果
**前台程序运行时按其他键盘按键:**只是将屏幕的输入缓存到缓冲区。乱码被认为是命令。
图6-9 中途乱按运行状态
**Kill命令:**挂起的进程被终止,在ps中无法查到到其PID。
图6-10 输入kill命令运行状态
。
6.7本章小结
进程给应用程序提供的关键抽象,使得进程可以并发地执行。信号和异常的处理,使得并发执行的过程变得井然有序,如信号处理程序,一切都按照规shell-bash的建立,给用户和进程之间提供了一个操作平台。
(第6章1分)
第7章 hello的存储管理
hello的存储器地址空间
- 逻辑地址
逻辑地址是指由程序产生的与段相关的偏移地址部分。逻辑地址由一个段(segment)和偏移量(offset)组成,偏移量指明了从段开始的地方到实际地址之间的距离。即hello.o里相对偏移地址。
- 线性地址
线性地址是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。
- 虚拟地址
虚拟地址是程序保护模式下,程序访问存储器所使用的逻辑地址称为虚拟地址,与实地址模式下的分段地址类似,虚拟地址也可以写为“段:偏移量”的形式,这里的段是指段选择器。就是hello里面的虚拟内存地址。
- 物理地址
CPU通过地址总线的寻址,找到真实的物理内存对应地址。
CPU对内存的访问是通过连接着CPU和北桥芯片的前端总线来完成的。在前端总线上传输的内存地址都是物理内存地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部分组成,段选择符和段内偏移量。
段选择符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。TI:0为GDT,1为LDT。Index指出选择描述符表中的哪个条目,RPL请求特权级。
最初8086处理器的寄存器是16位的,为了能够访问更多的地址空间但不改变寄存器和指令的位宽,所以引入段寄存器,8086共设计了20位宽的地址总线,通过将段寄存器左移4位加上偏移地址得到20位地址,这个地址就是逻辑地址。将内存分为不同的段,段有段寄存器对应,段寄存器有一个栈、一个代码、两个数据寄存器。
分段功能在实模式和保护模式下有所不同。
- 实模式
即不设防,说逻辑地址=线性地址=实际的物理地址。段寄存器存放真实段基址,同时给出32位地址偏移量,则可以访问真实物理内存。
- 保护模式
线性地址还需要经过分页机制才能够得到物理地址,线性地址也需要逻辑地址通过段机制来得到。段寄存器无法放下32位段基址,所以它们被称作选择符,用于引用段描述符表中的表项来获得描述符。描述符表中的一个条目描述一个段,构造如图7-2所示:
图7-2 段描述符
对各个部分的功能做简要介绍:
Base:基地址,32位线性地址,指向段的开始。
Limit:段界限,指出这个段的大小。
DPL:描述符的特权级0(最高特权,内核模式)-3(最低特权,用户模式)。
所有段描述符被保存在两个表中:全局描述符表(GDT)和局部描述符表(LDT)。电脑中的每一个CPU(或一个处理核心)都含有一个叫做gdtr的寄存器,用于保存GDT的首个字节所在的线性内存地址。为了选出一个段,必须向段寄存器加载以上格式的段选择符。
在保护模式下,分段机制就可以描述为:通过解析段寄存器中的段选择符在段描述符表中根据Index选择目标描述符条目Segment
Descriptor,从目标描述符中提取出目标段的基地址Base
address,最后加上偏移量offset共同构成线性地址Linear Address。
7.3 Hello的线性地址到物理地址的变换-页式管理
页式管理是一种内存空间存储管理的技术,页式管理分为静态页式管理和动态页式管理。将各进程的虚拟空间划分成若干个长度相等的页(page),页式管理把内存空间按页的大小划分成片或者页面(page
frame),然后把页式虚拟地址与内存地址建立一一对应页表,并用相应的硬件地址变换机构,来解决离散地址变换问题。页式管理采用请求调页或预调页技术实现了内外存存储器的统一管理。
图7-3 页式管理流程图
优点:
-
由于它不要求作业或进程的程序段和数据在内存中连续存放,从而有效地解决了碎片问题。
-
动态页式管理提供了内存和外存统一管理的虚存实现方式,使用户可以利用的存储空间大大增加。这既提高了主存的利用率,又有利于组织多道程序执行。
缺点:
-
要求有相应的硬件支持。例如地址变换机构,缺页中断的产生和选择淘汰页面等都要求有相应的硬件支持。这增加了机器成本。
-
增加了系统开销,例如缺页中断处理机,
-
请求调页的算法如选择不当,有可能产生抖动现象。
-
虽然消除了碎片,但每个作业或进程的最后一页内总有一部分空间得不到利用果页面较大,则这一部分的损失仍然较大。
7.4 TLB与四级页表支持下的VA到PA的变换
在Intel Core i7环境下研究VA到PA的地址翻译问题。前提如下:
虚拟地址空间48位,物理地址空间52位,页表大小4KB,4级页表。TLB
4路16组相联。CR3指向第一级页表的起始位置(上下文一部分)。
解析前提条件:由一个页表大小4KB,一个PTE条目8B,共512个条目,使用9位二进制索引,一共4个页表共使用36位二进制索引,所以VPN共36位,因为VA
48位,所以VPO 12位;因为TLB共16组,所以TLBI需4位,因为VPN 36位,所以TLBT 32位。
如图7-5所示
,CPU产生虚拟地址VA,VA传送给MMU,MMU使用前36位VPN作为TLBT(前32位)+TLBI(后4位)向TLB中匹配,如果命中,则得到PPN(40bit)与VPO(12bit)组合成PA(52bit)。
如果TLB中没有命中,MMU向页表中查询,CR3确定第一级页表的起始地址,VPN1(9bit)确定在第一级页表中的偏移量,查询出PTE,如果在物理内存中且权限符合,确定第二级页表的起始地址,以此类推,最终在第四级页表中查询到PPN,与VPO组合成PA,并且向TLB中添加条目。
如果查询PTE的时候发现不在物理内存中,则引发缺页故障。如果发现权限不够,则引发段错误。
图7-4 Core i7四级页表下地址翻译情况
7.5 三级Cache支持下的物理内存访问
CPU发送一条虚拟地址,随后MMU按照上述操作获得了物理地址PA。根据cache大小组数的要求,将PA分为CT(标记位)CS(组号),CO(偏移量)。根据CS寻找到正确的组,比较每一个cacheline是否标记位有效以及CT是否相等。如果命中就直接返回想要的数据,如果不命中,就依次去L2,L3,主存判断是否命中,当命中时,将数据传给CPU同时更新各级cache的cacheline(如果cache已满则要采用换入换出策略)。
7.6 hello进程fork时的内存映射
当fork函数被shell进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID,为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将这两个进程的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任意一个后来进行写操作时,写时复制机制就会创建新的页面,因此也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
Exeve函数在当前进程中加载并运行包含在可执行目标文件a.out中的程序,用a.out程序有效地代替了当前程序。加载并运行a.out需要以下几个步骤。
删除已经存在地用户区域,即删除当前进程虚拟地址地用户部分中的已经存在的区域结构。
映射私有区域,为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些结构都是私有的、写时复制的。代码和数据区域被映射为a.out文件中的.test和.data区。Bss区域是请求二进制零的,映射到匿名文件,其大小包含在a.out中。栈和堆区域也是请求二进制零的,初始长度为零。
映射共享区域,如果a.out程序与共享对象(或目标)链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
设置程序计数器(PC)
execve做的最后一件事情就是设置当前进程上下文的程序计数器,并使之指向代码区域的入口点。
设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
图7-5 加载器映射用户地址空间区域
缺页故障与缺页中断处理
- 缺页故障
缺页故障是一种常见的故障,当指令引用一个虚拟地址,在MMU中查找页表时发现与该地址相对应的物理地址不在内存中,因此必须从磁盘中取出时就会发生故障。故障处理流程如图7-8所示。
图7-6 缺页故障处理流
- 缺页中断处理
缺页处理程序是系统内核中的代码,选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令再次发送VA到MMU,这次MMU就能正常翻译VA了。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器分为两种基本风格:显式分配器、隐式分配器。
-
隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。如图7-9所示。
图7-7 隐式分配链表堆块格式
-
显式分配器:要求应用显式地释放任何已分配的块。如图7-10所示。
图7-8 双向空闲链表堆块格式
- 带边界标签的隐式空闲链表分配器管理
带边界标记的隐式空闲链表的每个块是由一个字的头部、有效载荷、可能的额外填充以及一个字的尾部组成的。
隐式空闲链表:在隐式空闲链表中,因为空闲块是通过头部中的大小字段隐含地连接着的。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。其中,一个设置了已分配的位而大小为零的终止头部将作为特殊标记的结束块。
当一个应用请求一个k字节的块时,分配器搜索空闲链表,查找一个足够大的可以放置所请求块的空闲块。分配器有三种放置策略:首次适配、下一次适配合最佳适配。分配完后可以分割空闲块减少内部碎片。同时分配器在面对释放一个已分配块时,可以合并空闲块,其中便利用隐式空闲链表的边界标记来进行合并。
- 显示空间链表管理
显式空闲链表是将空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。如,堆可以组织成一个双向链表,在每个空闲块中,都包含一个前驱与一个后继指针。
显式空闲链表:在显式空闲链表中。可以采用后进先出的顺序维护链表,将最新释放的块放置在链表的开始处,也可以采用按照地址顺序来维护链表,其中链表中每个块的地址都小于它的后继地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。
7.10本章小结
本章介绍了系统的存储器地址空间的概念讲述了虚拟地址、物理地址、线性地址以及逻辑地址的概念,还阐述了逻辑地址到线性地址、线性地址到物理地址的翻译过程,讲述了intel的管理方式。同时结合TLB与多级页表详细阐述了如何优化页地址的翻译过程讲述了系统运行时的存储方式。本章还讲述了进程运行时使用fork函数创建新的进程,以及execve函数加载新进程时系统对内存空间做了哪些事,其中比较有趣的是私有的写时复制,大大节省了内存空间的占用。本章还描述了系统是如何应对缺页故障现象的,讲述了动态存储分配的多种管理方式。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
每个Linux文件都有一个类型来表明它在系统中的角色:
普通文件:包含人一数据。应用程序常常需要区分文本文件和二进制文件,文本文件是只含有ASCLL或Unicode字符的普通文件;二进制文件是所有其他文件,对于内核来说这二者没有却别。
目录:是包含一组链接的文件,其中每一个链接都将一个文件名映射到另一个文件。
套接字:是用来与另一个进程进行跨网络通信的文件。
其他文件类型包括命名通道、符号链接以及字符和块设备。
unix io接口包括打开和关闭文件、读和写文件以及改变当前文件的位置。
8.2 简述Unix IO接口及其函数
1. Unix IO接口:
打开文件:内核返回一个非负整数的文件描述符,通过对此文件描述符对文件进行所有操作。
Linux
shell创建的每个进程开始时都有三个打开的文件:标准输入(文件描述符0)、标准输出(描述符为1),标准出错(描述符为2)。头文件<unistd.h>定义了常量STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO,他们可用来代替显式的描述符值。
改变当前的文件位置,文件开始位置为文件偏移量,应用程序通过seek操作,可设置文件的当前位置为k。
读写文件,读操作:从文件复制n个字节到内存,从当前文件位置k开始,然后将k增加到k+n;写操作:从内存复制n个字节到文件,当前文件位置为k,然后更新k
关闭文件:当应用完成对文件的访问后,通知内核关闭这个文件。内核会释放文件打开时创建的数据结构,将描述符恢复到描述符池中
2.Unix IO函数
2.1 int open(char* filename,int flags,mode_t mode)
进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
2.2 int close(fd)
fd是需要关闭的文件的描述符,close返回操作结果。
- ssize_t read(int fd,void *buf,size_t n)
read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
- ssize_t wirte(int fd,const void *buf,size_t n)
write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。
8.3 printf的实现分析
printf函数:
int printf(const char *fmt, …)
{
int i;
va_list arg = (va_list)((char *)(&fmt) + 4);
i = vsprintf(buf, fmt, arg);
write(buf, i);
return i;
}
vsprintf函数:
int vsprintf(char *buf, const char *fmt, va_list args)
{
char *p;
chartmp[256];
va_listp_next_arg = args;
for (p = buf; *fmt; fmt++)
{
if (*fmt != ‘%’)
{
*p++ = *fmt;
continue;
}
fmt++;
switch (*fmt)
{
case ‘x’:
itoa(tmp, *((int *)p_next_arg));
strcpy(p, tmp);
p_next_arg += 4;
p += strlen(tmp);
break;
case ‘s’:
break;
default:
break;
}
return (p - buf);
}
}
vsprintf函数将所有的参数内容根据格式转换字符串格式化之后缓存入buf,然后返回格式化数组的再buf中的长度。write函数将buf中的i个元素写到终端。从vsprintf生成显示信息,到write系统函数,到使用陷阱的系统调用
int
0x80或syscall.字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
查看getchar函数代码如下所示:
int getchar(void)
{
static char buf[BUFSIZ];
static char *bb = buf;
static int n = 0;
if(n == 0)
{
n = read(0, buf, BUFSIZ);
bb = buf;
}
return(–n >= 0)?(unsigned char) *bb++ : EOF;
}
getchar函数调用read函数,将整个缓冲区都读到buf里,并将缓冲区的长度赋值给n。返回时返回buf的第一个元素,除非n<0。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章简单讲述了Linux的IO设备管理方式讲述了文件和和unix io的接口介绍了几个unix
io接口的函数并结合hello讲述了printf函数以及getchar函数的运行机理。
(第8章1分)
结论
-
简单回顾hello的一生,它在出生时便知道自己的命运——被编译器编译然后运行完成自己的使命,它大致需要经过预处理、编译、汇编和链接四个操作。
-
预处理器将hello.c预处理称为hello.i,
获得了更多新的知识buff(如hello加载的库文件) -
然后编译器将hello.i翻译成了汇编语言hello.s,经过一些步骤翻译成了接近于机器指令的汇编代码。
-
之后汇编器又将hello会变成可重定位二进制文件hello.o就这样hello被改成了机器代码,但是它还有很多要做的事情(外部文件尚未被链接,多种信息需要重定位)
-
GCC中的链接器将可重定位目标文件和其它必要的可重定位目标文件一切链接,生成可执行目标文件。
-
在shell中运行hello,shell为hello创建一个子进程,并调用execve执行hello。
内存管理。在shell中运行hello的同时,为hello分配了虚拟地址空间。
-
在hello的运行过程中,任何指令语句的执行,都调动着系统和硬件的配合,将虚拟地址翻译成物理地址,并在主存中取址。是CPU和主存之间的交互。
-
信号与异常是hello运行过程中的协奏曲,很多信号与异常是必要的。但如果有不必要的信号与异常发生,会对hello造成一些影响。
-
hello由于某种原因(正常或非正常)而终止成为僵死进程,shell回收hello,同时内核删除hello的数据相关,为hello善后。仿佛他从未存在过
在一个学期里学完计算机系统大量的知识和技术,学期结束后,感觉自己仍有很多学习上的不足。简单的一个程序的诞生到终止就包含了如此之多的过程,在对计算机学习上仍需更多的努力。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
列出所有的中间产物的文件名,并予以说明起作用。
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[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分)
命,它大致需要经过预处理、编译、汇编和链接四个操作。
-
预处理器将hello.c预处理称为hello.i,
获得了更多新的知识buff(如hello加载的库文件) -
然后编译器将hello.i翻译成了汇编语言hello.s,经过一些步骤翻译成了接近于机器指令的汇编代码。
-
之后汇编器又将hello会变成可重定位二进制文件hello.o就这样hello被改成了机器代码,但是它还有很多要做的事情(外部文件尚未被链接,多种信息需要重定位)
-
GCC中的链接器将可重定位目标文件和其它必要的可重定位目标文件一切链接,生成可执行目标文件。
-
在shell中运行hello,shell为hello创建一个子进程,并调用execve执行hello。
内存管理。在shell中运行hello的同时,为hello分配了虚拟地址空间。
-
在hello的运行过程中,任何指令语句的执行,都调动着系统和硬件的配合,将虚拟地址翻译成物理地址,并在主存中取址。是CPU和主存之间的交互。
-
信号与异常是hello运行过程中的协奏曲,很多信号与异常是必要的。但如果有不必要的信号与异常发生,会对hello造成一些影响。
-
hello由于某种原因(正常或非正常)而终止成为僵死进程,shell回收hello,同时内核删除hello的数据相关,为hello善后。仿佛他从未存在过
在一个学期里学完计算机系统大量的知识和技术,学期结束后,感觉自己仍有很多学习上的不足。简单的一个程序的诞生到终止就包含了如此之多的过程,在对计算机学习上仍需更多的努力。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
列出所有的中间产物的文件名,并予以说明起作用。
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[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分)