HIT ICS:APP2018大作业:hello的程序人生

摘 要
本文的行文过程就是折腾hello的过程,从可怜的hello刚刚诞生,成为以字节序列形式存储的高级语言程序开始,经历编译链接成长为可执行目标文件(二进制文件)再由SHELL加载创建成为一个进程,为其分配空间、虚拟内存管理以及IO管理,最终生命终结被回收。借由hello的生命历程回顾了一个学期以来计算机系统的内容,对这门课各个部分有了一个很好的贯通。感谢hello!
关键词: IO,虚拟内存,编译,链接,进程

目 录

第1章 概述
1.1 HELLO简介
1.2 环境与工具
1.3 中间结果
1.4 本章小结 第2章 预处理
2.1 预处理的概念与作用
2.2在UBUNTU下预处理的命令
2.3 HELLO的预处理结果解析
2.4 本章小结 第3章 编译
3.1 编译的概念与作用
3.2 在UBUNTU下编译的命令
3.3 HELLO的编译结果解析
3.4 本章小结 第4章 汇编
4.1 汇编的概念与作用
4.2 在UBUNTU下汇编的命令
4.3 可重定位目标ELF格式
4.4 HELLO.O的结果解析
4.5 本章小结 第5章 链接
5.1 链接的概念与作用
5.2 在UBUNTU下链接的命令
5.3 可执行目标文件HELLO的格式
5.4 HELLO的虚拟地址空间
5.5 链接的重定位过程分析
5.6 HELLO的执行流程
5.7 HELLO的动态链接分析
5.8 本章小结 第6章 HELLO进程管理
6.1 进程的概念与作用
6.2 简述壳SHELL-BASH的作用与处理流程
6.3 HELLO的FORK进程创建过程
6.4 HELLO的EXECVE过程
6.5 HELLO的进程执行
6.6 HELLO的异常与信号处理
6.7本章小结 第7章 HELLO的存储管理
7.1 HELLO的存储器地址空间
7.2 INTEL逻辑地址到线性地址的变换-段式管理
7.3 HELLO的线性地址到物理地址的变换-页式管理
7.4 TLB与四级页表支持下的VA到PA的变换
7.5 三级CACHE支持下的物理内存访问
7.6 HELLO进程FORK时的内存映射
7.7 HELLO进程EXECVE时的内存映射
7.8 缺页故障与缺页中断处理
7.9动态存储分配管理
7.10本章小结 第8章 HELLO的IO管理
8.1 LINUX的IO设备管理方法
8.2 简述UNIX IO接口及其函数
8.3 PRINTF的实现分析
8.4 GETCHAR的实现分析
8.5本章小结 结论 附件 参考文献

第1章 概述

1.1 Hello简介
P2P:From Program to Process
图一
如上图:hello.c程序的生命周期是从一个高级C语言程序开始的,这种形式能被人读懂,但计算机并不能够直接执行hello.c,所以为了在系统上运行hello.c程序,每条C语句都必须被其他程序转化成一系列的低级机器语言指令,然后这些指令按照一种被称为可执行目标程序的格式打包并以二进制磁盘文件的形式存放起来
具体如下:预处理器(cpp)根据以字符#开头的命令,修改原始C程序。第一行的命令#include <stdio.h>告诉cpp(预处理器)读取系统头文件stdio.h的内容并把它直接插入到程序文本中,结果得到hello.i文件。编译器(ccl)将文本文件hello.i翻译成文本文件hello.s。hello.s包含一个汇编语言程序,该程序包含main函数的定义。汇编器(as)将hello.s翻译成机器语言指令,并把这些指令打包成一个可重定位目标程序并将结果保存到hello.o中(这是一个二进制文件)。hello程序调用了一个printf()函数,它是C编译器提供的一个标准C库中的一个函数,printf函数存在于一个名为printf.o的单独的预处理好了的目标文件中,而这个文件必须以某种方式合并到hello.o程序中,这个步骤又链接器(ld)进行,结果得到hello文件,它是一个可执行目标文件,可以被加载到内存中有系统执行。
020:From Zero-0 to Zero-0
系统调用fork函数为hello.c创建一个进程,然后为这个进程创建一个用户级虚拟地址空间,包括代码和数据段、堆、共享库以及用户栈,还创建了打开文件描述符;然后映射共享区域,设置程序计数器,使之指向代码区域的入口点,然后进入main函数,CPU为hello这个进程分配执行逻辑控制流的时间片。接下来hello通过Unix I/O管理来控制输出。hello执行完成后shell中的init会回收hello进程,并且内核会从系统中删除hello所有痕迹,至此,hello完成O2O的 过程
1.2 环境与工具
硬件环境:
Intel(R)Core™i5-7200U CPU 2.50GHx 2.70GHz 8G RAM X64 256GSSD + 1TGHD
软件环境:
操作系统:Winsows 64
虚拟机:Vmware 14
Linux:16.04
调试软件:EDB
1.3 中间结果

