CSAPP 大作业 Hello的一生

第1章 概述

1.1 Hello简介

P2P(Program to Process):将hello.c(Program),经过预处理(Precompile)->编译(Compile)->汇编(Assemble)->链接(Link)四个步骤生成hello的二进制可执行文件,然后由shell新建进程(Process)将其执行。
020(Zero to Zero):从一开始的什么都没有(Zero)开始,shell执行execve,为其映射出虚拟内存,然后在开始运行进程的时候分配并载入物理内存,开始执行hello的程序,将其output的东西显示到屏幕,然后hello进程结束,shell回收内存空间,最后还原会什么都没有(Zero)。

1.2 环境与工具

硬件环境:Intel Core i7-6700HQ x64CPU,8G RAM
软件环境:Ubuntu 16.04 LTS
开发与调试工具:gcc,as,ld,edb,readelf,HexEdit

1.3 中间结果

hello.i
hello.s
hello.o
hello

1.4 本章小结

总结了实验的中间结果和使用的工具

第2章 预处理

2.1 预处理的概念与作用

预处理的概念:编译器在编译之前进行的处理。
预处理的作用:方便编译器的编译工作。
预处理命令以符号“#”开头。
C语言的预处理主要有三个方面的内容:
1.宏定义(#define);
2.文件包含(#include);
3.条件编译(#ifdef/#ifndef/#if/#else/#elseif/#endif)。

2.2在Ubuntu下预处理的命令

首先我们执行命令>gcc -E -o hello.i hello.c来生成.i文件,这个.i文件就是预处理完成的文件。
在这里插入图片描述

2.3 Hello的预处理结果解析

这个.i文件有3k+行,里面包括了
标记一些头文件的位置
在这里插入图片描述
将引用的头文件完全复制加入这个.i文件
在这里插入图片描述
以及原本的函数内容
在这里插入图片描述

2.4 本章小结

本章节简单介绍了编译前的预处理过程,展示并且分析预处理的过程,对于我们对接下来的整个编译过程的理解有很大的帮助。

第3章 编译

3.1 编译的概念与作用

编译的概念:将高级语言实现的代码,翻译成为机器能够理解的机器语言的过程。
编译的作用:将高级语言转化为机器语言,使得机器能够更加容易理解和执行。
注意:这的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序

3.2 在Ubuntu下编译的命令

使用编译命令gcc -S -o hello.s hello.i,将预处理后的.i文件编译为汇编文件.s文件。
在这里插入图片描述
生成的.s文件内容:
在这里插入图片描述

3.3 Hello的编译结果解析

此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析。

3.3.1 汇编指令

.file:声明源文件
.text:声明代码段
.section .rodata:声明rodata段
.globl:声明全局变量
.type:指定函数类型或对象类型
.size:声明大小
.long、.string:声明一个long、string的变量类型
.align:声明对指令或者数据的存放地址的对齐方式

3.3.2 变量

helloc.c中使用了int,字符串和数组类型。

3.3.2.1 int

1.int sleepsecs=2.5;
sleepsecs在程序中被声明为全局变量,并且赋值,编译器处理时在.data段声明该变量,.data段存放已经初始化的全局和静态变量。
在这里插入图片描述
如上图所示,编译器首先将sleepsecs在.data代码段中声明为全局变量,并且设置对齐方式为4、设置类型为对象、设置大小为4字节、设置sleepsecs为long类型其值为2。
2.int i;
在这里插入图片描述
i作为局部变量,在程序中声明,使用-4(%rbp)代表的栈空间来保存i,同时也能看出来i是4字节的。
3.int argc
在这里插入图片描述
argc作为main函数的第一个参数,使用-0x20(%rbp)代表的栈空间来保存argc。
3.立即数
exit(1);
在这里插入图片描述
原程序中的1使用$1的立即数代替。

3.3.2.2 字符串

源程序中的字符串为
在这里插入图片描述
在这里插入图片描述
两个字符串都被保存到了rodata段中。
在这里插入图片描述

3.3.2.3 数组

源程序中的数组的是: char *argv[] \text{char *argv[]} char *argv[],是函数执行时输入的命令行,argv作为存放char指针的数组同时是第二个参数传入。
argv的一个元素是 char* \text{char*} char*,是指针类型,在64位系统下大小为8字节。
argv指针指向已经分配好的、一片存放着字符指针的连续空间,起始地址为argv。
在源程序中使用了argv[1]和argv[2]两个cha*,对应于下图中的第43行得到的$rax和第40行得到的$rdx。

3.3.3 赋值

1.int sleepsecs=2.5
因为sleepsecs是全局变量,所以直接在.data节中将sleepsecs声明为值2的long类型数据。
2.i=0
作为局部变量赋值,使用mov指令完成。
根据数据的大小不同,使用不同后缀:
movb 8bit(1Byte)
movw 16bit(2Byte)
movl 32bit(4Byte)
movq 64bit(8Byte)

3.3.4 类型转换

程序中使用的类型转换为:
int sleepsecs=2.5;
将浮点数类型的2.5转换为int类型。
但是发现,由于sleepsecs为全局变量,在.data段声明,且直接初始化为.long 2,是隐式转换。
在这里插入图片描述
当在double或float向int进行类型转换的时候,程序改变数值和位模式的原则是:值会向零舍入。例如1.999将被转换成1,-1.999将被转换成-1。进一步来讲,可能会产生值溢出的情况,与Intel兼容的微处理器指定位模式[10…000]为整数不确定值,一个浮点数到整数的转换,如果不能为该浮点数找到一个合适的整数近似值,就会产生一个整数不确定值。

浮点数默认类型为double,所以上述强制转化是double强制转化为int类型。遵从向零舍入的原则,将2.5舍入为2。

3.3.5 算数操作

进行数据算数操作的汇编指令有:
在这里插入图片描述 程序中使用的算数操作:
i++,对计数器i自增,使用程序指令addl,后缀l代表操作数是一个4B大小的数据。
在这里插入图片描述

3.3.6 关系操作

进行关系操作的汇编指令有:
在这里插入图片描述
程序中涉及的关系运算为:
1.argc!=3:判断argc不等于3。hello.s中使用cmpl $3,-20(%rbp),计算argc-3然后设置条件码,为下一步je利用条件码进行跳转作准备。
在这里插入图片描述
i<10:判断i小于10。hello.s中使用cmpl $9,-4(%rbp),计算i-9然后设置条件码,为下一步jle利用条件码进行跳转做准备。
在这里插入图片描述

3.3.7 控制转移

控制转移操作的汇编指令有:
在这里插入图片描述
程序中涉及的控制转移有:
1.if (argv!=3):当argv不等于3的时候执行程序段中的代码。如图3.6,对于if判断,编译器使用跳转指令实现,首先cmpl比较argv和3,设置条件码,使用je判断ZF标志位,如果为0,说明argv-3=0 argv==3,则不执行if中的代码直接跳转到.L2,否则顺序执行下一条语句,即执行if中的代码。
在这里插入图片描述
2.for(i=0;i<10;i++):使用计数变量i循环10次。如图3.7,编译器的编译逻辑是,首先无条件跳转到位于循环体.L4之后的比较代码,使用cmpl进行比较,如果i<=9,则跳入.L4 for循环体执行,否则说明循环结束,顺序执行for之后的逻辑。
在这里插入图片描述

3.3.8 函数操作

在原过程p中调用函数f包含以下动作:
传递控制:进行函数f的时候,程序计数器必须设置为函数f的代码的起始地址,然后在返回时,要把程序计数器设置为p中调用f后面那条指令的地址。
传递数据:p必须能够向函数f提供一个或多个参数,函数f必须能够向p中返回一个值。
分配和释放内存:在开始时,函数f可能需要为局部变量分配空间,而在返回前,又必须释放这些空间。

1.main函数:

  • 传递控制,main函数因为被调用call才能执行(被系统启动函数__libc_start_main调用),call指令将下一条指令的地址dest压栈,然后跳转到main函数。
  • 传递数据,外部调用过程向main函数传递参数argc和argv,分别使用%rdi和%rsi存储,函数正常出口为return 0,将%eax设置0返回。
  • 分配和释放内存,使用%rbp记录栈帧的底,函数分配栈帧空间在%rbp之上,程序结束时,调用leave指令,leave相当于mov %rbp,%rsp,pop %rbp,恢复栈空间为调用之前的状态,然后ret返回,ret相当pop IP,将下一条要执行指令的地址设置为dest。

2.printf函数:

  • 传递数据:第一次printf将%rdi设置为“Usage: Hello 学号 姓名!\n”字符串的首地址。第二次printf设置%rdi为“Hello %s %s\n”的首地址,设置%rsi为argv[1],%rdx为argv[2]。
  • 控制传递:第一次printf因为只有一个字符串参数,所以call puts@PLT;第二次printf使用call printf@PLT。

3.exit函数:

  • 传递数据:将%edi设置为1。
  • 控制传递:call exit@PLT。

4.sleep函数:

  • 传递数据:将%edi设置为sleepsecs。
  • 控制传递:call sleep@PLT。

5.getchar函数:

  • 控制传递:call gethcar@PLT

3.4 本章小结

本章节记录了从预编译文件.i到汇编文件.s的过程,分析了生成的汇编文件.s的代码。
具体分析了hello程序中的每一部分被编译成汇编以后的存储方式或者记录方法,以及hello中一些常量和格式串在汇编中的记录方法。

第4章 汇编

4.1 汇编的概念与作用

汇编的概念:汇编器将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将保存结果保存在在一个叫做hello.o的二进制文件中。
汇编的作用:将汇编语言翻译成一条条机器语言方便机器执行该段代码。

4.2 在Ubuntu下汇编的命令

在这里插入图片描述

4.3 可重定位目标elf格式

1.ELF Header,包含帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型、机器类型、字节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量等信息。
在这里插入图片描述
2.Section Header,节头部表,包含了文件中出现的各个节的语义,包括节的类型、位置和大小等信息。

3. rela.text,重定位节,一个.text节中位置的列表,包含.text节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。
参考下图和我们在上一章节中得到的.s文件:
在这里插入图片描述
在这里插入图片描述
重定位信息分别是对.L0(第一个printf中的字符串),puts函数,exit函数,.L1(第二个printf中的字符串),printf函数,全局变量sleepsecs,sleep函数,getchar函数进行重定位声明。

4.4 Hello.o的结果解析

通过objdump -d -r hello.o来生成反汇编代码。
在这里插入图片描述
主要差别:
1.分支转移:反汇编代码跳转指令的操作数使用的不是段名称如.L3,因为段名称只是在汇编语言中便于编写的助记符,所以在汇编成机器语言之后显然不存在,而是确定的地址。
2.函数调用:在.s文件中,函数调用之后直接跟着函数名称,而在反汇编程序中,call的目标地址是当前下一条指令。
在这里插入图片描述
这是因为hello.c中调用的是外部函数,需要在link后才知道运行时执行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其call指令后的相对地址设置为全0(目标地址正是下一条指令),然后在.rela.text节中为其添加重定位条目,等待静态链接的进一步确定。
3.全局变量访问:在.s文件中,访问rodata(printf中的字符串),使用段名称+%rip,在反汇编代码中0+%rip,因为rodata中数据地址也是在运行时确定,故访问也需要重定位。所以在汇编成为机器语言时,将操作数设置为全0并添加重定位条目。
在这里插入图片描述
变成了
在这里插入图片描述

4.5 本章小结

本章节记录了从汇编文件.s到机器指令hello.o的过程,通过反汇编手段,分析了机器指令和汇编指令的不同。
同时,也分析了hello.o的ELF信息,注意到了汇编过程发生的变化(跳转信息已经转换成了具体的地址位置)对汇编指令到机器指令的变换有了一定的了解。

第5章 链接

5.1 链接的概念与作用

链接的概念:链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载到内存并执行。链接可以执行于编译时,也就是在源代码被翻译成机器代码时,也可以执行于加载时,也就是在程序被加载器加载到内存并执行时,甚至执行于运行时,也就是由应用程序来执行。
链接的作用:链接是由叫做链接器的程序执行的。它在软件开发中扮演者一个关键的角色,链接器使得分离编译成为可能。

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的格式

1.ELF Header
描述了整个ELF表的信息
在这里插入图片描述
2.Section Headers
描述了各节的信息:
Name:各个Section的名字。
Type:各个Section的类型 。
Address:各个Section在虚拟内存中的存储位置。
Offset:各个Section相对于0x400000的偏移地址。
Size:各个Section的大小。
Align:各个Section的对齐位数。
Info:各个Section的信息。后两位对应类型,前六位对应在symtab节中的ndx值。
在这里插入图片描述

5.4 hello的虚拟地址空间

使用edb加载hello,可以在Data Dump中查询虚拟地址各段的信息。
在这里插入图片描述
我们可以看到程序从可执行文件中加载的信息从0x400000处开始存放,hello中存放位置为0x400000—0x400ff0。
由5.3节中Section Headers节中的信息可以知道这些虚拟内存中相应位置存储的信息是什么。
其次我们可以看到ELF文件中有一个Program Headers节。
在这里插入图片描述
可以看到可执行文件共分为8个节每个节对应的偏移地址,虚拟地址位置,物理地址位置,文件大小,存储大小,标志和对齐方式都存储在该节中。Program Header节告诉系统如何创建进程映像。

5.5 链接的重定位过程分析

objdump -d -r hello生成反汇编文件
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
与hello.o的反汇编结果相比较,hello的反汇编文件多了:
.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:初始化过的全局变量或者声明过的函数

分析重定位:
1.call 外部函数
在这里插入图片描述变成了
在这里插入图片描述
在前面将外部函数的地址确定了下来:
在这里插入图片描述
2.data/rodata节的地址确定
原先存储在data及rodata节中的信息,已经被程序放到了虚拟内存中去,再次调用时会直接从虚拟内存的相应位置读取(如sleepsecs及printf函数的格式串)
在这里插入图片描述变成了
在这里插入图片描述

5.6 hello的执行流程

使用edb单步调试执行hello注意call的调用
载入:
_dl_start
_dl_init
开始执行:
__stat
_cax_atexit
_new_exitfn
_libc_start_main
_libc_csu_init
运行:
_main
_printf
_exit
_sleep
_getchar
_dl_runtime_resolve_xsave
_dl_fixup
_dl_lookup_symbol_x
退出:
Exit

5.7 Hello的动态链接分析

首先找到GOT表地址:
在这里插入图片描述
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
在这里插入图片描述
变为了
在这里插入图片描述
在dl_init调用之后,0x6008b8和0x6008c0处的两个8B数据分别发生改变为00007f1ee350f168和00007f1ee32ff870,其中GOT[1]指向重定位表(依次为.plt节需要重定位的函数的运行时地址)用来确定调用的函数地址,GOT[2]指向动态链接器ld-linux.so运行时地址。

5.8 本章小结

本章介绍了链接的概念和作用,分析了hello的ELF格式,虚拟地址空间的分配,重定位和执行过程还有动态链接的过程。

第6章 hello进程管理

6.1 进程的概念与作用

进程的概念:进程是计算机程序需要进行对数据集合进行操作所运行的一次活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
进程的作用:进程为用户提供了以下假象:我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存,处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。

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

shell是一个应用程序,他在操作系统中提供了一个用户与系统内核进行交互的界面,用户通过这个界面访问操作系统内核的服务。
处理流程:
1.从终端读入输入的命令。
2.将输入字符串切分获得所有的参数
3.如果是内置命令则立即执行
4.否则调用相应的程序为其分配子进程并运行
5.shell应该接受键盘输入信号,并对这些信号进行相应处理

6.3 Hello的fork进程创建过程

终端程序调用fork函数创建一个新的运行的子进程,新创建的子进程几乎但不完全与父进程相同。
子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本。
这意味着,当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程与子进程之间最大的区别在于它们拥有不同的PID,父进程的PID非0,子进程PID为0。
父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的逻辑控制流的指令。
在子进程执行期间,父进程默认选项是显示等待子进程的完成。
简单进程图如下:
在这里插入图片描述

6.4 Hello的execve过程

Execve函数在当前进程的上下文中加载并运行一个新的程序。
Execve加载并运行可执行目标文件filename,且带参数列表argv和环境变量envp。只有当出现错误时,例如找不到filename,execve才会返回到调用程序。所以与fork调用一次返回两次不一样,execve调用一次并且从不返回。
使用execve函数之后,会清楚原先程序在虚拟内存中存储的信息,并重新初始化。
程序新开始时的栈帧如下:
在这里插入图片描述

6.5 Hello的进程执行

逻辑控制流:一系列程序计数器PC的值的序列叫做逻辑控制流,进程是轮流使用处理器的,在同一个处理器核心中,每个进程执行它的流的一部分后被抢占(暂时挂起),然后轮到其他进程。
用户模式和内核模式:处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。
在这里插入图片描述
现在来看看hello的sleep进程调度过程:
当调用sleep之前,如果hello程序不被抢占则顺序执行,假如发生被抢占的情况,则进行上下文切换,上下文切换是由内核中调度器完成的,当内核调度新的进程运行后,它就会抢占当前进程,
并进行
1)保存以前进程的上下文
2)恢复新恢复进程被保存的上下文
3)将控制传递给这个新恢复的进程
来完成上下文切换。
hello初始运行在用户模式,在hello进程调用sleep之后陷入内核模式,内核处理休眠请求主动释放当前进程,并将hello进程从运行队列中移出加入等待队列,定时器开始计时,内核进行上下文切换将当前进程的控制权交给其他进程,当定时器到时时(2.5secs)发送一个中断信号,此时进入内核状态执行中断处理,将hello进程从等待队列中移出重新加入到运行队列,成为就绪状态,hello进程就可以继续进行自己的控制逻辑流了。
在这里插入图片描述

