Intel 32 位处理器以及 IS-32 指令集架构

⚠ 转载请注明出处:作者:ZobinHuang,更新日期:Aug.4 2021


知识共享许可协议

    本作品ZobinHuang 采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可,在进行使用或分享前请查看权限要求。若发现侵权行为,会采取法律手段维护作者正当合法权益,谢谢配合。


目录

有特定需要的内容直接跳转到相关章节查看即可。

    Section 1. 寄存器组:介绍了 Intel 32 位处理器中的寄存器
        1.1 通用寄存器:介绍了 32 位处理器中通用处理器相对于 16 位处理器的扩展;
        1.2 段地址和段内偏移寄存器:介绍了 32 位处理器中段地址和段内偏移寄存器相对于 16 位处理器的扩展;
        1.3 标志位寄存器:介绍了 32 位处理器中标志位寄存器相对于 16 位处理器的扩展;

    Section 2. 访问内存的地址的变化:介绍了 32 位处理器不同于 16 位处理器的内存访问方式;

    Section 3. 现代处理器的结构与特点:介绍了现代处理器为了更高效执行程序,在微架构层面上所做的优化
        3.1 流水线:介绍了流水线机制,并对 指令集架构 和 微架构 这两个概念进行了区分;
        3.2 乱序执行:介绍了基于流水线机制下的微操作乱序执行机制;
        3.3 高速缓存:简单介绍了 CPU 中的高速缓存;
        3.4 临时寄存器和寄存器重命名:介绍了 32 位处理器中的临时寄存器,以及 CPU 在优化过程中是如何使用这些寄存器的;
        3.4 分支预测:简单地介绍了 32 位 CPU 中的分支预测机制;

    Section 4. 32 位处理器的汇编指令:介绍了 Intel 32 位处理器下的 IA-32 指令集架构;
        4.1 机器码:介绍了 IA-32 指令集被编译器编译后形成的机器码格式,以及它们在 16 位模式和 32 位模式下的区别;
        4.2 32 位处理器的指令:介绍了 IA-32 指令集和我们在 8086 实模式中看到的指令集的区别;
            4.2.1 shl 和 shr 逻辑移位指令
            4.2.2 循环指令
            4.2.3 32 位乘法操作
            4.2.4 32 位除法操作
            4.2.5 32 位堆栈立即数操作
            4.2.5 32 位堆栈寄存器操作

1. 寄存器组

    在本文中我们将重点关注 Intel 的 32 位处理器以及 IA-32 指令集架构。这是为了更好地理解后面的文章将要关注的 32 位保护模式,我们必须看看 Intel 的 32 位处理器的内部构成是怎么样的。(实际上 Intel 从 1982 年的 24 位处理器 80286 就开始支持保护模式,但是由于 80286 的通用处理器的通用寄存器仍然是 16 位的,因此限制了段内偏移地址的长度 —— 仍为 64KB,因此没有 16 位保护模式推广开来。)

1.1 通用寄存器

    凡是在 8086 中出现过的 8 个通用寄存器,到了 32 位处理器上都变成了 32 位的:EAX, EBX, ECX, EDX, ESI, EDI, ESP, EBP,其中前缀 E 是 Extended 的含义。如上图所示,我们以 EAX 为例,其低 16 位保持着和 16 位处理器的兼容性,里面还可以细分为 AH 和 AL 两个 8 位寄存器,正如在 16 位处理器中看到的那样;而高 16 位则没有这种用法。

1.2 段地址和段内偏移寄存器

    如上图所示,在 32 位处理器中,IP 寄存器拓展成为 32 位 EIP 寄存器,理论上已经能够实现使用 EIP 遍历完 4GB 的内存空间,即段基地址寄存器为 0x00000000,可以视为不分段,这种用法被称作 平坦模式

    但是,32 位处理器保留了分段访问内存的模型,它仍然提供了如上图所示的 16 位的各个段基址寄存器。但是注意到它们现在存储的不再是 16-bits 段基址,而是 段的选择器 (Segment Selector)。除了各个段选择器外,每个段选择器还提供了一个 64-bits 的不可见部分,称为 描述符高速缓存器,它用于存储:32-bits 的段的基地址以及各种段的访问属性。下面我们描述一下如此设计的动机,我们在更后面将补充更多的设计细节。

    在 16 位 处理器中,一个程序可以自由地访问不属于它的内存位置,这是十分危险的,特别是 32 位处理器是支持多任务的处理器,它可以宏观并行的运行多个任务。如果程序访问内存的方式不慎,很容易导致系统崩溃。因此在 32 位处理器中,引入了所谓的保护模式,也就是说处理器在加载程序的时候,会先将段基地址寄存器的值设置为要访问的段的序号,然后获取该段所对应的描述符,再根据描述符中段界限、特权级别、类型等属性发起对这些段的合法访问。

    细心的读者还会注意到,32 位处理器多了两个段基地址寄存器:GS 和 FS。

1.3 标志位寄存器

    如上图所示,32 位处理器的标志位寄存器也被扩展到了 32 位。

2. 访问内存的地址的变化

    在传统的 16 位汇编中,我们使用段基地址寄存器和段内偏移寄存器所组合出来的地址就是最终送上物理地址线的地址,我们把这种送上物理地址线的地址称为 物理地址 (Physical Address)。同时,我们把段基地址和段内偏移地址所组合出来的地址称作 逻辑地址 (Logic Address)。可见,在 16 位模式下,逻辑地址的值就是物理地址的值。

    【TODO: 32-bits 处理器的分段与分页】

3. 现代处理器的结构与特点

    在这里我们需要简单理解一下一些现代处理器的工作特点,这是我们在讨论 8086 处理器的时候没有讨论过的。

3.1 流水线

    我们在 时序逻辑电路基础 一文中曾经讲述过底层的流水线原理。在 CPU 的架构方面,对指令的处理同样采用了流水线的思想,将 CPU 指令的执行过程拆分成为若干部分 (e.g. 取指令、译码和执行),然后将各个指令的执行过程在时间上叠起来,从而加速指令的执行过程。

题外话

    这里我们可以理解一下两个不同的概念: 指令集架构 (ISA)微架构 (Microarchitecture)

    ISA 的全称是 Instruction Set Architecture,中文就是指令集架构,是指对程序员实际 "可见" 的指令集,包含了程序员编写一个能正确运行的二进制机器语言程序的所有信息,涉及到指令、 I/O 设备等。例如 Intel 的 IA-32、Intel 64、ARM 的 ARMv7、ARMv8 等等。

    微架构 (Microarchitecture),又称为微体系结构/微处理器体系结构。是将一种给定的指令集架构在处理器中执行的方法。一种给定的指令集可以在不同的微架构中执行。

    微架构与指令集是两个概念:指令集是 CPU 选择的语言,而微架构是具体的实现。

    ARM 公司将自己研发的指令集叫做 ARM 指令集,同时它还研发具体的微架构(如Cortex系列)并对外授权。但是,一款 CPU 使用了 ARM 指令集不等于它就使用了 ARM 研发的微架构。Intel、高通、苹果、Nvidia 等厂商都自行开发了兼容ARM指令集的微架构,同时还有许多厂商使用 ARM 开发的微架构来制造 CPU。通常,业界认为只有具备独立的微架构研发能力的企业才算具备了 CPU 研发能力,而是否使用自行研发的指令集则无关紧要。厂商研发 CPU 时并不需要获得指令集授权就可以获得指令集的相关资料与规范,指令集本身的技术含量并不是很高。获得授权主要是为了避免法律问题。然而微架构的设计细节是各家厂商绝对保密的,而且由于其技术复杂,即便获得相应文档也难以山寨。

    如前所述,仅仅从 ARM 购买微架构来组装芯片的厂商是不能被称作 CPU 研发企业的,这些芯片也不能被称为 "xx 厂商研发的 CPU"。典型如华为的海思 920、三星 Exynos 5430,只能说是 "使用ARM Cortex-A15 核心的芯片"。但是如果一款兼容 ARM 指令集的芯片使用了厂商自主研发的微架构,情况就不同了。高通骁龙800、苹果A7就是这样的例子——它们分别使用了高通、苹果自主研发的CPU。

    我们这里讨论的流水线,其具体实现方式是和具体的微架构相关的。即使同样使用了 IA-32 的指令集,如果底层微架构不同,最终指令在 CPU 的流水线运行方式也有不同。

3.2 乱序执行

    由于 CPU 使用了流水线来执行指令,因此需要把一条指令拆分成为更小的部分,即 微操作 (micro-operation, 也写作 `\mu` ops)

    思考下面的例子:

1
add [mem],eax

    在上面的这条指令中,我们可以把这条指令的运行拆为:

  • 从内存中读取数据
  • 执行相加操作
  • 将结果写回内存中

    当我们完成拆分之后,我们就可以将不互相冲突且不相互依赖的微操作 乱序执行 (Out-Of-Order Execution)。思考下面的例子:

1
2
3
4
mov eax, [mem1]
shl eax, 5
add eax, [mem2]
mov [mem3], eax

    在上面代码的 Line 1 中,当 CPU 从内存中获取到 mem1 的内容后,如果发现 Line3 需要的 mem2 数据不在 高速缓存(下面会进行介绍) 中,则会立刻发起从内存中读取 mem2 的操作,并且 Line1 和 Line2 的代码会同时进行。

    思考一下在堆栈操作下乱序操作带来的好处:

1
2
push eax
call func

    思考一下没有流水线和乱序执行的场景:首先执行 push 操作,修改 esp 的值并且把 eax 的值搬迁到响应的内存堆栈位置,然后再执行 call 指令,call 指令需要在堆栈中保存程序返回的地址。

    引入乱序执行后,我们可以把 Line 1 的 push 代码拆分为下面的微操作:

1
2
sub esp, 4
mov [esp], eax

    这样一来,当 push 操作在执行完 Line 1 的对 esp 的减操作之后,即使 Line 2 中 eax 的值没有准备好,call 指令也可以无缝地继续执行,因为它需要 esp 寄存器被更新后的值,而无需阻塞到 eax 中的值被移动到内存中之后再执行。

3.3 高速缓存

    为了适配内存 (DRAM) 和 CPU 之间带宽的 gap,CPU 中放置了存取速度更快的 高速缓存 (Cache) 来承接两者的数据交互。高速缓存使用的是 SRAM 存储器。

    高速缓存利用的是程序在运行时所具有的局部性规律,也即程序大概率地会访问刚刚访问过的指令和数据。因此当 CPU 要访问内存时,会首先检查高速缓存,命中 (Hit) 则直接读取,不中 (Miss) 则从内存中读取且重新装载高速缓存。高速缓存的存取是以块为单位的。

3.4 临时寄存器和寄存器重命名

    思考下面的这段代码

1
2
3
4
5
6
7
8
9
; 左移 [mem1] 的值
mov eax,[mem1]
shl eax,3
mov [mem2],eax

; 增加 [mem3] 的值
mov eax,[mem3]
add eax,2
mov [mem4],eax

    它其实干了两件不相干的事情,但是却占用了同一个寄存器 eax,这似乎使得 CPU 没法为这两件独立的工作进行优化使得它们能够并行地执行。

    实际上 CPU 可以。在 Intel 32 位 CPU 中有大量的临时寄存器,这些寄存器是不会暴露给汇编程序员的,程序员看到的仍然只有上面介绍过的寄存器。这些临时寄存器会在处理器需要优化像上面这种程序的时候,被重命名为一个逻辑寄存器 (e.g. eax) 使用。

    思考下面的例子:

1
2
3
4
5
6
mov eax,[mem1]
mov ebx,[mem2]
add ebx,eax
shl eax,3
mov [mem3],eax
mov [mem4],ebx

    假设 [mem1] 的数据已经在高速缓存中了,而 [mem2] 中的数据仍在躺在内存中。那么当我们在运行 Line 2 的代码的时候,实际上我们已经可以开始 Line 4 的对 eax 的左移操作了。处理器也是这么做的,它会使用一个临时寄存器来保存左移的结果。并且最后当所有的操作完成之后,临时寄存器中的数据将会被 引退 (Retirement) 回 eax 中。

3.5 分支预测

    从上文中,我们知道,处理器在应对程序的时候,会使用流水线,将指令拆分为:取指、译码、寄存器分配和重命名、微操作排序、执行和引退等步骤,来进行优化。

    流水线机制的一个最大的问题就是,当遇到程序有分支跳转的情况时,流水线必须被清空 (Flush),跳转到目的程序位置之后,重新建立流水线。这样一来,由于大型程序中存在的大量分支,加之复杂架构下流水线深度的增大,使得 CPU 束手无策。

    1996 年的 Pentium Pro 处理器引入了 分支预测 (Branch Prediction) 技术。

    简单来说就是,当 CPU 第一次遇到一条跳转指令的时候,CPU 会记住它的跳转方向 (i.e. 是否跳转),即将结果记录在 分支目标缓存器 (Branch Target Buffer, BTB) 中:[当前指令的地址, 分支目标的地址, 本次分支预测的结果]。下一次 CPU 遇到这个跳转指令的时候,就会到 BTB 查询上一次跳转的结果,并且按照上一次的方向将指令送入流水线。如果本次分支的执行方向与上一次不一致,则会清空流水线并且更新 BTB。

    实践证明,这个预测通常是比较准。虽然清空流水线和更新 BTB 的代价比较大,但是它们有效地遏止了清空更多流水线导致的更高代价。

4. 32 位处理器的汇编指令

    相比于单纯编写 16 位的汇编代码,由于在 32 位处理器中可以处理长达 32-bits 的数据,并且保持了对 16 位处理器的兼容,因此 32 位处理器的指令系统有很多可以介绍的东西。首先我们先来理解一下由汇编指令编译形成的机器码的本质,然后再来看 32 位处理器的汇编指令变化。

4.1 机器码

    实际上,我们的汇编指令在被编译之后形成的机器码,都是如下格式:

前缀 操作码 寻址方式和操作数类型 立即数 偏移量
  • 前缀:用于对指令作修饰,比如重复前缀(e.g. REP/REPE/REPNE),段超越前缀(e.g. ES:),总线封锁前缀(LOCK),和 16/32-bits 反转前缀(下文将会介绍)。一条指令可以有 0~4 个前缀;
  • 操作码:用于描述指令进行的操作:加法/减法/...
  • 寻址方式:给出了指令的寻址方式:基地址变址/...
  • 操作数类型:给出了指令使用的寄存器的类型(使用的是哪个寄存器)
  • 立即数:指令中使用的立即数在这部分给出
  • 偏移量:指令中寻址使用的偏移量在这部分给出,比如 mov ecx, [eax+0x02]

    CPU 运行上面的机器码的时候,如何解释机器码中各个部分的内容,视具体指令而定。

    通常来说,我们说 16 位模式指的就是 16 位实模式,我们说 32 位模式指的就是 32 位保护模式,但是注意!请不要把 32 位处理器的 "16 位模式和 32 位模式" 与 "实模式和保护模式" 等同起来。在实模式的情况下,CPU 认识的是以 16 位方式编码的机器码;在保护模式的大部分情况下,CPU 认识的是以 32 位方式编码的机器码。这里说的 16 位机器码和 32 位机器码,它们的不同主要在于内存寻址方式以及寄存器编码格式的不同。因此,我们对于我们编写的指令应该有一个清楚的意识:当我们在实模式下的时候,我们写的是最后会被翻译为 16 位机器码的指令;在保护模式的大部分情况下,我们写的是最终会被翻译为 32 位机器码的指令。为了让编译器能够将指令正确地翻译为预料中的 16 位模式编码或者 32 位编码,我们必须在代码中掺入伪指令来进行说明。对于 NASM 编译器来说,编译的默认编码格式是 16 位的,也就是说,我们在编写实模式下的代码的时候,并不需要显式地向编译器说明我们工作在 16 位模式下。而对于保护模式来说,由于我们大部分情况下写的是 32 位保护模式下的指令,因此我们在进入保护模式后必须显式地向编译器说明我们工作的模式,相关的 NASM 编译器伪指令如下所示:

1
2
3
4
;NASM Assembly
[bits 32]
mov cx,dx
mov eax,ebx

    另外,值得注意的是,32 位处理器是有 16 位模式的,这与它的兼容性有很大的关系。但是 32 位处理器的 16 位模式不能完全等价于我们以前在讲 8086 实模式的那种 16 位模式,因为 32 位处理器的 16 位模式是可以使用 32 位操作数、操作 32-bits 寄存器和使用 32 位模式的寻址方式的 (i.e. eax~edx, etc)。有读者会好奇:我们在 16 位模式下使用的是 16 位编码的机器码,里面并没有对 32 位操作数和寄存器作相关的编码,如何实现 16 模式下的 32 位操作呢?这就要归功于上图编码中的 "前缀" 字段 —— 操作数反转前缀 0x66寻址方式反转前缀 0x67。我们下面分别解释。

    对于操作数反转前缀:当我们在 16 位模式中的指令使用 32 位操作数的时候,编译器将会在编译后的机器码前面加上 0x66 的前缀,处理器在处理这条指令的时候,发现了这个 0x66 前缀,自然就知道指令后面的操作数和相关的寄存器编码使用的是 32 位模式下的编码格式,例子如下所示:

