如何将 3 个字节(24 位)从内存移动到寄存器?
How to MOVe 3 bytes (24bits) from memory to a register?
我可以使用 MOV
指令将存储在内存中的数据项移动到我选择的通用寄存器中。
MOV r8, [m8]
MOV r16, [m16]
MOV r32, [m32]
MOV r64, [m64]
现在,不要拍我,但是如何实现以下内容:MOV r24, [m24]
? (我明白后者是不合法的)。
在我的例子中,我想移动字符“Pip”,即0x706950h,来注册rax
。
section .data ; Section containing initialized data
14 DogsName: db "PippaChips"
15 DogsNameLen: equ $-DogsName
我首先考虑的是我可以单独移动字节,即首先是一个字节,然后是一个字,或者它们的某种组合。但是,我无法引用 eax
、rax
的“上半部分”,所以这在第一个障碍处失败了,因为我最终会覆盖首先移动的任何数据。
我的解决方案:
26 mov al, byte [DogsName + 2] ; move the character “p” to register al
27 shl rax, 16 ; shift bits left by 16, clearing ax to receive characters “pi”
28 mov ax, word [DogsName] ; move the characters “Pi” to register ax
我可以将“Pip”声明为已初始化的数据项,但示例只是一个示例,我想了解如何在汇编中引用 24 位,或 40、48 位……就此而言。
有没有更类似于MOV r24, [m24]
的指令?有没有办法 select 一系列内存地址,而不是提供偏移量和指定大小运算符。如何将 3 个字节从内存移动到 ASM 中的寄存器 x86_64?
NASM 版本 2.11.08 架构 x86
写入24位的唯一方法是使用MMX(MASKMOVQ
)或SSE(MASMODQU
)和掩码以防止您不想修改的字节被修改。但是,对于单次写入,MMX 和 SSE 过于复杂(并且可能更慢)。
请注意,通常读取比写入便宜(尤其是涉及多个 CPU 时)。考虑到这一点,另一种选择是:
shl eax,8
mov al,[DogsName+3]
ror eax,8
mov [DogsName],eax
这会用旧值覆盖后面的字节(如果后面的字节不可访问,或者后面的字节属于需要自动更新的任何内容,则可能会导致问题)。
如果您知道 3 字节 int 不在页面末尾,通常您会 执行 4 字节加载并屏蔽掉高垃圾带有您想要的字节,或者如果您正在处理不关心高位的数据,则直接忽略它。
不像商店1,加载你“不应该”的数据永远不会成为正确性的问题,除非你进入一个未映射的页。 (例如,如果 db "pip"
出现在页面末尾,而下一页未映射。)但在这种情况下,您 知道 它是一个较长字符串的一部分,因此如果宽负载扩展到下一个缓存行(因此负载跨越缓存行边界),唯一可能的缺点是性能。
对于任何 3 个字节,前一个字节或后一个字节始终可以安全访问(如果 3 个字节本身没有在两个缓存行之间拆分,则甚至不会跨越缓存行边界)。在 运行 时解决这个问题可能不值得,但是 如果你在编译时知道对齐方式,你可以做任何一个
mov eax, [DogsName-1] ; if previous byte is in the same page/cache line
shr eax, 8
mov eax, [DogsName] ; if following byte is in the same page/cache line
and eax, 0x00FFFFFF
我假设你想要 zero-extend the result into eax/rax, like 32-bit operand-size, instead of merging with the existing high byte(s) of EAX/RAX like 8 or 16-bit operand-size register writes. If you do want to merge, mask the old value and OR
. Or if you loaded from [DogsName-1]
so the bytes you want are in the top 3 positions of EAX, and you want to merge into ECX: shr ecx, 24
/ shld ecx, eax, 24
to shift the old top byte down to the bottom, then shift it back while shifting in the 3 new bytes. (There's no memory-source form of shld
, unfortunately. Semi-related: .) shld
is fast on Intel CPUs (especially Sandybridge and later: 1 uop), but not on AMD (http://agner.org/optimize/).
合并 2 个单独的负载
有很多方法可以做到这一点,但不幸的是,没有一种最快的方法可以跨越所有 CPUs。 。您的方式(字节加载/移位/字加载到 ax
)在 CPU 上相当不错,而不是 Core2/Nehalem(当您阅读 [=24 时,它将停止插入合并 uop =] 组装后)。但是从 movzx eax, byte [DogsName + 2]
开始打破对 rax
.
旧值的依赖
您希望编译器生成的经典“处处安全”代码是:
DEFAULT REL ; compilers use RIP-relative addressing for static data; you should too.
movzx eax, byte [DogsName + 2] ; avoid false dependency on old EAX
movzx ecx, word [DogsName]
shl eax, 16
or eax, ecx
这需要一条额外的指令,但避免写入任何部分寄存器。但是,在 Core2 或 Nehalem 以外的 CPUs 上,2 次加载的最佳选择是写入 ax
。 (Core2之前的Intel P6不能运行x86-64代码,CPU没有partial-register重命名的写ax
时会合并到rax
。 Sandybridge 仍然重命名 AX,但合并仅花费 1 uop,没有停顿,即与 OR 相同,但在 Core2/Nehalem 上,前端在插入合并 uop 时停顿了大约 3 个周期。
,因此在那些 CPU 中,加载到 AX 中是微融合加载+合并。 Agner Fog 没有在 Silvermont 或 Ryzen(或我查看的电子表格中的任何其他选项卡)上列出对 mov r16, m
的额外惩罚,因此可能其他没有部分注册重命名的 CPUs 也会执行 mov ax, [mem]
作为加载+合并。
movzx eax, byte [DogsName + 2]
shl eax, 16
mov ax, word [DogsName]
; when read eax:
; * Sandybridge: extra 1 uop inserted to merge
; * core2 / nehalem: ~3 cycle stall (unless you don't use it until after the load retires)
; * everything else (including IvB+): no penalty, merge already done
实际上,在 运行 时测试对齐可以高效地完成。给定寄存器中的指针,除非地址的最后几个 5 或 6 位全为零,否则前一个字节位于同一缓存行中。 (即地址与缓存行的开头对齐)。假设缓存行是 64 字节;所有当前的 CPUs 都使用它,我认为不存在任何具有 32 字节行的 x86-64 CPUs。 (而且我们仍然绝对避免页面交叉)。
; pointer to m24 in RSI
; result: EAX = zero_extend(m24)
test sil, 111111b ; test all 6 low bits. There's no TEST r32, imm8, so REX r8, imm8 is shorter and never slower.
jz .aligned_by_64
mov eax, [rsi-1]
shr eax, 8
.loaded:
...
ret ; end of whatever large function this is part of
; unlikely block placed out-of-line to keep the common case fast
.aligned_by_64:
mov eax, [rsi]
and eax, 0x00FFFFFF
jmp .loaded
所以在一般情况下,额外的成本只是一个未参加的测试和分支uop。
根据 CPU、输入和周围的代码,测试低 12 位(仅避免跨越 4k 边界)会为页面内的某些缓存行拆分提供更好的分支预测,但是仍然从不分页。 (在那种情况下 test esi, (1<<12)-1
。与使用 imm8
测试 sil
不同,使用 imm16
测试 si
不值得在英特尔 CPUs 来节省 1 个字节的代码。当然,如果您可以将指针放在 ra/b/c/dx 中,则不需要 REX 前缀,甚至还有 test al, imm8
的紧凑 2 字节编码。 )
您甚至可以无分支地执行此操作,但与仅执行 2 个单独的加载相比显然不值得!
; pointer to m24 in RSI
; result: EAX = zero_extend(m24)
xor ecx, ecx
test sil, 7 ; might as well keep it within a qword if we're not branching
setnz cl ; ecx = (not_start_of_line) ? : 1 : 0
sub rsi, rcx ; normally rsi-1
mov eax, [rsi]
shl ecx, 3 ; cl = 8 : 0
shr eax, cl ; eax >>= 8 : eax >>= 0
; with BMI2: shrx eax, [rsi], ecx is more efficient
and eax, 0x00FFFFFF ; mask off to handle the case where we didn't shift.
真正的架构 24 位加载或存储
在体系结构上,x86 没有使用 整数 寄存器作为目标或源的 24 位加载或存储。正如 Brandon 指出的那样,MMX / SSE 掩码存储(如 MASKMOVDQU
, not to be confused with pmovmskb eax, xmm0
)可以 store 来自 MMX 或 XMM reg 的 24 位,给定一个仅设置了低 3 个字节的矢量掩码.但是它们几乎从来没有用过,因为它们很慢并且总是有 NT 提示(所以它们围绕缓存写入,并像 movntdq
那样强制驱逐)。 (AVX dword/qword 掩码 load/store 指令并不意味着 NT,但不适用于字节粒度。)
AVX512BW (Skylake-server) adds vmovdqu8
为加载和存储提供字节屏蔽,并对被屏蔽的字节进行故障抑制。 (即,如果 16 字节加载包含未映射页面中的字节,只要未为该字节设置掩码位,您就不会出现段错误。但这确实会导致速度大幅下降)。所以 微架构 它仍然是一个 16 字节的负载,但是对架构状态的影响(即除性能之外的所有内容)恰好是真正的 3 字节 load/store (具有正确的面具)。
您可以在 XMM、YMM 或 ZMM 寄存器上使用它。
;; probably slower than the integer way, especially if you don't actually want the result in a vector
mov eax, 7 ; low 3 bits set
kmovw k1, eax ; hoist the mask setup out of a loop
; load: leave out the {z} to merge into the old xmm0 (or ymm0 / zmm0)
vmovdqu8 xmm0{k1}{z}, [rsi] ; {z}ero-masked 16-byte load into xmm0 (with fault-suppression)
vmovd eax, xmm0
; store
vmovd xmm0, eax
vmovdqu8 [rsi]{k1}, xmm0 ; merge-masked 16-byte store (with fault-suppression)
这与 NASM 2.13.01 一起组装。 IDK 如果您的 NASM 足够新以支持 AVX512。您可以使用 Intel 的 Software Development Emulator (SDE)
在没有硬件的情况下玩 AVX512
这看起来很酷,因为只需 2 微指令就可以将结果输入 eax
(设置掩码后)。 (但是,Skylake-X 的 http://instlatx64.atw.hu/'s spreadsheet of data from IACA 不包括带掩码的 vmovdqu8
,仅包括未掩码的形式。这些确实表明它仍然是单个 uop 负载,或者像常规一样的微融合存储vmovdqu/a
)
但是当心如果 16 字节加载出现故障或越过高速缓存行边界,速度会变慢。我认为它在内部 确实 执行加载然后丢弃字节,如果需要抑制故障,可能会有昂贵的特殊情况。
此外,对于商店版本,请注意屏蔽商店不会有效地转发到负载。 (有关更多信息,请参阅英特尔的优化手册)。
脚注:
- 宽存储是一个问题,因为即使您替换旧值,您也会执行非原子读-修改-写,如果您放回的那个字节是一个锁,这可能会破坏事情,例如. 不要在对象外部存储,除非您知道接下来会发生什么并且它是安全的,例如你放在那里的填充允许这个。你可以可以
lock cmpxchg
一个修改过的4字节值到位,以确保你没有踩到另一个线程对额外字节的更新,但显然做 2 个单独的存储比原子 cmpxchg
重试循环 多 性能更好。
BZHI r32a, r/m32, r32b Zero bits in r/m32 starting with the position in r32b, write result to r32a
BZHI r64a, r/m64, r64b Zero bits in r/m64 starting with the position in r64b, write result to r64a
所以load the low 24 bits from [mem]
你可以使用
MOV eax, 24
BZHI eax, [mem], eax
有了这个,你还可以从内存中加载可变数量的位
我可以使用 MOV
指令将存储在内存中的数据项移动到我选择的通用寄存器中。
MOV r8, [m8]
MOV r16, [m16]
MOV r32, [m32]
MOV r64, [m64]
现在,不要拍我,但是如何实现以下内容:MOV r24, [m24]
? (我明白后者是不合法的)。
在我的例子中,我想移动字符“Pip”,即0x706950h,来注册rax
。
section .data ; Section containing initialized data
14 DogsName: db "PippaChips"
15 DogsNameLen: equ $-DogsName
我首先考虑的是我可以单独移动字节,即首先是一个字节,然后是一个字,或者它们的某种组合。但是,我无法引用 eax
、rax
的“上半部分”,所以这在第一个障碍处失败了,因为我最终会覆盖首先移动的任何数据。
我的解决方案:
26 mov al, byte [DogsName + 2] ; move the character “p” to register al
27 shl rax, 16 ; shift bits left by 16, clearing ax to receive characters “pi”
28 mov ax, word [DogsName] ; move the characters “Pi” to register ax
我可以将“Pip”声明为已初始化的数据项,但示例只是一个示例,我想了解如何在汇编中引用 24 位,或 40、48 位……就此而言。
有没有更类似于MOV r24, [m24]
的指令?有没有办法 select 一系列内存地址,而不是提供偏移量和指定大小运算符。如何将 3 个字节从内存移动到 ASM 中的寄存器 x86_64?
NASM 版本 2.11.08 架构 x86
写入24位的唯一方法是使用MMX(MASKMOVQ
)或SSE(MASMODQU
)和掩码以防止您不想修改的字节被修改。但是,对于单次写入,MMX 和 SSE 过于复杂(并且可能更慢)。
请注意,通常读取比写入便宜(尤其是涉及多个 CPU 时)。考虑到这一点,另一种选择是:
shl eax,8
mov al,[DogsName+3]
ror eax,8
mov [DogsName],eax
这会用旧值覆盖后面的字节(如果后面的字节不可访问,或者后面的字节属于需要自动更新的任何内容,则可能会导致问题)。
如果您知道 3 字节 int 不在页面末尾,通常您会 执行 4 字节加载并屏蔽掉高垃圾带有您想要的字节,或者如果您正在处理不关心高位的数据,则直接忽略它。
不像商店1,加载你“不应该”的数据永远不会成为正确性的问题,除非你进入一个未映射的页。 (例如,如果 db "pip"
出现在页面末尾,而下一页未映射。)但在这种情况下,您 知道 它是一个较长字符串的一部分,因此如果宽负载扩展到下一个缓存行(因此负载跨越缓存行边界),唯一可能的缺点是性能。
对于任何 3 个字节,前一个字节或后一个字节始终可以安全访问(如果 3 个字节本身没有在两个缓存行之间拆分,则甚至不会跨越缓存行边界)。在 运行 时解决这个问题可能不值得,但是 如果你在编译时知道对齐方式,你可以做任何一个
mov eax, [DogsName-1] ; if previous byte is in the same page/cache line
shr eax, 8
mov eax, [DogsName] ; if following byte is in the same page/cache line
and eax, 0x00FFFFFF
我假设你想要 zero-extend the result into eax/rax, like 32-bit operand-size, instead of merging with the existing high byte(s) of EAX/RAX like 8 or 16-bit operand-size register writes. If you do want to merge, mask the old value and OR
. Or if you loaded from [DogsName-1]
so the bytes you want are in the top 3 positions of EAX, and you want to merge into ECX: shr ecx, 24
/ shld ecx, eax, 24
to shift the old top byte down to the bottom, then shift it back while shifting in the 3 new bytes. (There's no memory-source form of shld
, unfortunately. Semi-related: shld
is fast on Intel CPUs (especially Sandybridge and later: 1 uop), but not on AMD (http://agner.org/optimize/).
合并 2 个单独的负载
有很多方法可以做到这一点,但不幸的是,没有一种最快的方法可以跨越所有 CPUs。 ax
)在 CPU 上相当不错,而不是 Core2/Nehalem(当您阅读 [=24 时,它将停止插入合并 uop =] 组装后)。但是从 movzx eax, byte [DogsName + 2]
开始打破对 rax
.
您希望编译器生成的经典“处处安全”代码是:
DEFAULT REL ; compilers use RIP-relative addressing for static data; you should too.
movzx eax, byte [DogsName + 2] ; avoid false dependency on old EAX
movzx ecx, word [DogsName]
shl eax, 16
or eax, ecx
这需要一条额外的指令,但避免写入任何部分寄存器。但是,在 Core2 或 Nehalem 以外的 CPUs 上,2 次加载的最佳选择是写入 ax
。 (Core2之前的Intel P6不能运行x86-64代码,CPU没有partial-register重命名的写ax
时会合并到rax
。 Sandybridge 仍然重命名 AX,但合并仅花费 1 uop,没有停顿,即与 OR 相同,但在 Core2/Nehalem 上,前端在插入合并 uop 时停顿了大约 3 个周期。
mov r16, m
的额外惩罚,因此可能其他没有部分注册重命名的 CPUs 也会执行 mov ax, [mem]
作为加载+合并。
movzx eax, byte [DogsName + 2]
shl eax, 16
mov ax, word [DogsName]
; when read eax:
; * Sandybridge: extra 1 uop inserted to merge
; * core2 / nehalem: ~3 cycle stall (unless you don't use it until after the load retires)
; * everything else (including IvB+): no penalty, merge already done
实际上,在 运行 时测试对齐可以高效地完成。给定寄存器中的指针,除非地址的最后几个 5 或 6 位全为零,否则前一个字节位于同一缓存行中。 (即地址与缓存行的开头对齐)。假设缓存行是 64 字节;所有当前的 CPUs 都使用它,我认为不存在任何具有 32 字节行的 x86-64 CPUs。 (而且我们仍然绝对避免页面交叉)。
; pointer to m24 in RSI
; result: EAX = zero_extend(m24)
test sil, 111111b ; test all 6 low bits. There's no TEST r32, imm8, so REX r8, imm8 is shorter and never slower.
jz .aligned_by_64
mov eax, [rsi-1]
shr eax, 8
.loaded:
...
ret ; end of whatever large function this is part of
; unlikely block placed out-of-line to keep the common case fast
.aligned_by_64:
mov eax, [rsi]
and eax, 0x00FFFFFF
jmp .loaded
所以在一般情况下,额外的成本只是一个未参加的测试和分支uop。
根据 CPU、输入和周围的代码,测试低 12 位(仅避免跨越 4k 边界)会为页面内的某些缓存行拆分提供更好的分支预测,但是仍然从不分页。 (在那种情况下 test esi, (1<<12)-1
。与使用 imm8
测试 sil
不同,使用 imm16
测试 si
不值得在英特尔 CPUs 来节省 1 个字节的代码。当然,如果您可以将指针放在 ra/b/c/dx 中,则不需要 REX 前缀,甚至还有 test al, imm8
的紧凑 2 字节编码。 )
您甚至可以无分支地执行此操作,但与仅执行 2 个单独的加载相比显然不值得!
; pointer to m24 in RSI
; result: EAX = zero_extend(m24)
xor ecx, ecx
test sil, 7 ; might as well keep it within a qword if we're not branching
setnz cl ; ecx = (not_start_of_line) ? : 1 : 0
sub rsi, rcx ; normally rsi-1
mov eax, [rsi]
shl ecx, 3 ; cl = 8 : 0
shr eax, cl ; eax >>= 8 : eax >>= 0
; with BMI2: shrx eax, [rsi], ecx is more efficient
and eax, 0x00FFFFFF ; mask off to handle the case where we didn't shift.
真正的架构 24 位加载或存储
在体系结构上,x86 没有使用 整数 寄存器作为目标或源的 24 位加载或存储。正如 Brandon 指出的那样,MMX / SSE 掩码存储(如 MASKMOVDQU
, not to be confused with pmovmskb eax, xmm0
)可以 store 来自 MMX 或 XMM reg 的 24 位,给定一个仅设置了低 3 个字节的矢量掩码.但是它们几乎从来没有用过,因为它们很慢并且总是有 NT 提示(所以它们围绕缓存写入,并像 movntdq
那样强制驱逐)。 (AVX dword/qword 掩码 load/store 指令并不意味着 NT,但不适用于字节粒度。)
AVX512BW (Skylake-server) adds vmovdqu8
为加载和存储提供字节屏蔽,并对被屏蔽的字节进行故障抑制。 (即,如果 16 字节加载包含未映射页面中的字节,只要未为该字节设置掩码位,您就不会出现段错误。但这确实会导致速度大幅下降)。所以 微架构 它仍然是一个 16 字节的负载,但是对架构状态的影响(即除性能之外的所有内容)恰好是真正的 3 字节 load/store (具有正确的面具)。
您可以在 XMM、YMM 或 ZMM 寄存器上使用它。
;; probably slower than the integer way, especially if you don't actually want the result in a vector
mov eax, 7 ; low 3 bits set
kmovw k1, eax ; hoist the mask setup out of a loop
; load: leave out the {z} to merge into the old xmm0 (or ymm0 / zmm0)
vmovdqu8 xmm0{k1}{z}, [rsi] ; {z}ero-masked 16-byte load into xmm0 (with fault-suppression)
vmovd eax, xmm0
; store
vmovd xmm0, eax
vmovdqu8 [rsi]{k1}, xmm0 ; merge-masked 16-byte store (with fault-suppression)
这与 NASM 2.13.01 一起组装。 IDK 如果您的 NASM 足够新以支持 AVX512。您可以使用 Intel 的 Software Development Emulator (SDE)
在没有硬件的情况下玩 AVX512这看起来很酷,因为只需 2 微指令就可以将结果输入 eax
(设置掩码后)。 (但是,Skylake-X 的 http://instlatx64.atw.hu/'s spreadsheet of data from IACA 不包括带掩码的 vmovdqu8
,仅包括未掩码的形式。这些确实表明它仍然是单个 uop 负载,或者像常规一样的微融合存储vmovdqu/a
)
但是当心如果 16 字节加载出现故障或越过高速缓存行边界,速度会变慢。我认为它在内部 确实 执行加载然后丢弃字节,如果需要抑制故障,可能会有昂贵的特殊情况。
此外,对于商店版本,请注意屏蔽商店不会有效地转发到负载。 (有关更多信息,请参阅英特尔的优化手册)。
脚注:
- 宽存储是一个问题,因为即使您替换旧值,您也会执行非原子读-修改-写,如果您放回的那个字节是一个锁,这可能会破坏事情,例如. 不要在对象外部存储,除非您知道接下来会发生什么并且它是安全的,例如你放在那里的填充允许这个。你可以可以
lock cmpxchg
一个修改过的4字节值到位,以确保你没有踩到另一个线程对额外字节的更新,但显然做 2 个单独的存储比原子cmpxchg
重试循环 多 性能更好。
BZHI r32a, r/m32, r32b Zero bits in r/m32 starting with the position in r32b, write result to r32a
BZHI r64a, r/m64, r64b Zero bits in r/m64 starting with the position in r64b, write result to r64a
所以load the low 24 bits from [mem]
你可以使用
MOV eax, 24
BZHI eax, [mem], eax
有了这个,你还可以从内存中加载可变数量的位