1.hello.c 源文件
2.hello.i 预处理后生成的.i文件
3.hello.s 编译中生成的汇编文件
4.hello.o hello.s经翻译成机器指令后打包成的可重定位目标文件
5.hello_asm_o.txt:hello.o反汇编形成的汇编文件
6.hello.elf hello.o的elf文件
7.hello_o.elf hello.o的elf文件

1.4 本章小结
对hello的一系列变化有一个直观的了解,也是下文的一个总纲;

第2章 预处理

2.1 预处理的概念与作用
(一) 预处理概念:所谓预处理是指进行编译的第一遍扫描之前所做的工作,是C语言中的一个重要功能,由预处理程序负责完成。
(二) 预处理作用:
C语言提供了多种预处理功能,如宏定义,文件包含,条件编译等;

  1. 宏定义
    A. 不带参数
    宏定义又称为宏代换、宏替换,简称“宏”。预处理(预编译)工作也叫 做宏展开:将宏名替换为字符串, 即在对相关命令或语句的含义和功能作具体分析之前就要换。
    格式:#define 标识符 字符串

a) 宏名一般用大写;
b) 使用宏可提高程序的通用性和易读性,减少不一致性,减少输入错误和便于修改。例如:数组大小常用宏定义;
c) 预处理是在编译之前的处理,而编译工作的任务之一就是语法检查,预处理不做语法检查;
d) 宏定义末尾不加分号;
e) 宏定义写在函数的花括号外边,作用域为其后的程序,通常在文件的最开头;
f) 可以用#undef命令终止宏定义的作用域;
g) 宏定义允许嵌套;
h) 字符串( " " )中永远不包含宏;
i) 宏定义不分配内存,变量定义分配内存;
j) 宏定义不存在类型问题,它的参数也是无类型的。

B. 带参数

