2022年07月28日
Thread Local StorageをOSに頼らず実装する方法
特定の OS を前提としないベアメタルのツールチェーン(いわゆる aarch64-unknown-elf のようなターゲット)に付属するライブラリは、マルチスレッド関係のライブラリの排他制御などが全て OFF になった状態です。pthread などのスレッドライブラリを前提にすることは当然できませんが、Thread Local Storage(TLS)だけならば OS に依存しない形で実装でき、かつ OS を使う場合は無変更でライブラリ関数のスレッドセーフ化が可能なのではないか?と思いつき、調査した時のメモです。
以下は clang で aarch64-unknown-elf ターゲットで、ダイナミックリンカが無い OS レス環境なので 当然 shared library も無効で、シンプルに TLS は Local Executable model のみとします。
TLS 変数は、以下のように宣言します。
いろいろ苦労したのですが、最終的には crt で以下のような初期化をすることにより、ベアメタルのシングルスレッド環境でも正しく TLS 変数にアクセスできるようになりました。
(1) リンカスクリプトで TLS セクションを指定する。
TLS 変数は、初期値ありの場合は .tdata セクション、0 の場合は .tbss セクションとなります。
(2)スタートアップで TPIDR_EL0 を .tdata の先頭 - TCB ヘッダに設定する。
ここが一番ハマったのですが、TPIDR_EL0 は .tdata の先頭を指すわけではなく、aarch64 の場合は 16 バイト手前を指します。(ABI でそのように決まってるようですが、ちゃんと確認してません。)
OS ありの環境の場合は、OS がスレッドを生成する時に、先頭の TCB ヘッダ + TLS セクションぶんメモリを確保し、TCB ヘッダ以後に丸ごとコピーし、TPIDR_EL0 がそこを指すようにすることで、リビルド無しで完全な TLS によるスレッド安全コードを実現できます。
TLS 変数は、以下のように宣言します。
/* GNU 拡張 */ #ifdef GNU_THREAD __thread /* C++11 */ #elif defined(__cplusplus) thread_local #else /* C11 */ _Thread_local #endif int x = 10; void foo(int n) { x = n; }全て、出るコードは同じです。(C++ はマングリングされますが、割愛します。)出るコードをすっきりさせるため -O2 でコンパイルしました。
$ clang --target=aarch64-unknown-elf -march=armv8-a -O2 -S -xc -DGNU_THREAD tls.cpp -o gnu.s $ clang --target=aarch64-unknown-elf -march=armv8-a -O2 -S -xc tls.cpp -o c11.s $ clang --target=aarch64-unknown-elf -march=armv8-a -O2 -S tls.cpp -o cxx11.s ... foo: mrs x8, TPIDR_EL0 add x8, x8, :tprel_hi12:x add x8, x8, :tprel_lo12_nc:x str w0, [x8] retこの TPIDR_EL0 に対して、オフセットアクセスしています。
いろいろ苦労したのですが、最終的には crt で以下のような初期化をすることにより、ベアメタルのシングルスレッド環境でも正しく TLS 変数にアクセスできるようになりました。
(1) リンカスクリプトで TLS セクションを指定する。
TLS 変数は、初期値ありの場合は .tdata セクション、0 の場合は .tbss セクションとなります。
.type x,@object .section .tdata,"awT",@progbits .globl x .p2align 2 x: .word 10 .size x, 4リンカスクリプトを以下のように定義します。(ここではデバッガ、あるいはシミュレータにより直接 RAM にロードされることを想定します。)
.tdata : { _tdata = . ; *(.tdata .tdata.* .gnu.linkonce.td.*) _etdata = . ; } .tbss : { _tbss = . ; *(.tbss .tbss.* .gnu.linkonce.tb.*) *(.tcommon) . = ALIGN(16); _etbss = . ; }仕様などはちゃんと見てないのですが、.tbss の変数は .tdata の先頭からのオフセットでアクセスするようです。
(2)スタートアップで TPIDR_EL0 を .tdata の先頭 - TCB ヘッダに設定する。
ここが一番ハマったのですが、TPIDR_EL0 は .tdata の先頭を指すわけではなく、aarch64 の場合は 16 バイト手前を指します。(ABI でそのように決まってるようですが、ちゃんと確認してません。)
/* aarch64 */ #define TCB_SIZE 16 ... #if HAS_THREAD_LOCAL extern char _tdata[], _tbss[], _etbss[]; asm volatile("msr tpidr_el0, %0" : : "r" (((uintptr_t)&_tdata) - TCB_SIZE)); /* この時点では MMU などが初期化されていないので memset は使えない。 */ p = (unsigned char *)&_tbss; size = (size_t)&_etbss - (size_t)&_tbss; while (size--) { *p++ = 0; } #endifこれだけで、OS レス環境でも TLS 変数に通常の .data や .bss 変数と同じようにアクセスできるようになりました。わかってしまえばけっこう簡単なので、組み込みでも TLS は積極的に使って良いのではないかと思いました。
OS ありの環境の場合は、OS がスレッドを生成する時に、先頭の TCB ヘッダ + TLS セクションぶんメモリを確保し、TCB ヘッダ以後に丸ごとコピーし、TPIDR_EL0 がそこを指すようにすることで、リビルド無しで完全な TLS によるスレッド安全コードを実現できます。