1
2
3
4
;NASM Assembly
; 指令 机器码
mov ax, 0x1234 B8 34 12
mov eax, 0x1234 66 B8 34 12 00 00

    同样地,当 CPU 处于 32 位模式下的时候,如果它发现了一条待执行机器码带 0x66 的前缀,它就会以 16 位编码来解析这条指令 —— 16-bits 寄存器编码格式、16-bits 操作数大小,比如:

1
2
3
4
5
;NASM Assembly
[bits 32]
; 指令 机器码
mov ax, 0x1234 66 B8 34 12
mov eax, 0x1234 B8 34 12 00 00

    对于寻址方式反转前缀:当我们在 16 位模式下使用 32 位模式下的内存寻址方式时,编译器将会在编译后的机器码前面加上 0x67 的前缀,处理器在处理这条指令的时候,发现了 0x67 前缀,就知道指令使用的寻址方式使用的是 32 位模式下的寻址方法,例子如下所示:

1
2
3
4
5
;NASM Assembly
; 指令 机器码
mov word [bx], 0x1234 C7 07 34 12
mov word [eax], 0x1234 67 C7 00 34 12
mov dword [eax], 0x1234 66 67 C7 00 34 12 00 00

    上面例子的第三条中,我们发现 0x66 和 0x67 前缀是可以一起使用的。

    同样地,对于 CPU 在 32 位模式下使用了 16 位寻址方式的指令,编译器也会打上 0x67 的前缀,例子如下所示:

1
2
3
4
5
6
;NASM Assembly
[bits 32]
; 指令 机器码
mov dword [eax], 0x1234 C7 00 34 12 00 00
mov word [eax], 0x1234 66 C7 00 34 12
mov dword [bx], 0x1234 67 C7 07 34 12 00 00

    理解了上面所描述的 16 位模式和 32 位模式之后,现在我们来看一个特例:从通用寄存器到段寄存器的传送,来看下面的例子:

1
2
3
4
5
6
;NASM Assembly
bits 16
mov ds, ax

bits 32
mov ds, ax

    在 16 位模式下,Line 3 的代码会被编译为 8E D8,没有加 0x66 前缀是因为它操作的本身就是 16 位的操作数。而在 32 位模式下,按我们上面的说法来看,Line 6 的代码会被编译为 0x66 0x8E 0xD8,因为它是在 32 位模式下使用了 16 位的操作数。但是,由于加了 0x66 的前缀使得 CPU 在运行这条指令的时候会额外的多出一个时钟周期,并且这种 从通用寄存器到段寄存器的传送指令 还特别常见,因此处理器特别为这种指令做出了优化设计,我们之后在运行 Line 6 这种指令的时候,将不会添加 0x66 前缀,因为不论是在 16 位模式还是 32 位模式下,它们的操作码都是一样的,所以我们会看到:

1
2
3
4
5
6
7
8
;NASM Assembly
[bits 16]
mov ds,ax ;8E D8
mov ds,eax ;8E D8

[bits 32]
mov ds,ax ;8E D8
mov ds,eax ;8E D8

    它们之所以能被设置称为一样的操作码,原因之一本人认为是因为不论是在实模式下还是保护模式下,段寄存器永远都是 16-bits 的,所以不论我们是在 16-bits 模式下还是 32-bits 模式下,上面代码中 ax 和 eax 有效的部分永远只会是低 16 位,因此在机器码上可以实现统一。

4.2 32 位处理器的指令

    除去那些在本小节中特别提及的指令,32 位处理器的指令集除了把处理宽度拓展到了 32 位之外,和 16 位处理器的指令集并无过多区别。比如,指令 add 现在支持 8 位、16 位和 32 位的操作:

