HELLO 程序人生

计算机科学与技术学院
2019年12月
摘 要
Hello程序看似简单实现起来却需要大量流程。Hello程序也有着其不平凡的一生,本文以hello.c为例子,讲述在linux系统下从一个c文件至可执行文件,并且经过处理成为一个进程,后又在软硬件支持下运行,直到进程被回收的过程。以此为例,更好的讲述计算机的底层实现,表明程序的一生。

关键词:P2P;O2O;linux;计算机系统;

(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)

目 录

第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驱动程序首先运行C预处理器(cpp),它将C的源程序翻译成一个ASCII码的中间文件hello.i,接下来驱动程序运行C编译器,将hello.i翻译成一个ASCII汇编语言文件hello.s,然后驱动程序运行汇编器,将hello.s翻译成一个可重定位目标文件hello.o,最后运行链接器程序将hello.o和必要的系统目标文件链接组合起来,创建一个可执行文件hello。执行文件,操作系统会fork产生子进程,再调用execve函数加载该进程。
O2O:shell调用execve函数加载并运行hello,进行虚拟内存的映射,设置当前进程上下文中的程序计数器,指向代码区域的入口点,CPU引用第一个被映射的虚拟页面后开始按需载入物理内存。之后进入main函数执行代码,CPU为hello分配时间片执行逻辑控制流。程序运行结束后,shell父进程回收hello进程,其相关数据结构被内核删除

1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件环境:Intel® Core™ i7-8750H CPU @ 2.20GHz
软件环境:windows10 64位
虚拟机VMware Workstation Pro
Ubuntu 18.04
调试工具:gcc edb gdb objdump ld realef
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
1.4 本章小结
本章主要简单的介绍了hello以及其P2P和O2O的过程,列出当前软硬件环境和编译以及调试工具,并且列出了编写此论文需要的中间产物及其作用。
(第1章0.5分)

第2章 预处理
2.1 预处理的概念与作用
概念:预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如hello.c中第一行的#include命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中。结果就得到了一个C程序,通常是以.i作为文件扩展名。
2.作用:将源文件中以“include”格式包含的文件复制到编译的源文件中;用实际值替换#define定义的字符串;根据#if后面的条件决定需要编译的代码

2.2在Ubuntu下预处理的命令
Gcc -E hello.c -o hello.i

					图2-1 hello.c预处理生成hello.i

2.3 Hello的预处理结果解析
预处理的结果得到的hello.i文件得到了扩展,文件大小从534bytes扩展到66.1kB,从hello.c文件的38行扩展至3118行,证明其将头文件内容进行了引入,对文件中的宏进行了宏展开。
2.4 本章小结
了解了预处理的概念和作用以及ubuntu下预处理命令,分析了.i文件中的信息。
(第2章0.5分)

第3章 编译
3.1 编译的概念与作用
概念:编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序,以一种文本格式描述了低级机器语言指令。
作用:检查语法错误、代码规范性、进行语法分析,通过文本格式描述了低级机器语言指令,汇编语言为不同的编译器提供了通用的输出语言。

3.2 在Ubuntu下编译的命令
Gcc -S hello.i -o hello.s

							图3-1 hello.i 编译生成hello.s

3.3 Hello的编译结果解析

(以下格式自行编排,编辑时删除)
3.3.1 汇编文件解析:
.file 源文件的声明
.text 代码段声明
.globl 全局变量的声明
.data 数据段的声明
.align 对齐方式
.type 类型指定
.size 大小声明
.section .rodata 只读数据段

3.3.2数据
1.int sleepsecs=2.5

			图3-2 sleepsecs全局变量

从图中可以分析出,sleepsecs位于.data段,并且是全局变量globl,对齐方式为4,类型为object,大小.size为4字节。
2.argc是传入main函数的第一个参数,位于%rdi寄存器,后又放置在栈中。
3.常量3,9以立即数的形式出现在代码段。

		图3-3立即数3

		图3-4立即数9

