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 変数は、以下のように宣言します。
/* 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 によるスレッド安全コードを実現できます。

kmckk at 19:00コメント(0)Clang | 若槻 

コメントする

名前
 
  絵文字
 
 
記事検索
最新コメント
アクセスカウンター
  • 今日:
  • 昨日:
  • 累計:

QRコード
QRコード