[前へ]   [目次へ]   [次へ]

コンパイルと実行の裏側(2)(C/C++)


今回は、前回のソースを実際にVisual C++ 6.0でコンパイルしてみました。

以下は、Visual C++のデバッガの機能を使って実行コードを表示させたものです。
これは完全にアセンブラの世界になっているので、違う世界ではあるのですが、
「実際にはこんな風にコンパイルされてるんだな〜」というのを感じていただければと思います。

とりあえず、今回は赤字にしたアセンブラコードの部分に注目していきます。

紫字:ソースコード 青字:アセンブラコード 赤字:注目してほしいアセンブラコード
1:    #include <stdio.h>
2:    #include <stdlib.h>
3:    #include <time.h>
4:
5:    int main(void){

00401000   push        ebp
00401001   mov         ebp,esp
00401003   sub         esp,14Ch

00401009   push        edi
6:        int now;//現在値
7:        int sel=0;//プレイヤーの選択(選択結果)

0040100A   mov         dword ptr [ebp-148h],0
8:        int next;//次回値
9:        int loopcnt;//成功回数
10:       char buf[256]="";//入力バッファ

00401014   mov         al,[string "" (0040ca80)]
00401019   mov         byte ptr [ebp-104h],al
0040101F   mov         ecx,3Fh
00401024   xor         eax,eax
00401026   lea         edi,[ebp-103h]
0040102C   rep stos    dword ptr [edi]
0040102E   stos        word ptr [edi]
00401030   stos        byte ptr [edi]

11:       int cmpres=0;//現在値と次回値の比較結果
00401031   mov         dword ptr [ebp-108h],0
12:
13:       int numbers[10]={0,1,2,3,4,5,6,7,8,9};//出現させる数字配列

0040103B   mov         dword ptr [ebp-134h],0
00401045   mov         dword ptr [ebp-130h],1
0040104F   mov         dword ptr [ebp-12Ch],2
00401059   mov         dword ptr [ebp-128h],3
00401063   mov         dword ptr [ebp-124h],4
0040106D   mov         dword ptr [ebp-120h],5
00401077   mov         dword ptr [ebp-11Ch],6
00401081   mov         dword ptr [ebp-118h],7
0040108B   mov         dword ptr [ebp-114h],8
00401095   mov         dword ptr [ebp-110h],9

14:       int i,n,n2;
15:       //randを初期化
16:       srand(time(NULL));

0040109F   push        0
004010A1   call        _time (00401549)
004010A6   add         esp,4
004010A9   push        eax
004010AA   call        _srand (00401521)
004010AF   add         esp,4

17:       //前回の方法で数字配列をシャッフル
18:       for(i=10;i;i--){

004010B2   mov         dword ptr [ebp-10Ch],0Ah
004010BC   jmp         _main+0CDh (004010cd)

004010BE   mov         ecx,dword ptr [ebp-10Ch]
004010C4   sub         ecx,1
004010C7   mov         dword ptr [ebp-10Ch],ecx

004010CD   cmp         dword ptr [ebp-10Ch],0
004010D4   je          _main+124h (00401124)

19:           n=rand()%i;
004010D6   call        _rand (0040152b)
004010DB   cdq
004010DC   idiv        eax,dword ptr [ebp-10Ch]
004010E2   mov         dword ptr [ebp-140h],edx

20:           n2=numbers[i-1];
004010E8   mov         edx,dword ptr [ebp-10Ch]
004010EE   mov         eax,dword ptr [ebp+edx*4-138h]
004010F5   mov         dword ptr [ebp-4],eax

21:           numbers[i-1]=numbers[n];
004010F8   mov         ecx,dword ptr [ebp-10Ch]
004010FE   mov         edx,dword ptr [ebp-140h]
00401104   mov         eax,dword ptr [ebp+edx*4-134h]
0040110B   mov         dword ptr [ebp+ecx*4-138h],eax

22:           numbers[n]=n2;
00401112   mov         ecx,dword ptr [ebp-140h]
00401118   mov         edx,dword ptr [ebp-4]
0040111B   mov         dword ptr [ebp+ecx*4-134h],edx

23:       }
00401122   jmp         _main+0BEh (004010be)
24:       //初期値を代入
25:       now=numbers[0];

00401124   mov         eax,dword ptr [ebp-134h]
0040112A   mov         dword ptr [ebp-13Ch],eax

26:       //ループ
27:       for(loopcnt=0;loopcnt<9;loopcnt++){//(1)

00401130   mov         dword ptr [ebp-138h],0
0040113A   jmp         _main+14Bh (0040114b)
0040113C   mov         ecx,dword ptr [ebp-138h]
00401142   add         ecx,1
00401145   mov         dword ptr [ebp-138h],ecx
0040114B   cmp         dword ptr [ebp-138h],9
00401152   jge         _main+236h (00401236)

28:           printf("現在の値:%d\n"
29:               "1:現在値<次回値 2:現在値>次回値\n"
30:               "次回はどうなると思いますか?:",now);

00401158   mov         edx,dword ptr [ebp-13Ch]
0040115E   push        edx
0040115F   push        offset string "\x8c\xbb\x8d\xdd\x82\xcc\x92l\x81F%d\n1:\x8c\xbb\x8d\xdd\x92l<\x8e\x9f\x8
00401164   call        _printf (004014f0)
00401169   add         esp,8

31:           fgets(buf,255,stdin);
0040116C   push        offset __iob (0040a0c8)
00401171   push        0FFh
00401176   lea         eax,[ebp-104h]
0040117C   push        eax
0040117D   call        _fgets (004013e0)
00401182   add         esp,0Ch

32:           if((buf[0]>='1')&&(buf[0]<='2')){
00401185   movsx       ecx,byte ptr [ebp-104h]
0040118C   cmp         ecx,31h
0040118F   jl          _main+1ADh (004011ad)
00401191   movsx       edx,byte ptr [ebp-104h]
00401198   cmp         edx,32h
0040119B   jg          _main+1ADh (004011ad)

33:               sel=buf[0]-'0';
0040119D   movsx       eax,byte ptr [ebp-104h]
004011A4   sub         eax,30h
004011A7   mov         dword ptr [ebp-148h],eax

34:           }
35:           next=numbers[loopcnt+1];//次回値を代入

004011AD   mov         ecx,dword ptr [ebp-138h]
004011B3   mov         edx,dword ptr [ebp+ecx*4-130h]
004011BA   mov         dword ptr [ebp-144h],edx

36:           if(now<next){
004011C0   mov         eax,dword ptr [ebp-13Ch]
004011C6   cmp         eax,dword ptr [ebp-144h]
004011CC   jge         _main+1DAh (004011da)

37:               cmpres=1;
004011CE   mov         dword ptr [ebp-108h],1
38:           }
39:           else{

004011D8   jmp         _main+1E4h (004011e4)
40:               cmpres=2;
004011DA   mov         dword ptr [ebp-108h],2
41:           }
42:           now=next;

004011E4   mov         ecx,dword ptr [ebp-144h]
004011EA   mov         dword ptr [ebp-13Ch],ecx

43:           if(sel==cmpres){
004011F0   mov         edx,dword ptr [ebp-148h]
004011F6   cmp         edx,dword ptr [ebp-108h]
004011FC   jne         _main+214h (00401214)

44:               printf("成功!%dでした。\n\n",now);
004011FE   mov         eax,dword ptr [ebp-13Ch]
00401204   push        eax
00401205   push        offset string "\x90\xac\x8c\xf7\x81I%d\x82\xc5\x82\xb5\x82\xbd\x81B\n\n" (0040a068)
0040120A   call        _printf (004014f0)
0040120F   add         esp,8

45:           }
46:           else{

00401212   jmp         _main+231h (00401231)
47:               //成功数表示
48:               printf("失敗!%dでした。\n"
49:                   "%d回成功しました。\n",now,loopcnt);

00401214   mov         ecx,dword ptr [ebp-138h]
0040121A   push        ecx
0040121B   mov         edx,dword ptr [ebp-13Ch]
00401221   push        edx
00401222   push        offset string "\x8e\xb8\x94s\x81I%d\x82\xc5\x82\xb5\x82\xbd\x81B\n%d\x89\xf1\x90\xac\x8c
00401227   call        _printf (004014f0)
0040122C   add         esp,0Ch

50:               break;//ゲーム終了
0040122F   jmp         _main+236h (00401236)
51:           }
52:       }

00401231   jmp         _main+13Ch (0040113c)
53:       if(loopcnt==9){//(2)失敗してなければloopcntは9になるはず
00401236   cmp         dword ptr [ebp-138h],9
0040123D   jne         _main+24Ch (0040124c)

54:           puts("ゲームクリア!");
0040123F   push        offset string "\x83Q\x81[\x83\x80\x83N\x83\x8a\x83A\x81I" (0040a030)
00401244   call        _puts (00401379)
00401249   add         esp,4

55:       }
56:       //終了待ち
57:       getchar();

0040124C   mov         eax,[__iob+4 (0040a0cc)]
00401251   sub         eax,1
00401254   mov         [__iob+4 (0040a0cc)],eax
00401259   cmp         dword ptr [__iob+4 (0040a0cc)],0
00401260   jl          _main+286h (00401286)
00401262   mov         ecx,dword ptr [__iob (0040a0c8)]
00401268   movsx       edx,byte ptr [ecx]
0040126B   and         edx,0FFh
00401271   mov         dword ptr [ebp-14Ch],edx
00401277   mov         eax,[__iob (0040a0c8)]
0040127C   add         eax,1
0040127F   mov         [__iob (0040a0c8)],eax
00401284   jmp         _main+299h (00401299)
00401286   push        offset __iob (0040a0c8)
0040128B   call        __filbuf (004012a0)
00401290   add         esp,4
00401293   mov         dword ptr [ebp-14Ch],eax

58:       return 0;
00401299   xor         eax,eax
59:   }
0040129B   pop         edi
0040129C   mov         esp,ebp
0040129E   pop         ebp
0040129F   ret


