哈工大深入理解计算机系统大作业

哈工大计算机系统大作业

题 目 程序人生-Hello’s P2P
专 业 软件工程
学  号
班  级
学 生    
指 导 教 师

计算机科学与技术学院
2019年12月
摘 要
本文主要讲述了hello.c程序在编写完成后运行在linux中的生命历程,记住相关工具分析预处理、编译、汇编、链接等各个过程在linux下实现的原理,分析了这些过程中产生的文件的相应信息和作用。并介绍了shell的内存管理、IO管理、进程管理等相关知识,了解了虚拟内存、异常信号等相关内容。
关键词:预处理;编译;汇编;链接;shell;IO管理;进程管理;虚拟内存;异常信号

目 录

第1章 概述 - 4 -
1.1 HELLO简介 - 4 -
1.2 环境与工具 - 4 -
1.3 中间结果 - 4 -
1.4 本章小结 - 5 -
第2章 预处理 - 6 -
2.1 预处理的概念与作用 - 6 -
2.2在UBUNTU下预处理的命令 - 6 -
2.3 HELLO的预处理结果解析 - 7 -
2.4 本章小结 - 7 -
第3章 编译 - 8 -
3.1 编译的概念与作用 - 8 -
3.2 在UBUNTU下编译的命令 - 8 -
3.3 HELLO的编译结果解析 - 9 -
3.3.1处理变量 - 9 -
3.3.2处理关系操作符与控制语句 - 11 -
3.3.3处理四则运算与复合语句 - 13 -
3.3.4处理数组、指针与结构体 - 14 -
3.3.5处理函数 - 14 -
3.4本章小结 - 15 -
第4章 汇编 - 16 -
4.1 汇编的概念与作用 - 16 -
4.2 在UBUNTU下汇编的命令 - 16 -
4.3 可重定位目标ELF格式 - 16 -
4.4 HELLO.O的结果解析 - 20 -
4.5 本章小结 - 22 -
第5章 链接 - 23 -
5.1 链接的概念与作用 - 23 -
5.2 在UBUNTU下链接的命令 - 23 -
5.3 可执行目标文件HELLO的格式 - 23 -
5.4 HELLO的虚拟地址空间 - 25 -
5.5 链接的重定位过程分析 - 27 -
5.6 HELLO的执行流程 - 28 -
5.7 HELLO的动态链接分析 - 29 -
5.8 本章小结 - 29 -
第6章 HELLO进程管理 - 30 -
6.1 进程的概念与作用 - 30 -
6.2 简述壳SHELL-BASH的作用与处理流程 - 30 -
6.3 HELLO的FORK进程创建过程 - 30 -
6.4 HELLO的EXECVE过程 - 30 -
6.5 HELLO的进程执行 - 31 -
6.6 HELLO的异常与信号处理 - 31 -
6.7本章小结 - 32 -
第7章 HELLO的存储管理 - 33 -
7.1 HELLO的存储器地址空间 - 33 -
7.2 INTEL逻辑地址到线性地址的变换-段式管理 - 33 -
7.3 HELLO的线性地址到物理地址的变换-页式管理 - 33 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 34 -
7.5 三级CACHE支持下的物理内存访问 - 35 -
7.6 HELLO进程FORK时的内存映射 - 35 -
7.7 HELLO进程EXECVE时的内存映射 - 35 -
7.8 缺页故障与缺页中断处理 - 36 -
7.9动态存储分配管理 - 37 -
7.9.1带边界标记的隐式空闲链表 - 38 -
7.9.2显示空间链表 - 38 -
7.10本章小结 - 39 -
第8章 HELLO的IO管理 - 40 -
8.1 LINUX的IO设备管理方法 - 40 -
8.2 简述UNIX IO接口及其函数 - 40 -
8.3 PRINTF的实现分析 - 41 -
8.4 GETCHAR的实现分析 - 43 -
8.5本章小结 - 43 -
结论 - 43 -
附件 - 44 -
参考文献 - 45 -

第1章 概述

1.1 Hello简介

P2P:From Program to Process
用高级语言编写得到.c文件,再经过编译器预处理得到.i文件,进而对其编译得到.s汇编语言文件。此后通过汇编器将.s文件翻译成机器语言,将指令打包成为可重定位的.o目标文件,再通过链接器与库函数链接得到可执行文件hello,执行此文件hello,操作系统会为其fork产生子进程,再调用execve函数加载进程。至此,P2P结束。
020:From Zero-0 to Zero-0
操作系统调用execve后映射虚拟内存,先删除当前虚拟地址的数据结构并为hello创建新的区域结构,进入程序入口后载入物理内存,再进入main函数执行代码。代码完成后,父进程回收hello进程,内核删除相关数据结构。

