如何比较程序集中的两个字符串(nasm)

时间:2015-09-29 16:16:39

标签: assembly nasm x86-64

修订后的代码: 有没有办法优化这个?

read_pass:
    ;read passowrd

    ; read(int fd, void *buf, size_t count);
    ; #define __NR_read 0
    ; rdi = unsigned int fd
    ; rsi = char *buf
    ; rdx = size_t count
    xor rax, rax
    mov rdi, rax
    mov rsi, userpass
    mov rdx, rax
    add rdx, 0x64 ; 100 
    syscall

    lea rdi, [passcode]
    lea rsi, [userpass]
    mov rcx, pclen

    repe cmpsb 
    je do_something

    jmp read_pass

section .data
    passcode db 'hi', 0xa  
    pclen equ $ - passcode 
    userpass times 100 db 0
    uplen equ $ - userpass

ORIGINAL Q评论回复是询问类似的代码,但使用不正确的操作数cmps

如何比较程序集中的两个字符串(nasm)?

当我编译时,我收到以下错误:
Pass.nasm:129: error: invalid combination of opcode and operands

第129行是:
cmpsq userpass, passcode

(我也试过cmp和cmps)

1 个答案:

答案 0 :(得分:1)

由于这是一个读/检查密码功能,因此优化重复呼叫的速度是没有意义的。优化代码大小(并且在第一次运行时没有任何主要停顿)是最佳方法,以最大限度地减少缓存污染(尤其是小而且非常有价值的uop-cache)。有关优秀信息,请参阅http://agner.org/optimize/(以及与https://stackoverflow.com/tags/x86/info相关联的其他一些资源)。

我确实在代码中发现了一些错误/安全漏洞,以及节省字节的方法。此外,将读缓冲区保留在堆栈上将节省100个字节的BSS空间。见下文。

看起来你想要从stdin(fd 0)硬编码read(2)到一个长度为100的缓冲区。如果你实际只读了99个字符,你的字符串仍然是零终止的,所以我和#39;建议这样做。

最好将全局变量/数组的地址加载到AMD64中的寄存器中,最好使用mov r32, imm32according to gcc/clang/icc。如果您不知道地址是否适合虚拟内存的低32位,或者您需要创建与位置无关的代码,那么RIP相对的lea是最佳选择。 Linux x86_64编程模型中的low32中的数据段地址,因此5字节mov r32, imm32有效。 mov r64, imm32 sign - 扩展32位值。我们不需要它,它需要一个REX前缀字节,因此将已知的32位地址加载到32位寄存器中实际上更好(但更难以阅读)。如果你这样做,显然会截断任意地址。如果不确定,请使用lea r64, [rel addr],当使用地址作为函数参数或w / e时,当然总是使用64位操作数大小。

