当前位置: 首页 > 知识库问答 >
问题:

如何将3个字节(24位)从内存移动到寄存器?

欧阳鸿德
2023-03-14

我可以使用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

我首先考虑我可以单独移动字节,即首先是一个字节,然后是一个单词,或它们的某种组合。但是,我不能引用eaxrax的“上半部分”,所以这在第一个障碍时就失败了,因为我最终会重写首先移动的任何数据。

我的解决方案:

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]的指令?是否有办法选择一系列内存地址,而不是提供偏移量和指定大小运算符。如何将3个字节从内存移动到ASMx86_64中的注册?

NASM版本2.11.08架构x86


共有3个答案

连曜灿
2023-03-14

使用BMI2,您可以使用BZHI

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

因此,要从mem加载低24位,可以使用

MOV  eax, 24
BZHI eax, [mem], eax

有了这个,您还可以从内存中加载可变数量的位

步建茗
2023-03-14

写入24位的唯一方法是使用MMX(MASKMOVQ)或SSE(MASMODQU)和掩码来防止您不想修改的字节被修改。但是,对于单次写入,MMX和SSE过于复杂(并且可能更慢)。

请注意,通常读取比写入便宜(尤其是在涉及多个CPU时)。考虑到这一点,另一种选择是:

    shl eax,8
    mov al,[DogsName+3]
    ror eax,8
    mov [DogsName],eax

这将使用其旧值覆盖后字节(如果后字节无法访问,或者后字节属于需要原子更新的任何内容,则可能会导致问题)。

栾英资
2023-03-14
匿名用户

如果您知道3字节int不在页面的末尾,通常您会进行4字节加载并屏蔽所需字节附带的高垃圾,或者如果您正在处理不关心高位的数据,则直接忽略它。如果只需要结果的低部分,在不将输入中的高位置零的情况下,可以使用哪个2的补码整数运算?

与stores不同,加载“不应该”的数据从来都不是正确性的问题,除非您进入一个未映射的页面。(例如,如果db“pip”出现在页面的末尾,并且下一个页面未映射。)但在这种情况下,您知道它是较长字符串的一部分,因此,如果较宽的负载扩展到下一个缓存线(因此负载跨越缓存线边界),则唯一可能的缺点是性能。在x86和x64上读取同一页中的缓冲区是否安全?

对于任何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

