目 录
第1章 概述................................................................................................................ - 4 -
1.1 Hello简介......................................................................................................... - 4 -
1.2 环境与工具........................................................................................................ - 4 -
1.3 中间结果............................................................................................................ - 4 -
1.4 本章小结............................................................................................................ - 4 -
第2章 预处理............................................................................................................ - 5 -
2.1 预处理的概念与作用........................................................................................ - 5 -
2.2在Ubuntu下预处理的命令............................................................................. - 5 -
2.3 Hello的预处理结果解析................................................................................. - 5 -
2.4 本章小结............................................................................................................ - 5 -
第3章 编译................................................................................................................ - 6 -
3.1 编译的概念与作用............................................................................................ - 6 -
3.2 在Ubuntu下编译的命令................................................................................ - 6 -
3.3 Hello的编译结果解析..................................................................................... - 6 -
3.4 本章小结............................................................................................................ - 6 -
第4章 汇编................................................................................................................ - 7 -
4.1 汇编的概念与作用............................................................................................ - 7 -
4.2 在Ubuntu下汇编的命令................................................................................ - 7 -
4.3 可重定位目标elf格式.................................................................................... - 7 -
4.4 Hello.o的结果解析......................................................................................... - 7 -
4.5 本章小结............................................................................................................ - 7 -
第5章 链接................................................................................................................ - 8 -
5.1 链接的概念与作用............................................................................................ - 8 -
5.2 在Ubuntu下链接的命令................................................................................ - 8 -
5.3 可执行目标文件hello的格式....................................................................... - 8 -
5.4 hello的虚拟地址空间..................................................................................... - 8 -
5.5 链接的重定位过程分析.................................................................................... - 8 -
5.6 hello的执行流程............................................................................................. - 8 -
5.7 Hello的动态链接分析..................................................................................... - 8 -
5.8 本章小结............................................................................................................ - 9 -
第6章 hello进程管理...................................................................................... - 10 -
6.1 进程的概念与作用.......................................................................................... - 10 -
6.2 简述壳Shell-bash的作用与处理流程........................................................ - 10 -
6.3 Hello的fork进程创建过程........................................................................ - 10 -
6.4 Hello的execve过程.................................................................................... - 10 -
6.5 Hello的进程执行........................................................................................... - 10 -
6.6 hello的异常与信号处理............................................................................... - 10 -
6.7本章小结.......................................................................................................... - 10 -
第7章 hello的存储管理................................................................................... - 11 -
7.1 hello的存储器地址空间............................................................................... - 11 -
7.2 Intel逻辑地址到线性地址的变换-段式管理............................................... - 11 -
7.3 Hello的线性地址到物理地址的变换-页式管理.......................................... - 11 -
7.4 TLB与四级页表支持下的VA到PA的变换................................................ - 11 -
7.5 三级Cache支持下的物理内存访问............................................................. - 11 -
7.6 hello进程fork时的内存映射..................................................................... - 11 -
7.7 hello进程execve时的内存映射................................................................. - 11 -
7.8 缺页故障与缺页中断处理.............................................................................. - 11 -
7.9动态存储分配管理.......................................................................................... - 11 -
7.10本章小结........................................................................................................ - 12 -
第8章 hello的IO管理.................................................................................... - 13 -
8.1 Linux的IO设备管理方法............................................................................. - 13 -
8.2 简述Unix IO接口及其函数.......................................................................... - 13 -
8.3 printf的实现分析........................................................................................... - 13 -
8.4 getchar的实现分析....................................................................................... - 13 -
8.5本章小结.......................................................................................................... - 13 -
结论............................................................................................................................ - 14 -
附件............................................................................................................................ - 15 -
参考文献.................................................................................................................... - 16 -
Hello 的 P2P,即 From Program to Process 的过程为:程序员用键盘输入 hello.c文件,,hello.c 经过 cpp 的预处理变成hello.i、ccl 的编译生成hello.s、经过as的汇编生产hello.o、ld 的链接最终成为可执行目标程序 hello。之后,在shell中输入./hello运行hello程序,shell为其fork,产生子进程,于是hello便由Program变为Process.此为P2P过程.
然后, 调用 execve 函数在新的子进程中加载并运行 hello,调用_start函数,之后转移到hello的mian函数,CPU 为运行的 hello 分配时间片执行逻辑 控制流。当程序运行结束后, 终止的hello进程被父进程bash回收。
以上全部即020(From Zero to Zero)的全过程。
硬件环境:Intel Core i5-7300HQ CPU; 2.5GHz;8G RAM;
软件环境:Windows 10 64 位操作系统;VMware 14;Ubuntu 16.04 LTS;
gcc; edb; readelf; objdump; gedit; CodeBlocks 17.12
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
hello.i |
hello预处理之后的文本文件 |
hello.s |
hello编译之后的汇编文件 |
hello.o |
hello汇编之后的可重定位目标文件 |
hello |
hello链接之后的可执行目标文件 |
hello.objdump |
hello的反汇编代码 |
hello.elf |
hello的ELF文件信息 |
helloo.objdmp |
hello.o的反汇编代码 |
helloo.elf |
Hello.o 的 ELF 格式 |
本章简要介绍了hello的P2P,O2O过程,并列出了本次实验的环境和中间结果
(第1章0.5分)
预处理又称预编译,(对于c/c++来说)预处理指的是在程序编译之前,根据以字符#开头的命令(即头文件/define/ifdef之类的),修改原始的c程序,例如大作业所提供的hello.c文件,就有三个头文件:stdio.h,unistd.h,stdlib.h
预处理的作用:
1. 执行源文件包含。#include 指令告诉预处理器(cpp)读取源程序所引用 的系统源文件,并把源文件直接插入程序文本中。
2. 执行宏替换。用实际值替换用#define 定义的字符串
3. 条件编译。根据#if 后面的条件决定需要编译的代码 , #endif 是#if, #ifdef, #ifndef 这些条件命令的结束标志。
此时hello.i有3188行,下图为main函数所在
经过预处理的hello代码被展开,在main之前插入了大量代码。这些都是 stdio.h unistd.h stdlib.h 的依次展开.
原文件中的宏进行了宏展开,头文件中的内容被包含进该文件中。
本阶段完成了对hello.c的预处理工作。介绍了预处理的定义与作用、并结合预处理之后的程序对预处理结果进行了解析。
(第2章0.5分)
编译程序也称为编译器,是指把用高级程序设计语言书写的源程序,翻译成等价的汇编语言格式目标程序的翻译程序。编译器将文本文件 hello.i 翻译成文本文件 hello.s,这个过程称为编译。
命令:gcc -S hello.i -o hello.s
.file 源文件
.data 数据段
.globl 全局标识符
.string 字符串类型
.long 声明long类型
.text 代码段
.section .rodata rodata节
.align 数据或指令存放地址对齐方式
.type 指定函数或对象类型
.size 声明大小
hello.s 中用到的 C 数据类型:整数类型、字符串、数组.
整数类型:
hello.c中的整数类型有全局变量int sleepsecs,main的参数int argc,局部变量int i。
hello.c 在 main 函数之前先定义了一个整型全局变量 sleepsecs,并赋予了初值:
经过编译器处理, hello.s文件先声明了 sleepsecs 这个全局变量,然 后将 sleepsecs 存放在.data 节,可以看到第7行为其分配大小4字节,第9行为其赋初值2。
参数int argc,局部变量i出现在main的栈帧中,它们没有标识符,也不需要被声明,而是直接使用。int i 是循环中用来计数的局部变量,,argc 是从终端输入的参数的个数,也是 main 函数的第一个参数。编译器一般会将局部变量存储在寄存器或者程序栈中,可以看出 hello.s 将 i 存储在-4(%rbp)中,初始值为 0,每次循环加一,退出循环条件是 i 大于 9
argc 作为第一个参数,由寄存器%edi 保存,然后又被存入-20(%rbp)
字符串:
两个printf语句中的格式字符串出现在.rodata段。
第一个字符串.LC0 包含汉字,每 个汉字在 utf-8 编码中被编码为三个字节,第二个字符串的两个%s 为用户 在终端输入的两个参数。
数组:
作为main参数的char *argv[]则出现在栈帧中。
赋值操作
hello.c 的赋值操作有两处,一是将全局变量 sleepsecs 赋值为 2.5,二是在循 环开始时将 i 赋值为 0。源程序对i赋值为零的操作使用mov语句实现的。
比较操作
数组操作
argv[1]:首先从-32(%rbp)读取argv地址存入rax,接下来rax增加8个字节,此时rax中存放的是&(argv[1]),读取此地址指向的argv[1]放入rax,最后存入rsi。
argv[2]:首先从-32(%rbp)读取argv地址存入rax,接下来rax增加16个字节,此时rax中存放的是&(argv[2]),读取此地址指向的argv[2]放入rdx。
控制转移往往与关系操作配合进行,如果满足某个条件,则跳转至某个位置。
对于 hello.c 中的 if(argc!=3),编译器处理为了先判断 argc 和 3 是否相等,若 相等,则跳转至.L2,否则继续执行之后的内容。而对于 for 循环中的 i<10, 编译器则会判断 i 是否小于等于 9,若是,则继续执行循环体内的内容,否则
main 函数
main 函数开始时被存在.text 节,标记类型为函数,程序运行时,将由系统 启动函数调用,因此 main 函数是 hello.c 的起点。main 函数的两个参数分 别为 argc 和 argv[],由命令行输入,存储在%rdi 和%rsi 中。
printf 函数
printf的调用,参数被存放在寄存器传递。以printf(“Hello %s %s\n”,argv[1],argv[2]);为例,格式化字符串被存放在edi传递,argv[1]被放在rsi,argv[2]被放在rdx。使用call来调用printf,而printf的返回值则会被存入eax返回。
exit 函数
exit函数的调用,参数被存放在edi传递,然后使用call调用exit。
sleep 函数
对sleep的调用,参数被存放在edi传递,然后使用call调用sleep。
getchar 函数
对getchar的调用直接使用了call。
main函数的返回值放在eax传递
本阶段完成了对hello.i的编译工作。使用Ubuntu下的编译指令可以将其转换为.s汇编语言文件。此外,本章通过与源文件C程序代码进行比较,完成了对汇编代码的解析工作。完成该阶段转换后,可以进行下一阶段的汇编处理。
(第3章2分)
汇编器(as)将.s 汇编程序翻译成机器语言指令,把这些指令打包成可重定位 目标程序的格式,并将结果保存在.o 目标文件中,.o 文件是一个二进制文件,它包含程序的指令编码。这个过程称为汇编,亦即汇编的作用。
。
命令:as hello.s -o hello.o
使用 readelf 获得 hello.o 的 elf 格式:readelf -a hello.o > hello.elf。接下来分析 可重定位文件 hello.o 的 elf 格式:
名称 |
作用 |
ELF头 |
描述了生成该文件的系统的大小和字节顺序以及帮助链接器语法分析和解释目标文件的信息 |
.text |
已编译的程序的机器代码 |
.rodata |
只读数据 |
.data |
已初始化的全局和静态C变量 |
.bss |
未初始化的全局和静态C变量 |
.symtab |
一个符号表,存放在程序中定义和引用的函数和全局变量的信息 |
.rel.text |
.text节的重定位记录表 |
.rel.data |
被模块引用或定义的所有全局变量的重定位信息 |
.debug |
一个调试符号表 |
.line |
原始C源程序的行号和.text节中机器指令之间的映射 |
.strtab |
一个字符串表 |
节头部表 |
每个节的偏移量大小 |
使用readelf –h hello.o查看文件头信息。根据文件头的信息,可以知道该文件是可重定位目标文件,有13个节
使用readelf –S hello.o查看节头表。从而得知各节的大小,以及他们可以进行的操作
使用readelf –s hello.o可以查看符号表的信息
由于是可重定位目标文件,所以每个节都从0开始,用于重定位。在文件头中得到节头表的信息,然后再使用节头表中的字节偏移信息得到各节在文件中的起始位置,以及各节所占空间的大小。同时可以观察到,代码段是可执行的,但是不能写;数据段和只读数据段都不可执行,而且只读数据段也不可写。
在终端输入objdump -d -r hello.o > helloo.objdump 将hello.o的反汇编代码输出到hello.d 中,与 hello.s 进行对照分析:
机器语言是计算机能直接理解的语言,完全由二进制数构成,在这里为了 方便显示成了 16 进制。每两个 16 进制数构成一个字节编码,是机器语言 中能解释一个运算符或操作数的最小单位
首先,把机器语言反汇编后得到的代码与 hello.s 中的汇编代码大致相同, 只有一些地方存在差异:
本阶段完成了对hello.s的汇编工作。使用Ubuntu下的汇编指令可以将其转换为.o可重定位目标文件。此外,本章通过将.o文件反汇编结果与.s汇编程序代码进行比较,了解了二者之间的差别。完成该阶段转换后,可以进行下一阶段的链接工作。
(第4章1分)
链接程序将分别在不同的目标文件中编译或汇编的代码收集到一个可直接执行的文件中。它还连接目标程序和用于标准库函数的代码,以及连接目标程序和由计算机的操作系统提供的资源(例如,存储分配程序及输入与输出设备)。链接工作大致包含两个步骤,一是符号解析,二是重定位。在符号解析步骤中,链接器将每个符号引用与一个确定的符号定义关联起来。将多个单独的代码节和数据节合并为单个节。将符号从它们的在.o文件的相对位置重新定位到可执行文件的最终绝对内存位置。更新所有对这些符号的引用来反映它们的新位置。
命令:
图5.1
图5.2使用 ld 命令链接生成可执行程序 hello
使用 readelf -a hello > hello.elf 命令生成 hello 程序的 ELF 格式文件。
图5.3可执行文件 hello 的 ELF 格式(节头部表)
在 ELF 格式文件中,Section Headers 对 hello 中所有的节信息进行了声明,其 中包括大小 Size 以及在程序中的偏移量 Offset,因此根据 Section Headers 中的信 息我们就可以用 HexEdit 定位各个节所占的区间(起始位置,大小)。其中 Address 是程序被载入到虚拟地址的起始地址。
使用edb加载hello,查看本进程的虚拟地址空间。根据5.3节的信息,可以找到各节的二进制信息。代码段的信息如下所示。代码段开始于0x400550处,大小为0x01f2。
图5.4 使用edb查看.text段
使用 objdump -d -r hello > hello.objdump 获得 hello 的反汇编代码。
图5.5 hello反汇编文件
在反汇编生成的汇编代码方面,hello 和 hello.o 完全相同,hello 与 hello.o 两者 的反汇编文件的唯一不同之处在于:地址由相对偏移变为了可以由 CPU 直接访问 的虚拟地址,链接器把 hello.o 中的偏移量加上程序在虚拟内存中的起始地址 0x0040000 和.text 节的偏移量就得到了 hello1.d 中的一个个地址。函数内的控制转 移即 jmp 指令后的地址由偏移量变为了偏移量+函数的起始地址(虚拟地址);call 后 的地址由链接器执行重定位后计算出实际地址。
· hello.o的objdump没有_init函数、_start函数、plt表等。
· hello.o的objdump中对全局变量的引用地址均为0,函数调用的地址也只是当前指令的下一条指令的地址
通过使用objdump查看反汇编代码,以及使用gdb单步运行,可以找出.text节中main函数前后执行的函数名称。在main函数之前执行的程序有:_start、__libc_start_main@plt、__libc_csu_init、_init、frame_dummy、register_tm_clones。在main函数之后执行的程序有:exit、cxa_thread_atexit_impl、fini。
无论在内存中的何处加载一个目标模块(包括共享目标模块),数据段与代码段的距离总是保持不变。因此,代码段中任何指令和数据段中任何变量之间的距离都是一个运行时常量,与代码段和数据段的绝对内存位置是无关的。
而要生成对全局变量PIC引用的编译器利用了这个事实,它在数据段开始的地方创建了一个表,叫做全局偏移量表(GOT)。在GOT中,每个被这个目标模块引用的全局数据目标(过程或全局变量)都有一个8字节条目。编译器还为GOT中每个条目生成 一个重定位记录。在加载时,动态链接器会重定位GOT中的每个条目,使得它包含目标的正确的绝对地址。
hello中对.got的初始化是由_dl_start函数执行的。下面的四张图片反应了这一过程:
.got _dl_start执行前
.got.plt _dl_start执行前
.got _dl_start执行后
.got.plt _dl_start执行后
hello要调取由共享库定义的函数puts,printf,而程序调用一个由共享库定义的函数,编译器没有办法预测这个函数的运行地址,因为定义它的共享模块在运行时可以加载到任何位置。为了解决这个问题,GNU编译系统使用了延迟绑定技术:
当hello尝试调用puts时,不直接调用puts,而是调用进入puts对应的PLT条目。这个条目会尝试利用GOT项进行间接跳转。
第一次被调用时,GOT项的值为PLT条目中的下一条指令地址,因而接下来会跳回PLT条目,在把puts的ID 0压入栈后,会转到PLT[0]的位置,PLT[0]通过GOT[1]间接地把动态链接器的一个参数压入栈中,然后通过GOT[2]跳转进动态链接器中。动态链接器使用两个栈条目来确定puts的运行时位置,用这个地址重写puts的GOT项,再把控制传递给puts
在下一次执行到puts对应的PLT条目时,GOT项已经被修改,因此利用GOT项进行的间接跳转会直接跳转到puts函数
(第5章1分)
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。程序是指令、数据及其组织形式的描述,进程是程序的实体。进程是一个具有独立功能的程序关于某个数据集合的一次运行活动。它可以申请和拥有系统资源,是一个动态的概念,是一个活动的实体。它不只是程序的代码,还包括当前的活动,通过程序计数器的值和处理寄存器的内容来表示。
Shell 的作用:Shell 是一个用 C 语言编写的程序,是用户使用 Linux 的桥梁。 Shell 是指一种应用程序,Shell 应用程序提供了一个界面,用户通过这个界面访问操作系统内核的服务。
shell-bash处理流程:
1)从终端读入输入的命令。
2)将输入字符串切分获得所有的参数
3)如果是内置命令则立即执行
4)否则调用相应的程序为其分配子进程并运行
5)shell 应该接受键盘输入信号,并对这些信号进行相应处理
首先,要运行 hello 程序,需要在 shell 输入./hello 1173710213 ylt。
运行的终端程序会 对输入的命令行进行解析,因为 hello不是一个内置的 shell 命令所以解析之后终端 程序判断./hello 的语义为执行当前目录下的可执行目标文件 hello,之后终端程序 首先会调用 fork 函数创建一个新的运行的子进程,新创建的子进程几乎但不完全与父进程相同,子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,这就意味着,当父进程调用 fork 时,子进程可以读写父进程中打开的 任何文件。父进程与子进程之间最大的区别在于它们拥有不同的 PID。
流程图如下:
在新创建的子进程中,execve函数加载并运行hello,且带参数列表argv和环境变量envp。在execve加载了hello之后,它调用_start,_start设置栈,并将控制传递给新程序的主函数。
在输入合适参数执行hello程序之后,hello进程一开始运行在用户模式。内核为hello维持一个上下文,它由一系列的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构(比如页表、进程表、文件表)。
在hello运行时,也有一些其它进程在并行地运行,这些进程的逻辑流的执行时间与hello的逻辑流重叠,称为并发流。而一个进程和其它进程轮流运行的概念叫作多任务,一个进程执行它的控制流的一部分的每一时间段叫做时间片。因此,多任务也叫做时间分片。
不久hello调用printf与sleep,这两个函数引发系统调用,系统调用使得进程从用户模式变为内核模式,处理程序运行在内核模式中,当它返回到应用程序代码时,处理器就把模式从内核模式改回到用户模式,而执行sleep系统调用时,内核可能会执行上下文切换而非将控制返回给hello进程。在切换的第一部分中,内核代表hello在内核模式下执行指令,然后在某一时刻,它开始代表另一个进程在内核模式下执行指令,在切换之后,内核代表那个进程在用户模式下执行指令。
而这个切换过程可以分为三个步骤:
1.保存当前进程的上下文。
2.恢复某个先前被抢 占的进程被保存的上下文。
3.将控制传递给这个新恢复的进程。
这时我们说内核调度了一个新的进程,在内核调度了一个新的进程后,它就抢占了当前进程。
不仅仅是系统调用会导致上下文切换,中断也会。当hello执行了一段时间(通常是1-10ms)后,定时器引发的中断也会导致内核执行上下文切换并调度一个新的进程。
接下来的十秒中,内核继续执行上下文切换,轮流运行hello与其它进程,十次循环结束后,hello返回,程序终止
hello 执行过程中可能出现四类异常:中断、陷阱、故障和终止。
1. 中断是来自 I/O 设备的信号,异步发生,中断处理程序对其进行处理,返 回后继续执行调用前待执行的下一条代码,就像没有发生过中断。
2. 陷阱是有意的异常,是执行一条指令的结果,调用后也会返回到下一条指 令,用来调用内核的服务进行操作。帮助程序从用户模式切换到内核模式。
3. 故障是由错误情况引起的,它可能能够被故障处理程序修正。如果修正成 功,则将控制返回到引起故障的指令,否则将终止程序。
4. 终止是不可恢复的致命错误造成的结果,通常是一些硬件的错误,处理程 序会将控制返回给一个 abort 例程,该例程会终止这个应用程序。
图6.6.1正常运行 hello 程序
当程序执行完成之后,进程被 回收。
图6.6.2运行中途按下 ctrl-c
当按下 ctrl-c 之 后,shell 父进程收到 SIGINT 信号,信号处理函数的逻辑是结束 hello,并回收 hello 进程。
图6.6.3 运行中途按下 ctrl-z
当按下 ctrl-z 之后,shell 父进程收到 SIGSTP 信号,信号处理函数的逻辑是打印屏幕回显、将 hello 进程挂起,通过 ps 命令我们可以看出 hello 进程没有被回收,此时他的后台 job 号是 1,调用 fg 1 将其调到前台,此时 shell 程序首先打印 hello 的命令行命令, hello 继续运行打印剩下的 8 条 info,之后输入字串,程序结束,同时进程被回收。.
图6.6.4运行中途乱按
可以看到在 hello 运行过程中乱按键盘会在屏幕上显示出按的内容,但不会影响 hello 的 输出,在 hello 的循环结束后调用 getchar 函数,读入了一行输入,而之后的输入每一行都会被 shell 当作一个命令,可见在运行 hello 的过程中从键盘的输入被缓存到了输入缓存区。
本阶段通过在hello.out运行过程中执行各种操作,了解了与系统相关的若干概念、函数和功能。分析了在程序运行过程中,计算机硬件、软件和操作系统之间的配合和协作的方式
(第6章1分)
线性地址:地址空间是一个非负整数地址的有序集合,而如果此时地址空间中 的整数是连续的,则我们称这个地址空间为线性地址空间。
物理地址:计算机系统的主存被组织成一个由 M 个连续的字节大小的单元组 成的数组,其每一个字节都被给予一个唯一的地址,这个地址称为物理地址。物理 地址也是计算机的硬件中的电路进行操作的地址。
逻辑地址:由程序产生的与段有关的偏移地址。分为两个部分,一个部分为段 基址,另一个部分为段偏移量。
虚拟地址:CPU 启动保护模式后,程序运行在虚拟地址空间中。与物理地址相 似,虚拟内存被组织为一个存放在磁盘上的 N 个连续的字节大小的单元组成的数 组,其每个字节对应的地址成为虚拟地址。虚拟地址包括 VPO(虚拟页面偏移量)、 VPN(虚拟页号)、TLBI(TLB 索引)、TLBT(TLB 标记)。
hello 反汇编得到的文件中第 145 行:0000000000400606
图7.2
实模式下:逻辑地址CS:EA=物理地址CS*16+EA
保护模式下:以段描述符作为下标,到GDT/LDT表查表获得段地址,
段地址+偏移地址=线性地址。
hello的线性地址到物理地址的变换需要查询页表得出,hello的线性地址被分成两个部分,第一部分虚拟页号VPN用于在页表查询物理页号PPN,而第二部分虚拟页偏移量VPO则与查询到的物理页号PPN一起组成物理地址。
虚拟地址VA被分成VPN和VPO两部分,VPN被分为TLBT和TLBI用于在TLB中查询。根据TLBI确定TLB中的组索引,并根据TLBT判断PPN是否已被缓存到TLB中,如果TLB命中,则直接返回PPN,否则会到页表中查询PPN。在页表中查询PPN时,VPN会被分为四个部分,分别用作一二三四级页表的索引,而前三级页表的查询结果为下一级页表的基地址,第四级页表的查询结果为PPN。将查询到的PPN与VPO组合,得到物理地址PA。
图7.5.1三级Cache支持下的物理内存访问
首先CPU发出一个虚拟地址,在TLB里面寻找。如果命中,那么将PTE发送给L1Cache,否则先在页表中更新PTE。然后再进行L1根据PTE寻找物理地址,检测是否命中的工作。这样就能完成Cache和TLB的配合工作。具体流程如图7.5.1所示。
虚拟内存和内存映射解释了fork函数如何为每个新进程提供私有的虚拟地址空间。Fork函数为新进程创建虚拟内存。创建当前进程的的mm_struct, vm_area_struct和页表的原样副本,两个进程中的每个页面都标记为只读,两个进程中的每个区域结构(vm_area_struct)都标记为私有的写时复制(COW)。在新进程中返回时,新进程拥有与调用fork进程相同的虚拟内存,随后的写操作通过写时复制机制创建新页面。
execve函数在当前进程中加载并运行包含在可执行文件hello中的程序,加载并运行hello时出现的内存映射有:
a.映射私有区域 为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区。Bss区域时请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。
b.映射共享区域 如果hello程序与共享对象(或目标链接),比如C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内
缺页故障:虚拟内存中的字不在物理内存中(DRAM缓存不命中)
如下图,VP3已经被映射到页表中,但却没有被缓存到物理内存中,此时堆VP3的引用会引发缺页故障。.
图7.8.1
缺页会导致页面出错引发一个缺页中断,而缺页异常处理程序会选择一个牺牲页(如下图选择了VP4,将VP4从内存交换到磁盘,并从磁盘读取VP3交换到物理内存)。
图7.8.2
此时令导致缺页的指令重新启动,就可以使得页面命中了。
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长。对于每个进程,内核维护着一个变量brk,它指向堆的顶部。分配器将堆视为一组不同大小的块的集合来维护,每个块就是一个连续的需内存片,要么是已分配的,要么是空闲的。已分配的块显示地保留为供应用程序使用。空闲块可以用来分配。空闲块保持空闲,直到它显示地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显示执行的,要么是内存分配器自身隐式执行的。
两种堆的数据结构组织形式:
图7.9.1带标签的隐式空闲链表
空闲块是通过头部中的大小字段隐含地连接着的。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。
显式空闲链表:
显式空闲链表将链表的指针存放在空闲块的主体里面。堆被组织成一个双向空闲链表,在每个空闲块中,都包含一个pred和succ指针,如下图所示:
图7.9.2显式空闲链表
本章通过hello的内存管理,复习了与内存管理相关的重要的概念和方法。加深了对动态内存分配的认识和了解
(第7章 2分)
Unix I/O 接口的几种操作:
1. 打开文件:程序要求内核打开文件,内核返回一个小的非负整数(描 述符),用于标识这个文件。程序在只要记录这个描述符便能记录打 开文件的所有信息。 2. shell 在进程的开始为其打开三个文件:标准输入、标准输出和标准错 误。
3. 改变当前文件的位置:对于每个打开的文件,内核保存着一个文件位 置 k,初始为 0。这个文件位置是从文件开头起始的字节偏移量。应用 程序能够通过执行 seek 操作显式地设置文件的当前位置为 k。
4. 读写文件:一个读操作就是从文件复制 n>0 个字节到内存,从当前文 件位置 k 开始,然后将 k 增加到 k+n。给定一个大小为 m 字节的文件, 当 k>=m 时执行读操作会出发一个称为 EOF 的条件,应用程序能检测 到这个条件,在文件结尾处并没有明确的 EOF 符号。
5. 关闭文件:内核释放打开文件时创建的数据结构以及占用的内存资源, 并将描述符恢复到可用的描述符池中。无论一个进程因为何种原因终 止时,内核都会关闭所有打开的文件并释放它们的内存资源。
Unix I/O 函数:
1. int open(char *filename, int flags, mode_t mode); open 函数将 filename 转换为一个文件描述符,并且返回描述符数字。 返回的描述符总是在进程中当前没有打开的最小描述符,flags 参数指 明了进程打算如何访问这个文件,mode 参数指定了新文件的访问权限 位。
2. int close(int fd); 关闭一个打开的文件。
3. ssize_t read(int fd, void *buf, size_t n); read 函数从描述符为 fd 的当前文件位置赋值最多 n 个字节到内存位置 buf。返回值-1 表示一个错误,0 表示 EOF,否则返回值表示的是实际 传送的字节数量。
4. ssize_t write(int fd, const void *buf,size_t); write 函数从内存位置 buf 复制至多 n 个字节到描述符 fd 的当前文件位置
(char*)(&fmt) + 4) 表示的是…可变参数中的第一个参数的地址。而vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。接着从vsprintf生成显示信息,到write系统函数,直到陷阱系统调用 int 0x80或syscall。字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
getchar函数的大致实现如下:
getchar函数通过调用read函数来读取字符。read函数由三个参数,第一个参数为文件描述符fd,fd为0表示标准输入;第二个参数为输入内容的指针;第三个参数为读入字符的个数。read函数的返回值是读入字符的个数,若出错则返回-1。
当用户按键时,键盘接口会产生一个键盘扫描码和一个中断请求,中断处理程序会从键盘接口取得按键扫描码并把它转换成ASCII码,保存到系统的键盘缓冲区。
read执行一个系统调用,按照系统调用从键盘缓冲区读取按键ASCII码,直到接受到回车键才返回。
(第8章1分)
hello所经历的过程
1.用户从键盘输入,得到hello.c源文件。
2.编译器和汇编器对hello.c进行预处理,然后对其进行编译和汇编,得到可重定位目标文件hello.o。
3.链接器对hello.o进行链接,并得到可执行目标文件hello,此时hello已经可以被操作系统加载和执行。
4.bash执行hello,首先bash会fork一个进程,然后在这个新的进程中execve hello,execve会清空当前进程的数据并加载hello,然后把rip指向hello的程序入口,把控制权交给hello。
5.hello与许多进程并行执行,执行过程中由于系统调用或者计时器中断,会导致上下文切换,内核会选择另一个进程进行调度,并抢占当前的hello进程。
6.hello执行的过程中可能收到来自键盘或者其它进程的信号,当收到信号时hello会调用信号处理程序来进行处理,可能出现的行为有停止终止忽略等。
7.hello输出信息时需要调用printf和getchar,而printf和getchar的实现需要调用Unix I/O中的write和read函数,而它们的实现需要借助系统调用。
8.hello中的访存操作,需要经历逻辑地址到线性地址最后到物理地址的变换,而访问物理地址的数据可能已被缓存至高速缓冲区,也可能位于主存中,也可能位于磁盘中等待被交换到主存。
9.hello结束进程后,bash作为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分)