2018-HIT-CSAPP-hello的一生

第1章 概述

1.1 Hello简介

根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。

P2P:自白中P2P指的是‘Program
to Process’,即从程序到进程。hello.c通过某种类型的编辑器编辑完成,经过cpp预处理、ccl编译、as汇编、ld链接四步骤生成可执行目标程序hello,./hello执行program程序hello时通过fork函数产生子进程process,至此,Program成功转化为Process。

020:即From
Zero to Zero。Shell调用execve函数加载程序,先删除已存在的用户区域,再为hello映射新的私有区域及共享区域,然后跳转到 main函数起始地址执行,CPU为运行的hello分配时间片执行逻辑控制流。程序运行结束之后,该进程会保持在一种已终止的状态中,直到该进程被其父进程shell回收,内核删除相关数据结构,shell再次变成hello执行之前的状态,即又变成Zero。

1.2 环境与工具

硬件环境:

软件环境:Ubuntu18.04.1 LTS

开发与调试工具:gcc,gdb,read-elf

1.3 中间结果

中间结果文件

作用

Hello

可执行目标文件

Hello.c

源代码

Hello.i

预处理后文件

Hello.s

编译后文件

Hello.o

汇编后可重定位目标文件

Hello.elf

Elf文件

Hello.txt

Hello的反汇编代码

Helloo.txt

Hello.o的反汇编代码

1.4 本章小结

解释了p2p和020的概念,列举了硬件软件环境和调试工具,分析了中间结果文件的作用。

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

概念:预处理(preprocess)是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。

作用:预处理阶段,预处理器会解析所有#开头的指令,如宏定义#difine、条件编译#ifdef、源文件引用(包含)#include等(也存在有些现阶段用得比较少的指令,如行控制#line、错误指令#error等)。预处理的作用是把源代码分割或处理成为特定的单位,预处理记号用来支持语言特性(如C/C++的宏调用)。

2.2在Ubuntu下预处理的命令

预处理之后生成hello.i文件

2.3
Hello的预处理结果解析

可以看到,cpp预处理之后,源文件增加到三千多行,main函数位于3102行。Cpp会对所有的#进行解析,例如#include stdlib.h部分解析为3091行及以下的内容:

usr/include/stdlib.h是文件stdlib.h的绝对路径,cpp对所有的include指令进行解析,将目标文件中的代码段引用在hello.i中,并对引用文件中的#进行更深层次的解析,一些其他的指令会被不同地解析(如#ifdef,cpp会根据条件值绝对是否执行其中的逻辑)。至此,cpp对hello.c的解析完成,新生成的hello.i不存在任何如#define、#include的#语句。(以上内容表述并不一定完全准确,仅个人理解!)

2.4 本章小结

本章从起始源文件hello.c入手,通过cpp将其预处理成为hello.i文件,简单讨论了预处理的概念、作用以及对预处理实际的功能的分析与思考。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

概念:利用编译程序从源语言编写的源程序产生目标程序的过程。

作用:将代码翻译为汇编语言,以便汇编器翻译为机器码。

3.2 在Ubuntu下编译的命令

3.3
Hello的编译结果解析

3.3.1 汇编指令

汇编指令

含义

.file

声明源文件

.text

声明代码段

.globl

声明全局变量

.type

指定目标为对象object或函数function

.size

声明对象或函数大小

.long

定义long类型变量

.section
.rodata

声明rodata段

.align

对齐需要

.string

定义字符串

3.3.2 数据

hello.c中共有三种类型的数据:分别是整数、字符串和数组。

3.3.2.1 整数

   (1)

sleepsecs:sleepsecs是在main外部定义的long类型的全局变量,赋值为2,保存在 .data中,大小为4字节(在源程序中被定义为int类型,编译器将其修改为long类型,可能是编译器偏好)

   (2)argc:argc是传入main函数的第一个参数,含义是命令行中输入的命令拆分后的个数(如./main,argc = 1;./main

1170300808 葛润禅,argc = 3)保存在寄存器rsi(esi)中。

   (3)i:main函数中定义的局部整型变量i,初始化为0,保存在栈中-4(%rbp)的位置。

3.3.2.2 字符串

字符串通过.string的方式定义,两个字符串的声明如下:

.string “Hello %s %s\n”

