跳至内容

x86 汇编/16、32 和 64 位

来自 Wikibooks,开放的书籍,面向开放的世界

使用 x86 汇编时,必须考虑 16 位、32 位和 64 位体系结构之间的差异。此页面将介绍不同位宽的体系结构之间的一些基本差异。

寄存器

[编辑 | 编辑源代码]

8086 及所有后续 x86 处理器上的寄存器如下:AX、BX、CX、DX、SP、BP、SI、DI、CS、DS、SS、ES、IP 和 FLAGS。这些都是 16 位宽。

在 DOS 和 32 位 Windows 中,您可以从 DOS shell 运行一个非常实用的程序,名为“debug.exe”,它非常适合学习 8086。如果您使用的是 DOSBox 或 FreeDOS,可以使用“debug.exe”FreeDOS 提供的

AX、BX、CX、DX
这些通用寄存器也可以被视为 8 位寄存器。因此 AX = AH(高 8 位)和 AL(低 8 位)。
SI、DI
这些寄存器通常用作数据空间中的偏移量。默认情况下,SI 相对于 DS 数据段偏移,DI 相对于 ES 扩展段偏移,但这两个偏移都可以被覆盖。
SP
这是堆栈指针,通常相对于堆栈段 SS 偏移。数据被推入堆栈以进行临时存储,并在需要时从堆栈弹出。
BP
堆栈帧,通常被视为堆栈段 SS 的偏移量。子例程的参数通常在调用子例程时被推入堆栈,并在子例程开始时将 BP 设置为 SP 的值。然后可以使用 BP 在堆栈上查找参数,无论在此期间堆栈使用了多少。
CS、DS、SS、ES
段指针。这些分别是当前代码段、数据段、堆栈段和扩展段在内存中的偏移量。
IP
指令指针。从代码段 CS 偏移,指向当前正在执行的指令。
FLAGS (F)
许多单比特标志,指示(或有时设置)处理器的当前状态。

随着芯片开始支持 32 位数据总线,寄存器也扩展到 32 位。32 位寄存器的名称就是在 16 位名称前加一个“E”。

EAX、EBX、ECX、EDX、ESP、EBP、ESI、EDI
这些是上面显示的寄存器的 32 位版本。
EIP
IP 的 32 位版本。在 32 位系统上始终使用它而不是 IP。
EFLAGS
16 位 FLAGS 寄存器的扩展版本。

64 位寄存器的名称与 32 位寄存器相同,只是以“R”开头。

RAX、RBX、RCX、RDX、RSP、RBP、RSI、RDI
这些是上面显示的寄存器的 64 位版本。
RIP
这是完整的 64 位指令指针,应该使用它而不是 EIP(如果地址空间大于 4 GiB,则 EIP 将不准确,即使只有 4 GiB 或更少的 RAM 也是如此)。
R8–15
这些是 64 位的新增寄存器。它们被计数为上面的寄存器是 0 到 7 号寄存器(包含),而不是 1 到 8 号。

R8–R15 可以被访问为 8 位、16 位或 32 位寄存器。以 R8 为例,对应于这些宽度的名称分别是 R8B、R8W 和 R8D。64 位版本的 x86 还允许直接访问 RSP、RBP、RSI、RDI 的低字节。例如,可以使用 SPL 访问 RSP 的低字节。无法直接访问这些寄存器的第 8–15 位,就像 AH 允许访问 AX 一样。

128 位、256 位和 512 位 (SSE/AVX)

[编辑 | 编辑源代码]

64 位 x86 包括 SSE2(32 位 x86 的扩展),它为特定指令提供 128 位寄存器。自 2011 年以来制造的大多数 CPU 也具有 AVX,这是一个进一步的扩展,它将这些寄存器的长度扩展到 256 位。一些 CPU 还具有 AVX-512,它将它们的长度扩展到 512 位并添加了 16 个额外的寄存器。

XMM0~7
SSE2 及更新版本。
XMM8~15
SSE3 及更新版本以及 AMD(而非 Intel)SSE2。
YMM0~15
AVX。每个 YMM 寄存器都包含其下半部分的相应 XMM 寄存器。
ZMM0~15
AVX-512F。每个 ZMM 寄存器都包含其下半部分的相应 YMM 寄存器。
ZMM16~31
AVX-512F。512 位寄存器,除非实现了 AVX-512VL,否则在较窄的模式下无法寻址。
XMM16~31
AVX-512VL。每个都是相应 ZMM 寄存器的下四分之一。
YMM16~31
AVX-512VL。每个都是相应 ZMM 寄存器的下半部分。

寻址内存

[编辑 | 编辑源代码]

8086 和 80186

[编辑 | 编辑源代码]