如果你确实需要处理全局变量的64位地址,它可能只需加载一次,然后在整个系统调用中保存/恢复它(在另一个寄存器中它不会破坏,或者实际上是推/弹因为我认为系统调用会破坏所有调用者保存的寄存器。即如果我们使用rbx,我们必须在函数的开头/结尾按下/弹出调用者的rbx,因为它' sa callee -saved register。

    xor eax, eax                ;  writing a 32bit reg always zeros the upper32, and saves a REX prefix byte
    xor edi, edi                ; read(fd 0)
    mov esi, userpass           ; lea rsi, [rel userpass]
    lea edx, [rax + uplen - 1]  ;  shorter and harder for humans to read than mov edx, uplen - 1
    syscall

    ; continued below

section .rodata
    ; passcode can be part of the shared read-only mapping of the executable, not copy-on-write.
    passcode db 'hi', 0xa    ; it's not normal to include the newline in the password, but it does make the code simpler I guess
    pclen equ $ - passcode

section .data
    userpass times 100 db 0
    uplen equ $ - userpass

从归零寄存器移位归零也是一个2字节的指令,就像xor一样。它可能在AMD CPU上有一些优势,它可以在更多的执行端口上运行。在Intel上,Sandybridge处理xor相同,在寄存器重命名阶段处理相同,根本不使用执行单元,并且每个时钟的吞吐量为4。 IDK,如果AMD将采取这个技巧。直到IvyBridge运动注册,reg也在管道的寄存器重命名阶段处理,并且也不需要执行单元。可能两种方式都没有可衡量的差异,因为它是在一个短的依赖链的开始,所以我更喜欢xor-zeroing只是为了让它更容易阅读(即你不必请记住,在查看xor edi,edi时,eax已归零。)

要将缓冲区长度放入寄存器,从技术上讲,可能是较短的代码,使reg为零,然后是add reg,imm8,但是那2个Intel uops / AMD宏操作,而不仅仅是一个一个mov reg, imm32,只有一个字节长。 (感谢在编写32位寄存器时自动将upper32置零。)实际上,保存2个字节的正常方法是lea edx, [rax + uplen - 1],其中rax是一个刚刚归零的reg。带有符号8位移位的lea只需要3个字节进行编码。在长模式下,默认操作数大小为32位,默认地址大小为64位,这就是32位dest寄存器和使用64位寄存器的寻址模式最紧凑的原因。有时候查看objdump -d /bin/ls或者其他东西是检查某种指令编码需要多少字节的最快方法,如果你知道规则是什么使你想要的指令长度与你能找到的相同以类似的方式使用其他寄存器。

现在让我们看看您的实际密码检查代码。首先,仅存储密码哈希,而不是明文密码本身是正常的。任何考虑实际使用此代码进行任何非玩具使用的人都应该停止阅读并继续查看。您可以越多地重复使用经过良好测试的库,忽略安全漏洞的风险就越小。

; continuing from above:
; ... syscall

test eax, eax       ; read(2) result in eax
jle  EOF_or_error   ; In C, most of the code in systems programming is checking for errors.

; lea rdi, [passcode]
; lea rsi, [userpass]
; If you use lea, make sure you use RIP-rel, because 64bit absolute addressing is only available for mov rax, [addr64].

mov edi, passcode   ; 5 bytes, see above discussion of loading addresses.
lea rsi, [rdi + userpass - passcode]  ; This is only 4 bytes.  3 bytes if dest is esi, not rsi. (no REX needed).

mov ecx, pclen   ; we know pclen < uplen, so this can't buffer overflow, but see text for security problems from not looking at length of read
repe cmpsb 
jne read_pass
; fall through to do_something, or to a ret insn.  Saves a jmp

这看起来很合理。在密码中包含换行符可以让您无需查看所读密码的长度即可。您需要检查是否已阅读某些内容,否则您可能只是将密码与之前的正确输入进行比较,如果read未触及缓冲区中的任何字节。

实际上,tty在线缓冲&#34;煮熟&#34;输入模式,read(2)返回当你按ctrl-d(EOF)时键入的内容,即使它没有包含换行符。随后的read调用将会有更多内容。所以你需要担心这一点,以及中断的系统调用(例如通过信号)。这是库I / O函数为您处理的事情之一。

尝试使用cat:您可以输入一些字符,然后按ctrl-d让它们在没有换行符的情况下回显。所以这个密码例程有一个巨大的安全漏洞:如果以前正确的密码位于缓冲区中,我所要做的就是猜测第一个字符。我可以反复猜测,只需在每个角色后按ctrl-d。

如果您将缓冲区归零(mov eax, ecx / xor eax,eax / rep stosb,则可以避免此问题,其中eax是您已检查的读取返回值是> = 0)。一旦检查了旧密码条目,就会从内存中擦除它。当然,正确的密码只是以纯文本形式存在。如果您不关心内存中的密码,您可以根据正确密码的长度检查读取的字符数。

; not shown: check for EOF/error

mov ecx, pclen
cmp ecx, eax    ; check lengths to avoid EOF first-char guessing
jne read_pass

; not shown: set up addresses

repe cmpsb      ; check contents
jne read_pass

; They match, do whatever here.

我没有看到一种巧妙的方法,只使用一个测试或cmp指令来检查零/负返回值并检查

另一点:密码输入缓冲区可能在堆栈上。如果此代码不必在Windows上运行,您甚至不需要使用RSP,您可以使用当前堆栈指针下方的红色区域,信号处理程序赢得& #39; t clobber。然后,您不会永久浪费100个字节,只能在密码输入期间使用缓冲区。既然我已经证明你应该真正检查read的返回值,那么旧的内容并不重要,无论你是在堆栈还是malloc中。

对于重复strcmp次呼叫的速度rep cmpsb的启动开销可能会使其比短字符串的正常循环更糟糕。对于memset / memcpy,我认为在具有快速字符串操作的英特尔CPU(IvB及更高版本)上,rep stos / rep movs比优化的SSE循环快约128B左右的阈值。