阅读笔记 摘自 《深刻理解计算机系统》仅记录了感兴趣的内容node
计算机系统是由硬件和软件组成的,它们共同工做来运行应用程序linux
/*hello.c*/ #include<stdio.h> int main() { printf("hello,word\n"); return 0; }
本次经过追踪hello程序的生命周期来开始对系统的初步了解程序员
位 源程序实际上就是一个由值0和1组成的位(又称比特)序列web
大部分的现代计算机系统都使用ASCII标准来表示文本字符,这种方式实际上就是用一个惟一的单字节大小的整数值来表示每一个字符算法
对于计算机系统来讲,信息就是位,都是由一串比特表示,而区分不一样数据对象的惟一方法是咱们读到这些数据对象时的上下文,这里先简单理解为经过上下文的信息识别该数据的类型shell
在Unix系统上,从源文件到目标文件的转化是由编译器驱动程序完成的:编程
预处理阶段:其实就是预处理C程序文件头,并把它直接插入程序文本中,结果就获得另外一个C程序,一般以.i做为文件拓展名ubuntu
编译阶段:编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,汇编语言程序。该程序包含函数main的定义,以下所示:windows
main: subq $8, %rsp movl $.LCO, %edi call puts movl $0, %eax addq $8, %rsp ret
汇编阶段:汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫作可重定位目标程序的格式,并将结果保存在目标文件hello.o中。hello.o是个二进制文件,都是指令编码数组
连接阶段:hello程序调用了printf函数,它是每一个C编译器都提供的标准C库中的一个函数。printf函数存在于一个名为printf.o的单独的预编译好了的目标文件中,而这个文件必须以某种方式合并到咱们的hello.o程序中那个。连接器(ld)就负责处理这种合并。结果就获得hello文件,他是一个可执行目标文件(简称可执行文件),能够被加载到内存中,由系统执行
shell是一个命令行解释器,它输出一个提示符,等待输入一个命令行,而后执行这个命令,若是该命令行的第一个单词不是一个内置的shell命令,那么shell就会假设这是一个可执行文件的名字,它将加载并执行这个文件
在Unix系统下,咱们要运行hello程序能够经过在shell中输入它的文件名:
linux> ./hello hello,word linux>
为了理解运行hello程序时发送了什么,咱们须要了解一个典型系统的硬件知识
总线
贯穿整个系统的一组电子管道,称做总线,它携带信息字节并负责在各个部件间传递。一般总线被设计成传送定长的字节块,也就是字。不一样系统设定的字节数不尽相同,主要分为32位和64位。
I/O设备
I/O设备是系统与外部世界的联系通道。每一个I/O设备都经过一个控制器或者适配器与I/O总线相连。控制器和适配器之间的区别主要在于他们的封装方式。控制器是I/O设备自己或者系统的主印刷电路板(即主板)上的芯片组。而适配器则是一块插在主板插槽上的卡。
主存
主存是一个临时存储设备,在处理器执行程序时,用来存放程序和程序处理的数据。而存储器是一个线性的字节数组,每一个字节都有其惟一的地址(数组索引),这些地址都是从零开始的,为了便于地址分配,进程资源管理等,后期还会引入虚拟内存的几率。
处理器
中央处理单元(CPU),简称处理器,是解释(或执行)存储在主存中指令的引擎。处理器的核心是一个大小为一个字的存储设备(或寄存器),称为程序计数器(PC)。在任什么时候刻,PC都指向主存中的某条机器语言指令(即含有该指令的地址)。从系统通电开始,直到系统断电,处理器一直在不断地执行程序计数器指向的指令,再更新程序计数器。
处理器看上去是按照一个很是简单的指令执行模型来操做的,这个模型是由指令集架构决定的。在这个模型中,指令按照严格的顺序执行,而执行一条指令包含执行一系列的步骤。
处理器看上去是它的指令集架构的简单实现,可是实际上现代处理器使用了很是复杂的机制来加速程序的执行。所以,咱们将处理器的指令集架构和处理器的微体系结构区分开来:指令集架构描述的是每条机器代码的效果;而微体系结构描述的是处理器其实是如何实现的。
了解这些基础硬件,应该可以大概描述出运行一个程序所进行的操做。
为了加快程序运行的速度,开始出现高速缓存存储器,存储设备造成层次结构
程序并无直接访问键盘、显示器、磁盘或者主存,而是依靠操做系统提供的服务,咱们能够把操做系统当作是应用程序和硬件之间插入的一层软件,全部应用程序对硬件的操做的尝试都必须经过操做系统
操做系统有两个基本功能:
(1)防止硬件被失控的应用程序滥用;
(2)向应用程序提供简单一致的机制来控制复杂而又一般大不相同的低级硬件设备;
操做系统经过几个抽象概念来实现这两个功能:进程、虚拟内存、文件
像hello这样的程序在现代系统上运行时,操做系统会提供一种假象,就好像系统上只有这个程序在运行。程序看上去是独占地使用处理器、主存和I/O设备。
进程其实是操做系统对一个正在运行的程序的一种抽象。在一个系统上能够同时运行多个程序,而每一个进程都好像在独占地使用硬件。而并发运行,则是说一个进程的指令和另外一个进程的指令是交错执行的。而操做系统实现这种交错执行的机制称为上下文切换。
操做系统保持跟踪进程运行所需的全部状态信息。这种状态,也就是上下文,包括许多信息,好比PC和寄存器文件的当前值,以及主存的内容。
在任何一个时刻,单处理器系统都只能执行一个进程的代码。当操做系统决定要把控制权从当前进程转移到某个新进程时,就会进行上下文切换,即保存当前进程的上下文、恢复新进程的上下文,而后将控制权传递到新进程。新进程就会从它上次中止的地方开始
现代系统中,一个进程实际上能够由多个称为线程的执行单元组成,每一个线程都运行在进程的上下文中,并共享一样的代码和全局数据。因为网络服务器对并行处理的需求,线程成为愈来愈多重要的编程模型,由于多线程之间比多进程之间更容易共享数据,比进程更高效
虚拟内存是一个抽象概念,它为每一个进程提供了一个假象,即每一个进程都在独占地使用主存。每一个进程看到的内存都是一致的,称为虚拟地址空间。
虚拟内存的运做须要硬件和操做系统软件之间精密复杂的交互,包括对处理器生成的每一个地址的硬件翻译。基本思想是把一个进程虚拟内存的内容存储在磁盘上,而后用主存做为磁盘的高速缓存。
文件就是字节序列。每一个外部设备均可以当作是文件,系统中的全部输入输出都是经过使用一小组称为Unix I/O的系统函数调用读写文件来实现的
它向应用程序提供了一个统一的试图,来看待系统中可能含有的全部各式各样的I/O设备。
系统常常经过网络和其余系统链接到一块儿,从一个单独的系统来看,网络可视为一个I/O设备。当系统从主存赋值一串字节到网络适配器时,数据流通过网络到达另外一台机器。类似地,系统能够读取其余机器发送来的数据,并把数据复制到本身的内存
譬如:客户端和服务器之间交互的类型在全部网络应用中是最典型的
数字计算机的整个历史中,有两个需求是驱动进步的持续动力:一个是咱们想要计算机作得更多,另外一个是咱们想要计算机运行得更快。
并发:指一个同时具备多个活动的系统;
并行:指的是用并发来是一个系统运行得更快
按照系统层次结构中由高到低的顺序重点强调三个层次
1.线程级并发
2.指令级并行
3.单指令、多数据并行
计算机系统提供的一些抽象,计算机系统中的一个重大主题就是提供不一样层次的抽象表示,来隐藏实际实现的复杂性
计算机一般使用8位的块,或者字节(byte),做为最小的可寻址的内存单位,而不是访问内存中单独的位
机器级程序将内存视为一个很是大的字节数组,称为虚拟内存。内存的每一个字节都由一个惟一的数字来标识,称它为地址,全部可能地址的集合称为虚拟地址空间。
每一个程序对象能够简单地视为一个字节块,而程序自己就是一个字节序列。
在C语言中,以0x或0X开头的数字常量被认为是十六进制(简写为“hex”)的值。例如 0xFA1D37B
记住十六进制数字A、C和F相应的十进制值 A->10 ; C->12 ; F->15
关于进制间的转换,熟能生巧,忘了就查资料,一般用脚本或工具会比较多
每台计算机都有一个字长,也就是处理器一次处理的最大数据块长度。由于虚拟地址是以这样的一个字来编码的,因此字长决定的最重要的系统参数就是虚拟地址空间的最大大小。也就是说,对于一个字长为w位的机器而言,虚拟地址的范围就是0~2w-1,程序最多访问2w个字节
32位字长限制虚拟地址空间为4GB,扩展到64位字长使得虚拟地址空间为16EB
"32位程序"或"64位程序"区别在于该程序如何编译的,而不是其运行的机器类型
基本C数据类型的典型大小(以字节为单位),分配的字节数受程序是如何编译的影响而变化
大端法:最高有效字节在最前面
小端法:最低有效字节在最前面
X86体系都是选用小端模式
这种字节顺序在单一系统上是没有什么区别的,不过有时候,字节顺序会成为问题。
首先是在不一样类型的机器之间经过网络传送二进制数据时,一个常见的问题是当小端法机器产生的数据被发送到大端法机器或者反过来时,接收程序会发现,字里的字节成了反序的,这就须要机器在发送时转化为网络标准,而接收方则将网络标准转化为它的内部表示
第二种状况,当阅读表示整数数据的字节序列时字节顺序也很重要。
第三种.......
每一个字符都由某个标准编码来表示,最多见的是ASCII字符码
使用ASCII码做为字符码的任何系统上都将获得相同的结果,与字节顺序和字大小规则无关。所以,文本数据比二进制数据具备更强的平台独立性
ASCII字符集适合编码英文文档,可是在表达一些特殊字符方面并无太多办法,所以引入了unicode编码,称为"统一字符集"
关于无符号数、有符号数、小数、浮点数四则运算、规格化、运算溢出等知识,在大学的专业基础课中都有学过,这里再也不作补充,理解就好,仍是同样,须要用的时候忘了就查资料
须要时,为加深理解再回来从新整理
IA32,x86-64的32位前身,是Intel在1985年提出的
ISA,指令集体系结构或指令集架构,定义机器级程序的格式和行为,定义处理器状态、指令的格式,以及每条指令对状态的影响
目标是利用编译器[GCC](https://zh.wikipedia.org/wiki/GCC)
展现如何查看汇编代码,并将它反向映射到高级编程语言中的结构
/*p.c*/ #include <stdio.h> long mult2(long,long); void multstore(long x,long y,long *dest){ long t = mult2(x,y); *dest = t; }
在命令行上使用“-S”选项,就能看到C语言编译器产生的汇编代码:
linux> gcc -og -S p.c
这会使GCC运行编译器,产生一个汇编文件p.s,可是不作其余进一步的工做(一般状况下,它还会继续调用汇编器产生目标代码文件)
汇编代码文件包含各类声明:
.file "p.c" .text .globl multstore .type multstore, @function multstore: .LFB0: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 subq $48, %rsp movq %rdi, -24(%rbp) movq %rsi, -32(%rbp) movq %rdx, -40(%rbp) movq -32(%rbp), %rdx movq -24(%rbp), %rax movq %rdx, %rsi movq %rax, %rdi call mult2 movq %rax, -8(%rbp) movq -40(%rbp), %rax movq -8(%rbp), %rdx movq %rdx, (%rax) nop leave .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: .size multstore, .-multstore .ident "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.11) 5.4.0 20160609" .section .note.GNU-stack,"",@progbits
上面代码中,multstore函数声明下每一个缩进去的行都对应一条机器指令。
好比,pushq指令表示应该将寄存器%rbp的内容压入程序栈中。
若是咱们使用"-c"命令行选项,GCC会编译并汇编该代码:
linux> gcc -og -c p.c
这就会产生目标代码文件p.o,它是二进制格式的,因此没法直接查看
下面是文件中一段16字节的序列,它的十六进制表示为:
55 48 89 e5 48 83 ec 30 48 89 7d e8 48 89 75 e0
机器执行的程序只是一个字节序列,它是对一系列指令的编码。机器对产生这些指令的源代码几乎一无所知
要查看机器代码文件(可重定位文件)的内容,有一类称为反汇编器(disassembler)的程序很是有用
这些程序根据机器代码产生一种相似于汇编代码的格式。在Linux中,带-d
命令行标志的程序OBJDUMP(表示"object dump")能够充当这个角色:
linux> objdump -d p.o
结果以下:
Disassembly of section .text: 0000000000000000 <multstore>: 0: 55 push %rbp 1: 48 89 e5 mov %rsp,%rbp 4: 48 83 ec 30 sub $0x30,%rsp 8: 48 89 7d e8 mov %rdi,-0x18(%rbp) c: 48 89 75 e0 mov %rsi,-0x20(%rbp) 10: 48 89 55 d8 mov %rdx,-0x28(%rbp) 14: 48 8b 55 e0 mov -0x20(%rbp),%rdx 18: 48 8b 45 e8 mov -0x18(%rbp),%rax 1c: 48 89 d6 mov %rdx,%rsi 1f: 48 89 c7 mov %rax,%rdi 22: e8 00 00 00 00 callq 27 <multstore+0x27> 27: 48 89 45 f8 mov %rax,-0x8(%rbp) 2b: 48 8b 45 d8 mov -0x28(%rbp),%rax 2f: 48 8b 55 f8 mov -0x8(%rbp),%rdx 33: 48 89 10 mov %rdx,(%rax) 36: 90 nop 37: c9 leaveq 38: c3 retq
在左边,咱们看到按照前面给出的字节顺序排列的16个十六进制字节值,它们分红了若干组,每组有1~5个字节。每组都是一条指令,右边是等价的汇编语言。
其中一些关于机器代码和它的反汇编表示的特性值得注意:
- x86-64 的指令长度从1到15字节不等,经常使用的指令以及操做数较少的指令所需的字节数少,而那些不太经常使用或操做数较多的指令所需字节数较多
- 设计指令格式的方式是,从某个给定位置开始,能够将字节惟一地解码成机器指令。例如,只有质量pushq %rbp是以字节值55开头的
- 反汇编器只是基于机器代码文件中的字节序列来肯定汇编代码。它不须要访问该程序的源代码或汇编代码
- 反汇编器使用的指令命名规则与GCC生成的汇编代码使用的有写细微的差异,好比pushq,‘q'彻底能够省略
生成实际可执行文件的代码须要对一组目标代码文件运行连接器,而这一组目标代码文件中必须含有一个main函数:
/* pp.c */ #include <stdio.h> void multstore(long,long,long *); int main(){ long d; multstore(2,3,&d); printf("2 * 3--> %ld\n",d ); return 0; } long mult2(long a,long b){ long s = a * b; return s; }
而后,用以下方法生成可执行文件prog
linux> gcc -og -o prog pp.c p.c
文件prog变成了8655个字节,由于它不只包含了两个过程的代码,还包含了用来启动和终止程序的代码,以及用来与操做系统交互的代码。咱们也能够反汇编prog文件:
linux> objdump -d prog
反汇编会抽取出各类代码序列:
Disassembly of section .init: 0000000000400428 <_init>: 400428: 48 83 ec 08 sub $0x8,%rsp 40042c: 48 8b 05 c5 0b 20 00 mov 0x200bc5(%rip),%rax # 600ff8 <_DYNAMIC+0x1d0> 400433: 48 85 c0 test %rax,%rax 400436: 74 05 je 40043d <_init+0x15> 400438: e8 53 00 00 00 callq 400490 <__libc_start_main@plt+0x10> 40043d: 48 83 c4 08 add $0x8,%rsp 400441: c3 retq Disassembly of section .plt: 0000000000400450 <__stack_chk_fail@plt-0x10>: 400450: ff 35 b2 0b 20 00 pushq 0x200bb2(%rip) # 601008 <_GLOBAL_OFFSET_TABLE_+0x8> 400456: ff 25 b4 0b 20 00 jmpq *0x200bb4(%rip) # 601010 <_GLOBAL_OFFSET_TABLE_+0x10> 40045c: 0f 1f 40 00 nopl 0x0(%rax) 0000000000400460 <__stack_chk_fail@plt>: 400460: ff 25 b2 0b 20 00 jmpq *0x200bb2(%rip) # 601018 <_GLOBAL_OFFSET_TABLE_+0x18> 400466: 68 00 00 00 00 pushq $0x0 40046b: e9 e0 ff ff ff jmpq 400450 <_init+0x28> 0000000000400470 <printf@plt>: 400470: ff 25 aa 0b 20 00 jmpq *0x200baa(%rip) # 601020 <_GLOBAL_OFFSET_TABLE_+0x20> 400476: 68 01 00 00 00 pushq $0x1 40047b: e9 d0 ff ff ff jmpq 400450 <_init+0x28> 0000000000400480 <__libc_start_main@plt>: 400480: ff 25 a2 0b 20 00 jmpq *0x200ba2(%rip) # 601028 <_GLOBAL_OFFSET_TABLE_+0x28> 400486: 68 02 00 00 00 pushq $0x2 40048b: e9 c0 ff ff ff jmpq 400450 <_init+0x28> Disassembly of section .plt.got: 0000000000400490 <.plt.got>: 400490: ff 25 62 0b 20 00 jmpq *0x200b62(%rip) # 600ff8 <_DYNAMIC+0x1d0> 400496: 66 90 xchg %ax,%ax Disassembly of section .text: 00000000004004a0 <_start>: 4004a0: 31 ed xor %ebp,%ebp 4004a2: 49 89 d1 mov %rdx,%r9 4004a5: 5e pop %rsi 4004a6: 48 89 e2 mov %rsp,%rdx 4004a9: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp 4004ad: 50 push %rax 4004ae: 54 push %rsp 4004af: 49 c7 c0 c0 06 40 00 mov $0x4006c0,%r8 4004b6: 48 c7 c1 50 06 40 00 mov $0x400650,%rcx 4004bd: 48 c7 c7 96 05 40 00 mov $0x400596,%rdi 4004c4: e8 b7 ff ff ff callq 400480 <__libc_start_main@plt> 4004c9: f4 hlt 4004ca: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1) 00000000004004d0 <deregister_tm_clones>: 4004d0: b8 47 10 60 00 mov $0x601047,%eax 4004d5: 55 push %rbp 4004d6: 48 2d 40 10 60 00 sub $0x601040,%rax 4004dc: 48 83 f8 0e cmp $0xe,%rax 4004e0: 48 89 e5 mov %rsp,%rbp 4004e3: 76 1b jbe 400500 <deregister_tm_clones+0x30> 4004e5: b8 00 00 00 00 mov $0x0,%eax 4004ea: 48 85 c0 test %rax,%rax 4004ed: 74 11 je 400500 <deregister_tm_clones+0x30> 4004ef: 5d pop %rbp 4004f0: bf 40 10 60 00 mov $0x601040,%edi 4004f5: ff e0 jmpq *%rax 4004f7: 66 0f 1f 84 00 00 00 nopw 0x0(%rax,%rax,1) 4004fe: 00 00 400500: 5d pop %rbp 400501: c3 retq 400502: 0f 1f 40 00 nopl 0x0(%rax) 400506: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1) 40050d: 00 00 00 0000000000400510 <register_tm_clones>: 400510: be 40 10 60 00 mov $0x601040,%esi 400515: 55 push %rbp 400516: 48 81 ee 40 10 60 00 sub $0x601040,%rsi 40051d: 48 c1 fe 03 sar $0x3,%rsi 400521: 48 89 e5 mov %rsp,%rbp 400524: 48 89 f0 mov %rsi,%rax 400527: 48 c1 e8 3f shr $0x3f,%rax 40052b: 48 01 c6 add %rax,%rsi 40052e: 48 d1 fe sar %rsi 400531: 74 15 je 400548 <register_tm_clones+0x38> 400533: b8 00 00 00 00 mov $0x0,%eax 400538: 48 85 c0 test %rax,%rax 40053b: 74 0b je 400548 <register_tm_clones+0x38> 40053d: 5d pop %rbp 40053e: bf 40 10 60 00 mov $0x601040,%edi 400543: ff e0 jmpq *%rax 400545: 0f 1f 00 nopl (%rax) 400548: 5d pop %rbp 400549: c3 retq 40054a: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1) 0000000000400550 <__do_global_dtors_aux>: 400550: 80 3d e9 0a 20 00 00 cmpb $0x0,0x200ae9(%rip) # 601040 <__TMC_END__> 400557: 75 11 jne 40056a <__do_global_dtors_aux+0x1a> 400559: 55 push %rbp 40055a: 48 89 e5 mov %rsp,%rbp 40055d: e8 6e ff ff ff callq 4004d0 <deregister_tm_clones> 400562: 5d pop %rbp 400563: c6 05 d6 0a 20 00 01 movb $0x1,0x200ad6(%rip) # 601040 <__TMC_END__> 40056a: f3 c3 repz retq 40056c: 0f 1f 40 00 nopl 0x0(%rax) 0000000000400570 <frame_dummy>: 400570: bf 20 0e 60 00 mov $0x600e20,%edi 400575: 48 83 3f 00 cmpq $0x0,(%rdi) 400579: 75 05 jne 400580 <frame_dummy+0x10> 40057b: eb 93 jmp 400510 <register_tm_clones> 40057d: 0f 1f 00 nopl (%rax) 400580: b8 00 00 00 00 mov $0x0,%eax 400585: 48 85 c0 test %rax,%rax 400588: 74 f1 je 40057b <frame_dummy+0xb> 40058a: 55 push %rbp 40058b: 48 89 e5 mov %rsp,%rbp 40058e: ff d0 callq *%rax 400590: 5d pop %rbp 400591: e9 7a ff ff ff jmpq 400510 <register_tm_clones> 0000000000400596 <main>: 400596: 55 push %rbp 400597: 48 89 e5 mov %rsp,%rbp 40059a: 48 83 ec 10 sub $0x10,%rsp 40059e: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax 4005a5: 00 00 4005a7: 48 89 45 f8 mov %rax,-0x8(%rbp) 4005ab: 31 c0 xor %eax,%eax 4005ad: 48 8d 45 f0 lea -0x10(%rbp),%rax 4005b1: 48 89 c2 mov %rax,%rdx 4005b4: be 03 00 00 00 mov $0x3,%esi 4005b9: bf 02 00 00 00 mov $0x2,%edi 4005be: e8 50 00 00 00 callq 400613 <multstore> 4005c3: 48 8b 45 f0 mov -0x10(%rbp),%rax 4005c7: 48 89 c6 mov %rax,%rsi 4005ca: bf d4 06 40 00 mov $0x4006d4,%edi 4005cf: b8 00 00 00 00 mov $0x0,%eax 4005d4: e8 97 fe ff ff callq 400470 <printf@plt> 4005d9: b8 00 00 00 00 mov $0x0,%eax 4005de: 48 8b 4d f8 mov -0x8(%rbp),%rcx 4005e2: 64 48 33 0c 25 28 00 xor %fs:0x28,%rcx 4005e9: 00 00 4005eb: 74 05 je 4005f2 <main+0x5c> 4005ed: e8 6e fe ff ff callq 400460 <__stack_chk_fail@plt> 4005f2: c9 leaveq 4005f3: c3 retq 00000000004005f4 <mult2>: 4005f4: 55 push %rbp 4005f5: 48 89 e5 mov %rsp,%rbp 4005f8: 48 89 7d e8 mov %rdi,-0x18(%rbp) 4005fc: 48 89 75 e0 mov %rsi,-0x20(%rbp) 400600: 48 8b 45 e8 mov -0x18(%rbp),%rax 400604: 48 0f af 45 e0 imul -0x20(%rbp),%rax 400609: 48 89 45 f8 mov %rax,-0x8(%rbp) 40060d: 48 8b 45 f8 mov -0x8(%rbp),%rax 400611: 5d pop %rbp 400612: c3 retq 0000000000400613 <multstore>: 400613: 55 push %rbp 400614: 48 89 e5 mov %rsp,%rbp 400617: 48 83 ec 30 sub $0x30,%rsp 40061b: 48 89 7d e8 mov %rdi,-0x18(%rbp) 40061f: 48 89 75 e0 mov %rsi,-0x20(%rbp) 400623: 48 89 55 d8 mov %rdx,-0x28(%rbp) 400627: 48 8b 55 e0 mov -0x20(%rbp),%rdx 40062b: 48 8b 45 e8 mov -0x18(%rbp),%rax 40062f: 48 89 d6 mov %rdx,%rsi 400632: 48 89 c7 mov %rax,%rdi 400635: e8 ba ff ff ff callq 4005f4 <mult2> 40063a: 48 89 45 f8 mov %rax,-0x8(%rbp) 40063e: 48 8b 45 d8 mov -0x28(%rbp),%rax 400642: 48 8b 55 f8 mov -0x8(%rbp),%rdx 400646: 48 89 10 mov %rdx,(%rax) 400649: 90 nop 40064a: c9 leaveq 40064b: c3 retq 40064c: 0f 1f 40 00 nopl 0x0(%rax) 0000000000400650 <__libc_csu_init>: 400650: 41 57 push %r15 400652: 41 56 push %r14 400654: 41 89 ff mov %edi,%r15d 400657: 41 55 push %r13 400659: 41 54 push %r12 40065b: 4c 8d 25 ae 07 20 00 lea 0x2007ae(%rip),%r12 # 600e10 <__frame_dummy_init_array_entry> 400662: 55 push %rbp 400663: 48 8d 2d ae 07 20 00 lea 0x2007ae(%rip),%rbp # 600e18 <__init_array_end> 40066a: 53 push %rbx 40066b: 49 89 f6 mov %rsi,%r14 40066e: 49 89 d5 mov %rdx,%r13 400671: 4c 29 e5 sub %r12,%rbp 400674: 48 83 ec 08 sub $0x8,%rsp 400678: 48 c1 fd 03 sar $0x3,%rbp 40067c: e8 a7 fd ff ff callq 400428 <_init> 400681: 48 85 ed test %rbp,%rbp 400684: 74 20 je 4006a6 <__libc_csu_init+0x56> 400686: 31 db xor %ebx,%ebx 400688: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1) 40068f: 00 400690: 4c 89 ea mov %r13,%rdx 400693: 4c 89 f6 mov %r14,%rsi 400696: 44 89 ff mov %r15d,%edi 400699: 41 ff 14 dc callq *(%r12,%rbx,8) 40069d: 48 83 c3 01 add $0x1,%rbx 4006a1: 48 39 eb cmp %rbp,%rbx 4006a4: 75 ea jne 400690 <__libc_csu_init+0x40> 4006a6: 48 83 c4 08 add $0x8,%rsp 4006aa: 5b pop %rbx 4006ab: 5d pop %rbp 4006ac: 41 5c pop %r12 4006ae: 41 5d pop %r13 4006b0: 41 5e pop %r14 4006b2: 41 5f pop %r15 4006b4: c3 retq 4006b5: 90 nop 4006b6: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1) 4006bd: 00 00 00 00000000004006c0 <__libc_csu_fini>: 4006c0: f3 c3 repz retq Disassembly of section .fini: 00000000004006c4 <_fini>: 4006c4: 48 83 ec 08 sub $0x8,%rsp 4006c8: 48 83 c4 08 add $0x8,%rsp 4006cc: c3 retq
抽出一段:
0000000000400613 <multstore>: 400613: 55 push %rbp 400614: 48 89 e5 mov %rsp,%rbp 400617: 48 83 ec 30 sub $0x30,%rsp 40061b: 48 89 7d e8 mov %rdi,-0x18(%rbp) 40061f: 48 89 75 e0 mov %rsi,-0x20(%rbp) 400623: 48 89 55 d8 mov %rdx,-0x28(%rbp) 400627: 48 8b 55 e0 mov -0x20(%rbp),%rdx 40062b: 48 8b 45 e8 mov -0x18(%rbp),%rax 40062f: 48 89 d6 mov %rdx,%rsi 400632: 48 89 c7 mov %rax,%rdi 400635: e8 ba ff ff ff callq 4005f4 <mult2> 40063a: 48 89 45 f8 mov %rax,-0x8(%rbp) 40063e: 48 8b 45 d8 mov -0x28(%rbp),%rax 400642: 48 8b 55 f8 mov -0x8(%rbp),%rdx 400646: 48 89 10 mov %rdx,(%rax) 400649: 90 nop 40064a: c9 leaveq 40064b: c3 retq 40064c: 0f 1f 40 00 nopl 0x0(%rax)
这段代码与以前p.c反汇编产生的代码几乎彻底同样。其中主要的区别是左边列出的地址不一样-----连接器将这段代码的地址移到了一段不一样的地址范围中
第二个不一样之处在于连接器填上了callq指令调用函数mult2须要使用的地址;连接器的任务之一就是为函数调用找到匹配的函数的可执行代码的位置。
最后一个区别是多了两行代码,插入这些指令是为了使函数代码变为16字节,对程序并无影响,使得就存储器系统性能而言,能更好地放置下一个代码块
大多数GCC生成的汇编代码指令都有一个字符的后缀,代表操做数的大小。例如,数据传送指令由四个变种:movb(传送字节)、movw(传送字)、movl(传送双字)、和movq(传送四字)
一个x86-64的中央处理单元(CPU)包含一组16个存储64位值得通用目的寄存器,用来存储整数数据和指针
一般有一组标准的编程规范控制着如何使用寄存器来管理栈、传递函数参数、从函数的返回值,以及存储局部和临时数据。
大多数指令有一个或多个操做数,指示出执行一个操做中要使用的源数据值,以及放置结果的目的位置。
源数据能够以常数形式给出,或是从寄存器或内存中读出。结果能够存放在寄存器或内存中。所以,各类不一样的操做数的可能性被分为三种类型:当即数 、寄存器、内存引用
也就是三种基本的寻址方式,能够在学习汇编语言的时候接触到
最频繁使用的指令是将数据从一个位置复制到另外一个位置的指令。
四种不一样的指令都执行相同的操做;主要区别在于它们操做的数据大小不一样
源操做数指定的值是一个当即数,存储在寄存器中或者内存中。目的操做数指定一个位置,要么是一个寄存器或者,要么是一个内存地址。
x86-64加了一条限制,传送指令的两个操做数不能都指向内存位置。将一个值从一个内存位置复制到另外一个内存位置须要两条指令:
第一条指令将源值加载到寄存器中,第二条将该寄存器值写入目的的位置,也就是必须通过寄存器
mov指令只会更新目的操做数指定的那些寄存器字节或内存位置,惟一例外的是movl指令以寄存器做为目的时,会把该寄存器的高位4字节设置为0
在将较小的源值复制到较大的目的时使用双大小指示符:
第一个字符指定源的大小,第二个指名目的的大小
数据扩展方式
①零扩展:MOVZ类中的指令把目的中剩余的字节填充为0
②符号扩展:MOVS类中的指令经过符号扩展来填充,把源操做的最高位进行复制
/* C语言代码*/ long exchange(long *xp,long y) { long x = *xp; *xp = y; return x; }
/*汇编代码*/ exchange: movq (%rdi),%rax movq %rsi,(%rdi) ret
函数exchange由三条指令实现:两条数据传送(movq),加上一条返回函数被调用点的指令(ret)
可知参数经过寄存器传递给函数,函数经过把值存储在寄存器%rax或者寄存器的某个低位部分中返回
执行过程描述: 过程参数xp和y分别存储在寄存器%rdi和%rsi中。而后,指令2从内存中读出x,把它存放到寄存器%rax中,直接实现了C程序中的操做x=xp。稍后,用寄存器%rax从这个函数返回一个值,于是返回值就是x。指令3将y写入到寄存器%rdi中的xp指向的内存位置,直接实现了xp=y。
栈是一种数据结构,能够经过添加或者删除值,不过要遵循“后进先出”的原则。经过push操做把数据压入栈中,经过pop操做删除数据;它具备一个属性:弹出的值永远是最近被压入并且仍然在栈中的值。在x86-64中,程序栈存放在内存中某个区域,栈向下增加,这样一来,栈顶元素的地址是全部栈中元素地址最低的,栈指针%rsp保存着栈顶元素的地址
分为四类:加载有效地址、一元操做、二元操做和移位
加载有效地址(load effective address)指令leaq其实是movq指令的变形。它的指令形式是从内存读数据到寄存器,但实际上它根本就没有引用内存
它的第一个操做数看上去是一个内存引用,但该指令并非从指定的位置读入数据,而是将有效地址写入到目的操做数。
leaq指令能执行加法和有限形式的乘法,例如:若是寄存器%rdx的值为x,那么指令leaq 7(%rdx,%rdx,4),%rax 将设置寄存器%rax的值为5x+7.
一元操做,只有一个操做数,既是源又是目的。这个操做数能够是一个寄存器,也能够是一个内存位置
二元操做,其中,第二个操做数既是源又是目的。即源操做数是第一个,目的操做数是第二个,最终的结果是放回目的,第二个操做数不能是当即数
先给出移位量,而后第二项给出的是要移位的数。能够进行算术和逻辑移位。移位量能够是一个当即数,或者放在单字节寄存器%cl中。原则上1字节的移位量是的移位量的编码范围能够达到28-1=255。x86-64中,移位操做对w位长的数据值进行操做,移位量又%cl寄存器的**低m位决定的,这里2m=w。高位会被忽略**。
例如:寄存器%cl的十六进制值为0xFF时,指令salb会移7位,salw会移15位,sall会移21位,而salq会移63位
左移指令有两个名字:SAL和SHL。二者的效果同样的,都是将右边填上0
右移指令不一样,SAR执行算术移位(填上符号位,其实就是移位后符号不能变),而SHR执行逻辑移位(填上0)
移位操做数的目的操做数能够是一个寄存器或者一个内存位置;只有右移位须要区分符号和无符号数
对两个64位有符号或无符号整数相乘获得的乘积须要128位来表示。x86-64指令集对128位数的操做提供有限的支持
扩展方式是高64位放在%rdx,低64位放在%rax
到目前为止,咱们只考虑了直线代码的行为,也就是指令一条接着一条顺序地执行。但若是要实现条件控制、循环等操做,该怎么办呢
除了整数寄存器,CPU还维护着一组单个位的条件码寄存器,它们描述了最近的算术或逻辑操做的属性。还能够检测这些寄存器来执行条件分支指令,常见的条件码有:
CF:进位标志。最近的操做使最高位产生了进位。可用来检查无符号操做的溢出
ZF:零标志。最近的操做得出的结果为0
SF:符号标志。最近的操做获得的结果为负数
OF:溢出标志。最近的操做致使一个补码溢出----正溢出或负溢出
leaq指令不改变任何条件码,由于它是用来进行地址计算的
与条件码相关的指令(只设置条件码而不更新目的寄存器):CMP(比较大小)、TEST(与AND指令相同操做)
大多数运算符都会更新目的寄存器,同时设置对应的条件码
条件码一般不会直接读取,经常使用的使用方法有三种:
①能够根据条件码的某种组合,将一个字节设置为0或1
SET指令;指令名字的不一样后缀指明了他们所考虑的条件码的组合,必须明确指令的后缀表示不一样的条件而不是操做数大小
②能够条件跳转到程序的某个其余的部分
③能够有条件地传送数据
正常执行的状况下,指令按照它们出现的顺序一条条地执行。跳转(jump)指令会致使执行切换到程序中一个全新的位置
跳转指令jmp能够是无条件跳转;也能够是直接跳转;或者是间接跳转,区别只是跳转目标是否做为指令的一部分编码,是否来源于寄存器或内存位置读出
了解跳转指令的目标如何编码,对研究连接很是有帮助,也能帮助理解反汇编器的输出
①条件控制
用跳转指令jmp、jge,结合有条件和无条件跳转
当条件知足时,程序沿一条直线路径执行,当条件不知足时就走另外一条
②条件传送(数据)
用comv指令
这种方法计算一个条件操做的两种结果,而后再根据条件是否知足从中选取一个
了解常见的几种循环在汇编代码的表示:do、while、for、switch
逆向工程:理解产生的汇编代码与原始代码之间的关系,关键是找到程序值和寄存器之间的映射关系
x86-64 的过程实现包括一组特殊的指令和一些对机器资源(例如寄存器和程序内存)使用的约定规则
要提供对过程的机器级支持,必需要处理许多不一样的属性。假设过程p调用过程Q,Q执行后返回p。这些动做包括下面一个或多个机制:
传递控制 在进入过程Q的时候,程序计数器必须被设置为Q的代码的起始地址,而后再返回时,要把程序计数器设置为p中调用Q后面那条指令的地址
传递数据 p必须可以向Q提供一个或多个参数,Q必须可以向p返回一个值
分配和释放内存 在开始时,Q可能须要为局部变量分配空间,而在返回前,又必须释放这些存储空间
栈数据结构提供了后进先出的内存管理原则;程序能够用栈来管理它的过程所须要的存储空间,栈和程序寄存器存放着传递控制和数据、分配内存所需的信息。当p调用Q时,控制和数据信息添加到栈尾,当p返回时,这些信息会释放掉
当x86-64过程须要的存储空间超出寄存器可以存放的大小时,就会在栈上分配空间。这个部分称为过程的栈帧。当前正在执行的过程的帧总在栈顶,大多数过程的栈帧都是定长的,在过程的开始就分配好了,而实际上,许多函数甚至根本就不须要栈帧。
将控制从函数p转移到函数Q只须要简单地把程序计数器(PC)设置为Q的代码起始位置。不过,当稍后从Q返回的时候,处理器必须记录好它须要继续p的执行的代码位置。在x86-64机器中,这个信息是用指令call Q调用过程Q来记录的。该指令会把地址A压入栈中,并将PC设置为Q的起始地址。压入的地址A被称为返回地址,是紧跟着call指令后面的那条指令的地址。对应的指令ret会从栈中弹出地址A,并把PC设置为A
当调用一个过程时,除了要把控制传递给它并在过程返回时再传递回来以外,过程调用还可能包括把数据做为参数传递,而从过程返回还可能包括一个返回值。x86-64中,大部分过程间的数据传送是经过寄存器实现。
寄存器不足够存放全部的本地数据;
对一个局部变量使用地址运算符‘&’,所以必须可以为它产生一个地址;
某些局部变量是数组或结构,所以必须可以经过数组或结构引用被访问到
通常来讲,过程经过减少栈指针在栈上分配空间。分配的结果做为栈帧的一部分,标号为“局部变量”
寄存器组是惟一被全部过程共享的资源
必须确保当一个过程(调用者)调用另外一个过程时,被调用者不会覆盖调用者稍后会使用的寄存器值
压入寄存器的值会在栈帧中建立标号“保存的寄存器”
寄存器和栈的惯例使得过程可以递归地调用它们自身。每一个过程调用在栈中都有它本身的私有空间,所以多个未完成调用的局部变量不会相互影响。此外,栈的原则很天然地就提供了适当的策略,当过程被调用时分配局部存储,当返回时释放存储
C语言的不一样寻常的特色是能够产生指向数组中元素的指针,并对这些指针进行运算。在机器代码中,这些指针会被翻译成地址计算
x86-64的内存引用指令能够用来简化数组访问。例如,假设E是一个int型的数组,而咱们想计算E[i],在此,E的地址存放在寄存器%rdx中,而i存放在寄存器%rcx中,而后,指令movl (%rdx,%rcx,4),%eax
会执行地址计算Xe+4i,读这个内存位置的值,并将结果存放到寄存器%eax中
经过下面的命令行来启动GDB:
linux> gdb prog
commands | result |
---|---|
quit | 退出GDB |
run | 运行程序(在此给出命令行参数) |
kill | 中止程序 |
break multstore | 在函数multstore入口处设置断点 |
break * 0x400540 | 在地址0x400540 |
delete 1 | 删除断点1 |
delete | 删除全部断点 |
stepi | 执行1条指令 |
stepi 4 | 执行4条指令 |
nexti | 相似stepi,但以函数调用为单位 |
continue | 继续执行 |
finish | 运行到当前函数返回 |
disas | 反汇编当前函数 |
disas multstore | 反汇编函数multstore |
disas 0x400544 | 反汇编位于地址0x400544附件的函数 |
disas 0x400540,0x40054d | 反汇编指定地址范围内的代码 |
print /x $rip | 以十六进制输出程序计数器的值 |
print $rax | 以十进制输出%rax的内容 |
print /x $rax | 以十六进制输出%rax的内容 |
print /t $rax | 以二进制输出%rax的内容 |
print 0x100 | 输出0x100的十进制表示 |
print /x 555 | 输出555的十六进制表示 |
print /x ($rsp+8) | 以十六进制输出%rsp的内容加上8 |
print *(long *) 0x7fffffffe818 | 输出位于地址0x7fffffffe818的长整数 |
print *(long *)($rsp+8) | 输出位于地址%rsp+8处的长整数 |
x/2g 0x7fffffffe818 | 检查从地址0x7fffffffe818开始的双字 |
x20bmultstore | 检查函数multstore的前20个字节 |
info frame | 有关当前栈帧的信息 |
info registers | 全部寄存器的值 |
help |
对越界的数组元素的写操做会破坏存储在栈中的状态信息
一种特别常见的状态破坏称为缓冲区溢出(buffer overflow)。一般,在栈中分配某个字符数组来保存一个字符串,可是字符串的长度超出了为数组分配的空间
缓冲区溢出
的一个更加致命的使用就是让程序执行它原本不肯意执行的函数
一般,输入给程序一个字符串,这个字符串包含一些可执行代码的字节编码,称为攻击编码(exploit code),另外,还有一些字节会用一个指向攻击代码的指针覆盖返回地址。那么,执行ret指令的效果就是跳转到攻击代码
在一种攻击形式中,攻击代码会使用系统调用启动一个shell程序,给攻击者提供一组操做系统函数。另外一种攻击形式中,攻击代码会执行一些未受权的任务,修复栈的破坏,而后第二次执行ret指令,(表面上)正常返回到调用者
在1988年11月,著名的Internet蠕虫病毒经过Internet以四种不一样的方法获取对多计算机的访问。一种是对finger守护进程fingerd的缓冲区溢出攻击,fingerd服务FINGER命令请求。经过以一个适当的字符串调用FINGER,蠕虫能够远程的守护进程缓冲区并执行一段代码,让蠕虫访问远程系统。一旦蠕虫得到了对系统的访问,它就能自我复制,几乎彻底地消耗掉机器上全部的计算机资源。
1.栈随机化
为了在系统中插入攻击代码,攻击者既要插入代码,也要插入指向这段代码的指针,这个指针也是攻击字符串的一部分。产生这个指针须要知道这个字符串放置的栈地址。
栈随机化的思想使得栈的位置在程序每次运行时都有变化。所以,即便许多机器都运行一样的代码,他们的栈地址都是不一样的
然而一个执著的攻击者老是可以用蛮力克服随机化,他能够反复用不一样的地址进行攻击。一种常见的把戏就是在实际的攻击代码前插入很长一段nop(读做“no op”,no operatioin的缩写)指令。执行这种指令除了对程序计数器加一,使之指向下一条指令以外,没有任何效果。只要攻击者可以猜中这段序列中的某个地址,程序就会通过这个序列,到达攻击代码。这个序列经常使用的术语是“空操做雪橇(nop sled)”,意思是程序会“滑过”这个序列。若是咱们创建一个256字节的nop sled,那么枚举215=32768个起始地址,就能破解n=223的随机化,这对于一个顽固的攻击者来讲,是彻底可行的。若是是对于64位的系统,就要尝试枚举2^24=16777216
2.栈破坏检测
在产生的代码中加入了一种栈保护者机制,来检测缓冲区越界,其思想是在栈帧中任何局部缓冲区与栈状态之间存储一种特殊的金丝雀值,也称哨兵值
,是在程序每次运行时随机产生的;在恢复寄存器状态和从函数返回以前,程序检查这个金丝雀值是否杯该函数的某个操做或者该函数调用的某个函数的某个操做改变了,若是是,那么程序异常停止
3.限制可执行代码区域
消除攻击者想系统中插入可执行代码的能力,限制哪些内存区域可以存放可执行代码。
为了管理变长栈帧,x86代码使用寄存器%rbp做为帧指针(frame pointer)(有时称为基指针(base pointer),这也是%rbp中bp两个字母的由来)。当使用帧指针时,栈帧的组织结构与图中函数vframe的状况同样。能够看到代码必须把%rbp以前的值保存到栈中,由于它是一个被调用者保存寄存器。而后在函数的整个执行过程当中,都使得%rbp指向那个时刻栈的位置,而后用固定长度的局部变量相对于%rbp的偏移量来引用他们
程序中的每条指令都会读取或修改处理器状态的某些部分。这称为程序员可见状态。
Y86-64程序员可见状态。同x86-64 同样,Y86-64的程序能够访问和修改程序寄存器、状态码、程序计数器(PC)和内存。状态码指明程序是否容许正常,或者发生了某个特殊事件
内存从概念上来讲就是一个很大的字节数组,保存着程序和数据。Y86-64程序用虚拟地址来引用内存位置。硬件和操做系统软件联合起来将虚拟地址翻译成实际物理地址,指明数据实际存在内存中哪一个地方。
程序状态的最后一部分是状态码Stat,它代表程序执行的整体状态
指令的字节级编码。每条指令须要1~10个字节不等,这取决于须要哪些字段。每条指令的第一个字节代表指令的类型,这个字节分为两部分,每部分4位:高4位是代码部分,低4位是功能部分
功能值只有在一组相关指令共用一个代码时才有用,区分不一样功能。
Y86-64 15个程序寄存器中每一个都有一个相对应的范围在0到0xE之间的寄存器标识符,程序寄存器存在CPU中的一个寄存器文件中,这个寄存器文件就是一个小的、以寄存器ID为地址的随机访问存储器。在指令编码中以及在硬件设计中,当须要指明不该访问任何寄存器时,就用ID值0xF来表示
有的指令只有一个字节长,而有的须要操做数的指令编码就更长一些。指令集的一个重要性质就是字节编码必须惟一的解释。任意一个字节序列要么是一个惟一的指令序列的编码,要么就不是一个合法的字节序列。
例如:用十六进制来表示指令rmmovq %rsp,0x123456789abcd(%rdx)的字节编码。
rmmovq的第一个字节为40.源寄存器%rsp应该编码放在rA字段中,而基址寄存器%rdx应该编码放在rB字段中。两个寄存器的ID分别为4和2.最后,偏移量编码放在8字节的常数字中。首先在0x123456789abcd的前面填充0变成8个字节,变成字节序列00 01 23 45 67 89 ab cd。写成按字节反序就是4042cdab896745230100
比较x86-64和Y86-64的指令编码
同x86-64中的指令编码相比,Y86-64的编码简单得多,可是没有那么紧凑。在全部的Y86-64指令中,寄存器字段的位置都是固定的,而在不一样的x86-64指令中,它们的位置是不同的。x86-64能够将常数值编码成一、二、4或8个字节,而Y86-64老是将常数值编码成8字节
RISC和CISC指令集
对Y86-64来讲,程序员可见的状态包括状态码Stat,它描述程序执行的整体状态
当遇到这些异常的时候,咱们简单地让处理器中止执行指令。在更完整的设计中,处理器一般会调用一个异常处理程序,这个过程被指定用来处理遇到的某种类型的异常
SEQ处理器,每一个时钟周期时间,SEQ执行处理一条完整指令所需的全部步骤。不过,这须要一个很长的时钟周期时间,所以时钟周期频率会低到不可接受,而最终要实现的是流水线化的处理器
一般,处理一条指令包括不少操做。将它们组织成某个特殊的阶段序列,即便指令的动做差别很大,但全部的指令都遵循统一的序列。每一步的具体处理取决于正在执行的指令。
取指(fetch):取指阶段从内存读取指令字节,地址为程序计数器(PC)的值。从指令中抽取出指令指示符字节的两个四位部分,称为icode(指令代码)和ifun(指令功能)。它可能取出一个寄存器指示符字节(寄存器ID),指明一个或两个寄存器操做数
译码(decode):译码阶段从寄存器文件读入最多两个操做数
执行(execute):在执行阶段,算术/逻辑单元(ALU)要么执行指令指明的操做(根据ifun的值),计算内存引用的有效地址,要么增长或减小栈指针。在此可能设置条件码,对一条条件指令来讲,这个阶段会检验条件码和传送条件,若是条件成立,则更新目标寄存器。一样,对一条跳转指令来讲,这个阶段会决定是否是应该选择分支
访存(memory):访存阶段能够将数据写入内存,或者从内存读出数据。
写回(write back):写回阶段最多能够写两个结果到寄存器文件
更新PC(PC update):将PC设置成下一条指令的地址
----SEQ硬件结构
----乱序处理器框图
本章作了一个大概的浏览,若是已经上过相关计算机基础课程,我想已经对存储器技术及在计算机中的地位有了必定的了解,这本书讲的很详细,这里只作个小结,方便往后须要时再去翻阅
基于存储技术包括随机存储器(RAM)、非易失性存储器(ROM)和磁盘。RAM有两种基本类型。静态RAM(SRAM)快一些,可是也贵一些,它便可以用做CPU芯片上的高速缓存,也能够也用做芯片下的高速缓存。动态RAM(DRAM)慢一些,也便宜一些,用做主存和图形帧缓冲区。即便是在关电的时候,ROM也能保持他们的信息,能够用来存储固件。旋转磁盘是机械的非易失性存储设备,以每一个位很低的成本保存大量的数据,可是其访问时间比DRAM长不少。固态硬盘(SSD)基于非易失性的闪存,对某些应用来讲,愈来愈成为旋转磁盘的具备吸引力的替代产品.
通常而言,较快的存储技术每一个位会更贵,并且容量更小。这些技术的价格和性能属性正在以显著不一样的速度变化着。特别的,DRAM和磁盘访问时间远远大于CPU周期时间。系统经过将存储器组织成存储设备的层次结构来弥补这些差别,在这个层次结构中,较小、较快的设备在顶部,较大、较慢的设备在底部。由于编写良好的程序有好的局部性,大多数数据均可以较高层获得服务,若是就是存储系统能以较高层的速度运行,但却有较低层的成本和容量.
连接(linking)是将各类代码和数据片断收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。
在早期的计算机系统中,连接是手动执行的。在现代系统中,连接是由叫连接器(linker)的程序自动执行的。连接器在软件开发中扮演着一个关键的角色,由于它们使得分离编译成为可能,不用担忧牵一发动全身。
大多数编译系统提供编译器驱动程序,它表明用户在须要时调用语言预处理器、编译器、汇编器和连接器
举例:
linux> gcc -og -o add main.c sum.c
执行完该指令后,ASCII码源文件翻译成可执行目标文件过程以下
这个过程在前面已经详述过了 --> hello.c经历的四个阶段
可重定位目标文件由各类不一样的代码和数据节(section)组成,每一节都是一个连续的字节序列
为了构造可执行文件,连接器必须完成两个主要任务:
目标文件是字节块的集合。这些块包含程序代码,有些包含程序数据,而其余的则包含引导连接器和加载器的数据结构。
目标文件有三种形式:
编译器和汇编器生成可重定位目标文件(包括共享目标文件)。连接器生成可执行目标文件。从技术上来讲,一个目标模块就是一个字节序列,而一个目标文件就是一个以文件形式存放在磁盘中的目标模块。
如图是一个典型的ELF可重定位目标文件格式。ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。夹在ELF头和节头部表之间的都是节,节头部表描述中间各个不一样节的位置和大小,其中目标文件中每一个节都有一个固定的条目。
.text:已编译程序的机器代码
.rodata:只读数据
.data:已初始化的全局和静态C变量。局部C变量在运行时被保存在栈中,即不出如今.data节中,也不出如今.bss节(表示未初始化的数据)中
.symtab:一个符号表,它存放在程序中定义和引用的函数和全局变量的信息
.rel.text:一个.text节中位置的列表,当连接器把这个目标文件和其余文件组合时,须要修改这些位置
.rel.data:被模块引用或定义的全部全局变量的重定位信息
.debug:一个调试符号表,其条目是程序中定义的局部变量和类型定义,程序中定义和引用的全局变量,以及原始的C的源文件
.line:原始C源程序中行号和.text节中机器指令之间的映射
.strtab:一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部表的节名字。字符串表就是以null结尾的字符串的序列
在符号解析阶段,连接器从左到右按照它们在编译器驱动程序命令行上出现的顺序来扫描可重定位目标文件和存档文件,将扫描到的符号分为三类,其中一类就是未解析的符号,即须要利用静态库来解析的引用符号
连接器一般会利用静态库来解析引用:全部的编译系统都有提供一种机制,将全部相关的目标模块打包成为一个单独的文件,它能够用做连接器的输入。当连接器构造一个输出的可执行文件时,它只复制静态库里被应用程序引用的目标模块,例如libc.a中的printf.o模块
重定位节和符号定义 使程序中的每条指令和全局变量都有惟一的运行时内存地址
重定位节中的符号引用 连接器修改代码节和数据节中对每一个符号的引用,使得它们指向正确的运行时的地址。要执行这一步,连接器依赖于可重定位目标模块中的重定位条目(rel.text、rel.data)
当汇编器生成一个目标模块时,它并不知道数据和代码最终将放在内存中的什么位置。它也不知道这个模块引用的任何外部定义的函数或者全局变量的位置。因此,不管什么时候汇编器遇到对最终位置未知的目标引用,它就会生成一个重定位条目,告诉连接器在将目标文件合并成可执行文件时如何修改这个引用。代码重定位条目放在.rel.text中,已初始化数据的重定位条目放在.rel.data中
ELF定义了32种不一样的重定位类型,其中有两种最基本的重定位类型:
R_X86_64_PC32 重定位一个使用32位PC相对地址的引用
R_X86_64_32 重定位一个使用32位绝对地址的引用,CPU直接在指令中编码的32值做为有效地址
举例:反编译一个可重定位文件,其中一个指令有以下的重定位的形式
指令地址 指令编码 指令 重定位地址 <符号引用> 4004de: e8 05 00 00 00 callq 4004e8 <sum>
一个典型的ELF可执行文件包含加载程序到内存并运行它所需的全部信息
ELF头描述文件的整体格式
起始地址,也就是程序的入口点(entry point)也就是当程序运行时要执行的第一条指令的地址
.text、.rodata和.data节与可重定位目标文件中的节是相识的,除了这些节已经被重定位到它们最终的运行时内存地址外
.init节定义了一个叫作_init的函数,程序的初始化代码会调用它。由于可执行文件是彻底连接的(已被重定位),因此它不须要rel节
ELF可执行文件被设计得很容易加载到内存,可执行文件的连续的片(chunk)被映射到连续的内存段。程序头部表描述了这种映射关系
off:目标文件中的偏移;vaddr/paddr:内存地址;align:对齐要求;filesz:目标文件中的段大小;memsz:内存中的段大小;flags:运行时访问权限
这里先只关注可执行目标文件的内容初始化两个内存段。
LOAD off 0x0000000000000000 vaddr 0x0000000000400000 paddr 0x0000000000400000 align 2**21 filesz 0x000000000000085c memsz 0x000000000000085c flags r-x LOAD off 0x0000000000000e10 vaddr 0x0000000000600e10 paddr 0x0000000000600e10 align 2**21 filesz 0x0000000000000230 memsz 0x0000000000000238 flags rw-
一、2两行告诉咱们第一段(代码段)有读/执行 访问权限,开始于内存地址0x400000处,总共的内存大小是0x85c字节,而且被初始化为可执行目标文件的头0x85c字节,其中包括ELF头、程序头部表以及.init、.text和.rodata节
三、4行告诉咱们第二段(数据段)有读/写 访问权限,开始于内存地址0x600e10处,总的内存大小为0x238字节,并用从目标文件中偏移0xe10处开始的.data节中的0x230个字节初始化.该段中剩下的8个字节对应于运行时将被初始化为0的.bss数据
对于任何段s,连接器必须选择一个起始地址vaddr,使得vaddr mod align=off mod align
这里,off是目标文件中段的第一个节的偏移量,align是程序头部中指定的对齐(2^21=0x200000)
举例:
vaddr mod align = 0x600e10 mod 0x200000 = 0xe10
off mod align = 0xe10 mod 0x200000 = 0xe10
这个对齐要求是一种优化,使得当程序执行时,目标文件中的段可以颇有效率地传送到内存中
首先shell会认为prog是一个可执行目标文件,经过调用某个驻留在存储器中称为加载器(loader)的操做系统代码来运做它。任何Linux程序均可以经过调用execve
函数来调用加载器。
加载器将可执行目标文件中的代码和数据从磁盘复制到内存中,而后经过跳转到程序的第一条指令或入口点来运行该程序,这个将程序复制到内存并运行的过程叫作加载
每一个Linux程序都有一个运行时内存映像。在Linux x86-64系统中,代码段老是从地址0x400000处开始,32位系统从0x08048000处开始,后面是数据段。运行时堆在数据段以后,经过调用malloc库往上增加。堆后面的区域是为共享模块保留的。用户栈老是从最大的合法用户地址(248-1)开始,向较小内存地址增加。栈上的区域,从地址248开始,是为内核中的代码和数据保留的,所谓内核就是操做系统驻留在内存的部分
栈顶放在最大的合法用户地址处
加载器实际是如何工做的?这里作个概述
Linux系统中每一个程序都运行在一个进程上下文中,有本身的虚拟地址空间。当shell运行一个程序时,父shell进程生成一个子进程,它是父进程的一个复制。子进程经过execve系统调用启动加载器。加载器删除子进程现有的虚拟内存段,并建立一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零。经过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据被初始化为可执行文件的内容。最后,加载器跳转到_start地址,它最终会调用应用程序的main函数。除了一些头部信息,在加载过程当中没有任何从磁盘到内存的数据复制。直到CPU引用一个被映射的虚拟页时才会进行复制,此时,操做系统利用它的页面调度机制自动将页面从磁盘传送到内存
共享库(shared library)是致力于解决静态库缺陷的一个现代创新产物。共享库是一个目标模块,在运行或加载时,能够加载到任意的内存地址,并和一个内存中的程序连接起来。这个过程为动态连接(dynamic linking),是由一个叫作动态连接器(dynamic linker)的程序执行的。共享库也成为共享目标(shared object),在Linux系统中一般用.so后缀
来表示。微软的操做系统大量地使用了共享库,它们称为DLL(动态连接库)
没有任何libvector.so的代码和数据节真的被复制到可执行文件prog21中。反之,连接器复制了一些重定位和符号表信息,它们使得运行时能够解析对libvector.so中代码和数据的引用。
当加载器加载和运行可执行文件prog21时,加载部分连接的可执行文件prog21.接着,prog21中包含一个.interp节
,这一节包含动态连接器的路径名,动态连接器自己就是一个共享目标,加载器不会像它一般所作的那样将控制传递给应用,而是加载和运行这个动态连接器。而后,动态连接器经过执行重定位完成连接任务:重定位共享库文本和数据到某个内存段,重定位共享库定义的符号的引用
最后,动态连接器将控制传递给应用程序,从这一时刻开始,共享库的位置就固定了,而且在程序执行的过程当中都不会改变
这是共享库的一种使用情景,无需在编译时将那些库连接到库中。如:微软windows应用开发者分发软件;构建高性能web服务器
共享库的主要目的就是运行多个正在运行的进程共享内存中相同的库代码
为解决多个进程共享程序的一个副本时,形成了库在内存中的分配管理问题
现代系统以这样一种方式编译共享模块的代码段,使得能够把它们加载到内存的任何位置而无需连接器修改。能够加载而无需重定位的代码称为位置无关代码
在x86-64系统中,对同一个目标模块中符号的引用是不须要特殊处理使之成为PIC,能够用PC相对寻址来编译这些引用,构造目标文件时由静态连接器重定位。
ldd:列出一个可执行文件在运行时所须要的共享库
objdunp:全部二进制工具之母。可以显示一个目标文件中全部的信息。它最大的做用是反汇编.text节中的二进制指令
size:列出目标文件中节的名字和大小
readelf:显示一个目标文件的完整结构,包括ELF头中编码的全部信息
异常控制流(ECF)发生在计算机系统的各个层次,是计算机系统中提供并发的基本机制。
在硬件层,异常是由处理器中的事件触发的控制流中的突变。控制流传递给一个软件处理程序,该处理程序进行一些处理,而后返回控制给被中断的控制流
有四种不一样类型的异常:中断
、故障
、终止
和陷阱
。当一个外部I/O设备设置了处理器芯片上的中断管脚时,中断会异步的发生。控制返回到故障指令后面的那条指令,一条指令的执行可能致使故障和终止同步发生。故障处理程序会从新启动故障指令,而终止处理程序从不将控制返回给被中断的流。最后,陷阱就像是用来实现向应用提供操做系统代码的受控的入口点的系统调用的函数调用
在操做系统层,内核用ECF提供进程的基本概念。进程提供给应用两个重要的抽象:1)逻辑控制流,它提供给每一个程序一个假象,好像它是在独占地使用处理器,2)私有地址空间,它提供给每一个程序一个假象,好像它是在独占地使用主存
在操做系统和应用程序之间的接口处,应用程序能够建立子进程,等待它们的子进程中止或者终止,运行新的程序,以及捕获来自其余进程的信号。
最后,在应用层,C程序可使用非本地跳转来规避正常的调用/返回栈规则,而且直接从一个函数分支到另外一个函数
程序和进程
程序时一堆代码和数据;程序能够做为目标文件存在于磁盘上,或者做为段存在于地址空间中。进程是执行中程序的一个具体的实例;程序老是运行在某个进程的上下文中。
fork函数在新的子进程中运行相同的程序,新的子进程是父进程的复制品。execve函数在当前进程的上下文中加载并执行一个新的程序。它会覆盖当前进程的地址空间,但并无建立一个新进程。新的程序仍然有相同的PID,而且继承了调用execve函数时已达开的全部文件描述符
虚拟内存是对主存的一个抽象。支持虚拟内存的处理器经过使用一种叫作虚拟寻址的间接形式来引用主存。处理器产生一个虚拟地址,在被发送到主存以前,这个地址被翻译成一个物理地址。从虚拟地址空间到物理地址空间的地址翻译要求硬件和软件紧密合做。专门的硬件经过使用页表来翻译虚拟地址,而页表的内容是由操做系统提供的。
虚拟内存提供三个重要的功能。第一,它在主存中自动缓存最近使用的存放磁盘上的虚拟地址空间的内容。虚拟内存缓存中的块叫作页。对磁盘上页的引用会触发缺页,缺页将控制转移到操做系统中的一个缺页处理程序。缺页处理程序将页面从磁盘复制到主存缓存,若是必要,将写回被驱逐的页。第二,虚拟内存简化了内存管理,进而又简化了连接、在进程间共享数据、进程的内存分配以及程序加载。最后,虚拟内存经过在每条页表条目中加入保护位,从而了简化了内存保护。
地址翻译的过程必须和系统中全部的硬件缓存的操做集成在一块儿。大多数页表条目位于L1高速缓存中,可是一个称为TLB的页表条目的片上高速缓存,一般会消除访问在L1上的页表条目的开销。
现代系统经过将虚拟内存片和磁盘上的文件片关联起来,来初始化虚拟内存片,这个过程称为内存映射
。内存映射为共享数据、建立新的进程以及加载程序提供了一种高效的机制。应用可使用mmap函数来手工地建立和删除虚拟地址空间的区域。然而,大多数程序依赖于动态内存分配器,例如malloc,它管理虚拟地址空间区域内一个称为堆的区域。动态内存分配器是一个感受像系统级程序的应用级程序,它直接操做内存,而无需类型系统的不少帮助。分配器有两种类型。显式分配器
要求应用显式地释放它们的内存块。隐式分配器
(垃圾收集器)自动释听任何未使用的和不可达的块。
对于C程序员来讲,管理和使用虚拟内存是一件困难和容易出错的任务。常见的错误示例包括:间接引用坏指针
,读取未初始化的内存
,容许栈缓冲区溢出
,假设指针和它们指向的对象大小相同,引用指针而不是它所指向的对象,误解指针运算
,引用不存在的变量
,以及引发内存泄漏
。
后续内容不太想记录,根据感兴趣的点进行补充,便于查询,其余直接阅读书籍理解下了解下便可,网络编程和并发编程貌似不是我读这本书太关注的重点,并且也将在其余书籍研究这块内容,读到这附近我以为我已经弥补了不熟悉的部分
虚拟内存是单机系统最重要的几个底层原理之一,它由底层硬件和操做系统二者软硬件结合来实现,是硬件异常,硬件地址翻译,主存,磁盘文件和内核的完美交互。它主要提供了3个能力:
给全部进程提供一致的地址空间,每一个进程都认为本身是在独占使用单机系统的存储资源
保护每一个进程的地址空间不被其余进程破坏,隔离了进程的地址访问
根据缓存原理,上层存储是下层存储的缓存,虚拟内存把主存做为磁盘的高速缓存,在主存和磁盘之间根据须要来回传送数据,高效地使用了主存
包括几块内容
虚拟地址和物理地址
页表
地址翻译
虚拟内存相关的数据结构
内存映射
对于每一个进程来讲,它使用到的都是虚拟地址,每一个进程都看到同样的虚拟地址空间,对于32位计算机系统来讲,它的虚拟地址空间是 0 - 2^32,也就是0 - 4G。对于64位的计算机系统来讲,理论的虚拟地址空间是 0 - 2^64,远高于目前常见的物理内存空间。虚拟地址空间不须要和物理地址空间同样大小。
Linux内核把虚拟地址空间分为两部分: 用户进程空间和内核进程空间,二者的比例通常是3:1,好比4G的虚拟地址空间,3G用户用户进程,1G用于内核进程。
在说CPU高速缓存的时候说过CPU只直接和寄存器和高速缓存打交道,CPU在执行进程的指令时要取一个实际的物理地址的值的时候主要有几步:
把进程指令使用的虚拟地址经过MMU转换成物理地址
把物理地址映射到高速缓存的缓存行
若是高速缓存命中就返回
若是不命中,就产生一个缓存缺失中断,从主存相应的物理地址取值,并加载到高速缓存中。CPU从中断中恢复,继续执行中断前的指令
因此高速缓存是和物理地址相映射的,进程指令中使用到的是虚拟地址。
在缓存原理中,数据都是按块来进行逻辑划分的,一次换入/换出的数据都是以块为最小单位,这样提升了数据处理的性能。一样的原理应用到具体的内存管理时,使用了页(page)来表示块,虚拟地址空间划分为多个固定大小的虚拟页(Virtual Page, VP),物理地址空间划分为多个固定大小的物理页(Physical Page, PP), 一般虚拟页大小等于物理页大小,这样简化了虚拟页和物理页的映射。虚拟页的大小一般在4KB - 2MB之间。在JVM调优的时候有时候会使用2MB的大内存页来提升GC的性能。
要明白一个重要的概念:
对于CPU来讲,它的目标存储器是物理内存,使用高速缓存作物理内存的缓存
一样,对于虚拟内存来讲,它的目标存储器是磁盘空间,使用物理内存作磁盘的缓存
因此,从缓存原理的角度来理解,在任什么时候刻,虚拟页的集合都分为3个不相交的子集:
未分配的页,即没有任何数据和这些虚拟页关联,不占用任何磁盘空间
缓存的页,即已经分配了的虚拟页,而且已经缓存在具体的物理页中
未缓存的页,即已经为磁盘文件分配了虚拟页,可是尚未缓存到具体的物理页中
虚拟内存系统和高速缓存系统同样,须要判断一个虚拟页面是否缓存在DRAM(主存)中,若是命中,就直接找到对应的物理页。若是不命中,操做系统须要知道这个虚拟页对应磁盘的哪一个位置,而后根据相应的替换策略从DRAM中选择一个牺牲的物理页,把虚拟页从磁盘中加载到DRAM物理主存中
虚拟内存的这种缓存管理机制是经过操做系统内核,MMU(内存管理单元)中的地址翻译硬件和每一个进程存放在主存中的页表(page table)数据结构来实现的。
页表(page table)是存放在主存中的,每一个进程维护一个单独的页表。它是一种管理虚拟内存页和物理内存页映射和缓存状态的数据结构。它逻辑上是由页表条目(Page Table Entry, PTE)为基本元素构成的数组。
数组的索引号对应着虚拟页号
数组的值对应着物理页号
数组的值能够留出几位来表示有效位,权限控制位。有效位为1的时候表示虚拟页已经缓存。有效位为0,数组值为null时,表示未分配。有效位为0,数组值不为null,表示已经分配了虚拟页,可是还未缓存到具体的物理页中。权限控制位有可读,可写,是否须要root权限
SRAM缓存表示位于CPU和主存之间的L一、L2和L3高速缓存。
DARM缓存的命中称为页命中,不命中称为缺页。举个例子来讲,
CPU要访问的一个虚拟地址在虚拟页3上(VP3),经过地址翻译硬件从页表的3号页表条目中取出内容,发现有效位0,即没有缓存,就产生一个缺页异常
缺页异常调用内核的缺页异常处理程序,它会根据替换算法选择一个DRAM中的牺牲页,好比PP3。PP3中已经缓存了VP4对应的磁盘文件的内容,若是VP4的内容有改动,就刷新到磁盘中去。而后把VP3对应的磁盘文件内容加载到PP3中。而后更新页表条目,把PTE3指向PP3,并修改PTE4,再也不指向PP3.
缺页异常处理程序返回后从新启动缺页异常前的指令,这时候虚拟地址对应的内容已经缓存在主存中了,页命中也可让地址翻译硬件正常处理了
磁盘和主存之间传送页的活动叫作交换(swapping)或者页面调度(页面调入,页面调出)。现代操做系统都采用按需调度的策略,即不命中发生时才调入页面。操做系统都会在主存中分配一块交换区(swap)来做缓冲区,加速页面调度。
因为页的交换会引发磁盘流量,因此具备好的局部性的程序能够大大减小磁盘流量,提升性能。而若是局部性很差产生大量缺页,从而致使不断地在磁盘和主存交换页,这种现象叫缓存颠簸。能够用Unix的函数getrusage来统计缺页的次数
现代操做系统都采用多级页表的方式来压缩页表的大小。举个例子,
对于32位的机器来讲,支持4G的虚拟内存大小,若是每一个页是4KB大小,那么采用一级页表的话,须要10^6个页表条目PTE。32位机器的页表条目是4个字节,那么页表须要4MB大小的空间。
假设使用4MB大小的页,那么只须要103个页表项。假设每一个4MB大小的页又分为4KB大小的子页,那么每一个4MB大小的页须要103个的页表项来指向子页。也就是说能够分为两级页表,第一级页表项只须要4KB大小的页表项,每一个一级页表项又指向一个4KB大小的二级页表,二级页表项则指向实际的物理页。
页表项加载是按需加载的,没有分配的虚拟页不须要创建页表项, 因此能够一开始只创建一级页表项,而二级页表项按需建立,这样大大压缩了页表的空间。
地址翻译就是把N个元素的虚拟地址空间(VAS)映射到M个元素的物理地址空间(PAS)的过程。
总结一下地址翻译的过程:
CPU拿到一个虚拟地址,分为两步,先经过页表机制肯定该地址所在虚拟页的内容是否从磁盘加载到物理内存页中,而后经过高速缓存机制从该物理地址中取到数据
地址翻译硬件要把这个虚拟地址翻译成一个物理地址,从而能够再根据高速缓存的映射关系,把这个物理地址对应的值找到
地址翻译硬件利用页表数据结构,TLB硬件缓存等技术,目的只是把一个虚拟地址映射到一个物理地址。要记住DRAM缓存是全相联的,因此一个虚拟地址和一个物理地址是动态关联的,不能直接根据虚拟地址推导出物理地址,必须根据DRAM从磁盘把数据缓存到DRAM时存到页表时存的实际物理页才能获得实际的物理地址,用物理页PPN + VPO就能算出实际的物理地址 (VPO = PPO,因此直接用VPO便可)。 PPN的值是存在页表条目PTE中的。地址翻译作了一堆工做,就是为了找到物理页PPN,而后根据VPO页面偏移量,就能定位到实际的物理地址。
获得实际物理地址后,根据高速缓存的原理,把一个物理地址映射到高速缓存具体的组,行,块中,找到实际存储的数据。
Linux把虚拟内存划分红区域area的集合,每一个存在的虚拟页面都属于一个area。一个area包含了连续的多个页。Linux经过area相关的数据结构来灵活地管理虚拟内存。
内核为每一个进程维护了一个单独的任务结构 task_struct
task_struct的mm指针指向了mm_struct,该结构描述了虚拟内存的运行状态
mm_struct的pgd指针指向该进程的一级页表的基地址。mmap指针指向了vm_area_struct链表
vm_area_struct是描述area结构的一个链表,链表节点的几个重要属性以下:vm_start表示area的开始位置,vm_end表示area的结束位置,vm_prot描述了area内的页的读写权限,vm_flags描述该area内的页面是与其余进程共享仍是进程私有, vm_next指向下一个area节点
在Linux系统中,当MMU翻译一个虚拟地址发生缺页异常时,跳转到内核的缺页异常处理程序。
Linux的缺页异常处理程序会先检查一个虚拟地址是哪一个area内的地址。只须要比较全部area结构的vm_start和vm_end就能够知道。area都是一个连续的块。若是这个虚拟地址不属于任何一个area,将发生一个段错误,终止进程
要访问的目标地址是否有相应的读写权限,若是没有,将触发一个保护异常,终止进程
选择一个牺牲页,若是牺牲页被修改过,那么把它交换出去。从磁盘加载虚拟页内容到物理页,更新页表
虚拟内存的目标存储器是磁盘,因此虚拟内存区域是和磁盘中的文件对应的。初始化虚拟内存区域的内容时,会把虚拟内存区域和一个磁盘文件对象对应起来,这个过程叫内存映射(memory mapping)。虚拟内存能够映射的磁盘文件对象包括两种:
一个普通的磁盘文件,文件中的内容被分红页大小的块。由于按需进行页面调度,只有真正须要读取这些虚拟页时,才会交换到主存
一个匿名文件,匿名文件是内核建立的,内容全是二进制0,它至关于一个占位符,不会产生实际的磁盘流量。映射到匿名文件中的页叫作请求二进制零的页(demand zero page)
一旦一个虚拟页面被初始化了,它就在一个由内核维护的专门的交换区(swap area)之间换来换去。
因为内存映射机制,因此一个磁盘文件对象能够被多个进程共享访问,也能够被多个进程对象私有访问。若是是共享访问,那么一个进程对这个对象的修改会显示到其余进程。若是是私有访问,内核会采用写时拷贝copy on write的方式,若是一个进程要修改一个私有的写时拷贝的对象,会产生一个保护故障,内核会拷贝这个私有对象,写进程会在新的私有对象上修改,其余进程仍指向原来的私有对象。
理解了内存映射机制就能够理解几个重要的函数:
fork函数会建立带有独立虚拟地址空间的新进程,内核会为新进程建立各类数据结构,分配一个惟一的PID,把当前进程的mm_struct, area结构和页表都复制给新进程。两个进程的共享一样的区域,这些区域包括共享的内存映射和私有的内存映射。私有的内存映射区域都被标记为私有的写时拷贝。若是新建的进程对这些虚拟页作修改,那么会触发写时拷贝,为新的进程维护私有的虚拟地址空间。
mmap函数能够建立新的虚拟内存area,并把磁盘对象映射到新建的area。
mmap能够用做高效的操做文件的方式,直接把一个文件映射到内存,经过修改内存就至关于修改了磁盘文件,减小了普通文件操做的一次拷贝操做。普通文件操做时会先把文件内容从磁盘复制到内核空间管理的一块虚拟内存区域area,而后内核再把内容复制到用户空间管理的虚拟内存area。 mmap至关于建立了一个内核空间和用户空间共享的area,文件的内容只须要在这个area对应的物理内存和磁盘文件之间交换便可。
mmap也能够经过映射匿名文件的方式来分配内存空间。好比malloc当要求分配的内存大小超过了MMAP_THRESHOLD(默认128kb)时,会使用mmap私有的,匿名文件的方式来分配大块的内存空间。
动态内存分配器维护者一个进程的虚拟内存区域,称为堆(heap)。向上生长(向更高地址),对每一个进程,内核维护着一个变量brk,它指向堆的顶部
程序使用动态内存分配的最重要的缘由是常常直到程序实际运行时才知道某些数据结构的大小
Linux提供了少许的基于Unix I/O模型的系统级函数,它们容许应用程序打开、关闭、读和写文件,提取文件的元数据,以及执行I/O重定向。Linux 的读和写操做会出现不足值,应用程序必须能正确地预计和处理这种状况。应用程序不该直接调用UnixI/O函数,而应该使用RIO包,RIO包经过反复执行读写操做,直到传送完全部的请求数据,自动处理不足值。
Linux内核使用三个相关的数据结构来表示打开的文件。描述符表中的表项指向打开文件表中的表,项,而打开文件表中的表项又指向v-node表中的表项。每一个进程都有它本身单独的描述符表,而全部的进程共享同一个打开文件表和v-node表。理解这些结构的通常组成就能使咱们清楚地理解文件共享和1/O重定向。
标准I/O库是基于Unix I/O实现的,并提供了一组强大的高级I/O例程。对于大多数应用程序而言,标准I/O更简单,是优于Unix I/O的选择。然而,由于对标准I/O和网络文件的一些相互不兼容的限制,UnixI/O比之标准I/O更该适用于网络应用程序。
每一个网络应用都是基于客户端一服务器模型的。根据这个模型,一个应用是由一个服务器和一个或多个客户端组成的。服务器管理资源,以某种方式操做资源,为它的客户端提供服务。客户端-服务器模型中的基本操做是客户端-服务器事务,它是由客户端请求和跟随其后的服务器响应组成的。
客户端和服务器经过因特网这个全球网络来通讯。从程序员的观点来看,咱们能够把因特网当作是一个全球范围的主机集合,具备如下几个属性: 1)每一个因特网主机都有一个惟-一的32位名字,称为它的IP地址。2)IP地址的集合被映射为一个因特网域名的集合。3)不一样因特网主机上的进程可以经过链接互相通讯。
客户端和服务器经过使用套接字接口创建链接。一个套接字是链接的一个端点,链接以文件描述符的形式提供给应用程序。套接字接口提供了打开和关闭套接字描述符的函数。客户端和服务器经过读写这些描述符来实现彼此间的通讯。
Web服务器使用HTTP协议和它们的客户端(例如浏览器)彼此通讯。浏览器向服务器请求静态或者动态的内容。对静态内容的请求是经过从服务器磁盘取得文件并把它返回给客户端来服务的。对动态内容的请求是经过在服务器上一个子进程的上下文中运行一个程序并将它的输出返回给客户端来服务的。CGI标准提供了一.组规则,来管理客户端如何将程序参数传递给服务器,服务器如何将这些参数以及其余信息传递给子进程,以及子进程如何将它的输出发送回客户端。只用几百行C代码就能实现一个简单可是有功效的Web服务器,它既能够提供静态内容,也能够提供动态内容。
一个并发程序是由在时间上重叠的一组逻辑流组成的。三种不一样的构建并发程序的机制:进程、I/O 多路复用和线程。咱们以一个并发网络服务器做为贯穿全章的应用程序。
进程是由内核自动调度的,并且由于它们有各自独立的虚拟地址空间,因此要实现共享数据,必需要有显式的IPC机制。事件驱动程序建立它们本身的并发逻辑流,这些逻辑流被模型化为状态机,用I/O多路复用来显式地调度这些流。由于程序运行在一个单一进程中,因此在流之间共享数据速度很快并且很容易。线程是这些方法的混合。同基于进程的流同样,线程也是由内核自动调度的。同基于I/O 多路复用的流同样,线程是运行在一个单一进程的上下文中的,所以能够快速而方便地共享数据。
不管哪一种并发机制,同步对共享数据的并发访问都是一个困难的问题。提出对信号量的P和V操做就是为了帮助解决这个问题。信号量操做能够用来提供对共享数据的互斥访问,也对诸如生产者-消费者程序中有限缓冲区和读者-写者系统中的共享对象这样的资源访问进行调度。一个并发预线程化的echo服务器提供了信号量使用场景的很好的例子。
并发也引人了其余一些困难的问题。被线程调用的函数必须具备一种称为线程安全的属性。咱们定义了四类线程不安全的函数,以及- -些将它们变为线程安全的建议。可重人函数是线程安全函数的一个真子集,它不访问任何共享数据。可重人函数一般比不可重人函数更为有效,由于它们不须要任何同步原语。竞争和死锁是并发程序中出现的另外一些困难的问题。当程序员错误地假设逻辑流该如何调度时,就会发生竞争。当一个流等待一个永远不会发生的事件时,就会产生死锁。