跳转到内容

x86 汇编/X86 架构

来自维基教科书,开放书籍,开放世界

X86 架构

[编辑 | 编辑源代码]

X86 架构有 8 个通用寄存器 (GPR)、6 个段寄存器、1 个标志寄存器和一个指令指针。64 位 X86 有额外的寄存器。

通用寄存器 (GPR) - 16 位命名约定

[编辑 | 编辑源代码]

8 个 GPR 如下[1]

  1. 累加器寄存器 (AX)。用于算术运算。将常量合并到累加器的操作码为 1 字节。
  2. 基址寄存器 (BX)。用作指向数据的指针(位于段寄存器 DS 中,当处于分段模式时)。
  3. 计数器寄存器 (CX)。用于移位/旋转指令和循环。
  4. 堆栈指针寄存器 (SP)。指向堆栈顶部的指针。
  5. 堆栈基址指针寄存器 (BP)。用于指向堆栈的底部。
  6. 目标索引寄存器 (DI)。用作指向流操作中目标的指针。
  7. 源索引寄存器 (SI)。用作指向流操作中源的指针。
  8. 数据寄存器 (DX)。用于算术运算和 I/O 操作。

它们在这里列出的顺序是有原因的:它与在推入堆栈操作中使用的顺序相同,这将在后面讨论。

所有寄存器都可以在 16 位和 32 位模式下访问。在 16 位模式下,寄存器由上面列表中的两位字母缩写标识。在 32 位模式下,这个两位字母缩写以 'E'(扩展)为前缀。例如,'EAX' 是作为 32 位值的累加器寄存器。

类似地,在 64 位版本中,'E' 被替换为 'R'(寄存器),因此 'EAX' 的 64 位版本称为 'RAX'。

还可以以 16 位的大小访问前四个寄存器 (AX、CX、DX 和 BX) 的两个 8 位部分。最低有效字节 (LSB) 或低位部分,通过将 'X' 替换为 'L' 来标识。最高有效字节 (MSB) 或高位部分,使用 'H' 代替。例如,CL 是计数器寄存器的 LSB,而 CH 是它的 MSB。

总的来说,这给了我们五种方法来访问累加器、计数器、数据和基址寄存器:64 位、32 位、16 位、8 位 LSB 和 8 位 MSB。另外四个只可以用四种方法访问:64 位、32 位、16 位和 8 位。下表总结了这一点

寄存器 累加器 基址 计数器 堆栈指针 堆栈基址指针 目标 数据
64 位 RAX RBX RCX RSP RBP RDI RSI RDX
32 位 EAX EBX ECX ESP EBP EDI ESI EDX
16 位 AX BX CX SP BP DI SI DX
8 位 AH AL BH BL CH CL SPL BPL DIL SIL DH DL
访问寄存器及其部分的标识符

段寄存器

[编辑 | 编辑源代码]

6 个段寄存器是

  • 堆栈段 (SS)。指向堆栈('S' 代表 '堆栈')。
  • 代码段 (CS)。指向代码('C' 代表 '代码')。
  • 数据段 (DS)。指向数据('D' 代表 '数据')。
  • 附加段 (ES)。指向额外数据('E' 代表 '额外';'E' 在 'D' 之后)。
  • F 段 (FS)。指向更多额外数据('F' 在 'E' 之后)。
  • G 段 (GS)。指向更多更多额外数据('G' 在 'F' 之后)。

大多数现代操作系统(如 FreeBSD、Linux 或 Microsoft Windows)上的大多数应用程序使用一种内存模型,该模型将几乎所有段寄存器都指向同一个位置(并使用分页来代替),从而有效地禁用了它们的使用。通常 FS 或 GS 的使用是对此规则的例外,而是用于指向线程特定的数据。

EFLAGS 寄存器

[编辑 | 编辑源代码]

EFLAGS 是一个 32 位寄存器,用作表示布尔值的位的集合,以存储操作的结果和处理器的状态。

这些位的名称是

31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16
0 0 0 0 0 0 0 0 0 0 ID VIP VIF AC VM RF
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
0 NT IOPL OF DF IF TF SF ZF 0 AF 0 PF 1 CF

名为 0 和 1 的位是保留位,不应该修改。

