我试图了解堆栈的内存页面是如何分配/分配的。
我写了下面的概念验证C代码,这显然会导致分段错误(在x86_64 Linux上):
#include <string.h>
int main()
{
char a;
memset( (&a - 4444444), 0, 3333333 );
return 0;
}
汇编代码的以下片段(AT&amp; T语法)由gcc从上面的C程序生成:
subq $16, %rsp
leaq -1(%rbp), %rax
subq $4444444, %rax
movl $3333333, %edx
movl $0, %esi
movq %rax, %rdi
call memset
如果我在致电subq $5555555, %rsp
之前手动添加memset
:
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
时生成了哪个异常?我建议这将是“堆栈错误”异常,但我没有找到确切的证据。
答案 0 :(得分:2)
我明白了。首先,减去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)
时,堆栈地址范围会扩展,如上所述。
答案 1 :(得分:0)
该行没有例外。
但是,memset
的序言代码在尝试通过将寄存器保存到堆栈来保留寄存器时会导致访问冲突,因为堆栈指针无效。
在大多数环境中,只有一个防护页面可以触发其他堆栈页面。在这种情况下,不会通过增加堆栈来处理访问冲突,程序将简单地崩溃。
如果您的操作系统确实处理了寄存器保存期间引起的访问冲突,它将提交堆栈的所有中间页面并重试该操作(PUSH
指令)。然后,这些介入页面将由memset
内的循环成功编写。
当然,如果减法导致RSP
指向为堆栈增长保留的地址空间之外,则所有投注都将关闭。你甚至可以使其他一些线程的堆栈增长。