a) 实参如果是表达式容易出问题
b) 宏名和参数的括号间不能有空格;
c) 宏替换只作替换,不做计算,不做表达式求解;
d) 函数调用在编译后程序运行时进行,并且分配内存。宏替换在编译前进行,不分配内存
e) 宏的哑实结合不存在类型,也没有类型转换。
f) 宏展开使源程序变长,函数调用不会
g) 宏展开不占运行时间,只占编译时间,函数调用占运行时间(分配内存、保留现场、值传递、返回值)。

  1. 文件包含
    A. 文件包含处理是指在一个源文件中,通过文件包含命令将另一个源文件的内容全部包含在此文件中。在源文件编译时,连同被包含进来的文件一同编译,生成目标目标文件。
    B. 文件包含的处理方法:
    (1) 处理时间:文件包含也是以"#"开头来写的(#include ), 那么它就是写给预处理器来看了, 也就是说文件包含是会在编译预处理阶段进行处理的。
    (2) 处理方法:在预处理阶段,系统自动对#include命令进行处理,具体做法是:将包含文件的内容复制到包含语句(#include )处,得到新的文件,然后再对这个新的文件进行编译。
  2. 条件编译
    A. 程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。条件编译指令将决定哪些代码被编译,而哪些不被编译的。可以根据表达式的值或者某个特定的宏是否被定义来确定编译条件。
    2.2在Ubuntu下预处理的命令
    图二
    2.3 Hello的预处理结果解析
    在预处理文本文件hello.i中,首先是对文件包含中系统头文件的寻址和解析;
    在这里插入图片描述
    在这里插入图片描述

在这里插入图片描述

2.4 本章小结
本章就是了解预处理的概念,以及hello预处理的结果;

第3章 编译

3.1 编译的概念与作用
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
(一) 编译概念:将便于人编写,阅读,维护的高级计算机语言所写作的源代码,翻译为计算机能解读、运行的低阶机器语言的程序。
(二) 编译作用:编译作用主要是将文本文件hello.i翻译成文本文件hello.s,并在出现语法错误时给出提示信息;

  1. 词法分析:编译器的第一个步骤称为词法分析或扫描。词法分析器读入组成源程序的字符流,并将其组成有意义的词素的序列。形如<token-name, attribute-value>这样的词法单元。(token-name是由语法分析使用的抽象符号,attribute-value是指向符号表中关于这个词法单元的条目,符号表条目的信息会被语义分析和代码生成步骤使用)
  2. 语法分析:编译的第2个步骤称为语法分析或解析。语法分析器使用由词法分析器生成的各词法单元的第一个分量来创建树形的中间表示。该中间表示给出了词法分析产生的词法单元的语法结构。常用的表示方法是语法树,树中每个内部节点表示一个运算,而该节点的子节点表示运算的分量。
  3. 语义分析:使用语法树和符号表中的信息来检查源程序是否和语言定义的语义一致。它同时收集类型信息,并存放在语法树或符号表中,以便在中间代码生成过程使用。
  4. 中间代码生成:在源程序的语法分析和语义分析完成之后(也会生成中间表示,区别语法树),很多编译器生成一个明确的低级的或类机器语言的中间表示。该中间表示有两个重要的性质:1.易于生成;2.能够轻松地翻译为目标机器上的语言。
  5. 代码优化:代码优化试图改进中间代码,以便生成更好的目标代码。即更快(省时),更短(省空间)或能耗更低。
  6. 代码生成:代码生成以中间表示形式作为输入,并把它映射为目标语言。如果目标语言是机器代码,则必须为每个变量选择寄存器或内存位置,中间指令则被翻译为能够完成相同任务的机器指令序列。
    3.2 在Ubuntu下编译的命令
    在这里插入图片描述

3.3 Hello的编译结果解析
3.3.1全局变量和静态变量
32位的全局变量是利用间接寻址法进行寻址而64位是利用直接寻址来进行的;下面是hello.c中的全局变量定义:
int sleepsecs=2.5;
下面是hello.s中关于全局变量的信息:

其中显示了全局变量sleepsecs位于.symtab.globl中;类型和数值为long和2;对齐是4字节;链接器修改引用的方式是object;
3.3.2局部变量
局部变量是利用栈来管理的,调用函数把栈顶指针压栈后就会为局部变量腾出空间,最后再释放;
hello.c中的局部变量:
int i;

把rsp减去0x32就是在栈中为局部变量准备空间;
3.3.3数据类型
i. 整型
这个就和普通的全局变量或局部变量一样,就不做详述了;
ii. 字符串
(一) 概念
字符串作为printf也就是puts的参数,是作为全局变量被维护的
,被保存在.rodata中;
(二) 在hello中的应用
hello中共有两个全局变量字符:

iii. 数组
(一) 概念及性质
数组作为参数使用寄存器rsi和rdi传参的,传完后被保存进栈中
(二) 在hello中的应用

上面是for循环中对main函数参数数组argv的操作,对应下面的C代码:
for(i=0;i<10;i++)
{
printf(“Hello %s %s\n”,argv[1],argv[2]);
sleep(sleepsecs);
}
由汇编代码以及函数传参的知识可知,argv[2]作为printf函数的第三个参数,应当存于寄存器%rdx中,因此可推断argv[2]地址为-0x16(%rbp);argv[1]作为printf第二个参数,应当存于寄存器%rsi中,因此可推断argv[1]地址为-0x2A(%rbp)中,数组首地址位于-0x32(%rbp) ,以上所占字节数为8。
3.3.4数据传送
(一)概念及特点
整型数据的赋值使用mov指令完成,根据数据的大小不同使用不同后缀,分别为:
指令 效果 描述
MOV S,D D<—S 传送
movb R<—I 传送字节
movw R<—I 传送字
movl R<—I 传送双字
movq R<—I 传送四字
(二)在hello中的应用

在hello中多次用到mov指令,这里不一一列举了;
3.3.5算术和逻辑操作
(一)概念及特点
指令 效果 描述
leaq S,D D<—&S 加载有效地址
INC D D<—D+1 加一
DEC D D<—D-1 减一
NEG D D<—-D 取负
NOT D D<—~D 取补
ADD S,D D<—D+S 加
SUB S,D D<—D-S 减
IMUL S,D D<—D*S 乘
XOR S,D D<—D^S 异或
OR S,D D<—D|S 或
AND S,D D<—D&S 与
SAL k,D D<—D<<k 左移
SHL k,D D<—D>>k 左移(等同于SAL)
SAR k,D D<—D>>(A)k 算术右移
(二)在hello中的应用
增加栈空间和恢复栈:

利用间接寻址访问数组时对地址:

3.3.6跳转指令
(一)概念及特点

(二)在hello中的应用
argc与3不等的情况下:

比较argc与3如果相等则执行循环,不等则打印提示信息退出程序:

3.3.7转移控制(call)

(二)在hello中的应用

  1. printf

  2. sleep

  3. getchar

3.4 本章小结
本章的内容是围绕hello的汇编代码展开的,展示了C语言的数据类型、函数调用、参数传递等操作的汇编实现;

第4章 汇编

4.1 汇编的概念与作用
(一) 汇编概念::把汇编语言翻译成机器语言的过程称为汇编。
(二) 作用:汇编器(as)将hello.s翻译成机器语言指令,并把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在二进制目标文件hello.o中
4.2 在Ubuntu下汇编的命令

4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
ELF头:

重定位项目分析:
a)重位条目放在.rel.text中,被组织成如下的结构体格式;offset是需要被修改的引用的节偏移。Symbol标识被修改引用应该指向的符号。type告知链接链接器如何修改新的引用。addend是一个有符号常数,一些类型的重定位要使用它对被修改引用的值做偏移调整;
typedef struct
{
long offset; /Offset of the reference to relocate/
long type:32, /Relocation type/
symbol:32; /Symbol table index/
long addend; /Constant part of relocation expression/
}Elf64_Rela;
b)ELF定义了32种不同的重定位类型,有些相当隐秘,我们只关心两种最基本的重定位类型:
(1) R X86_ 64 PC32。 重定位-一个使用32位PC相对地址的引用。一个PC相对地址就是距程序计数器(PC)的当前运行时值的偏移量。当CPU执行一条使用PC相对寻址的指令时,它就将在指令中编码的32位值加上PC的当前运行时值,得到有效地址(如call指令的目标),PC值通常是下一条指令在内存中的地址。
(2) R X86_ 64 _32。 重定位一个使用32位绝对地址的引用。通过绝对寻址,CPU直接使用在指令中编码的32位值作为有效地址,不需要进一步修改。
同时对于不同类型的地址也有不同的代码实现之:

