目次: C言語とlibc
Linuxはプロセスの起動時に、引数や環境変数に加えてAuxiliary Vector(補助ベクタ−)というデータを渡します。ダンプするにはLD_SHOW_AUXVを定義してから起動すると良いです。
$ LD_SHOW_AUXV=1 /bin/echo AT_SYSINFO_EHDR: 0x7ffec8bcc000 AT_??? (0x33): 0x6f0 AT_HWCAP: 178bfbff AT_PAGESZ: 4096 AT_CLKTCK: 100 AT_PHDR: 0x5650e2ab4040 AT_PHENT: 56 AT_PHNUM: 11 AT_BASE: 0x7f5d30b65000 AT_FLAGS: 0x0 AT_ENTRY: 0x5650e2ab6980 AT_UID: 1000 AT_EUID: 1000 AT_GID: 1000 AT_EGID: 1000 AT_SECURE: 0 AT_RANDOM: 0x7ffec8bb3499 AT_HWCAP2: 0x2 AT_EXECFN: /bin/echo AT_PLATFORM: x86_64
補助ベクターは基本的には存在するかしないか定かではなく任意です。が、どうもglibcはいくつかの補助ベクターの存在を前提としていて、存在しない場合は起動すらしないようです。
補助ベクターを変更するスマートな方法がありそうですが、わかりませんでした。仕方ないのでデバッガを使って力尽くで書き換えます。まずはテストプログラムを書きます。mainに到達する前のことを扱うためプログラムの中身は何でも良いです。
// a.c
#include <stdio.h>
int main(int argc, char *argv[], char *envp[])
{
printf("hello\n");
return 0;
}
実行してmainでブレークし、環境変数の配列envpをダンプします。envpの終端はNULLポインタですが、実はその先に補助ベクターが置かれています。
$ gcc -Wall -g -O0 -static a.c $ gdb a.out ... (gdb) b main Breakpoint 1 at 0x4017d8: file a.c, line 5. (gdb) r Starting program: /home/katsuhiro/share/a/a.out Breakpoint 1, main (argc=1, argv=0x7fffffffdcf8, env=0x7fffffffdd08) at a.c:5 5 printf("hello\n"); (gdb) x/32x envp 0x7fffffffdd08: 0xffffe039 0x00007fff 0xffffe049 0x00007fff 0x7fffffffdd18: 0xffffe05c 0x00007fff 0xffffe070 0x00007fff 0x7fffffffdd28: 0xffffe089 0x00007fff 0xffffe09f 0x00007fff 0x7fffffffdd38: 0xffffe0b3 0x00007fff 0xffffe495 0x00007fff 0x7fffffffdd48: 0xffffe4a9 0x00007fff 0xffffe4d8 0x00007fff 0x7fffffffdd58: 0xffffe503 0x00007fff 0xffffe50c 0x00007fff 0x7fffffffdd68: 0xffffe534 0x00007fff 0xffffe549 0x00007fff 0x7fffffffdd78: 0xffffe55e 0x00007fff 0xffffe571 0x00007fff (...略...) 0x7fffffffde88: 0xffffefb1 0x00007fff 0x00000000 0x00000000 ★★envpの終端★★ 0x7fffffffde98: 0x00000021 0x00000000 0xf7ffd000 0x00007fff ★★補助ベクターの始まり★★ ...
補助ベクターは下記のような構造です。
typedef struct
{
long int a_type; /* Entry type */
union
{
long int a_val; /* Integer value */
void *a_ptr; /* Pointer value */
void (*a_fcn) (void); /* Function pointer value */
} a_un;
} auxv_t;
簡単に言えばx86_64の場合8バイトのtypeと、8バイトのunionが並んでいるだけです。最初のデータ(アドレス0x7fffffffde98)を例に取るとtype = 0x21, data = 0x7fff_f7ff_d000です。わかりやすいですね。
デバッガから起動する場合はargv, envp, 補助ベクターのアドレスは常に同じです。そのため、
このような手順を取ってglibcが補助ベクターを見る前に補助ベクターの値を好きに変更できます。試しにglibcが起動時に参照していてNULLにすることが許されないAT_RANDOMを書き換えます。AT_RANDOMはtype = 0x19です。
(gdb) b _start Breakpoint 2 at 0x4016a0 (gdb) r The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /home/katsuhiro/share/a/a.out Breakpoint 2, 0x00000000004016a0 in _start () (gdb) x/32x 0x7fffffffde98 0x7fffffffde98: 0x00000021 0x00000000 0xf7ffd000 0x00007fff 0x7fffffffdea8: 0x00000033 0x00000000 0x000006f0 0x00000000 (...略...) 0x7fffffffdf98: 0x00000019 0x00000000 0xffffdfe9 0x00007fff ★★書き換え対象AT_RANDOM★★ 0x7fffffffdfa8: 0x0000001a 0x00000000 0x00000002 0x00000000 0x7fffffffdfb8: 0x0000001f 0x00000000 0xffffefce 0x00007fff 0x7fffffffdfc8: 0x0000000f 0x00000000 0xffffdff9 0x00007fff 0x7fffffffdfd8: 0x00000000 0x00000000 0x00000000 0x00000000 ★★補助ベクターの終端type = 0★★ 0x7fffffffdfe8: 0xf8e1f900 0x056adb4e 0xb6df71ae 0x67d4fca2 ... (gdb) set *0x7fffffffdfa0=0 (gdb) set *0x7fffffffdfa4=0 (gdb) x/32x 0x7fffffffdf98 0x7fffffffdf98: 0x00000019 0x00000000 0x00000000 0x00000000 ★★NULLポインタに書き換えた★★ 0x7fffffffdfa8: 0x0000001a 0x00000000 0x00000002 0x00000000 0x7fffffffdfb8: 0x0000001f 0x00000000 0xffffefce 0x00007fff ... (gdb) c Continuing. Program received signal SIGSEGV, Segmentation fault. 0x00000000004032fd in __libc_start_main ()
実行を継続するとmainに到達することなくSEGVでクラッシュします。glibcは起動時にAT_RANDOMの指す配列を参照していて、アドレスをNULLポインタに書き換えたからです。glibcはLinux専用のlibcですから、AT_RANDOMが存在しない場合を考慮する必要はないとはいえ、なんか微妙な動作ですね……うーん。
目次: 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レジスタは「スレッドポインタ」レジスタといって、まさにスレッドローカルストレージのためのレジスタです。
次回はスレッドポインタがいつどこで初期化されるかを見たいと思います。
< | 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 |
合計:
本日: