2.共有ライブラリの正体

 それでは,共有ライブラリの実体について調べてみましょう.

 そもそもhello.cはなぜ外部ライブラリが必要になったのでしょうか.コンパイル後に生成されるオブジェクトファイルを使い,その理由を調べてみましょう.先ほどのコマンドではオブジェクトファイルは生成されなかったので,-cオプションを用いてhello.oファイルを作成します.

 Cソースで定義される変数や関数は,オブジェクトファイル中では「シンボル」として管理されます.それぞれのシンボルには属性があり,4バイト整数を指すのか,関数のエントリアドレスを指すのかなどの情報が記録されています.オブジェクトファイルの時点ではアドレスは決定されませんが,リンカであるldコマンドがリンクを行うことで,シンボルへの最終的なアドレス割り付けが行われます.

 オブジェクトファイル中のシンボル一覧は,nmコマンドで閲覧することができます.シンボルにはグローバルスコープをもつものと,ローカルスコープをもつものの2種類があるので,通常は-g(Global)オプションを指定することで,グローバルシンボルのみを表示させます.

 リスト4でわかるように,hello.cをコンパイルして得られたhello.oオブジェクトファイル中には,二つのシンボルが含まれています.一つはmain,もう一つはprintfです.mainはmain関数のエントリアドレスを指していますが,シンボルタイプがTであることに注意してください.これはmainシンボルが.textセクションに存在することを意味しています.UNIXでは実行ファイル内部は複数のセクションから構成されており,実行コードは.textセクションに格納される決まりになっています.一方,シンボルprintfのタイプはUになっています.UはUndefinedの略で,該当オブジェクトファイル中でそのシンボルは定義されていないことを表しています.つまり,printf関数は外部に存在するというわけです.

〔リスト4〕
nmコマンドによるシンボル表示(hello)
$ gcc -c hello.c
$ nm -g hello.o
00000000 T main
         U printf

 それでは,printfはどこにあるのでしょうか.ある実行可能ファイルが「依存」しているファイルの一覧は,lddコマンドで参照することができます(リスト5).

〔リスト5〕lddコマンドによる依存ファイル一覧(hello)
$ ldd hello
        libc.so.6 => /lib/libc.so.6 (0x4001b000)
        /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
$ ls -l /lib/libc.so.6
lrwxrwxrwx    1 root     root              /lib/libc.so.6 -> libc-2.2.4.so
$ ls -l /lib/libc-2.2.4.so
-rwxr-xr-x    1 root     root      1171196 /lib/libc-2.2.4.so
$ ls -l /lib/ld-linux.so.2
lrwxrwxrwx    1 root     root              /lib/ld-linux.so.2 -> ld-2.2.4.so
$ ls -l /lib/ld-2.2.4.so
-rwxr-xr-x    1 root     root        94529 /lib/ld-2.2.4.so
$ nm -g hello
080494c4 ? _DYNAMIC
080494a4 ? _GLOBAL_OFFSET_TABLE_
08048470 R _IO_stdin_used
08049564 A __bss_start
08049484 D __data_start
         w __deregister_frame_info@@GLIBC_2.0
         w __gmon_start__
         U __libc_start_main@@GLIBC_2.0
         w __register_frame_info@@GLIBC_2.0
08049564 A _edata
0804957c A _end
08048450 ? _fini
0804846c R _fp_hw
08048298 ? _init
08048310 T _start
08049484 W data_start
080483f0 T main
         U printf@@GLIBC_2.0

 lddコマンドによると,helloは/lib/libc.so.6および/lib/ld-linux.so.2に依存しています.二つのファイルは/lib/ディレクトリ中でシンボリックリンクとして作成されており,それぞれの実体はlibc-2.2.4.so,ld-2.2.4.soとなっています.libc-2.2.4.soはGNU C library(glibc)バージョン2.2.4の共有ライブラリ版,ld-2.2.4.soはglibc 2.2.4用のELF版ダイナミックリンカ・ローダです.libc-2.2.4.soは1Mバイトを越える巨大ライブラリファイルであることに注目しましょう.

 ダイナミックリンカ・ローダとは,実行ファイルが必要とする共有ライブラリのセットアップを行い,ライブラリ関数アドレスの最終的な解決を行うためのプログラムです.現在,ダイナミックリンカ・ローダには実行可能ファイル形式に応じてa.out用およびELF(Executable and Linking Format)用の2種類が存在します(比較的最近までUNIX界ではa.out形式が一般的だったが,現在はほとんどがELFへと移行している).

 ファイル形式上,ld-2.2.4.soは共有オブジェクトファイルということになっていますが,じつはその正体は「インタプリタ」です.ELFファイルの解析ツールreadelfに-lオプションを指定し,hello中のプログラムヘッダと呼ばれる構造を表示させてみましょう(リスト6).

