哈工大计算机系统大作业
目录
- 第一章 概述
- 第二章 预处理
- 第三章 编译
- 第四章 汇编
- 第五章 链接
- 第六章 Hello进程管理
- 第七章 Hello内存管理
- 第八章 Hello的I/O管理
第一章 概述
第二章 预处理
第三章 编译
第四章 汇编
第五章 链接
第六章 Hello进程管理
第七章 Hello内存管理
第八章 Hello的I/O管理
第1章 概述
1.1 Hello简介
P2P:
1) Program:在IDE中输入代码,即可完成程序。
2) Process:
hello.c经过C preprocessor(cpp)预处理,插入#include指定文件,扩展用#define指定的宏,得到hello.i文件。
hello.i文件经过C compiler(ccl)编译,得到hello.s汇编文件。
hello.s文件经过Assembler(ass)汇编,得到二进制目标代码文件hello.o。
hello.o文件经过Linker(ld)链接,将目标代码文件与实现库函数的代码合并,得到最终的可执行文件Hello。
020:
1)shell为hello进程execve,映射虚拟内存,进入程序入口之后,程序开始载入物理内存。
2)进入main函数执行目标代码,CPU为运行的hello分配时间片,进入逻辑控制流。
3)当程序执行完毕之后,shell父进程回收hello进程,内核删除相关的数据和代码。
1.2 环境与工具
硬件环境:
处理器:Intel® Core™ i5-8265U CPU @ 1.60GHz 1.80 GHz
RAM:8.00 GB
版本:Windows 10 家庭中文版
软件环境:Win10 64位,VMware Workstation Pro 16.0,Ubuntu 20.04
开发和调试环境:Vscode,gcc,ming64,readelf,gdb
1.3 中间结果
文件作用 文件名字
源代码c文件 hello.c
hello.c预处理得到的文件(2章-预处理) hello.i
hello.i编译得到的汇编文件(3章-编译) hello.s
hello.s汇编得到的二进制目标代码文件(4章-汇编) hello.o
hello.o反汇编得到的文本文件(4章-汇编) hello.asm
hello.o的elf文件(4章-汇编) hello.elf
hello.o链接得到的可执行文件(5章-链接) hello
hello的反汇编文件(5章-链接) hello_asm
hello的elf文件(5章-链接) hello_elf
1.4 本章小结
这一章对hello.c的生命周期进行了总体的概述,之后我们会对第一章的内容详细展开。
第2章 预处理
2.1 预处理的概念与作用
1.概念:
预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。,其中 ISO C/C++要求支持的包括#if、 #ifdef、 #ifndef、 #else、 #elif、 #endif(条件编译)、 #define(宏定义)、 #include(源文件包含)、 #line(行控制)、 #error(错误指令)、 #pragma(和实现相关的杂注)以及单独的#(空指令)。
2.作用:
1)对于#define宏定义,用实际值替换所有源程序当中出现的被替换变量
2)对于#include引入库文件,引入这些库文件
3)根据#if 后面的条件决定需要编译的代码
还有对于其他的以#开头指令的处理。在此不过多介绍。
2.2在Ubuntu下预处理的命令
2.3 Hello的预处理结果解析
如上图所示,main源程序在3047行,在这之前,是源代码引入的库文件的展开。即如下所示的三个库文件:
库文件的展开如下所示。后面还有很多,在此只展示一部分。
其中包括一些变量的定义:
还有一些库函数的声明:
需要注意的是,在整个hello.i文件当中,没有源代码注释的任何表明。说明从预处理开始,就忽视了注释的内容。
2.4 本章小结
这一章主要介绍了预处理的过程。通过分析预处理之后得到的hello.i文件里面的内容,验证了我们之前学过的预处理过程对源文件hello.c所做的处理。
第3章 编译
3.1 编译的概念与作用
1.概念:
编译器(ccl)将文本文件hello.i编译成汇编文件hello.s。这一步编译语言程序将所有的指令翻译成与源文件效果等价的机器指令,也就是汇编代码。
2.作用:
编译包括以下基本流程:
1)语法分析:编译程序的语法分析器以单词符号作为输入,分析单词符号串是否形成符合语法规则的语法单位,方法分为两种:自上而下分析法和自下而上分析法。
2)中间代码:源程序的一种内部表示,或称中间语言。中间代码的作用是可使编译程序的结构在逻辑上更为简单明确,特别是可使目标代码的优化比较容易实现中间代码。
3)代码优化:指对程序进行多种等价变换,使得从变换后的程序出发,能生成更有效的目标代码。
4)目标代码:生成是编译的最后一个阶段。目标代码生成器把语法分析后或优化后的中间代码变换成目标代码。此处指汇编语言代码,须经过汇编程序汇编后,成为可执行的机器语言代码。
3.2 在Ubuntu下编译的命令
3.3 Hello的编译结果解析
3.3.1 hello.s文件结构
所有以.开头的行都是指导汇编器和编译器工作的伪指令。现在对这些行常见的伪指令都声明了什么进行说明。
名字 内容
.file 声明源文件
.text 以下是代码段
.section .XXX 以下是XXX节
.align 以某种对齐方式,在未使用的存储区域填充值
.string 定义一个字符串
.type name,@type 将符号name的type属性设为type。
其中type可以是function或object
.size name,expression 将符号name所占空间设为expression
.globl 定义一个全局符号
.long 定义一个长整型
3.3.2 字符串
源代码如下所列部分:
其中的这个字符串是被保存在.rodata节当中的,如下所示:
这里需要注意的是,对于字符串而言,没有大端序和小端序之分,都是按照字符串本身的顺序存储的。大端序和小端序之分只针对指针地址、整型数据、浮点型数据而言。
3.3.3 整型常量
源代码如下所列部分:
其中关于argc和整型常量4的比较部分的汇编代码,如下所示:
从此我们可以得出,整型常量4,是保存在.text部分当中的,是汇编代码的一部分。
因为这个数字既不是来于寄存器,也不是内存,而是作为指令的一部分出现的。
同理可得,其余的整型变量同样是汇编代码的一部分。
图中0 8 1 2 3,都是同样的道理。
3.3.4 全局变量
初始化的全局变量保存在.data节当中。
3.3.5 局部变量
局部变量保存在寄存器当中。如果寄存器数量不够,则会保存在运行时栈当中。
源代码如下所列部分:
对应于下列汇编代码:
由此可以看出,i被保存在%rbp-4处,是被保存在运行时栈上的。
3.3.6 算术运算
源代码如下所列部分:
i完成了自加运算。对应于汇编代码如下所示:
3.3.7 条件判断
源代码如下所列部分:
判断argc和4的关系。对应于汇编代码如下所示:
源代码如下所列部分:
判断i和8的关系。对应于汇编代码如下所示:
这里和7进行比较而不是8的原因,是在这之前,i进行了自加的操作:
另外,在switch语句当中,修改需要被比较的值很常见。因为汇编代码会优化代码结构,首先对需要被比较的变量进行加或减一个数值,不过最后的结果与源代码意图完全相同。
3.3.8 指针数据
源代码如下所列部分:
对于argc和argv指针数组而言,它被保存在运行时栈当中。如下所示:
argc存储在%edi,argv存储在%esi,二者之前都被保存在运行时栈当中。通过%rsi-8和%rsi-16,分别得到argv[1]和argv[2]两个字符串
3.3.9 函数
首先,我们说明寄存器保存传入函数的形参变量的规则:以64位为例。
1 2 3 4 5 6
%rdi %rsi %rdx %rcx %r8 %r9
函数的返回值保存在%rax中。
3.3.9.1 main函数
传入参数argc和argv[],分别用寄存器%rdi和%rsi存储。
设置%eax为0并且返回,对应return 0 。
源代码如下所列部分:
汇编代码如下所列部分:
3.3.9.2 printf函数
源代码如下所列部分:
汇编代码如下所列部分:
puts对应printf函数,只有一个变量,是要输出的字符串,它的首地址存放于.string节当中,在引用这个函数之前,要先把这个变量存放于%rdi当中。
3.3.9.2 sleep函数
源代码如下所列部分:
汇编代码如下所列部分:
传入参数atoi(argv[3]),之前这个参数被保存于%eax当中。
3.3.9.3 exit函数
源代码如下所列部分:
汇编代码如下所列部分:
传入参数1。
3.3.9.4 getchar函数
源代码如下所列部分:
汇编代码如下所列部分:
没有参数传入。
3.4 本章小结
本章介绍了编译的概念以及过程。以及查看了对于代码中的数据常量、算术运算、条件判断、函数调用等过程,机器是怎么通过机器指令完成源代码的意图的。这让我们对机器指令的了解更深一层。
第4章 汇编
4.1 汇编的概念与作用
1.概念
驱动程序运行汇编器as,将汇编语言(这里是hello.s)翻译成机器语言(hello.o)的过程称为汇编,同时这个机器语言文件也是可重定位目标文件。
2.作用
汇编就是将高级语言转化为机器可直接识别执行的代码文件的过程,汇编器将.s汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在.o目标文件中,.o文件是一个二进制文件,它包含程序的指令编码。
4.2 在Ubuntu下汇编的命令
4.3 可重定位目标elf格式
4.3.1 ELF头:
4.3.2 节头目表:
4.3.3 一些说明:
4.3.4 重定位条目:
4.3.5 符号表:
4.4 Hello.o的结果解析
考察hello.o反汇编文本文件hello.asm,并且与第3章的汇编文件hello.s对照分析:
hello.o内容如下:
hello.s代码如下:
通过上面的对照分析,我们可以知道:
1) 分支跳转:hello.s当中还为分配实际地址,跳转的地址为段地址,是.L2等名称,hello.asm当中是间接地址,是地址偏移量。
2) 数的表示:hello.s当中是十进制数,hello.asm是十六进制。
3) 函数调用:hello.s使用的是函数名称调用,hello.asm使用的是函数的相对偏移地址。因为函数只有在链接之后才能确定运行执行的地址,因此在.rela.text节中为其添加了重定位条目。
4.5 本章小结
本章讲述了汇编的概念和作用。对可重定位目标的elf格式进行了详尽的姐hi。并且将我的反汇编文件和之前生成的hello.s文件进行了对比。对汇编的理解更进一层。
第5章 链接
5.1 链接的概念与作用
1)概念
链接是将各种代码和数据片段收集并且组合成一个单一文件的过程。这个文件可以被加载到内存并且执行。
2)作用
静态链接器以一组可重定位目标文件和命令行参数作为输入,生成一个完全链接的、可以加载和运行的可执行目标文件作为输出。为了构造可执行文件,链接器完成符号解析和重定位两个过程。链接器的工作原理为分离编译提供了可能,节省了大量的地址空间。
5.2 在Ubuntu下链接的命令
5.3 可执行目标文件hello的格式
节头目表如下所示:
5.4 hello的虚拟地址空间
观察edb的Data Dump窗口。窗口显示虚拟地址由0x400000开始,到0x400fff结束,这之间的每一个节对应5.3中的每一个节头表的声明,如下图。
第4步,观察edb的Sympols小窗口。我们发现确实从虚拟地址从0x400000开始和5.3节中的节头表是一一对应的(从.interp节到…en_frame对应),如下图。
第5步,关于5.3节节头表中的.dynamic到.shstrtab的处理。首先查看hello的elf格式文件重的程序头,它包含的信息:类型,偏移,虚拟地址,物理地址,对齐,标志等,如下图。通过Data Dump窗口查看虚拟地址段 0x600000到0x602000的部分,在0到fff的空间中,与0x400000到0x401000段的存放的程序相同;而在 fff之后存放的是.dynamic到.shstrtab节。
5.5 链接的重定位过程分析
分析hello.asm和hello_asm的不同:
hello.asm
hello.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 :
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: 48 83 ec 20 sub $0x20,%rsp
c: 89 7d ec mov %edi,-0x14(%rbp)
f: 48 89 75 e0 mov %rsi,-0x20(%rbp)
13: 83 7d ec 04 cmpl $0x4,-0x14(%rbp)
17: 74 16 je 2f
19: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 20
1c: R_X86_64_PC32 .rodata-0x4
20: e8 00 00 00 00 callq 25
21: R_X86_64_PLT32 puts-0x4
25: bf 01 00 00 00 mov $0x1,%edi
2a: e8 00 00 00 00 callq 2f
2b: R_X86_64_PLT32 exit-0x4
2f: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
36: eb 48 jmp 80
38: 48 8b 45 e0 mov -0x20(%rbp),%rax
3c: 48 83 c0 10 add $0x10,%rax
40: 48 8b 10 mov (%rax),%rdx
43: 48 8b 45 e0 mov -0x20(%rbp),%rax
47: 48 83 c0 08 add $0x8,%rax
4b: 48 8b 00 mov (%rax),%rax
4e: 48 89 c6 mov %rax,%rsi
51: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 58
54: R_X86_64_PC32 .rodata+0x22
58: b8 00 00 00 00 mov $0x0,%eax
5d: e8 00 00 00 00 callq 62
5e: R_X86_64_PLT32 printf-0x4
62: 48 8b 45 e0 mov -0x20(%rbp),%rax
66: 48 83 c0 18 add $0x18,%rax
6a: 48 8b 00 mov (%rax),%rax
6d: 48 89 c7 mov %rax,%rdi
70: e8 00 00 00 00 callq 75
71: R_X86_64_PLT32 atoi-0x4
75: 89 c7 mov %eax,%edi
77: e8 00 00 00 00 callq 7c
78: R_X86_64_PLT32 sleep-0x4
7c: 83 45 fc 01 addl $0x1,-0x4(%rbp)
80: 83 7d fc 07 cmpl $0x7,-0x4(%rbp)
84: 7e b2 jle 38
86: e8 00 00 00 00 callq 8b
87: R_X86_64_PLT32 getchar-0x4
8b: b8 00 00 00 00 mov $0x0,%eax
90: c9 leaveq
91: c3 retq
hello_asm
hello: file format elf64-x86-64
Disassembly of section .init:
0000000000401000 <_init>:
401000: f3 0f 1e fa endbr64
401004: 48 83 ec 08 sub $0x8,%rsp
401008: 48 8b 05 e9 2f 00 00 mov 0x2fe9(%rip),%rax # 403ff8 <gmon_start>
40100f: 48 85 c0 test %rax,%rax
401012: 74 02 je 401016 <_init+0x16>
401014: ff d0 callq *%rax
401016: 48 83 c4 08 add $0x8,%rsp
40101a: c3 retq
Disassembly of section .plt:
0000000000401020 <.plt>:
401020: ff 35 e2 2f 00 00 pushq 0x2fe2(%rip) # 404008 <GLOBAL_OFFSET_TABLE+0x8>
401026: f2 ff 25 e3 2f 00 00 bnd jmpq *0x2fe3(%rip) # 404010 <GLOBAL_OFFSET_TABLE+0x10>
40102d: 0f 1f 00 nopl (%rax)
401030: f3 0f 1e fa endbr64
401034: 68 00 00 00 00 pushq $0x0
401039: f2 e9 e1 ff ff ff bnd jmpq 401020 <.plt>
40103f: 90 nop
401040: f3 0f 1e fa endbr64
401044: 68 01 00 00 00 pushq $0x1
401049: f2 e9 d1 ff ff ff bnd jmpq 401020 <.plt>
40104f: 90 nop
401050: f3 0f 1e fa endbr64
401054: 68 02 00 00 00 pushq $0x2
401059: f2 e9 c1 ff ff ff bnd jmpq 401020 <.plt>
40105f: 90 nop
401060: f3 0f 1e fa endbr64
401064: 68 03 00 00 00 pushq $0x3
401069: f2 e9 b1 ff ff ff bnd jmpq 401020 <.plt>
40106f: 90 nop
401070: f3 0f 1e fa endbr64
401074: 68 04 00 00 00 pushq $0x4
401079: f2 e9 a1 ff ff ff bnd jmpq 401020 <.plt>
40107f: 90 nop
401080: f3 0f 1e fa endbr64
401084: 68 05 00 00 00 pushq $0x5
401089: f2 e9 91 ff ff ff bnd jmpq 401020 <.plt>
40108f: 90 nop
Disassembly of section .plt.sec:
0000000000401090 puts@plt:
401090: f3 0f 1e fa endbr64
401094: f2 ff 25 7d 2f 00 00 bnd jmpq *0x2f7d(%rip) # 404018
40109b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
00000000004010a0 printf@plt:
4010a0: f3 0f 1e fa endbr64
4010a4: f2 ff 25 75 2f 00 00 bnd jmpq *0x2f75(%rip) # 404020
4010ab: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
00000000004010b0 getchar@plt:
4010b0: f3 0f 1e fa endbr64
4010b4: f2 ff 25 6d 2f 00 00 bnd jmpq *0x2f6d(%rip) # 404028
4010bb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
00000000004010c0 atoi@plt:
4010c0: f3 0f 1e fa endbr64
4010c4: f2 ff 25 65 2f 00 00 bnd jmpq *0x2f65(%rip) # 404030
4010cb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
00000000004010d0 exit@plt:
4010d0: f3 0f 1e fa endbr64
4010d4: f2 ff 25 5d 2f 00 00 bnd jmpq *0x2f5d(%rip) # 404038
4010db: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
00000000004010e0 sleep@plt:
4010e0: f3 0f 1e fa endbr64
4010e4: f2 ff 25 55 2f 00 00 bnd jmpq *0x2f55(%rip) # 404040
4010eb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
Disassembly of section .text:
00000000004010f0 <_start>:
4010f0: f3 0f 1e fa endbr64
4010f4: 31 ed xor %ebp,%ebp
4010f6: 49 89 d1 mov %rdx,%r9
4010f9: 5e pop %rsi
4010fa: 48 89 e2 mov %rsp,%rdx
4010fd: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp
401101: 50 push %rax
401102: 54 push %rsp
401103: 49 c7 c0 30 12 40 00 mov $0x401230,%r8
40110a: 48 c7 c1 c0 11 40 00 mov $0x4011c0,%rcx
401111: 48 c7 c7 25 11 40 00 mov $0x401125,%rdi
401118: ff 15 d2 2e 00 00 callq *0x2ed2(%rip) # 403ff0 <__libc_start_main@GLIBC_2.2.5>
40111e: f4 hlt
40111f: 90 nop
0000000000401120 <_dl_relocate_static_pie>:
401120: f3 0f 1e fa endbr64
401124: c3 retq
0000000000401125 :
401125: f3 0f 1e fa endbr64
401129: 55 push %rbp
40112a: 48 89 e5 mov %rsp,%rbp
40112d: 48 83 ec 20 sub $0x20,%rsp
401131: 89 7d ec mov %edi,-0x14(%rbp)
401134: 48 89 75 e0 mov %rsi,-0x20(%rbp)
401138: 83 7d ec 04 cmpl $0x4,-0x14(%rbp)
40113c: 74 16 je 401154
40113e: 48 8d 3d c3 0e 00 00 lea 0xec3(%rip),%rdi # 402008 <_IO_stdin_used+0x8>
401145: e8 46 ff ff ff callq 401090 puts@plt
40114a: bf 01 00 00 00 mov $0x1,%edi
40114f: e8 7c ff ff ff callq 4010d0 exit@plt
401154: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
40115b: eb 48 jmp 4011a5
40115d: 48 8b 45 e0 mov -0x20(%rbp),%rax
401161: 48 83 c0 10 add $0x10,%rax
401165: 48 8b 10 mov (%rax),%rdx
401168: 48 8b 45 e0 mov -0x20(%rbp),%rax
40116c: 48 83 c0 08 add $0x8,%rax
401170: 48 8b 00 mov (%rax),%rax
401173: 48 89 c6 mov %rax,%rsi
401176: 48 8d 3d b1 0e 00 00 lea 0xeb1(%rip),%rdi # 40202e <_IO_stdin_used+0x2e>
40117d: b8 00 00 00 00 mov $0x0,%eax
401182: e8 19 ff ff ff callq 4010a0 printf@plt
401187: 48 8b 45 e0 mov -0x20(%rbp),%rax
40118b: 48 83 c0 18 add $0x18,%rax
40118f: 48 8b 00 mov (%rax),%rax
401192: 48 89 c7 mov %rax,%rdi
401195: e8 26 ff ff ff callq 4010c0 atoi@plt
40119a: 89 c7 mov %eax,%edi
40119c: e8 3f ff ff ff callq 4010e0 sleep@plt
4011a1: 83 45 fc 01 addl $0x1,-0x4(%rbp)
4011a5: 83 7d fc 07 cmpl $0x7,-0x4(%rbp)
4011a9: 7e b2 jle 40115d
4011ab: e8 00 ff ff ff callq 4010b0 getchar@plt
4011b0: b8 00 00 00 00 mov $0x0,%eax
4011b5: c9 leaveq
4011b6: c3 retq
4011b7: 66 0f 1f 84 00 00 00 nopw 0x0(%rax,%rax,1)
4011be: 00 00
00000000004011c0 <__libc_csu_init>:
4011c0: f3 0f 1e fa endbr64
4011c4: 41 57 push %r15
4011c6: 4c 8d 3d 83 2c 00 00 lea 0x2c83(%rip),%r15 # 403e50 <_DYNAMIC>
4011cd: 41 56 push %r14
4011cf: 49 89 d6 mov %rdx,%r14
4011d2: 41 55 push %r13
4011d4: 49 89 f5 mov %rsi,%r13
4011d7: 41 54 push %r12
4011d9: 41 89 fc mov %edi,%r12d
4011dc: 55 push %rbp
4011dd: 48 8d 2d 6c 2c 00 00 lea 0x2c6c(%rip),%rbp # 403e50 <_DYNAMIC>
4011e4: 53 push %rbx
4011e5: 4c 29 fd sub %r15,%rbp
4011e8: 48 83 ec 08 sub $0x8,%rsp
4011ec: e8 0f fe ff ff callq 401000 <_init>
4011f1: 48 c1 fd 03 sar $0x3,%rbp
4011f5: 74 1f je 401216 <__libc_csu_init+0x56>
4011f7: 31 db xor %ebx,%ebx
4011f9: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
401200: 4c 89 f2 mov %r14,%rdx
401203: 4c 89 ee mov %r13,%rsi
401206: 44 89 e7 mov %r12d,%edi
401209: 41 ff 14 df callq *(%r15,%rbx,8)
40120d: 48 83 c3 01 add $0x1,%rbx
401211: 48 39 dd cmp %rbx,%rbp
401214: 75 ea jne 401200 <__libc_csu_init+0x40>
401216: 48 83 c4 08 add $0x8,%rsp
40121a: 5b pop %rbx
40121b: 5d pop %rbp
40121c: 41 5c pop %r12
40121e: 41 5d pop %r13
401220: 41 5e pop %r14
401222: 41 5f pop %r15
401224: c3 retq
401225: 66 66 2e 0f 1f 84 00 data16 nopw %cs:0x0(%rax,%rax,1)
40122c: 00 00 00 00
0000000000401230 <__libc_csu_fini>:
401230: f3 0f 1e fa endbr64
401234: c3 retq
Disassembly of section .fini:
0000000000401238 <_fini>:
401238: f3 0f 1e fa endbr64
40123c: 48 83 ec 08 sub $0x8,%rsp
401240: 48 83 c4 08 add $0x8,%rsp
401244: c3 retq
由上面的对比我们可以得出:
1) hello_asm比hello.asm多出了许多的内容,hello.asm(hello.o反汇编得到)只有.text节,而hello_asm(hello反汇编得到)有.init/.plt/.plt.sec等
2) hello_asm(hello反汇编)文件中的地址是虚拟地址,而hello.asm(hello.o反汇编)节中的是相对偏移地址
3) hello_asm中增加了许多外部链接的共享库函数。如puts@plt共享库函数,printf@plt共享库函数以及getchar@plt函数等
4) 跳转和函数调用的地址在hello_asm中是虚拟内存地址(都以main函数内部调用puts函数和exit函数为例),是基地址加上偏移量的形式
5.6 hello的执行流程
在edb中找到并加载hello可执行文件,如下图。
列出所有过程(第一种情况:终端输入./hello 1190200916 cy)。
子程序名 程序地址(16进制)
ld -2.27.so!_dl_start 7efb ff4d8ea0
ld-2.27.so!_dl_init 7efb ff4e7630
hello!_start 400500
libc-2.27.so!__libc_start_main 7efb ff100ab0
hello!printf@plt(调用了10次) 4004c0
hello!sleep@plt(调用了10次) 4004f0
hello!getchar@plt 4004d0
libc-2.27.so!exit 7efbff122120
列出所有过程(第二种情况:终端输入./hello)
子程序名 程序地址(16进制)
ld-2.27.so!_dl_start 7efb ff4d8ea0
ld-2.27.so!_dl_init 7efb ff4e7630
hello!_start 400500
libc-2.27.so!__libc_start_main 7efb ff100ab0
hello!puts@plt 4004b0
hello!exit@plt 4004e0
5.7 Hello的动态链接分析
1)对于动态共享链接库中PIC函数,编译器没有办法预测函数的运行时地址,所以需要为其添加重定位记录,并等待动态链接器处理。为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表PLT和全局偏移量表GOT实现函数的动态链接。其中GOT 中存放函数目标地址,PLT使用 GO T中地址跳转到目标函数。
2)附上dl_init函数调用前后GOT信息变化截图
dl_init函数调用前
dl_init函数调用后
3)我们进一步发现,改变的是:从地址0x6001008处,由00 00 00 00 00 00变为了70 01 70 ff fb 7e。由00 00 00 00 00 00变为80 e6 4e ff fb 7e。由于机器为小端,则这两处改编成的地址应该是0x7e fb ff 70 01 70和0x7e fb ff 4e e6 80。
4)在之后的函数调用时,首先跳转到PLT执行.plt中逻辑,第一次访问跳转时,GOT 地址为下一条指令,将函数序号压栈,然后跳转到PLT[0],在 PLT[0]中将重 定位表地址压栈,然后访问动态链接器,在动态链接器中使用函数序号和重定位 表确定函数运行时地址,重写 GOT,再将控制传递给目标函数。之后如果对同样函数调用,第一次访问跳转直接跳转到目标函数。
5.8 本章小结
这一章主要了解了链接的过程。通过对比hello和hello.o的反汇编代码,更好的掌握了链接当中重定位的原理。对链接的理解更深一步。
第6章 hello进程管理
6.1 进程的概念与作用
1)概念
进程的经典定义就是一个执行中的程序的实例。
2)作用
进程为应用程序提供关键的抽象:
·一个独立的逻辑控制流,它提供一个抽象,好像我们的程序独占地使用处理器。
·一个私有地地址空间,它提供一个抽象,好像我们的程序独占地使用内存系统。
6.2 简述壳Shell-bash的作用与处理流程
1)作用:
判断输入命令行是否为内置命令,如果是则解析它,否则认为是打开文件filename。
2)处理流程:
·将用户输入的命令行进行解析,分析是否是内置命令;
·若是内置命令,直接执行;若不是内置命令,则bash在初始子进程的上下文中加载和运行它。
·本质上就是shell在执行一系列的读和求值的步骤,在这个过程中,他同时可以接受来自终端的命令输入。
6.3 Hello的fork进程创建过程
父进程通过调用fork函数创建一个新的运行的子进程。新创建的子进程几乎但是并不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的但是独立的一份副本。这就意味着当父进程调用fork的时候,子进程可以读写进程当中任何的文件。父进程和子进程最大的区别在于它们有不同的进程ID。fork被调用一次,返回两次:一次是在父进程当中,一次是在新创建的子进程当中。
输入命令执行hello之后,父进程shell判断不是内部指令,认为是执行文件。所以会通过fork函数创建子进程执行hello。
6.4 Hello的execve过程
execve函数在当前进程的上下文当中记载并且运行一个新程序。execve函数加载并且运行可执行目标文件filename,且带参数列表argv和环境变量列表envp。
int execve(const char *filename,const char *argv[],const char *envp[])
其中:
filename:程序所在的路径和名称
argv:传递给程序的参数,数组指针argv必须以程序(filename)开头,NULL结尾
envp:传递给程序的新环境变量,无论是shell脚本,还是可执行文件都可以使用此环境变量,必须以NULL结尾
在execve加载了Hello之后,它调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数,该主函数有如下的原型:
int main(intargc , char **argv , char *envp)
结合上面的原理具体分析加载和执行hello的过程:
1.删除已存在的用户区域(自父进程独立)。
2.映射私有区:为Hello的代码、数据、.bss和栈区域创建新的区域结构,所有这些区域都是私有的、写时才复制的。
3.映射共享区:比如Hello程序与标准C库libc.so链接,这些对象都是动态链接到Hello的,然后再映射到用户虚拟地址空间中的共享区域内。
4.设置PC:exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。
另附进程地址空间:
其中,第一部分内核虚拟内存,也是内核空间,包括与进程相关的数据结构(页表、task、mm、系统内核栈),这个部分的内容依据每一个进程而定。另外,还包括物理存储区还有内核代码数据,这是所有进程都相同的区域。
6.5 Hello的进程执行
1)进程时间片
一个进程执行它的控制流的一部分的每一段时间叫做时间片。
2)调度
在进程执行的某些时刻,内核可以决定抢占当前进程,并且重新开始一个先前被抢占了的进程。这种决策就叫做调度,是由内核当中成为调度器的代码处理的。当内核选择了一个新的进程运行的时候,我们就说内核调度了这个进程。
3)进程上下文切换
上下文就是内核重新启动一个被抢占的进程所需要恢复的原来的状态,由寄存器、程序计数器、用户栈、内核栈和内核数据结构等对象的值构成。
在内核调度了一个新的进程之后,它就抢占当前的进程,并且使用上下文转换来把控制权转交给新的进程。但是需要注意的是,上下文切换有三种情况:1.保存当前进程的上下文2.恢复某个先前被抢占的进程所保存的上下文3.将控制权移交给新的进程。上下文切换如下所示:
4) 用户态和内核态的切换
处理器通常是用某个控制寄存器的一个模式位来限制一个应用可以执行的指令以及它可以访问的地址空间范围,这个寄存器描述了当前进程所拥有的权力。一个运行在内核模式下的进程可以执行任何指令,并且可以访问任何内存位置。进程运行在用户模式下就多了很多的限制。不允许停止寄存器,改变模式位,或者发起I/O操作。也不允许访问内核区域的代码数据。
6.6 hello的异常与信号处理
1)异常类型
类别 原因 异步/同步 返回行为
中断 来自I/O设备的信号 异步 总是返回到下一条指令
陷阱 有意的异常 同步 总是返回到下一条指令
故障 潜在可恢复的错误 同步 可能返回到当前指令
终止 不可恢复的错误 同步 不会返回
2)处理方式
中断:
陷阱:
故障:
终止:
3) 正常运行
4) Ctrl+C
进程收到 SIGINT 信号,结束 hello。在ps中查询不到其PID,在job中也没有显示,可以看出hello已经被彻底结束。
5) Ctrl+Z
进程收到 SIGSTP 信号, hello 进程挂起。用ps查看其进程PID,可以发现hello的PID是5269;再用jobs查看此时hello的后台 job号是1,调用 fg 1将其调回前台。其中,图中输入fg 1之后,并不再输出Hello 1190200916 cy的原因是:我们之前键入Ctrl+Z较晚,在此之前已经输出了所有的Hello 1190200916 cy。
6) 乱按
只是将屏幕的输入存入缓存区内,乱码也会试别是否是命令
7) Kill
挂起来的进程会被终止,ps当中没有PID
6.7本章小结
本章介绍了进程的创建、加载和终止。而这背后,都是由内核调用异常处理程序,处理不同的异常信号来完成的。程序是指令、数据及其组织形式的描述,进程是程序的实体。可以说,进程是运行的程序。对于进程的理解更进一步。
第7章 hello的存储管理
7.1 hello的存储器地址空间
1)逻辑地址空间:存储单元的地址可以用段基值和段内偏移量来表示,段基值确定它所在的段居于整个存储空间的位置,偏移量确定它在段内的位置,这种地址表示方式称为逻辑地址。在这里是指由hello产生的与段有关的偏移地址部分(hello.o)。
2)线性地址空间: 非负整数地址的有序集合。是逻辑地址到物理地址变换的中间一步。hello产生的段偏移加上基地址,就生成了线性地址空间。
3)虚拟地址空间: 有的时候我们也把线性地址空间称作虚拟地址空间。因为二者都与实际物理内存无关,都是虚拟的地址。
4)物理地址空间: 用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。加载到内存地址寄存器中的地址,内存单元的真正地址。如果启用了分页机制,那么hello的线性地址会使用页目录和页表中的项变换成hello的物理地址;如果没有启用分页机制,那么hello的线性地址就直接成为物理地址了。
7.2 Intel逻辑地址到线性地址的变换-段式管理
实模式下:逻辑地址CS:EA到物理地址CS*16+EA
保护模式下:以段描述符作为下标,到GDT/LDT表查表获得段地址,段地址+偏移地址=线性地址。
段选择符各字段含义
15-4 3-2 1-0
索引 TI RPL
TI=0,选择全局描述符表(GDT),TI=1,选择局部描述符表(LDT)。
RPL=00,为第0级,位于最高级的内核态,RPL=11,为第3级,位于最低级的用户态,第0级高于第3级。高13位,对应于8K个索引用来确定当前使用的段描述符在描述符表中的位置
截图1:intel储存器寻找
被选中的段描述符先被送至描述符cache,每次从描述符cache中取32位段基址,与32位段内偏移量(有效地址)相加得到线性地址(附上截图)
7.3 Hello的线性地址到物理地址的变换-页式管理
页式管理是为了减少碎片以及为了只在内存中存放那些反复执行或即将执行的程序段与数据部分,而把那些不经常执行的程序段和数据存放在外存待执行时调入,以提高内存利用率。
各进程的虚拟空间被划分为若干个长度相等的页(page).进程的虚地址变成页号p和页内地址w。内存空间也按页的大小划分为片或页面。
现在具体分析hello进程。
hello的线性地址空间划分:4GB = 1K个子空间1K个页面/子空间4KB/页。
虚拟地址被分为k个VPN和1个VPO。每一个VPNi都是一个到第i级页表的索引。第j级页表中的每一个PTE,都指向第j+1级的每一个页表的基地址。dik级页表中的每一个PTE包含某个物理页面的PPN,或者一个磁盘的地址。为了构造物理地址,在能够确定PPN之前,MMU必须访问k个PTE。PPO和VPO相同。
页式管理的优点
1)实现了内存碎片的减少。
2)实现了连续存储到非连续存储。为在内存中局部动态的存储那些反复执行或即将执行的数据和程序段打下了基础。
3)实现内外存存储器的统一管理。采用了请求调页或预调页技术,在内存中只存放那些经常执行或即将执行的页,存放与外存中需要时再调入。
7.4 TLB与四级页表支持下的VA到PA的变换
TLB:Translation Lookaside Buffer,翻译后备缓冲器,是MMU中的一个关于PTE的缓存。它是一个小的、虚拟寻址的缓存,它的每一行都保存着一个由单个PTE组成的块,结构如下:
n+1 p+t p+t-1 p p-1 0
TLB标记(TLBT) TLB索引(TLBI) VPO
前两者又称作虚拟页号(VPN),后p位是VPO(虚拟页面偏移量)。TLB命中的步骤如下所示:
-
CPU产生一个虚拟地址
-
MMU通过TLB缓冲的比对,得到相应的PTE
-
MMU将这个虚拟地址翻译成一个物理地址,把它发送给高速缓存/主存
-
高速缓存/主存将所请求的数据发送给CPU
下面是如果TLB没有命中的情形,如图:
-
CPU产生一个虚拟地址
-
MMU把VPN传送到TLB当中,但是没有命中,触发缺页异常处理程序
-
MMU从L1高速缓存当中取出相应的PTE,并且把新的PTE存放在TLB当中(这个过程中可能会覆盖其他原有的PTE)
-
缺页异常处理程序返回到触发的进程,再次执行导致缺页的命令
-
MMU通过TLB缓冲的比对,得到相应的PTE
-
MMU将这个虚拟地址翻译成一个物理地址,把它发送给高速缓存/主存
-
高速缓存/主存将所请求的数据发送给CPU
7.5 三级Cache支持下的物理内存访问
1)得到了物理地址VA,首先使用物理地址的CI进行组索引(每组8路),对8路的块分别匹配 CT进行标志位匹配。如果匹配成功且块的valid标志位为1,则命中hit。然后根据数据偏移量 CO取出数据并返回。
2)若没找到相匹配的或者标志位为0,则miss。那么cache向下一级cache,这里是二级cache甚至三级cache中寻找查询数据。然后逐级写入cache。
3)在更新cache的时候,需要判断是否有空闲块。若有空闲块(即有效位为0),则写入;若不存在,则进行驱逐一个块(LRU策略)。
7.6 hello进程fork时的内存映射
1)虚拟内存和内存映射解释了fork函数如何为hello进程提供私有的虚拟地址空间。
2)fork为hello的进程创建虚拟内存
创建当前进程的的mm_struct,vm_area_struct和页表的原样副本;两个进程中的每个页面都标记为只读;两个进程中的每个区域结构(vm_area_struct)都标记为私有的写时复制(COW)
3)在hello进程中返回时,hello进程拥有与调用fork进程相同的虚拟内存。
4)随后的写操作通过写时复制机制创建新页面
7.7 hello进程execve时的内存映射
1)在bash中的进程中执行了如下的execve调用:execve(“hello”,NULL,NULL);
2)execve函数在当前进程中加载并运行包含在可执行文件hello中的程序,用hello替代了当前bash中的程序。
下面是加载并运行hello的几个步骤:
·删除已存在的用户区域。
·映射私有区域
·映射共享区域
·设置程序计数器(PC)
exceve做的最后一件事是设置当前进程的上下文中的程序计数器,是指指向代码区域的入口点。而下一次调度这个进程时,他将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
7.8 缺页故障与缺页中断处理
页面命中完全是由硬件完成的,而处理缺页是由硬件和操作系统内核协作完成的:
图 27 缺页中断处理
整体的处理流程:
- 处理器生成一个虚拟地址,并将它传送给MMU
- MMU生成PTE地址,并从高速缓存/主存请求得到它
- 高速缓存/主存向MMU返回PTE
- PTE中的有效位是0,所以MMU出发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序。
- 缺页处理程序确认出物理内存中的牺牲页,如果这个页已经被修改了,则把它换到磁盘。
- 缺页处理程序页面调入新的页面,并更新内存中的PTE
- 缺页处理程序返回到原来的进程,再次执行导致缺页的命令。CPU将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面已经换存在物理内存中,所以就会命中。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称之为堆。堆的结构如下图:
用户栈
(向下生长)
共享库的内存映射区域
堆
(向上生长)
未初始化的数据(.bss)
已初始化的数据(.data)
代码(.text)
带边界标签的隐式空闲链表分配器管理
带边界标记的隐式空闲链表的每个块是由一个字的头部、有效载荷、可能的额外填充以及一个字的尾部组成的。
隐式空闲链表:在隐式空闲链表中,因为空闲块是通过头部中的大小字段隐含地连接着的。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。其中,一个设置了已分配的位而大小为零的终止头部将作为特殊标记的结束块。
当一个应用请求一个k字节的块时,分配器搜索空闲链表,查找一个足够大的可以放置所请求块的空闲块。分配器有三种放置策略:首次适配、下一次适配合最佳适配。分配完后可以分割空闲块减少内部碎片。同时分配器在面对释放一个已分配块时,可以合并空闲块,其中便利用隐式空闲链表的边界标记来进行合并。
块大小(头部) a/f
有效载荷
(只包括已分配的块)
填充(可选)
块大小(脚部) a/f
显示空间链表管理
显式空闲链表是将空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。如,堆可以组织成一个双向链表,在每个空闲块中,都包含一个前驱与一个后继指针。
显式空闲链表:在显式空闲链表中。可以采用后进先出的顺序维护链表,将最新释放的块放置在链表的开始处,也可以采用按照地址顺序来维护链表,其中链表中每个块的地址都小于它的后继地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。
31 3 210
块大小(头部) a/f
pred(祖先)
succ(后继)
填充(可选)
块大小(脚部) a/f
7.10本章小结
通过本章内容,我们探讨了虚拟地址、物理地址、线性地址、逻辑地址的区别。有明白了如何进行段式管理和页式管理,其中多级页表的复杂程度难以想象。而后我们又探讨了对于fork函数和execve函数,是怎么进行内存映射的。最后提供了动态内存分配的堆块的模型。通过这个模型来模拟计算机当中实际的操作。总而言之,我们对虚拟内存这一章的内存了解的更加透彻了。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
1)设备的模型化:文件
所有的I/O设备都被抽象成一个文件。甚至是磁盘、网络、终端、内核。所有的输入输出都被当做是文件的读和写来进行。这使得对于所有的设备,我们都能以一种通用的形式来管理。
2)设备管理:unix io接口
我们将所有的I/O设备都看作是文件之后,那么我们就允许Linux内核引出一个简单而低级的接口,称之为Unix I/O接口。我们可以借此,完成打开文件、改变文件当前的位置、读写文件和关闭文件。
8.2 简述Unix IO接口及其函数
1)接口
1.打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息,应用程序只需要记住这个描述符。
2.linuxshell创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。头文件定义了常量STDIN_FILENO、STOOUT_FILENO和STDERR_FILENO,它们可用来代替显式的描述符值。
3.改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。
4.读写文件。一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的文件,当k~m时执行读操作会触发一个称为end-of-file(EOF)的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF符号”。类似地,写操作就是从内存复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
2)函数
1.打开和关闭文件。
打开文件函数原型:int open(char* filename,int flags,mode_t mode)
返回值:若成功则为新文件描述符,否则返回-1;
flags:O_RDONLY(只读),O_WRONLY(只写),O_RDWR(可读写)
mode:指定新文件的访问权限位。
关闭文件函数原型:int close(fd)
返回值:成功返回0,否则为-1
2.读和写文件
读文件函数原型:ssize_t read(int fd,void *buf,size_t n)
返回值:成功则返回读的字节数,若EOF则为0,出错为-1
描述:从描述符为fd的当前文件位置复制最多n个字节到内存位置buf
写文件函数原型:ssize_t wirte(int fd,const void *buf,size_t n)
返回值:成功则返回写的字节数,出错则为-1
描述:从内存位置 buf 复制至多 n 个字节到描述符为 fd 的当前文件位置
8.3 printf的实现分析
printf函数:
int printf(const char *fmt, …)
{
int i;
va_list arg = (va_list)((char *)(&fmt) + 4);
i = vsprintf(buf, fmt, arg);
write(buf, i);
return i;
}
所引用的vsprintf函数
int vsprintf(char *buf, const char *fmt, va_list args)
{
char *p;
chartmp[256];
va_listp_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函数将所有的参数内容格式化之后存入buf,然后返回格式化数组的长度。write函数将buf中的i个元素写到终端。从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等。字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
getchar有一个int型的返回值。当程序调用getchar时,程序就等着用户按键,用户输入的字符被存放在键盘缓冲区中直到用户按回车为止(回车字符也放在缓冲区中)。
当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的第一个字符的ascii码,如出错返回-1,且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完为后,才等待用户按键。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章介绍了Linux的I/O设备的基本概念和管理方法,以及相关函数。最后分析了printf和getchar的工作原理。对Linux I/O设备的理解更进一步。
结论
Hello的一生是曲折的:
1) hello.c经过C preprocessor(cpp)预处理,插入#include指定文件,扩展用#define指定的宏,得到hello.i文件。
2) hello.i文件经过C compiler(ccl)编译,得到hello.s汇编文件。
3) hello.s文件经过Assembler(ass)汇编,得到二进制目标代码文件hello.o。
4) hello.o文件经过Linker(ld)链接,将目标代码文件与实现库函数的代码合并,得到最终的可执行文件Hello。
5) shell为hello进程execve,映射虚拟内存,进入程序入口之后,程序开始载入物理内存。
6) 进入main函数执行目标代码,CPU为运行的hello分配时间片,进入逻辑控制流。
7) 当程序执行完毕之后,shell父进程回收hello进程,内核删除相关的数据和代码。
CSAPP这本书也让我拓宽了眼界,原来国外的大学教材编写的如此详细,原来大学里的教材还可以如此面面俱到,不愧是计算机基础的巅峰之作。一开始我拿到这本沉甸甸的书那种震惊仿佛就在昨天,但是今天这门课的内容我已经都学完了,恍如隔日。好像都像做梦一样。不知不觉当中,我都已经到了该准备考研的阶段了,真是岁月不饶人啊。
附件
文件作用 文件名字
源代码c文件 hello.c
hello.c预处理得到的文件(2章-预处理) hello.i
hello.i编译得到的汇编文件(3章-编译) hello.s
hello.s汇编得到的二进制目标代码文件(4章-汇编) hello.o
hello.o反汇编得到的文本文件(4章-汇编) hello.asm
hello.o的elf文件(4章-汇编) hello.elf
hello.o链接得到的可执行文件(5章-链接) hello
hello的反汇编文件(5章-链接) hello_asm
hello的elf文件(5章-链接) hello_elf
参考文献
[1] 博客园 [转]printf 函数实现的深入剖析
[2] CSDN博客 物理地址虚拟地址逻辑地址线性地址
[3] 《深入理解计算机系统》 Randal E.Bryant David R.O’Hallaron 机械工业出版社