6.6 hello的异常与信号处理

1.正常运行
回车不会发出能接受的信号,结果除了会显示空行以外,与正常运行无异。
在这里插入图片描述
2.Ctrl-Z
这个操作向进程发送了一个sigtstp信号,让进程暂时挂起,输入ps命令符可以发现hello进程还没有被关闭。
在这里插入图片描述
3.Ctrl-C
这个操作向进程发送了一个sigint信号,让进程直接结束,输入ps命令可以发现当前hello进程已经被终止了。
先杀死了前面Crtl-Z挂起的进程。
在这里插入图片描述

6.7本章小结

本章介绍了进程的概念和作用,描述了shell如何在用户和系统内核之间建起一个交互的桥梁。
介绍了Shell的一般处理流程,调用fork创建新进程,调用execve执行hello,hello的进程执行,hello的异常,信号处理和hello进程如何在内核和前端中反复跳跃运行的。

第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:是程序运行由CPU产生的与段相关的偏移地址部分。他是描述一个程序运行段的地址。
物理地址:计算机系统的主存被组织成一个由M个连续的字节大小的单元组成的数组。每个字节都有唯一的物理地址。
虚拟地址:虚拟地址就是逻辑地址,又叫虚地址。
线性地址:分段机制下CPU寻址是二维的地址即,段地址:偏移地址,CPU不可能认识二维地址,因此需要转化成一维地址即,段地址*16+偏移地址,这样得到的地址便是线性地址(在未开启分页机制的情况下也是物理地址)。

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

