csapp hello的一生

csapp hello的一生

摘 要
摘要是论文内容的高度概括,应具有独立性和自含性,即不阅读论文的全文,就能获得必要的信息。摘要应包括本论文的目的、主要内容、方法、成果及其理论与实际意义。摘要中不宜使用公式、结构式、图表和非公知公用的符号与术语,不标注引用文献编号,同时避免将摘要写成目录式的内容介绍。

关键词:CSAPP;O2O;P2P;Hello.c;
最简单的hello.c程序也有它自己的底层运行原理。在程序员写完代码之后经历过许多阶段才能将结果展现在我们的电脑屏幕上。从预处理、编译、汇编、链接到创建进程、分配存储空间、到最后的IO输出到我们的屏幕上。本文详细说明了最简单的hello.c程序以上所有的过程,即hello.c的一生。
(摘要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:最初的我们除了通过键盘键入的hello.c(program)之外什么都没有,必须经过预处理(处理“#”;“#define”宏替换;处理条件编译指令;插入“#include”包含的文件;删除注释等等)、编译(语法语义分析;优化代码并生成汇编代码;将汇编代码转换为计算机可识别的二进制机器语言(可重定位目标文件))、链接(链接可重定位目标文件和调用的库文件生成可执行目标文件),最后在shell中启动(fork产生子进程变成process)。
O2O:shell的进程需要execve,并映射虚拟内存;之后载入物理内存,进入main函数执行目标代码;运行结束后shell父进程回收hello 进程,释放相关占用的资源,程序结束。
1.2 环境与工具
Windows10 家庭版64位;
Oracle VM VirtualBox;
Linux 64位;
Intel® Core™ i7-9750H CPU @ 2.60GHz;
GDB ;Objdump ;readelf;gcc;ld
1.3 中间结果
hello.i 预处理之后的文本文件
hello.o 编译之后的汇编文件
hello.s 汇编之后的可重定位目标文件
Hello_elf.txt hello的elf
Hello_t.txt hello的反汇编代码
Hello_ot.txt hello.o的反汇编代码
Hello_oelf.txt hello.o的elf
1.4 本章小结
本章简要分析了hello程序的P2P和O2O的过程,是以下章节的简要介绍。简单介绍了一下实验所用到软硬件环境以及中间所生成的文件的名称和介绍。
(第1章0.5分)

第2章 预处理
2.1 预处理的概念与作用
1.概念:
预处理指在进行编译之前所进行的工作。预处理器会对“#”开头的指令做解释(宏定义,文件插入,条件编译等)。

图表 1
就像我们的hello.c文件中的三个头文件中,“#include<stdio.h>”会告诉预处理器要读取系统文件stdio.h中的内容并把它直接插入到我们的程序文本之中。经过预处理后会得到一个.i文件。
2.作用:
增强了代码的可移植性,以后代码的维护和会更加方便。
2.2在Ubuntu下预处理的命令
命令:gcc hello.c -E -o hello.i
在这里插入图片描述

图表 2
2.3 Hello的预处理结果解析
在这里插入图片描述

图表 3
我们可以看到,经过预处理后,前面的三个预处理指令没有了,取而代之的是多达三千多行的代码。这些代码来自于原来的三个系统头文件。由此可以看出,在经过预处理后,三个头文件中的代码插入到了我们的hello.c文件中拼接成了hello.i文件。
2.4 本章小结
预处理是c程序从.c文件变成可执行文件的第一步,其主要内容是处理以“#”开头的预处理指令(其中包括宏定义,头文件插入,条件编译)。
(第2章0.5分)

第3章 编译
3.1 编译的概念与作用
1.概念:
将某一种程序设计语言写的程序翻译成等价的另一种语言的程序, 称之为编译程序。
对于hello程序来说,就是将高级语言写成的代码文本文件(hello.i)翻译成汇编语言的代码文本文件(hello.s)。
2.作用:
把高级语言C语言代码翻译成比较低级的汇编语言。用高级语言写代码更加方便人的理解,对人友好,降低开发门槛和难度。
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.i -o hello.s
在这里插入图片描述

图表 4
3.3 Hello的编译结果解析

在这里插入图片描述
在这里插入图片描述

图表 5
1.指令:
.file 声明原文件
.text 代码段
.section .rodata rodata节,存放字符串和const
.globl 声明一个全局变量
.type 用来指定类型(函数类型或者对象类型)
.size 声明大小
.long .string 声明一个long、string类型
.align 声明对指令或者数据的存放地址进行对齐的方式

2.数据:
在这里插入图片描述

图表 6
这里存放的是字符串“用法: Hello 学号 姓名 秒数!\n”和字符串“Hello %s %s\n”。下面的main是一个函数类型的全局变量。
在这里插入图片描述

图表 7
上图为局部变量,局部变量存放在函数的堆栈当中。在hello分配到堆栈空间之后,利用mov语句将立即数0存放在-4(%rbp)的位置。

3.操作:
赋值:通过传送指令实现内存到寄存器,寄存器到内存,寄存器到寄存器,立即数到寄存器的赋值操作。movx a ,b(x为字节数,可为b,w,l,q分别为1,2,4,8字节)。
压栈弹栈:push,pop操作
在这里插入图片描述

图表 8
比较:cmp指令,cmp S1 S2 执行 S2–S1,如果结果为0,设置ZF 0标志条件码寄存器中的值为1,如果结果为负数,设置SF负数条件码寄存器为1。
在这里插入图片描述
在这里插入图片描述

图表 9
如图是比较-4(%rbp)(存储的值是i)和7的大小,即判断循环条件是否满足。
函数传递参数:对于函数来说,是通过寄存器和栈来传递参数的。通过寄存器最多可以传递6个参数,并且第一到六个参数分别存放在%rdi,%rsi,%rdx,%rcx,%r8,%r9。其余的参数通过栈传递。
在这里插入图片描述

图表 10
在hello中只有两个参数(argc在%rdi,argv在%rsi)。在函数分配栈空间后,利用mov指令把两个参数传送到函数的堆栈中。
跳转:先利用cmp语句比较大小(设置跳转条件),然后根据跳转条件执行相应的指令,跳转到相应的位置。
在这里插入图片描述

图表 11
在hello中的if语句就用到跳转指令:
在这里插入图片描述
在这里插入图片描述

图表 12
不满足条件就继续下一条指令:leaq .LC0(%rip),%rdi ,之后调用puts和exit;
在这里插入图片描述

图表 13
可以看出是提示的输入信息(“用法: Hello 学号 姓名 秒数!\n”)
若满足条件则跳转.L2(给i初值0)

在这里插入图片描述

在这里插入图片描述

图表 14
还有for循环指令也用到跳转:

在这里插入图片描述
在这里插入图片描述

图表 15
上文已经说过.L2是为i赋初值0;之后跳转到.L3(条件判断i<=7?)满足跳转到.L4(循环体),最后执行i++操作;不满足条件则调用getchar结束程序。
函数调用:call指令。函数调用的第一步就是把函数需要的参数放入相应的寄存器中,由前面解析我们已经知道,函数的第一个参数存放在%rdi中,所以这里首先把.LC0中数据放入edi(见图12)。可知里面的数据就是我们的输出,然后再call puts函数。call Q 指令会把地址Addr压入栈中,并将PC设置位Q的起始地址,压入栈中的地址称位返回地址,是call指令后面的那条指令的地址。同理可得exit(0)的调用过程。

此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析。
3.4 本章小结
汇编语言是比C语言低一级的语言,汇编语言中涉及到了计算机的低层原理,充分理解汇编语言可以深刻的理解计算机是如何真正的执行我们的程序的。可能在高级语言中一个非常简单的循环语句、判断语句、函数的调用语句在汇编语言中就需要非常多行代码来实现。即使现在几乎都用高级语言来编写程序,但是理解汇编代码对我们优化高级语言编写的代码和发现解决高级语言中出现的一些难以预料的bug有非常大的帮助。
(第3章2分)

第4章 汇编
4.1 汇编的概念与作用
1.概念:
把汇编语言翻译成机器语言的过程成为汇编。
2.作用:
汇编代码人可以理解,但是机器不能。机器的“眼”里只有0或者1。通过汇编这个过程能把汇编代码转化成计算机完全能理解的二进制机器代码。
.s文件经过汇编转化成.o文件(二进制文件,可重定位目标程序)。

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

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

获得hello.o的ELF格式(hello_oelf.txt):命令:readelf -a hello.o >hello_oelf.txt
在这里插入图片描述

图表 17
在这里插入图片描述

图表 18
图18展示了hello.o的elf头。Magic描述了生成该文件的系统的字的大小和字节顺序,ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括节头大小、目标文件的类型、系统及硬件类型、字节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量等信息。
在这里插入图片描述

图表 19
在ELF之后都是节,节头中有它的基本信息。
.text: 已编译的机器代码,类型为PROBITS,意思是程序数据,旗标为AX,意思是分配内存且可执行

.rela.text 一个.text节中位置的列表

.data: 这个里面是已初始化的全局变量和静态c变量,类型也为PROBITS,旗标WA意思是分配内存且可修改

.bss: 这里面放的是未初始化的全局变量和静态c变量,类型NOBITS,意思是暂时没有存储空间,说明这个节在开始是不占据实际的空间

.rodata: 只读数据,如switch中的跳转表,printf中的格式串。hello程序中的printf中的格式串就存放在这里。

.comment: 这个节中包含了版本控制信息

.note.GNU_stack: 用来标记executable stack(可执行堆栈)
.eh_frame:主要就是用来处理异常
.rela.eh_frame:.eh_frame的重定位信息
.symtab:,装载符号信息
.strtab: 一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部的节名字。
.shstrtab:该区域包含节区名称

符号表:
在这里插入图片描述

图表 20
每个可重定位目标模块都有一个符号表,符号表示一个结构型的数组,每个条目包括名称、大小和符号的位置。符号表是由汇编器构造的。
在这里插入图片描述

图表 21
上图为hello的符号表。Eg:main是全局符号,位于.text段,偏移量为0,大小为146字节的一个函数。

重定位节:
在这里插入图片描述

图表 22
利用objdump能获得hello的重定位条目。每个重定位条目的名称,偏移量和偏移类型都已经声明。其中R_X86_64_32意思是重定位时使用一个32位的绝对地址的引用,通过绝对寻址,CPU直接使用在指令中编码的32位值作为有效地址,不需要进一步修改。R_X86_64_PC32意思是重定位时使用一个32位PC相对地址的引用。一个pc相对地址就是据程序计数器的当前运行值的偏移量。

4.4 Hello.o的结果解析
(以下格式自行编排,编辑时删除)
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。

获得hello.o的反汇编(hello_ot.txt):命令:objdump -d hello.o>hello_ot.txt
在这里插入图片描述

图表 23
在这里插入图片描述

图表 24
机器语言就是二进制数集,可以看到每一条指令都有他自己的唯一的机器代码,例如:retq的机器码是0xc3。
可以看见hello.o的反汇编结果与hello.s有很大不同。
1.首先,在反汇编的代码中已经没有类似于.L1这样的标识了,这些标识变成了相应的地址。
2.堆栈大小表示不同。在机器码中申请的栈空间为0x20,而在.s文件中申请了32字节的大小。
在这里插入图片描述

图表 25

3.跳转不同。在机器码中,跳转的不是标记符,而是具体的跳转地址。
在这里插入图片描述
在这里插入图片描述

图表 26
如图26,74代表je,0x16代表跳转到地址相对下一条执行指令(pc)的偏移量,即:0x16 = 0x2f-0x19。
4.函数调用的方式不同。在机器码中,函数调用的不再是函数名称,而是目标函数的地址。
在这里插入图片描述

图表 27

说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
4.5 本章小结
从汇编代码变成机器代码,程序就变成了计算机能够读懂的代码。同时也理解了数据,程序是如何在计算机中存储的。在经过汇编之后的机器代码与汇编代码有很大的不同,代码变成了ELF格式,不同的量放在不同的段里,寻找更加方便。
(第4章1分)

第5章 链接
5.1 链接的概念与作用
1.概念:
将可重定位目标程序和这个文件可能会调用的目标代码进行合并,最后变成一个可执行目标文件。
2.作用:
把可重定位目标文件和命令行参数作为输入,产生一个完整的,可以加载运行的可执行目标文件。
在这里指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
在这里插入图片描述

图表 28
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
获得hello的ELF格式(hello_elf.txt):命令:readelf -a hello>hello_elf.txt
在这里插入图片描述

图表 29
ELF头:
在这里插入图片描述

图表 30
节头:
在这里插入图片描述
在这里插入图片描述

我们可以发现hello的ELF文件比hello.o的ELF文件多出很多东西。这是因为计算机进行了动态链接。
1.interp段:动态链接器在操作系统中的位置不是由系统配置决定,也不是由环境参数指定,而是由 ELF 文件中的 .interp 段指定。该段里保存的是一个字符串,这个字符串就是可执行文件所需要的动态链接器的位置,常位于 /lib/ld-linux.so.2。(通常是软链接)
2.hash段:在动态链接下,需要在程序运行时查找符号,为了加快符号查找过程,增加了辅助的符号哈希表。
3.dynsym段:该段与 “.symtab”段类似,但只保存了与动态链接相关的符号。该符号表中记录了动态链接符号在动态符号字符串表中的偏移。
4.dynstr段:该段是 .dynsym 段的辅助段。
5.real.dyn段:对数据引用的修正,其所修正的位置位于 “.got”以及数据段
6.real.plt段:对函数引用的修正,其所修正的位置位于 “.got.plt
7.dynamic段:该段中保存了动态链接器所需要的基本信息,是一个结构数组,可以看做动态链接下 ELF 文件的“文件头”。存储了动态链接会用到的各个表的位置等信息。

分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
在这里插入图片描述

图表 31
用edb查看到hello程序的虚拟地址为0x401000-0x402000,并且给我们列出了整个文件在内存上存储的是什么。
5.5 链接的重定位过程分析
(以下格式自行编排,编辑时删除)
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
命令:objdump -d hello>hello_t.txt

在这里插入图片描述
在这里插入图片描述

图表 32
可以看到这里多了.init和.plt段,这是在动态链接的过程中产生的。

.text段的代码在经过链接后的变化:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

图表 33
Call指令的地址发生了变化。在链接之后确定了exit函数的地址为0x4010d0,偏移量0xffffff7c = 0x4010d0(目标地址)-0x401154(当前pc)。
5.6 hello的执行流程
(以下格式自行编排,编辑时删除)
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。

名称 地址
ld-2.27.so! dl_start 00007f45d6db6100
hello!_start 00000000004010f0
hello_main 0000000000401125

5.7 Hello的动态链接分析
我们的程序中调用了很多共享库函数,编译器没有办法得到这些函数的运行时地址,这些共享模块可能被加载到程序的任意位置。所以对于这些函数,GNU采取延时绑定技术,将过程地址的绑定推迟到第一次调用该过程。需要用到两个数据结构PLT和GOT。
PLT节:

在这里插入图片描述
在这里插入图片描述

图表 34
每一个可执行的库函数都有自己的PLT条目,每个条目负责调用一个具体的函数。Eg:0x401090为puts函数,0x2f7d代表的是GOT条目与下一条指令的固定距离。
5.8 本章小结
链接是各种文件和代码和数据片段收集组合变成一个单一文件的过程,最后合并完的可执行目标文件可以加载到内存并执行。链接操作可以发生在编译阶段,加载阶段,甚至是运行时执行。链接是由链接器来执行的。
(第5章1分)

第6章 hello进程管理
6.1 进程的概念与作用
1.概念:
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
2.作用
通过进程,我们会得到一种假象,好像我们的程序是当前唯一运行的程序,我们的程序独占处理器和内存,我们程序的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
Shell俗称壳(用来区别于核),是指“为使用者提供操作界面”的软件(command interpreter,命令解析器)。它类似于DOS下的COMMAND.COM和后来的cmd.exe。它接收用户命令,然后调用相应的应用程序。Bash,Unix shell的一种,在1987年由布莱恩·福克斯为了GNU计划而编写。1989年发布第一个正式版本,原先是计划用在GNU操作系统上,但能运行于大多数类Unix系统的操作系统之上,包括Linux与Mac OS X v10.4都将它作为默认shell。
Bash是Bourne shell的后继兼容版本与开放源代码版本。
Bash是一个命令处理器,通常运行于文本窗口中,并能执行用户直接输入的命令。Bash还能从文件中读取命令,这样的文件称为脚本。和其他Unix shell 一样,它支持文件名替换(通配符匹配)、管道、here文档、命令替换、变量,以及条件判断和循环遍历的结构控制语句。

处理流程:shell 执行一系列的读/求值步骤,然后终止。读步骤读取来自用户的一个命令行。求值步骤解析命令行,并代表用户运行程序。
6.3 Hello的fork进程创建过程
我们在shell中输入“./hello”,这不是shell的内置命令,所以shell会认为他是个可执行文件。Shell运行一个程序时,父进程通过fork函数创建一个子进程给这个程序,子进程几乎与父进程相同:相同的虚拟地址空间的副本,其中包括代码、数据段、堆栈和共享库。不同的地方是有不同的PID。
6.4 Hello的execve过程
(以下格式自行编排,编辑时删除)
在这里插入图片描述

图表 35
execve函数在当前进程的上下文中加载并运行一个性的程序。

  1. execve就是一次系统调用,首先要做的将新的可执行文件的绝对路径从调用者拷贝到系统空间中。
    2.找到可执行文件并打开,,初始化操作系统已经为可执行文件设置的数据结构,保存一个可执行文件必要的信息。
    3找到正确的代理人,代理人放弃以前从父进程继承来的资源。主要是对信号处理表,用户空间和文件3大资源的处理。
    6.5 Hello的进程执行
    在hello的执行过程中的某些时刻,内核可以决定执行那个进程(调度)。当内核选择了一个新的进程,就说内核调度了这个进程。调度后抢占当前进程,并通过上下文切换的方式来转移控制新的进程。
    上下文切换:
    在这里插入图片描述

图表 36
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
6.6 hello的异常与信号处理
在输入的过程中没有回车就将输入的字符串缓存起来,当输入回车后就按照shell命令执行。
在这里插入图片描述

图表 37
在这里插入图片描述

图表 38
如果输入的是Ctrl+Z,就会发送一个SIGTSTP信号给前台进程组的每个进程,停止前台进程(hello程序)

![在这里插入图片描述](https://img-blog.csdnimg.cn/20210625005959512.png#pic_center)

在这里插入图片描述

图表 39
如果输入的是Ctrl+C,就会发送一个SIGINT信号给前台进程组的每个进程,终止前台进程(hello程序),可以看见已近回收了hello
在这里插入图片描述

图表 40
fg 1命令是将后台第一个作业(hello)变为前台,所以看到继续输出剩下的五次。
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
6.7本章小结
进程是程序执行的实例,我们的程序就是二进制字符串,通过fork为hello创建进程并为它分配上下文,看起来好像独享整个虚拟内存空间。在运行的时候会遇到各种的异常情况,产生异常会通过信号机制调用信号处理函数来处理。
(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:逻辑地址是指机器语言指令中,用来指定一个操作数或者是一条指令的地址。
在这里插入图片描述

图表 41
这里0x4010e0就是逻辑地址
线性地址:是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。它也是一个不真实的地址,
物理地址:计算机系统的主存被组织成一个由M个连续的字节大小单元组成的数组,每字节都有一个唯一的物理地址。
虚拟地址:也就是线性地址,现代计算机通过虚拟地址寻址,通过MMU(内存管理单元)翻译成物理地址。Hello程序中,各个节的地址都是指的虚拟地址
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
7.2 Intel逻辑地址到线性地址的变换-段式管理
在这里插入图片描述

图表 42
首先,给定一个完整的逻辑地址[段选择符:段内偏移地址],

  1. 看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。
    2.拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。
    3.把Base + offset,就是要转换的线性地址了。
    7.3 Hello的线性地址到物理地址的变换-页式管理
    计算机通过MMU来完成从虚拟地址到物理地址的转换
    在这里插入图片描述

图表 43
PTBR是CPU里的一个控制寄存器,指向当前页表。N位虚拟地址包括p位的虚拟页面偏移VPO和n-p位的VPN。MMU通过VPN选择适当的PTE,将其中的PPN和VPO串联起来组成物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
(以下格式自行编排,编辑时删除)
CPU产生一个虚拟地址,MMU需要查询一个PTE,如果运气不好,需要从内存中取得,这需要花费很多时间,通过TLB(翻译后备缓冲器)能够消除这些开销。TLB是一个小的,虚拟寻址的缓存,在MMU里,其每一行都保存着一个单个PTE组成的块,TLB通常具有高度相联度。
在这里插入图片描述

图表 44
多级页表:
如果是64位系统,我们有一个32位地址空间,4KB的页面和一个4字节的PTE,我们总需要8PB的空间来存放页表,这显然是不现实的。所以需要使用多级页表。
在这里插入图片描述

图表 45
图45是一个二级页表,第一级页表的每个PTE负责一个4MB的块,每个块由1024个连续的页面组成。二级页表每一个PTE负责一个4KB的虚拟地址页面。这样的好处在于,如果一级页表中有一个PTE是空,那么二级页表就不会存在,这样会有巨大的潜在节约,因为4GB的地址空间大部分都是未分配的。
在这里插入图片描述

图表 46
图46为K级页表地址翻译。
在这里插入图片描述

图表 47
上图是Core i7采用的4ji链表。
7.5 三级Cache支持下的物理内存访问
当我们得到MMU转换后的物理地址后,我们需要到相应的内存空间去寻找相应的数据。从内存直接读取数据太慢,而快的读取快的存储器造价又太高,所以采用cache(高速缓存)来达到节约成本并提升访问速度的目的。CPU取数据的时候先考虑cache里是否有要访问的数据。
在这里插入图片描述

图表 48:cache结构
根据cache的大小,我们把物理地址分成以下部分。其中S=2s,B=2b,剩下的都是标记位。得到物理地址后通过组索引可以知道在cache的哪一组,通过标记位确定是否与组中的某一行的标记相同和是否有效,若找到且有效,则通过块偏移确定是哪个具体的块,从而得到我们的数据。如果没有找到就要到它的下一级存储空间去寻找。在下一级找到后把它加载到cache中。高速缓存有三级:L1、L2、L3(最低),从L1高速缓存到web服务器,每一级都是下一级的缓存。Cache的替换策略有LFU(最不常使用)和LRU(最近最少使用)。
7.6 hello进程fork时的内存映射
Linux通过将虚拟内存区域与磁盘上的对象关联起来以初始化这个虚拟内存区域的内容。这个过程称为内存映射。
虚拟内存和内存映射解释了fork函数如何为每个新进程提供私有的虚拟地址空间。
为新进程创建虚拟内存:
 1.创建当前进程的的mm_struct, vm_area_struct和页表的原样副
本.
 2.两个进程中的每个页面都标记为只读
 3.两个进程中的每个区域结构(vm_area_struct) 都标记为私有的写时复制(COW)
在新进程中返回时,新进程拥有与调用fork进程相同的虚拟内存。
随后的写操作通过写时复制机制创建新页面。
7.7 hello进程execve时的内存映射
当我们fork一个hello进程并创建虚拟内存区域后,会调用execve函数加载并运行hello。
在这里插入图片描述

图表 49
步骤:
1.删除已存在的用户区域
2.创建新的区域结构
 2.1代码和初始化数据映射到.text和.data区(目标文件提供)
 2.2 .bss和栈映射到匿名文件
3.设置PC,指向代码区域的入口点
 Linux根据需要换入代码和数据页面
7.8 缺页故障与缺页中断处理
虚拟内存中,DRAM缓存不命中称为缺页。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

图表 50
以上图组描述了缺页及缺页处理。
缺页处理程序不是直接就替换,它会经过一系列的步骤:
1) 虚拟地址是合法的吗?如果不合法,它就会触发一个段错误;
2) 试图进行的内存访问是否合法?意思就是进程是否有读,写或者执行这个区域的权限;
经过上述判断,这时才能确定这是个合法的虚拟地址,然后才会执行上述的替换。
在这里插入图片描述

图表 51
7.9动态存储分配管理
C程序需要额外的虚拟内存区域的时候,会使用动态内存分配获得。动态内存分配器维持着一个进程的虚拟内存区域,称为堆。分配器将堆视作一组大小不同的块,每个块就是一个连续的虚拟内存块,这些要么是已分配的,要么是空闲的。
使用malloc函数来申请内存块。,他能够返回一个包含size大小的内存块指针,size的不一定是我们申请的大小(一定比我们申请的大),因为数据需要对齐。在32位中返回的地址总是8的倍数,64位中总是16的倍数。
在这里插入图片描述

图表 52
使用free函数把传入的地址对应的块给释放掉,变成空闲块。

一个分配器的设计需要考虑:空闲块的组织;如何放置新块;分割后的空闲块怎么办;怎么合并释放的块。
构造分配器的三种方式:
1.隐式空闲链表
数据结构:
在这里插入图片描述

图表 53
在这种结构下,块与块之间是通过头部的块大小隐式的连接在一起的(包括空闲块和分配块),如果要找到下一个块,只需要在当前块地址上加上块大小即可。
2.显式空闲链表
数据结构:
在这里插入图片描述

图表 54
这种结构比隐式多了两个指针,一个指向前一个空闲块,一个指向后一个空闲块。这样做在查找空闲块时只需要看空闲块即可而不需要看每一个块。(显式的只连接空闲块)。
组织这种链表有两种方式:A)LIFO,最新释放的空闲块放在链表的最前面。B)按照地址大小来维护。
3.分离的空闲链表
这种链表是显示空闲链表的优化,将所有的块按照大小划分为一些等价类,每个等价类都有一个链表,当我们寻找空闲块的时候就只需要到相应的链表里寻找。
在这里插入图片描述