这些标志的不同用途是
0. CF : 进位标志。如果上次算术运算在寄存器大小之外进位(加法)或借位(减法),则设置。然后在操作后紧随带有进位的加法或带有借位的减法进行检查,以处理超过单个寄存器可以容纳的值。
2. PF : 奇偶校验标志。如果最低有效字节中设置位的数量是 2 的倍数,则设置。
4. AF : 调整标志。二进制编码十进制 (BCD) 数算术运算的进位。
6. ZF : 零标志。如果操作的结果为零 (0),则设置。
7. SF : 符号标志。如果操作的结果为负,则设置。
8. TF : 陷阱标志。如果设置则一步一步调试。
9. IF : 中断标志。如果中断已启用,则设置。
10. DF : 方向标志。流方向。如果设置,字符串操作将递减其指针而不是递增其指针,反向读取内存。
11. OF : 溢出标志。如果带符号算术运算导致的值太大,以至于寄存器无法容纳,则设置。
12-13. IOPL : I/O 权限级别字段 (2 位)。当前进程的 I/O 权限级别。
14. NT : 嵌套任务标志。控制中断的链接。如果当前进程链接到下一个进程,则设置。
16. RF : 恢复标志。对调试异常的响应。
17. VM : 虚拟-8086 模式。如果处于 8086 兼容模式,则设置。
18. AC : 对齐检查。如果对内存引用的对齐检查已完成,则设置。
19. VIF : 虚拟中断标志。IF 的虚拟镜像。
20. VIP : 虚拟中断挂起标志。如果中断挂起,则设置。
21. ID : 标识标志。支持 CPUID 指令(如果可以设置)。

指令指针

[编辑 | 编辑源代码]

EIP 寄存器包含如果未执行分支则要执行的下一个指令的地址。

EIP 只能通过堆栈在 call 指令后读取。

X86 架构是小端,这意味着多字节值以最低有效字节优先写入。(这仅指字节的顺序,而不是位的顺序。)

因此,X86 上的 32 位值 B3B2B1B016 将在内存中表示为

小端表示
B0 B1 B2 B3

例如,32 位双字 0x1BA583D4(0x 表示十六进制)将写入内存中为

小端示例
D4 83 A5 1B

这在进行内存转储时将被视为 0xD4 0x83 0xA5 0x1B

二进制补码表示

[编辑 | 编辑源代码]

二进制补码是表示负整数的标准方法。符号通过反转所有位并加一来改变。

二进制补码示例
开始: 0001
反转: 1110
加一: 1111

0001 代表十进制 1

1111 代表十进制 -1

寻址模式

[编辑 | 编辑源代码]

在 x86 汇编语言中,寻址模式决定了指令中如何指定内存操作数。寻址模式允许程序员从内存中访问数据或有效地对操作数执行操作。x86 架构支持各种寻址模式,每种模式都提供了不同的方式来引用内存或寄存器。以下是一些 x86 中常见的寻址模式

寄存器寻址
(操作数地址 R 在地址字段中)
mov ax, bx  ; moves contents of register bx into ax
立即数
(实际值在字段中)
mov ax, 1   ; moves value of 1 into register ax

mov ax, 010Ch ; moves value of 0x010C into register ax
直接内存寻址
(操作数地址在地址字段中)
.data
my_var dw 0abcdh ; my_var = 0xabcd
.code
mov ax, [my_var] ; copy my_var content into ax (ax=0xabcd)
直接偏移寻址
(使用算术运算来修改地址)
byte_table db 12, 15, 16, 22 ; table of bytes
mov al, [byte_table + 2]
mov al, byte_table[2] ; same as previous instruction
寄存器间接寻址
(字段指向包含操作数地址的寄存器)
mov ax, [di]
用于间接寻址的寄存器是 BX、BP、SI、DI

通用寄存器 (64 位命名约定)

[编辑 | 编辑源代码]

64 位 x86 添加了 8 个通用寄存器,命名为 R8、R9、R10 等等,直到 R15。

  • R8–R15 是新的 64 位寄存器。
  • R8D–R15D 是每个寄存器的最低 32 位。
  • R8W–R15W 是每个寄存器的最低 16 位。
  • R8B–R15B 是每个寄存器的最低 8 位。

此外,64 位 x86 包括 SSE2,因此每个 64 位 x86 CPU 至少有 8 个寄存器(命名为 XMM0–XMM7),它们是 128 位宽,但只能通过 SSE 指令 访问。它们不能用于四精度 (128 位) 浮点运算,但它们可以分别容纳 2 个双精度或 4 个单精度浮点值,用于 SIMD 并行指令。它们也可以作为 128 位整数或更短整数的向量进行操作。如果处理器支持 AVX,如较新的 Intel 和 AMD 台式机 CPU,则这些寄存器实际上是 256 位寄存器(命名为 YMM0–YMM7)的下半部分,整个寄存器可以通过 AVX 指令访问,以实现进一步的并行化。