在段式存储管理中,将程序的地址空间划分为若干个段(segment),这样每个进程有一个二维的地址空间。在前面所介绍的动态分区分配方式中,系统为整个进程分配一个 连续的内存空间。而在段式存储管理系统中,则为每个段分配一个连续的分区,而进程中的各个段可以不连续地存放在内存的不同分区中。程序加载时,操作系统为 所有段分配其所需内存,这些段不必连续,物理内存的管理采用动态分区的管理方法。在为某个段分配物理内存时,可以采用首先适配法、下次适配法、最佳适配法 等方法。在回收某个段所占用的空间时,要注意将收回的空间与其相邻的空间合并。段式存储管理也需要硬件支持,实现逻辑地址到物理地址的映射。程序通过分段 划分为多个模块,如代码段、数据段、共享段。这样做的优点是:可以分别编写和编译源程序的一个文件,并且可以针对不同类型的段采取不同的保护,也可以按段为单位来进行共享。总的来说,段式存储管理的优点是:没有内碎片,外碎片可以通过内存紧缩来消除;便于实现内存共享。缺点与页式存储管理的缺点相同,进程必须全部装入内存。

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

在这个转换中要用到翻译后备缓冲器(TLB),首先我们先将线性地址分为VPN(虚拟页号)+VPO(虚拟页偏移)的形式,然后再将VPN拆分成TLBT(TLB标记)+TLBI(TLB索引)然后去TLB缓存里找所对应的PPN(物理页号)如果发生缺页情况则直接查找对应的PPN,找到PPN之后,将其与VPO组合变为PPN+VPO就是生成的物理地址了。

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

