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

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

1 Comment

  1. 混乱することもあるかもしれません。

Leave a Reply

Your email address will not be published. Required fields are marked *

CAPTCHA


© 2020 栗林健太郎

Theme by Anders NorénUp ↑