C++ TLS 触发的栈溢出

2021.06.21
SF-Zhou

先看一段代码,猜猜 Linux 下编译后执行会发生什么(编译参数 -pthread -O2 -std=c++11在线执行):

#include <iostream>
#include <thread>

thread_local char tls[8 << 20];

void func() {
  char arr[1024 * 6];
  arr[sizeof(arr) - 1] = 'A';

  printf("%p %p\n", &tls, &arr);
}

int main() { std::thread(func).join(); }

如果你系统的栈大小是默认的 8MB,那么上述代码会导致栈溢出。原因在于 pthread 实现中 TLS 和线程栈使用的是同一块内存空间,TLS 使用的多时留给线程栈的就少了。

pthread 初始化时会根据系统设定的栈大小、TLS 占用的内存空间大小计算默认的栈空间:

// 1. 获取 TLS 内存大小
size_t static_tls_align;
_dl_get_tls_static_info (&__static_tls_size, &static_tls_align);

// 2. 获取系统栈大小
if (__getrlimit (RLIMIT_STACK, &limit) != 0 || limit.rlim_cur == RLIM_INFINITY) {
  limit.rlim_cur = ARCH_STACK_DEFAULT_SIZE;
}

// 3. 设定 pthread 默认栈大小
const size_t minstack = pagesz + __static_tls_size + MINIMAL_REST_STACK;
if (limit.rlim_cur < minstack) {
  limit.rlim_cur = minstack;
}
limit.rlim_cur = ALIGN_UP (limit.rlim_cur, pagesz);
lll_lock (__default_pthread_attr_lock, LLL_PRIVATE);
__default_pthread_attr.stacksize = limit.rlim_cur;
__default_pthread_attr.guardsize = GLRO (dl_pagesize);
lll_unlock (__default_pthread_attr_lock, LLL_PRIVATE);

pthread_create 时如果没有指定使用的栈大小,则会使用上述计算的默认栈大小

if (attr->stacksize != 0) {
  size = attr->stacksize;
} else {
  lll_lock (__default_pthread_attr_lock, LLL_PRIVATE);
  size = __default_pthread_attr.stacksize;
  lll_unlock (__default_pthread_attr_lock, LLL_PRIVATE);
}

如果担心 TLS 触发栈溢出,可以在程序启动时通过下方代码获取 TLS 需要使用的内存空间总大小(在线执行),可以根据该值进行阈值的判断:

#include <iostream>

extern "C" void _dl_get_tls_static_info(size_t *sizep, size_t *alignp);

int main() {
  size_t size;
  size_t align;
  _dl_get_tls_static_info(&size, &align);
  printf("size %lu\n", size);
}

References

  1. "ELF Handling For Thread-Local Storage", Ulrich Drepper