图表 55
这种链表也有一个问题:如果我们分割了一个空闲块,剩下的大小就不在这个范围里了。有两个方法来解决这个问题:a)简单分离适配:处理时不分割(造成较多的碎片);b)分离适配:把块分割,然后再插入相应的空闲链表。

适配策略:

  1. 首次适配:从头搜索的第一个合适的块
  2. 下一次适配:从上一次分配结束的位置开始的首次适配
  3. 最佳适配:检查所有块,选择最适合的那个块
    合并策略:
    1.立即合并:每次释放一个块就合并
  4. 推迟合并:直到分配请求失败,再扫描整个堆进行合并
    Printf会调用malloc,请简述动态内存管理的基本方法与策略。
    7.10本章小结
    在这章中主要介绍了程序的存储结构,通过段式管理在逻辑地址到虚拟地址,页式管理从虚拟地址到物理地址。程序访问过程中的cache结构和页表结构,进程如何加载自己的虚拟内存空间,内存映射和动态内存分配。
    之所以这么设计,是为了加快访问时间的同时节约制造成本。
    (第7章 2分)

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:所有的IO设备都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单低级的应用接口,称为Unix I/O。
设备管理:进程要进行IO操作时,需向操作系统提出IO请求,然后由操作系统根据系统当前的设备使用状况,按照一定的策略,决定对改进程的设备分配。设备的应用领域不同,其物理特性各异,但某些设备之间具有共性,为了简化对设备的管理,可对设备分类,或对同类设备采用相同的管理策略,比如Linux主要将外部IO设备分为字符设备和块设备(又被称为主设备),而同类设备又可能同时存在多个,故而要定位具体设备还需提供“次设备号”。
8.2 简述Unix IO接口及其函数
1)打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。
2)Shell创建的每个进程都有三个打开的文件:标准输入,标准输出和标准错误。
3)改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置a,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置a。
4)读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
5)关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。

