在 pthreads start_routine 函数中声明数组导致分段错误
Declaring array in pthreads start_routine function causing segmentation fault
#include <stdio.h>
#include <pthread.h>
void* function(void* arg){
int picture[4096][4096];
}
int main(){
int N=10, S=10;
pthread_t pids[10];
pthread_create(&pids[0], NULL, function, NULL);
pthread_join(pids[0], NULL);
return 0;
}
我用以下代码编译了上面的代码:gcc test.c -pthread
。
在 运行 可执行文件上,它崩溃,显示:Segmentation fault
。
但是,如果我删除 int picture[4096][4096];
定义,它不会崩溃。
这可能是什么原因?
我生成了核心转储文件。我 运行 核心转储文件。它给了我以下内容:
#0 0x00005643352ba745 in function (arg=<error reading variable: Cannot access memory at address 0x7fe80b054ed8>) at Pthred_kk.c:5
picture = <error reading variable picture (value requires 67108864 bytes, which is more than max-value-size)>
#1 0x00007fe80f6526db in start_thread (arg=0x7fe80f055700) at pthread_create.c:463
pd = 0x7fe80f055700
now = <optimized out>
unwind_buf = {cancel_jmp_buf = {{jmp_buf = {140634661148416, 8554578219241222147, 140634661146560, 0, 0, 140724934020640,
-8545604918547140605, -8545605192128745469}, mask_was_saved = 0}}, priv = {pad = {0x0, 0x0, 0x0, 0x0}, data = {prev = 0x0,
cleanup = 0x0, canceltype = 0}}}
not_first_call = <optimized out>
#2 0x00007fe80f37b88f in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95
picture = <error reading variable picture (value requires 67108864
bytes, which is more than max-value-size)
在linux中,线程的最大堆栈大小约为 8MB。
如您所见,picture
的大小(67108864 字节)超过了最大大小(8MB = 8 * 1024 *1024 = 8388608)。
崩溃的程序是:
#include <stdio.h>
#include <pthread.h>
void *function(void *arg)
{
int picture[4096][4096]; // 4096*4096*sizeof(int) = 67108864 bytes = 64 MB
}
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 的无 read/write 权限的保护页).堆栈从高地址向低地址增长。
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() 获取默认堆栈大小,其中 returns 8 MB:
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
- pthread 库通过调用 mmap() 分配 8MB + 4 KB 保护页的堆栈区,没有 read/write 权限(即 PROT_NONE):
mmap(NULL, 8392704, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0x7fee8d4dc000
- pthreads 库调用 mprotect() 来设置 read/write(即 PROT_READ|PROT_WRITE) 除了前 4 KB 保护页之外的内存区域的权限(将用于检测堆栈溢出)
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
因此以下内存space布局:
+ +--------------------+ 0x7fee8d4dc000
| | |
4 KB | | RED ZONE |
(PROT_NONE)| | (guard page) |
+ +--------------------+ 0x7fee8d4dd000
| | |
| | |
| | ^ |
8192 KB | | | |
(PROT_READ/WRITE) | Stack |
| | | |
| | | |
| +--------------------+ 0x7fee8dcdbfb0
| | |
| | TCB + TLS |
| | |
+ +--------------------+ 0x7fee8dcdd000
为什么你的程序崩溃了
线程入口点定义了 table 个 4096x4096x4
字节,等于 64 MB。这对于 8 MB 长堆栈区域来说太多了。但是,我们可以预期根本不会发生崩溃,因为该函数定义了一个巨大的本地 table 但没有 read/write 访问它。所以,应该不会发生崩溃.
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 [=15=]x1000,%rsp
=> 0x00005594eb9461a0 <+23>: orq [=15=]x0,(%rsp)
0x00005594eb9461a5 <+28>: cmp %r11,%rsp
0x00005594eb9461a8 <+31>: jne 0x5594eb946199 <function+16>
0x00005594eb9461aa <+33>: sub [=15=]x20,%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(内存页面大小)产生一次堆栈访问。它首先将 R11
寄存器设置为本地 table 的开头地址(0x4000000 是 4096x4096xsizeof(int) = 67108864 字节):
0x00005594eb946191 <+8>: lea -0x4000000(%rsp),%r11
然后,它以每 4096 字节 (0x1000) 为 0 循环“或”堆栈的内容:
0x00005594eb946199 <+16>: sub [=17=]x1000,%rsp
=> 0x00005594eb9461a0 <+23>: orq [=17=]x0,(%rsp)
0x00005594eb9461a5 <+28>: cmp %r11,%rsp
0x00005594eb9461a8 <+31>: jne 0x5594eb946199 <function+16>
因此,崩溃是因为在某个时候,orq
指令出现在堆栈的保护页中!
N.B.:
- 生成代码“显然无用”的原因是针对 堆栈冲突 class 漏洞 的保护,如本
- 当然,使用优化选项编译相同的代码不会触发任何崩溃,因为function()不会包含任何代码:
$ gcc p.c -lpthread -O2
$ ./a.out
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 的默认堆栈。但它也提供了设置用户分配的堆栈或简单地通过以下步骤定义堆栈大小的能力:
- 用pthread_attr_init();
定义线程属性
- 在属性中设置堆栈大小 pthread_attr_setstacksize();
- 将属性作为第二个参数传递给 pthread_create();
- 调用pthread_attr_destroy()释放属性。
这是该程序的增强版本,它为线程定义了 65 MB 的堆栈:
#include <stdio.h>
#include <pthread.h>
void* function(void* arg)
{
int picture[4096][4096]; // 4096*4096*sizeof(int) = 67108864 bytes = 64 MB
}
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
因此新内存space布局:
+ +--------------------+ 0x7fe55afd3000
| | |
4 KB | | RED ZONE |
(PROT_NONE)| | |
+ +--------------------+ 0x7fe55afd4000
| | |
| | |
| | ^ |
66560 KB | | | |
(PROT_READ/WRITE) | Stack |
| | | |
| | | |
| +--------------------+ 0x7fe55f0d2fb0
| | |
| | TCB + TLS |
| | |
+ +--------------------+ 0x7FE55F0D4000
结论
从一个简单的程序结束到奇怪的崩溃,我们借此机会研究了GLIBC/pthread库中线程的堆栈布局以及针对的保护机制堆栈溢出和堆栈大小配置。
但是,从程序设计的角度,我们不应该在栈中分配这么大的变量。例如,在当前程序中,table 应该动态分配或定义为全局变量(在线程本地存储中)。但这是另一个故事...
#include <stdio.h>
#include <pthread.h>
void* function(void* arg){
int picture[4096][4096];
}
int main(){
int N=10, S=10;
pthread_t pids[10];
pthread_create(&pids[0], NULL, function, NULL);
pthread_join(pids[0], NULL);
return 0;
}
我用以下代码编译了上面的代码:gcc test.c -pthread
。
在 运行 可执行文件上,它崩溃,显示:Segmentation fault
。
但是,如果我删除 int picture[4096][4096];
定义,它不会崩溃。
这可能是什么原因?
我生成了核心转储文件。我 运行 核心转储文件。它给了我以下内容:
#0 0x00005643352ba745 in function (arg=<error reading variable: Cannot access memory at address 0x7fe80b054ed8>) at Pthred_kk.c:5
picture = <error reading variable picture (value requires 67108864 bytes, which is more than max-value-size)>
#1 0x00007fe80f6526db in start_thread (arg=0x7fe80f055700) at pthread_create.c:463
pd = 0x7fe80f055700
now = <optimized out>
unwind_buf = {cancel_jmp_buf = {{jmp_buf = {140634661148416, 8554578219241222147, 140634661146560, 0, 0, 140724934020640,
-8545604918547140605, -8545605192128745469}, mask_was_saved = 0}}, priv = {pad = {0x0, 0x0, 0x0, 0x0}, data = {prev = 0x0,
cleanup = 0x0, canceltype = 0}}}
not_first_call = <optimized out>
#2 0x00007fe80f37b88f in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95
picture = <error reading variable picture (value requires 67108864 bytes, which is more than max-value-size)
在linux中,线程的最大堆栈大小约为 8MB。
如您所见,picture
的大小(67108864 字节)超过了最大大小(8MB = 8 * 1024 *1024 = 8388608)。
崩溃的程序是:
#include <stdio.h>
#include <pthread.h>
void *function(void *arg)
{
int picture[4096][4096]; // 4096*4096*sizeof(int) = 67108864 bytes = 64 MB
}
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 的无 read/write 权限的保护页).堆栈从高地址向低地址增长。
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() 获取默认堆栈大小,其中 returns 8 MB:
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
- pthread 库通过调用 mmap() 分配 8MB + 4 KB 保护页的堆栈区,没有 read/write 权限(即 PROT_NONE):
mmap(NULL, 8392704, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0x7fee8d4dc000
- pthreads 库调用 mprotect() 来设置 read/write(即 PROT_READ|PROT_WRITE) 除了前 4 KB 保护页之外的内存区域的权限(将用于检测堆栈溢出)
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
因此以下内存space布局:
+ +--------------------+ 0x7fee8d4dc000
| | |
4 KB | | RED ZONE |
(PROT_NONE)| | (guard page) |
+ +--------------------+ 0x7fee8d4dd000
| | |
| | |
| | ^ |
8192 KB | | | |
(PROT_READ/WRITE) | Stack |
| | | |
| | | |
| +--------------------+ 0x7fee8dcdbfb0
| | |
| | TCB + TLS |
| | |
+ +--------------------+ 0x7fee8dcdd000
为什么你的程序崩溃了
线程入口点定义了 table 个 4096x4096x4
字节,等于 64 MB。这对于 8 MB 长堆栈区域来说太多了。但是,我们可以预期根本不会发生崩溃,因为该函数定义了一个巨大的本地 table 但没有 read/write 访问它。所以,应该不会发生崩溃.
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 [=15=]x1000,%rsp
=> 0x00005594eb9461a0 <+23>: orq [=15=]x0,(%rsp)
0x00005594eb9461a5 <+28>: cmp %r11,%rsp
0x00005594eb9461a8 <+31>: jne 0x5594eb946199 <function+16>
0x00005594eb9461aa <+33>: sub [=15=]x20,%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(内存页面大小)产生一次堆栈访问。它首先将 R11
寄存器设置为本地 table 的开头地址(0x4000000 是 4096x4096xsizeof(int) = 67108864 字节):
0x00005594eb946191 <+8>: lea -0x4000000(%rsp),%r11
然后,它以每 4096 字节 (0x1000) 为 0 循环“或”堆栈的内容:
0x00005594eb946199 <+16>: sub [=17=]x1000,%rsp
=> 0x00005594eb9461a0 <+23>: orq [=17=]x0,(%rsp)
0x00005594eb9461a5 <+28>: cmp %r11,%rsp
0x00005594eb9461a8 <+31>: jne 0x5594eb946199 <function+16>
因此,崩溃是因为在某个时候,orq
指令出现在堆栈的保护页中!
N.B.:
- 生成代码“显然无用”的原因是针对 堆栈冲突 class 漏洞 的保护,如本
- 当然,使用优化选项编译相同的代码不会触发任何崩溃,因为function()不会包含任何代码:
$ gcc p.c -lpthread -O2
$ ./a.out
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 的默认堆栈。但它也提供了设置用户分配的堆栈或简单地通过以下步骤定义堆栈大小的能力:
- 用pthread_attr_init(); 定义线程属性
- 在属性中设置堆栈大小 pthread_attr_setstacksize();
- 将属性作为第二个参数传递给 pthread_create();
- 调用pthread_attr_destroy()释放属性。
这是该程序的增强版本,它为线程定义了 65 MB 的堆栈:
#include <stdio.h>
#include <pthread.h>
void* function(void* arg)
{
int picture[4096][4096]; // 4096*4096*sizeof(int) = 67108864 bytes = 64 MB
}
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
因此新内存space布局:
+ +--------------------+ 0x7fe55afd3000
| | |
4 KB | | RED ZONE |
(PROT_NONE)| | |
+ +--------------------+ 0x7fe55afd4000
| | |
| | |
| | ^ |
66560 KB | | | |
(PROT_READ/WRITE) | Stack |
| | | |
| | | |
| +--------------------+ 0x7fe55f0d2fb0
| | |
| | TCB + TLS |
| | |
+ +--------------------+ 0x7FE55F0D4000
结论
从一个简单的程序结束到奇怪的崩溃,我们借此机会研究了GLIBC/pthread库中线程的堆栈布局以及针对的保护机制堆栈溢出和堆栈大小配置。
但是,从程序设计的角度,我们不应该在栈中分配这么大的变量。例如,在当前程序中,table 应该动态分配或定义为全局变量(在线程本地存储中)。但这是另一个故事...