【问题标题】:Declaring array in pthreads start_routine function causing segmentation fault在 pthreads start_routine 函数中声明数组导致分段错误
【发布时间】:2021-03-23 13:32:15
【问题描述】:
#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]; 定义,它不会崩溃。

这可能是什么原因?

【问题讨论】:

    标签: c segmentation-fault pthreads stack-overflow


    【解决方案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 =

    在 linux 中,线程的最大堆栈大小约为 8MB。
    如您所见,picture 的大小(67108864 字节)大于最大大小(8MB = 8 * 1024 *1024 = 8388608)。

    【讨论】:

    • 谢谢 这真的很酷。在旁注中。您是如何通过核心转储来查看该错误的?有没有办法增加线程的堆栈大小?
    • @Amitwadhwa 查看此链接以了解核心转储的生成jvns.ca/blog/2018/04/28/debugging-a-segfault-on-linux
    • 虽然与此没有直接关系。但只是问一下,以防你有一个快速的答案。我按照博客中的步骤操作,但看不到您粘贴的行。另外,我不确定如何执行此symbol-file /path/to/my/binary sharedlibrary
    • 1) ulimit -c unlimited 2) sudo sysctl -w kernel.core_pattern=/tmp/core_%e_%p 3) gdb
    • 在步骤 2) 之后,核心转储文件将存储在 /tmp 文件夹中。您可以根据需要更改路径。
    【解决方案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 库通过调用返回 8 MB 的 getrlimit() 来获取默认堆栈大小: prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
    • pthread 库通过调用 mmap() 分配 8MB + 4 KB 保护页的堆栈区域,但没有读/写权限(即 PROT_NONE): mmap(NULL, 8392704, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0x7fee8d4dc000
    • pthreads 库调用 mprotect() 来设置除前 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

    因此有以下内存空间布局:

                  +      +--------------------+ 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(内存页大小)产生一次堆栈访问。它首先将R11寄存器设置为本地表开头的地址(0x40000004096x4096xsizeof(int) = 67108864 bytes):

       0x00005594eb946191 <+8>: lea    -0x4000000(%rsp),%r11
    

    然后,它每 4096 字节 (0x1000) 将堆栈的内容与 0 进行“或”循环:

       0x00005594eb946199 <+16>:    sub    $0x1000,%rsp
    => 0x00005594eb9461a0 <+23>:    orq    $0x0,(%rsp)
       0x00005594eb9461a5 <+28>:    cmp    %r11,%rsp
       0x00005594eb9461a8 <+31>:    jne    0x5594eb946199 <function+16>
    

    因此,崩溃是因为在某些时候,orq 指令出现在堆栈的保护页中!

    注意

    • “显然无用”生成代码的原因是防止 Stack Clash 类漏洞,如 answer 中所述
    • 当然,使用优化选项编译相同的代码不会触发任何崩溃,因为 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 的默认堆栈。但它也提供了设置用户分配的堆栈或通过以下步骤简单地定义堆栈大小的能力:

    这是一个增强版的程序,它为线程定义了一个 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 库中线程的堆栈布局以及堆栈溢出和堆栈大小的保护机制配置。
    但是,从程序设计的角度来看,我们永远不应该在堆栈中分配这么大的变量。在当前程序中,表应该是动态分配或定义为全局变量(在线程本地存储中)的例子。但这是另一个故事......

    【讨论】:

      猜你喜欢
      • 2011-12-27
      • 1970-01-01
      • 2012-11-09
      • 2013-05-27
      • 2021-05-08
      • 2011-05-31
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多