CSAP大作业,Hello的一生
摘 要
摘要是论文内容的高度概括,应具有独立性和自含性,即不阅读论文的全文,就能获得必要的信息。摘要应包括本论文的目的、主要内容、方法、成果及其理论与实际意义。摘要中不宜使用公式、结构式、图表和非公知公用的符号与术语,不标注引用文献编号,同时避免将摘要写成目录式的内容介绍。
关键词:机器级编程,链接,进程,信号,虚拟内存.
此论文旨在阐述hello程序从创建到运行再到停止的整个过程,涉及系统层面与应用层面。主要内容有编译,链接,进程与虚拟内存,通过edb工具来观察程序运行的过程,最终追踪程序运行的轨迹.其理论意义在于给人们一个对hello程序深入理解的机会,有利于重新认识hello程序。
目 录
第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简介
程序员编辑好hello.c文件通过对其编译预处理,编译,汇编,链接得到可执行文件。内核通过加载器将可执行文件加载进内存并fork一个进程去执行它,最终得到结果.
1.2 环境与工具
软件环境:win10,Ubuntu 19.04
硬件环境:corei5-8300H的CPU,16G内存
工具:EDB,GDB,GCC,GNU
1.3 中间结果
hello.i:编译预处理得到的文件
hello.s:编译hello.i得到的文件
hello.ss:反汇编hello.o得到的文件
hello.sss:反汇编hello得到的文件
hello.o:可重定位目标文件
helloelf1:hello.o的elf
helloelf2:hello的elf。
1.4 本章小结
本章为一个概述
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如#include<stdio.h>命令高速预处理器读取系统头文件stdio.h的内容,并把它直接插入到程序文本中,还有#define 宏替换,用后面的去替换前面的,结果就得到了另一个C程序,通常以.i作为文件扩展名.
预处理能够处理的指令如下:
#if
#ifdef
#ifndef
#else #elif
#endif
#define
#undef
#line
#error
#pragma
#include
且.c文件里的所有注释,在.i文件里面都会被删除.
2.2在Ubuntu下预处理的命令
gcc -E hello.c -o hello.i
2.3 Hello的预处理结果解析
在预处理之后,<stdio.h>里面的代码全部被复制到源文件中,并且#include<stdio.h>这条语句也没有了。说明编译预处理将stdio.h里面的东西原封不动地替换了#inlcude<stdio.h>
2.4 本章小结
编译预处理是编译之前的预处理,将#开头的预处命令替换掉,得到的执行结果直接反映在源文件中,得到的文件一般以.i作为后缀。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它是用汇编语言写的。
汇编器包含一个汇编语言程序,该程序包含main的定义。汇编语言为不同的高级语言的不同编译器提供了通用的输出语言,其重要性不言而喻。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
3.3.1全局变量:已初始化的sleepsecs,放在.data段,注意到sleepsecs的大小为4字节,2.5被隐式地转化成了int(2).
3.3.2局部变量:argc,argv,i 放在栈里面
3.3.3字符串常量:“Hello %s %s\n”和“Usage: Hello \345\255\246\345\217\267 \345\247\223\345\220\215\357\274\201”放在.String 段
3.3.4下面是对数/指针argv[]的操作,其中红色框起来的部分表示对argv[1]和argv[2]的定位,-32(%rbp)里面存的是argv,而argv指向argv[0],argv[0]+8即为agrv[1].
argv[0]+16即为argv[2]。
3.3.4下面是对操作的分析
push 将一个变量压入栈,
movq %rsp, %rbp movq 将左边的赋值给右边的,是8字节的赋值.
subq $32, %rsp 将%rsp减去32再赋值给%rsp
cmpl $3, -20(%rbp) 比较右边的和左边的,本质是右边减去左边并设置符号位
je .L2 条件跳转,如果符号位为0则跳转到.L2处,一般此指令前面会有cmp指令.
leaq .LC0(%rip), %rdi 将左边的有效地址赋值给右边,一般而言是一种对leaq的灵活使用,目的是将 %rip+.LC0赋值给%rdi
call puts@PLT 此为函数调用
jmp .L3 无条件跳转
addq $16, %rax 加法操作,将右边的加上左边的再赋值给右边
jle .L4 条件跳转,当符号位小于等于0的时候跳转
ret 返回,即return
movl %edi, -20(%rbp) 可知argc被存放到-20(%rbp)当中
movl $0, -4(%rbp) i 被存放在-4(%rbp)当中
.L4:
movq -32(%rbp), %rax
addq $16, %rax
movq (%rax), %rdx
movq -32(%rbp), %rax
addq $8, %rax
movq (%rax), %rax
movq %rax, %rsi
leaq .LC1(%rip), %rdi
movl $0, %eax
call printf@PLT
movl sleepsecs(%rip), %eax
movl %eax, %edi
call sleep@PLT
addl $1, -4(%rbp)
.L3:
cmpl $9, -4(%rbp)
jle .L4 这里就是循环10次输出学号,姓名的控制程序
3.4 本章小结
本章主要围绕程序的机器级表示展开,从汇编层面来理解C程序,让我们初步了解了汇编语言.
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并把结果保存在目标文件hello.o当中。hello.o文件是一个二进制文件.
4.2 在Ubuntu下汇编的命令
gcc -c hello.s -o hello.o
4.3 可重定位目标elf格式
ELF文件头:存储所有段头部的信息,段头部是描述段信息的元数据,可以通过段头部来获取段信息。
.shstrtab段。存储所有的段的段名。通过ELF文件头,可以实现段的遍历,而无法识别具体的段(不同段类型可以相同),因此需要用段名来确定段。
.dynsym段:动态符号表,存储与动态链接相关的导入导出符号,不包括模块内部的符号。
.dynstr段:存储.dynsym段符号对应的符号名。
.symtab段:符号表。存储在程序中被定义和引用的函数和全局变量的信息。
.strtab段:存储.symtab段符号对应的符号名。
.comment段:程序相关信息,用与定位符号地址。
重定位节 ‘.rela.text’ at offset 0x338 contains 8 entries:
偏移量 信息 类型 符号值 符号名称 + 加数
000000000018 000500000002 R_X86_64_PC32 0000000000000000 .rodata - 4
00000000001d 000c00000004 R_X86_64_PLT32 0000000000000000 puts - 4
000000000027 000d00000004 R_X86_64_PLT32 0000000000000000 exit - 4
000000000050 000500000002 R_X86_64_PC32 0000000000000000 .rodata + 1a
00000000005a 000e00000004 R_X86_64_PLT32 0000000000000000 printf - 4
000000000060 000900000002 R_X86_64_PC32 0000000000000000 sleepsecs - 4
000000000067 000f00000004 R_X86_64_PLT32 0000000000000000 sleep - 4
000000000076 001000000004 R_X86_64_PLT32 0000000000000000 getchar – 4
在只读代码段,偏移量为0x338有8个条目,每个条目包含其相对于该节的偏移量,信息,类型,符号值,和符号名称与加数.
重定位节 ‘.rela.eh_frame’ at offset 0x3f8 contains 1 entry:
偏移量 信息 类型 符号值 符号名称 + 加数
000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text + 0
4.4 Hello.o的结果解析
反汇编得到的.s文件里面有机器指令,且一条机器指令对应一条汇编指令,每一行指令前都有索引,且立即数是2进制的.没有.LFB6,.L2,.L4等助记符.而编译得到的.s文件里面没有机器指令,索引,有.LFB6,.L2,.L4等.且立即数是10进制的.在分支转移方面汇编指令通过助记符来完成,而机器指令则直接通过地址来完成.
机器指令通常由操作码和操作数两部分组成,操作码指出该指令所要完成的操作,即指令的功能,操作数指出参与运算的对象,以及运算结果所存放的位置等。一条机器指令对应于一条汇编指令,可以说汇编指令是机器指令的文本表示.
机器指令的操作数是以操作数的补码的形式出现,而汇编则是以其16进制的形式出现,汇编里面通过对函数名的引用来调用函数,而在机器语言中,函数调用是通过对在.rela.text节中的重定位条目进行解析从而得到目的函数地址.
4.5 本章小结
在本章中,我们窥视了C语言提供的抽象层下面的东西,以了解机器级编程。通过让编译器产生机器级程序的汇编代码表示,我们了解了编译器和它的优化能力,汇编器则进一步将汇编代码变成可重定位的二进制文件.
(第4章1分)
第5章 链接
5.1 链接的概念与作用
链接器(ld)将程序员生成的.o文件与标准C库里面的用到的函数如printf函数所在的预编译好了的单独的文件printf.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
5.3 可执行目标文件hello的格式:
ELF 头:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
类别: ELF64
数据: 2 补码,小端序 (little endian)
Version: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
类型: EXEC (可执行文件)
系统架构: Advanced Micro Devices X86-64
版本: 0x1
入口点地址: 0x401080
程序头起点: 64 (bytes into file)
Start of section headers: 14112 (bytes into file)
标志: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 10
Size of section headers: 64 (bytes)
Number of section headers: 25
Section header string table index: 24
上图充分反映了各段的大小和地址空间.可根据地址,大小和偏移量来确定各节所存储的区域,各节使用的均为虚拟地址.
5.4 hello的虚拟地址空间
在edb中可看到程序是从地址处0x401080开始,这与elf头的程序入口地址相同,
在Data Dump里面即为本进程虚拟地址空间各段的信息.与5.3对比可发现是一一对应的.
5.5 链接的重定位过程分析
上图为hello与hello.o相比多出来的部分(未完全列出),hello的objdump -d -r结果多了许多函数信息,且虚拟地址也给出来了.链接的过程就是将代码段重组合并,数据段也重组合并,还有一些其他的段也是如此.形成新的链接好的可加载到内存执行的可执行文件.
以第一个子符串为例说明,在hello.o里面是 .rodata-0x4,在其elf文件里面找到.rodata如下:
000000000018 000500000002 R_X86_64_PC32 0000000000000000 .rodata - 4
可知offset=0x18
addend=-4
在hello的elf文件里面找到.rodata:
[15] .rodata PROGBITS 0000000000402000 00002000
000000000000002f 0000000000000000 A 0 0 4
所以ADDR(.rodata)=0x402000
又找到.text
[13] .text PROGBITS 0000000000401080 00001080
0000000000000121 0000000000000000 AX 0 0 16
所以ADDR(.text)=0x401080
因为这里是pc相对寻址:
由公式:
*refptr=(unsigned)(ADDR(.rodata)+addend-ADDR(.text)-offset)
=(unsigned)(0x402000+(-4)-(0x401080)-0x18)=f64
不知道为啥与main中有所不同,main中如下:
4010c6: 48 8d 3d 37 0f 00 00 lea 0xf37(%rip),%rdi # 402004 <_IO_stdin_used+0x4>
还有一种寻址是pc绝对寻址
计算公式如下:
*refptr=(unsigned)(ADDR(r.symbol)+r.addend),例子省略.
5.6 hello的执行流程
_start
__libc_csu_init
_init
frame_dummy
register_tm_clones
main
{printf@plt
sleep@plt} 10遍
getchar@plt
__do_global_dtors_aux
__cxa_finalize@plt
deregister_tm_clones
_fini
5.7 Hello的动态链接分析
动态库加载到哪里是未知的,为了使代码段里对数据及函数的引用与具体地址无关,ELF的做法是在动态库的数据段加一个表项,叫作GOT,里面存放全局符号的地址,通过此来引用数据及函数.
5.8 本章小结
链接可以在编译时由静态编译器来完成,也可以在加载时和运行时由动态链接器来完成.可重定位的目标文件由静态链接器合并成一个可执行的目标文件,共享目标文件是在运行时由动态链接器链接和加载的.
链接器的两个主要任务是符号解析和重定位.
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程就是一个执行中程序的实例.系统中的每一个程序都运行在某个进程的上下文中.进程提供给应用程序两个关键抽象:
一个独立的逻辑控制流
一个私有的地址空间
6.2 简述壳Shell-bash的作用与处理流程
壳,是相对于内核而言的.这里的shell-bash是命令行界面,是系统内核的一层壳,作用是保护内核同时传递信息给内核,我们可以将它视为操作计算机的工具.
在shell上我们用的是脚本语言(解释型语言)来操作计算机.
处理流程:
用户输入命令
shell 对用户的命令进行解释并判断是否为内置命令.
若为内置命令则执行,否则fork()一个子进程并调用execve函数去执行.
若为前台进程则等待其完成,若为后台则转入后台运行,直接开始下一次用户输入命令.
6.3 Hello的fork进程创建过程
子进程程序
shell waitpid
父进程 exit
父进程调用fork()函数创建一个子进程,新建的子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段,堆,共享库以及用户栈,子进程可以说是父进程复制了一份,他们之间的不同在于pid。
6.4 Hello的execve过程
execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境列表envp。只有当出现错误的时候,例如找不到filename,才会返回到调用函数,一般情况下execve不返回.execve是在当前进程的上下文中加载并运行一个新的程序。它会覆盖当前进程的地址空间,程序并没有创建一个新的进程。
6.5 Hello的进程执行
操作系统内核使用一种称为上下文切换的较高层形式的异常控制流来实现多任务。内核为每个进程维持一个上下文。上下文就是内核重新启动一个被抢占的进程所需的状态.
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策称为调度.
当内核选中了一个进程执行时,分给该进程时间片,通过上下文切换将控制转移到该进程,异常可以导致上下文切换.在进行切换的时候,比如从A切换到B,先从A的用户模式到A的内核模式,再到B的内核模式,再到B的用户模式.
6.6 hello的异常与信号处理
(以下格式自行编排,编辑时删除)
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
类别 原因 异步/同步 返回行为
中断 来自I/O设备的信号 异步 总是返回到下一条指令
陷阱 有意的异常 同步 总是返回到下一条指令
故障 潜在可恢复的错误 同步 可能返回到当前指令
终止 不可恢复的错误 同步 不会返回
当进程接收到一个信号时,控制转移到信号处理程序,处理完了再转移回来.
当处理器的状态发生变化将触发一个异常,控制转移到异常处理程序,等处理完了异常可能返回可能不返回.
6.7本章小结
异常控制流发生在计算机系统的各个层次,是计算机系统中提供并发的基本机制。在硬件层,异常是由处理器的事件触发的控制流的突变。在操作系统层,内核用ECF提供进程的基本概念。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址(Logical Address) 是指由程式产生的和段相关的偏移地址部分。例如,你在进行C语言指针编程中,能读取指针变量本身值(&操作),实际上这个值就是逻辑地址,他是相对于你当前进程数据段的地址,不和绝对物理地址相干。只有在Intel实模式下,逻辑地址才和物理地址相等(因为实模式没有分段或分页机制,Cpu不进行自动地址转换);
逻辑也就是在Intel保护模式下程式执行代码段限长内的偏移地址(假定代码段、数据段如果完全相同)。应用程式员仅需和逻辑地址打交道,而分段和分页机制对你来说是完全透明的,仅由系统编程人员涉及。应用程式员虽然自己能直接操作内存,那也只能在操作系统给你分配的内存段操作。
例如在hello.ss,和hello.sss文件中的地址则为逻辑地址.
线性地址(Linear Address) 是逻辑地址到物理地址变换之间的中间层。程式代码会产生逻辑地址,或说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。如果启用了分页机制,那么线性地址能再经变换以产生一个物理地址。若没有启用分页机制,那么线性地址直接就是物理地址。Intel 80386的线性地址空间容量为4G(2的32次方即32根地址总线寻址)。
物理地址(Physical Address) 是指出目前CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。如果启用了分页机制,那么线性地址会使用页目录和页表中的项变换成物理地址。如果没有启用分页机制,那么线性地址就直接成为物理地址了。在hello.ss中并未出现物理地址.
虚拟内存(Virtual Memory)是指计算机呈现出要比实际拥有的内存大得多的内存量。因此他允许程式员编制并运行比实际系统拥有的内存大得多的程式。这使得许多大型项目也能够在具有有限内存资源的系统上实现。一个非常恰当的比喻是:你不必非常长的轨道就能让一列火车从上海开到北京。你只需要足够长的铁轨(比如说3公里)就能完成这个任务。采取的方法是把后面的铁轨即时铺到火车的前面,只要你的操作足够快并能满足需求,列车就能象在一条完整的轨道上运行。
这也就是虚拟内存管理需要完成的任务。在Linux0.11内核中,给每个程式(进程)都划分了总容量为64MB的虚拟内存空间。因此程式的逻辑地址范围是0x0000000到0x4000000。有时我们也把逻辑地址称为虚拟地址。因为和虚拟内存空间的概念类似,逻辑地址也是和实际物理内存容量无关的。逻辑地址和物理地址的“差距”是0xC0000000,是由于虚拟地址->线性地址->物理地址映射正好差这个值。这个值是由操作系统指定的。机理 逻辑地址(或称为虚拟地址)到线性地址是由CPU的段机制自动转换的。如果没有开启分页管理,则线性地址就是物理地址。如果开启了分页管理,那么系统程式需要参和线性地址到物理地址的转换过程。具体是通过设置页目录表和页表项进行的。
7.2 Intel逻辑地址到线性地址的变换-段式管理
为了实现段式管理,操作系统需要如下的数据结构来实现进程的地址空间到虚拟内存空间的映射,并跟踪虚拟内存的使用情况,以便在装入新的段的时候,合理地分配内存空间。
·进程段表:描述组成进程地址空间的各段,可以是指向系统段表中表项的索引。每段有段基址(baseaddress),即段内地址。
在系统中为每个进程建立一张段映射表,如图:
·系统段表:系统所有占用段(已经分配的段)。
·空闲段表:内存中所有空闲段,可以结合到系统段表中。
5.3 段式管理的地址变换
在段式 管理系统中,整个进程的地址空间是二维的,即其逻辑地址由段号和段内地址两部分组成。为了完成进程逻辑地址到虚拟地址的映射,处理器会查找内存中的段表,由段号得到段的首地址,加上段内地址,得到虚拟地址的这个过程也是由处理器的硬件直接完成的,操作系统只需在进程切换时,将进程段表的首地址装入处理器的特定寄存器当中。这个寄存器一般被称作段表地址寄存器。
7.3 Hello的线性地址到物理地址的变换-页式管理
如果启用了分页机制,那么线性地址可以再经过变换以产生一个物理地址。当采用4KB分页大小的时候,线性地址的高10位为页目录项在页目录表中的编号,中间10位为页表中的页号,其低12位则为偏移地址。如果是使用4MB分页机制,则高10位页号,低22位为偏移地址。如果没有启用分页机制,那么线性地址直接就是物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
CPU产生一个VA,VA先分成VPN和VPO。1.根据VPN去TLB中寻找PPN,做法是将VPN分成TLBT,TLBI,与高速缓存类似,去寻找那一行,如果命中,则取出PPN和VPO(PPO)组成PA。
2.当不命中的时候,将VPN分成VPN1,VPN2,VPN3,VPN4,通过CR3找到一级页表,根据VPN1找到PTE1,这个PTE1指向某一个二级页表,然后二级页表里面通过VPN2……依次类推,最终找到PPN,然后与VPO(PPO)组成PA。
7.5 三级Cache支持下的物理内存访问
物理地址PA被分为:CT,CI,CO. 其中CT是行标记,CI是组索引,CO是块偏移。根据PA先去cache 1里面找,如果命中则返回该数据,如果不命中则从下一层(cache 2)取出包含所需数据的那一块,然后再去把这块加到cache 1里面,可能需要替换某个块,一般采取LRU策略。如果cache 2里面也没有则去cache 3里面找,依次类推,直到找到该数据。
7.6 hello进程fork时的内存映射
如果虚拟内存系统可以集成到传统的文件系统上中,那么就能提供一种简单而高效的把程序和数据加载到内存中的方法。一个对象可以被映射到虚拟内存的一个区域,要么作为共享对象,要么作为私有对象。如果是共享对象,那么一个进程对其写,对其他进程也是可见的,私有对象则不可见。对于私有对象,采取的是一种叫作写时复制的巧妙技术将其映射到虚拟内存中,在物理内存里面只保存私有对象的一份副本。比如两个进程的私有对象共享同一个物理副本,当一个进程企图写的时候会触发一个保护故障。故障处理程序会在物理内存中创建这个页面的一个新副本,更新页表条目指向这个新的副本,然后恢复这个页面的可写权限。
7.7 hello进程execve时的内存映射
删除已存在的用户区域
映射私有区域:为新程序的代码,数据,bss和栈区域创建新的区域结构。所有这些新的区域均为私有的,写时复制的。
映射共享区域:如果程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间的共享区域。
设置程序计数器。
7.8 缺页故障与缺页中断处理
当MMU正在翻译某个虚拟地址A时,触发了一个缺页。这个异常导致控制转移到内核的缺页处理程序,处理程序随后执行如下步骤:
- 判断地址A是否合法,非法则终止进程
- 判断内存访问是否合法,非法则终止进程
- 若1,2均判断合法,则选择一个牺牲页面,如果被修改过,那么将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令将再次发送A到MMU,这次即可正常翻译,不会产生缺页中断。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆.系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址) .对于每个进程,内核维护着一个变量brk, 它指向堆的顶部.
分配器将堆视为一组不同大小的块的集合来维护.每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的.已分配的块显式地保留为供应用程序使用.空闲块可用来分配.空闲块保持空闲,直到它显式地被应用所分配.一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的.
分配器有两种基本风格. 两种风格都要求应用显式地分配块.它们的不同之处在于由哪个实体来负责释放已分配的块
显式分配器(explicit allocator):
要求应用显式地释放任何已分配的块.例如,C标准库提供一种叫做malloc程序包的显式分配器.C程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块.C++中的new和delete操作符与C中的malloc和free相当.
隐式分配器(implicit allocator):
要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块.隐式分配器也叫做垃圾收集器(garbage collector),而自动释放未使用的已分配的块的过程叫做垃圾收集( garbage collection).例如,诸如Lisp、ML以及Java之类的高级语言就依赖垃圾收集来释放已分配的块.
一个块是由一个字的头部,有效载荷,以及可能的一些额外的填充组成的.头部编码了这个块的大小,以及这个块是已分配的还是空闲的.如果我们强加一个双字的对齐约束条件,那么块大小就总是8的倍数,且块大小的最低3位总是零.因此,我们只需要内存大小的29个高位,释放剩余的3位来编码其他信息.在这种情况中,我们用其中的最低位(已分配位)来指明这个块是已分配的还是空闲的.
带边界标签的隐式空闲链表:
在隐式空闲链表堆块的基础上,在每个块的结尾处添加一个脚部(footer),边界标记),其中脚部就是头部的一个副本.如果每个块包括这样一个脚部,那么分配器就可以通过检查它的脚部,判断前面一个块的起始位置和状态,这个脚部总是在距当前块开始位置一个字的距离,这样就允许在常数时间内进行对前面块的合并.
显示空闲链表
一种更好的方法是将空闲块组织为某种形式的显式数据结构.因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面.例如,堆可以组织成一一个双向空闲链表,在每个空闲块中,都包含一个pred(前驱)和succ(后继)指针.
7.10本章小结
虚拟内存是对主存的一个抽象,支持虚拟内存的处理器通过使用一种叫做虚拟寻址的间接形式来引用主存。处理器产生一个虚拟地址,在被发送到主存前,这个地址被翻译成一个物理地址。
虚拟内存提供三个重要功能。第一,它在主存中自动缓存最近使用的存放磁盘上的虚拟地址空间的内容。第二,虚拟内存简化了内存管理。第三,虚拟内存通过在每条页表条目中加入保护位,从而简化了内存保护。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
所有的I/O设备(例如网络,磁盘和终端)都被模型化为文件,而所有的输入输出都被当作对相应文件的读和写来执行。
这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单,低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。
8.2 简述Unix IO接口及其函数
Unix I/O 通过系统调用来访问。
相关函数如下:
int Open(char filename,int flags,mode_t mode); 此函数将filename转换为一个文件描述符,并且返回描述符数字。flags参数指明了进程打算如何访问这个文件:
O_RDONLY –只读 O_WRONLY—只写 O_RDWR—可读可写。mode参数指定了新文件的访问权限位。
int close(int fd)用来关闭一个已打开的文件,关闭一个已关闭的文件会出错。
ssize_t read(int fd,void buf,size_t n);read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,而返回值为0表示EOF,否则,返回值表示的是实际传送的字节数量。
ssize_t write(int fd,const voidbuf,size_t n);write函数从内存位置buf复制最多n个字节到描述符fd的当前文件位置。
int stat(const charfilename,struct stat *buf);stat函数以一个文件名作为输入,并填写stat数据结构中的各个成员。
8.3 printf的实现分析
(以下格式自行编排,编辑时删除)
https://www.cnblogs.com/pianist/p/3315801.html
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
(以下格式自行编排,编辑时删除)
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
Linux提供了少量的基于Unix I/O模型的系统级函数,它们允许应用程序打开,关闭,读和写文件,提取文件的元数据,以及执行I/O的重定向。
Linux内核使用三个相关的数据结构来表示打开的文件。描述符表中的表项指向打开文件表中的表项,而打开文件表中的表项又指向v-node表中的表项。每个进程都有它自己单独的描述符表,而所有的进程共享同一个打开文件表和v-node表。
(第8章1分)
结论
程序员使用C编写hello.c文件,通过编译预处理得到hello.i文件,再通过编译得到hello.s,再汇编得到hello.o再将libc里面的库函数.o文件与hello.o链接起来得到可执行文件hello。在shell里面输入可执行文件hello的全路径或者当前路径下的./hello即可执行该文件。内核里面的加载器将可执行文件加载进内存,并创建一个进程去执行它,内核为该进程分时间片让他得以执行,通过段页式管理得到虚拟地址与物理地址,获得代码与数据,最终得到运行结果。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
hello.i:编译预处理得到的文件
hello.s:编译hello.i得到的文件
hello.ss:反汇编hello.o得到的文件
hello.sss:反汇编hello得到的文件
hello.o:可重定位目标文件
helloelf1:hello.o的elf
helloelf2:hello的elf。
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] CSAPP课本
[2]CSDN网站
[3]简书