美文网首页
linux用户空间 - 线程的栈与guard page

linux用户空间 - 线程的栈与guard page

作者: 404Not_Found | 来源:发表于2021-08-02 07:35 被阅读0次
  • 作者: 雪山肥鱼
  • 时间:20210802 22:47
  • 目的:了解线程的栈 与 guard page
# 线程的双重特性
  ## 代码模拟 线程的二义性
# 栈的 guard page
  ## guard page 代码举例
  ## strace 代码追踪线程的栈
# 再谈栈和堆
  ## 再谈线程的栈

线程栈的双重特性

  • 线程的栈是每个线程独有的,保存其运行状态和局部自动变量
  • 栈在线程开始的时候初始化,每个线程的栈相互独立,因此栈是thread safe 的。

线程的栈是相对安全的,但是栈所占的内存空间,又属于整个进程。 这就是双重特性。


线程栈与内存的关系.png

进程是资源单位,线程是调度单位
栈显然是内存的资源。从资源的角度来看,每个线程的栈都属于进程。
但是从用法的角度来说,T1 T2 T3 T4 自己有自己的栈。栈中存放着各个层级函数的回溯,还有一些参数,临时变量等。
函数调度的越深,栈就一直往下降。函数越回退,栈就越浅。

这就造成了线程栈的二义性。

  • 不像进程,每个程序有自己的页表,是不可能互相踏得。
  • 线程T1 踏了 线程 T2 T3 的栈,从内存资源上来看是合法的,因为整个地址空间是相同得,地址转换又是一样得。
  • 但是从用法上来讲,显然是不合法得.

代码模拟 线程得二义性

#include <limits.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

static char *p ; //bss -> 0
static void * thread_routine(void *arg) {
  char buf[10] = {0};
  p = buf; 
  while(1 ) {
     printf("buf is %s\n", buf);
     sleep(1);
  }
  return NULL;
}

int main(int argc, char ** argv) {
  pthread_t thread_id1;
  status = pthread_create(&thread_id1, NULL, thread_routine, NULL);
  if(status != 0)
    exit(-1);
  int i = 0;
  while(1) {
    if(p) {
      if(i++ %2 == 0) {
        strcpy(p, "hello");
      } else {
        strcpy(p, "world");
      }
    sleep(4);
    }
  }
  pthread_join(thread_id1, NULL);
  return 0;
}

主线程一直在修改子线程得栈。验证了 线程得栈依旧属于内存。

子线程的栈被修改.png

用法上是绝对错误的,每个线程应该有独立的栈

如果linux做成每根线程不会去践踏另一根线程,那么 每根线程都需要有mmu, 都要有自己对应的页表。线程切换的时候就要切换page table。这就是进程的概念了,大家有各自的页表,每一段有不同的权限。

上述这种内存践踏,asan 是检测不出来的。

// 程序正常运行
gcc -g -fsanitize=address ./stack_corruption.c

如果修改成了:

while(1) {
  if(p) {
    if(i++ %2 == 0)
      strcpy(p, "hello111111");
    else
      strcpy(p, "world");
  }
}
gcc -g -fsanitize=address ./stack_corruption.c

./a.out 就有问题了。


报错.png

栈的 guard page

guard page.png

内核空间就不说啦,当讨论多线程的时候,就是指的用户空间。
每个线程的栈会存在Guard page 保护页, 这种保护页是没有权限的。当一个线程进入了另一个线程的guardpage, 出现page falult 则程序崩溃。
线程种 程函数调用的越深,用的临时变量越多,则栈的空间越大。飞到guard page时,直接page fault.
作用:
T1线程不会篡改T2 线程的数据,问题出在哪里就死在哪里。
让出错的现场就是出错的原因。利于debug。

guard page 代码举例

int pthread_attr_set_guardsize(pthread_attr_t * attr, size_t guardsize);
int pthread_attr_getguardsize(const pthread_attr_t * attr, size_t *guardsize);
/*
 * thread_attr.c
 *
 * Create a thread using a non-default attributes object,
 * thread_attr. The thread reports its existence, and exits. The
 * attributes object specifies that the thread be created
 * detached, and, if the stacksize attribute is supported, the
 * thread is given a stacksize twice the minimum value.
 */
#include <limits.h>
#include <pthread.h>

#define _POSIX_THREAD_ATTR_STACKSIZE

void err_abort(int status, char *p)
{
    perror("%s\n", p);
    exit(status);
}

static void *access_overflow(void)
{
    char q[11*1024];
    int i = 10;
    for (i = 10;i>=0;i--) {
        printf("%s %d %p\n", __func__, i, &q[i*1024]);
        q[i*1024] = 'a';
    }
    while(1);
    printf("The thread is here2\n");
}