アセンブラコードから、各変数に割り当てられた相対IDを調べてみました。
main 関数ブロック ネームテーブル
名前型名相対ID
selint-328
nextint-324
nint-320
nowint-316
loopcntint-312
numbersint[10]-308
iint-268
cmpresint-264
bufchar[256]-260
n2int-4
Visual C++はどうやら相対IDを負値に割り当てているようです。
加えてソースコード上で変数を宣言/定義した順序とは全く関係ない順番になっています。

ちなみにどうやって判断しているかというと、
0040100A   mov         dword ptr [ebp-148h],0
とかの ebp-148h という部分です。
ebp に確保したメモリIDの最大値が入っていて、
そこから -148h (hはアセンブラでの16進数を意味する)している、
ということは、ここでアクセスしたい変数の相対IDが -148h 、つまり-328です。
対応するソースは
7:        int sel=0;//プレイヤーの選択(選択結果)
となっているので、ここから sel の相対IDが-328であるということが分かります。


それでは、赤字にした部分を見ていきたいと思います。
00401001   mov         ebp,esp
00401003   sub         esp,14Ch

この部分は「ローカル変数用のメモリ確保」の処理です。
ローカル変数用のメモリはプログラムが起動すると同時に、
ある程度のサイズが確保されています。
そして、その部分を後から「何バイト使うぜ」と持っていくことで確保します。
esp というのはこの確保済みの領域のどこからが
利用可能かという情報が格納されている(ことになっている)レジスタです。
このレジスタを変更することで「利用中ということにする」という処理になります。

