哈工大计算机系统大作业 程序人生-Hello’s P2P

摘  要

对源文件hello.c文件变为hello可执行文件的中的过程,以及产生的中间文件,来描述关于预处理,编译,汇编和链接的内容。再利用hello可执行文件执行过程中进程切换,内存分配情况和输入输出情况对有关于进程管理,存储时地址的管理,地址的映射和转换,Linux下的io管理方法接口等进行描述。

关键词:计算机系统、编译,汇编,链接,进程,内存,shell,信号,cache

   目  录

第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 -

第1章 概述

1.1 Hello简介

P2P(From Program to Process):

一、编辑hello.c文本

二、预处理:处理”#’,将#define定义的宏作字符替换;处理条件编译指令;处理#include,将#include指向的文件插入;删除注释;添加行号和文件标示;保留#program编译器指令。

三、编译:语法分析;语义分析;优化后生成相应的汇编代码;从高级语言到汇编语言再到二进制的机器语言。

四、链接:将翻译生成的二进制文件绑定在一起。

五、在shell中启动,fork产生子进程,成为process。

020(From Zero-0 to Zero-0):

一、shell为其execve,映射虚拟内存。

二、载入物理内存。

三、进入 main函数执行目标代码。

四、CPU分配时间片执行逻辑控制流。

五、程序运行结束后,shell父进程回收hello进程。

六、内核删除相关数据结构,结束。

1.2 环境与工具

硬件环境:ntel(R) Core(TM) i7-8550U CPU @ 1.80GHz   2.00 GHz;8.00 GB RAM;1THD Disk 

软件环境:indows10 64 位;Vmware 15.5.0;Ubuntu 20.04 LTS

开发工具:Gdb/objdump; edb;code blocks

1.3 中间结果

hello.i:hello.c经预处理器生成的文件。

hello.s:hello.i经过编译器翻译成的文本文件hello.s,含汇编语言程序。

hello.o:hello.s经汇编器翻译成机器语言指令打包成的可重定位目标文件

hello.elf:hello.o的ELF格式。

hello_o_asm.txt:hello.o反汇编生成的代码。

hello:经过hello.o链接生成的可执行目标文件。

hello_out.elf:hello的ELF格式。

hello_out_asm.txt:hello反汇编生成的代码。

1.4 本章小结

本小节对P2P020的概念进行了简单的解释,列明了本次大作业的环境和工具,列出了中途生成的所有的辅助文件并解释其作用。

第2章 预处理

2.1 预处理的概念与作用

概念:预处理器cpp根据以字符#开头的命令(宏定义、条件编译),修改原始的C程序,将引用的所有库展开合并成为一个完整的文本文件。

主要功能如下:

1、将源文件中用#include形式声明的文件复制到新的程序中。比如hello.c第6-8行中的#include 等命令告诉预处理器读取系统头文件stdio.h unistd.h stdlib.h 的内容,并把它直接插入到程序文本中。

2、用实际值替换用#define定义的字符串。

3、根据#if后面的条件决定需要编译的代码。

2.2在Ubuntu下预处理的命令

在对应目录的终端下输入gcc -E hello.c -o hello.i

图2.1 生成hello.i

2.3 Hello的预处理结果解析

预处理结束得到hello.i文件。Hello.c得到了扩展,在hello.i中到了3060行。原文件中的宏进行了宏展开,增加的代码是三个头文件的源码。

图2.1 hello.i

2.4 本章小结

概括了预处理的概念和功能,结合预处理之后的hello.i程序对预处理的结果进行了简单分析

第3章 编译

3.1 编译的概念与作用

概念:
广义的编译是说将某一种程序设计语言写的程序翻译成等价的另一种语言。此处是指利用编译程序从预处理文本文件(.i)产生汇编程序(.s)的过程。
作用:
将输入的高级程序设计语言源程序翻译成以汇编语言或机器语言表示的目标程序作为输出。

3.2 在Ubuntu下编译的命令

在终端输入命令gcc -S hello.i -o hello.s

图3.1 生成hello.s

生成的hello.s的部分汇编代码如下:

