It doesn't generate any exception directly. It generates code which, when the stack is enlarged by more than one page, generates a read-write access to each page in the newly allocated region. That's all it does. Example:
extern void bar(char *);
void foo(void)
{
char buf[4096 * 8];
bar(buf);
}
compiles (with gcc 4.9, on x86-64, at -O2
) to:
foo:
pushq %rbp
movq $-32768, %r11
movq %rsp, %rbp
subq $4128, %rsp
addq %rsp, %r11
.LPSRL0:
cmpq %r11, %rsp
je .LPSRE0
subq $4096, %rsp
orq $0, (%rsp)
jmp .LPSRL0
.LPSRE0:
addq $4128, %rsp
leaq -32768(%rbp), %rdi
call bar
leave
ret
orq $0, (%rsp)
has no effect on the contents of the memory at (%rsp)
, but the CPU treats it as a read-write access to that address anyway. (I don't know why GCC offsets %rsp
by 4128 bytes during the loop, or why it thinks a frame pointer is necessary.)
The theory is that the OS can notice these accesses and do something appropriate if the stack has become too large. For a POSIX-compliant operating system, that would be delivery of a SIGSEGV
signal.
You may be wondering how the OS can notice such a thing. The hardware allows the OS to designate pages of address space as completely inaccessible; any attempt to read or write memory in those pages triggers a hardware fault which the OS can process as it sees fit (again, for a POSIX-compliant OS, delivery of SIGSEGV
). This can be used to place a "guard area" immediately past the end of the space reserved for the stack. That's why one access per page is sufficient.
What -fstack-check
is meant to protect you from, to be clear, is the situation where the "guard area" is very small - perhaps just one page - so allocating a large buffer on the stack moves the stack pointer past that area and into another region of accessible RAM. If the program then happens never to touch memory within the guard area, you won't get a prompt crash, but you will scribble on whatever that other region is, causing a delayed-action malfunction.