计算机系统大作业

计算机系统

大作业

题 目 程序人生-Hello’s P2P
专 业 计算学部
学   号 120L022026
班   级 2003007
学 生 唐xx  
指 导 教 师 吴锐

计算机科学与技术学院
2021年5月
摘 要
一个简单的Hello程序是所有程序员的出发点。然而,就是这样一个简单的程序同样也涉及到预处理、编译、汇编、链接以及运行时的进程管理、访存、IO管理的诸多过程,本作业将按照Hello程序在计算机中的生命周期过程逐一讨论该程序生成和运行的各个步骤的详细原理,以完成对计算机系统从微观到宏观的全面把握。

关键词:预处理;编译;汇编;链接;进程;虚拟地址;I/O;

目 录

第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的预处理结果解析 - 6 -
2.4 本章小结 - 6 -
第3章 编译 - 7 -
3.1 编译的概念与作用 - 7 -
3.2 在UBUNTU下编译的命令 - 7 -
3.3 HELLO的编译结果解析 - 7 -
3.4 本章小结 - 10 -
第4章 汇编 - 11 -
4.1 汇编的概念与作用 - 11 -
4.2 在UBUNTU下汇编的命令 - 11 -
4.3 可重定位目标ELF格式 - 11 -
4.4 HELLO.O的结果解析 - 14 -
4.5 本章小结 - 16 -
第5章 链接 - 17 -
5.1 链接的概念与作用 - 17 -
5.2 在UBUNTU下链接的命令 - 17 -
5.3 可执行目标文件HELLO的格式 - 17 -
5.4 HELLO的虚拟地址空间 - 19 -
5.5 链接的重定位过程分析 - 21 -
5.6 HELLO的执行流程 - 23 -
5.7 HELLO的动态链接分析 - 24 -
5.8 本章小结 - 25 -
第6章 HELLO进程管理 - 26 -
6.1 进程的概念与作用 - 26 -
6.2 简述壳SHELL-BASH的作用与处理流程 - 26 -
6.3 HELLO的FORK进程创建过程 - 26 -
6.4 HELLO的EXECVE过程 - 26 -
6.5 HELLO的进程执行 - 27 -
6.6 HELLO的异常与信号处理 - 28 -
6.7本章小结 - 30 -
第7章 HELLO的存储管理 - 31 -
7.1 HELLO的存储器地址空间 - 31 -
7.2 INTEL逻辑地址到线性地址的变换-段式管理 - 31 -
7.3 HELLO的线性地址到物理地址的变换-页式管理 - 31 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 32 -
7.5 三级CACHE支持下的物理内存访问 - 33 -
7.6 HELLO进程FORK时的内存映射 - 34 -
7.7 HELLO进程EXECVE时的内存映射 - 34 -
7.8 缺页故障与缺页中断处理 - 35 -
7.9动态存储分配管理 - 35 -
7.10本章小结 - 36 -
第8章 HELLO的IO管理 - 37 -
8.1 LINUX的IO设备管理方法 - 37 -
8.2 简述UNIX IO接口及其函数 - 37 -
8.3 PRINTF的实现分析 - 38 -
8.4 GETCHAR的实现分析 - 38 -
8.5本章小结 - 39 -
结论 - 39 -
附件 - 40 -
参考文献 - 41 -

第1章 概述
1.1 Hello简介
1.1.1 P2P:
P2P,即Program to Process,指通过预处理器将hello.c的源代码预处理为hello.i文件、再经过编译器生成hello.s的汇编语言代码,而后通过汇编器生成可重定位目标文件hello.o,最后由链接器生成可执行文件hello。最后shell通过fork函数和execve函数可将hello装载到内存中作为进程(Process)运行的过程。
1.1.2 020:
020,即Zero-0 to Zero-0,指shell调用execve函数执行hello后,内核为其映射虚拟内存并开始运行,随后,基于软件和硬件的一系列机制如进程的上下文切换、虚拟内存映射,内核的信号处理机制等将确保hello的正确执行,待hello进程结束运行后,shell进程再将其回收的过程。

