コグノスケ


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

link もっと前
2022年4月22日 >>> 2022年4月9日
link もっと後

2022年4月22日

glibcのスレッドローカルストレージ - TLS初期化編

目次: C言語とlibc

前回はtp(スレッドポインタ)レジスタの初期化を調べました。今回はTLSの初期化を見たいと思います。起動時に値が設定されている変数の初期化に関わる部分です。

TLSの初期化

初期化関数 __libc_setup_tls() ではtpの指す先(__sbrk() で確保したメモリ領域)も初期化しています。しかしこちらは結構複雑です。大雑把に言うと、

  • Auxiliary vectorのAT_PHDR(実行プログラムのプログラムヘッダーへのポインタ)を探す
  • もし存在しなければTLSの初期化は行わない
  • プログラムヘッダーからPT_TLSタイプのセグメントを探す
  • もし存在しなければTLSの初期化は行わない
  • PT_TLSタイプのセグメントのp_vaddrが指す位置からp_fileszバイトの初期データをTLSにコピー

このような処理が行われます。コードも見ておきましょう。

AT_PHDRを探す処理

// 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;
PT_TLSエントリ探しと、初期データをTLSにコピー

// 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となるはずです。

readelfの結果
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セクションを挟む形にすると、

TLS関係のセクションが分散している場合のリンクエラー
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/22 15:43)

コメント一覧

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



2022年4月21日

ソーシャルメディアと年代

SNSの利用者層を語るときTwitterはオジサンばかり、若者はInstagramなどと言われますが、SNSの利用率は総務省が調査していて、情報通信メディアの利用時間と情報行動に関する調査 - 総務省から見ることができます。


ソーシャルメディア利用率(令和2年)

令和2年のデータだけを抜き出してグラフにしてみました。結構世代で違うもんですね。

  • 万人向け: LINE, YouTube
  • 若者向け(左が高い): Instagram, Tiktok, ニコ動
  • オジサン化進行中: Twitter
  • オジサン化(真ん中〜右が高い): Facebook
  • 老人化(右が高い): なし

今のところの傾向としてはこんなもんでしょうか。大体イメージ通りの結果でしたが、個人的に意外だったのはニコニコ動画です。ここまで利用者層が若返っているのは意外でした。

私は日本語圏のゲーム系動画を見るならなんだかんだでニコ動が一番面白いと思うので、いまだにプレミアム会員のまま使っていますが、利用者の私ですらもうニコ動には懐ゲー好きのオジサンしかいないのかな?と思っていたくらいでした……。

一昔前、ニコ動のプレミアム会員数が減り始めたくらいから「オワコン」呼ばわりする人達が増えた記憶がありますが、復活したんですね。以前のように皆が戻って来ると良いですねえ。

編集者:すずき(2022/05/20 05:20)

コメント一覧

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



2022年4月20日

glibcのスレッドローカルストレージ - スレッドポインタ編

目次: C言語とlibc

前回はスレッドローカル変数のアドレス取得部分のコードを見て、tp(スレッドポインタ)レジスタが重要な役割をしていることがわかりました。今回はtpレジスタの初期化コードを見ます。

tpレジスタの初期化

レジスタの初期化は __libc_setup_tls() で行われます。少し長いですがコードを見ましょう。

TLSのメモリ領域確保とtpレジスタ初期化

// 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の初期化を見たいと思います。

編集者:すずき(2022/04/22 15:43)

コメント一覧

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



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 この記事にコメントする



2022年4月9日

glibcと最適化

目次: C言語とlibc

たまに最適化を無効にするとビルドできなくなるソフトウェアがあります。大抵はバグです。しかし何か目的があって、あえてやっている場合もあります。C言語標準ライブラリの実装で有名なglibcもその一つです。

最適化をわざと無効にしてglibcをビルドするとエラーが発生します。コードはglibc-2.35を使いました。

O2とO0でglibcをビルドした場合
$ cd glibc
$ mkdir _build
$ cd _build

$ ../configure CPPFLAGS="-O2 -g" --disable-sanity-checks
$ make

(成功する、ログは省略)

$ cd ../
$ rm -rf _build
$ mkdir _build
$ cd _build

$ ../configure CPPFLAGS="-O0 -g" --disable-sanity-checks
$ make

In file included from <command-line>:
./../include/libc-symbols.h:75:3: error: #error "glibc cannot be compiled without optimization"
   75 | # error "glibc cannot be compiled without optimization"
      |   ^~~~~

最適化を無効にするなと怒られました。コードを見ると__OPTIMIZE__ マクロの定義/未定義を見て検出していました。へえー。

最適化の有無を検出している場所

// glibc/include/libc-symbols.h

/* Some files must be compiled with optimization on.  */
#if !defined __ASSEMBLER__ && !defined __OPTIMIZE__
# error "glibc cannot be compiled without optimization"
#endif

このチェックをごまかして、最適化を有効にしつつも特定の最適化だけ無効にするとどうなるでしょう?