图3.2 hello.s的部分代码

3.3 Hello的编译结果解析

       3.3.1 数据类型

       (1)局部变量int

图3.3 i对应代码

局部数据空间的每个变量被认为储存在一个栈的寄存器或者栈中,在一个hello.s.的范例里面,编译器将i变量存储在栈上的局部空间-4(%rbp),i可以作为一个intt的类型大约占4个单字节。

(2)局部变量 argc
作为第一个参数存在存在寄存器edi

图3.4 argc对应代码

(3)数组argv[]

main类型函数程序执行时需要输入的是命令行,char类型单个字符串元素的命令行大小为8为,这个数组文件中存放的指针是输入字符串的指针,程序中的字符串argv[1]和程序中的argv[2]的字符串地址分别被输入的rax分两次进行读取,argv[3]的秒数作为字符串的秒数被第三次读取。

(4)字符串

字符以UTF-8编码的格式存在只读的数据段(rodata)。

       3.3.2 处理条件控制语句

       原程序出现以下条件控制语句:

图3.5 条件控制对应代码

对应汇编代码如下:

图3.6 条件控制对应汇编代码

argc存放在%rbp-0x20里,将这个数与4进行比较时,如果两数相等je条件成立,跳转.L2也就是后面的循环否则跳过je继续向下执行。可以看到关系操作符与控制语句是借助jx指令实现的,对与其它的关系操作符有:

图3.7 jx指令

      处理循环时也是同样道理,如果小于8时,借助跳转进行循环。

图3.8循环对应代码

      3.3.3 处理四则运算

       加: addq y,x

减: subq y,x
乘: imulq y,x

leaq B(x,y,A) z:z=x+Ay+B(A,B都是立即数)

本实验使用了add和leaq

图3.9 add所在代码

3.3.4 处理函数

使用call进行函数跳转,先设定寄存器,用来做函数传参。函数返回值放在%eax

图3.10 调用puts和exit代码

图3.11 调用函数代码

第一个printf转换成了puts,把.L0段的立即值传入%rdi,然后call跳转到puts。

exit是把立即数1传入到%edi中,然后call跳转到exit。

第二个printf有三个参数,第一个是.LC1中的格式化字符串%eax中,后面的两个依次是%rdi,%rsi,然后跳转到printf。

sleep有一个参数传到%edi中,之后call跳转到 sleep中。

getchar不需要参数,直接call跳转即可。

3.4 本章小结

概括了编译的概念和作用,根据当前程序,分析了c程序的数据与操作翻译成汇编语言时的表示和处理方法。

第4章 汇编

4.1 汇编的概念与作用

概念:
汇编是指从 .s .o 即编译后的文件到生成机器语言二进制程序的过程,将.s汇编程序翻译车工机器语言并将这些指令打包成可重定目标程序的格式存放在.o中。
作用:
将汇编代码转换为机器指令,使其在链接后能被机器识别并执行。

4.2 在Ubuntu下汇编的命令

gcc hello.s -c -o hello.o

在此目录下多了一个hello.o文件。

图4.1 生成hello.o

4.3 可重定位目标elf格式

使用readelf -a hello.o > hello.elf,目录下增加一个.elf文件。

图4.2 生成hello.elf

Elf结构如下:

图4.3 elf结构

.elf文件中的内容:

(1)ELF头:ELF头(ELF header)以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。

图4.4 ELF头

(2)节头:记录各节名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐。

图4.5 节头

(3)重定位节:

.rela.text,保存的是.text节中需要被修正的信息。本程序需要被重定位的是printf、puts、exit、sleepsecs、getchar、sleep和.rodata中的.L0和.L1。

.rela.eh_frame节是.eh_frame节重定位信息。

图4.6 重定位节

(4)符号表:.symtab,一个符号表,它存放在程序中定义和引用的函数和全局变量的信息,一些程序员错误地认为必须通过-g选项来编译一个程序,才能得到符号表信息。实际上每个可重定位目标文件在.symtab中都有一张符号表。然而,和编译器中的符号表不同,.symtab符号表不包含局部变量的条目。