1.2 环境与工具
硬件环境:Intel i7 10510U
软件环境:Win10;Ubuntu20.02
开发工具: Codeblocks;objdump;Gdb; EDB
1.3 中间结果
hello.c: 源代码
hello.i: 预处理后的文本文件
hello.s: 编译之后的汇编文件
hello.o: 汇编之后的可重定位目标执行文件
hello: 链接之后的可执行文件
hello.o.elf: hello.o的ELF格式
hello.elf: hello的ELF格式
hello.o.txt: hello.o反汇编代码
hello.txt: hello的反汇编代码

1.4 本章小结
本章介绍了P2P和O2O的概念,概述了hello程序的生命周期,记录了本次作的环境和工具,记录了作业过程中生成的中间文件。

第2章 预处理
2.1 预处理的概念与作用
概念:
由预处理器对程序源代码文本进行处理,在c/c++中体现为根据#if/#ifdef/#ifndef/#else/#elif/#endif(条件编译)、#define(宏定义)、#include(源文件包含)等#开头的指令对代码进行分割、替换等的处理后生成.i文件。

作用:
1.条件编译,实现部分代码在某些条件下的选择性编译。
2.宏定义,在预处理阶段用定义的实际数值将宏替换。
3. 实现头文件引用,将头文件的内容复制到源程序中以实现引用。
4. 删除注释。
5. 行控制、错误指令、和实现相关的杂注以及单独的#(空指令)。

2.2在Ubuntu下预处理的命令

预处理命令:gcc hello.c -E -o hello.i
在这里插入图片描述
图2.1

2.3 Hello的预处理结果解析
在这里插入图片描述
图2.2
可见hello.i比hello.c大了很多,达到了3060行,原因是预处理器将stdio.h等源文件全部复制到了hello源文件的开头。此外,.i文件还删除了注释
在这里插入图片描述
图2.3
滑倒.i文件的末尾发现与.c文件一致.
2.4 本章小结
本章介绍了预处理的概念与作用,展示了Linux下对.c文件的预处理命令,分析了预处理后.i文件相较.c文件的变化。

第3章 编译
3.1 编译的概念与作用
概念:
编译器将c语言文本文件的.i文件翻译为汇编语言文本文件.s文件的过程
作用:
通过词法分析,语法分析以及代码优化(-O2等)的机制最终生成汇编语言目标代码,为后续的汇编和链接做准备
3.2 在Ubuntu下编译的命令

编译命令: gcc -S hello.i -o hello.s
在这里插入图片描述
图3.1
3.3 Hello的编译结果解析

3.3.1 字符串
hello.c的语句:” printf(“用法: Hello 学号 姓名 秒数!\n”);”和” printf(“Hello %s %s\n”,argv[1],argv[2]);” 隐性地定义了两个字符串常量,储存在图3.2只读数据段:
在这里插入图片描述
3.3.2 main函数中传递的参数
main函数中传递的参数argc和argv初始时被存放于寄存器edi和rsi中,然后将他们储存在栈中.

图3.3
在这里插入图片描述

3.3.3 局部变量
hello.c中声明的局部变量i储存在栈中.
在这里插入图片描述
图3.4
3.3.4 关系操作
hello.c的两个关系操作”argc!=”和”i<8”分别使用cmpl命令实现,为紧随其后的条件跳转指令做了准备:
cmpl $4, -20(%rbp)
cmpl $7, -4(%rbp)

3.3.5 赋值
.c文件中的”i = 0”赋值语句在汇编中是通过在栈中压入立即数实现的,即赋了初值为0
在这里插入图片描述
图3.5
3.3.6 控制转移
.c文件中的if和for循环语句都涉及到控制转移,在.s文件中分别对应两个条件跳转语句
cmpl $4, -20(%rbp)
je .L2
cmpl $7, -4(%rbp)
jle .L4
其中,第二条语句将被多次执行,实现循环.

3.3.7 类型转换
.c文件中的atoi(argv[3])语句将字符串显示地转换为int类型,这在.s文件中是通过调用atoi函数实现的.
call atoi@PLT

