崩溃的程序是:
#include <stdio.h>
#include <pthread.h>
void *function(void *arg)
{
int picture[4096][4096];
}
int main()
{
pthread_t pids[10];
pthread_create(&pids[0],NULL, function, NULL);
pthread_join(pids[0],NULL);
return 0;
}
程序在执行时崩溃:
$ gcc p.c -lpthread
$ ./a.out
Segmentation fault (core dumped)
线程堆栈布局
GLIBC/pthread中线程的默认堆栈大小为8 MB。在线程创建时,线程描述符(也称为任务控制块(TCB))存储在堆栈底部,并且在堆栈顶部设置了一个红色区域(4 KB的守卫页没有读/写权限)。堆栈从高地址向低地址增长。
在strace
的控制下运行程序的结果:
$ strace -f ./a.out
[...]
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
mmap(NULL, 8392704, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0x7fee8d4dc000
mprotect(0x7fee8d4dd000, 8388608, PROT_READ|PROT_WRITE) = 0
brk(NULL) = 0x556cf1b72000
brk(0x556cf1b93000) = 0x556cf1b93000
clone(child_stack=0x7fee8dcdbfb0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTIDstrace: Process 3338 attached
, parent_tid=[3338], tls=0x7fee8dcdc700, child_tidptr=0x7fee8dcdc9d0) = 3338
[pid 3338] set_robust_list(0x7fee8dcdc9e0, 24 <unfinished ...>
[pid 3337] futex(0x7fee8dcdc9d0, FUTEX_WAIT, 3338, NULL <unfinished ...>
[pid 3338] <... set_robust_list resumed>) = 0
[pid 3338] --- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_ACCERR, si_addr=0x7fee8d4dcef0} ---
[pid 3337] <... futex resumed>) = ?
[pid 3338] +++ killed by SIGSEGV (core dumped) +++
+++ killed by SIGSEGV (core dumped) +++
Segmentation fault (core dumped)
在前面的代码中:
- pthread库通过调用getrlimit()来获取默认的栈大小,返回值为8MB:
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
- pthread库使用调用mmap()分配了8MB+4KB的栈空间,并且没有读写权限(即PROT_NONE):
mmap(NULL, 8392704, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0x7fee8d4dc000
- pthreads库调用mprotect()设置内存区域的读/写(即PROT_READ|PROT_WRITE)权限,但第一个4KB的防护页除外(将用于检测栈溢出)
mprotect(0x7fee8d4dd000, 8388608, PROT_READ|PROT_WRITE) = 0
- 使用调用clone()创建线程(栈的起始地址为0x7fee8dcdbfb0)
clone(child_stack=0x7fee8dcdbfb0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tid=[3338], tls=0x7fee8dcdc700, child_tidptr=0x7fee8dcdc9d0) = 3338
因此,内存空间布局如下:
+ +--------------------+ 0x7fee8d4dc000
| | |
4 KB | | RED ZONE |
(PROT_NONE)| | (guard page) |
+ +--------------------+ 0x7fee8d4dd000
| | |
| | |
| | ^ |
8192 KB | | | |
(PROT_READ/WRITE) | Stack |
| | | |
| | | |
| +--------------------+ 0x7fee8dcdbfb0
| | |
| | TCB + TLS |
| | |
+ +--------------------+ 0x7fee8dcdd000
为什么程序崩溃了
线程入口点定义了一个大小为4096x4096x4
字节的表,相当于64 MB。这对于8 MB长的堆栈区域来说太大了。然而,我们本可以期望完全没有崩溃,因为该函数定义了一个巨大的本地表,但是没有对其进行读写访问。因此,不应该发生崩溃。
strace
日志显示,崩溃发生在对地址0x7fee8d4dcef0的访问上,该地址位于分配的内存区域中的堆栈区域之上:[pid 3338] --- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_ACCERR, si_addr=0x7fee8d4dcef0} ---
实际上,它实在保护页中:
+ +--------------------+ 0x7fee8d4dc000
| | |
4 KB | | RED ZONE <--------- Trap @ si_addr=0x7fee8d4dcef0
(PROT_NONE)| | | si_code=SEGV_ACCERR
+ +--------------------+ 0x7fee8d4dd000
| | |
| | |
| | ^ |
8192 KB | | | |
(PROT_READ/WRITE) | Stack |
| | | |
| | | |
| +--------------------+ 0x7fee8dcdbfb0
| | |
| | TCB + TLS |
| | |
+ +--------------------+ 0x7fee8dcdd000
在
gdb
下进行的核心转储分析为崩溃提供了以下位置:
$ gdb a.out core
[...]
(gdb) where
#0 0x00005594eb9461a0 in function (arg=<error reading variable: Cannot access memory at address 0x7fe95459ded8>) at p.c:56
#1 0x00007fe95879d609 in start_thread (arg=<optimized out>) at pthread_create.c:477
#2 0x00007fe9586c4293 in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95
(gdb) disas /m
Dump of assembler code for function function:
56 void* function(void* arg){
0x00005594eb946189 <+0>: endbr64
0x00005594eb94618d <+4>: push %rbp
0x00005594eb94618e <+5>: mov %rsp,%rbp
0x00005594eb946191 <+8>: lea -0x4000000(%rsp),%r11
0x00005594eb946199 <+16>: sub $0x1000,%rsp
=> 0x00005594eb9461a0 <+23>: orq $0x0,(%rsp)
0x00005594eb9461a5 <+28>: cmp %r11,%rsp
0x00005594eb9461a8 <+31>: jne 0x5594eb946199 <function+16>
0x00005594eb9461aa <+33>: sub $0x20,%rsp
0x00005594eb9461ae <+37>: mov %rdi,-0x4000018(%rbp)
0x00005594eb9461b5 <+44>: mov %fs:0x28,%rax
0x00005594eb9461be <+53>: mov %rax,-0x8(%rbp)
0x00005594eb9461c2 <+57>: xor %eax,%eax
57 int picture[4096][4096];
58 }
上述线程入口点的反汇编代码显示,
gcc
每 4 KB(内存页面大小)生成一次堆栈访问。它首先使用本地表的起始地址(
0x4000000 是
4096x4096xsizeof(int) = 67108864 字节)设置
R11
寄存器:
0x00005594eb946191 <+8>: lea -0x4000000(%rsp),%r11
接着,它会每隔4096个字节(0x1000)将堆栈中的内容与0进行"oring"操作:
0x00005594eb946199 <+16>: sub $0x1000,%rsp
=> 0x00005594eb9461a0 <+23>: orq $0x0,(%rsp)
0x00005594eb9461a5 <+28>: cmp %r11,%rsp
0x00005594eb9461a8 <+31>: jne 0x5594eb946199 <function+16>
因此,在堆栈的守卫页面出现
orq
指令,导致崩溃发生!需要注意的是:所生成代码“看起来无用”的原因是为了保护防止
Stack Clash类漏洞,详见此
答案。当然,使用优化选项编译相同的代码
不会触发任何崩溃,因为
function()不包含任何代码。
$ gcc p.c -lpthread -O2
$ ./a.out
< p >
function()的优化反汇编代码是一个简单的“return”语句:
$ objdump -S a.out
[...]
00000000000011f0 <function>:
11f0: f3 0f 1e fa endbr64
11f4: c3 retq
11f5: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
11fc: 00 00 00
11ff: 90 nop
如何为线程设置更大的堆栈
如上所述,默认情况下,GLIBC/pthread库分配一个默认大小为8 MB的堆栈。但是它也提供了设置用户分配的堆栈或仅定义堆栈大小的能力,具体步骤如下:
以下是增强版程序,为线程定义了一个65 MB的堆栈:
#include <stdio.h>
#include <pthread.h>
void* function(void* arg)
{
int picture[4096][4096];
}
int main(void)
{
pthread_t pids[10];
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setstacksize(&attr, 65*1024*1024);
pthread_create(&pids[0], &attr, function, NULL);
pthread_join(pids[0], NULL);
pthread_attr_destroy(&attr);
return 0;
}
构建和执行:
$ gcc p2.c -lpthread
$ ./a.out
没有崩溃。使用strace
,我们可以验证其行为:
$ strace ./a.out
[...]
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
mmap(NULL, 68161536, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0x7fe55afd3000
mprotect(0x7fe55afd4000, 68157440, PROT_READ|PROT_WRITE) = 0
brk(NULL) = 0x55b9d7ade000
brk(0x55b9d7aff000) = 0x55b9d7aff000
clone(child_stack=0x7fe55f0d2fb0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tid=[5199], tls=0x7fe55f0d3700, child_tidptr=0x7fe55f0d39d0) = 5199
futex(0x7fe55f0d39d0, FUTEX_WAIT, 5199, NULL) = 0
munmap(0x7fe55afd3000, 68161536) = 0
exit_group(0) = ?
+++ exited with 0 +++
我们可以从上述跟踪信息中看到:
- 调用mmap()请求内存空间大小为65 MB + 4KB = 66564 KB = 68161536字节(即65 MB + 4 KB的保护页向上取整到更大的4 KB页面边界)
mmap(NULL, 68161536, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0x7fe55afd3000
- 在前68157440个字节上调用mprotect()来设置剩余4KB的保护页
mprotect(0x7fe55afd4000, 68157440, PROT_READ|PROT_WRITE) = 0
因此,新的内存空间布局如下:
+ +--------------------+ 0x7fe55afd3000
| | |
4 KB | | RED ZONE |
(PROT_NONE)| | |
+ +--------------------+ 0x7fe55afd4000
| | |
| | |
| | ^ |
66560 KB | | | |
(PROT_READ/WRITE) | Stack |
| | | |
| | | |
| +--------------------+ 0x7fe55f0d2fb0
| | |
| | TCB + TLS |
| | |
+ +--------------------+ 0x7FE55F0D4000
结论
从一个简单的程序结束到奇怪的崩溃,我们有机会研究了GLIBC/pthread库中线程栈布局以及对抗栈溢出和配置栈大小的保护机制。
然而,从程序设计的角度来看,我们不应该在栈中分配如此巨大的变量。在当前程序中,表格应该动态分配或定义为全局变量(例如在线程本地存储中)。但这是另一回事...
symbol-file /path/to/my/binary sharedlibrary
。 - Amit wadhwapicture = <error reading variable picture (value>
。虽然这些东西非常相似。 - Amit wadhwa