.string"Usage: Hello 1170300808
\350\221\233\346\266\246\347\246\205\357\274\201"

其中%s是格式控制符,\350\221\233\346\266\246\347\246\205\357\274\201是汉字“葛润禅”的utf-8的编码。

3.3.2.3 数组

   main函数的第二个参数*argv[]是一个字符型指针数组,保存在寄存器rdi(edi)中,其中的内容是输入的命令根据空格拆分后的片段(如./main 1170300808 葛润禅,argv[1] = “1170300808”,

argv[2] = “葛润禅”),argv[0] 保存在栈-32(%rbp)的位置;argv[1]
保存在栈-28(%rbp)的位置;argv[2]
保存在栈-24(%rbp)的位置。

3.3.2.4 常量

   常量以立即数的形式展示在.s文件中,如、等

3.3.3 操作:

3.3.3.1 赋值

   .s文件中对赋值的实现是应用mov指令,如

,其中-4(%ebp)是i在栈中的位置,这条指令相当于源码中的i=0。

3.3.3.2 类型转换

编译器会对有的变量做适当的类型转换,如源代码中的int
sleepsecs=2.5,因为2.5是浮点型而sleepsecs被定义为整型,因此编译器会将浮点数向零取整至整型2,再赋值给sleepsecs变量。另外,编译器会将浮点型默认为double,正如将int转化为long一样,这只是编译器的偏好(也许吧,自我理解的)。

3.3.3.3算术操作

hello.s中出现的算术操作有两个:subq和addq。功能如下:

subq a,b即b = b
– a addq a,b 即 b =
b + a

源代码中的i++就是通过add指令实现的。

3.3.3.4 关系操作

   hello.s中出现的关系操作有两个: != 和 < 。通过汇编语言实现如下:

a != b即cmp a,b

je .Lxxx 或 jne .Lxxx

比较a与b,如果a、b相等,则设置条件码为!!(b-a),再根据条件跳转指令je或jne跳转。<操作与!=类似,不多加赘述。

下面是hello.s中关系操作的一个实例:

源代码中关系操作如下:

汇编实现如下:

可以看到,关系操作通过cmp指令实现,-20(%rbp)是argc在栈中对应的位置,如果不相等则不会跳转到.L2的位置(je可以理解为jump (if) equal),那么接下来会将rdi更改为.LC0的地址,并通过调用puts函数实现输出字符串.LC0,最终exit(1)。

3.3.3.5 控制转移

几乎所有的控制转移语句都是通过多数的j**条件跳转和少数的jmp非条件跳转实现的,如if-else、for、while、do-while等等。hello源代码中有两个条件转移,一个是if语句,另一个是for语句,下面对两个条件转移逐个分析:

(1)
if(argc!=3)

{

   printf("Usage: Hello 1170300808 葛润禅!\n");

exit(1);

}

编译后实现如下:

其中-20(%rbp)是argc在栈中的位置,cmpl比较立即数3与argc,若两者不相等,则不设置标记位,那么接下来会将rdi更改为.LC0的地址,并通过调用puts函数实现输出字符串.LC0,最终exit(1)。

(2) for(i=0;i<10;i++)

{

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

sleep(sleepsecs);

}

编译后实现如下:

根据刚刚的if条件跳转,当argc != 3不成立,即argc的值为3时,跳转到.L2。其中-4(%rbp)是局部变量i在栈中的位置,初始化赋值为0,接着跳转到.L3,.L3中实现的功能即为for循环中判断的条件,只要当i<=9时就会跳转到.L4,L4的过程即为for循环内操作的实现。

3.3.3.6 函数操作

   首先简述编译器在遇到函数时的处理流程。当发生函数调用时,编译器会将其翻译成call语句+函数名(地址)的指令,作用是将当前指令的下一条指令的地址压入栈中,然后将控制(程序计数器PC)转移到函数的起始位置(即函数首地址),最终函数执行结束后ret语句会从栈中弹出一个地址,将控制转移回该地址的位置。在处理函数参数时,编译器会将前六个参数逐个保存在%rdi、%rsi、%rdx、%rcx、%r8、%r9六个寄存器中,如果参数超过六个,其余的会被保存在栈中(32位程序的情况较简单,所有的参数都会被压入栈中)。另外,函数在返回时,须将返回值保存在累加器%rax中,已达到返回返回值的目的。