static void *thread_routine(void *arg)
{
    char p[5*1014];
    p[4999] = 'a';
    p[0] = 'a';

    access_overflow();
    return NULL;
}

int main(int argc, char *argv[])
{
    pthread_t thread_id1, thread_id2;
    pthread_attr_t thread_attr;
    struct sched_param thread_param;
    size_t stack_size, guard_size;
    int status;

    status = pthread_attr_init(&thread_attr);
    if (status != 0)
        err_abort(status, "Create attr");

    /*
     * Create a detached thread.
     */
    status =
        pthread_attr_setdetachstate(&thread_attr, PTHREAD_CREATE_DETACHED);
    if (status != 0)
        err_abort(status, "Set detach");
#ifdef _POSIX_THREAD_ATTR_STACKSIZE
    /*
     * If supported, determine the default stack size and report
     * it, and then select a stack size for the new thread.
     *
     * Note that the standard does not specify the default stack
     * size, and the default value in an attributes object need
     * not be the size that will actually be used.  Solaris 2.5
     * uses a value of 0 to indicate the default.
     */
    status = pthread_attr_getstacksize(&thread_attr, &stack_size);
    if (status != 0)
        err_abort(status, "Get stack size");
    printf("Default stack size is %u; minimum is %u\n",
           stack_size, PTHREAD_STACK_MIN);

    status = pthread_attr_getguardsize(&thread_attr, &guard_size);
    if (status != 0)
        err_abort(status, "Get guard size");
    printf("Default guard size is %u\n", guard_size);

    status = pthread_attr_setstacksize(&thread_attr, PTHREAD_STACK_MIN);
    if (status != 0)
        err_abort(status, "Set stack size");
#endif
    status = pthread_create(&thread_id1, &thread_attr, thread_routine, NULL);
    if (status != 0)
        err_abort(status, "Create thread");
    
    status = pthread_create(&thread_id2, &thread_attr, thread_routine, NULL);
    if (status != 0)
        err_abort(status, "Create thread");

    while(1);
    return 0;
}

运行结果:


结果.png

代码流程描述:故意让栈越跑越深

  1. 初始化线程的属性 pthread_attr_init. 并没有对其进行任何赋值
  2. 拿到这个线程后,打印出其默认的栈大小 - 8M
  3. 拿到线程的 guard size - 4kb
  4. 修改线程栈大小 - 16kb
  5. 人为的让栈溢出
  6. Segmentation Fault

strace 代码追踪

strace.png
人为设定16kb 的 栈空间, 但是mmap了20kb 的空间。其中有4kb 是guard page.这4kb的的属性为 prot_none。
两根线程 栈的地址 相差就是 20kb。

再谈栈和堆

  • 栈和堆从内核的内存管理来看,区别其实不大。
    原因:都可以用mmap申请,从而得到一片vma的地址空间。
    但是 堆具有 on-demand 属性, 需要使用的时候,才会帮你申请
  • 栈的属性
    栈的属性有多种,可以是MAP_STACK,也可以是MAP_GROWSDOWN。
    GROUWSDOWN(有些像堆)可以让栈往下长, 而MAP_STACK是已经分配好栈的大小,固定了大小。
  • MAP_STACK (since Linux 2.6.27)
    Allocate the mapping at an address suitable for a process or thread stack. This flag is currently a no-op, but is used in the glibc threading implementation so that if some architectures require special treatment for stack allocations, support can later be transparently implemented for glibc.
  • MAP_GROWSDOWN
    Used for stacks. Indicates to the kernel virtual memory system that the mapping should extend downward in memory.

再谈线程的栈

由上讨论可知,这里并不是像教科书中说的,栈都是 growsdown的,栈的申请方式可以是多样的,工程中不同的库,对于栈的使用是不同的
而对于线程的栈来说,有两个层面。

  • 栈的来源
    来源可以是多样的,当然特也可以malloc(1M), 给thread使用。也可以申请一片栈给栈使用。
    也可以growsdown,使用内存。
  • 栈的使用
    用法是固定的,都是先进后出。

当然 如果在其他库中,线程的栈使用的是growpdown,则使用的是linux内核中的关于growsdown的保护机制。
大概算法是 地址减去一页,会不会落到另一个vma中,比如栈不停增长,飞到了 数据段里。如果落到了,则崩溃。
内核中使用的就是以上growpdown的机制。

记住:pthread glibc是用户空间,内核是内核,也有单独一套

相关文章

网友评论

      本文标题:linux用户空间 - 线程的栈与guard page

      本文链接:https://www.haomeiwen.com/subject/xiqsvltx.html