3.3.8 函数调用
.c文件中调用了五个函数分别是 printf()、exit()、sleep()、atoi()和getchar(),在.s文件中都是通过先传递参数至寄存器(可省略)再使用call命令实现的:
printf()的两次调用采用了不同的函数:
call puts@PLT
call printf@PLT
movl $1, %edi
call exit@PLT
movl %eax, %edi
call sleep@PLT
movq %rax, %rdi
call atoi@PLT
call getchar@PLT
调用后,其返回值会存放在rax(eax)寄存器内,如:
call atoi@PLT
movl %eax, %edi
call sleep@PLT
Sleep函数就使用了atoi函数存放在eax中的结果.
此外,除exit函数以外,函数的返回使用了ret语句,在.s的59行,main函数就使用了ret指令返回.
3.3.9 数组/指针操作
Main函数的argv参数既是指针,也代表一个数组,访问组内元素是通过数组首地址加偏移量的方式,如访问argv[3]:
在这里插入图片描述
图3.6
解析:32位运行环境下,每个指针4字节, %rbp-32处存放首地址也就是argv[0],由于连续存放,argv[3]存放于%rbp-20处.
3.3.10 算数操作
.c文件for循环语句中的i++在.s文件中通过add命令实现:
addl $1, -4(%rbp)
3.4 本章小结
本章介绍了.i文件如何被编译为汇编代码。解释了c语言中各数据与操作是如何被翻译为汇编语言的。

第4章 汇编
4.1 汇编的概念与作用
概念:
汇编是把汇编语言翻译成机器语言的过程,得到的.o文件被称为可重定位目标文件。
作用:
汇编的作用是把汇编语言文件转换成机器可以读取分析的二进制机器语言文件即.o文件, 为后续的链接做准备。
4.2 在Ubuntu下汇编的命令
命令: gcc -c -o hello.o hello.s
在这里插入图片描述
图4.1
4.3 可重定位目标elf格式
4.3.1 获取ELF的指令:
readelf -a hello.o > elf.txt
4.3.2 ELF格式
在这里插入图片描述
在这里插入图片描述

图4.2、4.3

4.3.3 ELF头
在这里插入图片描述

图4.4
内容解析:
Magic魔数用于检测文件类型,文件类别是ELF64,数据为二进制补码表示,小端序字节顺序,目标文件的类型为REL可重定位文件,机器类型为X86-64,没有程序头表,节头部表开始位置是1240字节处, ELF头大小为64字节,节头部表大小为64字节、节的数量为14

4.3.4 节头表
在这里插入图片描述
图4.5节头表
内容解析:
节头表记录了.o文件中每个节的名称、类型、地址、偏移量、大小、总大小、读写权限(flags)、链接、信息以及对齐方式信息。另外, 由于可重定位目标文件还未链接,每个节未被分配地址故地址信息都为0。

4.4.5 重定位节
在这里插入图片描述
图4.6重定位节
内容解析:
.rela.text节中存放的是.text节的所有重定向信息,程序中任何对外部函数或全局变量的引用在链接过程中都需要被重定向,而对于每一个符号的重定位,都需要到重定位节中找到相对应的重定位条目。每个重定位条目包含如下信息:符号名称、节内偏移、重定位类型和addend值。本程序中需要被重定位的是printf、puts、exit、sleepsecs、getchar、sleep等函数和.rodata中的两个字符串。
注意到:.rodate中两个隐性定义的全局字符串没在.rela.date节中,而被放在了.rela.text节中,是因为它们是只读数据。

4.4.6 符号表
在这里插入图片描述
图4.7符号表
内容解析:
符号表中存放着程序定义和引用的函数和变量的符号。每个表项记录了一个符号的偏移量(Value)、大小、类型、Bind说明是局部或全局变量、名称等信息。上图中,11号符号以下是外部符号,故类型为NOTYPE,由于未链接,所有偏移量都是0

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

4.4 Hello.o的结果解析
(以下格式自行编排,编辑时删除)
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
hello.o的反汇编结果如下:
在这里插入图片描述

图4.8 hello.o的反汇编结果
对比分析:

  1. 汇编语言中,代码的上下文定位是通过.LC0等标识符完成的,反汇编代码中,所有指令得到了具体的地址。
  2. 汇编代码中,分支跳转的目标是以.L2等标识符代表的代码段的开头,反汇编代码中,使用相对地址进行分支跳转。
  3. 汇编代码中,对外部函数的调用只使用了该函数的名称(符号),反汇编代码中,除了标识了call指令返回的下一条指令,还留下了重定位条目信息等待链接器补全被调用函数的地址。
  4. 汇编代码中使用.LC0(%rip)访问全局变量,反汇编代码中使用0x0(%rip),等待链接器通过重定位条目补全地址。
    说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
    4.5 本章小结
    本章总结了汇编的概念和作用,分析了ELF文件的内容,通过反汇编解析了.o文件,并比较了汇编代码和反汇编代码的差别,了解了从汇编语言与机器语言的映射关系。

