『Cプログラムの中身がわかる本』感想
「ポチのプログラミング講座」と銘打たれた『Cプログラミングの中身がわかる本』という書籍を読みました。どこで知ったのか忘れてしまいましたが、どなたかのブログで良書として紹介されていた本です。
- 作者: 日向俊二
- 出版社/メーカー: 翔泳社
- 発売日: 2008/02/20
- メディア: 単行本(ソフトカバー)
- 購入: 4人 クリック: 42回
- この商品を含むブログ (14件) を見る
タイトルや表紙など、一見「なんだこれ……」という感じなのですが、中身はプログラムにおける様々な要素(四則演算、制御構文、配列・ポインタ・構造体を用いたプログラム、マルチスレッド等)を、簡単なCのコードとそれに対応するアセンブラのコードによって、実際にどのようにプログラムが実行されるのかを逐一解説した本で、僕など、理屈ではわかってるつもりでも実際にはあまり馴染みのない話なので、ためになりました。
たとえばポインタの章は、以下のプログラムについて検討します。
pointer.c
#include <stdio.h> int main (int argc, char* argv[]) { int n[] = {1, 3, 5, 7, 11, -1}; int *p; p = n; while ( *p != -1 ) printf("%d ", *p++); return 0; }
これを次の通りにコマンドを実行し、アセンブラを書き出します。
$ gcc -S pointer.c
その結果がが以下(本書の環境はWindows + Cygwinで僕の環境はMac OSXなので、実際に書籍に掲載されているのとはちょっと違います)。この内容を、本書はひとつづつ追いながら丁寧に説明していきます。
pointer.s
.cstring LC0: .ascii "%d \0" .text .globl _main _main: pushl %ebp movl %esp, %ebp pushl %ebx subl $52, %esp call L6 "L00000000001$pb": L6: popl %ebx movl $1, -36(%ebp) movl $3, -32(%ebp) movl $5, -28(%ebp) movl $7, -24(%ebp) movl $11, -20(%ebp) movl $-1, -16(%ebp) leal -36(%ebp), %eax movl %eax, -12(%ebp) jmp L2 L3: movl -12(%ebp), %eax movl (%eax), %edx leal -12(%ebp), %eax addl $4, (%eax) movl %edx, 4(%esp) leal LC0-"L00000000001$pb"(%ebx), %eax movl %eax, (%esp) call L_printf$stub L2: movl -12(%ebp), %eax movl (%eax), %eax cmpl $-1, %eax jne L3 movl $0, %eax addl $52, %esp popl %ebx leave ret .section __IMPORT,__jump_table,symbol_stubs,self_modifying_code+pure_instructions,5 L_printf$stub: .indirect_symbol _printf hlt ; hlt ; hlt ; hlt ; hlt .subsections_via_symbols
上記で行われていることを簡単に説明すると:
- EBP-36 〜 EBP-16まで(int型の配列なので4バイトずつ)に、配列の各要素を格納する
- Cのコードの
int n[] = {1, 3, 5, 7, 11, -1};
の箇所
- Cのコードの
- EBP-36の値(配列先頭の1)が収められているアドレス(配列nの先頭アドレス)を、EAXを経由してEBP-12に保存
p = n
- L2へジャンプ
- EBP-12のアドレス(p)にある値(*p)を-1と比較
- 以上、
while ( *p != -1 )
- 以上、
- 比較した結果、ふたつの値が異なったらL3へジャンプ
- whileループの中へ入る
- pのアドレスにある値(*p)をEDXに保存
*p
を先に評価
- pのアドレスをEAXに保存し、4を足す(int型の配列なので)
p++
として、pをインクリメント
- EDXにある値をESP+4(スタック)にのせる
- L0の値("%d ")をEAX経由でスタック(ESP)にのせる
- printfを呼び出す(スタックにのせた値2つが引数として使われる)
- 以上、
printf("%d ", *p++);
の箇所
- 以上、
- また
while ( *p != 1 )
が評価される - while内条件がfalseになったらL3へは飛ばずにそのまま進む
return 0
して終了
……とまあ、こんな具合です。こうやって見てみると、ポインタは単にアドレスを保存していて、それが++されたりするとこの場合はint型の配列の先頭アドレスを差しているので、差している箇所が4バイトずつ動いていくというだけってな、ポインタといっても特に難しいことはないのだなあという感じでした。
そこで今度はmallocして確保したポインタで似たようなことをやってみたらどうなるんだろうなーってんで、次のようなコードを試してみました(こんなコードは普通書かないと思いますが、実験なので……)。
malloc.c
#include <stdio.h> #include <stdlib.h> int main () { int i; size_t size = 5; char *str; str = (char *)malloc(sizeof(char) * size); for (i = 0; i < size; i++) { str[i] = 0x41 + i; } puts(str); free(str); return 0; }
.text .globl _main _main: pushl %ebp movl %esp, %ebp subl $40, %esp movl $5, -16(%ebp) movl -16(%ebp), %eax movl %eax, (%esp) call L_malloc$stub movl %eax, -12(%ebp) movl $0, -20(%ebp) jmp L2 L3: movl -20(%ebp), %eax leal 65(%eax), %edx movl -20(%ebp), %eax addl -12(%ebp), %eax movb %dl, (%eax) leal -20(%ebp), %eax incl (%eax) L2: movl -20(%ebp), %eax cmpl -16(%ebp), %eax jb L3 movl -12(%ebp), %eax movl %eax, (%esp) call L_puts$stub movl -12(%ebp), %eax movl %eax, (%esp) call L_free$stub movl $0, %eax leave ret .section __IMPORT,__jump_table,symbol_stubs,self_modifying_code+pure_instructions,5 L_malloc$stub: .indirect_symbol _malloc hlt ; hlt ; hlt ; hlt ; hlt L_free$stub: .indirect_symbol _free hlt ; hlt ; hlt ; hlt ; hlt L_puts$stub: .indirect_symbol _puts hlt ; hlt ; hlt ; hlt ; hlt .subsections_via_symbols
やってることは、malloc(3)等を呼びだしたりしてる以外は、先のpointer.cとそんなに変わりません。が、これだけではmalloc(3)が実際にはどのようにして、どこにメモリを確保しているのかというのがよくわかりません。それを知るのはどうすればいいのだろう……というのが、とりあえず本書を読了後の宿題として残ったのでした。
ひとつ本書(初版)に苦言を呈するとすれば、誤植がとても多いです。注意深く読めば特に迷うことはないと思いますが、混乱することもあるかもしれません。