当我使用线程局部变量调用函数时,为什么这个 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 *))
可能更符合严格的别名。)
包含组件。这个周末我试图在没有任何 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 *))
可能更符合严格的别名。)