首先将VPN分成三段,对于TLBT和TLBI来说,如果可以在TLB中找到对应的PPN的话那肯定是最好不过的了,但是还有可能出现缺页的情况,这时候就需要到页表中去找。此时,VPN被分成了更多段(这里是4段)CR3是对应的L1PT的物理地址,然后一步步递进往下寻址,越往下一层每个条目对应的区域越小,寻址越细致,在经过4层寻址之后找到相应的PPN让你和和VPO拼接起来。

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

得到物理地址之后,先将物理地址拆分成CT(标记)+CI(索引)+CO(偏移量),然后在一级cache内部找,如果未能寻找到标记位为有效的字节(miss)的话就去二级和三级cache中寻找对应的字节,找到之后返回结果。

7.6 hello进程fork时的内存映射

mm_struct(内存描述符):描述了一个进程的整个虚拟内存空间。
vm_area_struct(区域结构描述符):描述了进程的虚拟内存空间的一个区间。
在用fork创建虚拟内存的时候,要经历以下步骤:
创建当前进程的mm_struct,vm_area_struct和页表的原样副本。
两个进程的每个页面都标记为只读页面。
两个进程的每个vm_area_struct都标记为私有,这样就只能在写入时复制。

7.7 hello进程execve时的内存映射

1.删除已存在的用户区域。
2.创建新的私有区域(.malloc,.data,.bss,.text)。
3.创建新的共享区域(libc.so.data,libc.so.text)。
4.设置PC,指向代码的入口点。

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