c)hello中重定位PC相对引用
以对puts函数的引用为例:
重定位条目r由4个字段组成:(书上P481

4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
主要差别如下:
分支转移:反汇编代码跳转指令的操作数使用的不是段名称如.L3,因为段名称只是在汇编语言中便于编写的助记符,所以在汇编成机器语言之后显然不存在,而是确定的地址。
函数调用:在.s文件中,函数调用之后直接跟着函数名称,而在反汇编程序中,call的目标地址是当前下一条指令。这是因为hello.c中调用的函数都是共享库中的函数,最终需要通过动态链接器才能确定函数的运行时执行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其call指令后的相对地址设置为全0(目标地址正是下一条指令),然后在.rela.text节中为其添加重定位条目,等待静态链接的进一步确定。
全局变量访问:在.s文件中,访问rodata(printf中的字符串),使用段名称+%rip,在反汇编代码中0+%rip,因为rodata中数据地址也是在运行时确定,故访问也需要重定位。所以在汇编成为机器语言时,将操作数设置为全0并添加重定位条目。

4.5 本章小结
本章介绍了hello.s到hello.o的历程,以及将编译生成的汇编文件与反汇编生成的文件进行比较;

第5章 链接
5.1 链接的概念与作用
概念:链接是将各种代码和数据片段收集并组合称为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。

作用:链接可以执行于编译时,也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被加载到内存并执行时;甚至执行于运行时,也就是由应用程序来执行。早期计算机系统中链接时手动执行的,在现代系统中,链接器由链接器自动执行。链接器使得分离编译成为可能。开发过程中无需将大型的应用程序组织委员一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。
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/5/crtbegin.o
/usr/lib/gcc/x86_64-linux-gnu/5/crtend.o
/usr/lib/x86_64-linux-gnu/crtn.o
hello.o -lc -z relro -o hello
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件

5.3 可执行目标文件hello的格式
 Elf 头
字大小、字节顺序、文件类型(.o,exec,.so),机器类型,等等
段头表/程序头表
页面大小,虚拟地址内存段(节),段大小
 .text 节(代码)
代码
 .rodata 节(只读数据)
只读数据 : 跳转表, …
 .data 节 (数据/可读写)
已初始化全局变量
 .bss 节 (未初始化全局变量)
未初始化的全局变量
“Block Started by Symbol” 符号开始的块
“Better Save Space” 更加节省空间
有节头,但不占用空间
 .symtab 节(符号表)
符号表
函数和静态变量名
节名称和位置
 .rel.text 节(可重定位代码)
 .text 节的可重定位信息
在可执行文件中需要修改的指令地址
需修改的指令.
 .rel.data 节(可重定位数据)
 .data 节的可重定位信息
在合并后的可执行文件中需要修改的指针数据的地址
 .debug 节(调试)
为符号调试的信息 (gcc -g)
节头表Section header table
每个节的偏移量和大小

节头部表中包含了各段的基本信息,包括名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐等信息:
ELF头:

节头部表: 程序头:

.symtab:

5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
下面是程序头部的数据结构:
struct Elf64_Phdr
{
Elf64_Word p_type; /* Segment type /
Elf64_Word p_flags; /
Segment flags /
Elf64_Off p_offset; /
Segment file offset /
Elf64_Addr p_vaddr; /
Segment virtual address /
Elf64_Addr p_paddr; /
Segment physical address /
Elf64_Xword p_filesz; /
Segment size in file /
Elf64_Xword p_memsz; /
Segment size in memory /
Elf64_Xword p_align; /
Segment alignment */
};
Edb下的虚拟地址空间:

以下进行举例对应:
程序头:

PHRD部分:

GUN_RELOAD部分:

5.5 链接的重定位过程分析
(以下格式自行编排,编辑时删除)
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
5.6 hello的执行流程
加载程序 ld-2.23.so!_dl_start
ld-2.23.so!_dl_init
ld-2.23.so!_dl_start
ld-2.23.so!_dl_init
LinkAddress!_start
ld-2.23.so!_libc_start_main
ld-2.23.so!_cxa_atexit
运行 LinkAddress!_libc_csu.init
程序终止 ld-2.23.so!_setjmp
5.7 Hello的动态链接分析
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
先观察调用dl_init前,动态库函数指向的地址。从图5.3.3中能够读取到GOT表的起始位置,即0x601000。在dl_init调用之前可以查看其值,发现均为0。调用dl_init后再次查看,根据GOT表的每一项为8字节,可以推得GOT[2]也就是存放动态链接器入口的地址为0x601010。经过dl_init的调用,这里已经有了一段地址,为0x7f906bbf2870。

Ddl_init调用前

Ddl_init 调用后
为了验证延迟绑定的实现,可以查看printf调用前后printf@plt的指令跳转地址,也就是对应GOT中的值,可以发现,调用后确实链接到了动态库。
5.8 本章小结
本章讲述了链接的过程,包括重定位,hello的ELF格式等,到此为止hello被编译的道路已经结束,
(第5章1分)

第6章 hello进程管理
6.1 进程的概念与作用
一、进程的概念:
进程是计算机科学中最深刻、最成功的概念之一,其最经典的定义就是一个执行中程序的实例;
二、进程的作用:
1.进程提供给我们一个假象,就好像我们的程序是系统中当前运行的唯一的程序一样。我们的程序好像是独占的使用处理器和内存。
2.进程提供给应用程序的关键抽象:
(1)一个独立的逻辑控制流:它提供一个假象,好像我们的程序独占的使用CPU;
(2)一个私有的地址空间:它提供一个假象,好像我们的程序独占的用内存系统;
6.2 简述壳Shell-bash的作用与处理流程
shell作为UNIX的一个重要组成部分,是它的外壳,也是用户于UNIX系统交互作用界面。Shell是一个命令解释程序,也是一种程序设计语言。

  1. 读入命令行、注册相应的信号处理程序、初始化进程组。

  2. 通过paraseline函数解释命令行,如果是内置命令则直接执行,否则阻塞信号后创建相应子进程,在子进程中解除阻塞,将子进程单独设置为一个进程组,在新的进程组中执行子进程。父进程中增加作业后解除阻塞。如果是前台作业则等待其变为非前台程序,如果是后台程序则打印作业信息。
    6.3 Hello的fork进程创建过程
    a) 创建过程:父进程调用fork()函数创建子进程,新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同但独立的一个副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大的区别是它们有不同的PID;
    b) 说明:值得注意的是fork函数被调用一次却会返回两次:一次在调用进程(父进程)中,一次是在新创建的子进程中。在父进程中,fork返回子进程的PID。在子进程中,fork返回0.因为子进程的PID总是为非0,返回值就提供一个明确的方法来分辨程序是在父进程还是在子进程中执行。
    以下为hello的进程图:

6.4 Hello的execve过程
加载过程:在execve加载了filename之后,它调用程序入口点处的函数,也就是_start函数的地址。start函数调用系统启动函数 _libc_start_main,它初始化执行环境,调用用户层的mian函数。上述的 启动代码设置栈。并将控制传递给新程序的主函数,该主函数有如下形式的原型:
int main(int argc, char **argv,char **envp)
或者等价的:
int main(int argc, char *argv[],char *envp[])
当main开始执行时,用户栈的组织结构如下图所示。

6.5 Hello的进程执行
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
a) 逻辑控制流:如果用调试器单步执行程序,我们会看到一系列程序计数器(PC)的值。这些值唯一的对应于包含在程序的可执行目标文件中的指令,或是包含在运行时动态链接带程序的共享对象中的指令。这个PC的值的序列坏主意逻辑控制流,或简称逻辑流;
b) 时间片:一个进程和其他进程轮流运行的概念成为多任务。一个进程执行它的控制流的一部分的没一时间段叫做时间片。
c) 调度:在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被枪占了的进程。这种决策叫调度。
d) 进程调度的过程:

如上图进程A初始试运行在用户模式中,直到它通过执行系统调用陷入内核。内核中的陷阱处理程序请求来自磁盘控制的DMA传输,并且安排在磁盘控制器完成从磁盘到内存的数据传输后,磁盘中断处理器。磁盘取数据要用一段相对较长的时间(数量级为几十毫秒),所以内核执行从进程A到进程B的上下文切换,而不是在这个间歇时间内等待,什么都不做。注意在切换之前,内核正代表进程A在用户模式下执行指令(即没有单独的内核进程)。在切换的第一部分中,内核代表进程A在内核模式下执行指令。然后在某一时刻,它开始代表进程B(仍然是内核模式下)执行指令。在切换之后,内核代表进程B在用户模式下执行指令。
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
如图,是正常执行hello程序的结果,当程序执行完成之后,进程被回收。