图4.7 符号表

4.4 Hello.o的结果解析

使用objdump -d -r hello.o > hello.txt

图4.8 生成hello.txt

       打开后为:

图4.9 hello.txt打开内容

将hello.s与反汇编文件比较:

(1)分支转移:在hello.s中,分支跳转是直接以.L0等助记符表示,但在反汇编代码中,分支转移表示为主函数+段内偏移量。

(2)函数调用:hello.s中函数调用时直接给函数名称,而在反汇编的文件中call之后加main+偏移量(定位到call的下一条指令),即用具体的地址表示。在.rela.text节中为其添加重定位条目等待链接。

(3)访问全局变量:汇编代码中使用.LC0(%rip),反汇编代码中为0x0(%rip),因为访问时需要重定位,所以初始化为0并添加重定位条目。

4.5 本章小结

概括了汇编的概念和作用,分析了ELF文件的内容。比较了重定位前汇编程序和重定位后反汇编的差别,了解从汇编语言翻译成机器语言的转换处理和机器语言和汇编语言的映射关系。

5章 链接

5.1 链接的概念与作用

概念:

链接(linking)是将各种代码和数据片段收集并合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时(compile time),也就是在源代码被翻译成机器代码时;也可以执行于加载时(load time),也就是在程序被加载器(loader)加载到内存并执行时;甚至执行于运行时(run time),也就是在由应用程序来执行。

作用:

链接器在软件开发过程中扮演着一个关键的角色,因为它们使得分离编译(separate compilation)成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其它文件。

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 生成hello

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

命令输入:readelf -a hello > hello1.elf

图5.2 生成hello1.elf

hello1.elf结构如下:

(1)ELF头:上次的节头数量为14个,这次变为27个。

图5.3 ELF头

2)节头:

图5.3 节头

5.4 hello的虚拟地址空间

图5.4 虚拟地址空间

PHDR:保存程序头表

INTERP:动态链接器的路径

LOAD:可加载的程序段

DYNAMIN:保存了由动态链接器使用的信息

NOTE保存辅助信息

GNU_STACK:标志栈是否可执行

GNU_RELRO:指定重定位后需被设置成只读的内存区域

使用edb打开hello从Data Dump窗口观察hello加载到虚拟地址的状况,并查看各段信息。

图5.5 edb查看各段信息

在0x400000~0x401000段中,程序被载入,自虚拟地址0x400000开始,到0x400fff结束。

5.5 链接的重定位过程分析

命令输入: objdump -d -r hello > hello1.txt

图5.6 生成hello1.txt

与hello.o生成的反汇编文件对比发现,hello1.txt中多了许多节。

hello0.txt中只有一个.text节,而且只有一个main函数,函数地址也是默认的0x000000.hello1.txt中有.init,.plt,.text三个节,每个节中有许多的函数。库函数的代码都已经链接到了程序中,程序各个节变的更加完整,跳转的地址也具有参考性。

hello比hello.o多出的节头表。

.interp:保存ld.so的路径

.note.ABI-tag

.note.gnu.build-i:编译信息表

.gnu.hash:gnu的扩展符号hash表

.dynsym:动态符号表

.dynstr:动态符号表中的符号名称

.gnu.version:符号版本

.gnu.version_r:符号引用版本

.rela.dyn:动态重定位表

.rela.plt:.plt节的重定位条目

.init:程序初始化

.plt:动态链接表

.fini:程序终止时需要的执行的指令

.eh_frame:程序执行错误时的指令

.dynamic:存放被ld.so使用的动态链接信息

.got:存放程序中变量全局偏移量

.got.plt:存放程序中函数的全局偏移量

.data:初始化过的全局变量或者声明过的函数

hello1.txt部分截图如下:

图5.7部分代码截图

5.6 hello的执行流程

(1) 载入:_dl_start、_dl_init

(2)开始执行:_start、_libc_start_main

(3)执行main:_main、_printf、_exit、_sleep、

_getchar、_dl_runtime_resolve_xsave、_dl_fixup、_dl_lookup_symbol_x

