【问题标题】:How do I compare two strings in assembly (nasm)如何比较汇编中的两个字符串(nasm)
【发布时间】:2015-09-29 16:16:39
【问题描述】:

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

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

cmets 回复的原始 Q 询问了类似的代码,但对 cmps 使用了不正确的操作数:

如何比较汇编中的两个字符串 (nasm)?

编译时出现以下错误:
Pass.nasm:129: error: invalid combination of opcode and operands

第 129 行是:
cmpsq userpass, passcode

(我也试过 cmp 和 cmps)

【问题讨论】:

  • 用力……呃……指令集参考!
  • 这个:cmpsq [userpass], [passcode] ?
  • 不,cmps 期望地址已经加载到 rsirdi 并且您可能想要 rcx 中的长度并使用 repe 前缀,否则它只会比较单个字母.
  • 现在可以编译了,但是是一个无限循环: read_pass: ;read passowrd ;读取(int fd,void *buf,size_t 计数); ; #define __NR_read 0 ; rdi = 无符号整数 fd ; rsi = 字符 *buf ; rdx = size_t count xor rax, rax mov rdi, rax mov rsi, userpass mov rdx, rax add rdx, 0x64 ; 100 系统调用 lea rdi, [passcode] lea rsi, [userpass] mov rdx, pclen repe cmpsb je do_something jmp read_pass
  • 你使用的是rdx而不是rcx

标签: assembly nasm x86-64


【解决方案1】:

由于这是一个读取/检查密码功能,因此优化重复调用的速度是没有意义的。优化代码大小(并且在第一次运行时没有任何重大停顿)是减少缓存污染(尤其是小型且非常有价值的 uop-cache)的方法。请参阅http://agner.org/optimize/(以及从https://stackoverflow.com/tags/x86/info 链接的一些其他资源)以获取出色的信息。

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

您似乎想将 stdin (fd 0) 中的 read(2) 硬编码到长度为 100 的缓冲区中。如果您实际上只读取 99 个字符,您的字符串仍将以零结尾,所以我会建议这样做。

在 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,因为它是一个 callee-saved注册。

    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 same,same,根本不使用执行单元,并为其提供每时钟 4 的吞吐量。 IDK 如果 AMD 会拿起这个把戏。直到 IvyBridge mov reg,reg 也在流水线的寄存器重命名阶段处理,也不需要执行单元。无论哪种方式都可能无法测量差异,因为它位于短依赖链的开头,所以我更喜欢 xor-zeroing 只是为了使其更易于阅读(即您不必记住 eax 在查看时被归零xor edi,edi.)

要将缓冲区长度放入寄存器,从技术上讲,它可能是更短的代码,将 reg 归零,然后 add reg,imm8,但这是 2 个 Intel uops / AMD 宏操作,而不是 mov reg, imm32 的一个 mov reg, imm32只有一个字节长。 (感谢在编写 32 位 reg 时将 upper32 自动归零。)实际上,保存 2 个字节的好方法是 lea edx, [rax + uplen - 1],其中 rax 是您刚刚归零的 reg。 lea 带符号 8 位位移只需要 3 个字节进行编码。在长模式下,默认操作数大小为 32 位,默认地址大小为 64 位,这就是为什么 32 位目标寄存器和使用 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

现在看起来很合理。在密码中包含换行符可以让您不必检查您阅读的密码长度。您确实需要检查您是否阅读了 something,否则您可能只是将密码与之前的正确输入进行比较,如果 read 没有触及缓冲区中的任何字节。

实际上,当 tty 处于行缓冲“熟”输入模式时,read(2) 会在您按下 ctrl-d (EOF) 时返回迄今为止输入的内容,即使它不包含换行符也是如此。随后的阅读调用将阅读更多内容。因此,您需要担心这一点,以及中断的系统调用(例如通过信号)。这是库 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,您只需使用当前堆栈指针下方的红色区域,信号处理程序不会破坏。这样您就不会为仅在密码输入期间使用的缓冲区永久浪费 100 个字节。既然我已经表明你真的应该检查 read 的返回值,那么旧的内容并不重要,无论你是在堆栈上还是从 malloc 中。


为了提高速度重复调用strcmprep cmpsb 的启动开销可能比短字符串的正常循环更糟糕。对于 memset/memcpy,我认为 rep stos / rep movs 比优化的 SSE 循环更快的阈值约为 128B 左右,在具有快速字符串操作(IvB 及更高版本)的 Intel CPU 上。

【讨论】:

  • 更新:repe cmpsb / repe scasb 不快。他们避免了分支错误预测,但除此之外,SSE pcmpeqb 循环应该会击败他们。只有 rep movs / rep stos (memcpy/memset) 具有优化的微码实现。即便如此,我认为 AMD 的微码也不是那么好,因此 SSE 循环是 AMD uarches 上 memset 的更好选择。有关更多信息,请参阅英特尔的优化手册。
猜你喜欢
  • 2014-05-24
  • 1970-01-01
  • 1970-01-01
  • 2023-03-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-11-10
  • 1970-01-01
相关资源
最近更新 更多