../things-from-cannot-allocate-memory-in-static-tls-block

Things learned from "cannot allocate memory in static TLS block"

Table of contents

0. pre

在 nginx 中加载动态模块时失败了,某个动态库在 nginx 里 dlopen 的时候出现里报错“cannot allocate memory in static TLS block”。直接把这个报错甩进 google 里面的时候,出来的基本上都是具体的某一个库加载的时候出现里问题,然后一些解决办法(更新或者 LD_PRELOAD),但是并没有出现关于为什么会有这个报错,以及如何从源码上解决的。 于是乎,开始。

1. 继续 google

减少关键词,进行搜索,出现里一篇关联性较强的文章,不过竟然时 csdn 的,如果时平时,当时是不会点开的,但是这次相关性较强的东西不多,于是就点进去里; https://blog.csdn.net/wowowoc/article/details/109275987 这篇文章出乎意料的还不错,虽然没有太直击源头,不过提供后续的搜索方向;从文章中可以得知这个报错是从 dlopen 加载动态库时特有的, tls (thread local storage)支持的 model 有4 种,其中 initial-exec 这种模式基本上是会导致编译出来的 so 在被 dlopen 加载的时候出现这个报错。

2. 首次尝试

在 so 的源码中搜索了一下,确实时存在 __attribute__((tls_model(initial-exec))) 这样代码,然后尝试修改成了 __attribute__((tls_model(global-dynamic))) ,再次编译之后还是会有同样的报错。没有解决,说明问题不在这里,但是似乎确实是这个方向了。

3. 再再搜索

搜索 thread local storage,学习一下概念,这是个什么东西。从名字上来说,线程本地存储。

搜索到的结果,提供了几个重要的信息:

  1. 当动态库存在一个 dynamic section 是 STATIC_TLS 的时候,加载就会报这个错误
  2. 如果动态库是以 -fPIC 编译的,理论上是不会在dynamic section 中出现 STATIC_TLS 的

然后问题的排查就变成了,so 在编译的时候是不是有忘记指定 -fPIC 呢,并没有。 反复阅读里 stackoverflow 上面的答案;理论上非 -fPIC 编译的代码是不能编译进动态库的,越看越奇怪,明明编译成了动态库,难道还能有代码是非 -fPIC 编译的?直到到代码里面发现汇编。

4. 再次尝试

突然想到手写的汇编是不是有可能在编译的时候指定 -fPIC 是没有用的。 于是,注释掉 asm("movq xxx@gottpoff(%%rip),%0" : "=r"(ret)) ; 再编译进去,用 dlopen 打开 so 就可以里。问题到这里似乎就解决了。

5. 复现

这个问题似乎并没有这么简单,多种因素在共同影响着结果。首先最直接的根据报错上来说,通过 dlopen 打开动态库,动态库里面的 thread local storage 使用的是 initial-exec model,并且 __thread 修饰的线程变量足够多,多到 glibc ld.so 的 static TLS block 中没有足够的空间来放置这些线程变量时,就会报错 "cannot allocate memory in static TLS block"

// no-pic.c
// gcc –shared no-pic.c -o libnopic.so

__thread int *xxx = NULL;
size_t no_pic_func(void) {
    size_t ret;
    asm("movq xxx@gottpoff(%%rip),%0" : "=r"(ret));
}
__thread int *t_0;
// many __thread int variable...
__thread int *t_xxx;

// tls_model.c
// gcc tls_model.c -ldl -o tls

#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv) {
    if (argc < 2) {
        printf("%s dym_so\n", argv[0]);
        exit(0);
    }
    void *handle;
    char *error;

    handle = dlopen(argv[1], RTLD_LAZY);
    if (!handle) {
        fprintf(stderr, "%s\n", dlerror());
        exit(EXIT_FAILURE);
    }

    dlclose(handle);
    exit(EXIT_SUCCESS);
}

final

出现这个报错的几个条件

  1. 手写的 asm 代码:这个会影响 -fPIC 选项的代码生成,无论 tls-model 指定的是什么,生成的动态库中都会带有 STATIC_TLS 的标记;
  2. 就算没有手写的 asm 代码,tls-model 指定为 initial-exec 时,生成的动态库一样会包含 STATIC_TLS 标记;
  3. 需要注意的是,其实不是所有的带有 STATIC_TLS 标记的动态库在使用 dlopen 加载的时候都会报错,而是需要一定量的 __thread 修饰的线程变量才会导致 static TLS block 不够用,然后报错。