如下图,是在程序输出2条info之后按下ctrl-z的结果,当按下ctrl-z之后,shell父进程收到SIGSTP信号,信号处理函数的逻辑是打印屏幕回显、将hello进程挂起,通过ps命令我们可以看出hello进程没有被回收,此时他的后台job号是1,调用fg 1将其调到前台,此时shell程序首先打印hello的命令行命令,hello继续运行打印剩下的8条info,之后输入字串,程序结束,同时进程被回收。

如下图是在程序输出3条info之后按下ctrl-c的结果,当按下ctrl-c之后,shell父进程收到SIGINT信号,信号处理函数的逻辑是结束hello,并回收hello进程。

6.7本章小结
在本章中,介绍了进程的定义与作用,以及Shell的一般处理流程,调用fork创建新进程,调用execve执行hello,hello的进程执行,hello的异常与信号处理。
(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
a) 逻辑地址空间:段地址:偏移地址
23:8048000 段寄存器(CS等16位):偏移地址(16/32/64)
实模式下: 逻辑地址CS:EA =物理地址CS*16+EA
保护模式下:以段描述符作为下标,到GDT/LDT表查表获得段地址,
段地址+偏移地址=线性地址。
b) 线性地址空间: 非负整数地址的有序集合:
{0, 1, 2, 3 … }
c) 虚拟地址空间: N = 2n 个虚拟地址的集合 ===线性地址空间
{0, 1, 2, 3, …, N-1}
d) 物理地址空间: M = 2m 个物理地址的集合
{0, 1, 2, 3, …, M-1}
e) 以hello中的puts调用为例:mov $0x400714,%edi callq 4004a0,$0x400714为puts输出字符串逻辑地址中的偏移地址,需要经过段地址到线性地址的转换变为虚拟地址,然后通过MMU转换为物理地址,才能找到对应物理内存。
7.2 Intel逻辑地址到线性地址的变换-段式管理
被选中的段描述符先被送至描述符cache,每次从描述符cache中取32位段基址,与32位段内偏移量(有效地址)相加得到线性地址

7.3 Hello的线性地址到物理地址的变换-页式管理
(一)页:VM系统将虚拟内存分割成成为虚拟页的大小固定的块。类似的,物理内存被分割为物理页(物理页也称页帧)。
在任意时刻,虚拟页的集合都分为三个不相交的子集:
未分配的、缓存的、未缓存的;
结构如下图所示:

(二)页表:就是一个页表条目的数组;

