C言語をセルフコンパイル出来るようになったらやっとC言語を習得できたと言っても良いんじゃないかな
レジスタ変数かメモリに割り付けられた変数かと最適化の具合によってで吐き出されるコードは異なりますが、C言語での演算子で i++
とかi += 2
、i <<= 2
なんて構文はまるでアセンブリ言語みたいです。
; i++
inc ax ; ax = ax + 1
; i += 2
add ax, 2 ; ax = ax + 2
; i <<= 2
shl ax, 2 ; axを左に2ビットシフト
コンパイルされた結果のアセンブリ言語を眺めてみる
昔話で申し訳ないのですが、Z80、MC68000、i8086とかの時代にC言語のコンパイル結果をアセンブリ言語で見るということをしていました。関数呼び出しでのスタックフレームの生成とか、オート変数がレジスタ割当でどのように実装されているかとか、関数の戻り値は必ずアキュームレータ (or 言語系で決めたレジスタ) に入れるとか、そのあたりを「ふむふむ」と眺めたり…。「そんなことの何が楽しいの?」と問われてもどうしようもないのですが、一人で「なるほど」と悦に入っていました。
自分でスタックフレームとか考えてみる
個人的にはC言語を初めて触ったのは、もうウン十年前ですが、学習当初はポインタの考え方を理解するのが大変でした (ポインタへのポインタとか、云々)。それもそのはず、C言語を高級言語と思い込んでいたからなんですね。カーニハンとリッチーの言ではないとは思うのですが、「C言語は高級言語ではなくて高級アセンブリ言語である」ということが理解できると、その辺りの誤解が氷解できると思うのです (CPUは何でも良いからアセンブリ言語をある程度理解していることが前提なのですが…。)。ポインタへのポインタの配列とかstructとかも、実際にどうメモリに配置されているかが想像できるようになると目からウロコが落ちる思いができるのではないかと思います。そして、スタックフレーム上に char[100]
とかの配列を配置して、その操作を誤るとリターンアドレスを書き換えてしまってバッファーオーバーランが起こることとかも…。
いかに機械語 (アセンブリ言語) でCPUがメモリから値を読み出すか。それが理解できれば、実はポインタなんてお茶の子さいさいです。ポインタの概念が理解できると、もっと高級言語で登場するオブジェクトへのリファレンスなんて概念も、それぞれが、個々のオブジェクト変数はオブジェクト実体へのリファレンスのアドレスを持っていて、開放されるときにデストラクタで実体の参照数がデクリメントされて、参照数がゼロになったらオブジェクトそのものが開放される (説明のためものすごくはしょって言っていますし、この辺りは、個人的な妄想でUnixenのファイルシステムでリンク数が0になったら本当に削除されるという概念も引っ張ってきています) ということも想像が出来るようになるのではと思います。
C言語は、いささか解りにくいニーモニック (アセンブリ言語) を高級言語っぽく表記したもの + 関数やループとか条件分岐という構文を導入したものくらいに思っておけば理解が進むのではないのかなと思った次第です。
【余談その1】
扱ったことがないのですが、RISC CPUのパイプラインの最適化などに関しては人手でシコシコとやるよりはコンパイラ任せの方が最適化が効くのではないかと思いますが、個人的には大昔のCISCについては、人間の最適化とコンパイラの最適化とでは大差が出ないと思っています。
【余談その2】
若かりし頃に某シャープのX68000という機体を触ったときに、グラディウス (オマケで付属していた2Dシューティングゲーム) 以外にすることがなくなって、オマケの「福袋」のアセンブラでハノイの塔をプログラミングしたときはMC68000のアセンブリ言語はZ80やi8086のそれよりも直交性が高くて解りやすいと感動したものです。ついでに、純正のXC (純正のCコンパイラ、X-BasicからのCトランスレーターも付属していたと記憶しています) とGCC (言わずと知れたGNU Compiler Collection) のコードの美しさの違いに泣いた覚え (もちろん、GCCの方が何倍もスマートだったように記憶しています) というのもあります。
【余談その3】
アセンブリ言語のニーモニックからマシン語のバイナリに手で変換するのをハンドアセンブルと呼んでいました。でも、ハンドアセンブルすると、なぜか常にジャンプアドレスとかがズレるんですよね。やっぱり、アセンブルはアセンブラに任すべきです。
終わりに
今回はいいささかノストラジックで散文的な内容ですが、今でも少なからず需要のある組み込み系ソフトウェア開発の方に少しでも足しになればと思います。