Categories Books | Hard | Hardware | Linux | MCU | Misc | Publish | Radio | Repository | Thoughts | Time | UNIX | Writing | プロフィール
failmalloc の仕組みを理解するためには、"動的リンク" に関する知識が必要になります。C言語によるプログラム開発は、プリプロセス・コンパイル・アセンブル・リンクの4工程を経ますが、最後のリンクはその方式により、動的リンクと静的リンクのふたつに分類されます。
BSD 環境では、危機管理のためにシステムの基幹部分に関するプログラムは静的リンク、それ以外の一般ユーザーアプリケーションは動的リンクにより作成されています。
これに対して、Linux 環境ではほぼ全てのプログラムが動的リンクにより作成されています(Debian Sarge で確認したところ、静的リンクで作成された実行可能ファイルは /sbin/ldconfig ただひとつでした)。この事実が意味するところは、これからの解説で明らかになります。
それでは、恒例の hello.c を題材にして解析を進めましょう。
#include <stdio.h> // puts() int main() { puts("Hello, world!"); return 0; }
都合により、printf に代わり puts 関数を使用していますが、ごく当たり前の Hello, world! プログラムです。
$ gcc -Wall -o hello hello.c $ ./hello Hello, world!
毎度お馴染みの gcc ドライバで、実行可能ファイル hello をビルドしています。ここまでは、どの入門書にも書かれていることですが、私達が本当に知るべき問題は hello の内部に隠されています。
$ file hello hello: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), for GNU/Linux 2.4.1, dynamically linked (uses shared libs), not stripped
file コマンドで hello の正体を確認すると、"動的リンクに基づく ELF 実行可能ファイルである" と表示されました。動的リンクというのは、最終的なリンク作業が、実行時に行われることを意味しています。別の表現をすれば、hello は現時点では "リンクが未完了" なのです。
$ nm -u hello w __gmon_start__ w _Jv_RegisterClasses U __libc_start_main@@GLIBC_2.0 U puts@@GLIBC_2.0
nm コマンドの -u (Undefined) オプションは、オブジェクトファイル中の未定義シンボルを表示するためのものです。この結果から、hello は __libc_start_main, puts を外部参照しており、このふたつのシンボルのリンクは現時点で完了していないことが分かります。
上段に表示されている w は Weak symbol の略ですが、今回の解析には必要ないので、grep コマンドで必要な情報のみを残しておきましょう。
$ nm -u hello | grep "U " U __libc_start_main@@GLIBC_2.0 U puts@@GLIBC_2.0
hello が未完成ということは、起動する前に誰かが "完成させる" 必要があります。この "誰か" については、ldd (List Dynamic Dependency)コマンドで知ることができます。
$ ldd hello linux-gate.so.1 => (0xffffe000) libc.so.6 => /lib/tls/libc.so.6 (0xb7e6f000) /lib/ld-linux.so.2 (0xb7fb0000)
詳細については割愛しますが、hello を完成させるのは、/lib/ld-linux.so.2 であり、その際には puts 関数を提供する GNU C library (libc6: libc.so.6) が使われます。
次に、動的リンクの対極に位置する、静的リンクについて見てみましょう。gcc ドライバを用いて、静的リンクでプログラムをビルドするためには、-static オプションを指定します。
$ gcc -Wall -static -o hello_static hello.c $ wc -c hello hello_static 6985 hello 502393 hello_static 509378 total
hello 実行可能ファイルは 6985 バイトでしたが、静的リンク版の hello_static は 502K バイトにも及んでいます。なぜこれほどのサイズ差が生じるのかについては、場を改めて解説いたします。
$ file hello_static hello_static: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), for GNU/Linux 2.4.1, statically linked, not stripped $ ldd hello_static not a dynamic executable $ nm -u hello_static |grep "U " $
file コマンドで hello_static を解析すると、今度は "静的リンクに基づく ELF 実行可能ファイルである" と表示されました。ldd コマンドでは何も表示されませんので、hello_static は単独で完成したプログラムファイルであることが分かります。これは、他者の助けを借りず、自力で起動できるプログラムであることを意味しています。hello_static 内部に、未定義シンボルは存在しません。
$ ./hello Hello, world! $ ./hello_static Hello, world!
しかしながら、シェル上から観察する限り、どちらも全く同じ挙動を示しており、hello が外部プログラムの助けを借りている様子は、伺うことができません。
そこで登場するコマンドが readelf です。同コマンドに -l オプションを指定して、hello プログラムを解析してみてください。
$ readelf -l hello Elf file type is EXEC (Executable file) Entry point 0x80482e0 There are 7 program headers, starting at offset 52 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align PHDR 0x000034 0x08048034 0x08048034 0x000e0 0x000e0 R E 0x4 INTERP 0x000114 0x08048114 0x08048114 0x00013 0x00013 R 0x1 [Requesting program interpreter: /lib/ld-linux.so.2] LOAD 0x000000 0x08048000 0x08048000 0x0050c 0x0050c R E 0x1000 LOAD 0x00050c 0x0804950c 0x0804950c 0x00104 0x00108 RW 0x1000 DYNAMIC 0x000520 0x08049520 0x08049520 0x000c8 0x000c8 RW 0x4 NOTE 0x000128 0x08048128 0x08048128 0x00020 0x00020 R 0x4 GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4 Section to Segment mapping: Segment Sections... 00 01 .interp 02 .interp .note.ABI-tag .hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .eh_frame 03 .ctors .dtors .jcr .dynamic .got .got.plt .data .bss 04 .dynamic 05 .note.ABI-tag 06
実行可能ファイル中の "プログラムヘッダー" と呼ばれる構造が表示されますが、この中の INTERP というヘッダーに注目してください。INTERP ヘッダーの内部に、"Requesting program interpreter: /lib/ld-linux.so.2" という記載がありますが、これは hello がプログラムインタープリタとして /lib/ld-linux.so.2 を必要としていることを意味しています。
インタープリタと言えば、往年の BASIC が有名ですが、ELF におけるプログラムインタープリタは、プログラム実行時に最終的なリンクを完成させる "ダイナミック・リンカーローダ" の役目を負っています。
LD.SO(8) LD.SO(8) NAME ld.so/ld-linux.so - dynamic linker/loader DESCRIPTION ld.so loads the shared libraries needed by a program, prepares the pro- gram to run, and then runs it. Unless explicitly specified via the -static option to ld during compilation, all Linux programs are incom- plete and require further linking at run time.
man ページに記載されている通り、ダイナミック・リンカーローダは、プログラムが必要とする外部ライブラリ(共有ライブラリ)をメモリ上にロードし、未完成プログラムの実行前に最終リンクを行います。
それでは、静的リンク版 hello_static のプログラムヘッダーはどのような構造になっているのでしょうか。
$ readelf -l hello_static Elf file type is EXEC (Executable file) Entry point 0x80480f0 There are 4 program headers, starting at offset 52 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align LOAD 0x000000 0x08048000 0x08048000 0x6a344 0x6a344 R E 0x1000 LOAD 0x06b000 0x080b3000 0x080b3000 0x00d34 0x01f98 RW 0x1000 NOTE 0x0000b4 0x080480b4 0x080480b4 0x00020 0x00020 R 0x4 GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4 Section to Segment mapping: Segment Sections... 00 .note.ABI-tag .init .text __libc_freeres_fn .fini .rodata __libc_atexit __libc_subfreeres .eh_frame 01 .ctors .dtors .jcr .data.rel.ro .got .got.plt .data .bss __libc_freeres_ptrs 02 .note.ABI-tag 03
予想通り、INTERP ヘッダーは登録されていませんでした。
ダイナミック・リンカーローダは黒子として、私達の目が届かない裏方でひっそりと作業していることになります。こうなると、一目その姿を見てみたい・・というのが、人情。そこで、今度は strace (System TRACE)コマンドの登場です。
strace は、指定されたプログラム内部で実行されるシステムコールの呼び出し状況を引数付きで、つぶさに解析するためのシステムツールです。まず最初に、静的リンク版 hello_static を解析してみましょう。
$ strace ./hello_static execve("./hello_static", ["./hello_static"], [/* 20 vars */]) = 0 uname({sys="Linux", node="Sarge", ...}) = 0 brk(0) = 0x80b5000 brk(0x80d6000) = 0x80d6000 fstat64(1, {st_mode=S_IFREG|0644, st_size=225, ...}) = 0 mmap2(NULL, 131072, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7f34000 write(1, "Hello, world!\n", 14Hello, world! ) = 14 munmap(0xb7f34000, 131072) = 0 exit_group(0) = ?
前半で初期化のためのシステムコールがいくつか実行されますが、hello_static 内部の main 関数から呼び出される puts 関数の実体は、最後から3番目の write システムコールに対応します。
次に、動的リンク版 hello を見てみましょう。
$ strace ./hello execve("./hello", ["./hello"], [/* 20 vars */]) = 0 uname({sys="Linux", node="Sarge", ...}) = 0 brk(0) = 0x804a000 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7ef1000 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) open("/etc/ld.so.cache", O_RDONLY) = 3 fstat64(3, {st_mode=S_IFREG|0644, st_size=24726, ...}) = 0 mmap2(NULL, 24726, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb7eea000 close(3) = 0 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) open("/lib/tls/libc.so.6", O_RDONLY) = 3 read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\260O\1"..., 512) = 512 fstat64(3, {st_mode=S_IFREG|0755, st_size=1270928, ...}) = 0 mmap2(NULL, 1276892, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xb7db2000 mmap2(0xb7ee0000, 32768, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x12e) = 0xb7ee0000 mmap2(0xb7ee8000, 7132, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xb7ee8000 close(3) = 0 mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7db1000 mprotect(0xb7ee0000, 20480, PROT_READ) = 0 set_thread_area({entry_number:-1 -> 6, base_addr:0xb7db18e0, limit:1048575, seg_32bit:1, contents:0, read_exec_only:0, limit_in_pages:1, seg_not_present:0, useable:1}) = 0 munmap(0xb7eea000, 24726) = 0 fstat64(1, {st_mode=S_IFREG|0644, st_size=1569, ...}) = 0 mmap2(NULL, 131072, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7d91000 write(1, "Hello, world!\n", 14Hello, world! ) = 14 munmap(0xb7d91000, 131072) = 0 exit_group(0) = ?
かなりの数のシステムコールが追加されていますが、先頭部分で /etc/ld.so.nohwcap, /etc/ld.so.preload, /etc/ld.so.cache ファイルへのアクセスが行われています。hello は外部ファイルへアクセスしていませんので、これらはダイナミック・リンカーローダの仕業であることが分かります。確かに、裏方で /lib/ld-linux.so.2 が作業しているようです。
ここで、man ld.so を再度参照してみてください。
LD_TRACE_LOADED_OBJECTS If present, causes the program to list its dynamic library depen- dencies, as if run by ldd, instead of running normally.
外部からダイナミック・リンカーローダを制御するために、いくつかの環境変数が用意されています。この中のひとつ、LD_TRACE_LOADED_OBJECTS が定義されていると、ダイナミック・リンカーローダはリンク処理を行わず、対象プログラムが依存している外部ファイルのリストのみを表示して終了します。試してみましょう。
$ LD_TRACE_LOADED_OBJECTS=1 ./hello linux-gate.so.1 => (0xffffe000) libc.so.6 => /lib/tls/libc.so.6 (0xb7de4000) /lib/ld-linux.so.2 (0xb7f25000)
bash 上で、起動プロセスに対して一時的に環境変数を指定する場合は、このようにコマンドの前に記述します。1 で定義された LD_TRACE_LOADED_OBJECTS 環境変数は、hello の前に起動される /lib/ld-linux.so.2 に伝えられ、外部依存リストのみを表示して、終了します。
さて、この出力リストに見覚えはないでしょうか。この結果は、ldd コマンドの出力と全く同じですね。
$ which ldd /usr/bin/ldd $ file `which ldd` /usr/bin/ldd: Bourne-Again shell script text executable
実は、ldd コマンドの実体はシェルスクリプトになっており、内部では先ほどの実行例と同じように、LD_TRACE_LOADED_OBJECTS 環境変数を 1 に定義して、対象プログラムを実行しています。
ダイナミック・リンカーローダの仕組みを理解し、その存在を意識できるようになると、途端に心配事が雨雲のように頭の中を広がり、夜も眠れなくなってしまう方もいらっしゃることでしょう。
「動的リンクを受けたプログラム群の生命線は、外部ライブラリはもちろんのこと、ダイナミック・リンカーローダである。もしもこの生命線が絶たれてしまったら、私のサーバーどうなっちゃうの?!」これは、システム管理者として極めて正しい直感と言えます。
PC-UNIX の起動時、カーネルが最初に実行するプロセスは /sbin/init (swapper)ですが、まず OpenBSD 上の /sbin/init を観察してみましょう。
$ uname -a OpenBSD xxx.xxx.xxx 3.6 GENERIC#278 i386 $ file /sbin/init /sbin/init: executable, regular file, no read permission $ ldd /sbin/init /sbin/init: ldd: /sbin/init: Permission denied $ ls -l /sbin/init -r-x------ 1 root bin 219552 Jan 16 2005 /sbin/init
一般ユーザが file, ldd コマンドを用いて /sbin/init を解析しようとすると、アクセス拒否を受けます。ls コマンドでパーミッションを確認すると、root ユーザのみ Read/eXecute 可能となっています(root ユーザですら Write は許可されていない点に注目)。BSD において、ファイルパーミッションは単なる飾りではなく、長年にわたる失敗と経験に基づいた、最良の設定が施されています。BSD は「失敗することを知っている」のです。
スーパーユーザとして /sbin/init の解析を続けます。
$ sudo file /sbin/init /sbin/init: ELF 32-bit LSB executable, Intel 80386, version 1, for OpenBSD, statically linked, stripped $ sudo ldd /sbin/init /sbin/init: ldd: /sbin/init: not a dynamic executable
OpenBSD 上の /sbin/init は静的リンクで作成されているため、万が一Cライブラリやダイナミック・リンカーローダに障害が発生したとしても、影響は一切受けないことが分かります。
$ sudo which mount /sbin/mount $ sudo file /sbin/mount /sbin/mount: ELF 32-bit LSB executable, Intel 80386, version 1, for OpenBSD, statically linked, stripped
ちなみに、システム起動時のルートファイルシステムのマウント時に必要となる mount コマンドも、同じく静的リンクで作成されています。このように、BSD ではシステム起動に必要な基幹ツールはすべて静的リンクで作成されているため、仮にCライブラリやダイナミック・リンカーローダに致命的な障害が発生したとしても、最低限の環境で再起動することができるのです。
一方 Linux はどうでしょうか。
$ uname -a Linux Sarge 2.6.12-1-686-smp #1 SMP Tue Sep 27 13:10:31 JST 2005 i686 GNU/Linux $ ls -l /sbin/init -rwxr-xr-x 1 root root 35244 2006-02-10 09:23 /sbin/init $ file /sbin/init /sbin/init: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), for GNU/Linux 2.2.0, dynamically linked (uses shared libs), stripped $ ldd /sbin/init linux-gate.so.1 => (0xffffe000) libsepol.so.1 => /lib/libsepol.so.1 (0xb7f65000) libselinux.so.1 => /lib/libselinux.so.1 (0xb7f52000) libc.so.6 => /lib/tls/libc.so.6 (0xb7e19000) libdl.so.2 => /lib/tls/libdl.so.2 (0xb7e15000) /lib/ld-linux.so.2 (0xb7fa8000)
まず、/sbin/init のファイルパーミッション自体が OpenBSD とは似ても似つかぬものとなっています。このように、OpenBSD と Debian のファイルパーミッションには、"大人と赤子" ほどの違いがあるのです。
また、驚いたことに init プログラムが、動的リンクで作成されています。依存ライブラリの内容から見ると、libselinux.so を用いてセキュリティ強化を計っているようですが、これでは本末転倒ではないでしょうか。
Debian GNU/Linux では、/sbin/init だけでなく mount コマンドなどもすべて動的リンクで作成されているため、先ほどの心配にもあった通り、Cライブラリやダイナミック・リンカーローダのアップデートに失敗したり、障害が発生すれば、二度とシステムが起動することはありません。/sbin/init の段階でハングアップしてしまうからです。
今回は、動的リンクの仕組みを簡単にご紹介しましたが、技術に対する理解が深まると様々なことが見えてきます。私達は、自分が置かれた環境を中心として世界観を構築しがちですが、たまには住み慣れた世界を離れ、新世界を探訪することも良いものです。「失敗することを知っている BSD」の世界は、Linux に欠けている危機意識と謙虚さ、そして用心深さを教えてくれることでしょう。
当初の予想を超えて、その3に突入です。