(4)退出:exit

程序名称

地址

ld-2.27.so!_dl_start

0x7fb85a93aea0

ld-2.27.so!_dl_init

0x7f9612138630

hello!_start

0x400582

lib-2.27.so!__libc_start_main

0x7f9611d58ab0

hello!puts@plt

0x4004f0

hello!exit@plt

0x400530

5.7 Hello的动态链接分析

首先进行静态链接,生成部分链接的可执行目标文件hello。此时共享库中的代码和数据没有被合并到hello中。加载hello时,加载共享库,生成完全链接的可执行目标文件。

动态链接采用了延迟加载的策略,即在调用函数时才进行符号的映射。使用偏移量表GOT+过程链接表PLT实现函数的动态链接。GOT中存放函数目标地址,为每个全局函数创建一个副本函数,并将对函数的调用转换成对副本函数调用。

调用init之前的.got.plt

图5.8 调用init之前的.got.plt

调用init之后的.got.plt

图5.9 调用init之后的.got.plt

从图中可以看到.got.plt的条目发生变化。

5.8 本章小结

概括了链接的概念和作用,重点分析了hello程序的虚拟地址空间、重定位和执行过程。简述了动态链接的原理

6章 hello进程管理

6.1 进程的概念与作用

概念:

进程是一个执行中的程序的实例,每一个进程都有它自己的地址空间,一般情况下,包括文本区域、数据区域、和堆栈。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储活动过程调用的指令和本地变量。

作用:进程为用户提供了以下假象:

(1) 我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存。

(2) 处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。

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

作用:是一种交互型的应用级程序,时Linux的外壳,提供了一个界面,用户可以通过这界面访问操作系统内核。

处理流程:

(1)终端进程读取用户由键盘输入的命令行。

(2)分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量

(3)检查第一个(首个、第0个)命令行参数是否是一个内置的shell命令

(4)如果不是内部命令,调用fork( )创建新进程/子进程

(5)在子进程中,用步骤2获取的参数,调用execve( )执行指定程序。