4.局部变量int i,局部变量一般保存在寄存器或者栈中,i保存在栈中,大小四字节。

		图3-5局部变量i

5.输出字符串位于rodata节,且编码已经给出

3.3.3赋值
1.对全局变量sleepsecs的赋值为2

	图3-6 sleepsecs赋值

2.对于局部变量i的赋值
通常情况下,赋值操作汇编为汇编指令MOV,根据指令movx(x为b,w,l,q,absq等),上图3-5是对局部变量i的赋值。

3.3.4类型转换
在这个文件中的类型转换涉及全局变量sleepsecs,由于定义的时候为int类型,赋值2.5的时候,进行了隐式类型转换被赋值2,如图3-6。

3.3.5算术操作
在c文件中i++为算术操作,经过编译器利用指令编译为

     图3-7 i++操作

3.3.6关系操作
1.i<10经过编译器编译为:

	图3-8 i<10关系操作

2.argc!=3经过编译器编译为:

	图3-9 argc!=3编译操作

上述都是通过cmpl指令设置条件码,为下一步的jle和je操作做准备

3.3.7数组/指针/结构操作
指针数组:char *argv[]:在argv[0]指向输入程序的路径和名称,argv[1]和argv[2]表示指向两个字符串的指针。如图3-10所示,-32(%rbp)中存储的是argv[0]的地址,之后进行加法操作和内存操作数先后取出argv[2]和argv[1]。

			图3-10 取指针数组操作

3.3.8控制转移
1.if(argc!=3)的控制转移操作
由图3-9可知,先通过cmpl语句进行比较立即数3和-20(%rbp),设置条件码后,je根据条件码,如果ZF零标志代表着比较结果相等,则跳转至.L2中,否则跳转继续执行
2.for(i=0;i<10;i++) for循环的控制转移操作
如图3-8可知,先通过比较立即数9和-4(%rbp)代表i进行cmpl操作,设置条件码,jle根据设置的条件码决定跳转至.L4还是继续运行下一条指令。

3.3.9函数操作
1.main函数:
传递参数:argc、argv分别在寄存器%rdi、%rsi
函数调用:被系统启动运行
返回情况:在%eax寄存器中设置返回值,return 0
2.printf函数
传递参数:puts时,将字符串首地址传入%rdi
Printf调用时,argv[1]、argv[2]传入对应的%rsi、%rdi
函数调用:for循环后被调用
3.exit函数
传递参数:传入立即数1给%edi
函数调用:if条件判断后,若满足相应条件
4.sleep函数
传入参数:传入sleepsecs参数
函数调用:for循环下被调用
5.getchar函数
函数调用:main函数中

   图3-11 main函数及其他函数调用

3.4 本章小结
明确了编译的概念和功能,进行语法分析和检查是否符合语法规则,并且将其转换为低级机器语言指令,为不同的编译器提供了通用的输出语言。
对编译的结果进行分析,更加深入和明确地理解了c语言的数据和操作。
(第3章2分)

第4章 汇编
4.1 汇编的概念与作用

汇编器将hello.s翻译成机器语言指令,把这些指令都打包成一种叫做可重定位目标程序的格式,并且将结果保存在hello.o文件中。Hello.o文件是一个二进制文件,它包含的是程序的指令编码。

注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
4.2 在Ubuntu下汇编的命令
Gcc -c hello.s -o hello.o

						图4-1 汇编命令

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

  1. ELF HEADER(ELF 头)
    ELF头以一个十六字节的序列开始,这个序列描述了生成该文件的系统的字大小和字节序列。剩下的部分帮助链接器语法分析和解释目标文件的信息。其包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的数量和大小。

    						图4-2 ELF头
    
  2. 节头部表
    节头部表描述了不同节的位置和大小,其中目标文件每一个节都有一个大小固定的条目。

    					图4-3 节头部表
    
  3. 重定位条目
    当汇编器生成一个目标模块是,它并不知道数据和代码最终将放在内存中的什么位置,它也不知道这个模块引用的任何外部定义的函数或者全局变量的位置。所以,无论何时汇编器遇到对最终位置未知的目标引用,它就会生成一个重定位条目,告诉链接器在将目标文件合并成可执行目标文件时如何修改这个引用。代码的重定位条目放在.rel.text中,已初始化数据的重定位条目放在.rel.data中。

