减去 ESP 或 RSP 寄存器时会产生哪些异常?(堆栈增长)



我试图了解堆栈的内存页是如何分配/分配的。

我编写了以下概念验证 C 代码,这显然会导致分段错误(在 x86_64 Linux 上):

#include <string.h>
int main()
{
    char a;
    memset( (&a - 4444444), 0, 3333333 );
    return 0;
}

以下汇编代码片段(AT&T语法)由gcc从上面的C程序生成:

subq    $16, %rsp
leaq    -1(%rbp), %rax
subq    $4444444, %rax
movl    $3333333, %edx
movl    $0, %esi
movq    %rax, %rdi
call    memset

如果我在调用memset之前手动添加subq $5555555, %rsp

subq    $16, %rsp
leaq    -1(%rbp), %rax
subq    $4444444, %rax
movl    $3333333, %edx
movl    $0, %esi
movq    %rax, %rdi
subq    $5555555, %rsp /* added manually */
call    memset

然后分段错误消失,因为在减去寄存器后分配了堆栈rsp虚拟内存页导致一些硬件异常并调用了分配的异常处理程序(当然,在内核空间中)。

我知道在此处调用memset会导致"小页面错误"异常。但这是一个不同的故事(即分配物理内存页)。

我的问题是:调用subq $5555555, %rsp时生成了哪个异常?我建议这将是"堆栈故障"异常,但我没有找到确切的证据。

我想通了。首先,减去寄存器rsp没有任何作用。其次,当我们尝试写入非映射堆栈区域时,"次要页面错误"会在内核空间中调用异常处理程序。然后,此页面错误处理程序检查它是合法写入还是非合法写入。我认为页面错误处理程序与线程的当前堆栈指针进行比较(在我们的例子中,它是寄存器rsp保存的值)。如果进程尝试写入的地址高于当前堆栈指针,则页面错误处理程序将扩展进程的虚拟地址空间并将此虚拟页面映射到物理页面,否则处理程序会将 SIGSEGV 发送到进程。

我使用 GDB 和/proc/[pid]/maps 检查了以下片段:

subq    $1500016, %rsp
movq    %fs:40, %rax
movq    %rax, -8(%rbp)
xorl    %eax, %eax
movb    $44, -1500016(%rbp)
movb    $55, -1100016(%rbp)
movb    $66, -600016(%rbp)

调用subq $1500016, %rsp时,堆栈地址范围不会更改。但是当第一次写入发生在movb $44, -1500016(%rbp)时,堆栈地址范围会像我上面解释的那样扩展。

这一行没有例外。

但是,memset 的序言代码在尝试通过将寄存器保存到堆栈来保留寄存器时会导致访问冲突,因为堆栈指针无效。

在大多数环境中,只有一个保护页触发要提交的其他堆栈页。 在这种情况下,访问冲突将不会通过增加堆栈来处理,程序只会崩溃。

如果您的操作系统确实处理了在寄存器保存期间引起的访问冲突,它将提交堆栈的所有干预页面并重试该操作(PUSH指令)。 然后那些中间的页面将由memset内的循环成功写入。

当然,如果减法导致RSP指向为堆栈增长保留的地址空间之外,则所有赌注都失败了。 您甚至可能导致其他线程的堆栈增长。

最新更新