(6)如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid(或wait…等待作业终止后返回。

(7)如果用户要求后台运行(如果命令末尾有&号),则shell返回;

6.3 Hello的fork进程创建过程

在终端中输入命令行./hello 1183710129 邓昆昆 1后,shell会处理该命令,如果判断出不是内置命令,则会调用fork函数创建一个新的子进程,子进程几乎但不完全与父进程相同。通过fork函数,子进程得到与父进程用户级虚拟地址空间相同的但是独立的一份副本,拥有不同的PID。

6.4 Hello的execve过程

execve的功能是在当前进程的上下文中加载并运行一个新程序。在执行fork得到子进程后随即使用解析后的命令行参数调用execve,execve调用启动加载器来执行hello程序。加载器执行的操作是,加删除子进程现有的虚拟内存段,并创建新的代码、数据、堆和栈段。代码和数据段被初始化为hello的代码和数据。堆和栈被置空。然后加载器将PC指向hello程序的起始位置,即从下条指令开始执行hello程序。

6.5 Hello的进程执行

       逻辑控制流:如果想用调试器单步执行程序,我们会看到一系列的程序计数器(PC)的值,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或是包含在运行时动态链接到程序的共享对象中的指令。这个PC值的序列叫做逻辑控制流,或者简称为逻辑流。

上下文切换:如果系统调用因为等待某个事件发生而阻塞,那么内核可以让当前进程休眠,切换到另一个进程,上下文就是内核重新启动一个被抢占的进程所需要的状态,是一种比较高层次的异常控制流。。

时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。

用户模式和内核模式:shell使得用户可以有机会修改内核,所以需要设置一些防护措施来保护内核,如限制指令的类型和可以作用的范围。

开始Hello运行在用户模式,收到信号后进入内核模式,运行信号处理程序,之后再返回用户模式。运行过程中,cpu不断切换上下文,使运行过程被切分成时间片,与其他进程交替占用cpu,实现进程的调度。

6.6 hello的异常与信号处理

hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。

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

执行过程可能出现的异常一共有四种:中断、陷阱、故障、终止。

中断:来自I/O设备的信号,异步发生,总是返回到下一条指令。

陷阱:有意的异常,同步发生,总是返回到下一条指令。

故障:潜在可恢复的错误,同步发生,可能返回到当前指令或终止。

终止:不可恢复的错误,同步发生,不会返回。

(1)正常运行

图6.1 正常运行

(2)ctrl+c终止

图6.2 ctrl+c终止

(3)ctrl+z暂停,输入ps可以发现hello并未关闭

图6.3 ctrl+z暂停,输入ps

(4)运行过程中乱按,无关输入被缓存到stdin,并随着printf被输出到结果。

图6.4 乱按

6.7本章小结

阐述进程的定义与作用,同时介绍了 Shell 的一般处理流程和作用,并且着重分析了调用 fork 创建新进程,调用 execve函数 执行 hello,hello的进程执行,以及hello 的异常与信号处理。

7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:程序经过编译后出现在汇编代码中的地址。逻辑地址用来指定一个操作数或者是一条指令的地址。是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 [段标识符:段内偏移量]。

线性地址:逻辑地址向物理地址转化过程中的一步,逻辑地址经过段机制后转化为线性地址。

虚拟地址:保护模式下程序访问存储器所用的逻辑地址。

物理地址:加载到内存地址寄存器中的地址,内存单元的真正地址。CPU通过地址总线的寻址,找到真实的物理内存对应地址。在前端总线上传输的内存地址都是物理内存地址。

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

一个逻辑地址由两部份组成,段标识符: 段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节,表示具体的是代码段寄存器还是栈段寄存器抑或是数据段寄存器。

索引号就是“段描述符(segment descriptor)”的索引,段描述符具体地址描述了一个段。很多个段描述符,就组了一个数组,叫“段描述符表”,这样,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符。

Base字段,表示的是包含段的首字节的线性地址,也就是一个段的开始位置的线性地址。一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。

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

系统将虚拟页作为进行数据传输的单元。Linux下每个虚拟页大小为4KB。物理内存也被分割为物理页, MMU(内存管理单元)负责地址翻译,MMU使用页表将虚拟页到物理页的映射,即虚拟地址到物理地址的映射。

图7.1 页式管理

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

Core i7采用四级页表的层次结构。CPU产生VA,VA传送给MMU,MMU使用VPN高位作为TLBT和TLBI向TLB中寻找匹配。如果命中,则得到PA。如果TLB中没有命中,MMU查询页表,CR3确定第一级页表的起始地址,VPN1确定在第一级页表中的偏移量,查询出PTE,以此类推,最终在第四级页表中找到PPN,与VPO组合成PA,添加到PLT。

图7.2 VA到PA的转换

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

使用7.4环境中获得的PA,首先取组索引对应位,向L1cache中寻找对应组。如果存在,则比较标志位,并检查对应行的有效位是否为1。如果上述条件均满足则命中。否则按顺序对L2cache、L3cache、内存进行相同操作,直到出现命中。然后向上级cache返回直到L1cache。如果有空闲块则将目标块放置到空闲块中,否则将缓存中的某个块驱逐,将目标块放到被驱逐块的原位置。

7.6 hello进程fork时的内存映射

当 fork 函数被 shell 进程调用时,内核为新进程创建各种数据结构,并分配给 它一个唯一的 PID,为了给这个新进程创建虚拟内存,它创建了当前进程的 mm_struct、区域结构和页表的原样副本。它将这两个进程的每个页面都标记为只 读,并将两个进程中的每个区域结构都标记为私有的写时复制。

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.3 内存映射

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

如果程序执行过程中遇到了缺页故障,则内核调用缺页处理程序。处理程序会进行如下步骤:检查虚拟地址是否合法,如果不合法则触发一个段错误,程序终止。然后检查进程是否有读、写或执行该区域页面的权限,如果不具有则触发保护异常,程序终止。在两步检查都无误后,内核选择一个牺牲页面,如果该页面被修改过则将其交换出去,换入新的页面并更新页表。然后将控制转移给hello进程,再次执行触发缺页故障的指令。

图7.4 缺页中断处理

7.9动态存储分配管理

动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。

分配器分为两种基本风格:显式分配器、隐式分配器。

显式分配器:要求应用显式地释放任何已分配的块。

隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。

堆中的块主要组织为两种形式:

1.隐式空闲链表(带边界标记)

在块的首尾的四个字节分别添加header和footer,负责维护当前块的信息(大小和是否分配)。由于每个块是对齐的,所以每个块的地址低位总是0,可以用该位标注当前块是否已经分配。可以利用header和footer中存放的块大小寻找当前块两侧的邻接块,方便进行空闲块的合并操作。

2.显式空闲链表

在未分配的块中添加两个指针,分别指向前一个空闲块和后一个空闲块。采用该策略,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。

7.9.1带边界标记的隐式空闲链表

(1)堆及堆中内存块的组织结构:

图7.5 堆及堆中内存块的组织结构

在内存块中增加4B的Header和4B的Footer,其中Header用于寻找下一个blcok,Footer用于寻找上一个block。Footer的设计是专门为了合并空闲块方便的。因为Header和Footer大小已知,所以我们利用Header和Footer中存放的块大小就可以寻找上下block。

(2)隐式链表

所谓隐式空闲链表,对比于显式空闲链表,代表并不直接对空闲块进行链接,而是将对内存空间中的所有块组织成一个大链表,其中Header和Footer中的block大小间接起到了前驱、后继指针的作用。

(3)空闲块合并

因为有了Footer,所以我们可以方便的对前面的空闲块进行合并。合并的情况一共分为四种:前空后不空,前不空后空,前后都空,前后都不空。对于四种情况分别进行空闲块合并,我们只需要通过改变Header和Footer中的值就可以完成这一操作。

7.9.2显示空间链表

将空闲块组织成链表形式的数据结构。堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个pred(前驱)和succ(后继)指针,如下图:

图7.6 pred(前驱)和succ(后继)指针

使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。维护链表的顺序有:后进先出(LIFO),将新释放的块放置在链表的开始处,使用LIFO的顺序和首次适配的放置策略,分配器会最先检查最近使用过的块,在这种情况下,释放一个块可以在线性的时间内完成,如果使用了边界标记,那么合并也可以在常数时间内完成。按照地址顺序来维护链表,其中链表中的每个块的地址都小于它的后继的地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。平衡点在于,按照地址排序首次适配比LIFO排序的首次适配有着更高的内存利用率,接近最佳适配的利用率。

7.10本章小结

主要介绍了hello的存储器的地址空间,介绍了四种地址空间的差别和地址的相互转换。同时介绍了hello的四级页表的虚拟地址空间到物理地址的转换。阐述了三级cashe的物理内存访问、进程 fork 时的内存映射、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。

8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:所有IO设备都被模型化为文件,所有的输入和输出都能被当做相应文件的读和写来执行。
设备管理:Linux内核有一个简单、低级的接口,成为Unix I/O,是的所有的输入和输出都能以一种统一且一致的方式来执行。

8.2 简述Unix IO接口及其函数

Unix I/O 接口:

(1)打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想 要访问一个 I/O 设备,内核返回一个小的非负整数,叫做描述符,它在 后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文 件的所有信息。

(2)Shell 创建的每个进程都有三个打开的文件:标准输入,标准输出,标 准错误。

(3)改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位 置 k,初始为 0,这个文件位置是从文件开头起始的字节偏移量,应用 程序能够通过执行 seek,显式地将改变当前文件位置 k。

(4)读写文件:一个读操作就是从文件复制 n>0 个字节到内存,从当前文 件位置 k 开始,然后将 k 增加到 k+n,给定一个大小为 m 字节的而文 件,当 k>=m 时,触发 EOF。类似一个写操作就是从内存中复制 n>0 个字节到一个文件,从当前文件位置 k 开始,然后更新 k。

(5)关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢 复到可用的描述符池中去。

Unix I/O 函数:

(1)int open(char* filename,int flags,mode_t mode) ,进程通过调用 open 函 数来打开一个存在的文件或是创建一个新文件的。 open函数将filename 转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在 进程中当前没有打开的最小描述符,flags 参数指明了进程打算如何访 问这个文件,mode 参数指定了新文件的访问权限位。

(2)int close(fd),fd 是需要关闭的文件的描述符,close 返回操作结果。

(3) ssize_t read(int fd,void *buf,size_t n),read 函数从描述符为 fd 的当前文 件位置赋值最多 n 个字节到内存位置 buf。返回值-1 表示一个错误,0 表示 EOF,否则返回值表示的是实际传送的字节数量。

(4) ssize_t wirte(int fd,const void *buf,size_t n),write 函数从内存位置 buf 复制至多 n 个字节到描述符为 fd 的当前文件位置。

8.3 printf的实现分析

以下格式自行编排,编辑时删除

首先查看printf函数的函数体:

static int printf(const char *fmt, ...)

          va_list args; 

          int i; 

          va_start(args, fmt);  

          write(1,printbuf,i=vsprintf(printbuf, fmt, args)); 

          va_end(args); 

          return i; 

}

