为什么这段代码在地址随机化开启时会崩溃?

8

我正在学习amd64汇编语言,并尝试实现一个简单的Unix过滤器。出于未知原因,即使将其简化到最基本的版本(如下所示的代码),它也会随机崩溃。

我尝试在GNU调试器(gdb)中调试此程序。在gdb的默认配置下,程序运行正常,但是如果我启用地址随机化(set disable-randomization off),程序就开始崩溃(SIGSEGV)。问题指令在列表中标记如下:

format ELF64 executable

sys_read                        =       0
sys_write                       =       1
sys_exit                        =       60

entry $
foo:
    label .inbuf   at rbp - 65536
    label .outbuf  at .inbuf - 65536
    label .endvars at .outbuf
    mov rbp, rsp

    mov rax, sys_read
    mov rdi, 0
    lea rsi, [.inbuf]
    mov rdx, 65536
    syscall

    xor ebx, ebx
    cmp eax, ebx
    jl .read_error
    jz .exit

    mov r8, rax  ; r8  - count of valid bytes in input buffer
    xor r9, r9   ; r9  - index of byte in input buffer, that is being processed.
    xor r10, r10 ; r10 - index of next free position in output buffer.

.next_byte:
    cmp r9, r8
    jg .exit
    mov al, [.inbuf + r9]
    mov [.outbuf + r10], al ;; SIGSEGV here in GDB
    inc r10
    inc r9
    jmp .next_byte

.read_error:
    mov rax, sys_exit
    mov rdi, 1
    syscall
.exit:
    mov rax, sys_write
    mov rdi, 1
    lea rsi, [.outbuf]
    mov rdx, r10
    syscall

    mov rax, sys_exit
    xor rdi, rdi
    syscall



这个程序旨在从stdin最多读取64kB的数据,将其存储到堆栈上的缓冲区中,逐字节地将读取的数据复制到输出缓冲区中,并将输出缓冲区的内容写入标准输出流中。本质上,它应该像cat的限制版本一样运行。
在我的电脑上,它要么按预期工作,要么以大约1次成功运行对4次崩溃的频率崩溃。
2个回答

6

sub rsp, <size>用于在访问栈之前保留栈空间,如果您在RSP下使用了超过128字节。


当程序崩溃时,请查看进程内存映射。您可能使用的内存远低于RSP,因此内核不会增长堆栈映射,因此它只是对未映射页面的普通访问 = 无效页面错误 => 内核发送SIGSEGV。
(ABI仅定义了一个128字节的红区,但实际上唯一可以破坏该内存的是信号处理程序(您没有安装)或使用程序堆栈调用程序中的函数来运行GDB的print some_func()。)
通常情况下,Linux很愿意在不接触中间页面的情况下增长堆栈映射,但显然确实检查了RSP的值。通常情况下,您移动RSP而不仅仅是使用远低于堆栈指针的内存(因为不能保证其安全)。请参见How is Stack memory allocated when using 'push' or 'sub' x86 instructions?

另一个重复的问题: 当减去ESP或RSP寄存器时可能会生成哪些异常?(堆栈增长),在接触新的堆栈内存之前使用sub rsp, 5555555就足够了。

堆栈ASLR可能会使RSP相对于页面边界处于不同位置,因此有时可能刚好能逃过一劫。 Linux最初映射132kiB的堆栈空间,其中包括进入_start时栈中环境和参数的空间。 您的128kiB非常接近,因此有时随机工作是完全合理的。


顺便说一下,在用户空间实际上没有任何理由复制内存,特别是一次只复制1个字节。只需将相同的地址传递给write

如果可能的话,至少进行原地过滤,这样您的缓存占用就会更小。

此外,加载字节的正常方式是movzx eax,byte [mem]。仅在您想要与RAX的旧值合并时才使用mov al,[mem]。在某些CPU上,moval具有对旧值的错误依赖性,您可以通过写入完整寄存器来打破它。


顺便说一下,如果你的程序总是使用这个空间,那么最好在BSS中静态分配它。如果选择汇编一个位置相关(非PIE)可执行文件,则可以实现更有效的索引寻址。


我知道,在用户空间复制1个字节很奇怪,但我不得不简化我的代码。最初有一些处理。感谢您提供关于movzx的信息。 - KAction
@DmitryBogatov:你的循环仍然过于复杂:P两个独立的索引,而且你仍在使用索引寻址模式,而不是指针增量。而且你底部没有条件分支。请参见为什么循环总是编译成“do...while”样式(尾跳转)? - Peter Cordes

4

在amd64中,红色区域仅有128个字节长,但是你正在使用rsp下方的131072个字节。将堆栈指针向下移动以包含你想要存储在堆栈上的缓冲区。


1
这并没有完全解释为什么它在实践中会出现故障,特别是有时候。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接