O2でインライン展開のみ無効にした場合
$ cd glibc
$ mkdir __build
$ cd __build

$ ../configure CPPFLAGS="-O2 -g -fno-inline" --disable-sanity-checks --disable-werror
$ make

(...略...)

gcc   -nostdlib -nostartfiles -r -o glibc/__build/elf/librtld.os '-Wl,-(' glibc/__build/elf/dl-allobjs.os glibc/__build/elf/rtld-libc.a -lgcc '-Wl,-)' \
          -Wl,-Map,glibc/__build/elf/librtld.os.map
gcc   -nostdlib -nostartfiles -shared -o glibc/__build/elf/ld.so.new         \
          -Wl,-z,combreloc -Wl,-z,relro -Wl,--hash-style=both -Wl,-z,defs      \
          glibc/__build/elf/librtld.os -Wl,--version-script=glibc/__build/ld.map         \
          -Wl,-soname=ld-linux-x86-64.so.2                      \
          -Wl,-defsym=_begin=0
/usr/bin/ld: glibc/__build/elf/librtld.os: in function `_dl_close_worker':
glibc/elf/dl-close.c:383: undefined reference to `malloc'
/usr/bin/ld: glibc/elf/dl-close.c:689: undefined reference to `free'
/usr/bin/ld: glibc/elf/dl-close.c:691: undefined reference to `free'
/usr/bin/ld: glibc/elf/dl-close.c:693: undefined reference to `free'
/usr/bin/ld: glibc/elf/dl-close.c:701: undefined reference to `free'
/usr/bin/ld: glibc/elf/dl-close.c:710: undefined reference to `free'
/usr/bin/ld: glibc/__build/elf/librtld.os:glibc/elf/dl-close.c:715: more undefined references to `free' follow
/usr/bin/ld: glibc/__build/elf/librtld.os: in function `_dl_map_object_deps':
glibc/elf/dl-deps.c:438: undefined reference to `malloc'
/usr/bin/ld: glibc/elf/dl-deps.c:479: undefined reference to `malloc'
/usr/bin/ld: glibc/elf/dl-deps.c:571: undefined reference to `malloc'
/usr/bin/ld: glibc/elf/dl-deps.c:543: undefined reference to `malloc'
/usr/bin/ld: glibc/__build/elf/librtld.os: in function `_dl_error_free':
glibc/elf/dl-exception.c:41: undefined reference to `free'
/usr/bin/ld: glibc/__build/elf/librtld.os: in function `__GI__dl_exception_create':
glibc/elf/dl-exception.c:89: undefined reference to `malloc'

(...略...)

/usr/bin/ld: glibc/elf/../sysdeps/posix/getcwd.c:249: undefined reference to `malloc'
/usr/bin/ld: glibc/elf/../sysdeps/posix/getcwd.c:493: undefined reference to `free'
/usr/bin/ld: glibc/elf/../sysdeps/posix/getcwd.c:469: undefined reference to `realloc'
/usr/bin/ld: glibc/__build/elf/librtld.os: in function `__alloc_dir':
glibc/elf/../sysdeps/unix/sysv/linux/opendir.c:115: undefined reference to `malloc'
/usr/bin/ld: glibc/elf/../sysdeps/unix/sysv/linux/opendir.c:115: undefined reference to `malloc'
/usr/bin/ld: glibc/__build/elf/librtld.os: in function `__closedir':
glibc/dirent/../sysdeps/unix/sysv/linux/closedir.c:50: undefined reference to `free'
/usr/bin/ld: glibc/__build/elf/librtld.os: in function `scratch_buffer_free':
glibc/malloc/../include/scratch_buffer.h:86: undefined reference to `free'
/usr/bin/ld: glibc/__build/elf/librtld.os: in function `__libc_scratch_buffer_set_array_size':
glibc/malloc/scratch_buffer_set_array_size.c:51: undefined reference to `malloc'
/usr/bin/ld: glibc/__build/elf/librtld.os: in function `__strdup':
glibc/string/strdup.c:42: undefined reference to `malloc'
collect2: error: ld returned 1 exit status
make[2]: *** [Makefile:1234: glibc/__build/elf/ld.so] エラー1
make[2]: ディレクトリ 'glibc/elf' から出ます
make[1]: *** [Makefile:483: elf/subdir_lib] エラー2
make[1]: ディレクトリ 'glibc' から出ます
make: *** [Makefile:9: all] エラー2

リンク時に激しく怒られてしまいダメでした。小細工は効きませんね。

O2以外の最適化レベル

デバッグ時に見やすくするためにOgやO1といった最適化レベルを使うことは珍しくないと思いますが、glibcはOgやO1だとおかしなビルドエラーが発生します。

Ogでビルドするとエラー
$ ../configure CPPFLAGS="-Og -g" --disable-sanity-checks

(...略...)

canonicalize.c: In function ‘realpath_stk’:
canonicalize.c:424:50: error: ‘dest’ may be used uninitialized in this function [-Werror=maybe-uninitialized]
  424 |   return scratch_buffer_dupfree (rname_buf, dest - rname);
      |                                             ~~~~~^~~~~~~
cc1: all warnings being treated as errors

警告がエラー扱いされただけなので、configureに --disable-werrorを渡して警告をエラーと見なさないようにするとビルドは成功します(実は前節でもしれっと使っていました)。

しかし良く考えるとO2では無警告なのにOgでは警告が出るとは?なかなか興味深い現象です。警告が出ているrealpath_stk() のコードを見ます。

realpath_stk() のコード

// glibc/stdlib/canonicalize.c

static char *
realpath_stk (const char *name, char *resolved,
              struct scratch_buffer *rname_buf)
{
  char *dest;
  char const *start;
  char const *end;
  int num_links = 0;

  //...

  struct scratch_buffer extra_buffer, link_buffer;
  scratch_buffer_init (&extra_buffer);
  scratch_buffer_init (&link_buffer);
  scratch_buffer_init (rname_buf);
  char *rname_on_stack = rname_buf->data;
  char *rname = rname_on_stack;
  bool end_in_extra_buffer = false;
  bool failed = true;

  /* This is always zero for Posix hosts, but can be 2 for MS-Windows
     and MS-DOS X:/foo/bar file names.  */
  idx_t prefix_len = FILE_SYSTEM_PREFIX_LEN (name);

  if (!IS_ABSOLUTE_FILE_NAME (name))    //★★この条件が成立、かつ★★
    {
      while (!__getcwd (rname, rname_buf->length))    //★★この条件が成立、かつ★★
        {
          if (errno != ERANGE)    //★★この条件が不成立、かつ★★
            {
              dest = rname;
              goto error;
            }
          if (!scratch_buffer_grow (rname_buf))    //★★この条件が成立した場合を考えると★★
            goto error_nomem;    //★★destが未定義のまま、このgotoに到達する★★
          rname = rname_buf->data;
        }
      dest = __rawmemchr (rname, '\0');
      start = name;
      prefix_len = FILE_SYSTEM_PREFIX_LEN (rname);
    }

  //...

error_nomem:
  scratch_buffer_free (&extra_buffer);
  scratch_buffer_free (&link_buffer);

  if (failed || rname == resolved)    //★★しかし、前半のfailed = trueの条件が必ず成立していて、未定義変数の使用は発生しない★★
    {
      scratch_buffer_free (rname_buf);
      return failed ? NULL : resolved;
    }

  return scratch_buffer_dupfree (rname_buf, dest - rname);    //★★ここで未定義変数の使用の警告が出る★★
}

コード上はdestが未定義のまま使われる可能性があるように見えますし、Ogの警告はもっともに思えます。しかしコードを良く見るとdestが未定義のままerror_nomemに来る場合、failed = trueが必ず成立して警告が出る行には到達しません。他にもgoto error_nomemする箇所はありますが、同様に必ずfailed = trueが成立します。

O2だとif文は必ず成立する場合(異常パス)と、しない場合(正常パス)でコードが複製されるようです。if文が成立する側のコードでは、if文の後は不要 = 未使用コードを消去 = dest - rnameというコードそのものがなかったことになる、というメカニズムが働いて警告が出ないのでしょう。たぶん。

GCCの警告は大体合っていますが、これはコンパイラが誤警告を出してしまうちょっと珍しい例でした。個人的には紛らわしいコードを書くんじゃねえ!と思いますが、歴史あるコードですし……何か理由があってこうなっているんでしょう。

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

コメント一覧

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



link もっと前
2022年4月22日 >>> 2022年4月9日
link もっと後

管理用メニュー

link 記事を新規作成

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

最近のコメント5件

  • link 21年9月20日
    すずきさん (11/19 01:04)
    「It was my pleasure.」
  • link 21年9月20日
    whtさん (11/17 23:41)
    「This blog solves my ...」
  • link 24年10月1日
    すずきさん (10/06 03:41)
    「xrdpで十分動作しているので、Wayl...」
  • link 24年10月1日
    hdkさん (10/03 19:05)
    「GNOMEをお使いでしたら今はWayla...」
  • link 24年10月1日
    すずきさん (10/03 10:12)
    「私は逆にVNCサーバーに繋ぐ使い方をした...」

最近の記事3件

  • link 23年4月10日
    すずき (11/15 23:48)
    「[Linux - まとめリンク] 目次: Linux関係の深いまとめリンク。目次: RISC-V目次: ROCK64/ROCK...」
  • link 24年11月6日
    すずき (11/15 23:47)
    「[Ubuntu 24.04 LTS on ThinkPad X1 Carbon Gen 12] 目次: Linux会社ではTh...」
  • link 24年11月11日
    すずき (11/15 23:26)
    「[Pythonのテストフレームワーク] 目次: Python最近Pythonを触ることが増えたのでテストについて調べようと思い...」
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

最終更新: 11/19 01:04