ELF重定位条目:
typedef struct{
long offset;
long type:32,
symbol:32;
long attend;
}Elf64_Rela;
两种主要的重定位类型:
1.R_X86_64_PC32:重定位一个试用32位PC相对地址的引用
2.R_X86_64_32:重定位一个试用32位绝对地址的引用

            图4-4重定位条目
  1. symtab是一个符号表,他存放在在程序中定义和引用的函数和全局变量

    				图4-5符号表
    

4.4 Hello.o的结果解析

objdump -d -r hello.o
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。

机器语言和汇编语言的差别:

  1. 操作数的不同:机器语言的操作数位十进制,而汇编语言的操作数位16进制

  2. 分支转移:机器语言在进行分支转移的时候是跳转至类似于.L1 .L2的段,而反汇编代码中跳转至相对偏移的地址

  3. 函数调用:在机器语言中call指令之后直接是函数名称,反汇编语言call之后是函数的相对偏移地址。并且只有链接后才能确定函数的地址所以,需要重定位来确定。

  4. 对于全局变量的访问:在hello.s机器语言中,对于全局变量是用名称(sleepsecs或$.LC0)访问,而在反汇编代码中全部置零,地址是在运行时确定的,需要重定位。

    				图4-6-1 反汇编语言
    
    					图4-6-2 反汇编语言
    
    	图4-7 机器语言
    

4.5 本章小结
通过汇编生成可重定位目标文件,为后面的链接做好了准备。通过对反汇编指令和hello.s中指令的对比,更加深入的理解汇编的过程。
(第4章1分)

第5章 链接
5.1 链接的概念与作用
概念:链接是将各种代码和数据片段收集并组合为一个单一的文件的过程,这个文件可以被加载(复制)到内存并执行
作用:将函数调用的其他目标文件中的函数、变量通过链接器将文件合并到hell.o程序中,得到可执行文件hello,可以被加载到内存中并且有系统执行。并且链接使得分离编译成为可能。
注意:这儿的链接是指从 hello.o 到hello生成过程。
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
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件

			图5-1 链接命令

5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
可执行目标文件hello的格式类似于可重定位目标文件hello.o的格式,ELF头描述文件的总体格式。它还包括程序的入口点,也就是当程序运行时要执行的第一条指令的地址。.text、.rodata和.data节与可重定位目标文件中的节是类似的,除了这些节已经被重定位到它们最终的运行时的内存地址外。.init节定义了一个小函数_init,程序初始化代码会调用它。因为可执行文件时完全连接的,所以无.rel节。

						图5-2-1 节头部表(1)

						图5-2-2 节头部表(2)

5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。

					图5-3虚拟地址各段信息

打开edb,加载hello,打开symbolviewer查看虚拟各段的信息,与5.3中的信息对比发现和readelf查看结果相同,此外也包含了一些按照地址顺序的一些其他信息。
5.5 链接的重定位过程分析
(以下格式自行编排,编辑时删除)
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
Hello和hello.o的不同:

  1. Section(节)的不同:hello中增加了节.init 以及.plt、.fini,并且.text节也增加了start等函数
  2. 链接增加的函数:hello中链接加入了hello.c中的函数:puts、printf、getchar、exit
  3. 函数调用时:hello中没有重定位条目,跳转和函数调用的地址全部变成虚拟地址。这些地址是hello.o依靠重定位条目链接后才能确定的。
  4. 地址访问:访问变量(例如全局变量sleepsecs)时,在连接后变成了虚拟地址,而hello.o仍然依靠重定位,并且操作数是全部置零的。

hello链接过程:链接器在这个过程中链接器通过符号解析和重定位将各个可重定位目标文件和命令行参数作为输入,按照一定的规则将可重定位目标文件的各个函数和数据段进行收集和合并,生成一个完全链接的、可以加载和运行的可执行目标文件。

