在自定义的libc中实现线程局部存储

9
我正在实现一个非常小且静态链接程序的小型libc子集,我认为添加TLS支持是一次很好的学习经验。我使用Ulrich Drepper的TLS文档作为参考。
我有两个字符串设置来尝试这个:
static __thread const char msg1[] = "TLS (1).\n"; /* 10 bytes */
static __thread const char msg2[] = "TLS (2).\n"; /* 10 bytes */

编译器生成以下指令来访问它们:

mov    rbx, QWORD PTR fs:0x0 ; Load TLS.
lea    rsi, [rbx-0x14]       ; Get a pointer to 'msg1'. 20 byte offset.
lea    rsi, [rbx-0xa]        ; Get a pointer to 'msg2'. 10 byte offset.

假设我将TCB放置在堆栈的某个位置:
struct tcb {
    void* self; /* Points to self. I read that this was necessary somewhere. */
    int errno;  /* Per-thread errno variable. */
    int padding;
};

然后将TLS区域放在其旁边,位置为tls = &tcb - tls_size。然后我设置FS寄存器指向fs = tls + tls_size,并将TLS初始化图像复制到tls中。但是,这样做不起作用。我已经验证了通过将tls_image处的20个字节写入stdout来正确定位TLS初始化图像。这要么让我相信我没有正确放置TCB和/或TLS区域,要么就是我没有遵守ABI。

  • 我使用arch_prctl(2)设置FS寄存器。我需要以某种方式使用set_thread_area(2)吗?
  • 我没有dtv。我假设这不是必需的,因为我是静态链接的。

有什么想法,我做错了什么吗?非常感谢!


2
如果你正在使用Linux,应该尝试适应AT&T语法。 - caf
3
不确定你出了什么问题,但是看一下我们在musl中的实现可能会有所帮助:http://git.musl-libc.org/cgit/musl/tree/src/env/__init_tls.c - R.. GitHub STOP HELPING ICE
@R.. 很好的参考资料。但我不明白 libc.tls_size = 2*sizeof(void *)+size+align+sizeof(struct pthread);mem += sizeof(void *) * 2; 中的两个空指针是用来做什么的? - haste
好的观点 - 那应该加上注释。不看代码,我猜它是为了数字电视。 - R.. GitHub STOP HELPING ICE
1
请注意,原则上,“__tls_get_addr”也需要为静态链接的程序工作,因为没有要求链接器执行TLS优化,并且特定的目标文件可能已经使用需要它的模型构建(例如,如果使用了“-fPIC”)。但是,由于只有一个TLS段,“__tls_get_addr”实际上不需要dtv来操作。 - R.. GitHub STOP HELPING ICE
你可以查看现有的libc是如何实现的。musl-libc的代码非常易读。 - Basile Starynkevitch
1个回答

4
我正在为非常小且静态链接的程序实现libc的一个小子集,我认为添加TLS支持将是一个很好的学习经验。
太棒了!因为我不能使用像pthread这样的常见线程库,所以我不得不在一个项目中实现自己的TLS。我没有完全解决你的问题,但分享我的经验可能会有用。
我使用arch_prctl(2)设置FS寄存器。我需要以某种方式使用set_thread_area(2)吗?
答案取决于您实际使用的体系结构。如果您使用的是x86-64位,则应该使用arch_prctl来将FS寄存器设置为要用作TLS的内存区域(它允许您寻址大于4GB的内存区域)。而对于x86-32,则必须使用set_thread_area,因为它是内核支持的唯一系统调用。
我的实现思路是为每个线程分配一个私有内存区域,并将其地址保存到%GS寄存器中。这是一种相当简单的方法,但在我的情况下,它运行得非常好。每次访问线程的私有区域时,只需要使用保存在%GS中的值作为基地址和标识内存位置的偏移量。我通常为每个线程分配一个内存页面(4096),并将其分成8字节块。因此,我为每个线程拥有512个私有内存槽,可以像数组一样访问,索引范围从0到511。
这是我使用的代码:
#define _GNU_SOURCE 1 

#include "tls.h"
#include <asm/ldt.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/prctl.h>
#include <asm/prctl.h>
#include <sys/syscall.h> 
#include <unistd.h> 

void * install_tls() {
  void *addr = mmap(0, 4096, PROT_READ|PROT_WRITE,
                       MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
  if (syscall(SYS_arch_prctl,ARCH_SET_GS, addr) < 0) 
      return NULL;

   return addr;
}

void freeTLS() {
    void *addr;
    syscall(SYS_arch_prctl,ARCH_GET_GS, &addr);  
    munmap(addr, 4096);
}

bool set_tls_value(int idx, unsigned long val) {
    if (idx < 0 || idx >= 4096/8) {
      return false;
    }
    asm volatile(
        "movq %0, %%gs:(%1)\n"
        :
        : "q"((void *)val), "q"(8ll * idx));
    return true;
}


unsigned long get_tls_value(int idx) {
    long long rc;
    if (idx < 0 || idx >= 4096/8) {
      return 0;
    }
    asm volatile(
        "movq %%gs:(%1), %0\n"
        : "=q"(rc)
        : "q"(8ll * idx));
    return rc;
  }

这是带有一些宏定义的标题:

#ifndef TLS_H
#define TLS_H

#include <stdbool.h>

void *install_tls(); 
void freeTLS();
bool set_tls_value (int, unsigned long); 
unsigned long get_tls_value(int ); 

/*
 *macros used to set and retrieve the values 
 from the tls area
*/ 

#define TLS_TID 0x0
#define TLS_FD  0x8 
#define TLS_MONITORED 0x10

#define set_local_tid(_x) \
    set_tls_value(TLS_TID, (unsigned long)_x)

#define set_local_fd(_x) \
    set_tls_value(TLS_FD, (unsigned long)_x)

#define set_local_monitored(_x) \
    set_tls_value(TLS_MONITORED, (unsigned long)_x)

#define get_local_tid() \
    get_tls_value(TLS_TID)

#define get_local_fd() \
    get_tls_value(TLS_FD)

#define get_local_monitored() \
    get_tls_value(TLS_MONITORED)



#endif /* end of include guard: TLS_H */

每个线程要完成的第一项任务是安装TLS内存区域。一旦TLS被初始化,每个线程都可以将此区域用作私有TLS。


1
谢谢您的输入。我实际上还没有尝试修复我的实现,但是这个答案包含了一些有用的信息。我将不得不花一个周末并尽快解决它。 :) - haste

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