函数对栈的使用只是暂时借用,调用结束后栈会恢复原来的情况。一般函数调用时都会将原rbp寄存器的值压入栈中,而将rsp的值赋给rbp,这样两个寄存器一个用作栈指针,一个用作栈帧指针。

接下来对hello.c中全部函数作以分析,共五个函数,分别叙述其参数、控制传递的流程以及作用。

(1)
main函数:

main函数是程序的主函数,共有两个参数,分别是argc和argv,分别用寄存器rdi和rsi保存(这是在外部调用main时设置好的,由__libc_start_main调用)。控制从外部调用时传入main开始,不断更新PC,逐条指令执行。先保存旧的栈帧指针rbp的值到栈中,通过将栈指针向栈顶移动的方法为函数分配空间,在该空间内为局部变量和参数分配具体的空间,再不断执行具体指令,最终ret指令返回,结束程序。

(2)
printf函数和puts函数:

两个函数实现的功能相同,各被调用一次。

第一次将上图所示的字符串首地址传给rdi并调用puts,输出该字符串。

第二次将将上图所示的字符串的首地址传给rdi,将rsi和rdx的值分别设成argv[1]和argv[2],调用printf函数,输出该字符串。

(3)
exit函数

参数为退出状态,0为正常退出,仅一次调用,将参数1传给rdi后调用exit结束程序。

(4)
sleep函数:

参数为休眠时间,保存在edi中,接着调用sleep函数实现程序暂停一段时间

(5)
getchar函数:

无参数,作用是等待缓冲区用户输入,已达到暂停的效果。

3.4 本章小结

本章从汇编指令、数据、操作三个方面逐步深层次地分析了编译器如何编译hello.i文件形成汇编语言构成的hello.s文件,以便之后翻译成机器码得以运行。

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

(第3章2分)

第4章 汇编

4.1 汇编的概念与作用

概念:将汇编语言翻译成二进制的机器码的过程成为汇编。

作用:将.s文件中的汇编语言翻译成二进制的机器码后打包成一个可重定位目标文件(.o文件),用以机器执行。

4.2 在Ubuntu下汇编的命令

4.3 可重定位目标elf格式

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

(1)ELF Header:如下图所示。Magic序列中45
4c 46是ELF的ASCII码值,接着显示了系统字大小和大小端序,再之后的内容包括帮助链接器语法分析和解释目标文件的信息,有 ELF Header的大小、目标文件的类型、机器类型、节头部表中条目的大小和数量,以及section header table的文件索引等信息。

(2):Section Header:结头部表。包含各个节的大小、类型、地址、偏移量等信息。

(3)重定位节:rela.text。显示了.text节中需要重定位的段的信息,当链接器将多个.o文件链接时,需要修改这些段的信息(即重定位的字面意思,重新定位其在新文件中的位置),如下图所示,需要修改的段分别是.rodata(即两个需要printf的字符串)、puts函数、exit函数、printf函数、sleepsecs函数、sleep函数和getchar函数。

以下是.rela.text存储的信息:

Offset

重定位时代码在.text和数据在.data中的新位置,8字节

Info

由symbol+type组成。均为4字节,symbol代表重定位的目标段在.symtab中的偏移量,type代表重定位的类型

Type

重定位的类型

Name

重定向到的目标的名称

Addend

计算重定位位置的辅助信息,共占8个字节

4.4
Hello.o的结果解析

根据命令objdump -d -r hello.o,得到如上汇编代码。对比.s文件,最大的区别是条件跳转指令,从.s文件中的段的名称(如.L2)变为在main函数基础上的偏移,如jle 34
<main+0x34>,这个确定的地址即是重定位后的新地址。

第二个不同是对函数的调用,:在.s文件中,函数调用时展示的是函数名称。而在反汇编.o文件得到的代码中,函数名字被替换成了函数的偏移地址。对于hello.c中调用的共享库中的函数,需要通过动态链接器来确定函数的运行时真实地址,在汇编器翻译为机器码时,对于这些不确定地址共享库中的函数调用,将其call指令后的相对地址设置为全0(目标地址就是下一条指令),然后在.rela.text 节中为其添加重定位条目,等待静态链接的进一步确定。

