2011年11月24日木曜日

「宿題の解答」の解説

ある朝、私の部屋の前に問題が貼られていた話をブログで書かせていただきました(リンク)。この記事は色々反響を呼んでいます。
  • ログインせずにコメントが書きたい
    • ⇒ 承認制ですが、コメントが書けるようになりました
  • 「最後の計算結果を返り値とする」のはRubyの仕様です。Rubyじゃあるまし、何故?とtwitterで話題に(リンク)
  • 前回の記事は、情報工学科の1年生の授業であるC言語の採点をしているTAの疑問からでした。同様に、情報メディア科の1年生の授業であるC言語の採点をしているTAから、同じような現象にあい、答えを知りたいという、質問がある
そのため、解説記事を書かせていただきます。以前の貼紙(リンク)よりも丁寧に説明させていただきます。

説明を貼り付けた状態です(再掲)

■必要な知識1(レジスタとCPUの命令セットの知識)

コンピュータはメモリ上にある値を直接計算することができません。レジスタと呼ばれるCPUの中にあるメモリに一旦コピーしてから計算します。つまり、以下の3段階の手順を踏んで計算します。
  1. メモリからレジスタに値をコピー
  2. レジスタ上で計算
  3. レジスタの値をメモリにコピー
■制限されたC言語でアセンブラ(レジスタ)の知識を学ぶ

x86の命令セットを、C言語の世界の代入文で表現すると、以下の制約を持っているC言語と解釈することができます。
  • 演算は、「?=」という形式のみ(*=, +=, など)
  • 「?=」の左辺と右辺には単独の変数、または単独の定数しか書けない
  • 「?=」の左辺と右辺の両方に変数を書くことはできない
  • レジスタと呼ばれる特殊な(一時)変数がある(レジスタは、EAX、EBXなどの名前である)

例題1:C言語で「a = b;」という文は、この制限されたC言語では「EAX = a; b = EAX;」の2行で表されます。

例題2:C言語で「a = a * 13;」という文は、「EAX = a; EAX *= 13; a = EAX;」という3行の文で表されます。

例題3:C言語で「a = 3;」という文は、同様に「a = 3;」という1行で表されます。つまりレジスタには値が何も入りません。

■必要な知識2(関数の返り値の受け渡し方)

以下の話は、正確にはコンパイラ依存です。ですが、大体、以下のようになっています。関数に引数を渡すときは、スタックに値を積みます。関数の計算結果はレジスタを使って受け取ります。すなわち以下のような約束事で関数の値を受け取ります。
  • 関数が呼び出された側は、関数の終了直後に、計算結果をEAXレジスタに格納しておく
  • 関数を呼び出した側は、関数呼び出しから返ってきたら、すぐにEAXレジスタから値を取得する

■ソースコードを読み解く

ソースコードとアセンブル結果とコメントを載せておきます。

; 10   :   y = mul(x);
mov      eax, DWORD PTR _x$[ebp]    ’ 変数xの値をEAXレジスタに格納
push eax ’ EAXレジスタの値をスタックに積む(つまり、xの値を引数として渡す)
call     _mul’ mul関数を呼び出す
add      esp, 4’ スタックポインタをずらす(関数呼び出しの後始末)
mov      DWORD PTR _y$[ebp], eax’ EAXレジスタの値を変数yに格納
; 15   : int mul(int x) {
push      ebp
  mov       ebp, esp
  sub        esp, 192           ;000000c0H                                 
  push      ebx
  push      esi                                                        
  push      edi
  lea         edi, DWORD PTR [ebp-192]
  mov       ecx, 48                              ; 00000030H
  mov       eax, -858993460                   ; ccccccccH
  rep stosd
左の10行は、関数呼び出しのわずらわしい処理(気にしなくてよい)
; 16   :   x = x * 13;
mov       eax, DWORD PTR _x$[ebp]; EAX = x;
imul      eax, 13; EAX *= 13;
mov       DWORD PTR _x$[ebp], eax; x = EAX;
; このとき、EAXレジスタには、計算結果が残っている
; 17   : }; (EAXレジスタに入っている値を、呼び出し側が受け取る処理をする)

■上記の予備知識を踏まえて解説

つまり本来ならば「returnで渡す値はEAXレジスタに書き込んで置き」、そして、呼び出し側が受け取るわけです。returnがないので、EAXへの書き込みは、今回ありません。しかし、計算にも同じEAXレジスタを使うので、最後の計算結果が残ってしまっています。今回たまたま、「最後の計算結果の値」と「返す値」が同じなので、実際には値を返していないのに、返したのと同じ結果になっている、ということです。


■発展
幾つか類題(?)を出しておきます。

◆ 関数の中で、以下のようにローカル変数に代入したときは、a(b)の値が返ってきます。

int foo(int a)
{
    int b = a;
}

◆ 関数の中で、以下のようにローカル変数や引数に値を直接代入したときは、値は返しません。EAXレジスタを使わずに代入できるからです。

int foo(int a)
{
    int b = 0;
}

◆ 関数の中で、以下のように比較して大きい値を返そうとしたときは、値を返します。比較するために、値をEAXレジスタに格納するためです。

int myMax(int a, int b)
{
    int c = b;
    if (a > b) {
       c = a;
    }
    /* return文を書かない(書き忘れる) */
}

◆ Borland Cでは、return文が無いのでエラーになる。

◆ gccは、Visual Studioと同様の結果を返す。

0 件のコメント:

コメントを投稿