堆栈是一种后进先出 (LIFO) 数据结构;数据被压入堆栈并以相反的顺序弹出。

mov ax, 006Ah
mov bx, F79Ah
mov cx, 1124h

push ax ; push the value in AX onto the top of the stack, which now holds the value 0x006A.
push bx ; do the same thing to the value in BX; the stack now has 0x006A and 0xF79A.
push cx ; now the stack has 0x006A, 0xF79A, and 0x1124.

call do_stuff ; do some stuff. The function is not forced to save the registers it uses, hence us saving them.

pop cx ; pop the element on top of the stack, 0x1124, into CX; the stack now has 0x006A and 0xF79A.
pop bx ; pop the element on top of the stack, 0xF79A, into BX; the stack now has just 0x006A.
pop ax ; pop the element on top of the stack, 0x006A, into AX; the stack is now empty.

堆栈通常用于向函数或过程传递参数,以及在使用 call 指令时跟踪控制流。堆栈的另一个常见用途是临时保存寄存器。

CPU 操作模式

[编辑 | 编辑源代码]

实模式

[编辑 | 编辑源代码]

实模式是最初的 Intel 8086 的遗留产物。通常你不需要了解它(除非你正在为基于 DOS 的系统或更可能是编写由 BIOS 直接调用的引导加载程序进行编程)。

Intel 8086 使用 20 位地址访问内存。但由于处理器本身是 16 位的,英特尔发明了一种寻址方案,它提供了一种将 20 位地址空间映射到 16 位字的方法。如今的 x86 处理器从所谓的实模式开始,这是一种操作模式,它模拟 8086 的行为,并有一些非常小的差异,以便向后兼容。

在实模式下,段寄存器和偏移寄存器一起使用以产生最终的内存地址。段寄存器中的值乘以 16(向左移 4 位),偏移值加到结果中。这提供了一个可用的 1 MB 地址空间。然而,寻址方案中的一个怪癖允许在使用 0xFFFF(最高可能的)段地址时访问超出 1 MB 限制的区域;在 8086 和 8088 上,对该区域的所有访问都会绕回到内存的低端,但在 80286 及更高版本上,如果 A20 地址线启用,则可以访问超过 1 MB 标记的 65520 字节。参见:A20 门事件

实模式分段和 保护模式多段内存模型 共享的一个好处是,所有地址都必须相对于另一个地址给出(即,段基地址)。程序可以拥有自己的地址空间并完全忽略段寄存器,因此不需要重新定位指针来运行程序。程序可以在同一个段内执行调用和跳转,并且数据始终相对于段基地址(在实模式寻址方案中,段基地址是从段寄存器中加载的值计算出来的)。

这就是 DOS *.COM 格式的工作原理;文件的内容被加载到内存中并盲目运行。但是,由于实模式段始终是 64 KB 长的,因此 COM 文件不能大于此(事实上,它们必须适合 65280 字节,因为 DOS 使用段的前 256 字节用于内部数据);多年来,这并不是问题。

保护模式

[编辑 | 编辑源代码]

扁平内存模型

[编辑 | 编辑源代码]

如果在现代 32 位操作系统(如 Linux、Windows)中进行编程,那么你基本上是在扁平的 32 位模式下进行编程。任何寄存器都可以用于寻址,并且通常使用完整的 32 位寄存器而不是 16 位寄存器部分更有效。此外,段寄存器在扁平模式中通常未使用,并且在扁平模式中使用它们不被认为是最佳实践。

多段内存模型

[编辑 | 编辑源代码]

使用 32 位寄存器来寻址内存,程序可以访问现代计算机中的(几乎)所有内存。对于早期处理器(只有 16 位寄存器),使用分段内存模型。'CS'、'DS' 和 'ES' 寄存器用于指向不同的内存。对于小型程序(小型模型),CS=DS=ES。对于更大的内存模型,这些'段'可以指向不同的位置。

长模式

[编辑 | 编辑源代码]

术语“长模式”指的是 64 位模式。

  1. https://www.swansontec.com/sregisters.html
华夏公益教科书