第三个不同是hello.c中两个字符串的访问。.s中访问字符串是通过修改rip到.LC0 .LC1的段进行访问,而反汇编程序中对其访问和对函数调用类似,由于保存字符串的rodata段的地址也是在运行时确定,因此同样需要重定位,那么也是先将相对地址设置为0,添加重定位条目,等待静态链接的进一步确定。

4.5 本章小结

本章从汇编出发,讨论了.s文件汇编成.o文件的过程,逐段分析了elf中各段的含义,通过反汇编objdump查看hello.o对应的反汇编代码并与hello.s中的汇编码比较,分析汇编语言如何映射为机器语言。

(第4章1分)

第5章 链接

5.1 链接的概念与作用

概念:将各种代码和数据片段收集并组合成一个单一文件的过程

作用:链接使分离编译成为可能,多个源文件互相引用以实现最终功能。链接的时机也是不确定的,编译、汇编甚至加载和执行时都可以执行链接。

5.2 在Ubuntu下链接的命令

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

   在elf格式文件中,结头部表展示了hello中所有的段的信息,包括大小、类型、地址(载入到虚拟内存中的起始地址)、偏移量等等。







分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。

5.4
hello的虚拟地址空间

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

Edb打开链接后的hello可执行文件。

data dump窗口可查看该进程在虚拟地址空间内各个地址的值。下图是edb中利用data dump查看各段的值。

各段的起始地址(VirAddr)及size(MemSiz):

.PHDR段应该是内核部分的段,信息是乱码。

.interp从地址0x400200开始,大小为0x1c,可以看到对应的信息是/lib64/ld-linux-x86-64.so.2这样一个共享库。

其他段道理相同,由于信息基本都是乱码,不一一列举。

对于ELF文件中的程序头表Program Headers,执行时会被调用,它告诉链接器运行时加载的内容并提供动态链接的信息。每一个表项提供了各段在虚拟地址空间和物理地址空间的大小、位置、标志、访问权限和对齐方面的信息。程序头表包含的八个段:

PHDR 保存程序头表

INTERP指定在程序已经从可执行文件映射到内存之后,必须调用的解释 器(例如动态链接器)。

LOAD 表示一个需要从二进制文件映射到虚拟地址空间的段。其中保存了 常量数据(如字符串)、程序的目标代码等。

DYNAMIC 保存了由动态链接器使用的信息。

NOTE 保存辅助信息。

GNU_STACK:权限标志,标志栈是否是可执行的。

GNU_RELRO:指定在重定位结束之后那些内存区域是需要设置只读。

通过Data Dump查看虚拟地址段 0x600000~0x602000,在 0~0xfff 空间中,与0x4000000x401000段的存放的程序相同,在fff之后存放的是.dynamic.shstrtab节。

5.5 链接的重定位过程分析

使用 objdump -d -r hello

hello.txt 获得 hello 的反汇编代码。

观察hello.txt和helloo.txt异同:

hello.o反汇编得到的helloo.txt中,只有一节地址为0的main,而链接后产生的可执行文件hello经过反汇编得到的hello.txt中,地址是新的虚拟地址(链接完成后产生),增加了.init、.plt、.fini段,下面分别阐述三段作用。

.init

程序初始化时执行的代码段

.plt

动态链接-过程链接表

.fini

程序正常终止执行的代码段

通过两者的比较可以看出链接器的功能:

函数调用。动态链接器将/lib64/ld-linux-x86-64.so.2,crt1.o、crti.o、crtn.o中的必要函数链接到hello中(如刚刚的.init中的初始化函数);并从动态链接共享库libm.so中调用hello.c中用到的printf、exit等函数(此类函数在共享库中被定义)。

.rodata的引用。动态链接库面对重定位后的两个R_X86_64_PC32类型的.rodata,因为.rodata与.text相对位置确定,链接器会计算出相对距离的差值并直接更改函数调用指令处call后的地址,从而实现对.rodata的引用(hello中.rodata中保存的是之前提过多次的两个字符串,因此只是为两个字符串重定条目)。

5.6
hello的执行流程

edb执行hello,下面列出所有被执行的函数及其地址。

函数名称

函数地址

ld-2.27.so!_dl_start

0x7fc8d2698ea0

ld-2.27.so!_dl_init

0x7fc8d26a7630

hello!_start

0x400500

libc-2.27.so!__libc_start_main

