見出し画像

【C言語】コンパイラが何をしているのか、ちょっとのぞいてみよう その3 コンパイル

C言語のコンパイラというのは、次の4つのステップで成り立ちます。

(1)プリプロセッサ
(2)コンパイル
(3)アセンブル
(4)リンク

前回は「(1)プリプロセッサ」について記載しました。

次は「(2)コンパイル」について書いてみましょう。


コンパイルは、C言語を翻訳してアセンブリコードを出力します。

アセンブリコードはCPUによって異なります。このため、いろいろなCPUに対応したいろいろなCコンパイラがあります。ですから、C言語で書けば理屈的にはどんなCPUにも対応できるわけですね。

サンプルとなるC言語のコードはこちら。

int add(int a, int b)
{
        return (a + b);
}

このコードは、Termuxのclangで次のようなARMのアセンブリコードに展開されます。

    sub     sp, sp, #0x10
    str     w0, [sp, #12]
    str     w1, [sp, #8]
    ldr     w8, [sp, #12]
    ldr     w9, [sp, #8]
    add     w0, w8, w9
    add     sp, sp, #0x10
    ret

w0, w1 は引数です。
w0=a
w1=b

それをそれぞれスタックに待避し、w8, w9に取り出しています。

(a)引数を受け取る

引数はレジスタ w0, w1, w2, ・・・を使用して受け取ります。

(b)引数は全てスタックへ

引数を全部スタックに入れます。
こうすることで、別の関数を呼び出す時に再び w0, w1, ・・・を使うことができます。
w0, w1, ・・・で自分がもらった引数は全てスタックに待避したので、w0, w1, ・・・はつぶれても構いません。

(c)演算する

スタックからレジスタに取り出して演算する。

(d)戻り値

戻り値はレジスタw0を使用します。

コンパイラはスタックを使いまくります。
関数に入ったところでスタックを確保し、関数を抜けるときにスタックを戻します。ほとんどこの繰り返しです。アセンブラにおけるスタックの有用性についてはこちらに解説しました。

昨今はレジスタの数が多いので、スタックでなくレジスタを使うというコンパイラもあるようです。
今回の clang においてレジスタの使用は引数と演算に特化しているように見受けられます。
他は全部、スタック。
これはこれでスッキリしているようにも思えます。

C言語においてオート変数は関数内でのみ存在します。
その意味がわかりましたでしょうか。
オート変数は、レジスタ、もしくはスタックに確保されます。関数を抜けるとスタックは戻され、レジスタは別の用途に使われます。
文字通り「関数内でしか」存在しません。

コンパイラでアクセスが制限されているということではありません。存在しているがアクセスできないというのとは違うのです。
物理的に存在しないのです。

「オート変数のポインタを呼び出し側に返す」

まれにそういうコードを見かけることがあります。それがどれだけ危険かということもこれで明白です。
関数から抜けたら存在しなくなる変数のアドレスを他の関数に教えていることになるからです。

一方、呼び出し側はどうなってるでしょう。
例えば、次のように呼び出します。

int x;
x = add(1, 2);

このアセンブラは次の通り。

mov	w0, #1
mov	w1, #2
bl	add
stur	w0, [x29, #-4]

w0, w1 に 1, 2 を入れて、
「add」を呼び出します。


今回のサンプルコードはとても簡単なものですが、アセンブラをみているとそれでも疑問が少々わいてきます。

[1]レジスタに入りきらない引数はどうする?
[2]引数が8個以上になったらどうする?
[3]w8, w9 は破壊してよい?(スタックに待避しなくてよい?)

また、引数をポインタにしたら・・ということも興味深いですね。

これらはいずれまた調べてみましょう。


この記事が気に入ったらサポートをしてみませんか?