第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.1
5.3 可执行目标文件hello的格式
获取ELF文件:readelf -a hello > helloelf.txt
图5.2 hello的ELF格式:
在这里插入图片描述

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

图5.4 节和段的映射关系如下:

在这里插入图片描述

图5.5 段头表(程序头表):

在这里插入图片描述

其各段的起始地址,大小等信息见上图。

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

5.4 hello的虚拟地址空间
使用edb加载hell0:
在这里插入图片描述
图5.6
5.3中LOAD段表示可加载的程序和数据段,可见,第一个load段的起始地址是0x400000,在edb中定位到虚拟地址为0x400000处:
在这里插入图片描述
图5.7
通过gdb测试可见,确实起始于0x400000处,ELF文件开始于0x400000处。
ELF头中显示程序起始地址为0x401f0,这也是.text节的起始位置,在gdb测试中得到验证:
在这里插入图片描述
图5.8
.rodata只读数据节开始于0x402000
在这里插入图片描述
图5.9 只读数据节
.dynamic段保存了动态链接器相关的信息,开始于0x403e50处,在Data Dump中可看到动态链接相关信息
在这里插入图片描述
图5.10 .dynamic段
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
5.5 链接的重定位过程分析
objdump后得到的结果较长,main函数如下:
在这里插入图片描述
图5.11 反汇编后的main函数
hello与hello.o的不同:

  1. hello中的每句指令都分配了具体的虚拟地址

  2. hello中对printf等外部函数的调用都已有了具体的寻址,也就是说,经过链接器的作用,外部函数都已被加入了hello程序中(通过动态链接)

  3. hello相比hello.o多了一些节和函数,例如.ini和.plt和.plt.sec等
    举例而言,.plt和.plt.sec节用于调用动态链接库中的printf等函数:
    在这里插入图片描述

    					图5.12
    

    链接和重定位过程:

  4. 符号解析:链接器将每一个对符号的引用与一个具体的符号定义相联系

  5. 重定位:1.将多个代码段与数据段分别合并为一个单独的代码段和数据段2.计算每个定义的符号在虚拟地址空间中的绝对地址3. 将可执行文件中符号引用处的地址修改为重定位后的地址信息
    举例说明:
    对printf的重定位:

依赖重定位条目:

并已确定refaddr = 0x401125+0x5e=0x401183
在这里插入图片描述
图5.13
和ADDR(r.symbol)=0x4010a0
在这里插入图片描述
图5.14

可见采用相对寻址,具体计算如下:
(unsigned)(ADDR(r.symbol)+r.addend-refaddr)=0xFFFFFF19(-231)
经验证,结果正确: 在这里插入图片描述

						图5.15

objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
5.6 hello的执行流程
正常输入参数的情况:
ld-2.27.so!_dl_start 0x00007f962ba38df0
ld-2.27.so!_dl_init 0x00007f962ba48c10
hello!_start 0x4010f0
libc-2.31.so!_libc_start_main 0x00007ff16f8d3fc0
libc-2.31.so!_setjump 0x7fc983680cb0
hello!main 0x401125
printf@plt 0x4010a0
atoi@plt 0x4010c0
sleep@plt 0x4010e0
循环之后:
getchar@plt 0x4010b0

使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
5.7 Hello的动态链接分析
动态链接库是包含代码和数据的目标文件,在它们的加载时或运行时,被动态地加载并链接到应用程序中。
动态的链接器在正常工作时链接器采取了延迟绑定的链接器策略,由于静态的编译器本身无法准确预测变量和函数的绝对运行时地址,动态的链接器需要等待编译器在程序开始加载时再对编译器进行延迟解析。
延迟绑定是通过GOT和PLT实现的。GOT是数据段的一部分,而PLT是代码段的一部分。两表内容分别为:
PLT:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。
GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[0]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。
.got和. got.plt节的位置分别位于0x403ff0和0x40400:
在这里插入图片描述

								图5.16

初始时,0x403ff0后的16字节都为0,plt[1]和plt[2]都是0
在这里插入图片描述

								图5.17

