実行結果がリンクの順番に依存するコード
はじめに
以前リンカのお仕事という記事を書きました。関数やグローバル変数のアドレスは、コンパイル時にはとりあえずラベルとして定義され、そのアドレスはリンク時にリンカが決めます。その際、リンクの順番によりアドレスが変わります。その結果、リンクの順番により実行結果が変わるようなコードが書けます。それについてちょっとだけ説明しようと思います。
グローバル変数
関数の外で宣言された変数はグローバル変数となります。何も指定しなければ、それはグローバルシンボルとなり、別のファイルからもアクセスできます。コンパイラに「実体が別のオブジェクトファイルにあるよ」と教えるにはextern
宣言を使います。
main.cpp
#include <cstdio>
extern int a, b;
int main() {
printf("a: %p\n", &a);
printf("b: %p\n", &b);
}
a
、b
の実体をそれぞれa.cpp
、b.cpp
に書いてやりましょう。
int a = 1;
int b = 1;
コンパイル、実行します。
$ g++ main.cpp a.cpp b.cpp
$ ./a.out
a: 0x55b27b601010
b: 0x55b27b601014
a
とb
のアドレスの下3桁が010
と014
になっていることに注意しましょう。readelf
でa.out
を見てみます。
$ readelf -s a.out | grep OBJECT
(snip)
51: 0000000000004014 4 OBJECT GLOBAL DEFAULT 25 b
(snip)
63: 0000000000004010 4 OBJECT GLOBAL DEFAULT 25 a
a
とb
のアドレスが4010、4014と、下3桁がさっきの結果と一致していますね。
さて、これらのグローバル変数のアドレスを決めるのはリンカで、リンカは原則として渡されたオブジェクトファイルの順番通りにくっつけるので、順序を入れ替えるとアドレスも入れ替わります。
$ g++ main.cpp b.cpp a.cpp
$ ./a.out
a: 0x564c18c36014
b: 0x564c18c36010
a
とb
のアドレスの順序が入れ替わりました。
なお、グローバル変数をそのファイルの中でのみ使い、他のオブジェクトファイルから参照されたくない場合はstatic
をつけます。こんなc.cpp
ファイルを作ってみましょう。
#include <cstdio>
static int c = 1;
void printc() {
printf("c: %p\n", &c);
}
static
宣言をしたので、このグローバル変数c
は外から参照できないはずです。確かめてみましょう。main.cpp
をこのように書き換えます。
#include <cstdio>
extern int a, b, c;
int main() {
printf("a: %p\n", &a);
printf("b: %p\n", &b);
printf("c: %p\n", &c);
}
コンパイルは問題なくできますが、リンク時に怒られます。
$ g++ -c main.cpp a.cpp b.cpp c.cpp
$ g++ main.o a.o b.o c.o
/usr/bin/ld: main.o: in function `main':
main.cpp:(.text+0x3b): undefined reference to `c'
collect2: error: ld returned 1 exit status
しかし、c
はグローバル変数なので、どこかのアドレスにはあります。それを表示させてみましょう。そのためにc.cpp
にprintc
という関数を用意しておきました。main.cpp
を以下のように書き換えます。
#include <cstdio>
extern int a, b;
void printc();
int main() {
printf("a: %p\n", &a);
printf("b: %p\n", &b);
printc();
}
コンパイル、実行してみましょう。
$ g++ -c main.cpp a.cpp b.cpp c.cpp
$ g++ main.o a.o b.o c.o
$ ./a.out
a: 0x5561e1a7d010
b: 0x5561e1a7d014
c: 0x5561e1a7d018
4バイトずつ順番に配置されていますね。readelfで見てみましょう。
$ readelf -s a.out | grep OBJECT
40: 0000000000004018 4 OBJECT LOCAL DEFAULT 25 _ZL1c
(snip)
53: 0000000000004014 4 OBJECT GLOBAL DEFAULT 25 b
(snip)
65: 0000000000004010 4 OBJECT GLOBAL DEFAULT 25 a
a
やb
にGLOBALという指定があるのに対して、c
は名前がマングリングされ、さらにLOCAL指定がついています。これにより、他のグローバル変数と同じ領域に配置されつつ、他のオブジェクトファイルから参照できないようになっています。当然、リンク順序を入れ替えれば場所も変わります。
$ g++ main.o c.o a.o b.o
$ ./a.out
a: 0x561f4bfce014
b: 0x561f4bfce018
c: 0x561f4bfce010
c,a,bの順番でリンクしたので、アドレスもc,a,bの順番に配置されています。
リンクの順序に依存して実行結果が変わるコード
グローバル変数のアドレスがリンクの順序に依存することを見ました。その結果、グローバル変数として定義されたクラスのインスタンスの初期化の順番もリンクの順序に依存することになります。それを見てみましょう。
関数を登録して、後でその関数をまとめて実行するためのクラスCaller
と、そのクラスに関数を登録するためのクラスCallee
を定義してみましょう。
まずはcaller.h
はこんな感じです。
#include <vector>
struct Callee;
class Caller {
public:
void add(Callee *c) {
v.push_back(c);
}
void call_all();
static Caller &get_instance();
private:
std::vector<Callee *> v;
Caller(){};
};
struct Callee {
virtual void func() = 0;
Callee() {
Caller::get_instance().add(this);
}
};
Callerクラスは、Calleeクラスのインスタンスの配列を持っていて、call_all
で登録された関数を全て呼び出します。また、シングルトンパターンにより、Caller
クラスのインスタンスを一つに限定し、Caller::get_instance()
で取得できるようにしておきます。Calleeクラスは、コンストラクタでCaller::get_instance()
を使ってCaller
のインスタンスを取得し、自分を登録します。
Callerクラスのインスタンスはcaller.cpp
に定義されたget_instance
関数のstaticなローカル変数として定義しておきます。call_all
もここで定義しておきましょう。
caller.cpp
#include "caller.h"
Caller &Caller::get_instance() {
static Caller c;
return c;
}
void Caller::call_all() {
for (auto c : v) {
c->func();
}
}
main関数では、Caller::get_instance().call_all()
を呼び出すだけです。
main.cpp
#include "caller.h"
int main() {
Caller::get_instance().call_all();
}
コンパイル、実行してみましょう。
$ g++ -c main.cpp caller.cpp
$ g++ main.o caller.o
$ ./a.out
何も登録していないので、何もおきません。
さて、関数を登録するためのファイルを作りましょう。こんなhoge1.cpp
を作ります。
#include "caller.h"
#include <cstdio>
struct Hoge1 : public Callee {
void func() {
puts("Hello from Hoge1!");
}
};
Hoge1 hoge1;
Calleeクラスを継承したHoge1クラスを作って、そのインスタンスをグローバル変数として定義しています。このHoge1 hoge1
が初期化された際、Callerクラスにこの関数が登録されます。コンパイル、リンクしてみましょう。コンパイルするのはhoge1.cpp
だけです。
$ g++ -c hoge1.cpp
$ g++ main.o caller.o hoge1.o
$ ./a.out
Hello from Hoge1!
main
関数を再コンパイルすることなく、新たにオブジェクトファイルをリンクするだけで、その実行結果を変えることができました。
関数は後からいくつでも追加できます。
hoge2.cpp
#include "caller.h"
#include <cstdio>
struct Hoge2 : public Callee {
void func() {
puts("Hello from Hoge2!");
}
};
Hoge2 hoge2;
$ g++ -c hoge2.cpp
$ g++ main.o caller.o hoge1.o hoge2.o
$ ./a.out
Hello from Hoge1!
Hello from Hoge2!
このように、グローバル変数の初期化をうまく使うことで、既存のコードを修正、再コンパイルすることなく、機能を追加することができます。
ただし、グローバル変数の初期化を利用しているため、実行結果は初期化の順番に依存します。
$ g++ main.o caller.o hoge2.o hoge1.o
$ ./a.out
Hello from Hoge2!
Hello from Hoge1!
初期化順序が守られないと困るコード
グローバル変数のアドレスはリンカが決めるため、リンクの順番に依存します。また、グローバル変数として定義されたインスタンスの初期化順序もリンクの順番に依存することになります。すると、例えばA
というインスタンスが初期化されてからB
というインスタンスが初期化されなければならないのに、リンクの順序が変わってバグる、なんてことがおきます。それを見てみましょう。ちょっとわざとらしいコードになっていますが、例ということで気にしないでください。
シングルトンパターンを実装したクラスAを用意します。こいつにはpublicな配列としてint *buf
があり、A::init
で初期化されることとしましょう。
a.h
#include <cstdio>
class A {
public:
static A &get_instance();
void init() {
buf = new int(10);
puts("A is initialized");
}
int *buf;
private:
A(){};
};
このシングルトンを初期化するため、A_initializer
というクラスを作り、そのコンストラクタでA::init
を呼び出すことにします。
a.cpp
#include "a.h"
#include <cstdio>
A &A::get_instance() {
static A a;
return a;
}
struct A_initializer {
A_initializer() {
A::get_instance().init();
}
};
A_initializer ai;
これにより、グローバル変数A_initializer
が初期化されたタイミングで、A
が初期化されることになります。
何もしないmain関数を作ります。
main.cpp
#include "a.h"
int main(){
}
コンパイル、実行してみましょう。
$ g++ main.cpp a.cpp
$ ./a.out
A is initialized
main関数は何にもしませんが、A_initiazlier
が初期化されたタイミングでA::init
が呼ばれています。
さて、このA::buf
を使うクラスを作って追加してみましょう。
b.cpp
#include "a.h"
struct B {
B() {
puts("Hello from B!");
A a = A::get_instance();
a.buf[3] = 12345;
}
};
B b;
コンストラクタでA
のインスタンスを手に入れ、A::buf
をさわるだけのコードです。コンパイル、実行します。
$ g++ main.cpp a.cpp b.cpp
$ ./a.out
A is initialized
Hello from B!
A
が初期化された後にB
が触りにいっているので問題はおきません。しかし、リンクの順番を変えると当然バグります。
$ g++ main.cpp b.cpp a.cpp
Hello from B!
zsh: segmentation fault ./a.out
A
が初期化される前にB
のインスタンスが作られ、それがA
を触りにいってしまったためです。
まとめ
分割コンパイル時のグローバル変数のアドレスはリンクの順番に依存し、それに伴ってグローバル変数として定義されたインスタンスの初期化順序もリンクの順序に依存します。リンクの順序に依存して振る舞いが変わるコードは、当然のことながら思わぬバグを引き起こします。例えばGNU MakefileをCMakeに置き換えたらバグった、ファイル名を変えただけでバグった、なんてイヤなバグり方をします。上記で挙げた例はあまり良くない例であり、現在はこういう書き方をする人は少ないと思いますが、たまに外部デバイスを触るコードなどでこういう書き方をするコードを見かけたりします。「リンクの順番を変えたら動作がおかしくなった」なんて現象に出会ったら、「グローバル変数の初期化順序の問題かも」と思えると、沼にハマる時間が短くなるかもしれません。本稿が、将来「沼」にハマった人の脱出に役立つことを願っています。
参考文献
- リンカのお仕事
- 実践的低レイヤプログラミングのリンカの章 リンカが何をしているか詳しく書いてあるのでざっと読んでおくと良いことがあるかも。
Discussion