コードを書いて実行すると画面に文字が出ます。その間には、文字列として保存されたソースを読み、実行できる形に変換し、メモリへ配置し、CPUへ処理を進めさせる段階があります。ここを区別できると、「ビルドは通るのに起動できない」「環境によって動かない」といった問題を調べやすくなります。
ソースと実行ファイルは役割が違う
ソースコードは、人がプログラムを記述するための表現です。CPUが直接理解する命令とは限りません。Cのような言語では、通常、コンパイラがソースを解析して対象CPU向けの機械語へ変換します。構文だけでなく、名前や型の整合性もこの段階で検査されます。最適化によって、書いた式と命令が一対一には対応しなくなります。
#include <stdio.h>
int main(void) {
puts("Hello, LavaCS");
return 0;
}この例はputsというライブラリ関数を使います。関数名を書くだけで、その実装がソースへ自動的に貼り付くわけではありません。ヘッダによる宣言、コンパイルされた呼出し、実際の関数の実装を結び付ける処理を分けて考えます。OSや実行形式、静的リンクか動的リンクかによって詳細は変わります。
コンパイルとリンクを分けて見る
Cの典型的な流れでは、前処理でincludeやマクロを扱い、構文・意味解析を経て中間表現を作り、最適化とコード生成を行います。アセンブラはアセンブリ表現をオブジェクトファイルに変換し、リンカは複数のオブジェクトやライブラリを結合します。clangのようなコマンドは、これらを順番に呼び出すドライバとしても働きます。
clang -E hello.c -o hello.i # 前処理の結果
clang -S hello.c -o hello.s # アセンブリ表現
clang -c hello.c -o hello.o # オブジェクトファイル
clang hello.o -o hello # リンク
./hello文法違反ならコンパイル段階、宣言された関数の実装が見つからなければリンク段階で失敗することがあります。実行時に共有ライブラリが見つからない問題は、さらに後の段階です。同じ「動かない」でも調べる対象が変わるため、最後の一行だけでなく、どのコマンドが失敗したかを記録します。
起動するとプロセスになる
ディスク上の実行ファイル自体が動き続けるわけではありません。OSは実行に必要なメモリ領域や資源を用意し、コードやデータを対応付けます。実行中の状態を持つ単位がプロセスです。同じ実行ファイルから二つ起動すれば、それぞれに実行位置やメモリなどの状態があります。ファイルと、その実行中の個体を区別してください。
実行時には標準ライブラリ、メモリ管理、例外処理などのランタイムが働くこともあります。画面への出力やファイルへのアクセスにはOSの機能が関わります。アプリの関数呼出しがすべてシステムコールになるわけではありません。例えばライブラリの出力はバッファにたまり、後でまとめてOSへ渡される場合があります。
インタプリタとコンパイラは併用される
「Pythonはインタプリタ、Cはコンパイラ」と言語名だけで分類すると実態を見失います。代表的なPython実装であるCPythonはソースをバイトコードへコンパイルし、その命令を仮想機械で実行します。別の実装ではJITによって実行中に機械語を生成することもあります。言語の規則と、その言語を実現する処理系は別です。
バイトコードは、必ずしも物理CPUの命令ではありません。対象が仮想機械なら、その仮想機械を実行するプログラムが必要です。またコンパイルが成功しても、入力値、権限、ネットワーク、未定義動作などによって実行が失敗することがあります。ビルド成功が保証する範囲を広く見積もりすぎないようにします。
理解を確かめる
同じ実行ファイルを二回起動すると、実行中の状態は同じになるか。通常は二つのプロセスとなり、それぞれに状態があります。共有メモリやファイルを使う場合は、意図的に共有する部分もあります。
Cの関数の宣言はあるが実装をリンクしていない場合、どこで失敗しやすいか。リンク時です。エラーメッセージに未解決のシンボルが示されることがあります。
インタプリタ型の処理系ではコンパイルが一切行われないと言えるか。言えません。CPythonのようにバイトコードへのコンパイルを含む処理系があります。
この単元の概念:コンパイラ バイトコード プロセス
続けて読む:カーネル・特権モード・システムコール