__attribute__)((init_priority(N)))(で、C++のグローバル変数の初期化順序を制御する

g++の __attribute__)((init_priority(N)))( なる機能の日本語の解説が見当たらないので、いつものように紹介文を書いてみます。これは、C++グローバル変数の初期化順序を制御するためのGCC拡張機能(attribute)です。

ごく短い説明


動的な初期化が必要なC++グローバル変数の定義に__attribute__)((init_priority(N)))(をくっつけると、.oファイル上で、その変数の初期化関数(コンストラクタ等)のアドレスが.ctorsではなく.ctors.M セクションに入ります。M は左0パディング5桁固定の数値で、M = 65535 - N です。あとは、shinhさんの2005/11の記事を。特に、リンカスクリプトのSORTのとこに注目、これにより初期化順がinit_priorityで指定した通りになります。Nが小さい程先に初期化と。


以上。

長い説明, static initialization order から


C++で、コンストラクタ呼び等で動的に初期化されるかもしれないグローバル変数*1、たとえば次のような変数ですが、

  • int gFoo = ::bar(); のように、関数の戻り値で初期化される変数
  • CFoo gFoo; のように、コンストラクタで初期化されるオブジェクト

このようなグローバル変数が複数あって、の間に依存関係(どれかがどれかを読み書きしている)があると、プログラムの実行がmainに到達する前に、プログラムがSEGVって死んだり誤動作したりすることがあります。リンク順を変えたり、いったんmake cleanすると再現しなくなったりする、やや嫌な部類の問題です。


今回は、そのSEGV現象を__attribute__でどうにか誤魔化す話です。ありびゅー。確か以前にも日記のどこかに書いてますけど、例えば死ぬのはこんな例:

// a.h
struct A {
  A() : x_(new int()) {}
  void foo() { *x_ = 1; }
  int* x_;
};

クラスAは、コンストラクタでx_用のメモリを確保してますので、もしコンストラクタより先にfoo()が呼ばれてしまうSEGVります。

// a.cpp
#include "a.h"
A a;

aをグローバル変数として確保しています。

#include "a.h"

struct B {
  B() {
    extern A a;
    a.foo();
  }
};
B b;

int main() {}

クラスBのコンストラクタはグローバル変数aに依存しています。


さて、これを二種類の方法でコンパイル・リンクしてみます*2

% g++ -m32 -o good b.cpp a.cpp
% ./good 

% g++ -m32 -o bad  a.cpp b.cpp
% ./bad 
zsh: segmentation fault  ./bad

見ての通り、リンクをどういう順序で行うかによって、aのコンストラクタよりbのコンストラクタが先に走ってしまい(badのほう)、SEGVします。なぜなら、B::B() は a.foo() を呼んでいて、そのときまだaに対してA::A()は呼ばれておらず、a.foo()の中で未初期化のポインタx_を参照(*x_ = 1)してしまうからです。C++の規格では、コンパイル単位をまたいだグローバル変数の初期化順序は規定されていませんので、g++に責任はありません。この初期化順が不定な問題は、init_priority属性を使うと手軽に解決できます。

A a __attribute__((init_priority(101)));
    ...
B b __attribute__((init_priority(102)));

グローバル変数a,bの定義部分をそれぞれ上記のようにすればOKです。どういう順序でリンクしても問題が発生しなくなります。

普通の対策と無理矢理な対策


なお、ふつー init_priority なんていうGCC拡張など使わずに、

  • 動的な初期化を伴うグローバル変数なんて使うな
  • どうしても使いたいなら "construct on first use" idiom で対処

とかすると思います。たぶん。グローバル変数aのかわりに、フリー関数aを準備、みたいなidiomですね。このFAQの10.13から10.16に書いてあります*3。これの日本語訳である「C++ FAQ 第2版 - C++プログラミングをきわめるためのQ&A集」にも書いてあると思われます。他のトリックもあると思いますが略。


まーそれはともかく、init_priority拡張の話を続けます。真っ先に初期化したいことがわかりきってるオブジェクト(std::coutに相当するものを自作とか)がある場合なんかはinit_priorityは普通に便利だと思うです。

ほんとに b のコンストラクタが先に走っているのか(__do_global_ctors_auxについて)


bad では、ほんとにaよりbのコンストラクタが先に走っているんでしょうか?printfしたら負けという俺ルールで確認してみましょう。そもそも、だれがグローバル変数のコンストラクタを呼んでくれるんでしたっけ? このへんもobjdump付の日本語解説がそれほど多くなさげなので書きます。えー、仕組みをざっと。good を使ってみてみます。ELFの実行バイナリには、.ctorsというセクションがあります。readelfかobjdumpで見てみます。

% objdump -sj .ctors good 
Contents of section .ctors:
 804970c ffffffff b8840408 2a850408 00000000  ........*.......

ここで、ffffffff と 00000000 は、順に

% nm --numeric-sort good | grep __CTOR
000000000804970c d __CTOR_LIST__
0000000008049718 d __CTOR_END__

__CTOR_LIST__ と __CTOR_END__ と呼ばれるシンボル(の値)です。この間に挟まれた数字は、mainの実行前に呼ぶべき関数のアドレスです。__attribute__((constructor)) で登録した関数のアドレスもここに入ります。いま、.ctorsセクションに含まれているのはどの関数のアドレスなのでしょう?確認します。

% objdump -sj .ctors good 
Contents of section .ctors:
 804970c ffffffff b8840408 2a850408 00000000  ........*.......

のアドレスを、エンディアン(今はEL, リトルです)に注意しつつnmで探しますと、

% nm -C good | grep 080484b8
00000000080484b8 t global constructors keyed to b
% nm -C good | grep 0804852a
000000000804852a t global constructors keyed to a

だそうです。nm -C でdemangle指定をしているので、"global constructors keyed to X" とpretty print されていますが、生のシンボル 「_GLOBAL__I.なんちゃら_X」が見たければ-Cをはずしてください。えー、ともかく、

  • b8840408 は、B::B() を呼び出す関数のアドレス
  • 2a850408 は、A::A() を呼び出す関数のアドレス

ということですね。では、この "global constructors keyed to X" 関数を呼んでくださるのは誰なのでしょうか?gdb上で確認します。

% gdb good
...
Breakpoint 1, 0x08048548 in A::A ()
(gdb) backtrace
#0  0x08048548 in A::A ()
#1  0x08048527 in __static_initialization_and_destruction_0 ()
#2  0x0804853f in global constructors keyed to a ()
#3  0x0804860b in __do_global_ctors_aux ()
#4  0x08048369 in _init ()
#5  0x08048599 in __libc_csu_init ()
#6  0x002dded1 in __libc_start_main () from /lib/libc.so.6
#7  0x080483f1 in _start ()

__do_global_ctors_aux () という関数のようです。これはgccのcrtbegin.oの中の関数です。"global constructors keyed to a" と "global constructors keyed to b" のどちらが先に呼ばれるのでしょうか? GCCソースコードで確認します。

// gcc-4.1.x/gcc/crtstuff.c

#ifdef OBJECT_FORMAT_ELF
static void __attribute__((used))
__do_global_ctors_aux (void)
{
  func_ptr *p;
  for (p = __CTOR_END__ - 1; *p != (func_ptr) -1; p--)
    (*p) ();
}

わかりやすいですね。__CTOR_END__ 側から、objdumpの表示順とは逆順に呼ばれるっぽいです。一応objdumpでgoodを逆アセンブルして、再確認します。

% nm good | grep __do_global 
00000000080485f0 t __do_global_ctors_aux
0000000008048420 t __do_global_dtors_aux

% objdump -d --start-address=0x80485f0 good
Disassembly of section .text:

080485f0 <__do_global_ctors_aux>:
 80485f0:       55                      push   %ebp
 80485f1:       89 e5                   mov    %esp,%ebp
 80485f3:       53                      push   %ebx
 80485f4:       bb 14 97 04 08          mov    $0x8049714,%ebx
 80485f9:       83 ec 04                sub    $0x4,%esp
 80485fc:       a1 14 97 04 08          mov    0x8049714,%eax
 8048601:       83 f8 ff                cmp    $0xffffffff,%eax
 8048604:       74 0c                   je     8048612 <__do_global_ctors_aux+0x22>
 8048606:       83 eb 04                sub    $0x4,%ebx
 8048609:       ff d0                   call   *%eax
 804860b:       8b 03                   mov    (%ebx),%eax
 804860d:       83 f8 ff                cmp    $0xffffffff,%eax
 8048610:       75 f4                   jne    8048606 <__do_global_ctors_aux+0x16>
 8048612:       83 c4 04                add    $0x4,%esp
 8048615:       5b                      pop    %ebx
 8048616:       5d                      pop    %ebp
 8048617:       c3                      ret    

$0xffffffff、つまり__CTOR_LIST__とcmpしているあたりとか、sub $0x4 しているあたりに着目。逆順になってます(説明になっていない)。以上により、good では、A::A()が先に呼ばれ、そのあとB::B()が呼ばれるとわかります。


次にbadのほう。脳内エンディアン変換が面倒なので、objdumpではなくて、.ctorsのELFファイル上のオフセットを調べてodすることにします。odの出力の03414(8進数)はオフセットの0x70cのことです。アドレス以外は --format で16進数表示にしてあります。と、ここで Binary Hacks ―ハッカー秘伝のテクニック100選 の Hack #4 (by ukaiさん) を見たら、オフセット表示も od -A x で16進にできることがわかりましたが書き直すのが面倒なのでこのままで....。

% readelf -S bad | egrep -e 'Nr|.ctors' 
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [17] .ctors            PROGBITS        0804970c 00070c 000010 00  WA  0   0  4

% od --skip-bytes=0x70c -N 0x10 --format=x4 bad 
0003414 ffffffff 0804849e 08048528 00000000

% nm -C bad | egrep '(0804849e|08048528)'
000000000804849e t global constructors keyed to a
0000000008048528 t global constructors keyed to b

08048528 が先に呼ばれるのだから、B::B() が先ということですな。ということで、bad がSEGVる理由が確認できました。

とりあえずこの問題を解決してみる (init_priorityのつかいかた)


a.cppとb.cppの、グローバル変数の宣言部分をそれぞれ次のように変更します。init_priority(N)のNの部分の数字が「優先度」で、101以上、65535以下の数値を書けます。小さい数字を指定した変数が先に初期化されるようになります。だから、次の例だと、常にbよりもaが先に初期化されると期待されます。

A a __attribute__((init_priority(101)));
B b __attribute__((init_priority(102)));

確認します。

% g++ -m32 -o good b.cpp a.cpp 
% ./good 

% g++ -m32 -o bad  a.cpp b.cpp 
% ./bad  (落ちない)

グッジョブ。落ちません。一応 goodとbadの .ctors の内容を確認しますと、

% od --skip-bytes=0x70c -N 0x10 --format=x4 good
0003414 ffffffff 080484b4 08048522 00000000

% nm -C good | egrep -e '(080484b4|08048522)'
0000000008048522 t _GLOBAL__I.00101_a
00000000080484b4 t _GLOBAL__I.00102_b
% od --skip-bytes=0x70c -N 0x10 --format=x4 bad 
0003414 ffffffff 08048520 0804849a 00000000
0003434
% nm -C bad | egrep -e '(08048520|0804849a)'
000000000804849a t _GLOBAL__I.00101_a
0000000008048520 t _GLOBAL__I.00102_b

nmの-Cオプション(demangle)がちゃんと効いてなくて、pretty printされていないのがアレですが、good も bad も、aの初期化が先に行われることがわかります。badがbadではなくなりました。GJ.

init_priorityのしくみを見てみる


g++の単なる利用者としてはここまで理解していれば問題ないんですが、どういうtrickで初期化順が保証されるのかを追います。まぁそんなに難しくないです。bad がどうコンパイル,リンクされるかを中心に見ます。GNU ldのマニュアルの http://www.sra.co.jp/wingnut/ld/ld-ja_3.html#SEC29 を見れば、以下を読むまでもなく(わかるひとは)すぐわかるという話もあります。

% g++ -m32 -c a.cpp b.cpp

a.cpp と b.cpp をコンパイルして、a.o と b.o を得ます。この2つの.oをリンクするとgoodまたはbadというバイナリができるわけですが、それはまだしません。リンクするかわりに、a.o と b.o のセクション名を見てみます。

% readelf -S a.o | grep .ctors
  [ 6] .ctors.65434      PROGBITS        00000000 00007c 000004 00  WA  0   0  4
% readelf -S b.o | grep .ctors
  [ 7] .ctors.65433      PROGBITS        00000000 00009c 000004 00  WA  0   0  4

すると、init_priority を指定しなかったときには数字なしの単なる .ctors だった*4セクション名が、なにやら謎の数値付きのものになっています。まぁ、ほぼ自明ですが、この数字は init_priority で指定した優先度をNとして、「65535 マイナス N」な数字になっております。a.o と b.o の数字が一つ違いなのはNが違うからです(N=101と102)。ちなみに、.ctors.MMMMM セクションに格納されているのは関数のアドレスだけです(セクションのサイズが000004になっていますよね)。開始終了マークはありません。これらはcrtbegin.o, crtend.oに格納されているのです、後述。ひとまず、「関数のアドレスだけ」な証拠を:

% od --skip-bytes=0x7c -N 0x4 --format=x4 a.o
0000174 00000026
% nm -C a.o | grep 00000026 
0000000000000026 t _GLOBAL__I.00101_a

この通り。さて、この2つをリンクしてみます。

% g++ -g3 -m32 -o hoge a.o b.o
% readelf -S hoge | grep .ctors
  [17] .ctors            PROGBITS        0804970c 00070c 000010 00  WA  0   0  4

数字がなくなり、単なる.ctorsセクションになりました。理由は、GNU ldが使用するデフォルトのリンカスクリプトを ld -verbose で見ればわかります。リンカスクリプトというのは、リンク対象の .o ファイルの様々なセクションを、リンク後のバイナリにどう取り込むか(など)を指定する設定ファイルです。ld -T filename でお好きなスクリプトを使うこともできます*5。デフォルトのはこうです:

% ld --verbose
...
  .ctors          :
  {
...
    KEEP (*(EXCLUDE_FILE (*crtend.o *crtend?.o ) .ctors))
    KEEP (*(SORT(.ctors.*)))
...
  }

これは、

  1. まず、*crtend.o *crtend?.o 以外の *.o の .ctors セクションを順序指定なしでかき集め、リンク後のELFバイナリの.ctorsセクションにぶちこめ。
  2. 次に、全ての *.o の .ctors.* セクションをセクション名をキーに昇順に(SORTという文字に注目)かき集めて、リンク後のELFバイナリの.ctorsセクションにぶちこめ

という指示です。これに従うと、b.oの.ctors.65433が先にぶちこまれ、次に a.oの.ctors.65434 がぶちこまれます(SORT、なので)。最初の方で見た通り、.ctors に先に現れる関数が後に呼ばれますので、aが先に初期化されるわけです。めでたしめでたし。


さらに、リンカスクリプトを見る限り、「同じ優先度の変数同士の初期化順は、リンクしてみないとわからない」ことと、スクリプトに KEEP (*(EXCLUDE_FILE (*crtend.o *crtend?.o ) .ctors)) が先に書かれていることから、「init_priority指定のない変数と、init_priority(65535) の変数では、後者の初期化の方が先に行われる」こともわかります。ただこの2つはinfoに明記されているわけではないので(今日時点)、将来万一デフォルトのリンカスクリプトの内容が変化し、動作が変化してしまっても文句は言えないかもしれません。なお、__CTOR_LIST__ (0xffffffff) と __CTOR_END__ (0x00000000) は、crtbegin.o と crtend.o から取り込まれます。リンカスクリプト

  .ctors          :
  {
    KEEP (*crtbegin.o(.ctors))   // __CTOR_LIST__ 取り込み
    KEEP (*crtbegin?.o(.ctors))  // __CTOR_LIST__ 取り込み
    KEEP (*(EXCLUDE_FILE (*crtend.o *crtend?.o ) .ctors))
    KEEP (*(SORT(.ctors.*)))
    KEEP (*(.ctors))             // __CTOR_END__  取り込み
  }

最初の2行と最後の1行で取り込んでいる...のでしょう。__CTOR_LIST__の取り込みが2行になってる理由はワカリマセン。確かにLISTとENDを取り込んでいる証拠:

% nm -C /usr/lib/gcc/x86_64-redhat-linux/4.1.1/32/crtbegin.o | grep __CTOR_
0000000000000000 d __CTOR_LIST__

% readelf -S /usr/lib/gcc/x86_64-redhat-linux/4.1.1/32/crtbegin.o | grep .ctors
  [ 6] .ctors            PROGBITS        00000000 000098 000004 00  WA  0   0  4

% od --skip-bytes=0x98 -N 4 --format=x4 /usr/lib/gcc/x86_64-redhat-linux/4.1.1/32/crtbegin.o   
0000230 ffffffff

LISTについては上記、ENDについては次の通りcrtend.oですが、以下同様なので略。

% nm /usr/lib/gcc/x86_64-redhat-linux/4.1.1/32/crtend.o | grep __CTOR_
0000000000000000 d __CTOR_END__

自分でlinker scriptを書くときは、0x00000000と0xffffffffくらいはlinker script内に即値で埋めちゃっても良いと思います。たぶん。

この仕組みの乱用 (section, used と constructor)


えー、まず、__attribute__((constructor)) は __attribute__((section)) の syntactic sugar だということが、このへんまで追えばわかるですね。下記コードで、foo()とbar()はほぼ同じ実行結果をもたらすということです:

__attribute__((constructor))
void foo() {  std::puts(__PRETTY_FUNCTION__); }

void bar() {  std::puts(__PRETTY_FUNCTION__); }
__attribute__((used, section(".ctors"))) static void (*ptr)() = bar;

used属性も忘れずに。つけないと、g++先生が最適化でptr変数をばっさり捨ててしまうかも。でまぁ、これを応用すると、init_priority(N)属性で許されているNは101-65535(inclusive)なわけですが、N=101より優先して関数を呼んでもらうことができるわけです。

// N=1相当
__attribute__((used, section(".ctors.65534"))) static void (*q)() = bar;
// N=0よりも優先
__attribute__((used, section(".ctors.a"))) static void (*r)() = bar;

自分で勝手にセクション名に数字を付け加えたり、数字ではないものを付け加えたりしてます。何に使えるのかは不明...。将来も大丈夫かどうかは、デフォルトのリンカスクリプトがどうなるかによります。__attributeではなくて、shinhさんが2005年にかかれているように、objcopy --rename-section を使う方法でもよいとおもわれます。

コンストラクタが呼ばれるまでの道のりの補足


コンストラクタが呼ばれるまでの道のりは、冒頭に書いた通り、

% gdb good
...
(gdb) bt
#0  A (this=0x804986c) at a.h:3
#1  0x08048498 in __static_initialization_and_destruction_0 (__initialize_p=1, __priority=101) at a.cpp:2
#2  0x0804853f in global constructors keyed to a ()
#3  0x0804860b in __do_global_ctors_aux ()
#4  0x08048369 in _init ()
#5  0x08048599 in __libc_csu_init ()
#6  0x002dded1 in __libc_start_main () from /lib/libc.so.6
#7  0x080483f1 in _start ()

なわけですが、__do_global_ctors_aux() しか説明していませんでした。他に次の4つの関数が関与しているようです:

  • _init() は /usr/lib/crti.o (glibc-2.x/sysdeps/generic/initfini.c ?)
  • __libc_csu_init() は /usr/lib/libc_nonshared.a (glibc-2.x/csu/elf-init.c)
  • __libc_start_main() は、上にも書いてある通り /lib/libc.so.6 (glibc-2.x/csu/libc-start.c)
  • _start() は /usr/lib/crt1.o (glibc-2.x/sysdeps/i386/elf/start.S)

ですね。BinaryHacksの#25でも、b2con2006の資料でもこのへんは説明を端折ったので、(私はあまり詳しいわけでもないですが)少し補足します。


まず、__libc_start_main 以外は実行バイナリ自体に含まれています。逆アセンブル例:

% nm hoge | grep __libc_csu_init           
0000000008048580 T __libc_csu_init

% objdump -d --start-address=0x8048580 hoge
08048580 <__libc_csu_init>:
 8048580:       55                      push   %ebp
 8048581:       89 e5                   mov    %esp,%ebp
(略)
 80485e8:       c3                      ret    

_initは.initセクションに含まれているので、nmで論理アドレスを調べるまでもなく % objdump -dj .init hoge で逆アセンブル可能です。__libc_csu_init が含まれている libc_nonshared.a は、g++ -v でhogeにどのような .o や .a がリンクされるかを観察しても、一見登場しないのですが、 /usr/lib/libc.so (何気にテキストファイル) の、GROUP文

% cat /usr/lib/libc.so
OUTPUT_FORMAT(elf32-i386)
GROUP ( /lib/libc.so.6 /usr/lib/libc_nonshared.a  AS_NEEDED ( /lib/ld-linux.so.2 ) )

経由でひっそりリンクされています。strace -f g++ すれば確かにリンクされているのがわかることでしょう。_startは、実行バイナリのエントリポイント(ELFヘッダのe_entry値)ですね。

% readelf -h hoge | grep Entry
  Entry point address:               0x80483d0
% nm hoge | grep 80483d0
00000000080483d0 T _start

エントリとはいっても、もちろん .interp の方に先に制御が移り、そのあとこのe_entryなわけですが。詳細は略。greeの勉強会の資料をご参照ください。

__static_initialization_and_destruction_0 と _GLOBAL__I.ほげ_X


__static_initialization_and_destruction_0 とか "global constructors keyed to a" は、.oの時点で含まれています。これは g++ -S あるいは g++ -save-temps で .s ファイルを吐くか、.o をnmすれば確認できます。のでまぁ、g++が勝手に生成すると。たとえばこんな感じの関数になっています。g++ -S してc++filtしたものです。

% c++file < a.s | less
...
__static_initialization_and_destruction_0(int, int):
.LFB6:
        pushl   %ebp
.LCFI3:
        movl    %esp, %ebp
.LCFI4:
        subl    $24, %esp
.LCFI5:
        movl    %eax, -4(%ebp)
        movl    %edx, -8(%ebp)
        cmpl    $1, -4(%ebp)
        jne     .L7
        cmpl    $101, -8(%ebp)
        jne     .L7
        movl    $a, (%esp)
        call    A::A()
.L7:
        leave
        ret

(略)

_GLOBAL__I.00101_a:
.LFB7:
        pushl   %ebp
.LCFI6:
        movl    %esp, %ebp
.LCFI7:
        subl    $8, %esp
.LCFI8:
        movl    $101, %edx
        movl    $1, %eax
        call    __static_initialization_and_destruction_0(int, int)
        leave
        ret

スタックではなく、eaxおよびedxレジスタを使って引数を渡している、つまり __attribute__)((regparm(2)))( 相当になってるのがちょっと目を引くくらいで、あとは特になにもしていないというか、A::A()を呼んでるだけというか。 A::A()に渡すthisのアドレスは__static_initialization_and_destruction_0が保持しています。eax (ここでは1) は、__initialize_p という名前の引数で、__static_initialization_and_destruction_0() にオブジェクトの構築をさせたいのか、解体をさせたいのかの指示のようです。でも、特別なコンパイルオプションを指定しない限りは、手元のgcc3/4で__initialize_pが0で呼ばれることはありませんでした。構築時に__static_initialization_and_destruction_0()が、解体関数を__cxa_atexit()に渡し、そっちに解体を任せてしまうので、呼んでもらう必要がないです。[http://sourceware.org/ml/libc-alpha/2005-08/msg00097.html:title=g++に、-fno-use-cxa-atexitというオプションを渡した時]だけ、"global destructors keyed to a" なる関数が .o に突っ込まれ、それが__static_initialization_and_destruction_0()を__initialize_p==0で呼ぶようになりました。

080484e2 <_GLOBAL__D.00101_a>:                                   <=== IではなくてD 
 80484e2:       55                      push   %ebp
 80484e3:       89 e5                   mov    %esp,%ebp
 80484e5:       83 ec 08                sub    $0x8,%esp
 80484e8:       ba 65 00 00 00          mov    $0x65,%edx
 80484ed:       b8 00 00 00 00          mov    $0x0,%eax         <=== ぜろ
 80484f2:       e8 ad ff ff ff          call   80484a4 <__static_initialization_and_destruction_0(int, int)>   <== このcall先でデストラクタ呼び
 80484f7:       c9                      leave
 80484f8:       c3                      ret

このように。この関数のアドレスは、(いままではカラだった).dtorsセクションに格納されていて、__do_global_dtors_aux() によってアドレスが拾われ、呼ばれます。gccのinfoを見るに*6、なんかまずいことがあるから__cxa_atexit()方式がいまは使われているのだと思うのですが、なにがまずいのかは追いきれませんでした。どなたか教えてくださいませ。


TIPS: g++ -S ではなく、コンパイル済の.oをobjdumpで逆汗して確認するときは、objdump -Cdr a.o のようにしましょう(>だれとなく)。-C しないとdemangleされませんし、-r しないと、なにをcallしているのか関数名ががわかりませんので。くだー。です。

DSOをまたいだ初期化順


init_priorityの番号による初期化順の制御は、単一の.so内、および単一の実行ファイル内でのみ効くようですね。複数の.soをまたいだ、あるいは.soと実行ファイル間の初期化順序はこのattributeでは制御できないようです。せいぜい、.soの初期化関数が先に呼ばれて、実行ファイルの初期化関数が最後に呼ばれる*7ことが期待される、というくらいか。力尽きてきていまはソースコード読む元気が出ないのであとで。

memo, 余談1


リンカスクリプトと言えば、LMAとVMAをずらすという話が面白い(かも)しれません。組み込み機器等で、初期値ありのグローバル変数をROM(LMAで指定したアドレス)に配置するときに使う技ですね。スタートアップルーチンで、ROM上の変数をRAM(VMAで指定したアドレス)にコピーしてからプログラムの実行を開始して辻褄を合わせます。このあたりは、ldのマニュアルにも一通り解説があります

memo、余談2


ELFの特定セクションのダンプは,

% readelf -Wx .ctors good
Hex dump of section '.ctors':
  0x08049674 00000000 080484ee 08048488 ffffffff ................

でも良い筈だけど、見てわかるように表示がおかしい。表示順が真逆。バグ? x86_64でx86のELFをダンプしているから? FC6/x86_64。とりあえず保留。

まとめ


説明が趣味とはいえ、長い。あと、長いわりによくわからない。かなり自分用メモ。でもこれだけキーワードを散りばめておけばいつかだれかの役に立つかもしれないので気にしない方向で。

*1:名前空間有効範囲の静的記憶域期間をもつオブジェクトのこと、ですが長いので以下グローバル変数と略記

*2:私はx86_64なカーネルの上で作業して(32bitなバイナリを作って)います。x86カーネルの方はg++に-m32を渡す必要はありません

*3:main到達前にマルチスレッドになるコードだと(どんなんだよ)、これだとまずいので注意してくださいw。Javaみたいな、クラスローダー様がロック獲得してほげ、とか一切ありませんので

*4:自分で確認してみてください

*5:組み込み機器をいじる際にはリンカスクリプトを自分で書くことが多いです

*6:% info gcc --index-search="fuse-cxa-atexit"

*7:もちろん.soをdlopenした場合は除きます