最初的 8086 只有 16 位大小的寄存器,实际上可以存储一个范围在 [0 - (216 - 1)] 之内的值(或更简单地说:它最多可以寻址 65536 个不同的字节,或 64 kibibytes) - 但地址总线(连接到内存控制器,它接收地址,然后从给定地址加载内容,并将数据通过数据总线返回到 CPU)是 20 位大小,实际上可以寻址高达 1 mebibyte 的内存。这意味着所有寄存器本身都不足以利用地址总线的整个宽度,留下了 4 位未用,将可用地址的数量缩小了 16 倍(1024 KiB / 64 KiB = 16)。

问题是:如何通过 16 位寄存器引用 20 位地址空间?为了解决这个问题,英特尔的工程师提出了段寄存器 CS(代码段)、DS(数据段)、ES(扩展段)和 SS(堆栈段)。要从 20 位地址转换,首先将其除以 16,并将商放在段寄存器中,并将余数放在偏移寄存器中。这表示为 CS:IP(这意味着,CS 是段,IP 是偏移量)。同样,当写入地址 SS:SP 时,意味着 SS 是段,SP 是偏移量。

这也适用于反向操作。如果一个人要创建 20 位地址,而不是从 20 位地址转换,则可以将段寄存器的 16 位值放在地址总线上,但将其左移 4 次(因此实际上将寄存器乘以 16),然后将另一个寄存器的未修改的偏移量添加到总线上的值,从而创建一个完整的 20 位地址。

如果 CS = 258C,IP = 001216,那么 CS:IP 将指向一个 20 位地址,相当于“CS × 16 + IP”,即

258C × 1016 + 001216 = 258C0 + 001216 = 258D2(记住:16 十进制 = 1016)。

20 位地址称为绝对(或线性)地址,Segment:Offset 表示法(CS:IP)称为分段地址。这种分离是必要的,因为寄存器本身无法保存需要超过 16 位编码的值。在 32 位或 64 位处理器上以保护模式编程时,寄存器足够大,可以完全填充地址总线,从而消除了分段地址 - 只有线性/逻辑地址通常在这种“平面寻址”模式中使用,尽管为了向后兼容,仍然支持Segment:Offset 体系结构。

需要注意的是,物理地址与分段地址之间不存在一一映射关系;对于任何物理地址,都可能存在多个分段地址。例如:考虑分段表示 B000:8000 和 B200:6000。经过计算,它们都映射到物理地址 B8000。

B000:8000 = B000 × 1016 + 800016 = B0000 + 800016 = B8000,以及

B200:6000 = B200 × 1016 + 600016 = B2000 + 600016 = B8000。

然而,使用合适的映射方案可以避免这个问题:这样的映射对物理地址进行线性变换,为每个物理地址创建唯一的分段地址。为了反向转换,映射 [f(x)] 只要简单地反转即可。

例如,如果段部分等于物理地址除以 1016,偏移量等于余数,则只生成一个分段地址。(没有偏移量会大于 0F16。) 物理地址 B8000 映射到 (B8000 / 1016):(B8000 mod 1016) 或 B800:0。这种分段表示有一个特殊的名称:这样的地址被称为“规范化地址”。

CS:IP (代码段:指令指针) 表示从哪里获取下一条要执行的指令的物理内存的 20 位地址。类似地,SS:SP (堆栈段:堆栈指针) 指向一个 20 位绝对地址,该地址将被视为堆栈顶端 (8086 使用此地址进行压栈/出栈操作)。

保护模式 (80286+)

[编辑 | 编辑源代码]

虽然这看起来很丑陋,但实际上它是在朝着以后芯片中使用的保护地址方案迈出的一步。80286 具有保护模式,在该模式下,其所有 24 条地址线都可用,允许寻址高达 16 MiB 的内存。在保护模式下,CS、DS、ES 和 SS 寄存器不是段,而是选择器,指向一个表,该表提供有关程序当前正在使用的物理内存块的信息。在这种模式下,指针值 CS:IP = 0010:2400 的使用方式如下

CS 值 001016 是选择器表中的一个偏移量,指向特定的选择器。该选择器将具有一个 24 位值来指示内存块的起始位置,一个 16 位值来指示块的长度,以及标志来指定该块是否可写、是否当前存在于内存中,以及其他信息。假设指向的内存块实际上从 24 位地址 16440016 开始,那么实际引用的地址就是 16440016 + 240016 = 16680016。如果选择器还包含该块长度为 240016 字节的信息,则该引用将指向该块后面的字节,这将导致异常:操作系统不应该允许程序读取它不拥有的内存。如果块被标记为只读,那么代码段内存应该这样,以防止程序覆盖自身,尝试写入该地址也将同样导致异常。

