Kentaro Kuribayashi's blog

Software Engineering, Management, Books, and Daily Journal.

『Cプログラムの中身がわかる本』感想

「ポチのプログラミング講座」と銘打たれた『Cプログラミングの中身がわかる本』という書籍を読みました。どこで知ったのか忘れてしまいましたが、どなたかのブログで良書として紹介されていた本です。

Cプログラムの中身がわかる本

Cプログラムの中身がわかる本

タイトルや表紙など、一見「なんだこれ……」という感じなのですが、中身はプログラムにおける様々な要素(四則演算、制御構文、配列・ポインタ・構造体を用いたプログラム、マルチスレッド等)を、簡単な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

上記で行われていることを簡単に説明すると:

  1. EBP-36 〜 EBP-16まで(int型の配列なので4バイトずつ)に、配列の各要素を格納する
    • Cのコードのint n[] = {1, 3, 5, 7, 11, -1};の箇所
  2. EBP-36の値(配列先頭の1)が収められているアドレス(配列nの先頭アドレス)を、EAXを経由してEBP-12に保存
    • p = n
  3. L2へジャンプ
  4. EBP-12のアドレス(p)にある値(*p)を-1と比較
    • 以上、while ( *p != -1 )
  5. 比較した結果、ふたつの値が異なったらL3へジャンプ
    • whileループの中へ入る
  6. pのアドレスにある値(*p)をEDXに保存
    • *pを先に評価
  7. pのアドレスをEAXに保存し、4を足す(int型の配列なので)
    • p++として、pをインクリメント
  8. EDXにある値をESP+4(スタック)にのせる
  9. L0の値("%d ")をEAX経由でスタック(ESP)にのせる
  10. printfを呼び出す(スタックにのせた値2つが引数として使われる)
    • 以上、printf("%d ", *p++);の箇所
  11. またwhile ( *p != 1 )が評価される
  12. while内条件がfalseになったらL3へは飛ばずにそのまま進む
  13. 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;
}

gcc -S malloc.cの結果は以下の通り。

        .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)が実際にはどのようにして、どこにメモリを確保しているのかというのがよくわかりません。それを知るのはどうすればいいのだろう……というのが、とりあえず本書を読了後の宿題として残ったのでした。

ひとつ本書(初版)に苦言を呈するとすれば、誤植がとても多いです。注意深く読めば特に迷うことはないと思いますが、混乱することもあるかもしれません。