/ «2006-07-15 (Sat) ^ 2006-07-22 (Sat)» ?
   西田 亙の本:GNU 開発ツール -- hello.c から a.out が誕生するまで --

Categories Books | Hard | Hardware | Linux | MCU | Misc | Publish | Radio | Repository | Thoughts | Time | UNIX | Writing | プロフィール


2006-07-18 (Tue)

[UNIX] malloc failure (その2)

リンク方式に見る Linux と BSD の違い

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 に定義して、対象プログラムを実行しています。

Linux と OpenBSD の /sbin/init

ダイナミック・リンカーローダの仕組みを理解し、その存在を意識できるようになると、途端に心配事が雨雲のように頭の中を広がり、夜も眠れなくなってしまう方もいらっしゃることでしょう。

「動的リンクを受けたプログラム群の生命線は、外部ライブラリはもちろんのこと、ダイナミック・リンカーローダである。もしもこの生命線が絶たれてしまったら、私のサーバーどうなっちゃうの?!」これは、システム管理者として極めて正しい直感と言えます。

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 の段階でハングアップしてしまうからです。

Linux と BSD の違い

今回は、動的リンクの仕組みを簡単にご紹介しましたが、技術に対する理解が深まると様々なことが見えてきます。私達は、自分が置かれた環境を中心として世界観を構築しがちですが、たまには住み慣れた世界を離れ、新世界を探訪することも良いものです。「失敗することを知っている BSD」の世界は、Linux に欠けている危機意識と謙虚さ、そして用心深さを教えてくれることでしょう。

つづく

当初の予想を超えて、その3に突入です。