【问题标题】: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?当我使用线程局部变量调用函数时,为什么这个 nostdlib C++ 代码会出现段错误?但不是使用全局变量或当我访问成员时?
【发布时间】:2021-12-05 20:33:24
【问题描述】:

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

【问题讨论】:

  • register 是自 C++17 以来未使用的关键字。
  • @FrançoisAndrieux 如果没有它,该程序集似乎无法工作
  • 感觉还是直接写汇编比较好。
  • @FrançoisAndrieux 我认为让人们复制/粘贴两个单独的文件不值得费心。它似乎不会影响我的问题
  • @FrançoisAndrieux:register __asm__ 是一个 gcc/clang 扩展,与内联汇编一起使用。见gcc.gnu.org/onlinedocs/gcc/…

标签: c++ linux gcc x86-64 thread-local-storage


【解决方案1】:

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

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

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

我会写一些类似的东西

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

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

【讨论】:

  • 没错,您的数据位于(Try1*)((u64)fsbasePtr - 8)。因此,要计算该地址,编译器需要知道fsbasePtr 的值。你知道它等于buffer+4096,但编译器不知道。缺少rdfsbase,它无法检索fsbasePtr 的值。像mov reg, fs:[offset] 这样的指令可以从线程本地存储块加载,但它们不能告诉我们它的线性地址(不,lea reg, fs:[offset] 不起作用;它只会返回@ 987654351@).
  • 所以fsbasePtr 的值需要存储在内存中的某个规范位置,编译器会知道在哪里找到它,还有什么地方比线程本地存储块本身更好?因此,ABI 要求将该地址存储在 TLS 块中的偏移量 0 处,以便mov rax, fs:0x0 将使用正确的fsbasePtr 值加载rax。因此,设置 TLS 块的人负责将其存储在那里 - 即您。
  • 这种增加的复杂性是我们使用fs 访问线程本地存储所付出的代价。如果我们只是将 TLS 指针保存在某个通用寄存器中,例如r15,会简单得多;那么获取this 地址就像lea rdi. [r15-8] 一样简单,加载和存储也很容易。其他架构可以做到这一点。但是我们会放弃将r15 用于其他任何事情的能力,而x86-64 并没有那么多空闲。因此,这种使用fs 的“hack”。
  • (我猜黑客真的来自 x86-32,它在寄存器方面更加人手不足。而且读取段基数更难,因为它甚至不在隐藏寄存器中,但在内核维护的本地描述符表中。)
  • 哦,我明白了。气体 Intel 格式允许您省略通常的方括号,并且反汇编程序选择以这种方式编写它并没有帮助。 mov rax, fs:[0x0] 会更清楚。
猜你喜欢
  • 1970-01-01
  • 2022-01-26
  • 1970-01-01
  • 1970-01-01
  • 2021-06-20
  • 2015-05-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多