コグノスケ


link 未来から過去へ表示(*)  link 過去から未来へ表示

link もっと前
2022年4月19日 >>> 2022年4月10日
link もっと後

2022年4月18日

glibcのスレッドローカルストレージ - 変数アドレス編

目次: 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を返す)。

スレッドローカル変数のアドレス取得、詳細

実際はどのようにアドレスを得るのでしょう?下記のようにコードを書き換えて、

改造後のコード(スレッドローカル変数にNULLを代入)

// 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/22 15:44)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2022年4月15日

glibcのスレッドローカルストレージ - 変数宣言編

目次: C言語とlibc

スレッドごとに独立したデータを保持する空間をTLS(Thread Local Storage)といいます。日本語だと「スレッド局所記憶」というそうです。TLSへのアクセス方法、TLSの初期化方法などはアーキテクチャ依存ですので、今回はglibcのRISC-V版の実装を観察します。

スレッドローカル変数の宣言

変数宣言は __libc_tsd_define() マクロを使います。例として文字処理に使われるctype関連の変数を見ます。

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つです。

スレッドローカル変数の宣言、詳細

これだけだと「そうですか」で終わってしまうので、もう少し詳しく調べます。

  • __thread: スレッドローカル変数であることを示します。
  • tls_model("initial-exec"): スレッドローカル変数のモデル、実行可能ファイルから参照可能な変数とします。

スレッドローカル変数のモデルは、Common Variable Attributes - Using the GNU Compiler Collection (GCC) を見る限り4種類あるようです。

  • global-dynamic
  • local-dynamic
  • initial-exec
  • local-exec

それぞれの意味は悲しいことにGCCのマニュアルに書いていないのです。どうして……。参考になるマニュアルとしては、-ftls-model (-qtls) - XL C/C++ for Linux - IBM Documentation もしくは スレッド固有ストレージのアクセスモデル - Oracle Solaris 11.1リンカーとライブラリガイドがわかりやすいです。

global-dynamic
すべてのスレッドローカル変数を参照できます。
local-dynamic
共有ライブラリのスレッドローカル変数を参照できますが、モジュール外からの参照は不可能です。global-dynamicより速いとのこと。
initial-exec
実行可能ファイルから共有ライブラリのスレッドローカル変数を参照できますが、実行可能ファイルと同時にロードされている必要があります(dlopenでロードしたライブラリは不可)。local-dynamicより速いとのこと。
local-exec
実行可能ファイル内のスレッドローカル変数のみ参照できます。initial-execより速いとのこと。

Solarisのリンカーのモデル名はGCCと少し違いますが、一見して対応がわかる程度の差でしょう。

次回は変数のアドレス取得について見たいと思います。

編集者:すずき(2022/04/22 03:01)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2022年4月13日

C言語とlibc - まとめリンク

目次: C言語とlibc

C言語について。

Cライブラリ(libc)について。

編集者:すずき(2024/01/13 17:22)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2022年4月10日

glibcとAuxiliary Vector

目次: C言語とlibc

Linuxはプロセスの起動時に、引数や環境変数に加えてAuxiliary Vector(補助ベクタ−)というデータを渡します。ダンプするにはLD_SHOW_AUXVを定義してから起動すると良いです。

Auxiliary Vectorのダンプ
$ 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ポインタですが、実はその先に補助ベクターが置かれています。

ビルド、envをダンプして補助ベクターの位置を調べる
$ 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, 補助ベクターのアドレスは常に同じです。そのため、

  • mainでブレーク
  • envpの後ろの補助ベクターのアドレスを調べる
  • 同じ実行ファイルを再実行
  • プロセス開始直後に実行される関数(_start)でブレーク
  • アドレスがわかっている補助ベクターをデバッガで書き換え

このような手順を取ってglibcが補助ベクターを見る前に補助ベクターの値を好きに変更できます。試しにglibcが起動時に参照していてNULLにすることが許されないAT_RANDOMを書き換えます。AT_RANDOMはtype = 0x19です。

AT_RANDOMを書き換える
(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が存在しない場合を考慮する必要はないとはいえ、なんか微妙な動作ですね……うーん。

編集者:すずき(2022/04/22 03:00)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



link もっと前
2022年4月19日 >>> 2022年4月10日
link もっと後

管理用メニュー

link 記事を新規作成

<2022>
<<<04>>>
-----12
3456789
10111213141516
17181920212223
24252627282930

最近のコメント5件

  • link 24年4月22日
    hdkさん (04/23 20:52)
    「おお... うちのHHFZ4310より後...」
  • link 20年6月19日
    すずきさん (04/06 22:54)
    「ディレクトリを予め作成しておけば良いです...」
  • link 20年6月19日
    斎藤さん (04/06 16:25)
    「「Preferencesというメニューか...」
  • link 21年3月13日
    すずきさん (03/05 15:13)
    「あー、このプログラムがまずいんですね。ご...」
  • link 21年3月13日
    emkさん (03/05 12:44)
    「キャストでvolatileを外してアクセ...」

最近の記事3件

  • link 24年4月22日
    すずき (04/23 20:13)
    「[仕事部屋の照明が壊れた] いきなり仕事部屋のシーリングライトが消えました。蛍光管の寿命にしては去年(2022年10月19日の...」
  • link 24年4月17日
    すずき (04/18 22:44)
    「[VSCodeとMarkdownとPlantUMLのローカルサーバー] 目次: LinuxVSCodeのPlantUML Ex...」
  • link 23年4月10日
    すずき (04/18 22:30)
    「[Linux - まとめリンク] 目次: Linuxカーネル、ドライバ関連。Linuxのstruct pageって何?Linu...」
link もっとみる

こんてんつ

open/close wiki
open/close Linux JM
open/close Java API

過去の日記

open/close 2002年
open/close 2003年
open/close 2004年
open/close 2005年
open/close 2006年
open/close 2007年
open/close 2008年
open/close 2009年
open/close 2010年
open/close 2011年
open/close 2012年
open/close 2013年
open/close 2014年
open/close 2015年
open/close 2016年
open/close 2017年
open/close 2018年
open/close 2019年
open/close 2020年
open/close 2021年
open/close 2022年
open/close 2023年
open/close 2024年
open/close 過去日記について

その他の情報

open/close アクセス統計
open/close サーバ一覧
open/close サイトの情報

合計:  counter total
本日:  counter today

link About www.katsuster.net
RDFファイル RSS 1.0

最終更新: 04/23 20:52