プログラム実行の仕組み


  1. プログラムを実行する環境


    1. プログラミング環境と実行環境

       コンピュータは各種のプログラムを実行する機械です。このためには、プログラムを作成する環境と実行する環境が必要です。実行する環境には、実行を支援するソフトやライブラリ、各種の入出力装置やOSが必要です。
       プログラムを作成するには、C言語などのコンパイラ言語を機械が実行可能な機械語に変換する環境が必要です。ここでは、C言語のプログラムを実行するための環境をまとめておさらいします。

    2. Windowsの実行環境

      プログラムはOSから実行されます。C言語のコンソールアプリケーションとしてコンパイルされた実行形式のプログラムは、xx.exe の形式のファイルに保存されます。これをダブルクリックするか、コマンドプロンプトウインドウで、実行ファイル名を指定して実行します。ただし、ダブルクリックした場合、別のコマンドプロンプトウインドウを開き、実行が完了するとすぐに閉じてしまうので、結果が確認できません。


       Windowsでは、ウインドウの中しか表示ができません。コンソールアプリケーションは、コマンドプロンプトウインドウを利用して実行します。 ウインドウを作成し表示する関数を利用すれば、コマンドプロンプトウインドウなしに実行することが可能になります。これらの関数は、WindowsのAPI(Application Interface)として提供されます。

    3. 表示、入力

       C言語の printf() などの表示関数は、あらかじめ用意されたライブラリ関数(APIの一部)を呼び出し、表示します。scanf( ) などの入力は、やはりライブラリ関数を呼び出します。この関数は、キーボードの入力を待ち、スリープ状態に入ります。OS経由で キー入力があると、実行を再開します。この間、OS は別のタスク(仕事)を行うことができます。ライブラリ関数がOSにサービスを依頼することを、システムコールと呼ぶことがあります。

  2. プログラム実行の仕組み


    1. プログラムの番地

       先にプログラムは番地を指定してジャンプをすることで、while() などの繰り返しや if() などの条件処理を実行すると説明しました。しかし、プログラムの番地が固定されていると、特定の番地でなければ実行できません。WindowsなどのOSでは、コンパイルされた番地は、プログラムの先頭からの相対番地です。実行時にOSはメモリの番地を定めてプログラムをディスクからメモリに移し、先頭番地をセグメントレジスタに設定して実行を開始します。プログラムで指定する番地はこのセグメントレジスタからの相対アドレスとして実行されます。たとえば、セグメントアドレスが1000番地で、プログラムで指定した番地が100番地なら、実際には1100番地のアドレスを参照します。

    2. 関数の動的なロード

       ライブラリなどの関数は、実行時にはメモリーにロード(コピー)されないで、実行時に必要になるとディスクからロードされて実行されます。これをダイナミック(動的)ロードと呼び、この種のライブラリファイルは 拡張子が .dll となります。大きなアプリケーションプログラムでは小さな .exe ファイルと、複数の .dll ファイルから構成されます。

  3. プログラムの作成


    1. コンパイラ言語

       C言語はコンパイラ言語と呼ばれ、記述されたプログラムはCPUが直接実行できる機械語(命令)に変換されます。C言語のコンパイラは、まず、オブジェクト形式のプログラムに変換します。この時点では、printf() などのライブラリ関数はまだ組み込まれていません。コンパイルが終了すると、リンカー(結合・編集)が実行され、ここで、ライブラリの呼び出し機能が組み込まれます。


    2. インタプリタ言語

       コンパイラ言語に対し、インタプリタ方式で実行される言語も少なくありません。インタプリタ方式では、元となる言語を実行しやすい形式の中間形式のファイルに変換します。実行はこのファイルを解釈(インタプリット:interpret)しながら実行します。たとえば、Java言語の場合、.java のソースファイルをコンパイラは class ファイルに変換します。このファイルは、ハードウエアに依存しない仮想マシンの機械語になっています。vm(仮想マシン)と呼ばれるプログラムでこのファイルを解釈・実行します。
       別のコンピュータで vm を作成すれば、.class ファイルを変更することなくプログラムを実行できます。一般に、解釈実行方式は、機械語のプログラムに比較して2〜10倍程度の実行時間が必要です。その代わり、コンピュータやOSに依存しないプログラムとなります。



  4. コンパイラの仕事


    1. コンパイル

       先に紹介した機械語でのプログラムの実行を復習しながら、コンパイラの仕事を考えて見ましょう。コンパイラの仕事は、式の処理と制御の処理、に大別できます。

    2. 式のコンパイル

       式にも、計算する式(算術式)と判断する式(論理式)があります。式のコンパイルではでは、変数の処理と計算する命令の生成、が必要です。たとえば、int a=7,b=3; b = b + a * 10; の場合、変数 gaku,kosu に対し、番地を割り当てます。次に、変数の番地を参照しながら、計算する命令を生成します。このとき、演算の優先順位を考える必要があります。先に a*10 を計算し、その後 b を加えてから、b に保存します。
       このプログラムのアセンブリ言語のソースと実行は次のようになります。4番地の HLT は停止命令です。
      5,6 番地の DC は初期値を設定する指示で、宣言命令とか擬似命令と呼ばれます。実行する以下が、仮想的に実行した経過です。左から、実行する番地(PCレジ)、実行する命令、Wレジスタの値、実行対象の番地または値、条件フラグ、最後が次に実行する番地になります。
      Z:\mito\Clang\AnsiC\asmsp>asmsp opr.asm
      opr.asm を読み込む
      
        0     LDI 5
        1     MLM a
        2     ADM b
        3     STR b
        4     HLT
        5 a DC 7
        6 b DC 3
      
      ----------
      実行する
      pc=  0: 命令 LDI   5 : W=  5 番地/値     (値  0) フラグ 次PC=  1:
      pc=  1: 命令 MLM   a : W= 35 番地/値   a (値  7) フラグ 次PC=  2:
      pc=  2: 命令 ADM   b : W= 38 番地/値   b (値  3) フラグ 次PC=  3:
      pc=  3: 命令 STR   b : W= 38 番地/値   b (値 38) フラグ 次PC=  4:
       この実行経過は、この仮想命令を解釈実行するプログラム(asmsp)で実行した出力です。

    3. 繰り返しのコンパイル

       while や if による実効制御は、演算結果に基づく条件フラグ(ビット)と条件付の命令で、コンパイルできました。
      たとえば、以下のプログラムは、s に10,9,8, ..,0 を加えるプログラムです。
       int i=10,s =0; 
       do { s = s + i,
           i--} 
       while ( i != 0) 
       このプログラムのアセンブリプログラムは以下のようになります。0番値のLpは記号番地で、4番地の JNZ lp に対応しています。0,1,2 番地の命令で s = s + i, を実行します。3番地のDEC i で i を一つ減らします。このとき、i が0になると、ZF(zero flag)が1になります。4番地の JNZ lp でZF が1でなければ0番地に戻ります。
         0 lp LDM s
         1     ADM i
         2     STR s
         3     DEC i
         4     JNZ lp
         5     HLT
         6 s dc 0
         7 i dc 10
      ----------
      実行する
      pc=  0: 命令 LDM   s : W=  0 番地/値   s (値  0) フラグ   : 次PC=  1:
      pc=  1: 命令 ADM   i : W= 10 番地/値   i (値 10) フラグ   : 次PC=  2:
      pc=  2: 命令 STR   s : W= 10 番地/値   s (値 10) フラグ   : 次PC=  3:
      pc=  3: 命令 DEC   i : W=  9 番地/値   i (値  9) フラグ   : 次PC=  4:
      pc=  4: 命令 JNZ  lp : W=  9 番地/値     (値  0) フラグ   : 次PC=  0:
      pc=  0: 命令 LDM   s : W= 10 番地/値   s (値 10) フラグ   : 次PC=  1:
      pc=  1: 命令 ADM   i : W= 19 番地/値   i (値  9) フラグ   : 次PC=  2:
      pc=  2: 命令 STR   s : W= 19 番地/値   s (値 19) フラグ   : 次PC=  3:
      pc=  3: 命令 DEC   i : W=  8 番地/値   i (値  8) フラグ   : 次PC=  4:
      pc=  4: 命令 JNZ  lp : W=  8 番地/値     (値  0) フラグ   : 次PC=  0:
      ... 略
      pc=  0: 命令 LDM   s : W= 52 番地/値   s (値 52) フラグ   : 次PC=  1:
      pc=  1: 命令 ADM   i : W= 54 番地/値   i (値  2) フラグ   : 次PC=  2:
      pc=  2: 命令 STR   s : W= 54 番地/値   s (値 54) フラグ   : 次PC=  3:
      pc=  3: 命令 DEC   i : W=  1 番地/値   i (値  1) フラグ   : 次PC=  4:
      pc=  4: 命令 JNZ  lp : W=  1 番地/値     (値  0) フラグ   : 次PC=  0:
      pc=  0: 命令 LDM   s : W= 54 番地/値   s (値 54) フラグ   : 次PC=  1:
      pc=  1: 命令 ADM   i : W= 55 番地/値   i (値  1) フラグ   : 次PC=  2:
      pc=  2: 命令 STR   s : W= 55 番地/値   s (値 55) フラグ   : 次PC=  3:
      pc=  3: 命令 DEC   i : W=  0 番地/値   i (値  0) フラグ Z : 次PC=  4:
      pc=  4: 命令 JNZ  lp : W=  0 番地/値     (値  0) フラグ Z : 次PC=  5:
      

    4. 条件付実行のコンパイル

      if 文による条件付実行の例を紹介します。下のプログラムは、a,b 大きいほうの値をmに代入します。
      int a=5,b=7,m;
      if (a < b) m=a;
      else m=b;
       これは、次のようにアセンブリ言語で実行されます。大小関係の比較には引き算を行い、負になるか否かで判断します。0,1 番地で引き算をします。a より b が大きいと、引き算の結果 sf (sign flag)が1になります。JNS が SF が1でないときジャンプします。この場合、ジャンプはしないで 3番地に進み Wレジに b の値をいれ I2 にジャンプします。ジャンプしないと、W に a の値が入ります。6 番地で大きい方の値がmに記憶されます。
       このように、if else では、条件付ジャンプと無条件ジャンプの組み合わせになります。
         0     LDM a
         1     SBM b
         2     JNS l1
         3     LDM b
         4     JMP l2
         5 l1 LDM a
         6 l2 STR m
         7     HLT
         8 a   DC 5
         9 b   DC 7
        10 m   DC 0
      ----------
      実行する
      pc=  0: 命令 LDM   a : W=  5 番地/値   a (値  5) フラグ   : 次PC=  1:
      pc=  1: 命令 SBM   b : W= -2 番地/値   b (値  7) フラグ  S: 次PC=  2:
      pc=  2: 命令 JNS  l1 : W= -2 番地/値     (値  0) フラグ  S: 次PC=  3:
      pc=  3: 命令 LDM   b : W=  7 番地/値   b (値  7) フラグ  S: 次PC=  4:
      pc=  4: 命令 JMP  l2 : W=  7 番地/値     (値  0) フラグ  S: 次PC=  6:
      pc=  6: 命令 STR   m : W=  7 番地/値   m (値  7) フラグ  S: 次PC=  7:

    5. その他の文のコンパイル

       C言語のすべての機能を小型のCPUで実行することは困難です。たとえば、少数計算を行う命令はありません。この場合、小数計算を行う「関数」をプログラムで作成しそれを呼び出して実行します。
       配列の処理を行うことは困難です。
       int a[10];  a[5] = 3 ;
      では、a から連続するメモリを確保し、a + 5 番地 に 3 を代入することになりますが、a + 5 番地を指定する命令がありません。通常、インデックスレジスタ(X)と呼ばれるレジスタに 5 の番地を記録し、STR するとき、この X レジスタと命令の番地を組み合わせて、配列の番地を指定します。
       また、関数の機能もありません。関数実行は 関数の先頭番地に JMP すればよいのですが、関数の終了で戻ることができません。JMP するとき戻り番地を記録しておく機能が必要です。

    6. 割込み機能とOS

       他に必須の機能として、割込み機能があります。記憶内臓方式のプログラム実行機能では、いったんプログラムの実行を開始すると、中断することができません。これでは、複数のプログラムを切り替えながら実行することはできません。
       割込み機能は、タイマーやキーボードなどのハードウエアからの要求で、現在実行中のプログラム実行を中断し、予め定められた OS のプログラムを実行する機能です。このとき、中断されたプログラムの再開番地を覚えておきますから、いつでも再実行可能です。この割込み処理は通常のCプログラムでは実行できません。組み込みコンピュータや、OS を作成する場合には割込み可能なプログラムの作成ができます。
       割り込みを利用するには、コンピュータのハードウエアに関する知識が必要になります。

  5. 参考文献


    天野 司 :「WIndowsはなぜ動くのか」  日経BP出版センター

    伊藤華子:「パソコンプログラミング入門以前」 毎日コミュニケーションズ