打开文件:
在这里插入图片描述

关闭文件:
在这里插入图片描述

读文件:
在这里插入图片描述

写文件:
在这里插入图片描述

8.3 printf的实现分析
https://www.cnblogs.com/pianist/p/3315801.html
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;
}

在形参列表里有这么一个token:“…”,这个是可变形参的一种写法。当传递参数的个数不确定时,就可以用这种方式来表示。很显然,我们需要一种方法,来让函数体可以知道具体调用时参数的个数

再看vsorintf:

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': 
itoa(tmp, *((int*)p_next_arg)); 
strcpy(p, tmp); 
p_next_arg += 4; 
p += strlen(tmp); 
break; 
case 's': 
break; 
default: 
break; 
} 
} 

return (p - buf); 

}
vsprintf返回一个长度,就是我们的字符串长度。Vsprintf的作用就是格式化,它接收确定输出格式的格式字符串fmt,用格式字符串读个数变化的参数进行格式化,产生格式化输出。

write函数:
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
这个地方int要调用中断门来实现特定的系统服务。
int INT_VECTOR_SYS_CALL表示要通过系统来调用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 1190201514 孟子繁”从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。
字符显示驱动子程序将通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中。
显示芯片会按照一定的刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
于是我们的打印字符串“Hello 1190201514 孟子繁”就显示在了屏幕上。