情况1:段错误:首先,先判断这个缺页的虚拟地址是否合法,那么遍历所有的合法区域结构,如果这个虚拟地址对所有的区域结构都无法匹配,那么就返回一个段错误(segment fault)。
情况2:非法访问:接着查看这个地址的权限,判断一下进程是否有读写改这个地址的权限。
情况3:如果不是上面两种情况那就是正常缺页,那就选择一个页面牺牲然后换入新的页面并更新到页表。

7.9动态存储分配管理

动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
1.分配器分为两种基本风格:显式分配器、隐式分配器。
2.显式分配器:要求应用显式地释放任何已分配的块。
3.隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。
1.带边界标签的隐式空闲链表
a)堆及堆中内存块的组织结构:
在这里插入图片描述
在内存块中增加4B的Header和4B的Footer,其中Header用于寻找下一个blcok,Footer用于寻找上一个block。Footer的设计是专门为了合并空闲块方便的。因为Header和Footer大小已知,所以我们利用Header和Footer中存放的块大小就可以寻找上下block。
在这里插入图片描述
b)隐式链表
所谓隐式空闲链表,对比于显式空闲链表,代表并不直接对空闲块进行链接,而是将对内存空间中的所有块组织成一个大链表,其中Header和Footer中的block大小间接起到了前驱、后继指针的作用。
c)空闲块合并
因为有了Footer,所以我们可以方便的对前面的空闲块进行合并。合并的情况一共分为四种:前空后不空,前不空后空,前后都空,前后都不空。对于四种情况分别进行空闲块合并,我们只需要通过改变Header和Footer中的值就可以完成这一操作。
2.显示空间链表
在这里插入图片描述
使用空闲块内的空间储存前驱指针和后继指针,来实现链表功能。

