プログラムをビルドする際には、色々なオプションをつけて実行することが多いですが、先日 -lluajit-5.1 -Wl,-E
というオプションを見ました。
この、-Wl,-E
というオプションがなんなのかわからなかったため、調査しました。この記事ではその調査結果を記載しています。
調査対象はコマンドのオプションなので、まずは、コマンドのマニュアルを見てみましょう。
このオプションを使ってビルドするときに使ったコマンドは gcc
だったので、gcc
のマニュアルを確認しました。
私の環境(Debian GNU/Linux 10.5)のマニュアルでは、 -Wl,-E
というオプションは以下のように説明されていました。
-Wl,option
オプション option をリンカに渡します。option がコンマを含む場合は、それらのコンマで複数のオプションとして分割されます。
つまり、-Wl,-E
は、-Wl,option
というオプションでoption
の部分にリンカーへのオプションを指定します。
したがって、-Wl,-E
はリンカーに-E
というオプションを渡すという意味になります。
ということで、次は、リンカーのマニュアルを参照してみましょう。
このときのリンカーは、GNU ld
を使っていたので、GNU ld
のマニュアルを参照します。
私の環境(Debian GNU/Linux 10.5)のマニュアルでは、 -E
オプションは以下のように説明されていました。
(英語のマニュアルのほうが情報が多かったので、英語のマニュアルを参照しました。)
-E
--export-dynamic
--no-export-dynamic
When creating a dynamically linked executable, using the -E option or the --export-dynamic option causes the linker to add all symbols to the
dynamic symbol table. The dynamic symbol table is the set of symbols which are visible from dynamic objects at run time.
If you do not use either of these options (or use the --no-export-dynamic option to restore the default behavior), the dynamic symbol table will
normally contain only those symbols which are referenced by some dynamic object mentioned in the link.
If you use "dlopen" to load a dynamic object which needs to refer back to the symbols defined by the program, rather than some other dynamic
object, then you will probably need to use this option when linking the program itself.
You can also use the dynamic list to control what symbols should be added to the dynamic symbol table if the output format supports it. See the
description of --dynamic-list.
Note that this option is specific to ELF targeted ports. PE targets support a similar function to export all symbols from a DLL or EXE; see the
description of --export-all-symbols below.
注目するのは以下の説明です。
When creating a dynamically linked executable, using the -E option or the --export-dynamic option causes the linker to add all symbols to the dynamic symbol table.
The dynamic symbol table is the set of symbols which are visible from dynamic objects at run time.
(動的にリンクされた実行ファイルを作成する時に、すべてのシンボルを動的シンボルテーブルに加えます。
動的シンボルテーブルは、実行時に動的オブジェクトから見えるシンボルのテーブルです。)
If you use "dlopen" to load a dynamic object which needs to refer back to the symbols defined by the program, rather than some other dynamic object,
then you will probably need to use this option when linking the program itself.
(他の動的オブジェクトではなく、そのプログラムで定義されているシンボルを参照し返す必要のある動的オブジェクトを
dlopen で ロードする場合は、おそらくプログラム自身をリンクする時にこのオプションを 使う必要があるでしょう。)
あまり、ピンときませんね! なので、実際に小さなプログラムを作って実験してみましょう。
では、「他の動的オブジェクトではなく、そのプログラムで定義されているシンボルを参照し返す必要のある動的オブジェクト」を考えてみましょう。 これは、例えば以下のようなケースです。
まず、extendable-program
というプログラムがあるとします。
このプログラム用のプラグインとしてextendable-program-plugin.so
という動的ライブラリーがあるとします。
以下のように、extendable-program-plugin.so
はextendable-program
に動的にロードして使われることを前提としています。
そして、extendable-program-plugin.so
は、extendable-program
のバージョンを取得する必要があるとします。
(例えば、extendable-program
のバージョンによって提供する機能を切り替えたりするのに使います。)
バージョンの取得は、extendable-program
に自身のバージョンを返す関数があり、extendable-program-plugin.so
からそれを呼び出すことで実現します。
このときに、-E
オプションをつけてextendable-program
がビルドされていると、以下のように、extendable-program
のシンボルが動的シンボルテーブルに登録されるので、extendable-program-plugin.so
からextendable-program
のシンボルが参照できるようになります。
では、実際にプログラムを作って上記の例を実験してみましょう。 実験用のプログラムは以下の通りです。
extendable-program
-
extendable-program.h
#pragma once typedef void (*ep_plugin_init_func)(void); extern int ep_get_major_version(void); extern int ep_get_minor_version(void); extern int ep_get_micro_version(void);
-
extendable-program.c
#include <stdio.h> #include <stdlib.h> #include <dlfcn.h> #include "extendable-program.h" enum Verion { MAJOR = 1, MINOR = 0, MICRO = 2 }; int ep_get_major_version(void) { return MAJOR; } int ep_get_minor_version(void) { return MINOR; } int ep_get_micro_version(void) { return MICRO; } int main(int argc, char **argv) { void *plugin = dlopen("./extendable-program-plugin.so", RTLD_NOW | RTLD_LOCAL); printf("plugin: %p\n", plugin); if (!plugin) { return EXIT_FAILURE; } ep_plugin_init_func ep_plugin_init = dlsym(plugin, "ep_plugin_init"); printf("ep_plugin_init: %p\n", ep_plugin_init); if (!ep_plugin_init) { dlclose(plugin); return EXIT_FAILURE; } ep_plugin_init(); dlclose(plugin); return EXIT_SUCCESS; }
extendable-program-plugin
-
extendable-program-plugin.c
#include <stdio.h> #include "extendable-program.h" extern void ep_plugin_init(void); void ep_plugin_init(void) { printf("ep-plugin-init: %d.%d.%d\n", ep_get_major_version(), ep_get_minor_version(), ep_get_micro_version()); }
Makefile
all: extendable-program-with-E
all: extendable-program-without-E
all: extendable-program-plugin.so
clean:
rm -f extendable-program-with-E
rm -f extendable-program-without-E
rm -f extendable-program-plugin.so
extendable-program-with-E: extendable-program.c extendable-program.h
$(CC) -o $@ -Wl,-E extendable-program.c -ldl
extendable-program-without-E: extendable-program.c extendable-program.h
$(CC) -o $@ extendable-program.c -ldl
extendable-program-plugin.so: extendable-program-plugin.c extendable-program.h
$(CC) -shared -o $@ extendable-program-plugin.c
以上が実験用のプログラムです。 さっそく実行してみましょう。
$ make
cc -o extendable-program-with-E -Wl,-E extendable-program.c -ldl
cc -o extendable-program-without-E extendable-program.c -ldl
cc -shared -o extendable-program-plugin.so extendable-program-plugin.c
$ ./extendable-program-without-E
plugin: (nil)
$ ./extendable-program-with-E
plugin: 0x55eead4df290
ep_plugin_init: 0x7f2d5f4a4135
ep-plugin-init: 1.0.2
-E
をつけずにビルドしたプログラムextendable-program-without-E
では、plugin: (nil)
となっていて、extendable-program-plugin.so
のロードに失敗しています。
(dlopen
のオプションにRTLD_NOW
が指定されているため、extendable-program-plugin.so
内で参照しているシンボルが解決できない場合はdlopen
が失敗します。したがって、これはextendable-program-plugin.so
で使用しているシンボルが解決できなかったことになります。)
つまり、-E
オプションをつけないと以下のような状態になります。
-E
オプションの説明には、「全てのシンボルを動的シンボルテーブルに追加する」と書かれていました。
つまり、-E
オプションをつけることで、extendable-program
のシンボルが全て動的シンボルテーブルに追加され、extendable-program-plugin
から参照できるようになります。
実際に、-E
オプションをつけてビルドしたextendable-program-with-E
では、extendable-program-plugin.so
がロードされ正常にextendable-program
のバージョンが取得できていることが確認できます。
このように、実行プログラム側で提供しているシンボルを動的ライブラリー側から参照する必要がある場合に-E
オプションが必要だということがわかりました。
-E
オプションの応用
-E
オプションは「全てのシンボルを動的シンボルテーブルに追加する」とマニュアルに記載されていました。これは、静的ライブラリーをリンクして使われることを想定しているLuaJITのようなプログラムにも応用できます。
どう応用できるのかを、先程の実験に使用したプログラムを改造して説明します。
まず、新しく静的ライブラリーlibadd.a
を追加します。これは、2値の整数を可算する関数(add_int()
)と2値の実数を加算する関数(add_double()
)を提供しています。
今回の例ではextendable-program
はこのlibadd.a
を静的にリンクしていることを前提とし、libadd.a
で提供する関数をextendable-program-plugin.so
からも使うこととします。
つまり、以下のような実装になります。
libadd.a
-
add.c
int add_int(int a, int b); int add_int(int a, int b) { return a + b; } double add_double(double a, double b); double add_double(double a, double b) { return a + b; }
extendable-program
-
extendable-program.h
#pragma once typedef void (*ep_plugin_init_func)(void); typedef void (*ep_plugin_aggregate_func)(void); extern int ep_get_major_version(void); extern int ep_get_minor_version(void); extern int ep_get_micro_version(void); extern int add_int(int a, int b);
extendable-program
-
extendable-program.c
#include <stdio.h> #include <stdlib.h> #include <dlfcn.h> #include "extendable-program.h" enum Verion { MAJOR = 1, MINOR = 0, MICRO = 2 }; int ep_get_major_version(void) { return MAJOR; } int ep_get_minor_version(void) { return MINOR; } int ep_get_micro_version(void) { return MICRO; } int main(int argc, char **argv) { int sum = add_int(3, 4); printf("sum=%d\n", sum); void *plugin = dlopen("./extendable-program-plugin.so", RTLD_NOW | RTLD_LOCAL); printf("plugin: %p\n", plugin); if (!plugin) { return EXIT_FAILURE; } ep_plugin_init_func ep_plugin_init = dlsym(plugin, "ep_plugin_init"); printf("ep_plugin_init: %p\n", ep_plugin_init); if (!ep_plugin_init) { dlclose(plugin); return EXIT_FAILURE; } ep_plugin_aggregate_func ep_plugin_aggregate = dlsym(plugin, "ep_plugin_aggregate"); printf("ep_plugin_aggregate: %p\n", ep_plugin_aggregate); if (!ep_plugin_aggregate) { dlclose(plugin); return EXIT_FAILURE; } ep_plugin_init(); ep_plugin_aggregate(); dlclose(plugin); return EXIT_SUCCESS; }
extendable-program-plugin
-
extendable-program-plugin.c
#include <stdio.h> #include "extendable-program.h" extern void ep_plugin_init(void); extern double add_double(double a, double b); extern void ep_plugin_aggregate(void); void ep_plugin_init(void) { printf("ep-plugin-init: %d.%d.%d\n", ep_get_major_version(), ep_get_minor_version(), ep_get_micro_version()); } void ep_plugin_aggregate(void) { printf("ep-plugin-aggregate: %g\n", add_double(1.5, 2.2)); }
Makefile
all: libadd.a
all: extendable-program-plugin.so
all: extendable-program-with-E
all: extendable-program-without-E
clean:
rm -f extendable-program-with-E
rm -f extendable-program-without-E
rm -f extendable-program-plugin.so
rm -f libadd.a
libadd.a: add.c
$(CC) -c add.c && ar rcs $@ add.o
extendable-program-with-E: extendable-program.c extendable-program.h
$(CC) -o $@ -Wl,-E extendable-program.c libadd.a -ldl
extendable-program-without-E: extendable-program.c extendable-program.h
$(CC) -o $@ extendable-program.c libadd.a -ldl
extendable-program-plugin.so: extendable-program-plugin.c extendable-program.h
$(CC) -shared -o $@ extendable-program-plugin.c
では、さっそく実行してみましょう。
$ make
cc -c add.c && ar rcs libadd.a add.o
cc -shared -o extendable-program-plugin.so extendable-program-plugin.c
cc -o extendable-program-with-E -Wl,-E extendable-program.c ./libadd.a -ldl
cc -o extendable-program-without-E extendable-program.c ./libadd.a -ldl
$ ./extendable-program-without-E
sum=7
plugin: (nil)
$ ./extendable-program-with-E
sum=7
plugin: 0x55c12f1636a0
ep_plugin_init: 0x7f800c18c145
ep_plugin_aggregate: 0x7f800c18c17e
ep-plugin-init: 1.0.2
ep-plugin-aggregate: 3.7
-E
をつけずにビルドしたプログラムでは、sum=7
と出力されているので、add_int
関数は正常に呼び出すことができています。
静的にリンクしたライブラリーの関数は正常に使えました。
extendable-program-plugin.so
はシンボルの解決ができないのでロードに失敗します。
つまり、以下のような状態になります。
-E
オプションをつけてビルドしたプログラムでは、ep-plugin-aggregate
が実行できています。
-E
は「全てのシンボルを動的シンボルテーブルに追加する」ので、静的にリンクされたライブラリーが持っているシンボルも動的シンボルテーブルに追加されるため、libadd.a
のシンボルをextendable-program-plugin.so
から参照できるのです。
つまり、以下のような状態になります。
このように、-E
をつけてビルドすることで、静的なライブラリーで実装されている関数を動的にロードされたライブラリーから参照できるということもわかりました。
特定の静的ライブラリーがリンクされることが前提のプログラムなら、-E
を用いて静的ライブラリーが持つ機能を動的ライブラリーで使うことができます。