从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
键盘中断的处理过程
当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求。键盘中断服务程序先从键盘接口取得按键的扫描码,然后根据其扫描码判断用户所按的键并作相应的处理,最后通知中断控制器本次中断结束并实现中断返回。
若用户按下双态键(如:Caps Lock、Num Lock和Scroll Lock等),则在键盘上相应LED指示灯的状态将发生改变;
若用户按下控制键(如:Ctrl、Alt和Shift等),则在键盘标志字中设置其标志位;
若用户按下功能键(如:F1、F2、…等),再根据当前是否又按下控制键来确定其系统扫描码,并把其系统扫描码和一个值为0的字节存入键盘缓冲区;
若用户按下字符键(如:A、1、+、…等),此时,再根据当前是否又按下控制键来确定其系统扫描码,并得到该按键所对应的ASCII码,然后把其系统扫描码和ASCII码一起存入键盘缓冲区;
若用户按下功能请求键(如:Print Screen等),则系统直接产生一个具体的动作。

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章主要介绍了Linux的IO设备管理方法、Unix IO接口及其函数。并且详细的分析了printf函数和getchar函数。从键盘输入到在屏幕上输出经历了一个复杂的处理过程。
(第8章1分)
结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
hello所经历的过程:
1、写程序代码(hello.c):写我们的C语言代码实现hello。
2、预处理:将带有“#”的指令解析,生成hello.i文件。
3、编译:将高级语言翻译成汇编语言,生成hello.s文件。
4、汇编:将汇编代码转换成机器代码,产生可重定位信息,生成hello.o文件。
5、链接:与动态库链接,生成可执行文件hello。
6、创建进程:在shell中运行hello程序,fork生成hello进程。
7、加载程序:通过调用execve函数加载hello进程的代码
8、执行指令:CPU取指令,通过给出虚拟地址由MMU转化为物理地址在相应的地址空间取数据。
9、异常处理(信号):运行时产生异常后由内核调度异常处理程序。
10、IO:经过IO设备输出。
11、结束:父进程回收子进程,内核回收占用的资源。

