目次: C言語とlibc
C言語について。
Cライブラリ(libc)について。
目次: C言語とlibc
スレッドごとに独立したデータを保持する空間をTLS(Thread Local Storage)といいます。日本語だと「スレッド局所記憶」というそうです。TLSへのアクセス方法、TLSの初期化方法などはアーキテクチャ依存ですので、今回はglibcのRISC-V版の実装を観察します。
変数宣言は __libc_tsd_define() マクロを使います。例として文字処理に使われるctype関連の変数を見ます。
// glibc/ctype/ctype-info.c
__libc_tsd_define (, const uint16_t *, CTYPE_B)
__libc_tsd_define (, const int32_t *, CTYPE_TOLOWER)
__libc_tsd_define (, const int32_t *, CTYPE_TOUPPER)
// glibc/sysdeps/generic/libc-tsd.h
#define __libc_tsd_define(CLASS, TYPE, KEY) \
CLASS __thread TYPE __libc_tsd_##KEY attribute_tls_model_ie;
// glibc/include/libc-symbols.h
#define attribute_tls_model_ie __attribute__ ((tls_model ("initial-exec")))
マクロは下記のように展開されます。
// glibc/ctype/ctype-info.c
__libc_tsd_define (, const uint16_t *, CTYPE_B)
↓
__thread const uint16_t * __libc_tsd_CTYPE_B __attribute__ ((tls_model ("initial-exec")));
// glibc/include/ctype.h
__libc_tsd_define (extern, const uint16_t *, CTYPE_B)
↓
extern __thread const uint16_t * __libc_tsd_CTYPE_B __attribute__ ((tls_model ("initial-exec")));
ごちゃごちゃして見えますが、普通の変数宣言との違いは__thread指定子とtls_model("initial-exec") 属性の2つです。
これだけだと「そうですか」で終わってしまうので、もう少し詳しく調べます。
スレッドローカル変数のモデルは、Common Variable Attributes - Using the GNU Compiler Collection (GCC) を見る限り4種類あるようです。
それぞれの意味は悲しいことにGCCのマニュアルに書いていないのです。どうして……。参考になるマニュアルとしては、-ftls-model (-qtls) - XL C/C++ for Linux - IBM Documentation もしくは スレッド固有ストレージのアクセスモデル - Oracle Solaris 11.1リンカーとライブラリガイドがわかりやすいです。
Solarisのリンカーのモデル名はGCCと少し違いますが、一見して対応がわかる程度の差でしょう。
次回は変数のアドレス取得について見たいと思います。
目次: C言語とlibc
前回はスレッドローカル変数宣言のコードを見ました。今回は変数のアドレス取得のコードを見ます。
初期化の際はスレッドローカル変数のアドレスを取得する必要があります。アドレスの取得には __libc_tsd_address() というマクロを使います。
// glibc/ctype/ctype-info.c
void
__ctype_init (void)
{
const uint16_t **bp = __libc_tsd_address (const uint16_t *, CTYPE_B);
*bp = (const uint16_t *) _NL_CURRENT (LC_CTYPE, _NL_CTYPE_CLASS) + 128;
const int32_t **up = __libc_tsd_address (const int32_t *, CTYPE_TOUPPER);
*up = ((int32_t *) _NL_CURRENT (LC_CTYPE, _NL_CTYPE_TOUPPER) + 128);
const int32_t **lp = __libc_tsd_address (const int32_t *, CTYPE_TOLOWER);
*lp = ((int32_t *) _NL_CURRENT (LC_CTYPE, _NL_CTYPE_TOLOWER) + 128);
}
libc_hidden_def (__ctype_init)
// glibc/sysdeps/generic/libc-tsd.h
#define __libc_tsd_address(TYPE, KEY) (&__libc_tsd_##KEY)
#define __libc_tsd_get(TYPE, KEY) (__libc_tsd_##KEY)
#define __libc_tsd_set(TYPE, KEY, VALUE) (__libc_tsd_##KEY = (VALUE))
先ほど宣言した変数のアドレスを返すだけの単純なコードです(例えばKEYがCTYPE_Bなら &__libc_tsd_CTYPE_Bを返す)。
実際はどのようにアドレスを得るのでしょう?下記のようにコードを書き換えて、
// glibc/ctype/ctype-info.c
void
__ctype_init (void)
{
const uint16_t **bp = __libc_tsd_address (const uint16_t *, CTYPE_B);
*bp = NULL;
}
libc_hidden_def (__ctype_init)
空のmain関数のみのコードをスタティックリンクでコンパイルし、実行ファイルを逆アセンブルします。
000000000002c0c6 <__ctype_init>:
void
__ctype_init (void)
{
const uint16_t **bp = __libc_tsd_address (const uint16_t *, CTYPE_B);
*bp = NULL;
2c0c6: 00045797 auipc a5,0x45
2c0ca: 7227b783 ld a5,1826(a5) # 717e8 <_GLOBAL_OFFSET_TABLE_+0x70>
2c0ce: 9792 add a5,a5,tp
2c0d0: 0007b023 sd zero,0(a5)
}
2c0d4: 8082 ret
逆アセンブルを見ると、GOTのエントリからロードしたオフセット+tpレジスタの値 = アドレス、という実装になっています。RISC-Vにおいてtpレジスタは「スレッドポインタ」レジスタといって、まさにスレッドローカルストレージのためのレジスタです。
次回はスレッドポインタがいつどこで初期化されるかを見たいと思います。
目次: C言語とlibc
前回はスレッドローカル変数のアドレス取得部分のコードを見て、tp(スレッドポインタ)レジスタが重要な役割をしていることがわかりました。今回はtpレジスタの初期化コードを見ます。
レジスタの初期化は __libc_setup_tls() で行われます。少し長いですがコードを見ましょう。
// glibc/sysdeps/riscv/nptl/tls.h
/* The TP points to the start of the thread blocks. */
# define TLS_DTV_AT_TP 1
# define TLS_TCB_AT_TP 0
// glibc/csu/libc-tls.c
void
__libc_setup_tls (void)
{
void *tlsblock;
//...
/* We have to set up the TCB block which also (possibly) contains
'errno'. Therefore we avoid 'malloc' which might touch 'errno'.
Instead we use 'sbrk' which would only uses 'errno' if it fails.
In this case we are right away out of memory and the user gets
what she/he deserves. */
#if TLS_TCB_AT_TP
//...
#elif TLS_DTV_AT_TP //★★RISC-Vではこちらのコードが使われる★★
tcb_offset = roundup (TLS_INIT_TCB_SIZE, align ?: 1);
tlsblock = __sbrk (tcb_offset + memsz + max_align
+ TLS_PRE_TCB_SIZE + GLRO(dl_tls_static_surplus));
tlsblock += TLS_PRE_TCB_SIZE;
#else
/* In case a model with a different layout for the TCB and DTV
is defined add another #elif here and in the following #ifs. */
# error "Either TLS_TCB_AT_TP or TLS_DTV_AT_TP must be defined"
#endif
//...
/* Initialize the thread pointer. */
#if TLS_TCB_AT_TP
//...
#elif TLS_DTV_AT_TP //★★RISC-Vではこちらのコードが使われる★★
INSTALL_DTV (tlsblock, _dl_static_dtv);
const char *lossage = TLS_INIT_TP (tlsblock); //★★tpレジスタ初期化★★
#else
# error "Either TLS_TCB_AT_TP or TLS_DTV_AT_TP must be defined"
#endif
// glibc/sysdeps/riscv/nptl/tls.h
register void *__thread_self asm ("tp");
# define READ_THREAD_POINTER() ({ __thread_self; })
//...
/* Code to initially initialize the thread pointer. */
# define TLS_INIT_TP(tcbp) \
({ __thread_self = (char*)tcbp + TLS_TCB_OFFSET; NULL; })
// glibc/sysdeps/riscv/nptl/tls.h
/* The thread pointer tp points to the end of the TCB.
The pthread_descr structure is immediately in front of the TCB. */
# define TLS_TCB_OFFSET 0
メモリ領域を__sbrk() で確保してアドレスをtpに格納しています。#ifdefで多少見づらいですが、そんなに難しくないはずです。
今まではTLSに含まれるスレッドローカル変数のうち、実行時に値を初期化する変数について見てきました。スレッドローカル変数は実行時に初期化するだけではなく、起動時に初期化される変数もあります。起動時に初期化される変数は、実行ファイルに初期値が含まれていてTLSの初期化時に値がコピーされる仕組みです。
次回はTLSの初期化を見たいと思います。
SNSの利用者層を語るときTwitterはオジサンばかり、若者はInstagramなどと言われますが、SNSの利用率は総務省が調査していて、情報通信メディアの利用時間と情報行動に関する調査 - 総務省から見ることができます。
令和2年のデータだけを抜き出してグラフにしてみました。結構世代で違うもんですね。
今のところの傾向としてはこんなもんでしょうか。大体イメージ通りの結果でしたが、個人的に意外だったのはニコニコ動画です。ここまで利用者層が若返っているのは意外でした。
私は日本語圏のゲーム系動画を見るならなんだかんだでニコ動が一番面白いと思うので、いまだにプレミアム会員のまま使っていますが、利用者の私ですらもうニコ動には懐ゲー好きのオジサンしかいないのかな?と思っていたくらいでした……。
一昔前、ニコ動のプレミアム会員数が減り始めたくらいから「オワコン」呼ばわりする人達が増えた記憶がありますが、復活したんですね。以前のように皆が戻って来ると良いですねえ。
目次: C言語とlibc
前回はtp(スレッドポインタ)レジスタの初期化を調べました。今回はTLSの初期化を見たいと思います。起動時に値が設定されている変数の初期化に関わる部分です。
初期化関数 __libc_setup_tls() ではtpの指す先(__sbrk() で確保したメモリ領域)も初期化しています。しかしこちらは結構複雑です。大雑把に言うと、
このような処理が行われます。コードも見ておきましょう。
// glibc/csu/libc-start.c
STATIC int
LIBC_START_MAIN (int (*main) (int, char **, char ** MAIN_AUXVEC_DECL),
int argc, char **argv,
#ifdef LIBC_START_MAIN_AUXVEC_ARG
ElfW(auxv_t) *auxvec,
#endif
__typeof (main) init,
void (*fini) (void),
void (*rtld_fini) (void), void *stack_end)
{
//...
# ifdef HAVE_AUX_VECTOR
/* First process the auxiliary vector since we need to find the
program header to locate an eventually present PT_TLS entry. */
# ifndef LIBC_START_MAIN_AUXVEC_ARG
ElfW(auxv_t) *auxvec;
{
char **evp = ev;
while (*evp++ != NULL)
;
auxvec = (ElfW(auxv_t) *) evp;
}
# endif
_dl_aux_init (auxvec); //★これ★
if (GL(dl_phdr) == NULL)
# endif
// glibc/elf/dl-support.c
void
_dl_aux_init (ElfW(auxv_t) *av)
{
//...
_dl_auxv = av;
for (; av->a_type != AT_NULL; ++av)
switch (av->a_type)
{
case AT_PAGESZ:
if (av->a_un.a_val != 0)
GLRO(dl_pagesize) = av->a_un.a_val;
break;
case AT_CLKTCK:
GLRO(dl_clktck) = av->a_un.a_val;
break;
case AT_PHDR:
GL(dl_phdr) = (const void *) av->a_un.a_val; //★これ★
break;
case AT_PHNUM:
GL(dl_phnum) = av->a_un.a_val;
break;
// glibc/csu/libc-tls.c
void
__libc_setup_tls (void)
{
//...
/* Look through the TLS segment if there is any. */
if (_dl_phdr != NULL)
for (phdr = _dl_phdr; phdr < &_dl_phdr[_dl_phnum]; ++phdr)
if (phdr->p_type == PT_TLS)
{
/* Remember the values we need. */
memsz = phdr->p_memsz;
filesz = phdr->p_filesz;
initimage = (void *) phdr->p_vaddr + main_map->l_addr;
align = phdr->p_align;
if (phdr->p_align > max_align)
max_align = phdr->p_align;
break;
}
//...
/* Initialize the TLS block. */
#if TLS_TCB_AT_TP
//...
#elif TLS_DTV_AT_TP //★★RISC-Vではこちらのコードが使われる★★
_dl_static_dtv[2].pointer.val = (char *) tlsblock + tcb_offset; //★★TLSのアドレス★★
main_map->l_tls_offset = tcb_offset;
#else
# error "Either TLS_TCB_AT_TP or TLS_DTV_AT_TP must be defined"
#endif
_dl_static_dtv[2].pointer.to_free = NULL;
/* sbrk gives us zero'd memory, so we don't need to clear the remainder. */
memcpy (_dl_static_dtv[2].pointer.val, initimage, filesz); //★★TLSに初期データをコピー★★
PT_TLSタイプのプログラムヘッダーがどこに配置されているか?など調べたい場合は、実行ファイルをreadelfで見るとわかりやすいです。該当するヘッダがある場合は、Type欄がTLSとなるはずです。
Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align LOAD 0x0000000000000000 0x0000000000010000 0x0000000000010000 0x000000000005d944 0x000000000005d944 R E 0x1000 LOAD 0x000000000005e4d0 0x000000000006f4d0 0x000000000006f4d0 0x0000000000002500 0x0000000000007938 RW 0x1000 NOTE 0x0000000000000190 0x0000000000010190 0x0000000000010190 0x0000000000000020 0x0000000000000020 R 0x4 TLS 0x000000000005e4d0 0x000000000006f4d0 0x000000000006f4d0 ★★TLS初期化に使われるセグメント★★ 0x0000000000000020 0x0000000000000068 R 0x8 GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 RW 0x10 GNU_RELRO 0x000000000005e4d0 0x000000000006f4d0 0x000000000006f4d0 0x0000000000000b30 0x0000000000000b30 R 0x1
PT_TLSが2つある場合はどうなるでしょう?実はPT_TLSは実行ファイルに1つまでなので気にしなくて良いです。プログラムヘッダは同属性のセクションをまとめて1つのセグメントにします。PT_TLSセグメントを1つだけにするには、TLSに関わるセクションを連続して配置する必要があります。
もしTLS関係のセクションを分散させて配置するとリンク時エラーになります。試しにリンカースクリプトを書き換えTLS関係の .tdataと .tbssセクションの間に、TLSと無関係の .dataセクションを挟む形にすると、
riscv64-unknown-linux-gnu/bin/ld: hello: TLS sections are not adjacent: riscv64-unknown-linux-gnu/bin/ld: TLS: .tdata riscv64-unknown-linux-gnu/bin/ld: non-TLS: .data riscv64-unknown-linux-gnu/bin/ld: TLS: .tbss riscv64-unknown-linux-gnu/bin/ld: map sections to segments failed: bad value collect2: error: ld returned 1 exit status make: *** [Makefile:22: hello] エラー1
リンカーに "TLS sections are not adjacent" と怒られました。
< | 2022 | > | ||||
<< | < | 04 | > | >> | ||
日 | 月 | 火 | 水 | 木 | 金 | 土 |
- | - | - | - | - | 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
合計:
本日: