如何将字符串的第一个字符与 x86-64 程序集中的另一个字符进行比较?
How can I compare the first character of a string with another character in x86-64 assembly?
我有一个已初始化的字符串 "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
的ASCII码。这让我尝试将 72
传递给 rsi
而不是 H
,但这也导致程序终止而没有跳转到 equal
标签。
如何正确比较 "Hello, World!" 中的第一个字符与传递到寄存器的字符?
我将并排解释这些变化(希望这样更容易理解):
global start
section .data
msg: db "Hello, World!", 10, 0
section .text
start:
mov rdx, msg
mov al, [rdx] ; moves one byte from msg, H to al, the 8-bit lower part of ax
mov ah, 'H' ; move constant 'H' to the 8-bit upper part of ax
cmp al, ah ; compares H with H
je equal ; yes, they are equal, so go to address at equal
mov rax, 0x2000001
mov rdi, [rdx]
syscall
equal: ; here we are
mov rax, 0x2000001
mov rdi, 58
syscall
如果您不理解 al
、ah
、ax
的用法/提及,请参阅 General-Purpose Registers。
您和@Rafael 的回答使您的代码过于复杂。
您通常不想将 mov rdi, msg
与绝对地址的 64 位立即数一起使用。 (参见 Mach-O 64-bit format does not support 32-bit absolute addresses. 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 的负载。请参阅 and - mov ah, 'H'
对 Haswell/Skylake 上 AH 的旧值有错误的依赖性,即使 AH 与 RAX 分开重命名。但是 AL 不是,所以是的,这很可能对负载有错误的依赖性,并行地从 运行 停止它并延迟 cmp
一个周期。
总之,这里的TL:DR就是,没必要的时候不要乱写AH/BH/CH/DH。阅读它们通常没问题,但可能会有更糟糕的延迟。请注意 cmp dil, ah
不可编码,因为 DIL 只能通过 REX 前缀访问,而 AH 只能在没有 REX 前缀的情况下访问。
我选择了 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 位整数的正常/推荐方式。合并到旧寄存器值的低字节通常更糟糕性能,这就是 Why do x86-64 instructions on 32-bit registers zero the upper part of the full 64-bit register?.)
的原因
我们可以 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
-立即使这 4 条指令而不是 mov eax
/ syscall
之前的 5,但除此之外它们是相等的。)
我有一个已初始化的字符串 "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
的ASCII码。这让我尝试将 72
传递给 rsi
而不是 H
,但这也导致程序终止而没有跳转到 equal
标签。
如何正确比较 "Hello, World!" 中的第一个字符与传递到寄存器的字符?
我将并排解释这些变化(希望这样更容易理解):
global start
section .data
msg: db "Hello, World!", 10, 0
section .text
start:
mov rdx, msg
mov al, [rdx] ; moves one byte from msg, H to al, the 8-bit lower part of ax
mov ah, 'H' ; move constant 'H' to the 8-bit upper part of ax
cmp al, ah ; compares H with H
je equal ; yes, they are equal, so go to address at equal
mov rax, 0x2000001
mov rdi, [rdx]
syscall
equal: ; here we are
mov rax, 0x2000001
mov rdi, 58
syscall
如果您不理解 al
、ah
、ax
的用法/提及,请参阅 General-Purpose Registers。
您和@Rafael 的回答使您的代码过于复杂。
您通常不想将 mov rdi, msg
与绝对地址的 64 位立即数一起使用。 (参见 Mach-O 64-bit format does not support 32-bit absolute addresses. 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 的负载。请参阅 mov ah, 'H'
对 Haswell/Skylake 上 AH 的旧值有错误的依赖性,即使 AH 与 RAX 分开重命名。但是 AL 不是,所以是的,这很可能对负载有错误的依赖性,并行地从 运行 停止它并延迟 cmp
一个周期。
总之,这里的TL:DR就是,没必要的时候不要乱写AH/BH/CH/DH。阅读它们通常没问题,但可能会有更糟糕的延迟。请注意 cmp dil, ah
不可编码,因为 DIL 只能通过 REX 前缀访问,而 AH 只能在没有 REX 前缀的情况下访问。
我选择了 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 位整数的正常/推荐方式。合并到旧寄存器值的低字节通常更糟糕性能,这就是 Why do x86-64 instructions on 32-bit registers zero the upper part of the full 64-bit register?.)
我们可以 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
-立即使这 4 条指令而不是 mov eax
/ syscall
之前的 5,但除此之外它们是相等的。)