0x7fc8d22c7ab0

-libc-2.27.so!__cxa_atexit

NULL

-libc-2.27.so!__libc_csu_init

NULL

hello!_init

NULL

libc-2.27.so!_setjmp

NULL

-libc-2.27.so!_sigsetjmp

NULL

–libc-2.27.so!__sigjmp_save

NULL

hello!main

0x40055d

hello!puts@plt

0x4004b0

hello!exit@plt

0x4004e0

*hello!printf@plt

NULL

*hello!sleep@plt

NULL

*hello!getchar@plt

NULL

ld-2.27.so!_dl_runtime_resolve_xsave

NULL

-ld-2.27.so!_dl_fixup

NULL

–ld-2.27.so!_dl_lookup_symbol_x

NULL

libc-2.27.so!exit

NULL

根据edb分布执行得到各个函数地址。由于main中有两个分支(if argc !=3决定),每个进程只能进入一个分支,因此一次执行无法得到全部函数地址。而程序运行时每次地址会随机化,每次执行都不同,地址为NULL的函数即是另一分支的函数。

5.7
Hello的动态链接分析

当程序调用共享库中的函数时,编译时编译器无法确定该函数地址,只能添加重定位记录,等到动态链接器真正链接共享库时才会得到真正的地址。GOT和PLT两个表实现动态链接,其中PLT数组中每个条目负责调用一个函数(PLT[1]负责libc_start_main),而GOT保存目标函数地址。下面是section headers中关于GOT和PLT的信息:

可以看到,GOT的起始地址为0x601000,通过edb查看起始时GOT中的内容:

调用dl_start后地址0x601000的内容如下:

可以看到,地址0x601009至0x401018的内容发生变化。

在dl_init调用之前,对于每一条动态库中函数调用,调用的目标地址都实际指向 PLT 中的条目信息,GOT 存放的是
PLT 中函数调用指令的下一条指令地址。在dl_init调用之后,GOT[1]指向重定位表(.plt节中每个需要重定位的函数的运行时地址),例如GOT[2]指向动态链接器 ld-linux.so 运行时地址。因此调用dl_init就是重定位的过程。

5.8 本章小结

本章从hello.o到hello的链接过程出发,逐步分析了hello在虚拟内存空间中的虚拟地址、链接过程的重定位分析、、执行流程以及动态链接分析。

(第5章1分)

第6章 hello进程管理

6.1 进程的概念与作用

概念:进程是运行中程序的一个实例。

作用:系统中每个程序运行在某个进程的上下文中,进程为用户提供了以下假象:我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存,处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。

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

   Shell是一个命令解释器,提供给用户以访问操作系统内核的服务,bash是一种命令行式shell。

   处理流程以下:

读取输入的命令行

解析引用并分割命令行为各个单词(称为token),其中重定向所在的token会被保留,直到扩展步骤5后才进行相关处理。

检查命令行结构。主要检查是否有命令列表或shell内置命令。

  1. 对第一个token进行别名扩展。如果检查出它是别名,则扩展后回到2再次进行token分解过程。如果检查出它是函数,则执行函数体中的复合命令。如果它既是别名,又是函数(即命令别名和函数同名称的情况),则优先执行别名。在概念上,别名的临时性最强,优先级最高。

  2. 进行各种扩展。扩展顺序为:花括号扩展;波浪号扩展;参数、变量和命令替换、算术扩展(如果系统支持,此步还进行进程替换);单词拆分;文件名扩展。

6.3 Hello的fork进程创建过程

