在pthread的start_routine函数中声明数组会导致分段错误

3
#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];这一定义,它就不会崩溃。

这可能是什么原因?

2个回答

2

崩溃的程序是:

#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的守卫页没有读/写权限)。堆栈从高地址向低地址增长。

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(内存页面大小)生成一次堆栈访问。它首先使用本地表的起始地址(0x40000004096x4096xsizeof(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];    // 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
因此,新的内存空间布局如下:
              +      +--------------------+ 0x7fe55afd3000
              |      |                    |
      4 KB    |      |      RED ZONE      |
   (PROT_NONE)|      |                    |              
              +      +--------------------+ 0x7fe55afd4000
              |      |                    |
              |      |                    |
              |      |          ^         |
   66560 KB   |      |          |         |
(PROT_READ/WRITE)    |        Stack       |
              |      |          |         |
              |      |          |         |
              |      +--------------------+ 0x7fe55f0d2fb0
              |      |                    |
              |      |     TCB + TLS      |
              |      |                    |
              +      +--------------------+ 0x7FE55F0D4000

结论

从一个简单的程序结束到奇怪的崩溃,我们有机会研究了GLIBC/pthread库中线程栈布局以及对抗栈溢出和配置栈大小的保护机制。
然而,从程序设计的角度来看,我们不应该在栈中分配如此巨大的变量。在当前程序中,表格应该动态分配或定义为全局变量(例如在线程本地存储中)。但这是另一回事...


1

我生成了核心转储文件。我运行了核心转储文件,它给了我以下结果:

#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 = <读取变量picture时出错(值需要67108864字节,超过了最大值大小)

在Linux中,线程的最大堆栈大小约为8MB。
正如您所看到的,picture的大小(67108864字节)超过了最大大小(8MB = 8 * 1024 * 1024 = 8388608)。


谢谢,真的很酷。顺便问一下,你是如何获取核心转储以查看错误的?还有一种方法可以增加线程的堆栈大小吗? - Amit wadhwa
@Amitwadhwa 请查看此链接以了解如何生成核心转储 https://jvns.ca/blog/2018/04/28/debugging-a-segfault-on-linux/ - Krishna Kanth Yenumula
虽然这与本题不直接相关,但我想问一下,如果你有快速答案的话。 我按照博客中的步骤操作,但没有看到你粘贴的那行代码。此外,我不确定如何执行 symbol-file /path/to/my/binary sharedlibrary - Amit wadhwa
  1. ulimit -c unlimited
  2. sudo sysctl -w kernel.core_pattern=/tmp/core_%e_%p
  3. gdb <executable-file> <core dump file>
- Krishna Kanth Yenumula
在第二步完成后,核心转储文件将存储在 /tmp 文件夹中。您可以根据需要更改路径。 - Krishna Kanth Yenumula
我已经这样做了,但是没有看到其中的行picture = <error reading variable picture (value>。虽然这些东西非常相似。 - Amit wadhwa

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接