7.10本章小结

本章介绍了储存器的地址空间,讲述了虚拟地址、物理地址、线性地址、逻辑地址的概念,还有进程fork和execve时的内存映射的内容。描述了系统如何应对那些缺页异常,最后描述了malloc的内存分配管理机制。本章还描述了系统是如何应对缺页故障现象的,讲述了动态存储分配的多种管理方式。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件
文件的类型:
1.普通文件(regular file):包含任意数据的文件。
2.目录(directory):包含一组链接的文件,每个链接都将一个文件名映射到一个文件(“文件夹”)。
3.套接字(socket):用来与另一个进程进行跨网络通信的文件
4.命名通道
5.符号链接
6.字符和块设备
设备管理:unix io接口
1.打开和关闭文件
2.读取和写入文件
3.改变当前文件的位置

8.2 简述Unix IO接口及其函数

打开和关闭文件:
1.open()函数:打开一个已经存在的文件,若干不存在则创建一个新的文件。
int open(char* filename,int flags,mode_t mode);
2.close()函数:关闭一个已经打开的文件。
int close(fd);
读取和写入文件:
1.read()函数:从当前文件位置复制字节到内存位置。
ssize_t read(int fd,void *buf,size_t n);
2.write()函数:从内存复制字节到当前文件位置。
ssize_t wirte(int fd,const void *buf,size_t n);
读写文件时,如果返回值<0则说明出现错误
改变文件位置:
lseek()函数