1.2 环境与工具

(1)硬件环境:X64 CPU;2GHz;4GRAM;256Disk
(2)软件环境:Windows10 64位;Vmware 10;Ubuntu 16.04 LTS 64位
(3)使用工具:Codeblocks;Objdump;Gdb;Hexedit

1.3 中间结果

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.4 本章小结

本章主要介绍了hello的P2P,020过程,以及进行实验时的软硬件环境及开发与调试工具和在本论文中生成的中间结果文件。

第2章 预处理

2.1 预处理的概念与作用

概念:
程序设计领域中,预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。
作用:
最常见的预处理是C语言和C++语言。ISO C和ISO C++都规定程序由源代码被翻译分为若干有序的阶段(phase) ,通常前几个阶段由预处理器实现。预处理中会展开以#起始的行,试图解释为预处理指令 (preprocessing directive) ,其中ISO C/C++要求支持的包括#if/#ifdef/#ifndef/#else/#elif/#endif(条件编译)、#define(宏定义)、#include(源文件包含)、#line(行控制)、#error(错误指令)、#pragma(和实现相关的杂注)以及单独的#(空指令)。预处理指令一般被用来使源代码在不同的执行环境中被方便的修改或者编译。

2.2在Ubuntu下预处理的命令

对hello.c文件进行预处理的命令是:gcc -E -o hello.i hello.c
在这里插入图片描述

目录下会增加一个.i文件
在这里插入图片描述

hello.c截图:
在这里插入图片描述

hello.i部分截图:
在这里插入图片描述

2.3 Hello的预处理结果解析

预处理得到.i文件打开后发现得到了扩展,到了3000多行。原文件中的宏进行了宏展开,增加的文本其实是三个头文件的源码。

2.4 本章小结

概括了预处理的概念和作用,详细说明了ubuntu下预处理命令,并分析了.i文件。

第3章 编译

3.1 编译的概念与作用

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

3.2 在Ubuntu下编译的命令

对hello.i进行编译的命令是:gcc -S -o hello.s hello.i
在这里插入图片描述
目录下会增加一个.s文件
在这里插入图片描述
hello.s部分截图:
在这里插入图片描述

3.3 Hello的编译结果解析

3.3.1处理变量

源程序中只有局部变量,是int i;分析汇编代码并与源程序比较:

在这里插入图片描述在这里插入图片描述
可以看到局部变量放在了寄存器-4(%rbp)中。

3.3.2处理关系操作符与控制语句

本程序出现了if(argc!=4)的!=关系操作符,编译器转换成汇编语言后就成了:
在这里插入图片描述
在这里插入图片描述
我们可以看到argc与4进行比较时,可以看到je指令,cmpl与je是放在一起的,如果两数相等je条件成立,跳转.L2也就是后面的循环否则跳过je继续向下执行。可以看到关系操作符与控制语句是借助jx指令实现的,对与其它的关系操作符有:
在这里插入图片描述

3.3.3处理四则运算与复合语句

(1)加: x=x+y汇编语言是addq y,x
(2)减: x=x-y 汇编语言是subq y,x
(3)乘: x=x*y 汇编语言是imulq y,x
(4)除: z=x/y 汇编语言是
movq x, z
cqto
idivq y
复合语句就是上面的组合,或者也有复合的汇编语句:z=x+Ay+B(A,B都是立即数)的汇编语言是leaq B(x,y,A) z
本程序出现了addq:
在这里插入图片描述

3.3.4处理数组、指针与结构体

(1)数组:取数组头指针加上第i位偏移量来处理。
(2)指针与数组类似,如果rax表示指针所存的寄存器,访问x指向的值就是(%rax)
(3)结构体:通过结构体内部的偏移量来访问。
本程序中出现了数组,截图如下:
在这里插入图片描述

3.3.5处理函数
(1)函数的调用与传参:给函数传参需要先设定寄存器,将参数传给所设的寄存器,再通过call来跳转到调用的函数开头的地址。在源代码中调用了printf、atoi、getchar、sleep和exit:

在这里插入图片描述
第一个printf转换成了puts,把.L0段的立即值传入%rdi,然后call跳转到puts。
这里的exit是把立即数1传入到%edi中,然后call跳转到exit
第二个printf有三个参数,第一个是.LC1中的格式化字符串%eax中,后面的两个依次是%rdi,%rsi,然后跳转到printf
sleep有一个参数传到%edi中,之后call跳转到 sleep中
getchar不需要参数,直接call跳转即可。
(2)返回值:函数的返回值一般在寄存器%eax中,如果有返回值,则要先把返回值存到%eax中,再用ret返回。源程序中有主函数的return 0;就是先把返回值立即数0存到%eax中,再用ret返回。