随着 CS 和 IP 在 386 中扩展到 32 位,这种方案变得不再必要;使用指向物理地址 0000000016 的选择器,一个 32 位寄存器可以寻址高达 4 GiB 的内存。然而,选择器仍然用于保护内存免受恶意程序的侵害。例如,如果 Windows 中的程序试图读取或写入它不拥有的内存,它将违反选择器设置的规则,触发异常,Windows 将显示“通用保护错误”消息并将其关闭。

32 位寻址

[编辑 | 编辑源代码]

32 位地址可以覆盖高达 4 GiB 的内存。这意味着我们不需要在 32 位处理器中使用偏移地址。相反,我们使用所谓的“扁平寻址”方案,其中寄存器中的地址直接指向物理内存位置。段寄存器用于定义不同的段,这样程序就不会尝试执行堆栈部分,也不会意外地尝试在数据部分执行堆栈操作。

A20 门事件

[编辑 | 编辑源代码]

如前所述,8086 处理器有 20 条地址线 (从 A0 到 A19),因此它可以寻址的总内存为 1 MiB (或 2 的 20 次方)。但由于它只有 16 位寄存器,所以他们想出了:偏移量方案,否则使用单个 16 位寄存器,他们不可能访问超过 64 KiB (或 2 的 16 次方) 的内存。因此,这使得程序可以访问整个 1 MiB 内存。

但这种分段方案也带来了一种副作用。使用这种方案,你的代码不仅可以引用整个 1 MiB,实际上还可以引用比这稍多一些。让我们看看如何……

让我们记住如何从:偏移量表示转换为线性 20 位表示。

转换

:偏移量 = × 16 + 偏移量

现在,为了查看可以寻址的最大内存量,让我们将偏移量都填充到它们的最大值,然后将该值转换为其 20 位绝对物理地址。

所以,的最大值为 FFFF16偏移量的最大值为 FFFF16

现在,让我们将 FFFF:FFFF 转换为其 20 位线性地址,记住 1610 在十六进制中表示为 10。

所以我们得到,FFFF:FFFF -> FFFF × 1016 + FFFF = FFFF0 (1 MiB - 16 字节) + FFFF (64 KiB) = FFFFF + FFF0 = 1 MiB + FFF0 字节。

  • 注意:FFFFF 是十六进制,等于 1 MiBFFF0 等于 64 KiB 减去 16 字节。

故事的寓意:从实模式来看,程序实际上可以引用 (1 MiB + 64 KiB - 16) 字节的内存。

请注意这里使用的是“引用”一词,而不是“访问”。程序可以引用这么多的内存,但它是否能够访问,取决于实际存在的地址线的数量。所以对于 8086 来说,这绝对是不可能的,因为当程序引用超过 1 MiB 的内存时,放在地址线上的地址实际上超过了 20 位,这会导致地址绕回。

例如,如果代码引用 1 MiB,它将绕回并指向内存位置 0,类似地 1 MiB + 1 将绕回至地址 1 (或 0000:0001)。

现在,在那个时候有一些非常棒的程序员,他们在代码中利用了这种特性,即地址会绕回,使他们的代码速度更快,而且代码更短。使用这种技术,他们可以访问 32 KiB 的顶部内存区域 (即与 1 MiB 边界相接的 32 KiB) 和 32 KiB 的底部内存区域,而无需重新加载段寄存器!

你看到了简单的数学原理,如果在:偏移量表示中,你使保持不变,由于偏移量是一个 16 位的值,因此你可以在 64 KiB (或 2 的 16 次方) 的内存区域内随意移动。现在,如果你让你的段寄存器指向 1 MiB 标记以下的 32 KiB,你可以访问向上延伸至 1 MiB 边界的 32 KiB,然后访问再向上的 32 KiB,最终会绕回到最底部的 32 KiB。

现在,这些非常棒的程序员忽视了一个事实,那就是将会制造出具有更多地址线的处理器。(注意:比尔·盖茨被认为说过“谁会需要超过 640 KB 的内存?”,这些程序员可能也持有类似的想法。) 1982 年,仅仅在 8086 推出两年后,英特尔发布了具有 24 条地址线的 80286 处理器。虽然从理论上来说,它与传统的 8086 程序向后兼容,因为它也支持实模式,但许多 8086 程序不能正常工作,因为它们依赖于越界地址绕回到较低的内存段。因此,为了兼容性,IBM 工程师将 A20 地址线 (8086 具有 A0 - A19 线) 通过键盘控制器进行路由,并提供了一种机制来启用/禁用 A20 兼容模式。现在如果你想知道为什么是键盘控制器,答案是它有一个未使用的引脚。由于 80286 将被宣传为与 8086 完全兼容 (甚至还没有推出很久),所以如果 80286 不能实现完全的 bug-for-bug 兼容性,升级后的客户将非常生气,这样为 8086 设计的代码在 80286 上也能正常运行,只是速度更快。

华夏公益教科书