在dl_init后:
在这里插入图片描述

								图5.18

可见,在dl_init后.got和. got.plt节的内容都有所改变,其中,PLT数组的改变为外部函数提供具体的执行入口。对外部函数的调用都是先跳到PLT数组的对应项,在根据其内容跳转到具体的共享库中的函数入口。
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
5.8 本章小结
本章分析了链接的概念与作用,分析了链接的重定位过程、hello的虚拟地址空间等内容,正式将hello源文件变为了可执行文件。

第6章 hello进程管理
6.1 进程的概念与作用
概念:
进程是一个执行中程序的实例,进程是OS对CPU执行的程序的运行过程的一种抽象。进程有自己的生命周期,它由于任务的启动而创建,随着任务的完成(或终止)而消亡,它所占用的资源也随着进程的终止而释放。
进程是具有独立功能的一个程序关于某个数据集合的一次运行活动,因而进程具有动态含义。同一个程序处理不同的数据就是不同的进程。
作用:
1.使得程序好像是独占的使用处理器和内存。
2.使得处理器好像是无间断的执行该程序中的指令
6.2 简述壳Shell-bash的作用与处理流程
作用:
Shell 通过提示您输入,向操作系统解释该输入,然后处理来自操作系统的任何结果输出,简单来说Shell就是一个用户跟操作系统之间的一个命令解释器。

处理流程:
shell首先检查第一个命令参数判断是否是内部命令,如果是内部指令则立即执行,若不是则默认该命令打开一个可执行程序,并尝试创建新的子进程。如果找不到该可执行文件会提示没有该命令,并提示用户继续输入。同时shell还允许接收从键盘读入的外部信号,如ctrl+c等,并根据不同信号的功能进行对应的处理。

6.3 Hello的fork进程创建过程
一、用户输入命令,如:./hello tyt 120L022026
二、shell首先检查第一个命令参数判断是否是内部命令
三、判断不是内部命令,则调用fork()函数创建新的,与父进程除PID外完全相同的子进程,包括代码段、段、数据段、共享库以及用户栈。
四、父进程默认等待子进程执行完成。

6.4 Hello的execve过程
fork创建子进程后,子进程内部会根据PID的不同实现分支跳转(if语句)到父进程未执行的一段代码,这段代码中会调用exceve函数,该函数会覆盖原有的代码段、数据段、共享库以及用户栈等,在当前子进程的上下文加载并运行一个新的程序即hello程序。exceve函数执行一次从不返回。

具体流程:

  1. execve调用内存中的启动加载器执行Hello程序

  2. 删除当前进程虚拟地址的用户部分中已存在的内容

  3. 为hello程序在虚拟地址空间中映射新的.bss .date .text等私有区域和共享库等。

  4. 最后将程序计数器指向hello程序的起始位置(_start),开始执行hello的代码
    6.5 Hello的进程执行
    1.上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由 通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内 核数据结构等对象的值构成。

  5. 时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
    3.上下文切换:
    在这里插入图片描述

     					    图6.1 上下文切换
    

上面已提到过,进程会提供两个重要抽象:1.使得程序好像是独占的使用处理器和内存。2.使得处理器好像是无间断的执行该程序中的指令,而事实上,hello的进程执行过程并非是连续不断的,中间会涉及到中断和上下文切换。
具体来说,用户输入指令后,shell会将hello程序默认设为前台进程并等待它结束,当hello执行到sleep语句时计时器开始计时并进行上下文切换(进入内核模式再由内核调度执行另一程序),计时结束后会发送中断信号,再次经过内核调度上下文切换回hello进程。调用getchar函数也是同理。当hello进程正常终止后内核会发送sigchild信号给shell,由shell处理。

结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
6.6 hello的异常与信号处理
可能会出现的异常:

  1. 异步异常(中断):
    定时器中断:
    定时器芯片每隔几毫秒触发一次中断
    内核从用户程序取回控制权
    外部设备的I/O中断:
    键盘上敲击一个 Ctrl-C
    2.陷阱:陷阱是执行一条指令的主动的结果,会在hello执行sleep时出现。
    3.缺页故障:需要用到的数据指令等未载入内存
    4.终止:非故意,不可恢复的致命错误造成如:非法指令,奇偶校验错误,机器检查。