以hello程序为例,在终端输入命令./hello 1170300808葛润禅,shell会对命令按以上方法进行解析,shell在判断hello不是内置命令后会在当前目录下寻找可执行文件hello,并将后面两个token作为参数传给hello。Shell为当前进程fork子进程,新的子进程几乎但不完全与父进程相同。子进程得到与用户级虚拟地址空间相同(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本(子进程可以读写父进程中打开的任何文件)。子进程和父进程最大的区别是PID不同。子进程运行结束后通常由父进程通过函数waitpid回收,该函数参数有三个,分别是待回收子进程pid、进程状态statusp、选项options。

6.4
Hello的execve过程

   Execve函数在当前进程的上下文中加载并运行一个新程序。

定义如下:int execve (const char *filename, const char
*argv[], const char *envp[])

其中参数分别为目标文件filename,参数列表argv和环境变量列表envp。当execve加载了filename后,execve调用加载器,加载器将可执行目标文件的代码和数据复制到内存中,为当前程序创建新的内存映像:

然后跳转到程序第一条指令或入口点来运行该程序(即_start函数的地址)。这个函数在系统目标文件ctrl.o中定义。start调用系统启动函数 _libc_start_main,该函数定义在共享库libc.so中。他会初始化执行环境,调用用户层的main函数,处理main函数的返回值,最终将控制返还给内核。

6.5 Hello的进程执行

首先介绍进程执行当中的一些概念。

上下文切换context switch:内核使用上下文切换(一种异常控制流)来实现多任务。内核会为每个进程维护一个上下文,其中保存了进程被强占前的状态,包括一些寄存器、用户栈、程序计数器、内核栈以及其他的内核数据结构,如页表、进程表、文件表。

时间片:某个进程从内核开始执行它到内核抢占它并执行其他进程的时间。

   进程执行是通过内核完成的。流程如下:内核在进程执行的某个阶段,会由调度器抢占当前进程而重新开始另一个进程。这种调度使用上下文切换的方式,会先保存当前进程的上下文,再恢复先前被强占的进程的上下文,最终将控制转回

结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。

6.6
hello的异常与信号处理

Hello中的异常共有两种:中断(信号SIGSTP)和终止(信号SIGINT)。

运行程序hello,并通过Ctrl+Z的方式中断程序,查看各类信息如下:

首先中断程序

接下来使用进程查看指令ps,可以看到目前的进程有bash hello和ps三个

Jobs指令查看作业,仅有一个被终止的hello指令

Pstree指令查看进程树:

Fg指令在前台继续运行,发送了信号SIGCONT

Kill指令杀死一个进程,对比可看出hello被杀死

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

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

6.7本章小结

   本章介绍了进程的概念与作用,shell对命令的处理,fork创建进程,execve加载可执行文件。最后讨论了hello进程执行和异常与信号处理。

(第6章1分)

第7章 hello的存储管理

7.1
hello的存储器地址空间

逻辑地址:访问指令给出的地址,即平常所说的偏移地址(通过计算偏移量求得)。

线性地址:地址空间是由连续的非负整数组成的。

虚拟地址:即线性地址。

物理地址:在地址总线上以电子形式存在的、使数据总线可以访问主存的某个特定存储单元的内存地址。可以通过段地址+偏移地址计算得到。

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

在早期的8086处理器中,内存物理地址是这样得到的:把某一段寄存器(段基址)左移四位,然后与地址ADDR相加后被直接送到内存总线上,这个相加后的地址就是内存单元的物理地址,这个ADDR叫逻辑地址。

在80386的段机制中,逻辑地址由两部分组成,即段部分(选择符)及偏移部分。

(1)段的基地址:在线性地址空间中段的起始地址。

(2)段的界限:表示在逻辑地址中,段内可以使用的最大偏移量。

(3)段的属性:表示段的特性。例如,该段是否可被读出或写入,或者该段是否作为一个程序来执行,以及段的特权级等。

现代的x86系统已经不再使用段式管理,这里简单讨论一下逻辑地址怎样计算得到线性地址:

1.取逻辑地址中段标识符(即段内偏移量,13位索引+3位硬件信息)

2.根据段选择符T1确定当前要转换的是GDT(Global Descriptor
Table)中的段还是LDT(Local……)中的段。

3.根据相应寄存器,得到其地址和大小,并根据段标识符中的前13位索引查找对应的段描述符,得到基地址base

4.最后线性地址就是base+offset

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

虚拟地址由虚拟页号和虚拟页偏移量组成,虚拟页偏移量同物理页偏移量相同,因此翻译成物理地址时不会变;页式管理应用到了页表,虚拟页号作为索引到页表中查询相应的物理页号(其中如果有效位为0会产生缺页故障,由缺页异常处理程序处理),至此得到物理地址。

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

页表管理会用到翻译加速缓冲器TLB这种缓存来加快查阅PTE的速度,方式如下:

TLB的存在使得每次查询PTE不需要到高速缓存/内存中寻找,这样加速了查询(TLB在比高速缓存和内存快得多的MMU中)

多级页表的存在使得对内存空间的要求减少了许多。将一级页表存在主存中,只在需要时调出二级页表,而一级页表中许多条目都是空的,因此相应的二级页表不存在。这极大地节约了内存空间的使用。

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

MMU将翻译得到的物理地址传给L1,L1缓存从物理地址中得到缓存标记CT、缓存组索引CI以及缓存偏移CO。高速缓存根据CI作为索引找到缓存中的一组,并通过CT判断是否已经缓存地址对应的数据,若缓存命中,则根据偏移量直接从缓存中读取数据并返回;若缓存不命中,则向低级存储结构中寻找数据(依次为L2、L3、主存)。

1.6
hello进程fork时的内存映射

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

当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。

1.7
hello进程execve时的内存映射

execve函数在当前进程中加载并运行包含在可执行目标文件hello中的程序,会依次执行以下几个步骤:

加载器映射用户地址空间区域:

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

缺页故障:即虚拟地址在页表中翻译时不命中(有效位为0)

页面命中完全是硬件来处理的,然而不命中时需要硬件和操作系统共同处理。过程如下:

得到PTE之后,发现有效位为0,传递控制给内核中的缺页异常处理程序。

缺页异常处理程序选定物理内存中的牺牲页(如果dirty则需要写入磁盘)。

调入新的页面到牺牲页的位置。

返回原来进程再次执行。

7.9动态存储分配管理

   动态内存分配器维护着一个进程的虚拟内存区域,称为堆heap。系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高地址)。对于每个进程,内核维护着一个变量brk(读作break),它指向堆的顶部。

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

   分配器有两种风格,都要求应用显示地分配块。不同之处在于由哪个实体来负责释放已分配的块。

