[前へ] [目次へ] [次へ]
コンパイルと実行の裏側(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 |
---|
sel | int | -328 |
next | int | -324 |
n | int | -320 |
now | int | -316 |
loopcnt | int | -312 |
numbers | int[10] | -308 |
i | int | -268 |
cmpres | int | -264 |
buf | char[256] | -260 |
n2 | int | -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