Hello重定位过程:
1.重定位节和符号定义。在这一步中,链接器将所有相同类型的节合并为同一类型的新的聚合节。来自输入模块所有可重定位目标文件中的.data节被全部合并成一个节,这个节成为输出的可执行目标文件hello中的.data节。然后,链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。当这一步完成时,hello中每条指令和全局变量都有唯一的运行时内存地址了。
2.重定位节中的符号引用。链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确运行时的地址。这个过程中链接器依赖于hello.o中的重定位条目。
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
_dl_start
_dl_init
_start
_libc_start_main
_init
_main
_printf
_exit
_sleep
_getchar
_dl_runtime_resolve_xsave
_dl_fixup
_dl_lookup_symbol_x
exit
5.7 Hello的动态链接分析

分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
动态链接通过GOT和PLT这两个数据结构来实现,GOT是数据段的一部分,通过readelf查看hello的内容,得到.got和.got.plt的地址。

						图5-4 得到.got和.got.plt地址

接下来通过edb查看中找到该位置
在执行_dl_init之前如图:

						图5-5 _dl_init之前

					图5-6 _dl_init之后

5.8 本章小结
本章介绍了链接的过程和重定位的过程,以hello为例,解析了链接前后的不同hello.o和hello文件的不同之处,利用edb和objdump工具对hello可执行文件进行了解析,以及对动态链接的分析。
(第5章1分)

第6章 hello进程管理
6.1 进程的概念与作用
概念:进程就是执行程序中的实例。进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。
作用:每次用户向shell输入一个可执行目标文件的名字,运行程序时,shell就会创建一个新的进程,然后在这个进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,并且在这个新进程的上下文运行它们自己的代码或其他目标文件。进程提供给应用程序的关键抽象:一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器。一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用内存。
6.2 简述壳Shell-bash的作用与处理流程
作用:shell是一个交互型应用级程序,利用用户运行其他程序(是命令行解释器,以用户态方式运行的终端程序),其基本功能是解释并运行用户的指令,重复如下处理过程:
(1) 终端进程读取用户由键盘输入的命令行。
(2) 分析命令行字符串,获取命令行参数,并构造给execve的argv向量
(3) 检查第一个命令行参数是否是一个内置的shell命令
(4) 如果不是内部命令,调用fork()创建新进程/子进程
(5) 在子进程中,用步骤2获取的参数,调用execve()执行指定程序
(6) 如果用户没要求后台运行(命令末尾没有&号),否则shell使用waitpid(或wait)等待作业终止后返回。
(7) 如果用户要求后台运行(如果命令末尾有&号),则shell返回。
6.3 Hello的fork进程创建过程
当输入命令执行可执行目标文件hello时,父进程会通过shell函数创建一个新的运行的子进程hello。Hello进程几乎但不完全和父进程相同,hello进程得到与父进程用户级虚拟空间相同但是独立的一个副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程的内容,但是它们有着不同的pid,但在父进程中,fork返回子进程的pid,在子进程中,fork返回0.
6.4 Hello的execve过程
Execve删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
映射私有区域。为hello程序的代码、数据、bss和栈区域创建新的数据结构,并且所有这些新的区域都是私有的、写时复制的。代码和数据段被映射为hello程序中.text和.data区。.bss区是请求二进制零的,映射到匿名文件。其大小包含在hello中,栈和堆都是请求二进制零的,初始长度为零。
映射共享区域。将标准库libc.so,这些对象是动态链接到这个程序的,然后在映射到用户虚拟地址空间中的共享区域内。
设置程序计数器。Execve做的最后一件事情就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。
6.5 Hello的进程执行
进程上下文信息:上下文就是内核重新启动一个被抢占的进程所需的状态。它由一些对象的值组成。这些对象包括通用的目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构。
进程时间片:一个进程执行它的控制流的一部分时间的每一时间段叫做时间片。
Hello进程执行:首先当加载器结束对hello的加载后,操作系统执行上下文切换,切换到hello的进程中,这是我们在用户态,接下来hello调用sleep函数,显式地让hello进程休眠,进入内核态。进行上下文切换,调用其他进程运行。定时器中断后,内核就能判断当前hello休眠运行了足够长的时间,通过上下文切换,切回用户态。之后程序执行for循环会出现多次上下文切换。最后for循环结束后,调用getchar,需要读取键盘输入的信息,所以会进入内核态,再次上下文切换,回到hello进程,执行hello进程最后一条语句后,hello进程结束。
6.6 hello的异常与信号处理

hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
Hello的异常:
(1) 中断:中断是异步发生的,是来自处理器外部的I/O设备的信号的结果,中断处理程序需运行之后,返回到下一条指令。
(2) 陷阱和系统调用:陷阱是有意的异常,是执行一条指令的结果,就像中断处理程序一样,陷阱处理程序将控制返回到下一条指令,在用户程序和内核之间提供一个像过程一样的接口,即系统调用。如读一个文件、创建一个进程、加载一个新的程序等。
(3) 故障:当发生错误情况时,处理器将控制转移给故障处理程序,如果错误情况可以修正,则将控制返回到引起故障指令,重新执行,否则处理程序返回到内核abort,终止故障的应用程序。
(4) 终止:终止是不可恢复的知名错误造成的结果,会终止应用程序。
Hello的信号:
(1) 中断:信号SIGTSTP,默认行为是停止直到下一个SIGCONT
(2) 终止:信号SIGINT,默认行为是终止。
程序运行时执行命令:

  1. 程序运行的时候什么也不按,只按回车:

    				图6-1 运行
    
  2. 程序执行时乱按:

    					图6-2 随意输入
    
  3. 程序运行时ctrl+c

    					图6-3 ctrl+c
    
  4. 程序运行时ctrl+z

    					图6-4 ctrl+z
    

(1) ps

						图6-5 ps

(2) jobs

						图6-6 jobs

(3) pstree

					图6-7 pstree

(4) fg

(5) kill