printf程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。

接下来是write函数:

write:

 mov eax, _NR_write

 mov ebx, [esp + 4]

 mov ecx, [esp + 8]

 int INT_VECTOR_SYS_CALL

在printf中调用系统函数write(buf,i)将长度为i的buf输出,在write函数中,将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址,
int INT_VECTOR_SYS_CALLA代表通过系统调用syscall。

查看syscall函数体:

sys_call: 

call save

          push dword [p_proc_ready] 

          

          sti  

                

          push ecx  

          push ebx 

          call [sys_call_table + eax * 4] 

          add esp, 4 * 3 

       

          mov [esi + EAXREG - P_STACKBASE], eax  

          cli 

          ret

syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。

字符显示驱动子程序将通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中。

显示芯片会按照一定的刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

于是我们的打印字符串就显示在了屏幕上。

从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; 

 }

异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键 的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子 程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码 转换成 ASCII 码,保存到系统的键盘缓冲区之中。

getchar 函数落实到底层调用了系统函数 read,通过系统调用 read 读取存储在 键盘缓冲区中的 ASCII 码直到读到回车符然后返回整个字串,getchar 进行封装, 大体逻辑是读取字符串的第一个字符然后返回。

8.5本章小结

主要介绍了 Linux 的 IO 设备管理方法、Unix IO 接口及其函数,分析了 printf 函数和 getchar 函数的实现。