可产生的信号:SIGINT,SIGSTP,SIGCONT,SIGSEGV,SIGCHLD等
处理:
SIGINT(来自键盘的ctrl+c):终止hello
SIGSTP(来自键盘的ctrl+z):停止hello
SIGCONT:继续执行hello
SIGSEGV:处理页故障,即加载还未加载到内存中的内容
SIGCHLD:hello结束或停止或终止时由内核发送给shell,由shell处理

1.正常执行:
在这里插入图片描述

						    图6.2
  1. 输入Ctrl-C:内核会发送一个SIGINT信号到前台进程组的每个进程,默认终止前台作业,即hello程序
    在这里插入图片描述

    					    图6.3
    
  2. 输入ctrl-z:内核会发送一个SIGSTP信号到前台进程组的每个进程,默认停止前台作业,即hello程序
    在这里插入图片描述

    					    图6.4
    

再输入ps jobs pstree fg kill 等命令:
Ps jobs pstree:
在这里插入图片描述

						    图6.5

fg同时其中也有乱按键盘:
输入fg后内核发送SIGCONT给hello使之继续执行。
乱按输入的字符被缓存到 stdin,不会立刻对程序造成影响。当运行到getchar函数时,立刻从是stdin中读入一个字符并继续运行。
在这里插入图片描述

						    图6.6

kill:如图我使用kill命令发送了9号SIGKILL信号给hello使之强制终止
在这里插入图片描述

						    图6.7

6.7本章小结
本章介绍了进程的定义与作用,以hello为例介绍了 Shell 的一般处理流程和作用,分析了调用 fork和execve函数的过程。在执行hello的过程中,尝试了使用shell的各种指令以测试分析异常和信号处理机制。

第7章 hello的存储管理
7.1 hello的存储器地址空间

  1. 逻辑地址:是由编译器生成的,由选择符和偏移量组成,出现在汇编代码中的段内偏移地址,是一种相对地址,在hello的反汇编代码中可找到形如:

2.线性地址:对现代计算机而言,线性地址等同于虚拟地址。
3.虚拟地址:虚拟地址是逻辑地址计算后的结果,通过页表和MMU翻译形成对物理地址的映射。每个进程都被视为有一个独立的虚拟地址空间,因此虚拟地址也就是线性的。通过对可执行文件的逆向工程可得到基于虚拟地址寻址的代码:
在这里插入图片描述

						    图7.1
  1. 物理地址:虚拟地址经地址翻译得到的结果。每个物理地址唯一地对应一个内存里实际存放的字节

结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址由段标识符和段内偏移量组成。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号,后面3位包含一些硬件细节。
首先对于一个给定的逻辑地址,看其段选择符的T1=0还是=1,判断需要转换的是GDT中的段还是LDT中的段,再根据相应的寄存器得到地址和大小,获得一个段描述符表。拿出段选择符中的前13位,可以在这个数组中查找到对应的段描述符,这样就有了Base,即基地址就知道了。把基地址Base+Offset,就得到了对应的线性地址
7.3 Hello的线性地址到物理地址的变换-页式管理
概念上而言,虚拟内存被组织为一个由存放在磁盘上的N个连续字节大小的单元组成的数组。磁盘上数组的内容被缓存在物理内存中 (DRAM cache),这些内存块被称为页,每个页面的大小标准 4 KB, 有时可以达到 4 MB。
页表是一个页表条目的数组,储存某一虚拟页地址对应的物理页地址。
将一个虚拟地址转换为物理地址的地址翻译过程如下(不考虑TLB):

  1. 根据 PTBR页表基址寄存器,找到当前进程的物理页表地址。

  2. 根据该虚拟地址的虚拟页号 (VPN),在页表中找到对应的表项

  3. 如果页表项的有效位 = 1,对应的物理页已被缓存进内存,则将该页表项的物理页号 (PPN)取出传送给MMU, MMU将虚拟地址的虚拟页偏移量 (VPO)部分连接在PPN后,就得到了对应的物理地址。

  4. 如果页表项的有效位 = 0,对应的物理页未被缓存进内存或未分配,MMU 触发缺页异常,如果内存已满,缺页处理程序将确定物理内存中牺牲页,然后缺页处理程序调入新的页面,并更新内存中的PTE,最后缺页处理程序返回到原来进程,再次执行导致缺页的指令。
    整个过程如图:
    在这里插入图片描述

    					    图7.2地址翻译过程
    