なお、 mov というのは「代入」で、 sub というのは「減算」です。
C言語的にはここで見ている二個の命令は
ebp=esp;
esp-=0x14C;

といった感じになります。


0040100A   mov         dword ptr [ebp-148h],0
この部分は、 ebp-148h の値の絶対IDから4バイトの領域に0を代入する。
という多少ややこしい記述です。
とはいえ、対応するソースの通り、単に int 型の実体に0を代入しているだけです。
少々仰々しい書き方になっていますが、この部分こそが「ポインタの実態」なので、
慣れておくようにしてください。

C言語的にはここで見ている命令は
*(DWORD*)(ebp-0x148)=0;
といった感じになります。(DWORDとは4バイト、符号なし、整数というWindowsで使う型)
ポインタ記述がウヨウヨなのですが、 dword ptr というのがそういうものなのです。
ポインタに関しては「コンパイルと実行の裏側」の次で行う予定です。


0040103B   mov         dword ptr [ebp-134h],0
00401045   mov         dword ptr [ebp-130h],1
0040104F   mov         dword ptr [ebp-12Ch],2
00401059   mov         dword ptr [ebp-128h],3
00401063   mov         dword ptr [ebp-124h],4
0040106D   mov         dword ptr [ebp-120h],5
00401077   mov         dword ptr [ebp-11Ch],6
00401081   mov         dword ptr [ebp-118h],7
0040108B   mov         dword ptr [ebp-114h],8
00401095   mov         dword ptr [ebp-110h],9