7.10本章小结

   本章从存储器的地址空间开始,分析了各类地址及相应的段式管理和页式管理、虚拟地址到物理地址的翻译、多级页表的应用、cache物理内存访问、fork和execve的内存映射以及缺页故障和相应处理。

(第7章 2分)

第8章 hello的IO管理

8.1
Linux的IO设备管理方法

一个linux文件就是一个m个字节的序列:

B0, B1, … Bk, …, Bm-1

所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。

8.2 简述Unix IO接口及其函数

8.3
printf的实现分析

研究printf的实现,首先来看看printf函数的函数体:

其中,va_list是一个字符指针,arg表示函数的第二个参数。接下来是vsprintf函数:

显然,vsprintf返回的是要打印出来的字符串的长度,接下来讨论write函数:

功能是把buf中的第i个元素写到终端中。追踪write得到

一个int
INT_VECTOR_SYS_CALL表示要通过系统来调用sys_call这个函数。sys_call实现如下:

syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

Getchar实际上是read的包装函数,read函数第一个参数文件描述符为0,意思是标准输入;第二个为输入内容指针,第三个为输入内容个数。read返回字符个数,当个数不为1时出错,返回eof;否则返回内容的指针。

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。

getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

本章介绍了Linux的IO设备管理方法、接口及相应函数和printf与getchar两个函数的实现分析。

(第8章1分)

结论

用计算机系统的语言,逐条总结hello所经历的过程。

你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。

Hello的一生:

编写:编写并保存在hello.c文件中

预处理:解析hello.c,得到hello.i文件

编译:将hello.i翻译成汇编代码,存入hello.s

汇编:将汇编代码解释为01的机器码,存入hello.o

链接:将hello.o链接各类库,形成可执行文件hello

运行:shell为hello
fork出进程,execve加载进程并执行,通过CPU、操作系统及硬件的共同配合将字符串输出到屏幕上。

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

附件

列出所有的中间产物的文件名,并予以说明起作用。

(附件0分,缺失 -1分)

参考文献

为完成本次大作业你翻阅的书籍与网站等

[1]
https://www.cnblogs.com/pianist/p/3315801.html

[2]
深入理解计算机系统(第三版)/(美)兰德尔 布莱尔特. 北京:机械工业出版社 2016.7

[3]
https://blog.csdn.net/lin1746/article/details/23292473

[4]
http://www.kerneltravel.net

(参考文献0分,缺失 -1分)

发布了6 篇原创文章 · 获赞 0 · 访问量 988

猜你喜欢

转载自blog.csdn.net/weixin_43767154/article/details/85635633
今日推荐