首页 > 解决方案 > 如何将字符串的第一个字符与 x86-64 程序集中的另一个字符进行比较?

问题描述

我有一个初始化的字符串“Hello, World!” 我想从中提取第一个字符(即'H')并将其作为在运行时传递到寄存器中的字符。

我试过比较“Hello, World!”的第一个字符。通过以下代码使用“H”:

global start

section .data
msg: db "Hello, World!", 10, 0

section .text
start:
   mov rdx, msg
   mov rdi, [rdx]
   mov rsi, 'H'
   cmp rdi, rsi
   je equal

   mov rax, 0x2000001
   mov rdi, [rdx]
   syscall

equal:
   mov rax, 0x2000001
   mov rdi, 58
   syscall

但是,此代码终止而不跳转到equal标签。此外,我的程序的退出状态是72,这是H. 这让我尝试传递72rsi不是H,但这也导致程序终止而不跳转到equal标签。

如何正确比较“Hello, World!”中的第一个字符!与传递给寄存器的字符?

标签: assemblynasmx86-64

解决方案


您和@Rafael 的回答使您的代码过于复杂。

您通常永远不想使用mov rdi, msg绝对地址的 64 位立即数。(见Mach-O 64-bit format does not support 32-bit absolute address. NASM Accessing Array

使用default rel和使用cmp byte [msg], 'H'。或者,如果您想要 RDI 中的指针,以便可以在循环中递增它,请使用lea rdi, [rel msg].

您的分支之间唯一不同的是 RDI 值。您不需要复制 RAX 设置或syscall,只需在 RDI 中获取正确的值,然后让分支相互重新连接。(或者无分支地做。)

@Rafael 的答案由于某种原因仍在从字符串中加载 8 个字节,就像您的问题中的两个加载一样。大概是这样sys_exit,它忽略了高字节,只从低字节设置进程退出状态,但为了好玩,我们假设我们实际上想要为系统调用加载所有 8 个字节,而只比较低字节。

default rel         ; use RIP-relative addressing modes by default for [label]
global start

section .rodata                       ;; read-only data usually belongs in .rodata
msg: db "Hello, World!", 10, 0

section .text
start:
   mov   rdi, [msg]    ; 8 byte load from a RIP-relative address
   mov   ecx, 'H'

   cmp   dil, cl       ; compare the low byte of RDI (dil) with the low byte of RCX (cl)
   jne   .notequal
   ;; fall through on equal
   mov   edi, 58
.notequal:             ; .labels are local labels in NASM

   ; mov rdi, [rdx]    ; still loaded from before; we didn't destroy it.
   mov eax, 0x2000001
   syscall

尽可能避免写入 AH/BH/CH/DH。它要么对 RAX/RBX/RCX/RDX 的旧值具有错误的依赖性,要么如果您稍后读取完整的寄存器,它可能会导致部分寄存器合并停止。@Rafael 的答案没有这样做,但这mov ah, 'H'取决于某些 CPU 上加载到 AL 中的情况。请参阅为什么 GCC 不使用部分寄存器?以及Haswell/Skylake 上的部分寄存器的性能如何?编写 AL 似乎对 RAX 有错误的依赖,而 AH 不一致-mov ah, 'H'对 Haswell/Skylake 上 AH 的旧值有错误的依赖,即使 AH 与 RAX 分开重命名。但是 AL 不是,所以是的,这很可能对负载有错误的依赖性,阻止它并行运行并延迟cmp一个周期。

无论如何,这里的 TL:DR 是如果你不需要的话,你不应该乱写 AH/BH/CH/DH。阅读它们通常是可以的,但可能会有更糟糕的延迟。请注意,这cmp dil, ah是不可编码的,因为 DIL 只能通过 REX 前缀访问,而 AH 只能在没有前缀的情况下访问。

我选择了 RCX 而不是 RSI,因为 CL 不需要 REX 前缀,但是由于我们需要查看 RDI (dil) 的低字节,所以无论如何我们都需要在 cmp 上使用 REX 前缀。我可以mov cl, 'H'用来节省代码大小,因为对 RCX 的旧值的错误依赖可能没有问题。


顺便说一句,cmp dil, 'H'会和cmp dil, cl.

或者,如果我们将具有零扩展名的字节加载到完整的 RDI 中,我们可以使用它cmp edi, 'H'来代替它的低 8 版本。(零扩展负载是现代 x86-64 上处理字节和 16 位整数的正常/推荐方法。 合并到旧寄存器值的低字节通常对性能更差,这就是为什么 x86- 32 位寄存器上的 64 条指令将整个 64 位寄存器的上半部分归零?。)

而不是分支,我们可以 CMOV。对于代码大小和性能,这有时会更好,有时不会。

版本 2,仅实际加载 1 个字节:

start:
   movzx   edi, byte [msg]    ; 1 byte load, zero extended to 4 (and implicitly to 8)

   mov     eax, 58            ; ASCII ':'
   cmp     edi, 'H'
   cmove   edi, eax           ; edi =  (edi == 'H') ? 58 : edi

   ; rdi = 58 or the first byte,
   ; unlike in the other version where it had 8 bytes of string data here
   mov eax, 0x2000001
   syscall

(这个版本看起来要短得多,但大多数额外的行是空格、注释和标签。优化到cmp-immediate 使这 4 条指令而不是mov eax/之前的 5 条指令syscall,但除此之外它们是相等的。)


推荐阅读