【问题标题】:why does this asm strcmp() return wrong values为什么这个 asm strcmp() 返回错误的值
【发布时间】:2021-01-25 14:17:07
【问题描述】:

我正在尝试学习一些程序集(intel 语法,x86_64)。 我写了这段代码来做一个简单的 strcmp() 实现:

section .text
    global ft_strcmp

ft_strcmp:
    cmp byte [rsi], 0
    je  exit
    mov cl, byte [rsi]
    cmp byte [rdi], cl
    jne exit
    inc rsi
    inc rdi
    jmp ft_strcmp

exit:
    xor rax, rax
    mov al, [rdi]
    sub al, byte [rsi]
    ret

但是通过调用 ft_strcmp("Hello", "Hellooooo") 来尝试返回 145,而真正的 strcmp() 返回 -1,我似乎不知道为什么。 我的语法错了吗,还是我尝试这样做的方式?

【问题讨论】:

  • 除了第一次检查字符串结尾(达到 NUL 字节)外,您正在操作 qwords(8 个字节)。在比较和返回差异时,您还需要对字节进行操作。它可能不会返回 0,但您可能只打印结果的低 32 位。
  • 感谢@user786653,这让我走上了更好的道路。我相应地编辑了我的问题,但结果还不正确。我猜这是因为一个字节可以存储从 0 到 255 的值,而负值会导致它循环。如何在仍然处理 1 个字节的同时保留返回值的符号?
  • 您需要返回带符号的 32 位结果,因此将两个字节零扩展为 32 位正整数,然后执行 32 位减法。 exit: movzx eax, byte [rdi] ; movzx ecx, byte [rsi] ; sub eax, ecx.
  • 感谢@NateEldredge,这就是我正在寻找的解决方案! movzx 不是我使用的文档中列出的操作数,但是查找它,这一切都有意义。抱歉,我是 stackoverflow 的新手,我不知道如何将您的评论标记为正确答案...

标签: assembly x86-64 strcmp


【解决方案1】:

strcmp 应该在 eax 中返回一个 32 位的 int,根据第一个字符串是更大还是更少,它是正数还是负数。通过进行 8 位减法,eax 的高 24 位保持为零,因此当作为有符号整数时,结果为正。

您想要进行 32 位减法,因此您需要 32 位寄存器中的两个字节,并将它们的高 24 位归零。这对movzx 很有效:

exit:
    movzx eax, byte [rdi]
    movzx ecx, byte [rsi]
    sub eax, ecx
    ret

如果您不知道movzx,您可以将整个寄存器归零,然后加载低字节:

exit:
    xor eax, eax
    mov al, [rdi] ; 'byte' is unnecessary, operand size inferred from register al
    xor ecx, ecx
    mov cl, [rsi]
    sub eax, ecx
    ret

(作为旁注,xor rax, rax 之类的归零指令可以替换为更小但效果相同的xor eax, eaxWhy do x86-64 instructions on 32-bit registers zero the upper part of the full 64-bit register?

【讨论】:

  • 这就是问题所在。感谢您的链接和解释,现在很有意义!
【解决方案2】:

简单的实现——使用Compiler Explorer

strcmp:
        xor     ecx, ecx
.L3:
        movzx   eax, BYTE PTR [rdi+rcx]
        movzx   edx, BYTE PTR [rsi+rcx]
        cmp     al, dl
        jne     .L2
        inc     rcx
        test    al, al
        jne     .L3
.L2:
        sub     eax, edx
        ret

字节正确地进行了零扩展,因此返回值是 unsigned 字符与提升为 int 的结果的差值。

正如其他人所指出的,对于 C 字符串可能具有任意长度的 C 库实现,可以使用机器字或 SSE 指令,前提是设置成本按某个长度阈值摊销。

【讨论】:

  • 谢谢。由于我刚刚开始,我会坚持这个更简单的版本,但 SSE 似乎真的很有趣,我需要阅读更多关于它的内容
【解决方案3】:

你是对的,第二个字符串 nul 检查退出了比较操作,slick!

考虑以下几点:

内存引用越少越好: 您的代码在循环中有 3 个数据内存引用,在退出时有 2 个引用 (3N+2)。 此代码在循环中有 2 个数据内存引用,退出时有 0 个引用 (2N)。

指令字节越少越好: 您的代码有 37 个字节的循环代码和 14 个字节的退出代码。 这段代码有 20 个字节的循环代码和 3 个字节的退出代码。

一个技巧是使用返回值寄存器作为工作寄存器之一。另一个技巧是将操作数移动到寄存器中,然后在寄存器中进行操作。

编译完成于 https://defuse.ca/online-x86-assembler.htm#disassembly

代码未经测试。

ft_strcmp:
    movzx cl,[rsi]      #[2] get byte(1) from 1st string
    movzx al,[rdi]      #[2] get byte(2) from 2nd string
    test cl,cl          #[2] end of first string, is it nul?
    je exit             #[2] 
    cmp cl,al           #[2] compare byte(1) w/ byte(2)
    jne exit            #[2] differ?
    inc rsi             #[3] point to next byte
    inc rdi             #[3] ditto
    jmp ft_strcmp       #[2] test them
                        #[20] bytes of instructions in loop
exit:
    sub al, cl          #[2] generate return value
                        # if neither null; return difference of values
                        # if byte(1) is null and byte(2) is null; ret 0
                        # if byte(1) is null and byte(2) is not null; return positive
                        # if byte(1) is not null and byte(2) is null; return negative 
    ret                 #[1]
                        #[3] bytes of instructions in exit
    
    
ft_strcmp:
    cmp byte [rsi], 0   #[7]
    je  exit            #[2]
    mov cl, byte [rsi]  #[6]
    cmp byte [rdi], cl  #[6]
    jne exit            #[2]
    inc rsi             #[6]
    inc rdi             #[6]
    jmp ft_strcmp       #[2]
                        #[37] bytes of loop code
exit:
    xor rax, rax        #[2]
    mov al, [rdi]       #[5]
    sub al, byte [rsi]  #[6]
    ret                 #[1]
                        #[14] bytes of exit code

【讨论】:

  • movzx cl,[rsi] 没有组装; movzx 的目标必须至少为 16 位宽。在这种情况下,您需要 32 位双字 movzx ecx, byte [rsi]。 (您必须指定源是字节还是字,甚至是双字)。此外,您需要sub eax, ecx 才能利用您所做的零扩展负载。
  • 但是是的,您只想加载一次,然后将这些值重用于返回值。但是,如果您要谈论优化,请不要忘记Why are loops always compiled into "do...while" style (tail jump)? - 应该“旋转”循环以将其中一个 JCC 放在底部。
  • 当然,可以比 1 个字节更好,使用 SSE2 pcmpeqb / pmovmskb 来测试任何不匹配或任何零字节,例如glibc 的手写实现可以。但是适当地优化简单的字节循环仍然是一个有用的学习练习。
【解决方案4】:

您还需要检查 [rdi] 是否为空。第二个字符串可能比第一个短。

【讨论】:

  • 谢谢,但即使您为 [rdi] 添加相同的检查,我也会得到相同(错误)的结果。
  • 没必要。如果[rsi] 为非NUL,则rdi 字符串尚未完成,或者[rdi][rsi] 不相等。
  • 那是我最初的火车。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-01-08
  • 2014-05-09
  • 2018-09-03
  • 1970-01-01
  • 2021-07-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多