3.4本章小结

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

第4章 汇编

4.1 汇编的概念与作用

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

4.2 在Ubuntu下汇编的命令

命令为:gcc -c -o hello.o hello.s
在这里插入图片描述
目录下会增加一个.o文件:
在这里插入图片描述

4.3 可重定位目标elf格式

在linux下生成hello.o文件elf格式的命令:readelf -a hello.o > hello.elf
目录下会增加一个.elf文件:
在这里插入图片描述
在这里插入图片描述
分析.elf文件中的内容:
(1)ELF头:ELF头(ELF header)以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含了帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是有节头部表描述的,其中目标文件中每个节都有一个固定大小的条目(entry)。
在这里插入图片描述
(2)节头:记录各节名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐。
在这里插入图片描述
(3)重定位节:
.rela.text,保存的是.text节中需要被修正的信息;任何调用外部函数或者引用全局变量的指令都需要被修正;调用外部函数的指令需要重定位;引用全局变量的指令需要重定位; 调用局部函数的指令不需要重定位;在可执行目标文件中不存在重定位信息。本程序需要被重定位的是printf、puts、exit、sleepsecs、getchar、sleep和.rodata中的.L0和.L1。
.rela.eh_frame节是.eh_frame节重定位信息。
在这里插入图片描述
(4)符号表:.symtab,一个符号表,它存放在程序中定义和引用的函数和全局变量的信息,一些程序员错误地认为必须通过-g选项来编译一个程序,才能得到符号表信息。实际上每个可重定位目标文件在.symtab中都有一张符号表(除非程序员特意用STRIP命令去掉它)。然而,和编译器中的符号表不同,.symtab符号表不包含局部变量的条目。
在这里插入图片描述

4.4 Hello.o的结果解析

命令:objdump -d -r hello.o > hello0.txt
在这里插入图片描述在这里插入图片描述在这里插入图片描述
与hello.s比较发现以下差别:
(1)分支转移:在汇编代码中,分支跳转是直接以.L0等助记符表示,但在反汇编代码中,分支转移表示为主函数+段内偏移量。反汇编代码跳转指令的操作数使用的不是段名称,因为段名称知识在汇编语言中便于编写的助记符,所以在汇编成机器语言之后显然不存在,而是确定的地址。
(2)函数调用:汇编代码中函数调用时直接个函数名称,而在反汇编的文件中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.3 可执行目标文件hello的格式

命令:readelf -a hello > hello1.elf
在这里插入图片描述
(1)ELF头:上次的节头数量为13个,这次变为25个。
在这里插入图片描述
(2)节头:
在这里插入图片描述

5.4 hello的虚拟地址空间

在这里插入图片描述
PHDR:保存程序头表
INTERP:动态链接器的路径
LOAD:可加载的程序段
DYNAMIN:保存了由动态链接器使用的信息
NOTE保存辅助信息
GNU_STACK:标志栈是否可执行
GNU_RELRO:指定重定位后需被设置成只读的内存区域
使用edb打开hello从Data Dump窗口观察hello加载到虚拟地址的状况,并查看各段信息。
在这里插入图片描述
在0x400000~0x401000段中,程序被载入,自虚拟地址0x400000开始,到0x400fff结束,这之间每个节的地址同5.3中图(2)中的地址声明。

5.5 链接的重定位过程分析

命令: objdump -d -r hello > 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.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
在这里插入图片描述
调用init之后的.got.plt
从图中可以看到.got.plt的条目发生变化。

5.8 本章小结

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

第6章 hello进程管理

6.1 进程的概念与作用

概念:
狭义定义:进程是正在运行的程序的实例(an instance of a computer program that is being executed)。
广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
作用:
进程作为一个执行中程序的实例,系统中每个程序都运行在某个进程的上下文中,上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。

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

作用:
是一种交互型的应用级程序,时Linux的外壳,提供了一个界面,用户可以通过这界面访问操作系统内核。
处理流程:
(1)从终端读入输入的命令。
(2)将输入字符串切分获得所有的参数
(3)如果是内置命令则立即执行
(4)否则调用相应的程序为其分配子进程并运行
(5)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)正常运行
在这里插入图片描述
(2)ctrl+c终止
在这里插入图片描述
(3)ctrl+z暂停,输入ps可以发现hello并未关闭
在这里插入图片描述
(4)运行过程中乱按,无关输入被缓存到stdin,并随着printf指令被输出到结果。
在这里插入图片描述

6.7本章小结

概括了进程的概念和作用、shell-bash的处理过程与作用,介绍了fork和execve进程以及hello进程的执行过程中的信号异常处理过程。