〔リスト6〕helloのプログラムヘッダ構造

$ readelf -l hello

Elf file type is EXEC (Executable file)
Entry point 0x8048310
There are 6 program headers, starting at offset 52

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  PHDR           0x000034 0x08048034 0x08048034 0x000c0 0x000c0 R E 0x4
  INTERP         0x0000f4 0x080480f4 0x080480f4 0x00013 0x00013 R   0x1
      [Requesting program interpreter: /lib/ld-linux.so.2]
  LOAD           0x000000 0x08048000 0x08048000 0x00483 0x00483 R E 0x1000
  LOAD           0x000484 0x08049484 0x08049484 0x000e0 0x000f8 RW  0x1000
  DYNAMIC        0x0004c4 0x080494c4 0x080494c4 0x000a0 0x000a0 RW  0x4
  NOTE           0x000108 0x08048108 0x08048108 0x00020 0x00020 R   0x4

 Section to Segment mapping:
  Segment Sections...
   00
   01     .interp
   02     .interp .note.ABI-tag .hash .dynsym .dynstr .gnu.version .gnu.version_r .re l.got
.rel.plt .init .plt .text .fini .rodata
   03     .data .eh_frame .ctors .dtors .got .dynamic .bss
   04     .dynamic
   05     .note.ABI-tag

 プログラムヘッダ中に存在するインタプリタ情報(INTERP)を見てください.プログラムインタプリタとして/lib/ld-linux-so.2を呼び出すように設定されていることがわかります.helloを実行すると,まず最初にプログラムインタプリタ/lib/ld-linux-so.2が起動され,hello内部で未定義になっているprintf関数のエントリアドレスが解決されます.そのうえで初めてhelloコードが実行されるのです.この状況を実際に自分の目で確かめてみましょう.strace(System call TRACE)コマンドは指定されたプログラムの実行状況をシステムコールレベルで追跡するためのものです(リスト7).
〔リスト7〕helloのシステムコールのトレース結果

$ strace ./hello
execve("./hello", ["./hello"], [/* 22 vars */]) = 0
uname({sys="Linux", node="mebius", ...}) = 0
brk(0)                                  = 0x804957c
open("/etc/ld.so.preload", O_RDONLY)    = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=1, ...}) = 0
old_mmap(NULL, 1, PROT_READ|PROT_WRITE, MAP_PRIVATE, 3, 0) = 0x40016000
close(3)                                = 0
munmap(0x40016000, 1)                   = 0
open("/etc/ld.so.cache", O_RDONLY)      = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=19190, ...}) = 0
old_mmap(NULL, 19190, PROT_READ, MAP_PRIVATE, 3, 0) = 0x40016000
close(3)                                = 0
open("/lib/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(\327\1"..., 1024) = 1024
fstat64(3, {st_mode=S_IFREG|0755, st_size=1171196, ...}) = 0
old_mmap(NULL, 1187968, PROT_READ|PROT_EXEC, MAP_PRIVATE, 3, 0) = 0x4001b000
mprotect(0x40133000, 41088, PROT_NONE)  = 0
old_mmap(0x40133000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED, 3, 0x117000)
 = 0x40133000
old_mmap(0x40139000, 16512, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS
, -1, 0) = 0x40139000
close(3)                                = 0
munmap(0x40016000, 19190)               = 0
fstat64(1, {st_mode=S_IFCHR|0600, st_rdev=makedev(136, 0), ...}) = 0
old_mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x4001
6000
write(1, "Hello, world!\n", 14Hello, world!
)         = 14
munmap(0x40016000, 4096)                = 0
_exit(123)                              = ?

以降の内容は本誌を参照ください

インデックス
序章
 1.PC-UNIXの普及と従来型パッケージの限界
 2.Linuxの理解をはばむもの
 3.本質を見極めよう
Chapter1
 1.プログラムの再考
 2.共有ライブラリの正体

今月号特集トップページへ戻る


Copyright 2002 西田 亙