8.3 printf的实现分析

https://www.cnblogs.com/pianist/p/3315801.html
在这里插入图片描述
在printf的代码中,可以发现,调用了两个外部函数:vsprintf和write。
在这里插入图片描述
从上面vsprintf函数可以看出,这个函数的作用是将所有的参数内容格式化之后存入buf,然后返回格式化数组的长度。
write函数是将buf中的i个元素写到终端的函数。
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

getchar调用了一个read函数,这个read函数是将整个缓冲区都读到了buf里面,然后将返回值是缓冲区的长度。我们可以发现,如果buf长度为0,getchar才会调用read函数,否则是直接将保存的buf中的最前面的元素返回。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

本章节讲述了一下linux的I/O设备管理机制,了解了开、关、读、写、转移文件的接口及相关函数,简单分析了printf和getchar函数的实现方法以及操作过程。

结论

1.预处理:将hello.c调用的所有外部的库展开合并到一个hello.i文件中
2.编译,将hello.i编译成为汇编文件hello.s
3.汇编,将hello.s会变成为可重定位目标文件hello.o
4.链接,将hello.o与可重定位目标文件和动态链接库链接成为可执行目标程序hello
5.运行:shell中运行hello
6.创建子进程:shell进程调用fork为其创建子进程
7.运行程序:shell调用execve,execve调用启动加载器,加映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入 main函数。
8.执行指令:CPU为其分配时间片,在一个时间片中,hello享有CPU资源,顺序执行自己的控制逻辑流
9.访问内存:MMU将程序中使用的虚拟内存地址通过页表映射成物理地址。
10.动态申请内存:printf会调用malloc向动态内存分配器申请堆中的内存。
11.信号:如果运行途中键入ctr-c ctr-z则调用shell的信号处理函数分别停止、挂起。
12.结束:shell父进程回收子进程,内核删除为这个进程创建的所有数据结构。

附件

hello.i
hello.s
hello.o
hello

参考文献

为完成本次大作业你翻阅的书籍与网站等
[1] 内存管理之一 段式与页式管理(https://www.cnblogs.com/xavierlee/p/6400230.html)
[2] linux kill命令详解(https://www.cnblogs.com/wangcp-2014/p/5146343.html)
[3] 物理地址和逻辑地址(https://blog.csdn.net/tuxedolinux/article/details/80317419)
[4] LINUX 逻辑地址、线性地址、物理地址和虚拟地址(https://www.cnblogs.com/zengkefu/p/5452792.html)

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值