(三)地址翻译的过程:形式上来说地址翻译是一个N元素的虚拟地址空间(VAS)中的元素和一个M元素的物理地址空间(PAS)中元素的映射。CPU中存在一个控制寄存器为页表基址寄存器指向当前页表。n位的虚拟地址包含着p位的虚拟页面偏移和(n-p)位的虚拟页号。MMU利用VPN来选择适当的PTE,将页表条目中的物理页号和虚拟地址中的VPO串联起来就得到相对应的物理地址。

(四)页命中时CPU硬件的执行顺序:

  1. 处理器生成一个虚拟地址,并把它传送给MMU。
  2. MMU生成PTE地址,并从高速缓存/主存请求得到它。
  3. 高速缓存/主存向MMU返回PTE。
  4. MMU构造物理地址,并把它传送给高速缓存/主存。
  5. 高速缓存/主存返回所请求的数据字给处理器。

(五)缺页
第1步到第3步和上文的一样。
4. PTE中的有效位为零,所以MMU触发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序。
5. 缺页处理程序确定出物理内存中的牺牲页,如果这个也米娜已经修改
7.4 TLB与四级页表支持下的VA到PA的变换
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.5 三级Cache支持下的物理内存访问
Core i7内存系统

获得了物理地址VA,使用CI(后六位再后六位)进行组索引,每组8路,对8路的块分别匹配CT(前40位)如果匹配成功且块的valid标志位为1,则命中(hit),根据数据偏移量CO(后六位)取出数据返回。
如果没有匹配成功或者匹配成功但是标志位是1,则不命中(miss),向下一级缓存中查询数据(L2 Cache->L3 Cache->主存)。查询到数据之后,一种简单的放置策略如下:如果映射到的组内有空闲块,则直接放置,否则组内都是有效块,产生冲突(evict),则采用最近最少使用策略LFU进行替换。
7.6 hello进程fork时的内存映射
一个对象可以被映射到虚拟内存的一个区域,要么作为共享对象,要么作为私有对象。如果一个进程将一个共享对象映射到它的虚拟地址空间的一个区域内,那么这个进程对这个区域的任何写操作,对于那些也把这个共享对象映射到它们虚拟内存的其他进程而言,也是可见的。而且,这些变化也会反映在磁盘上的原始对象中。

另一方面,对于一个映射到私有对象的区域做的改变,对于其他进程来说是不可见的,并且进程对这个区域所做的任何写操作都不会反映在磁盘上的对象中。一个映射到共享对象的虚拟内存区域叫做共享区域。类似地,也有私有区域。下图为共享对象使用实例:

私有对象使用一种叫写时复制来映射至虚拟内存中,多个进程可将一个私有对象映射到其内存不同区域,共享该对象同一物理副本对于每个映射私有对象的进程,相应私有区域的页表条目都被标记为只读,并且区域结构被标记为私有的写时复制。只要没有进程试图写它自己的私有区域,它们就可以继续共享物理内存中对象的一个单独副本。然而,只要有一个进程试图写私有区域内的某个页面,那么这个写操作就会触发一个保护故障。

当故障处理程序注意到保护异常是由于进程试图写私有的写时复制区域中的一个页面而引起的,它就会在物理内存中创建这个页面的一个新副本,更新页表条目指向这个新的副本,然后恢复这个页面的可写权限。下图为写时复制的示例:

当fork函数被系统调用时,内核会为hello创建子进程,同时会创建各种数据结构并分配给hello唯一的PID。为了给hello创建虚拟内存,内核创建了当前进程的mm_struct、区域结构和样表的原样副本,并将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为写时复制。
7.7 hello进程execve时的内存映射
execve函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下几个步骤:

删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构。
映射私有区域,为新程序的代码、数据、bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区,bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零。
映射共享区域, hello程序与共享对象libc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
设置程序计数器(PC),execve做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。

7.8 缺页故障与缺页中断处理
缺页异常及其处理过程:如果DRAM缓存不命中称为缺页。当地址翻译硬件从内存中读对应PTE时有效位为0则表明该页未被缓存,触发缺页异常。缺页异常调用内核中的缺页异常处理程序。

缺页处理程序搜索区域结构的链表,把A和每个区域结构中的vm_start和vm_end做比较,如果指令不合法,缺页处理程序就触发一个段错误、终止进程。

缺页处理程序检查试图访问的内存是否合法,如果不合法则触发保护异常终止此进程。