配列の初期化です。[0]から[9]まで順に0〜9を代入しているわけですが、
第57回 で解説したように、
int 型のサイズ(4バイト)づつ離れて配置されていることが分かります。


004010B2   mov         dword ptr [ebp-10Ch],0Ah
004010BC   jmp         _main+0CDh (004010cd)

この部分は for ループの初回部分です。
最初に i(ebp-10Ch) に 0Ah(10) を代入します。
次の jmp というのは、「指定した位置に次の実行座標を設定する」という命令です。
なので、004010BCの jmp 命令が実行されると、次に実行されるのは004010cd、
004010CD   cmp         dword ptr [ebp-10Ch],0
この命令になります。


004010CD   cmp         dword ptr [ebp-10Ch],0
004010D4   je          _main+124h (00401124)

この部分は for ループの継続条件判定部分です。
最初の cmp 命令で i(ebp-10Ch) と0を比較して、続く je 命令で分岐します。
この je 命令は比較系の命令の実行後に残った比較結果を使って、
指定された場所に移動するか、何もしないかが切り替わります。
つまり、この比較+条件付き移動の命令は事実上ペアとして動作します。
この場合は i(ebp-10Ch) が0だと00401124の命令が次の実行座標として設定されます。
ソース上とは逆に、離脱条件が書いてある点がポイントですが、
これだと何故ループには継続条件を書くようになっているのか、若干謎ですね(笑)


004010F8   mov         ecx,dword ptr [ebp-10Ch]
004010FE   mov         edx,dword ptr [ebp-140h]
00401104   mov         eax,dword ptr [ebp+edx*4-134h]
0040110B   mov         dword ptr [ebp+ecx*4-138h],eax

numbers[i-1]=numbers[n];
という配列への処理部分です。
004010F8   mov         ecx,dword ptr [ebp-10Ch]
004010FE   mov         edx,dword ptr [ebp-140h]

まず ecx に i(ebp-10Ch) を、 edx に n(ebp-140h) を代入しています。
00401104   mov         eax,dword ptr [ebp+edx*4-134h]
続いて eax に配列要素を代入していますが、この表記をみると、
numbers(ebp-134h) に加えて +edx*4 と書いてあります。
4は numbers の型である int 型のサイズです。
以前解説したように、実際に「1個目のアドレス+要素番号*型のサイズ」が計算されることが分かります。
0040110B   mov         dword ptr [ebp+ecx*4-138h],eax
最後に eax を numbers[i-1] に代入しようとしています。
今回は -138h と書いてありますが、これは本来の ebp+ecx*4-1*4-134h の定数項(-1*4-134h)を先に計算したものです。


00401122   jmp         _main+0BEh (004010be)
ループの終端にある繰り返し用の移動命令です。
移動先はこのループの続行式の位置になっています。


004011C0   mov         eax,dword ptr [ebp-13Ch]
004011C6   cmp         eax,dword ptr [ebp-144h]
004011CC   jge         _main+1DAh (004011da)

if
 による分岐の判定ですが、ループの判定とパターンとしては同じです。
ここでもやはり条件が満たされなかった場合に実行位置が移動されるように編成されています。
この時の移動先は else ブロックの開始位置になっているので、条件判定は一度しか存在しません。


004011D8   jmp         _main+1E4h (004011e4)
if
 ブロックの終端に置いてある、 else ブロックをスキップするための移動命令です。


0040122F   jmp         _main+236h (00401236)
break
 に対応する命令ですが、単純にループの終了位置に移動しているだけであることがわかります。


0040129C   mov         esp,ebp
最後です。開始時に保存しておいた確保位置を復帰させることによって、
「確保済み」の領域情報を元に戻し、再利用可能にしています。
この結果、ローカル変数に当てられていた領域は「未割り当て」の状態としてマークされます。
C++だとここで破棄処理とか入る場合もあるのですが、今回はこれだけです。
マークを変えただけなので、この後もここで作成したローカル変数にある程度の時間、
正常にアクセスすることができてしまいますが、
いつ再利用されて別の値に書き換わるか分からないので、
「未割り当て」状態になった領域の変数へのアクセスはタチの悪いバグになることになります。



次回からはポインタについての予定です。

[前へ]   [目次へ]   [次へ]

プログラミング講座 総合目次

最終更新 2008/10/17