实际应用中,在访问页表前还会查找翻译后备缓冲器(TLB),也就是页表的缓存中是否有所需的PPN,这一部件大大提高了地址翻译的效率。

7.4 TLB与四级页表支持下的VA到PA的变换
由于虚拟地址空间往往很大,所需的页表条目过多,实际使用中采用多级页表的方式存储所需的页表项。
以二级页表为例,一级页表(常驻内存)的每个 PTE 指向一个页表,二级页表的每个 PTE 指向一页。多级页表也是同理。
在这里插入图片描述

						    图7.3 多级页表
而现在常用的48位的虚拟地址往往需要四级页表进行VA到PA的变换。48位地址的后12位(2^12=4096=4kb)作为页内偏移量,前36位被分为4个9位的地址,分别用于1-4级页表中查找对应的页表项。其中,1-3级页表指向的是下一级页表的地址,4级页表存储对应的PPN。最后再按照7.3中介绍的方法可完成地址翻译。

7.5 三级Cache支持下的物理内存访问
Intel Core i7的三级Cache示意图:
在这里插入图片描述

						    图7.4

本节以对一级Cache的访问为例解析Cache的工作原理,二三级Cache同理。
以图中的硬件结构为例:
在这里插入图片描述

						    图7.5
  1. 首先48位的虚拟地址通过7.4中介绍的方法被翻译位52位的物理地址。

  2. 将物理地址分为6位(每块内大小为64B)的CO(缓冲块内的字节偏移量) 6位(64组)的 CI( Cache 索引)和40位的CT( Cache 标记)。

  3. 通过组索引在L1中搜索组,再通过标记位匹配。如果匹配成功且有效位是1,cache命中,块偏移指向的块中的内容交给CPU

  4. 如果未命中,在下一级Cache中在重复上述操作,但每级cache对物理地址的划分都会依据硬件结构有所不同

  5. 如果全部未命中,则直接到内存对应的地址中寻找数据

  6. 找到数据后将内容写回L1,如果L1中没有空闲块,通常采用LRU算法选则牺牲块。
    7.6 hello进程fork时的内存映射
    输入指令后shell为hello进程创建虚拟内存,并为其分配唯一的PID

  7. 创建当前进程的的mm_struct, vm_area_struct和页表的原样副本.

  8. 两个进程中的每个页面都标记为只读

  9. 两个进程中的每个区域结构(vm_area_struct)都标记为私有的写时复制(COW)
    7.7 hello进程execve时的内存映射
    execv函数在当前进程中加载并运行hello程序,用hello程序有效的替代了当前程序,即fork出来的shell的子进程步骤如下

  10. execve调用内存中的启动加载器执行Hello程序

  11. 删除当前进程虚拟地址的用户部分中已存在的内容

  12. 为hello程序在虚拟地址空间中映射新的.bss .date .text等私有区域和共享区域等。

  13. 最后将程序计数器指向hello程序的起始位置(_start),开始执行hello的代码
    7.8 缺页故障与缺页中断处理
    Page fault缺页故障: 虚拟内存中的字不在物理内存中 (DRAM 缓存不命中)
    在这里插入图片描述

    					    图7.6
    

缺页中断处理:

  1. 虚拟地址对应的页表项的有效位= 0,对应的物理页未被缓存进内存或未分配,MMU触发缺页异常。
  2. 如果物理页未分配,缺页处理程序会抛出段错误并终止该程序。
  3. 如果访问内存的方式不合法,缺页处理程序会触发保护异常,并终止该程序
  4. 如果物理页未缓存,需要在内存中加入新的页面并更新页表。如果内存已满,则选择一个未被修改过的牺牲页,换入新的页面并更新页表。
    7.9动态存储分配管理
    动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
    分配器分为显式分配器和隐式分配器。
    显式分配器:要求应用显式地释放任何已分配的块。mallo函数就是一种显式分配器
    隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。

堆中的块主要有两种策略:
1.隐式空闲链表(带边界标记)
隐式空闲链表中,一个块是由一个字的头部、有效载荷以及可能的一些额外的填充组成的。其结构如下图所示。这种结构下块与块之间是通过头部的块大小隐式连接在一起,因此只需用当前块地址加上块大小即可找到下一个块。
在这里插入图片描述

						    图7.7 隐式空闲链表