第7章 hello的存储管理

7.1 hello的存储器地址空间

(1)逻辑地址:格式为“段地址:偏移地址”,是CPU生成的地址,在内部和编程使用,并不唯一。
(2)物理地址:加载到内存地址寄存器中的地址,内存单元的真正地址。CPU通过地址总线的寻址,找到真实的物理内存对应地址。在前端总线上传输的内存地址都是物理内存地址。
(3)虚拟地址:保护模式下程序访问存储器所用的逻辑地址。
(4)线性地址:逻辑地址向物理地址转化过程中的一步,逻辑地址经过段机制后转化为线性地址。

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

8086共设计了20位宽的地址总线,通过将段寄存器左移4位加上偏移地址得到20位地址,即逻辑地址。将内存分为不同的段,每个段有段寄存器对应,段寄存器有一个栈、一个代码、两个数据寄存器。

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

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

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.5 三级Cache支持下的物理内存访问

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

7.6 hello进程fork时的内存映射

在shell输入命令行后,内核调用fork创建子进程,为hello程序的运行创建上下文,并分配一个与父进程不同的PID。通过fork创建的子进程拥有父进程相同的区域结构、页表等的一份副本,同时子进程也可以访问任何父进程已经打开的文件。当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用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 缺页故障与缺页中断处理

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

7.9动态存储分配管理

printf函数会调用malloc,下面简述动态内存管理的基本方法与策略:
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器分为两种基本风格:显式分配器、隐式分配器。
显式分配器:要求应用显式地释放任何已分配的块。
隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。

堆中的块主要组织为两种形式:
1.隐式空闲链表(带边界标记)
在块的首尾的四个字节分别添加header和footer,负责维护当前块的信息(大小和是否分配)。由于每个块是对齐的,所以每个块的地址低位总是0,可以用该位标注当前块是否已经分配。可以利用header和footer中存放的块大小寻找当前块两侧的邻接块,方便进行空闲块的合并操作。
2.显式空闲链表
在未分配的块中添加两个指针,分别指向前一个空闲块和后一个空闲块。采用该策略,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。

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

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

7.10本章小结

本章简述了在计算机中的虚拟内存管理,虚拟地址、物理地址、线性地址、逻辑地址的区别以及它们之间的变换模式,以及段式、页式的管理模式,在了解了内存映射的基础上重新认识了共享对象、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和vsprintf代码是windows下的。
查看printf代码:
int printf(const char fmt, …)
{
int i;
char buf[256];
va_list arg = (va_list)((char
)(&fmt) + 4);
i = vsprintf(buf, fmt, arg);
write(buf, i);
return i;
}
首先arg获得第二个不定长参数,即输出的时候格式化串对应的值。
查看vsprintf代码:
int vsprintf(char *buf, const char fmt, va_list args)
{
char
p;
char tmp[256];
va_list p_next_arg = args;
for (p = buf; *fmt; fmt++)
{
if (*fmt != ‘%’) //忽略无关字符
{
*p++ = *fmt;
continue;
}
fmt++;
switch (*fmt
{
case ‘x’: //只处理%x一种情况
itoa(tmp, ((int)p_next_arg)); //将输入参数值转化为字符串保存在tmp
strcpy(p, tmp); //将tmp字符串复制到p处
p_next_arg += 4; //下一个参数值地址
p += strlen(tmp); //放下一个参数值的地址
break;
case ‘s’:
break;
default:
break;
}
}
return (p - buf); //返回最后生成的字符串的长度
}
则知道vsprintf程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。
在printf中调用系统函数write(buf,i)将长度为i的buf输出。write函数如下:
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
在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将字符串中的字节“Hello 1183710129 邓昆昆”从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。
字符显示驱动子程序将通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中。
显示芯片会按照一定的刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
于是我们的打印字符串“Hello 1183710129 邓昆昆”就显示在了屏幕上。

8.4 getchar的实现分析

异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码转换成ASCII码,保存到系统的键盘缓冲区之中。getchar函数落实到底层调用了系统函数read,通过系统调用read读取存储在键盘缓冲区中的ASCII码直到读到回车符然后返回整个字串,getchar进行封装,大体逻辑是读取字符串的第一个字符然后返回。

8.5本章小结

Linux提供了一种简单使用的抽象——将系统的IO设备抽象成文件,系统的输入和输出被抽象成文件的写和读操作。在此基础上,Linux对系统IO的操作可以以打开文件、改变文件位置、读写文件、关闭文件的操作进行。同时分析了printf和getchar的实现。
结论
空床坐听南窗雨,谁复挑灯夜补书。hello终于走完了它艰辛的路程:
(一) 编写,将代码键入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、付费专栏及课程。

余额充值