1
2
3
4
add al, bl
add ax, bx
add eax, ebx
add dword [ecx], 0x0000005f
(1) shl 和 shr 逻辑移位指令

    我们在 实例:使用 8086 CPU 读取 CMOS RAM 曾经介绍过 16 位实模式下 shl 和 shr 逻辑移位指令的用法。在 32 位处理器中,控制移动次数的寄存器依旧是 cl,并且 32 位处理器实际执行是回将 cl 中的值和 0x1F 相与,也即实际上移动的次数做多为 31 次。

(2) 循环指令

    32 位处理器用于控制循环次数的寄存器是 ecx 寄存器。

(3) 32 位乘法操作

    我们在 如何在汇编中完成乘法操作? 中曾经介绍过 16 位实模式下实现乘法的方法。在 32 位处理器上,除了 8 位乘法和 16 位乘法,又增加了对 32 位乘法的支持:

1
2
3
mov eax,0x10000
mov ebx,0x20000
mul ebx

    两个 32 位乘数分别放在 eax 和另一个 32 位寄存器中,64 位结果被放在 edx(高 32 位) 和 eax(低 32 位)中。

(4) 32 位除法操作

    除法也是类似的道理,我们在 如何在汇编中完成除法操作? 中曾经介绍过 16 位实模式下实现除法的方法。在 32 位处理器上对除法也做了 32 位的支持:被除数是 64 位的,高 32 位在 EDX 寄存器;低 32 位在 EAX 寄存器。除数是 32 位的,位于 32 位的寄存器,或者存放有 32 位实际操作数的内存地址。指令执行后,32 位的商在 EAX 寄存器,32 位的余数在 EDX 寄存器。

(5) 32 位堆栈立即数操作

    对于栈操作 push 和 pop,在立即数操作上,在原有基础上提供了对双字 (32-bits) 立即数的支持。现在它们支持的形态如下所示:

1
2
3
push imm8   ;操作码: 6A
push imm16 ;操作码: 68
push imm32 ;操作码: 68

    对于 8 位的操作,如下面 NASM 编译环境下的指令:

1
push byte 0x55

    我们在指令中加入的伪指令 byte,是写给编译器看的,告诉编译器压入的是字节,而不会对最终编译生成的机器码有任何的影响:在 16 位模式和 32 位模式下,它编译出来的结果都是 0x6A 0x55。

    值得注意的是,处理器在运行这条指令的时候并不是真的就往栈是中写了一个字节。因为在 16 位的模式下,默认的操作数字长是 16,因此处理器会先 sp = sp - 2,然后将操作数填充到 16 位,即 0x0055,并写入相应位置;在 32 位模式下,默认的操作数字长是 32,因此处理器会先 esp = esp - 4,然后将操作数填充到 32 位,即 0x000055,并写入相应位置。

    对 16-bits, 32-bits 立即数的栈操作的方法和原理和上面所述 8-bits 立即数操作相似,不再赘述。有一个值得注意的点,16 位模式下也是可以对 32-bits 的立即数作操作的,即下面的操作在 16 位模式下是被允许的:

1
push dword 0xfb
(6) 32 位堆栈寄存器操作

    与立即数的堆栈操作不同的是,如果 push/pop 的对象是寄存器的话,则只支持 16/32 位的寄存器,即只支持 字/双字 操作:

1
2
push ax
push edx

    无论被压入的数位于寄存器,还是位于内存单元,在 16 位模式下,如果压入的是字操作数,那么先将 sp 的内容减去 2;如果压入的是双字,应当先将 sp 的内容减去 4。在 32 位模式下,如果压入的是字操作数,那么先将 esp 的内容减去 2;如果压入的是双字,应当先将 esp 的内容减去 4。

    对段寄存器的操作比较特殊:

1
2
3
4
5
6
push cs   ;机器指令为 0E
push ds ;机器指令为 1E
push es ;机器指令为 06
push fs ;机器指令为 0F A0
push gs ;机器指令为 0F A8
push ss ;机器指令为 16

    在 16 位模式下,先将 SP 的内容减去 2,然后直接压入段寄存器的内容;在 32 位模式下,要先将段寄存器的内容用零扩展到 32 位,即高 16 位为全零。然后,将 ESP 的内容减去 4,再压入扩展后的 32 位值。

附录:参考源

  1. 漂泊的指针, 32位x86处理器编程导入——《x86汇编语言:从实模式到保护模式》读书笔记08