■ コラム(7章のp.170のコラムを差し替え) 2.2 関数エントリの登録 複数の関数を配列に登録しておいて,あるタイミングで自動的に順に呼び出したい,と いったことはよくあります.例えばプログラムに複数の機能を搭載する際に,機能ごと になんらかの関数(初期化用関数など)を登録しておいて,プログラムの起動時に初期化 関数をまとめて呼び出したい場合などです.この登録作業をC言語の文法の範囲内で行 おうとすると,配列を自前で作成して,手作業で登録をするしか方法がありません. しかし,セクションの知識を利用することで,この登録作業を,自動で行わせることが できるようになります.このテクニックは,NetBSD の sysctl の初期化部分に利用され ているので,ここで紹介します. sysctl は,様々なカーネルパラメータを,MIBスタイ ルのデータベースとして読み書きできるようにしてあるものです.例えば, # sysctl net.inet.ip.forwarding=0 とすると,ルーティングが無効になり, # sysctl net.inet.ip.forwarding=1 で,有効になります.現在の値は, % sysctl net.inet.ip.forwarding で確認できます. リスト15は,NetBSD のソース中の,sysctl の初期化部分です (sys/kern/ kern_sysctl.c). =============================================================================== リスト15: sysctlの初期化部分(sys/kern/kern_sysctl.c より抜粋) =============================================================================== void sysctl_init(void) { sysctl_setup_func * const *sysctl_setup, f; /* ...中略... */ __link_set_foreach(sysctl_setup, sysctl_funcs) { /* * XXX - why do i have to coerce the pointers like this? */ f = (void*)*sysctl_setup; (*f)(NULL); } /* ...後略... */ } =============================================================================== リスト15中で利用されている __link_set_foreach() は,sys/sys/cdefs.h, sys/sys/ cdefs_elf.h でリスト16のように定義されています. =============================================================================== リスト16: __link_set_foreach() の定義(cdefs.h, cdefs_elf.h より抜粋) =============================================================================== #define __link_set_foreach(pvar, set) \ for (pvar = __link_set_start(set); pvar < __link_set_end(set); pvar++) #define __link_set_start(set) (__start_link_set_##set) #define __link_set_end(set) (__stop_link_set_##set) =============================================================================== よって,リスト15の __link_set_foreach() は,リスト17のように展開されます. =============================================================================== リスト17: sysctl_init() のマクロを展開 =============================================================================== for (sysctl_setup = __start_link_set_sysctl_funcs; sysctl_setup < __stop_link_set_sysctl_funcs; sysctl_setup++) { ... } =============================================================================== ここで,sysctl_setup は sysctl_init() 内部で定義されているローカル変数ですが, __start_link_set_sysctl_funcs, __stop_link_set_sysctl_funcs はグローバル変数で すから,ソース中のどこかで定義されていることになります.しかし,これらの変数を grep でいくら検索しても,実は見つかりません. また,sysctl 用の初期化関数ですが,これは NetBSD のソースを展開し, % grep SYSCTL_SETUP */*.c してみてください.SYSCTL_SETUP() というマクロを利用している部分が,たくさん引っ かかります.リスト18は,netinet/ip_input.c にある,net.inet.ip の初期化部分の例 です. =============================================================================== リスト18: net.inet.ip の初期化部分(netinet/ip_input.c より抜粋) =============================================================================== SYSCTL_SETUP(sysctl_net_inet_ip_setup, "sysctl net.inet.ip subtree setup") { /* 中略 */ } =============================================================================== SYSCTL_SETUP() は,sys/sys/sysctl.h で定義してあるマクロです (リスト19).リスト 19を見た限りでは,関数のプロトタイプ宣言をした後, __link_set_add_text() を行い ,最後に関数の定義を行なっているようです. =============================================================================== リスト19: SYSCTL_SETUPの定義(sys/sys/sysctl.h より抜粋) =============================================================================== #define SYSCTL_SETUP(name, desc) \ static void name(struct sysctllog **); \ __link_set_add_text(sysctl_funcs, name); \ static void name(struct sysctllog **clog) =============================================================================== 問題は __link_set_add_text() ですが,これは sys/sys/cdefs_elf.h で定義されてい ます(リスト20). =============================================================================== リスト20: link_set_add_text()の定義(sys/sys/cdefs_elf.h より抜粋) =============================================================================== #define __link_set_make_entry(set, sym) \ static void const * const __link_set_##set##_sym_##sym \ __section("link_set_" #set) __used = &sym /* ...中略... */ #define __link_set_add_text(set, sym) __link_set_make_entry(set, sym) #define __link_set_add_rodata(set, sym) __link_set_make_entry(set, sym) #define __link_set_add_data(set, sym) __link_set_make_entry(set, sym) #define __link_set_add_bss(set, sym) __link_set_make_entry(set, sym) =============================================================================== リスト20には,__used, __section("...") という部分があります.これらは sys/sys/ cdefs.h で定義されています (リスト21).ただし sys/cdefs.h を見る限り, __attribute__((__used__)) が有効になるのは gcc-3.1 以降を利用した場合のようです . =============================================================================== リスト21: __section(), __used の定義(sys/sys/cdefs.h より抜粋) =============================================================================== #define __used __attribute__((__used__)) /* ...中略... */ #define __section(x) __attribute__((__section__(x))) =============================================================================== リスト18の SYSCTL_SETUP(name, desc) を,リスト19, リスト20, リスト21の定義に沿 って展開すると,リスト22のようになります. =============================================================================== リスト22: net.inet.ip の初期化部分(SYSCTL_SETUP()を展開) =============================================================================== static void sysctl_net_inet_ip_setup(struct sysctllog **clog); static void const * const __link_set_sysctl_funcs_sym_sysctl_net_inet_ip_setup __attribute__((__section__("link_set_sysctl_funcs"))) __attribute__((__used__)) = &sysctl_net_inet_ip_setup; static void sysctl_net_inet_ip_setup(struct sysctllog **clog) { /* 中略 */ } =============================================================================== つまり,sysctl_net_inet_ip_setup() という関数と, __link_set_sysctl_funcs_sym_sysctl_net_inet_ip_setup という変数を定義しています .変数 __link_set_sysctl_funcs_sym_sysctl_net_inet_ip_setup には,初期値として &sysctl_net_inet_ip_setup が設定されていますから,この変数は関数 sysctl_net_inet_ip_setup() のアドレスを初期値として持つことになります. 変数 __link_set_sysctl_funcs_sym_sysctl_net_inet_ip_setup には, __attribute__((__section__("link_set_sysctl_funcs"))) __attribute__((__used__)) という属性が設定されています. __attribute__((__section__("..."))) は,変数や関数を "..." というセクションに配 置するという,gcc の拡張機能です (*10).リスト23のようにして利用します. =============================================================================== リスト23: __attribute__の利用例 =============================================================================== (関数の場合) int __attribute__((__section__("funcsec"))) func(int x) { ... } (変数の場合) int __attribute__((__section__("valuesec"))) value1 = 10; int value2 __attribute__((__section__("valuesec"))) = 20; =============================================================================== __attribute__ による属性指定については,info gcc の C Extensions の章に,リファ レンスがあります.また,本連載の第4回でも触れていますので,そちらも参考にして ください. また先述したように,__attribute__((__used__)) は gcc-3.1 以降から追加された拡張 機能のようです.なおこの部分は,以前は __attribute__((__unused__)) が使われてい ました. __unused__ は,その変数(や関数)は未使用であるとコンパイラに対して明示 するためのものです.通常はあまり意味を持ちませんが, -Wall オプションを指定して コンパイルを行った際に,未使用の static 変数や static 関数などに対して, "... defined but not used" のワーニングを抑制するという役割があります. 変数 __link_set_sysctl_funcs_sym_sysctl_net_inet_ip_setup は, __attribute__ (()) のセクション指定により,"link_set_sysctl_funcs" というセクションが作成され ,そこに配置されます.よって, "link_set_sysctl_funcs" というセクションには, SYSCTL_SETUP() によって定義された関数へのポインタが配列となって配置されることに なります.sysctl の初期化時には,これらの配列化された関数を順に呼び出せば,登録 したすべての関数が順に呼ばれることになります. ここでリスト17をもう一度見てください. __start_link_set_sysctl_funcs, __stop_link_set_sysctl_funcs という変数が参照されています.これは,関数へのポイ ンタの配列の先頭と末尾を表しているはずです.しかし,実はこれらの変数は,ソース コード中をいくら grep しても発見できません.リンカスクリプト中にも定義はありま せん.では,いったいどこで定義されているのでしょうか? リスト9の説明で先述したように,セクションを新たに作成すると, GNU ld によって __start_(セクション名),__stop_(セクション名) というシンボルがセクションの先頭 と末尾に自動的に追加されます.これは __attribute__((__section__())) によるセク ション作成のときも同様です.このため,セクション link_set_sysctl_funcs を作成し たことで, __start_link_set_sysctl_funcs, __stop_link_set_sysctl_funcs というシ ンボルが作成され(*11),これらのシンボルを参照することで, link_set_sysctl_funcs セクションの位置を知ることができるのです. sysctl の初期化時に呼び出してほしい関数は,単に SYSCTL_SETUP() を利用して定義す ればいいことになります.これをC言語の文法の範囲内で実現しようとすると,関数の 定義とは別に,関数へのポインタの配列を別に作成し,そちらにも登録するようなこと が必要になりますが,SYSCTL_SETUP() を利用すれば,自動的に独自セクションに登録さ れ,呼び出されるようになります. GNU ld の機能を利用することで,登録作業を不要 にしているわけです. 実験してみましょう.section.c (リスト24)を見てください. section.c では,init1 (), init2(), init3() の3つの関数を定義しています.さらに,それぞれの関数へのポ インタとして,fp1, fp2, fp3 を定義しています.これらのポインタには, __attribute__((__section__("test_section"))) を付加することで, test_section セ クションに配置します. =============================================================================== リスト24: 関数の配列化(section.c) =============================================================================== #include void init1() { printf("init1\n"); } void init2() { printf("init2\n"); } void init3() { printf("init3\n"); } void (*fp1)(void) __attribute__((__section__("test_section"))) = init1; void (*fp2)(void) __attribute__((__section__("test_section"))) = init2; void (*fp3)(void) __attribute__((__section__("test_section"))) = init3; extern void (*__start_test_section)(void); extern void (*__stop_test_section)(void); int main() { void (**fp)(void); printf("__start_test_section = 0x%08x\n", (int)(&__start_test_section)); printf("__stop_test_section = 0x%08x\n", (int)(&__stop_test_section)); for (fp = &__start_test_section; fp < &__stop_test_section; fp++) (**fp)(); exit (0); } =============================================================================== __start_test_section, __stop_test_section は extern 宣言されていますが,どこに もその本体の定義はありません.先に説明したように,これらのシンボルは, test_section という新規セクションを作成する際に,ld によって自動的に作成されま す.このため,定義はどこにも存在しません. (このことを知らないと,OSのカーネル を読むときなどに,定義されていないシンボルの定義を探して苦労するようなことにな ります) main() 関数の内部では, __start_test_section から __stop_test_section までを順 に関数へのポインタとして呼び出します. section.c をコンパイル・リンクして,実行形式 section を作成します. ------------------------------------------------------------------------------- % gcc section.c -Wall -o section % ------------------------------------------------------------------------------- 実行形式 section に対して,nm を実行してみます(リスト25). =============================================================================== リスト25: nm section の結果 =============================================================================== % nm section ...(中略)... 0804962c A __start_test_section 08049638 A __stop_test_section ...(中略)... 0804962c ? fp1 08049630 ? fp2 08049634 ? fp3 ...(後略)... =============================================================================== リスト25を見ると, __start_test_section, __stop_test_section というシンボルが作 成され, fp1, fp2, fp3 といったシンボルが,__start_test_section と __stop_test_section の間に配置されていることがわかります.さらに,objdump の結 果を見てみましょう (リスト26). =============================================================================== リスト26: objdump -h section の結果 =============================================================================== % objdump -h section section: file format elf32-i386 Sections: Idx Name Size VMA LMA File off Algn ...(中略)... 12 test_section 0000000c 0804962c 0804962c 0000062c 2**2 CONTENTS, ALLOC, LOAD, DATA ...(後略)... =============================================================================== リスト26では,test_section というセクションが新規に作成されています.また, test_section の開始アドレスは,__start_test_section の位置に一致しています. section を実行してみます. ------------------------------------------------------------------------------- % ./section __start_test_section = 0x0804962c __stop_test_section = 0x08049638 init1 init2 init3 % ------------------------------------------------------------------------------- init1(), init2(), init3() が順に呼ばれていることがわかります. ただし,カーネルの移植の場合には,移植先のブートローダの機能によっては,このよ うな独自セクションを追加できない場合もあります (ブートローダが貧弱で,決めうち のセクションしか展開してくれないなど).このような場合には,リンカスクリプト中の データ領域あたりに, ------------------------------------------------------------------------------- __start_link_set_sysctl_funcs = . ; *(link_set_sysctl_funcs) __stop_link_set_sysctl_funcs = . ; ------------------------------------------------------------------------------- のように,__start_link_set_sysctl_funcs, __stop_link_set_sysctl_funcs を明示的 に定義することで,リンクエラーを回避する必要があります. 同様のテクニックは,Linux カーネルでも利用されています.というより,Linux のほ うが積極的に利用されているようです.以下,Linux カーネルについて説明します. 例えば,Linux カーネルの,コンソールの初期化部分を例に説明しましょう.カーネル のスタート時には,init/main.c:start_kernel() から drivers/char/ tty_io.c:console_init() が呼ばれますが,ここではリスト27のような処理が行われて います. =============================================================================== リスト27: console_init() の処理(drivers/char/tty_io.c より抜粋) =============================================================================== call = __con_initcall_start; while (call < __con_initcall_end) { (*call)(); call++; } =============================================================================== つまり,__con_initcall_start から __con_initcall_end の間に登録されている初期化 用関数を順次呼び出しています.しかし,これらの変数の定義をいくら探しても,見つ かりません.これらはリンカスクリプトで定義されています (リスト28). =============================================================================== リスト28: __con_initcall_* の定義(リンカスクリプトより抜粋) =============================================================================== __con_initcall_start = .; .con_initcall.init : { *(.con_initcall.init) } __con_initcall_end = .; =============================================================================== リスト28は,arch/ppc/kernel/vmlinux.lds.S からの抜粋です. .con_initcall.init というセクションに配置された関数が, __con_initcall_start と __con_initcall_end の間に配置されることになります. 初期化用の関数の登録には,console_initcall() というマクロを利用します. console_initcall() は,include/linux/init.h でリスト29 のように定義されています . =============================================================================== リスト29: console_initcall() の定義(init.h より抜粋) =============================================================================== #define console_initcall(fn) \ static initcall_t __initcall_##fn \ __attribute_used__ __attribute__((__section__(".con_initcall.init")))=fn =============================================================================== 行なっていることは,NetBSD の SYSCTL_SETUP() とほぼ同じです.関数へのポインタを 定義し,さらに __attribute__((__section__("..."))) により,定義したポインタを .con_initcall.init セクションに配置しています. Linux カーネルでは,この手のテクニックは頻繁に利用されています.例えば console_initcall() の他にも,__initcall(), __setup() といったような初期化の枠組 みがあります.同じ説明の繰り返しになるため,ここでは説明は省略しますが,C言語 の文法知識だけしか無いと,こういったコードに出会ったときに,コードの追跡にとて も苦労します. また,このようなコードを追跡したい場合,例えばリスト27のようなコードを追跡した い場合には,ソースだけを追いかけるのでなく,リンク済みカーネルに対して,nm で __con_initcall_start, __con_initcall_end のアドレスを調べて,その間に配置されて いる関数を調べる,といった手順を覚えると,調査がとても楽になります. ただし,ここで紹介したようなテクニックは,もちろんリンカ依存になります. OSのソ ースの場合には,リンカスクリプトは独自のものを利用するのが普通であるため,リン カスクリプトやリンカの機能を利用したテクニックが使われるのは,珍しいことではあ りません.しかしアプリケーション・プログラムの場合には,リンカスクリプトはOSに 付属のものが利用されるのが普通ですから,移植性を考慮して,このような裏技ともい えるテクニックを無闇に濫用すべきではないでしょう. 2.3 結合型の実行形式の作成 最近は組み込み機器に NetBSD や Linux などの高級なOSが組み込まれることも珍しくあ りません.組み込み機器と言えど,最近のものは,ひと昔前のPCよりも高スペックであ ったりします.これらの高性能なハードウエアを十分に使いこなすためには,やはりそ れなりに高級なOSが必要になってきますから, OSの候補に NetBSD や Linux があがっ てくるのは,もっともであると言えます.しかし,このようなときに問題となるのは, ターゲットとなるマシンのリソース制限です. 組み込み機器の場合には,ハードディスクを搭載することは考えづらいですし,フラッ シュメモリやRAMの容量などにも限度がありますから,記憶容量が制限されてきます.こ のとき問題となるのは,ls や cat などのコマンド群をどのように組み込むか,という ことです./bin や /usr/bin にあるような,よく利用されるコマンドをすべてそのまま 組み込もうとしたら,馬鹿にできないサイズになってきます.さらに,場合によっては 動的ライブラリが必要になるかもしれません.なにも考えず,なんでもかんでも欲張っ てどんどん追加していくと,標準的なコマンドだけでもすぐに数Mバイトを超えたサイ ズになってしまうでしょう. これには昔から知られている解決策がひとつあります.簡単に説明すると,「すべての コマンドをひとまとめにリンクして,単一の実行形式にしてしまう」というものです. つまり ls も cat も cp も,すべてひとつの実行形式にしてしまうわけです. 通常は ls や cat などのコマンドは,別々にコンパイル・リンクして,コマンドごとに 実行形式を作成します.もしもここでライブラリが静的にリンクされるとすると,コマ ンドごとにライブラリの本体を持つことになってしまいます.これではファイルサイズ の無駄です.これを避けるには,動的ライブラリを使うという方法がありますが,しか し,動的ライブラリ自体もそこそこ巨大です.また,/bin 以下にあるようなコマンドは ,問題発生時でも確実に使えるよう,静的リンクにしておきたい場合も多々あります (*12). そこで,すべてのコマンドを単一にリンクして,ひとつの実行形式にまとめてしまう, という方法があります.この場合,各コマンドから実行形式に対して,シンボリックリ ンクを張っておきます.例えばひとまとめにした実行形式として combine というファイ ルを作成するならば, # cd /bin # ln -s combine ls # ln -s combine cat # ln -s combine cp ... のようなことをしておくわけです. この場合,ls や cat などのコマンドの実行時に実際に起動されるのは,常に combine です.しかし,argv[0] の指す文字列は,コマンドごとに変わってきます.図4を見てく ださい.このように,すべてのコマンドは単一リンクにして,ひとつの実行形式 (combine)にまとめます. combine の持つ main() の内部では,argv[0] を見てどのコ マンドが実行されるべきかを判断し,そのコマンドが本来持っていた main() に処理を 渡せば,あとは本来の処理が行われる,という手順です. (figure/combine.png) [combine] 図4: すべてのコマンドを単一リンクにする このようにすべてのコマンドを単一リンクにすることで,以下のことが期待できます. 結果として,メモリなどの資源を大幅に節約することができます. ・ コマンドごとに静的ライブラリをリンクしていたのが共通化される (ファイルサイ ズの節約) ・ 仮想メモリをサポートするOSの場合には,複数のコマンドを同時に実行した際に, 書き込み不可の領域(テキスト領域)を共有できる(実行時の使用メモリの節約) このテクニックは,古くはインストール用フロッピーや,1FDでのOSの実現などに利用さ れています. NetBSD や FreeBSD だと,crunch というメカニズムで実現されています (このようにして作成された実行形式のことを,"crunched binary" と呼びます). FreeBSD ではインストール用フロッピーの作成に利用されていますので,詳しく知りた ければ,/usr/src/release で grep crunch などをしてみてください. また,BusyBox というフリーソフトウエアがあり,組み込み用などのためにサイズを削 減したコマンド群を提供しているのですが, BusyBox でも,このテクニックが利用され ています. Linux のインストーラには,(ディストリビューションによりますが) BusyBox が利用されていることが多いようです. このような単一リンクの実行形式を作成するためには,コマンドごとの main() を別名 に置き換える必要があります(名前が衝突してしまうので).ソースコードを直接書き換 えてもいいのですが,objcopy の --redefine-sym を利用することで,簡単に行うこと ができます. 実際に,複数のアプリケーションから,単一リンクの実行形式を作成してみます.まず , app1.c (リスト30) と app2.c (リスト31) を見てください.これらは単に文字列を 表示して終了するだけの,単純なアプリケーション・プログラムです. =============================================================================== リスト30: アプリケーションその1(app1.c) =============================================================================== #include int main(int argc, char *argv[]) { printf("app1 argc=%d, argv[0]=%s\n", argc, argv[0]); exit (1); } =============================================================================== =============================================================================== リスト31: アプリケーションその2(app2.c) =============================================================================== #include int main(int argc, char *argv[]) { printf("app2 argc=%d, argv[0]=%s\n", argc, argv[0]); exit (2); } =============================================================================== リスト32は,app1.c, app2.c をコンパイル・リンクして実行形式 app1, app2 を作成し ,実際に実行してみた結果です. app1, app2 とも,問題なく実行できています. =============================================================================== リスト32: アプリケーションのコンパイル・リンクと実行 =============================================================================== % gcc -c app1.c -Wall % gcc app1.o -Wall -o app1 % ./app1 app1 argc=1, argv[0]=./app1 % % gcc -c app2.c -Wall % gcc app2.o -Wall -o app2 % ./app2 app2 argc=1, argv[0]=./app2 % =============================================================================== これらの2つのアプリケーションを,単一リンクの実行形式にしてみましょう.まず, combine.c (リスト33) のようなプログラムを考えます. combine.c の main() では, argv[0] を見て,app1_main() か app2_main() に処理を分岐します.また,終了用の関 数として,combine_exit() を定義しています. =============================================================================== リスト33: 結合用の main() 関数(combine.c) =============================================================================== #include #include int app1_main(int argc, char *argv[]); int app2_main(int argc, char *argv[]); void combine_exit(int status); int main(int argc, char *argv[]) { char *ptr; printf("combine_main()\n"); ptr = rindex(argv[0], '/'); if (ptr) ptr++; else ptr = argv[0]; if (!strcmp(ptr, "app1")) return (app1_main(argc, argv)); if (!strcmp(ptr, "app2")) return (app2_main(argc, argv)); printf("Unknown command.\n"); exit (-1); } void combine_exit(int status) { printf("combine_exit(%d)\n", status); exit (status); } =============================================================================== さて,combine.c に app1.o, app2.o をリンクして,単一の実行形式にしたいところで すが,このままでは app1.o, app2.o, combine.c にひとつずつある main() 関数がぶつ かってしまいます.そこで,objcopy の --redefine-sym オプションを利用して,main () をアプリケーションごとの別名に変換します(リスト34).ついでに exit() も, combine.c が持つ combine_exit() を呼び出すように変更しています. =============================================================================== リスト34: main() を別名に変換する =============================================================================== % objcopy --redefine-sym main=app1_main --redefine-sym exit=combine_exit app1.o combine_app1.o % objcopy --redefine-sym main=app2_main --redefine-sym exit=combine_exit app2.o combine_app2.o =============================================================================== リスト34の操作により,新しいオブジェクトファイルとして,combine_app1.o, combine_app2.o が作成されます. nm によって,main() と exit() が置き換わってい ることを確認してみましょう.リスト35は,nm app1.o と,nm combine_app1.o の結果 です. app1.o の main というシンボルが,combine_app1.o では app1_main というシ ンボルに置き換わっています.また,exit というシンボルも,combine_exit に置き換 わっています. さらにリスト36は,nm app2.o, nm combine_app2.o の結果です.こちらも同様に,main と exit が置き換わっています. =============================================================================== リスト35: nm app1.o, nm combine_app1.o の結果 =============================================================================== % nm app1.o U exit 00000000 t gcc2_compiled. 00000000 T main U printf % % nm combine_app1.o 00000000 T app1_main U combine_exit 00000000 t gcc2_compiled. U printf % =============================================================================== =============================================================================== リスト36: nm app2.o, nm combine_app2.o の結果 =============================================================================== % nm app2.o U exit 00000000 t gcc2_compiled. 00000000 T main U printf % % nm combine_app2.o 00000000 T app2_main U combine_exit 00000000 t gcc2_compiled. U printf % =============================================================================== では,combine.c, combine_app1.o, combine_app2.o をコンパイル・リンクして,実行 形式を作成してみましょう.ついでに app1, app2 からのシンボリックリンクも作成し ます(リスト37). =============================================================================== リスト37: combine.c のコンパイル・リンク =============================================================================== % gcc combine.c combine_app1.o combine_app2.o -Wall -o combine % rm app1 % rm app2 % ln -s combine app1 % ln -s combine app2 % ls -l combine app? lrwxr-xr-x 1 hiroaki user 7 10 3 14:21 app1 -> combine lrwxr-xr-x 1 hiroaki user 7 10 3 14:21 app2 -> combine -rwxr-xr-x 1 hiroaki user 5218 10 3 14:21 combine =============================================================================== リスト37の操作により,実行形式 combine とシンボリックリンク app1, app2 が作成さ れました.実行してみましょう.リスト38は,combine, app1, app2 をそれぞれ実行し たときの結果です. =============================================================================== リスト38: combine, app1, app2 の実行結果 =============================================================================== % ./combine combine_main() Unknown command. % % ./app1 combine_main() app1 argc=1, argv[0]=./app1 combine_exit(1) % % ./app2 combine_main() app2 argc=1, argv[0]=./app2 combine_exit(2) =============================================================================== combine を実行したときには,Unknown command で終了しています.これは,argv[0] に対応する処理が見つからなかったためです.これに対して app1, app2 の実行時には ,combine.c の main() を通してから app1, app2 の持つ本来の処理が行われています( リスト32の app1, app2 の実行結果と比較してみてください).また,app1, app2 の終 了時には,exit() のかわりに combine_exit() が呼ばれていることがわかります. まとめ リンクとロードはメモリマップにかかわる部分であるため,コンピュータ・システムを 理解したい場合には,必須の知識になります (*13).本連載ではリンカとローダの原理 と動作を説明してきました.理論的な説明に終止するのではなく,実際に試してみて検 証することを意識してきました.これは筆者自身が理論的に説明できないためという理 由もあるのですが,やはり実際に動いているコードが一番説得力がある,という考えも ありました. 筆者が初めて OS のカーネルを読み出したとき,一番最初につまづいたのが,リンカに かかわる部分でした.アプリケーション・プログラムしか書いたことのなかった筆者に とっては,これはまったくの未知の領域でした.当時はリンカスクリプトというものを 知らなかったため,なぜ命令コードやデータがメモリ上に適切に配置されるのか,不思 議に思ったものです. 他にも割り込み,仮想メモリといったところでつまづきましたが,これらは書籍などが 多く出ていて,資料に困ることはそれほど無いと思います.しかしリンカとローダ(とく にリンカスクリプト)に関しては,和文のまとまった資料が無いのが現状でした.そのよ うな思いで書きはじめた連載でしたが,いかがでしたでしょうか.本連載が読者の方々 の一助になれば幸いです. 注釈 ・ (*10) __section__() でも section() でも,どちらでもいいようです.また __attribute__(()) も,__attribute(()) と書いてもいいようです. ・ (*11) このためソースコードやリンカスクリプトをいくら grep で検索しても, __start_link_set_sysctl_funcs 等の定義は見つかりません.筆者はこのことを知 らなかったため,NetBSD の移植を手がけた際に,まさにこの部分でハマりました. ・ (*12) FreeBSD では,/usr 以下がマウントできなかったときのことを考えて,/bin 以下のコマンドは静的リンクになっています. ・ (*13) 逆にいえば,メモリマッピングの知識があれば,リンカとローダの動作は素 直に理解できる,とも言えます. 参考文献 ・ [1] 「C言語 入門書の次に読む本」,坂井弘亮著,技術評論社 ・ [2] 「エキスパートCプログラミング − 知られざるCの深層」, Peter van der Linden 著,梅原系訳,アスキー ・ [3] 「BSD で Memory Disk を使う」,坂井弘亮,BSD magazine 2003 No.16 特集, ASCII ・ [4] 「Linkers & Loaders」,John R. Levine 著,榊原一矢監訳,ポジティブエッ ジ訳 ・ [5] 「ゼロから始める組み込みLinuxシステム構築」,西田亙,Embedded UNIX Vol.6 特集,CQ出版社