Emacs の dump-emacs 関数の怪
Emacs ほどの歴史の深いソフトウェアになると、他のソフトウェアには見られないような奇妙なトリックがいくつかあるものです。今回はその中の一つである dump-emacs 関数について説明したいと思います。
dump-emacs 関数は、現在実行中の状態を実行可能ファイルとして永続化して、後から復元して実行できるようにする関数です。一般的には Emacs の起動の高速化のために使われますが、百聞は一見にしかず、実際にデモしてみます。
$ /usr/bin/emacs --version GNU Emacs 23.0.60.1 Copyright (C) 2008 Free Software Foundation, Inc. GNU Emacs comes with ABSOLUTELY NO WARRANTY. You may redistribute copies of Emacs under the terms of the GNU General Public License. For more information about these matters, see the file named COPYING. $ /usr/bin/emacs --batch --eval '(progn (switch-to-buffer "*scratch*") (insert "Hello from myemacs.") (dump-emacs "myemacs" "/usr/bin/emacs"))'
上記のコマンドを実行すると、カレントディレクトリに myemacs という実行可能ファイルが生成されます。この myemacs は *scratch* バッファに insert した状態から実行できるオレオレ Emacs です。実際に myemacs を実行すると *scratch* バッファに "Hello from myemacs." と書き込まれていることを確認できると思います。
$ # *scratch* バッファの内容を確認 $ ./myemacs --batch --eval '(with-current-buffer "*scratch*" (princ (buffer-string)))' Hello from myemacs.
myemacs は insert された状態から実行を開始するのであって、起動のたびに insert しているのではないことに注意してください。これは以下によって確認することができます。
$ # *scratch* バッファに現在時刻を書き込んで dump-emacs $ /usr/bin/emacs --batch --eval '(let ((time (current-time-string))) (switch-to-buffer "*scratch*") (insert time) (dump-emacs "myemacs" "/usr/bin/emacs") (princ time))' Wed Oct 15 20:55:44 2008 $ date 2008年 10月 15日 水曜日 20:56:30 JST $ ./myemacs --batch --eval '(with-current-buffer "*scratch*" (princ (buffer-string)))' Wed Oct 15 20:55:44 2008
実は dump-emacs 関数は Emacs のビルドプロセスで使われています。インストールされている Emacs をちょっと覗いてみましょう。
$ ls -lL /usr/bin/emacs -rwxr-xr-t 1 root root 25410574 2008-09-13 18:29 /usr/bin/emacs $ size -A /usr/bin/emacs | grep -E '^(section|\.data|\.bss)' section size addr .data 2165976 8259776 .data 20957792 10425760 .bss 0 31383552
不思議な点が二つあります。一つは .data セクションが二つあること、もう一つは .bss セクションが空になっていることです。
ここで .data セクションと .bss セクションについて簡単な説明をしておきます。プログラムが利用するデータ領域には一般的に .data セクションと .bss セクションとがあり、 .data セクションはプログラムが利用するデータを保持する領域で、 .bss セクションはプログラム実行時にゼロで初期化されるデータを保持する領域です。 C 言語で言えば、初期値を持つグローバル変数や静的変数が .data セクションに配置され、初期値を持たないグローバル変数や静的変数が .bss セクションに配置されます [1] 。
[1] | このあたりはコンパイラ依存かもしれません |
test.c:
/* .data セクションに配置される */ /* コンパイル時に初期化される */ int data1 = 123; /* .bss セクションに配置される */ /* プログラム実行時に初期化される */ int data2; int main(int argc, char *argv[]) { printf("%d %d\n", data1, data2); return 0; }
$ gcc test.c $ ./a.out 123 0 $ objdump -t a.out | grep 'data[1234]' 0000000000601038 g O .bss 0000000000000004 data2 0000000000601020 g O .data 0000000000000004 data1
少し話が脱線しましたが、インストールされている Emacs の .data セクションや .bss セクションが不思議なことになっているのは dump-emacs 関数による影響です。というのも、インストールされている Emacs は、リンカによって生成された直接の成果物ではなくて、 dump-emacs 関数によって細工が施された間接的な成果物なのです。その辺りは Emacs のビルドプロセスを覗けば把握できます。 emacs 実行可能ファイルができるまでの過程は大体以下のようになります。
- C ファイルをコンパイルする
- オブジェクトファイルをリンクして tmeacs 実行可能ファイルを生成する
- temacs --batch --load loadup bootstrap を実行して emacs 実行可能ファイルを生成する
以上から、 Emacs をビルドすると tmeacs と emacs という二種類の Emacs が出来ることがわかります。実際に Emacs をビルドして確かめてみましょう。
$ ./configure $ make ... $ ls -l src/{t,}emacs -rwxr-xr-x 3 tomo tomo 30936776 2008-10-14 20:29 src/emacs -rwxr-xr-x 1 tomo tomo 9979560 2008-10-14 20:27 src/temacs $ # .bss セクションが空じゃない $ size -A src/temacs | grep -E '^(section|\.data|\.bss)' section size addr .data 2166552 8358144 .bss 415592 10524704 $ # .bss セクションが空になっている $ size -A src/emacs | grep -E '^(section|\.data|\.bss)' section size addr .data 2166552 8358144 .data 20957152 10524704 .bss 0 31481856
temacs はリンカによって生成された純粋な Emacs です。一方 emacs は loadup.el をあらかじめロードした高速版 Emacs です。どちらの Emacs も普通に起動することができますが、その起動速度差は歴然です。
$ time src/temacs --batch src/temacs --batch 3.77s user 0.06s system 99% cpu 3.867 total $ time src/emacs --batch src/emacs --batch 0.07s user 0.02s system 100% cpu 0.092 total
temacs のほうは起動時に loadup.el をロードしている分、起動に時間がかかるのですが、 emacs は loadup.el のロードを完全にスキップできるので高速なのです。これほどロードに時間がかかる loadup.el ですが、「 Emacs の機能」を装備するために内部では大量の Emacs Lisp をロードしていて、むしろ遅くないほうがおかしいのです。 loadup.el は Emacs の引数に bootstrap があると dump-emacs 関数を呼び出す機能をもっており、これがビルドプロセスに取り込まれているのです。
dump-emacs 関数は非常に奇怪な機能に見えますが、インストールされている Emacs が dump-emacs 関数で生成されたものと分かれば少しは信頼が増すのではないでしょうか。もし自分の Emacs の起動が遅いと思っているなら dump-emacs 関数を使う余地が十分あります。 .emacs をロードした状態の高速版 Emacs の作り方は以下のようになります。
$ /usr/bin/emacs --batch --load ~/.emacs --eval '(dump-emacs "myemacs" "/usr/bin/emacs")'
myemacs を起動すると非常に速くなっていることがわかるでしょう。ただし意図せず状態が復元されてプログラムの実行がおかしくなることがあります。そのような場合は .emacs を dump-emacs できるように修正する必要があります。
また、 myemacs は初期化ファイルを全くロードしません。何かしらロードさせたい場合は以下のように myemacs を生成するとよいでしょう。
$ # 起動時に ~/.fast-emacs をロードする Emacs $ /usr/bin/emacs --batch --load ~/.emacs --eval '(progn (add-hook 'emacs-startup-hook (lambda () (load "~/.fast-emacs"))) (dump-emacs "myemacs" "/usr/bin/emacs"))'
さて、 dump-emacs 関数の使い方はこれぐらいにして dump-emacs 関数の中身について説明しましょう。
dump-emacs 関数が呼び出されると、その内部で unexec 関数が呼び出されます。 実行可能ファイルからメモリイメージを読み込んで実行する exec 系関数に対して、 unexec 関数は現在のメモリイメージを永続化するのでそのような名前になっているのでしょう。 unexec 関数には実行可能ファイルフォーマットごとに実装が存在し、例えば ELF の場合は src/unexelf.c にその実装が記述してあります。ここでは実行可能ファイルフォーマットが ELF であることを前提にして話を進めます。
src/unexelf.c に記述してある unexec 関数は、元の実行可能ファイル [2] の各セクションをコピーしつつ細工を施します。元の実行可能ファイルの中で .bss セクションを見つけると、現在実行中のメモリイメージの該当の領域を .data セクションとしてコピーして、その .bss セクションの直前に挿入します。さらにその .bss セクションは空にしておきます。これにより .bss セクションのデータが起動時にゼロで初期化されず unexec 時の状態を復元する(ように見える)ようになります。
src/unexelf.c はポータブルなのでちょっとしたデモをやってみましょう。
[2] | dump-emacs 関数の第二引数 |
unexec.c:
extern void unexec (char *new_name, char *old_name, unsigned data_start, unsigned bss_start, unsigned entry_address); /* .bss セクション */ int initialized; char my_edata[]; int main(int argc, char *argv[]) { if (!initialized) { initialized = 1; unexec ("myunexec", "unexec", my_edata, 0, 0); puts("initialized"); } else { puts("already initialized"); } return 0; }
$ gcc -o unexec unexec.c unexelf.c $ ./unexec initialized $ ./myunexec already initialized
このように initialized 変数が .bss セクションに配置されているのにもかかわらず、 unexec したときの状態を復元して実行することができます。面白いですね。勘のいい人は気付いているかもしれませんが、 temacs が起動時に loadup.el をロードするかどうかも上記のような初期化済みフラグ変数によって決定しています。つまり temacs では初期化済みフラグ変数が false で、 emacs では初期化済みフラグ変数が true になるようにしているのです。
さらに面白いのは unexec 関数は malloc(3) したデータも永続化するという点です。
unexec.c:
extern void unexec (char *new_name, char *old_name, unsigned data_start, unsigned bss_start, unsigned entry_address); char *data; char my_edata[]; int main(int argc, char *argv[]) { if (data == NULL) { data = strdup("Hello, world"); /* malloc */ unexec ("myunexec", "unexec", my_edata, 0, 0); puts("initialized"); } puts(data); return 0; }
$ gcc -o unexec unexec.c unexelf.c $ ./unexec initialized Hello, world $ ./myunexec Hello, world
malloc(3) は要求されたメモリサイズがある閾値未満の場合は sbrk(2) [3] でデータセグメントを拡張してメモリを確保します。データセグメントの末尾アドレスは sbrk(2) の引数を 0 として呼び出すことで取得できます。 unexec 関数が .data セクションを生成する際、 sbrk(2) を呼び出して末尾アドレスを取得し、拡張されたデータセグメント分も .data セクションにコピーしています。だからこのような芸当ができるのです。
[3] | たぶん実装依存。閾値を越えると mmap(2) される |
以下のサンプルの場合、 unexec 関数は 6295592 から 6434816 までの領域のデータをコピーして .data を作ります。
test.c:
int main(int argc, char *argv[]) { printf("%ld\n", malloc(1)); printf("%ld\n", sbrk(0)); return 0; }
$ gcc test.c $ size -A a.out | grep -E '^(section|\.data|\.bss)' section size addr .data 16 6295576 .bss 16 6295592 $ ./a.out 6299664 6434816
以上から dump-emacs 関数がどのように動作し、生成された実行可能ファイルがどのように実行されるかがある程度理解できたと思います Emacs のソースコードはなかなか面白く、案外綺麗なので読んでみる価値があります。
- The URL to Trackback this entry is:
- http://dev.ariel-networks.com/Members/matsuyama/dump-emacs/tbping