[ebp*2] 是否引用 DS 或 SS 段?
Does [ebp*2] reference DS or SS segment?
IDM 说如果 EBP 用作基址寄存器,内存操作使用 SS 段。因此,[ebp + esi]
和 [esi + ebp]
分别引用 SS 和 DS 段。请参阅 NASM 的文档:3.3 Effective Address。
在上面同一节中,NASM 提到了如何通过将 [eax*2]
替换为 [eax+eax]
来生成更短的机器代码。
然而,NASM 也会为 [ebp*2]
生成 [ebp + ebp]
(即没有基址寄存器)。
我怀疑 [ebp+ebp]
引用了 SS 段,[ebp*2]
引用了 DS 段。
我问了NASM这个问题。他们认为 [ebp*2]
和 [ebp+ebp]
是一样的,但这对我来说没有意义。显然,[ebp+ebp]
(ebp 作为基址寄存器)引用了 SS 段。如果它们相同,[ebp*2
也必须引用 SS。这意味着只要 ebp
是基址或索引寄存器,就会引用 SS,这反过来意味着,[ebp + esi]
和 [esi + ebp]
都引用 SS 段,因此它们必须相同。
有谁知道 [ebp*2]
使用哪个段?
英特尔手册告诉我们下面图3-11,它处理Offset = Base + (Index * Scale) + Displacement
:
The uses of general-purpose registers as base or index components are restricted in the following manner:
- The ESP register cannot be used as an index register.
- When the ESP or EBP register is used as the base, the SS segment is the default segment. In all other cases, the DS segment is the default segment.
意思是NASM把[ebp*2]
改成[ebp+ebp]
时是错误的(为了避免32位的位移)
[ebp*2]
使用 DS
因为 ebp
不用作基础
[ebp+ebp]
使用 SS
因为 ebp
之一被 用作基础
那么最好指定您不希望 NASM.
出现这种行为
在 NASM 作者意识到他们的错误之前,您可以通过以下方式禁用此行为(其中 EBP
用作索引):
[NoSplit ebp*2]
的确,NASM的优化选择是不一致的,假设ss
和ds
在将[ebp*2]
拆分为[时可以互换(即平面内存模型) =14=] 以节省 3 个字节(disp32 与 disp8),但 未 将 [ebp + esi]
优化为 [esi + ebp]
以避免 disp8.
(和 the NASM manual even mentions the different default segment,与您从错误信息中得出的关于 [0 + ebp*2]
与 [0+ebp+ebp*1]
的结论相矛盾。)
EBP或ESP作为基址寄存器表示SS,否则默认为DS。当两个寄存器用于 NASM 寻址模式时,第一个是基数,除非你写 [ebp*1 + esi]
,明确地将比例因子应用于第一个。索引寄存器从不暗示段,如果您考虑设计意图,这是有意义的:相对于基址寄存器给出的 segment:offset 或绝对 disp32
.
的索引
正如所写,[ebp*2]
是一种索引寻址模式,隐含地需要 4 个字节的零作为 32 位位移。你可以得到 NASM 用 [nosplit ebp*2]
.
那样编码
也许 NASM 和 YASM 忽略了这个极端情况,因为平面内存模型在 16 位代码之外几乎是通用的。(并且 16 位寻址模式是不同并且不支持比例因子。尽管您 可以 在 16 位代码中使用 32 位寻址模式以利用比例因子和更广泛的寄存器选择,即使在纯实数中模式而不是 "unreal" mode which lets you set segment limits high enough that offsets > 2^16 are usable.)
所有主流的 32 位和 64 位 x86 操作系统都使用平面内存模型,其中 SS 和 DS 可以互换,当您不做任何奇怪的事情时,这种优化在这些操作系统下是安全的。分段有时是 used to make non-executable stacks before that was supported by page tables,但那仍然是一个平面内存模型。 (64 位代码将 base/limit 修复为 CS/DS/ES/SS,因此此优化在那里始终是安全的,除非 SS
是一个完全不可用的段,例如 write-protected 如果可能的话。)
不过,平面内存模型的任何假设都应该是可选的。这是 NASM 和 YASM 中的错误。他们应该尊重 SS 和 DS 之间的区别,或者应该充分利用平面内存模型来帮助那些不记得哪种寻址模式需要 "hidden" 额外字节的程序员,比如优化 [ebp+esi]
没有位移到 [esi+ebp]
。最好应该有一个选项或指令告诉汇编程序它可以假设 SS 和 DS 是相同的。
LEA 的操作数总是可以利用,因为 LEA 只处理地址的偏移部分,所以段是无关紧要的。 (这将是像 [ebp*2]
这样没有位移的寻址模式最常见的用例:使用它作为内存地址可能会模拟 word-addressable 内存?这很奇怪,通常有一个真正的指针作为地址的一个组成部分。)
了解 x86 32/64 位寻址模式:
除了 64 位 RIP-relative 寻址之外,32/64 位寻址模式是 disp0/8/32 + base_reg + idx_reg*1/2/4/8
的任何子集,其中 3 项中的每一项/ 组件是可选的。 但 至少需要 disp32 或基址寄存器之一。 (另见 )。
[disp32=0 + ebp*2]
(disp32=零)默认段 = DS。你可以从[nosplit ebp*2]
中获取NASM中的这种编码,而像[ebp*4]
这样的地址是不能拆分的。
[ebp + ebp + disp8=0]
默认段 = SS,因为 EBP 用作基址寄存器。
表示 ebp
没有位移的编码实际上意味着没有基数 reg 的 disp32,因此 disp32 实际上是基数(暗示段寄存器 DS,因为基数不是 EBP 或 ESP)。有或没有 SIB 字节都是这种情况,因此 [ebp + ebp*1]
仍然必须使用 disp8=0 进行编码。其他寄存器没有这个问题,所以通常拆分可以节省 4 个字节,而不是 EBP 仅节省 3 个字节。 (除了使用与 RBP 相同的 ModR/M 编码的 r13
外,我想这部分解码硬件不需要来自 REX 前缀的额外位。)
ESP 不能是变址寄存器,所以 [esp*2]
不可能编码有或没有分裂。所以NASM的优化特例只影响EBP*2
。 (base=ESP是SIB字节的转义码,SIB字节中的index=ESP表示没有索引,可以编码[esp + 12]
。)
但不幸的是 NASM/YASM 拆分 EBP*2
即使有一个常量无论如何都需要 disp32,比如 [symbol + ebp*2]
,但它不需要'保存任何字节,事实上 . 3-component lea eax, [symbol + ebp + ebp*1]
is slower than 2-component lea eax, [symbol + ebp*2]
: higher latency and 1-per-clock throughput instead of 2. According to http://agner.org/optimize/,那些在 AMD Bulldozer/Ryzen 上同样慢,因为缩放索引使 "slow-LEA" 即使只有 2 个组件。
IDK 如果任何旧 CPU 在非缩放索引和 3 分量寻址模式下做得更好,用于 LEA 或实际内存操作数。
NASM 和 YASM 行为:
$ nasm -felf32 -g -Fdwarf foo.asm
$ objdump -drwC -Mintel -S foo.o | sed 's/DWORD PTR//'
# (edited to put the NASM source line's addressing mode onto the same line as the disassembler output, instead of separate lines)
00000000 <sym-0x2c>:
0: 8b 04 2e mov eax, [esi+ebp*1] ; [esi+ebp]
3: 8b 44 35 00 mov eax, [ebp+esi*1+0x0] ; [ebp + esi]
7: 8b 04 2e mov eax, [esi+ebp*1] ; [ebp*1 + esi]
a: 8b 44 2d 00 mov eax, [ebp+ebp*1+0x0] ; [ebp*2]
e: 8b 04 6d 00 00 00 00 mov eax, [ebp*2+0x0] ; [nosplit ebp*2]
15: 8b 45 00 mov eax, [ebp+0x0] ; [ebp*1] ; "split" into base=ebp with no SIB byte
18: 8b 04 2d 00 00 00 00 mov eax, [ebp*1+0x0] ; [nosplit ebp*1]
1f: 8b 84 2d d2 04 00 00 mov eax, [ebp+ebp*1+0x4d2] ; [ebp*2 + 1234] ; bad split for LEA, neutral on modern CPUs for load/store
26: 8b 85 15 cd 5b 07 mov eax, [ebp+0x75bcd15] ; [ebp*1 + 123456789]
sym: ; using a symbol reference instead of a numeric constant doesn't change anything
2c: 8b 84 2d 2c 00 00 00 mov eax, [ebp+ebp*1+0x2c] 2f: R_386_32 .text ; [ebp*2 + sym]
33: 8b 84 2d 2c 00 00 00 mov eax, [ebp+ebp*1+0x2c] 36: R_386_32 .text ; [sym + ebp*2]
YASM 对所有这些情况的编码与 NASM 相同。
IDM 说如果 EBP 用作基址寄存器,内存操作使用 SS 段。因此,[ebp + esi]
和 [esi + ebp]
分别引用 SS 和 DS 段。请参阅 NASM 的文档:3.3 Effective Address。
在上面同一节中,NASM 提到了如何通过将 [eax*2]
替换为 [eax+eax]
来生成更短的机器代码。
然而,NASM 也会为 [ebp*2]
生成 [ebp + ebp]
(即没有基址寄存器)。
我怀疑 [ebp+ebp]
引用了 SS 段,[ebp*2]
引用了 DS 段。
我问了NASM这个问题。他们认为 [ebp*2]
和 [ebp+ebp]
是一样的,但这对我来说没有意义。显然,[ebp+ebp]
(ebp 作为基址寄存器)引用了 SS 段。如果它们相同,[ebp*2
也必须引用 SS。这意味着只要 ebp
是基址或索引寄存器,就会引用 SS,这反过来意味着,[ebp + esi]
和 [esi + ebp]
都引用 SS 段,因此它们必须相同。
有谁知道 [ebp*2]
使用哪个段?
英特尔手册告诉我们下面图3-11,它处理Offset = Base + (Index * Scale) + Displacement
:
The uses of general-purpose registers as base or index components are restricted in the following manner:
- The ESP register cannot be used as an index register.
- When the ESP or EBP register is used as the base, the SS segment is the default segment. In all other cases, the DS segment is the default segment.
意思是NASM把[ebp*2]
改成[ebp+ebp]
时是错误的(为了避免32位的位移)
[ebp*2]
使用 DS
因为 ebp
不用作基础
[ebp+ebp]
使用 SS
因为 ebp
之一被 用作基础
那么最好指定您不希望 NASM.
出现这种行为
在 NASM 作者意识到他们的错误之前,您可以通过以下方式禁用此行为(其中 EBP
用作索引):
[NoSplit ebp*2]
的确,NASM的优化选择是不一致的,假设ss
和ds
在将[ebp*2]
拆分为[时可以互换(即平面内存模型) =14=] 以节省 3 个字节(disp32 与 disp8),但 未 将 [ebp + esi]
优化为 [esi + ebp]
以避免 disp8.
(和 the NASM manual even mentions the different default segment,与您从错误信息中得出的关于 [0 + ebp*2]
与 [0+ebp+ebp*1]
的结论相矛盾。)
EBP或ESP作为基址寄存器表示SS,否则默认为DS。当两个寄存器用于 NASM 寻址模式时,第一个是基数,除非你写 [ebp*1 + esi]
,明确地将比例因子应用于第一个。索引寄存器从不暗示段,如果您考虑设计意图,这是有意义的:相对于基址寄存器给出的 segment:offset 或绝对 disp32
.
正如所写,[ebp*2]
是一种索引寻址模式,隐含地需要 4 个字节的零作为 32 位位移。你可以得到 NASM 用 [nosplit ebp*2]
.
也许 NASM 和 YASM 忽略了这个极端情况,因为平面内存模型在 16 位代码之外几乎是通用的。(并且 16 位寻址模式是不同并且不支持比例因子。尽管您 可以 在 16 位代码中使用 32 位寻址模式以利用比例因子和更广泛的寄存器选择,即使在纯实数中模式而不是 "unreal" mode which lets you set segment limits high enough that offsets > 2^16 are usable.)
所有主流的 32 位和 64 位 x86 操作系统都使用平面内存模型,其中 SS 和 DS 可以互换,当您不做任何奇怪的事情时,这种优化在这些操作系统下是安全的。分段有时是 used to make non-executable stacks before that was supported by page tables,但那仍然是一个平面内存模型。 (64 位代码将 base/limit 修复为 CS/DS/ES/SS,因此此优化在那里始终是安全的,除非 SS
是一个完全不可用的段,例如 write-protected 如果可能的话。)
不过,平面内存模型的任何假设都应该是可选的。这是 NASM 和 YASM 中的错误。他们应该尊重 SS 和 DS 之间的区别,或者应该充分利用平面内存模型来帮助那些不记得哪种寻址模式需要 "hidden" 额外字节的程序员,比如优化 [ebp+esi]
没有位移到 [esi+ebp]
。最好应该有一个选项或指令告诉汇编程序它可以假设 SS 和 DS 是相同的。
LEA 的操作数总是可以利用,因为 LEA 只处理地址的偏移部分,所以段是无关紧要的。 (这将是像 [ebp*2]
这样没有位移的寻址模式最常见的用例:使用它作为内存地址可能会模拟 word-addressable 内存?这很奇怪,通常有一个真正的指针作为地址的一个组成部分。)
了解 x86 32/64 位寻址模式:
除了 64 位 RIP-relative 寻址之外,32/64 位寻址模式是 disp0/8/32 + base_reg + idx_reg*1/2/4/8
的任何子集,其中 3 项中的每一项/ 组件是可选的。 但 至少需要 disp32 或基址寄存器之一。 (另见
[disp32=0 + ebp*2]
(disp32=零)默认段 = DS。你可以从[nosplit ebp*2]
中获取NASM中的这种编码,而像[ebp*4]
这样的地址是不能拆分的。
[ebp + ebp + disp8=0]
默认段 = SS,因为 EBP 用作基址寄存器。
表示 ebp
没有位移的编码实际上意味着没有基数 reg 的 disp32,因此 disp32 实际上是基数(暗示段寄存器 DS,因为基数不是 EBP 或 ESP)。有或没有 SIB 字节都是这种情况,因此 [ebp + ebp*1]
仍然必须使用 disp8=0 进行编码。其他寄存器没有这个问题,所以通常拆分可以节省 4 个字节,而不是 EBP 仅节省 3 个字节。 (除了使用与 RBP 相同的 ModR/M 编码的 r13
外,我想这部分解码硬件不需要来自 REX 前缀的额外位。)
ESP 不能是变址寄存器,所以 [esp*2]
不可能编码有或没有分裂。所以NASM的优化特例只影响EBP*2
。 (base=ESP是SIB字节的转义码,SIB字节中的index=ESP表示没有索引,可以编码[esp + 12]
。)
但不幸的是 NASM/YASM 拆分 EBP*2
即使有一个常量无论如何都需要 disp32,比如 [symbol + ebp*2]
,但它不需要'保存任何字节,事实上 lea eax, [symbol + ebp + ebp*1]
is slower than 2-component lea eax, [symbol + ebp*2]
: higher latency and 1-per-clock throughput instead of 2. According to http://agner.org/optimize/,那些在 AMD Bulldozer/Ryzen 上同样慢,因为缩放索引使 "slow-LEA" 即使只有 2 个组件。
IDK 如果任何旧 CPU 在非缩放索引和 3 分量寻址模式下做得更好,用于 LEA 或实际内存操作数。
NASM 和 YASM 行为:
$ nasm -felf32 -g -Fdwarf foo.asm
$ objdump -drwC -Mintel -S foo.o | sed 's/DWORD PTR//'
# (edited to put the NASM source line's addressing mode onto the same line as the disassembler output, instead of separate lines)
00000000 <sym-0x2c>:
0: 8b 04 2e mov eax, [esi+ebp*1] ; [esi+ebp]
3: 8b 44 35 00 mov eax, [ebp+esi*1+0x0] ; [ebp + esi]
7: 8b 04 2e mov eax, [esi+ebp*1] ; [ebp*1 + esi]
a: 8b 44 2d 00 mov eax, [ebp+ebp*1+0x0] ; [ebp*2]
e: 8b 04 6d 00 00 00 00 mov eax, [ebp*2+0x0] ; [nosplit ebp*2]
15: 8b 45 00 mov eax, [ebp+0x0] ; [ebp*1] ; "split" into base=ebp with no SIB byte
18: 8b 04 2d 00 00 00 00 mov eax, [ebp*1+0x0] ; [nosplit ebp*1]
1f: 8b 84 2d d2 04 00 00 mov eax, [ebp+ebp*1+0x4d2] ; [ebp*2 + 1234] ; bad split for LEA, neutral on modern CPUs for load/store
26: 8b 85 15 cd 5b 07 mov eax, [ebp+0x75bcd15] ; [ebp*1 + 123456789]
sym: ; using a symbol reference instead of a numeric constant doesn't change anything
2c: 8b 84 2d 2c 00 00 00 mov eax, [ebp+ebp*1+0x2c] 2f: R_386_32 .text ; [ebp*2 + sym]
33: 8b 84 2d 2c 00 00 00 mov eax, [ebp+ebp*1+0x2c] 36: R_386_32 .text ; [sym + ebp*2]
YASM 对所有这些情况的编码与 NASM 相同。