结论

(一) 编写,将代码键入hello.c

(二) 预处理,将hello.c调用的所有外部的库展开合并到一个hello.i文件中

(三) 编译,将hello.i编译成为汇编文件hello.s

(四) 汇编,将hello.s会变成为可重定位目标文件hello.o

(五) 链接,将hello.o与可重定位目标文件和动态链接库链接成为可执行目标程序hello

(六) 运行:在shell中输入./hello 1183710129 邓昆昆

(七) 创建子进程:shell进程调用fork为其创建子进程

(八) 运行程序:shell调用execve,execve调用启动加载器,加映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入 main函数。

(九) 执行指令:CPU为其分配时间片,在一个时间片中,hello享有CPU资源,顺序执行自己的控制逻辑流

(十) 访问内存:MMU将程序使用的虚拟内存地址通过页表映射成物理地址。

(十一) 动态申请内存:printf调用malloc向动态内存分配器申请堆中的内存。

(十二) 信号:如果运行途中键入ctr-c ctr-z则调用shell的信号处理函数分别停止、挂起。

(十三) 结束:shell父进程回收子进程,内核删除为这个进程创建的所有数据结构。

附件

hello.c:源代码

hello.i:预处理后的文本文件

hello.s:编译之后的汇编文件

hello.o:汇编之后的可重定位目标执行文件

hello:链接之后的可执行文件

hello.elf:hello.o的ELF格式

hello1.elf:hello的ELF格式

hello0.txt:hello.o反汇编代码

hello1.txt: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.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值