2.显式空闲链表
在未分配的块中添加两个指针,分别指向前一个空闲块和后一个空闲块。采用该策略,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。
Printf会调用malloc,请简述动态内存管理的基本方法与策略。
7.10本章小结
本章介绍了现代计算机以虚拟内存这个概念为核心的内存的分配和存储管理的方式,介绍了从逻辑地址到虚拟地址到物理地址的转换过程。分析了程序访问过程中cache和页表和TLB等部件的结构和功能,讨论了如何进行内存映射和动态内存分配等问题。

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:所有IO设备都被模型化为文件,所有的输入和输出都能被当做相应文件的读和写来执行。
设备管理:unix io接口的作用是使所有输入输出都能以统一且一致的方式执行。
8.2 简述Unix IO接口及其函数
Unix I/O接口规范:

  1. 打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。

  2. Shell创建的每个进程都有三个打开的文件:标准输入,标准输出,标准错误。

  3. 改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。

  4. 读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k

  5. 关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。
    相关函数:

1. int open(char* filename,int flags,mode_t mode) ,进程通过调用open函数来打开一个存在的文件或是创建一个新文件。

  1. int close(fd),fd是需要关闭的文件的描述符,close返回操作结果。

  2. ssize_t read(int fd,void *buf,size_t n),read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。

  3. ssize_t wirte(int fd,const void *buf,size_t n),write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。
    8.3 printf的实现分析
    printf函数的函数体:
    在这里插入图片描述

     			 图8.1 printf函数的函数体
    

其中传递参数中的…表示不确定个数。
函数中的va_list实际上就是typedef后的char*。而va_list arg = (va_list)((char*)(&fmt) + 4);这句操作实际上就是得到了…中的第一个量。
之后我们调用vsprintf函数。vsprintf函数将我们需要输出的字符串格式化并把内容存放在buf中。并返回要输出的字符个数i。然后调用系统函数write来在屏幕上打印buf中的前i个字符,也就是我们要输出的格式串。
调用write系统函数后,程序进入到陷阱,系统调用 int 0x80或syscall等。
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
异步异常-键盘中断的处理:在用户敲击键盘,会产生一个中断的请求,并触发中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章介绍了Linux的I/O管理和操作,以及两个I/O函数printf和getchar的具体实现方法。让我体会了所有IO设备都是文件这一绝妙的思想。
结论
我认为hello的一生分为两个阶段:从代码到可执行文件和从可执行文件到进程再到结束运行。
第一阶段下,我们使用cpp进行对hello.c进行预处理,生成.i文件。然后用cc1编译器进行编译,.i文件翻译成.s汇编文件。之后通过as汇编器进行汇编,将汇编文件生成.o可重定位文件。最后通过链接将其与库文件链接并进行重定位生成可执行文件hello。
第二阶段下,我们在shell键入“./hello 参数1 参数2”的命令,由shell通过fork为hello创建子进程。并为其分配一段虚拟内存,再通过execve执行hello函数。这样hello就成为了运行中的进程,在开始运行时,会产生缺页异常,从而将hello程序所需要使用的信息交换到主存,并为其分配物理地址。在hello的运行过程中,还涉及到地址翻译,信号处理,内存管理,IO操作等一系列问题。最后,当hello结束运行后后会被shell回收并从主存中删除与其相关的所有数据。到此,hello的一生也宣告完结
我认为对操作系统的全方位把握需要从微观到宏观,从抽象到具体全方位地理解操作系统。在学习过程中,我不断惊异于操作系统精妙的抽象概念和细致的具体实现之间的结合。

附件
hello.c: 源代码
hello.i: 预处理后的文本文件
hello.s: 编译之后的汇编文件
hello.o: 汇编之后的可重定位目标执行文件
hello: 链接之后的可执行文件
hello.o.elf: hello.o的ELF格式
hello.elf: hello的ELF格式
hello.o.txt: hello.o反汇编代码
hello.txt: hello的反汇编代码

参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] https://blog.csdn.net/gdj0001/article/details/80135196
[2] https://www.cnblogs.com/pianist/p/3315801.html
[3] 《深入理解计算机系统第三版》

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值