缺页处理程序确认引起异常的是合法的虚拟地址和操作,则选择一个牺牲页,如果牺牲页中内容被修改,内核会将其先复制回磁盘。无论是否被修改,牺牲页的页表条目均会被内核修改。接下来内核从磁盘复制需要的虚拟页到DRAM中,更新对应的页表条目,重新执行导致缺页的指令。以下为Linux缺页处理简图:

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

带边界标签的隐式空闲链表
1) 堆及堆中内存块的组织结构:

显示空闲链表:

使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。

维护链表的顺序有:后进先出(LIFO),将新释放的块放置在链表的开始处,使用LIFO的顺序和首次适配的放置策略,分配器会最先检查最近使用过的块,在这种情况下,释放一个块可以在线性的时间内完成,如果使用了边界标记,那么合并也可以在常数时间内完成。按照地址顺序来维护链表,其中链表中的每个块的地址都小于它的后继的地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。平衡点在于,按照地址排序首次适配比LIFO排序的首次适配有着更高的内存利用率,接近最佳适配的利用率。
7.10本章小结
本章介绍了各种地址空间以及不同的管理方式以及fork、execve的内存映射等,还有动态内存分配的内容;
(第7章 2分)

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
UnixIO接口:打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。
Shell创建的每个进程都有三个打开的文件:标准输入,标准输出,标准错误。
改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。
读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。
Unix I/O函数:int open(char* filename,int flags,mode_t mode) ,进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
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的实现分析
(以下格式自行编排,编辑时删除)
https://www.cnblogs.com/pianist/p/3315801.html
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
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;
}

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); //返回最后生成的字符串的长度
}
va_list的定义是:typedef char * va_list,因此通过调用va_start函数,获得的arg为第一个参数的地址。

vsprintf的作用是格式化。接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出,例如hello中:

printf(“Hello %s %s\n”,argv[1],argv[2]);

命令行参数为./hello 1173710217 hpy,则对应格式化后的字符串为:Hello 1173710217 hpy\n,并且i为返回的字符串长度

接下来是write函数:
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL

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 1170300825 lidaxin”从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。

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

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

于是我们的打印字符串“Hello 1170300825 lidaxin”就显示在了屏幕上。
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
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系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码转换成ASCII码,保存到系统的键盘缓冲区之中。

getchar函数落实到底层调用了系统函数read,通过系统调用read读取存储在键盘缓冲区中的ASCII码直到读到回车符然后返回整个字串,getchar进行封装,大体逻辑是读取字符串的第一个字符然后返回。
8.5本章小结
本章通过LinuxI/O设备管理方法以及Unix I/O接口及函数了解系统级I/O的底层实现机制。通过对printf和getchar函数的底层解析加深对Unix I/O以及异常中断等的了解。
(第8章1分)
结论
hello历险记,看看我们是怎么折腾它的吧:
 预处理:将hello.c调用的所有外部的库合并到hello.i文件中
 编译:将hello.i编译成为汇编文件hello.s
 汇编:将hello.s会变为可重定位目标文件hello.o
 链接:将hello.o与可重定位目标文件和动态链接库链接成为可执行目标程序hello
 加载运行:在shell中运行
 创建子进程:shell进程调用fork为其创建子进程
 运行程序:shell调用execve,execve调用启动加载器,映射虚拟内存,调用启动函数进入程序入口后程序开始载入物理内存,然后进入 main函数。
 执行逻辑控制流的指令:CPU为其分配时间片,在一个时间片中,hello享有CPU资源,顺序执行自己的控制逻辑流
 地址翻译:MMU将程序中使用的虚拟内存地址通过页表映射成物理地址。
 动态申请内存:printf会调用malloc向动态内存分配器申请堆中的内存。
 信号:如果运行途中键入ctr-c ctr-z则调用shell的信号处理函数分别停止、挂起。
 结束:shell父进程回收子进程,内核删除为这个进程创建的所有数据结构。
(结论0分,缺失 -1分,根据内容酌情加分)

附件
(附件0分,缺失 -1分)
hello.c hello.c 源文件
hello.i hello.i 预处理后生成的.i文件
hello.s 编译中生成的汇编文件
hello.o hello.s经翻译成机器指令后打包成的可重定位目标文件
hello_asm_o.txt hello.o反汇编形成的汇编文件
hello.elf hello的elf文件
hello_o.elf .hello.o的elf文件

参考文献
为完成本次大作业你翻阅的书籍与网站等
[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分)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ape:hello code world

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值