6.7本章小结
本章了解了hello进程的整个执行过程,对于fork、execve加载hello和运行shell进行了深入的分析。通过键盘输入,对信号和信号的处理有了更多的认识,从而对异常的掌握加深了。
(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
逻辑地址:包含在机器语言中用来指定一个操作数或一条指令的地址。每一个逻辑地址都由一个段(segment)和偏移量(offset)组成,偏移量指明了从段开始的地方到实际地址之间的距离。就是hello.o里相对偏移地址。
线性地址:逻辑地址和物理地址转换的中间层。程序代码会产生逻辑地址,加上段的基址生成线性地址。是hello中的虚拟内存地址
虚拟地址:在带有虚拟内存系统中,CPU有一个N=2^n个地址空间中生成虚拟地址。虚拟地址其实就是线性地址。
物理地址:用于内存芯片级的单元寻址,地址翻译会将hello的一个虚拟地址转化为物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
实模式:逻辑地址CS:EA=EA+16*CS
保护模式:逻辑地址由段标识和段偏移量组成。以段标识为下标,去索引段描述符表,若T1=0,索引全局段描述符表(GDT),若T1=1,索引局部段描述符表(LDT)。将段描述符表中的段地址(base字段)加上段偏移量,即为线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理

					图7-1 地址翻译

hello的线性地址到物理地址的变换,也就是从虚拟地址寻址物理地址,在虚拟地址和物理地址之间存在一种映射,MMU通过页表实现这种映射。
虚拟地址由虚拟页号(VPN)和虚拟页偏移量(VPO)组成,页表中由有效位和物理页号组成,VPN作为到页表的索引,去页表中寻找相应的PTE,其中PTE有三种情况,分别为已分配,未缓存,未分配。已分配表示已经将虚拟地址对应到物理地址,有效位为1,物理页号不为空。未缓存表示还未将虚拟内容缓存到物理页表中,有效位为0,物理页号不为空。未分配表示未建立映射关系,有效位为0,物理页号为空。
如果有效位为0,表示缺页,进行缺页处理,从磁盘读取物理页到内存,若有效位为1,则可以查询到相对应的PPN,物理页偏移量和VPO相同,PPN和PPO组成物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
Core i7 MMU如何使用四级页表来将虚拟地址翻译成物理地址。36位的虚拟地址被分割成4个9位的片。CR3寄存器包含L1页表的物理地址。VPN1有一个到L1 PTE的偏移量,找到这个PTE以后又会包含到L2页表的基础地址;VPN2包含一个到L2PTE的偏移量,找到这个PTE以后又会包含到L3页表的基础地址;VPN3包含一个到L3PTE的偏移量,找到这个PTE以后又会包含到L4页表的基础地址;VPN4包含一个到L4PTE的偏移量,找到这个PTE以后就是相应的PPN(物理页号)。

						图7-2 VP和PP

7.5 三级Cache支持下的物理内存访问
CPU访问物理地址是访问三级cache L1、L2、L3。MMU将物理地址发送给L1缓存,根据物理地址得出CT(缓存标记)、CI(缓存组索引)、CO(缓存偏移)。首先很具物理地址中CI缓存组索引找到L1缓存中对应的组,若缓存标记为1,根据缓存偏移直接从缓存中读取数据并返回。如果缓存标记为0,即缓存不命中,需要从L2、L3中去读取,如果在三级缓存中都不存在,需要到主存中读取。
当有空闲块时直接放置,否则必须驱逐一个现存的块,一般采用最近最少被使用策略LRU进行替换。
7.6 hello进程fork时的内存映射
当fork 函数被shell调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID 。为了给hello进程创建虚拟内存,它创建了hello进程的mm_struct 、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
7.7 hello进程execve时的内存映射
1.删除已存在的用户区域。
2.映射私有区域。为Hello的代码、数据、bss和栈区域创建新的区域结构,所有这些区域都是私有的、写时复制的。
3.映射共享区域。比如Hello程序与标准C库libc.so链接,这些对象都是动态链接到Hello的,然后再用户虚拟地址空间中的共享区域内。
4.设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
在虚拟内存的习惯说法中,DRAM缓存不命中称为缺页(page fault) 。假如CPU引用了VP3中的一个字,VP3并未缓存在DRAM中。地址翻译硬件从内存中读取PTE3,从有效位推断出VP3未被缓存,并且触发一个缺页异常。缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,在此例中就是存放在PP3中的VP4。如果VP4已经被修改了,那么内核就会将它复制回磁盘。无论哪种情况,内核都会修改VP4的页表条目,反映出VP4不再缓存在主存中这一事实。缺页处理程序从磁盘上用VP3的副本取代VP4,在缺页处理程序重新启动导致缺页的指令之后,该指令将从内存中正常地读取字,而不会再产生异常。
7.9动态存储分配管理
动态分配器维护这一个进程的虚拟内存区域,称为堆。系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长。对于每个进程,内核维护着一个变量brk,它指向堆的顶部。
分配器将堆视一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显示的保留为应用程序使用。空闲块可以用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存自身隐式执行的。
1.显式分配器:要求应用显式地释放任何已分配的块。例如C程序通过调用malloc函数来分配一个块,通过调用free函数来释放一个块。其中malloc采用的总体策略是:先系统调用sbrk一次,会得到一段较大的并且是连续的空间。进程把系统内核分配给自己的这段空间留着慢慢用。之后调用malloc时就从这段空间中分配,free回收时就再还回来(而不是还给系统内核)。只有当这段空间全部被分配掉时还不够用时,才再次系统调用sbrk。当然,这一次调用sbrk后内核分配给进程的空间和刚才的那块空间一般不会是相邻的。

2.隐式分配器:也叫做垃圾收集器,例如,诸如Lisp、ML、以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。
策略:
1.带边界标签的隐式空闲链表分配器:
一个块是由一个字的头部、有效载荷,可能的一些额外的填充,以及一个字的脚部组成。头部编码了这个块的大小(包括头部和所有的填充),以及这个块是已分配的还是空闲的,强加一个双字对齐的要求,那么块的大小就总是8的倍数,且块的大小的最低三位总是0,因此内存大小只需要29个高位,释放剩余的3位编码来编码其他信息。最低位表示这个块是已分配还是空闲的。头部后面的有效载荷是malloc请求的,填充则是因为可能是分配策略的一部分用来对付外部碎片或者满足对齐要求。
边界标记技术是脚部的由来,脚部是头部的一个副本,如果每个块包含一个脚部那么分配器就可以通过检查他的脚部来判断前一个块的起始位置和状态。
放置块可以采用首次适配、下一次适配、最佳适配等策略。

2.合并空闲块:
1.前面的块和后面的块都是已分配的
2.前面的块是已分配的,后面的块是空闲的
3.前面的块是空闲的,而后面的块是已分配的
4.前面的和后面的块都是空闲的
之后进行合并。
情况1:将块简单的从已分配变成空闲即可
情况2:将当前块和后面的块进行合并,用当前块和后面块的大小和来更新当前块的头部和后面块的脚部
情况3:当前块和前面的块进行合并,用当前块和前面块的大小和来更新当前块的脚部和前面块的头部。
情况4:当前块和前面的块以及后面的块进行合并,用当前块和前面块以及后面的块的大小和来更新后面块的脚部和前面块的头部。
最后操作更新后的bp返回。
3.显式空闲链表:
根据定义,程序不需要一个空闲块的主体,所以实现空闲链表数据结构的指针可以存放在这些空闲块的主体里面。
显式空闲链表结构将堆组织成一个双向空闲链表,在每个空闲块的主体中,都包含一个pred(前驱)和succ(后继)指针。
使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。不过,释放一个块的时间可以是线性的,也可能是个常数,这取决于空闲链表中块的排序策略。
一种方法是用后进先出(LIFO)的顺序维护链表,将新释放的块放置在链表的开始处。另一种方法是按照地址顺序来维护链表,其中链表中每个块的地址都小于它后继的地址。
7.10本章小结
本章主要介绍了程序的虚拟内存的信息。通脱对虚拟内存的介绍,详细解释了TLB和四节页表下VA和PA的转换,以及利用PA对cache和主存的访问。并且详细解释了动态内存分配器的管理和策略。
(第7章 2分)

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
(以下格式自行编排,编辑时删除)
设备的模型化:文件
设备管理:unix io接口
一个linux文件就是一个m个字节的序列:
B0 , B1 , … , Bk , … , Bm-1
所有的I/ O 设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux 内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。
8.2 简述Unix IO接口及其函数
Unix IO接口:
打开文件:一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个非负整数的文件描述符,她在后序的操作中标识这个文件,通过对此文件描述符对文件进行所有操作。
Linux shell创建的每个进程开始时都有三个打开的文件:标准输入(文件描述符0)、标准输出(描述符为1),标准出错(描述符为2)。头文件定义了常量STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO,他们可用来代替显式的描述符值。
改变当前的文件位置:对于每个打开的文件,内核保持这一个文件位置k,初始为0.这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek操作,显式地设置文件的当前位置为k。
读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n;类似地、写操作:从内存复制n个字节到文件,当前文件位置为k,然后更新k
关闭文件。当应用完成对文件的访问后,通知内核关闭这个文件。作为响应内核会释放文件打开时创建的数据结构,将描述符恢复到描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开文件并释放它们。

函数:
Open函数:打开一个已存在的文件或创建一个新文件。
函数原型:Int open(const char *filename,int flags,int perms),open函数将filename转换为一个文件描述符,并且返回描述符数字,若失败则返回-1。
Close函数:用于关闭一个被打开的文件
函数原型:Int close(int fd) fd为文件描述符。
Read函数:从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。
函数原型:sszie_t read(int fd,void *buf, size_t n)返回值为-1表示一个错误,而返回值为0表示EOF。否则,返回值表示的是实际传送的字节数量。
Write函数:从内存位置buf复制至多n个字节到描述符fd的当前文件位置。
函数原型:ssize_t write(int fd,const void *buf,size_t n)
Lseek函数:通过调用此函数,应用程序能够显式地修改当前文件位置。
8.3 printf的实现分析
https://www.cnblogs.com/pianist/p/3315801.html

					图8-1 printf函数主体

Printf函数先让va_list类型的变量指向输入第一个参数的一个地址,之后调用vsprintf函数
从图8-1可以看出,printf函数调用vsprintf函数,关于vsprintf函数如下:

						图8-2 vsprintf函数主体

该函数返回的是要打印出来的字符串的长度,vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
之后调用write系统函数进行操作。
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

			图8-3 getchar函数主体

getchar可用宏实现:#define getchar() getc(stdin)。getchar有一个int型的返回值。当程序调用getchar时.程序就等着用户按键。用户输入的字符被存放在键盘缓冲区中。直到用户按回车为止(回车字符也放在缓冲区中)。当用户键入回车之后,getchar才开始从stdin流中每次读入一个字符。getchar函数的返回值是用户输入的字符的ASCII码,若文件结尾(End-Of-File)则返回-1(EOF),且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完后,才等待用户按键。
8.5本章小结
本章简述了Linux系统的I/O机制,简述了系统级I/O的函数,了解了有关打开、关闭与读写文件的操作,并分析了printf和getchar的实现过程。
(第8章1分)
结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
Hello经历的过程:
关于Hello.c文件,首先GCC编译器驱动程序读取源程序文件hello.c,并把它翻译成一个可执行的目标文件hello,分为四个阶段:

  1. 预处理:预处理根据#字符开头的命令修改为原始c程序。得到.i文件。
  2. 编译阶段:编译器将文本文件hello.i翻译成hello.s
  3. 汇编阶段:汇编器产生可重定位目标文件,hello.o
  4. 链接阶段:通过链接器生成hello的二进制目标程序。
    之后shell经过fork和execve函数将可执行目标文件hello加载至其中。
    Hello运行时会与多个进程并行,而且会在中断和异常时发生上下文切换,转
    换到内核模式,进行内核调度。
    在运行的过程中接受键盘的信号,shell信号处理程序对其作出不同的反应。
    Hello访存时,请求一个虚拟地址,通过MMU,TLB,页表得到虚拟地址对应的
    物理地址。
    Hello中printf函数和getchar需要调用Unix I/O接口函数实现。
    最后hello进程终止,被shell回收。
    感悟:
    一个简单的hello程序真正被系统加载和运行,是软硬件共同支持的结果,需要大量的流程。其中包括预处理器、编译器、汇编器、链接器,加载和运行时必不可少的shell函数、信号处理等机制。运行时,进程的管理,存储的管理都是必不可少。一个简单的程序会依靠各种部分协作,缺一不可。
    其中还有很多细节等待我们去学习。

(结论0分,缺失 -1分,根据内容酌情加分)

附件
列出所有的中间产物的文件名,并予以说明起作用。
中间文件 作用
Hello.i 预处理产生的文件
Hello.s 将预处理文件变成汇编文件
Hello.o 汇编器将汇编文件变为可重定位文件
Hello 可执行文件
Helloelf.txt readelf查看hello
(附件0分,缺失 -1分)

参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 兰德尔 E.布莱恩特 大卫 R.奥哈拉伦. 深入理解计算机系统[M]. 机械工业出版社.
[2] CSDN 物理地址和逻辑地址
https://blog.csdn.net/tuxedolinux/article/details/80317419
[3] 博客园 printf函数实现的深入剖析
https://www.cnblogs.com/pianist/p/3315801.html
[4] 百度百科 getchar函数
https://baike.baidu.com/item/getchar/919709?fr=aladdin
.
(参考文献0分,缺失 -1分)

你可能感兴趣的:(HELLO 程序人生)