当我使用线程局部变量调用函数时,为什么这个 nostdlib C++ 代码会出现段错误?但不是使用全局变量或当我访问成员时?

Why does this nostdlib C++ code segfault when I call a function with a thread local variable? But not with a global var or when I access members?

包含组件。这个周末我试图在没有任何 C 库的情况下获得我自己的小型库 运行ning,线程本地的东西给我带来了问题。下面你可以看到我创建了一个名为 Try1 的结构(因为这是我的第一次尝试!)如果我设置线程局部变量并使用它,代码似乎执行得很好。如果我使用全局变量在 Try1 上调用 const 方法,它似乎 运行 很好。现在,如果我两者都做,那就不好了。尽管我能够访问成员并使用全局变量 运行ning 函数,但它还是出现了段错误。该代码将打印 Hello 和 Hello2 但不会打印 Hello3

我怀疑问题出在变量的地址上。我尝试使用 if 语句打印第一个 hello。 if ((s64)&t1 > (s64)buf+1024*16) 这是真的,所以这意味着指针不在我认为的位置。它也不是 gdb 建议的 -8(这是一个带符号的比较,我尝试使用 0 而不是 buf)

c++代码下的汇编。第一行是第一次调用 write

//test.cpp
//clang++ or g++ -std=c++20 -g -fno-rtti -fno-exceptions -fno-stack-protector -fno-asynchronous-unwind-tables -static -nostdlib test.cpp -march=native && ./a.out
#include <immintrin.h>
typedef unsigned long long int u64;

ssize_t my_write(int fd, const void *buf, size_t size) {
    register int64_t rax __asm__ ("rax") = 1;
    register int rdi __asm__ ("rdi") = fd;
    register const void *rsi __asm__ ("rsi") = buf;
    register size_t rdx __asm__ ("rdx") = size;
    __asm__ __volatile__ (
        "syscall"
        : "+r" (rax)
        : "r" (rdi), "r" (rsi), "r" (rdx)
        : "cc", "rcx", "r11", "memory"
    );
    return rax;
}

void my_exit(int exit_status) {
    register int64_t rax __asm__ ("rax") = 60;
    register int rdi __asm__ ("rdi") = exit_status;
    __asm__ __volatile__ (
        "syscall"
        : "+r" (rax)
        : "r" (rdi)
        : "cc", "rcx", "r11", "memory"
    );
}

struct Try1
{
    u64 val;
    constexpr Try1() { val=0; }
    u64 Get() const { return val; }
};

static char buf[1024*8]; //originally mmap but lets reduce code

static __thread u64 sanity_check;
static __thread Try1 t1;
static Try1 global;

extern "C"
int _start()
{
    auto tls_size = 4096*2;
    auto originalFS = _readfsbase_u64();
    _writefsbase_u64((u64)(buf+4096));

    global.val = 1;
    global.Get(); //Executes fine

    sanity_check=6;
    t1.val = 7;

    my_write(1, "Hello\n", sanity_check);
    my_write(1, "Hello2\n", t1.val); //Still fine
    my_write(1, "Hello3\n", t1.Get()); //crash! :/
    my_exit(0);
    return 0;
}

汇编:

4010b4:       e8 47 ff ff ff          call   401000 <_Z8my_writeiPKvm>
4010b9:       64 48 8b 04 25 f8 ff    mov    rax,QWORD PTR fs:0xfffffffffffffff8
4010c0:       ff ff 
4010c2:       48 89 c2                mov    rdx,rax
4010c5:       48 8d 05 3b 0f 00 00    lea    rax,[rip+0xf3b]        # 402007 <_ZNK4Try13GetEv+0xeef>
4010cc:       48 89 c6                mov    rsi,rax
4010cf:       bf 01 00 00 00          mov    edi,0x1
4010d4:       e8 27 ff ff ff          call   401000 <_Z8my_writeiPKvm>
4010d9:       64 48 8b 04 25 00 00    mov    rax,QWORD PTR fs:0x0
4010e0:       00 00 
4010e2:       48 05 f8 ff ff ff       add    rax,0xfffffffffffffff8
4010e8:       48 89 c7                mov    rdi,rax
4010eb:       e8 28 00 00 00          call   401118 <_ZNK4Try13GetEv>
4010f0:       48 89 c2                mov    rdx,rax
4010f3:       48 8d 05 15 0f 00 00    lea    rax,[rip+0xf15]        # 40200f <_ZNK4Try13GetEv+0xef7>
4010fa:       48 89 c6                mov    rsi,rax
4010fd:       bf 01 00 00 00          mov    edi,0x1
401102:       e8 f9 fe ff ff          call   401000 <_Z8my_writeiPKvm>
401107:       bf 00 00 00 00          mov    edi,0x0
40110c:       e8 12 ff ff ff          call   401023 <_Z7my_exiti>
401111:       b8 00 00 00 00          mov    eax,0x0
401116:       c9                      leave  
401117:       c3                      ret    

ABI 要求 fs:0 包含一个指针,该指针具有线程本地存储块的绝对地址,即 fsbase 的值。编译器需要访问这个地址来评估像 &t1 这样的表达式,在这里它需要它来计算要传递给 Try1::Get().

this 指针

在 x86-64 上恢复这个地址很棘手,因为 TLS 基地址不在方便的通用寄存器中,而是在隐藏的 fsbase 中。每次我们需要它时都执行 rdfsbase 是不可行的(昂贵的指令可能不可用),更糟糕的是调用 arch_prctl,所以最简单的解决方案是确保它在内存中可用一个已知的地址。请参阅 this past answer and sections 3.4.2 and 3.4.6 of "ELF Handling for Thread-Local Storage",它通过引用并入 x86-64 ABI。

0x4010d9 的反汇编中,您可以看到编译器试图从地址 fs:0x0 加载到 rax,然后添加 -8(t1 的偏移量在 TLS 块中)并将结果移动到 rdi 作为 Try1::Get() 的隐藏 this 参数。显然,因为你在 fs:0 处有零,结果指针无效,当 Try1::Get() 读取 val 时你会崩溃,这实际上是 this->val.

我会写这样的东西

void *fsbase = buf+4096;
_writefsbase_u64((u64)fsbase);
*(void **)fsbase = fsbase;

(或者 memcpy(fsbase, &fsbase, sizeof(void *)) 可能更符合严格的别名。)