感悟:
在我们写下一行行的代码后,轻松的按下回车就能让他运行起来。但是很少人了解它内部的运行原理。这正是作为一个程序员所欠缺的东西,只有深刻的理解程序在我们的计算机上是怎么一步一步运行的,我们才能在以后的项目工程中避免一些隐含的BUG而减少以后带来的隐患。在这次大作业中初步了解了最简单的hello程序运行的整个过程,其中还有很多的地方都是一笔带过,有许多还不是很理解的地方。还依旧需要日后的深入学习。
(结论0分,缺失 -1分,根据内容酌情加分)

附件
列出所有的中间产物的文件名,并予以说明起作用。
hello 链接后的可执行文件
hello.c hello的C语言文本文件
hello.i 预处理之后的文本文件
hello.o 编译之后的汇编文件
hello.s 汇编之后的可重定位目标文件
hello_elf.txt hello的elf
hello_t.txt hello的反汇编代码
hello_ot.txt hello.o的反汇编代码
hello_oelf.txt hello.o的elf
(附件0分,缺失 -1分)

参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 段页式访存——逻辑地址到线性地址的转换
HTTPS://BLOG.CSDN.NET/PIPCIE/ARTICLE/DETAILS/105670156
[2] 键盘中断的处理过程
HTTPS://BLOG.CSDN.NET/XUMINGJIE1658/ARTICLE/DETAILS/6965176?UTM_SOURCE=BLOGXGWZ0
[3] Linux内核:IO设备的抽象管理方式
HTTPS://BLOG.CSDN.NET/ROGER_RANGER/ARTICLE/DETAILS/78473886
[4] Linux GCC 常用命令
HTTP://WWW.CNBLOGS.COM/GGJUCHENG/ARCHIVE/2011/12/14/2287738.HTML
[5] linux bash总结
HTTP://WWW.CNBLOGS.COM/SKYWANG12345/ARCHIVE/2013/05/30/3106570.HTML.
[6] printf 函数实现的深入剖析
HTTPS://WWW.CNBLOGS.COM/PIANIST/P/3315801.HTML
[7] GOT和PLT原理简析
HTTPS://BLOG.CSDN.NET/SOFTEE/ARTICLE/DETAILS/41256595
[8] TLB的作用及工作过程
HTTPS://WWW.CNBLOGS.COM/ALANTU2018/P/9000777.HTML
(参考文献0分,缺失 -1分)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值