我假设您希望将结果零扩展到eax/rax,如32位操作数大小,而不是与eax/rax的现有高字节合并,如8或16位操作数大小的寄存器写入。如果确实要合并,请屏蔽旧值并或。或者,如果您从DogsName-1加载,那么您想要的字节位于EAX的前3个位置,并且您想要合并到ECX中:shr-ECX,24/shld-ECX,EAX,24将旧的顶部字节下移到底部,然后在移动3个新字节的同时将其向后移动。(遗憾的是,shld没有内存源形式。半相关:有效地从两个单独的DWORD加载到一个qword中。)shld在Intel CPU(尤其是Sandybridge和更高版本:1 uop)上速度很快,但在AMD上不快(http://agner.org/optimize/).

有很多方法可以做到这一点,但不幸的是,在所有CPU中没有一种最快的方法。部分寄存器写入在不同的CPU上表现不同。在除Core2/Nehalem之外的其他CPU上,您的方式(字节加载/移位/字加载到ax中)相当不错(在组装后读取eax时,它会暂停插入合并uop)。但是从movzx eax开始,字节[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以外的CPU上,2个负载的最佳选择是写入ax。(Core2之前的Intel P6无法运行x86-64代码,没有部分寄存器重命名的CPU在写入ax时将合并到rax中)。Sandybridge仍然重命名AX,但合并只需要1个uop,没有暂停,即与OR相同,但在Core2/Nehalem上,插入合并uop时前端暂停约3个周期。

Ivybridge和更高版本仅重命名为AH,而不是AX或AL,因此在这些CPU上,AX中的负载是微融合负载合并。Agner Fog没有列出Silvermont或Ryzen(或我查看的电子表格中的任何其他选项卡)上的mov r16,m的额外惩罚,因此可能其他没有部分reg重命名的CPU也会执行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字节;所有当前的CPU都使用它,我认为没有任何具有32字节行的x86-64 CPU存在。(我们仍然明确避免跨页)。

    ; 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边界)将权衡页面内某些缓存行拆分的更好分支预测,但仍然不会进行页面行拆分。(在这种情况下测试esi,(1

您甚至可以无分支地执行此操作,但显然不值得这样做,而只需执行两个单独的加载!

    ; 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.

架构上,x86没有24位加载或存储,以整数寄存器作为目标或源。正如Brandon指出的那样,MMX/SSE掩码存储(如MASKMOVDQU,不要与PMOVMkb eax、xmm0混淆)可以存储MMX或XMM reg中的24位,给定仅设置了低3字节的向量掩码。但是它们几乎没有什么用处,因为它们速度慢,并且总是有NT提示(因此它们在缓存周围写,并强制逐出,如movntdq)。(AVX dword/Q字掩码加载/存储指令并不意味着NT,但不适用于字节粒度。)

AVX512BW(Skylake服务器)添加了vmovdqu8,它为加载提供字节屏蔽,并对屏蔽的字节进行故障抑制存储。(即,如果16字节加载包括未映射页面中的字节,只要没有为该字节设置掩码位,就不会进行分段故障。但这确实会导致速度大幅放缓)。因此,微体系结构上,它仍然是一个16字节的加载,但对体系结构状态(即除性能外的所有内容)的影响与真正的3字节加载/存储(具有正确的掩码)完全相同。

您可以在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足够新,可以支持AVX512,则可以使用NASM 2.13.01.IDK进行组装。您可以使用Intel的软件开发模拟器(SDE)在没有硬件的情况下使用AVX512

这看起来很酷,因为只需2个UOP即可将结果输入eax(一旦设置了掩码)。(然而,http://instlatx64.atw.hu/来自IACA的Skylake-X数据电子表格不包括带掩码的vmovdqu8,只包括未屏蔽的表单。这些确实表明它仍然是一个单uop负载,或微熔合存储,就像一个常规的vmovdqu/a)

但是,如果16字节负载出现故障或跨越缓存线边界,请注意速度减慢。我认为它会在内部进行加载,然后丢弃字节,如果需要抑制故障,则这是一种潜在的昂贵的特殊情况。

此外,对于存储版本,请注意屏蔽存储不会有效地转发到加载。(有关更多信息,请参阅英特尔的优化手册)。

脚注:

  1. 存储是一个问题,因为即使您替换旧值,您也会执行非原子读取-修改-写入,例如,如果您放回的字节是锁,这可能会破坏东西。除非您知道接下来会发生什么并且它是安全的,否则不要存储在对象之外,例如您放置在那里以允许这样做的填充。您可以将cmpxchg修改后的4字节值锁定到位,以确保您没有踩到另一个线程对额外字节的更新,但显然进行2个单独的存储比原子cmpxchg重试循环的性能要好得多。

 类似资料:
  • 为什么我不能在英特尔x86-64汇编中直接将一个字节从内存移到64位寄存器? 例如,此代码: 印刷品: 为了使代码正常工作,我需要将寄存器r12和r13的移动字节更改为: 现在,它打印出预期内容: 我们为什么要这样做? 有更简单的方法吗? 谢谢

  • 本文向大家介绍verilog 移位寄存器,包括了verilog 移位寄存器的使用技巧和注意事项,需要的朋友参考一下 示例 具有异步复位功能的N位深移位寄存器。            

  • 我正在使用x86 Assembly,我遇到了使用指令的需要。 我的代码片段过去是这样的: 其中<代码> 所以我的程序正在命中未定义的行为,在这种情况下是一个无限循环,在检查了很长时间可能出错的地方之后,我认为我必须使用,因为 实际上将结果存储在< code>ax中。因此,我在< code>ax中有一个有符号的结果,而我希望在< code>al中有一个无符号的(有上溢/下溢)结果。 所以我的问题是:

  • 问题内容: 对于我的10,000点,我决定在这个很酷的网站上做出一些贡献:一种将位图缓存在本机内存中的机制。 背景 Android设备为每个应用程序分配的内存非常有限-堆的范围从16MB到128MB,具体取决于各种参数。 如果超过此限制,则会得到OOM,并且在使用位图时可能会发生多次。 很多时候,应用可能需要克服这些限制,对巨大的位图执行繁重的操作,或者只是将其存储以备后用,而您需要 我想出的是一

  • 我正在寻找一种方法来移动arch64寄存器x1中的任何32位常数。 是否有一种方法可以执行

  • 基本上指令有8->16、8->32、8->64、16->32和16->64。 32->64的转换在哪里?我必须使用签名版本吗? 如果是的话,您如何使用完整的64位来表示无符号整数?