プログラムについて知ろう

プログラムとは何か(3)~進化したプログラミング言語の翻訳処理~

  • このエントリーをはてなブックマークに追加
プログラムのイメージ

本記事は、プログラムとは何か(2)~コンピューターの積極活用へ~ からの続編です。

プログラミングの生産性を高めよう

コンピューターの利用機会が増えるほど、プログラムを作る機会も増えました。そして、プログラムを作る機会が増えるほど、次は「いかに効率良くプログラムを作れるか」というプログラミングの生産性に焦点が移ります。

設計の効率化とコーディングの効率化

プログラムを効率良く作るためには、2つの課題がありました。

  • プログラムの記述(コーディング)前の、設計(プログラムデザイン)の負担を減らすこと。
  • プログラムの記述(コーディング)量、そのものを減らすこと。

解決策としての高級言語とモジュール化

そして、その解決策として、

  • プログラムの設計負担を減らすために、数々の高級プログラミング言語が作られました。
  • プログラムの記述量を減らすために、モジュール化が進みました。

高級プログラミング言語については、前回の記事で説明しました。ここでは、モジュール化について、見ていきましょう。

モジュール化

効率良くプログラムを作るためには、複雑なプログラムをいかに単純な構成で実現できるかがカギになります。複雑と単純の両立で、一見、矛盾するようですが、これを可能にするのがモジュール化(部品化)という考え方です。

モジュール名への変換/モジュール名からの復元

複雑な処理部分をモジュール名に変換

主にルーチンをモジュール化

プログラムは、長く複雑になるほど、その中で同じような処理が何度も出てくるようになります。何度もプログラム中に再登場するような、よく行われる処理のことをルーチン(routine)と言います。

このようなルーチンを、ひとかたまりの部品 = モジュール(module) としてまとめ、簡単に使い回せるようにすることをモジュール化と言います。モジュールの1つ1つには、わかりやすいモジュール名(部品名)を付けておきます。

モジュール化してあれば、プログラム中でまた同じ処理が必要になっても、モジュール名(部品名)を一行書いておくだけで、あとでそのモジュールの処理をそこに復元できるのです。

お祈り
礼
拍手
お祈り(モジュール採用)

礼と拍手をモジュール化したお祈りの例(二礼二拍手一礼)

モジュール化の効用

再登場しない一度きりの処理(非ルーチン)に対しても、複雑な処理をひとことで言い表すモジュール名を付ければ、そこで行われる処理をイメージしやすくなります。

モジュール化すると、プログラム中のモジュール部分(ある程度複雑な記述部分)が、モジュール名1つに置き換えられるため、全体の見通しを単純化することができます。

また、大規模なプログラムであっても、モジュールに分けて、モジュール単位でファイルを分ければ、作業分担や問題箇所の特定・修正がしやすくなります。

あいまいな概念としてのモジュール

モジュールは、適用範囲や使い方などによって、サブルーチン(subroutine)、プロシージャ(procedure)、関数(function)、コンポーネント(component)などとも言われます。
また、それらの言葉が表す意味も、プログラミング言語によって多少異なります。

ここで述べているモジュールは、個々のプログラミング言語等によって明確に規定された意味のモジュールではなく、広い意味でのプログラム部品としてのモジュールを指しています。

モジュール化する処理の規模や用途に決まりはなく、1行だけの簡単な処理から、複数の処理をまとめた複雑な処理まで、用途に応じて様々な範囲の処理をモジュール化できます。親モジュールの中にいくつかの子モジュールというように、モジュールを階層化することもできます。

ただ、モジュールとしてのレベル(モジュール性)は、一部品としてきれいに他と分離できるほど、高いものと評価されます。

モジュール名を実際の処理に復元

モジュールの復元主体

では、モジュール名から実際の処理への復元は、誰がするのでしょうか?

プロセッサは、マシンコードによって論理回路の切り替えができる集積回路でしかありません。したがって、その切り替え指示は、すべてマシンコードプログラムに書かれていなければなりません。

つまり、

人間が作ったモジュール名の混じったソースコードを、マシンコードへ変換するプログラム

アセンブラコンパイラ

に、モジュール名を、実際の処理のマシンコードへ復元するしくみを入れるのです。

モジュールの復元方法

具体的な復元の実現方法としては、インライン展開(inline expansion)とリンク(link)の2つがあります。

inlineとは、プログラム文の行(line)の、中(in)に、という意味です。

  • モジュール名の位置に、モジュールの処理内容をそのまま復元(コピー)する(インライン展開)
  • モジュール名の位置から、すでに処理内容が記録されたメモリ領域へジャンプさせる(リンク)

どちらの方法が良いかは、その時々の状況によって変わります。

  • インライン展開にすると、リンクよりもプログラム全体の文字数(メモリ使用量)が多くなります。
  • リンクにすると、インライン展開よりもジャンプする分だけプログラム実行時間が長くなります。

アセンブラの進化

マクロ

アセンブラでは、よく使われるモジュール(ルーチン)をどのプログラムでも簡単に利用できるよう、アセンブラ自体の中にルーチンのマシンコードプログラムを用意しておき、そのルーチン専用に新たなニーモニック(疑似命令)が割り当てられるようになりました。このニーモニック1行で書き表せるようにしたルーチンマクロ(macro)と言います。

通常のニーモニックはマシンコードの最小単位(ミクロ)であるオペコードと1対1に対応するものですが、それに対して、新たに追加した1つのニーモニックに、何ステップかのオペコードを(1対多で)対応させたものが、マクロになります。

なお、マクロという用語は、アセンブリ言語での疑似命令に限らず、アプリでの「操作手順おまとめ機能」を表す言葉としても使われています。アプリの通常の1操作(ミクロ)に対して、複数の操作手順をひとまとめにしたボタンやメニューも、疑似命令からの類推でマクロと言います。さらに、このまとめ機能をより詳細に作り込むために使われる高級言語のことをマクロ言語と言います。例えば、Excelなどで使われるVBAはマクロ言語の一つです。

PCSプログラミングスクールの Excel VBA コースはこちら

リンク

アセンブリ言語でのリンクは、(1)モジュールへのラベル付け と、(2)本体からラベルへの参照 と、(3)モジュールからの戻り によって作られます。具体的には以下のように、コールスタックを利用して、標的となるメモリアドレスをあるべき順番どおりに記録/取得します。

1. ラベル = モジュール名 をつける

モジュールの作成者が、モジュールの先頭に、ラベルを付けます。アセンブリ言語では、ラベル名(ラベルの「:」より手前につけた文字列)がモジュール開始メモリアドレスの代名詞になり、アセンブル時にモジュール開始メモリアドレスに変換されます。したがって、ここで付けたラベル名がアセンブリ言語でのモジュール名に相当します。

2. ラベル参照(reference)

アセンブリ言語でプログラミングする時、モジュールの処理をさせたい部分に、「 CALL ラベル名 」と記述し、モジュール開始メモリアドレスへジャンプさせます。これを参照(reference)と言います。

CALL命令では、モジュール処理が終わった後に戻るべき命令のメモリアドレス(リターンアドレス)を、プログラムカウンタからコールスタックへ退避(PUSH)し、代わりにモジュール開始メモリアドレスをプログラムカウンタへ上書きして、そこへジャンプさせます。

3. 戻り(de-reference)

モジュールのソースコードには、必ず本体プログラムへプロセッサが戻れる仕掛け(dereference)を入れておきます。

その仕掛けとは、モジュールの冒頭と末尾それぞれに、決まった2行を入れておくことです。(「引数」がある場合は、さらにもう少し複雑になります)

モジュールの冒頭(プロローグ:prolog)
  • ベースポインタにある本体用スタックフレームのベースアドレスを、コールスタックに記録しておくPUSH行
  • スタックポインタにあるトップアドレスを、モジュール用スタックフレームのベースアドレスとしてベースポインタへ上書きするMOV行
モジュールの末尾(エピローグ:epilog)
  • コールスタックにある本体用スタックフレームのベースアドレスを、ベースポインタへ上書きするPOP行あるいはLEAVE行
  • コールスタックにあるリターンアドレスをPOPし、プログラムカウンタに入れてそこへジャンプさせるRET行

以上の仕掛けをプログラムに入れておくと、本体プログラムとモジュールの間の切り替えで、次図のような動きになります。

参照(reference)
動く矢印の先は、同色の各レジスタに入れられたアドレス。
アドレス太字はプロセッサで実行中のオペコード。
各レジスタに入っていた本体プログラム(main)の赤色アドレスが、
次第にモジュール(module1)の青色アドレスに入れ替えられていく。
412は、module1からmainへ戻る時のためのリターンアドレス。
1040は、mainのベースアドレス。
1020は、module1のベースアドレス。
戻り(dereference)
動く矢印の先は、同色の各レジスタに入れられたアドレス。
アドレス太字はプロセッサで実行中のオペコード。
各レジスタに入っていたモジュール(module1)の青色アドレスが、
次第に本体プログラム(main)の赤色アドレスに戻されていく。
1020は、module1のベースアドレス。
1040は、mainのベースアドレス。
412は、module1からmainへ戻る時のためのリターンアドレス。

このような本体とモジュール間での、アドレス指定によるメモリジャンプが、リンク(link)の正体です。アセンブリ言語は、基本的にはマシンコードと1対1対応しているものなので、高級言語をコンパイルして生成されるマシンコードのリンクにも、このような仕掛けが入れられます。この仕掛けルールを呼出規約(calling convention)と言います。

コンパイラの進化

ライブラリ

モジュールのストック

アセンブリ言語のマクロもその一例でしたが、高級言語でも当然、プログラムの共通部分はたくさん出てきます。そうしてモジュールのストックは、どんどん増えていきました。

モジュールの数が増えてくると、それらのモジュールを別個に集めてくるよりも、関連性のある複数のモジュールをあらかじめ束ねておいた方が便利になります。

そこで、関連性のある複数のモジュールを集め、それを様々なプログラムで共通して使えるように1つのファイルにまとめたものがライブラリです。

ライブラリの利用

ライブラリは、最初からOSと一緒にコンピューターに入っている場合もありますが、必要なライブラリがコンピューターに入っていない場合は、そのコンピューターの外部からライブラリファイルを持ってきて、コンピューターに入れておきます。

ライブラリを利用するプログラムでは、通常、以下のように2段階で記述します。

  1. ソースコードの冒頭に、どのライブラリを利用するかを記述。
  2. モジュールの処理が必要な行に、そのモジュール名を記述。

こうすることで、ライブラリの記述は冒頭に一回だけであっても、モジュールはプログラム中に何度も使うことができるようになります。

別ファイルへのリンク

実行ファイル形式

ライブラリの利用や、大規模プログラムでのモジュール分割など、複数ファイルからなるプログラムは、ファイルをまたいで結合し、最終的には実行ファイル(executable)という一つのマシンコードファイルになるように変換する必要があります。

実行ファイルが、正しく安全に効率良く動くようにするためには、OSとの連携が欠かせません。そこで、OSがプログラムの構造に合わせて適切にプロセッサやメモリを制御できるよう、セクションヘッダという2つの工夫が、実行ファイルに付け加えられました。

セクション

プログラムの中身を整理分類(セクション分け)して、OSがセクション毎に扱いを変えられるようにする。

セクションの例)

  • テキストセクション = マシンコードになったプログラムの集まり
  • データセクション = 初期値が入った変数の集まり
  • BSSセクション = 初期値の無い変数が入る予定のセクション(空)
ヘッダ

実行ファイルの冒頭(ヘッダ)に、セクション情報やプログラムの情報など、OSとの連携に必要な情報を入れておく。

これらの工夫は、プログラムを実行するプラットフォームに合わせてその形式(フォーマット)が決められています。これを実行ファイル形式(実行ファイルフォーマット)と言います。

実行ファイル形式の例:

  • a.out (assembler output)・・・初期のUNIXプラットフォーム
  • COFF (Common Object File Format)・・・Windowsプラットフォーム
  • ELF (Executable and Linkable Format) ・・・UNIXプラットフォーム など

オブジェクトファイル

コンパイラでソースコードをコンパイルすると、そのソースコードに対応した実行ファイルとして、オブジェクトファイルが1つ生成されます。

1つのプログラムが複数のソースコードファイルに分割されている場合は、各ソースコードファイルごとにコンパイルします。(分割コンパイル)

そしてコンパイラは、それぞれに対応したオブジェクトファイルを、ソースコードファイルの数だけ生成します。

それら複数のオブジェクトファイルは、あとで合体させ、各ファイルに分散した同種セクションを再配置(リロケート)して、ヘッダも書き換え、1つの実行ファイル(実行ファイル形式)にすることで、1つのプログラムとして動くようになります。

再配置(relocation)
再配置(リロケート)のイメージ

シンボルテーブルへの仮登録

したがってリロケート前のコンパイラでは、実行ファイルで各リンク先の開始アドレスがどこになるかは、まだわかっていません。

コンパイラが作成した、オブジェクトファイル内のリンクは、未結合のリンク(シンボルテーブルへのシンボル仮登録まで)になります。

シンボル

なお、シンボルとは参照の識別に使われる名前のことです。例えば、モジュール名や変数名などのことをシンボル名と言います。シンボルテーブルは、シンボルとそのメモリアドレスとの対応表です。コンパイラが行うシンボルの仮登録では、メモリアドレス無しでシンボル名だけをシンボルテーブルに登録しておきます。

変数については、別の記事で詳しく説明します。

シンボルテーブル
単純化したシンボルテーブルのイメージ
(実際はもっと複雑で、様々な情報を含みます)

リンカ

そして、必要な全てのオブジェクトファイルが揃ったところで、リンカ(linker)というまた別のプログラムによって、各セクションリロケート(再配置)し、1つの実行ファイルを生成します。

全セクションのリロケーション(再配置)が完了してようやく、各ファイルに入っていたシンボル実体の開始アドレス(リンク先)が定まります。

参照のリンク

その後の、シンボル名(リンク元)からシンボル実体の開始アドレス(リンク先)への変換には、次の2つの方式があります。

静的リンク
全てのファイルのコンパイルが完了した後で、リンカが、各オブジェクトファイルをリロケートした上で結合し、1ファイルのマシンコード(実行ファイル)にまとめる方式。コンパイルからオブジェクトファイルの結合までを合わせてビルドとも言います。
動的リンク
コンパイラで各ファイルをコンパイルしてオブジェクトファイルにだけしておき、シンボルテーブルにシンボルを仮登録します。本体オブジェクトファイルを実行ファイルとし、メモリにロードして実行します。そして必要になったモジュールだけを、OS(カーネル)が順次追加ロードし、シンボルテーブルにそのアドレスを登録することでリンク結合が完了する方式。WindowsのDLL(Dynamic Link Library)は、この方式でリンクできるライブラリになっています。

なお、静的(static)/動的(dynamic)という言葉は、メモリに記録されるものが、プログラム実行前から実行中もずっと変動しない場合に静的、プログラム実行中に変動し得る場合に動的、として使い分けられています。

言語処理(translate)

プログラミング言語処理系

上で見たようなソースコードから実行ファイルまでの変換をするプログラムプログラミング言語処理系(programming language processor)と言います。略して「言語処理系」、さらに略して単に「処理系」と表現されることもあります。

コンパイラは単体でも言語処理系になり得ますが、リンカまで含めての1セットでも一つの言語処理系として扱われます。

パース(parse)

異なる言語処理系であっても、共通しているのが、前半にあるパースという言語処理です。

パースでは、以下の2段階の処理が行われます。

  • 字句解析(lexical analysis) →トークン(token)分解
  • 構文解析(syntax analysis) →抽象構文木(AST)生成

高級プログラミング言語は、どれも英語の文化が基本になっているので、単語と単語の間を 空白/改行/記号 のどれかで区切ってあります。この 空白/改行/記号 で区切られていて一つの意味を持つ字句のことをトークンと言います。

トークン分解

パーサ(parser)というパースをするプログラムは、このトークンを抽出し、トークン間の関係を抽象構文木(Abstract Syntax Tree)という樹形図にマッピングします。

抽象構文木(AST)

樹形図へのマッピングは、

  1. 文の先頭から、各トークンをメモリに配置。
  2. そのトークンの定義に従って、その枝葉となる別トークンを特定する。
  3. 枝葉トークンのメモリアドレスを元トークンに対応づける。

を繰り返して完成します。

種々の言語処理系とその違い

通常のコンパイラの他にも、言語処理系にはインタプリタ(interpreter)や、JITコンパイラというものがあります。

前段のパースを、プログラム実行の前(AOT)にやるか/実行中(JIT)にやるか、後段で最適化を、するか/しないか等の違いによって、メリット/デメリットが異なります。

インタプリタ

インタプリタはプログラム実行フェーズ(run time)で、パースからマシンコード実行まで突貫で処理する言語処理系です。

処理の流れ

  • プログラム実行(run)
    • インタプリタ処理(interpretation)
      • パース(parse)
      • 意味解釈(semantics analysis)【ほとんど最適化できない】
      • マシンコード生成(code generation)【実行環境に合わせられる】
      • マシンコード実行(execution)

メリット

  • 実行環境を限定せずにプログラムを配布できる。
  • プログラムの一部分だけでも突貫処理できるため、部分修正の繰り返し時に便利。

デメリット

  • 実行時にする事が多いため、動作が遅く感じる。

JITコンパイラ

JITコンパイラは、プログラム実行中(run time)にコンパイルを行います。そしてコンパイル後、即マシンコードを実行します。JITは、Just-In-Time の略で、「ギリギリで」という意味です。

処理の流れ

  • プログラム実行(run)
    • JITコンパイル(JIT compilation)
      • パース(parse)
      • 中間コード生成(intermediate representation)
      • 最適化(code optimization)【短時間での中度最適化】
      • マシンコード生成(code generation)【実行環境に合わせられる】
    • マシンコード実行(execution)

メリット

短時間で効果が高い最小限の最適化だけを行うことで、インタプリタより動作を速められる。

事前コンパイルと違い、プログラム実行環境が限定されない。

デメリット

最適化の効果が出せない場合、インタプリタより動作が遅くなる。

AOTコンパイル

JITコンパイルに対して、通常のコンパイラが行う事前コンパイルをAOT(Ahead-Of-Time)コンパイルとも言います。

処理の流れ

  • 事前コンパイル(AOT compilation)
    • パース(parse)
    • 中間コード生成(intermediate representation)
    • 最適化(code optimization)【長時間での高度最適化】
    • マシンコード生成(code generation)【実行環境が限定される】
  • プログラム実行(run)
    • マシンコード実行(execution)

メリット

プログラム実行中ではないので、高度な最適化にも十分時間をかけられ、プログラム実行時の動作を最速にできる。

デメリット

プログラム実行前にマシンコード生成をするので、その時点で実行環境を限定せざるを得ない。

まとめ

どんどんプログラムを作っていくために、高級プログラミング言語ができ、より生産性を上げるため、モジュールもたくさん作られました。

アセンブラコンパイラは、単純にソースコードマシンコードに直訳するだけではなく、モジュールを自動でリンクしたり、OSと連携するためのコードを付加したり、マシンコードを最適化したりなど、プログラムを作る人(プログラマ)が効率良くプログラムを作れるように進化していきました。

また、実行時の環境に合わせて動的にマシンコードが作れるよう、インタプリタJITコンパイラにも進化しました。これらのソースコード→マシンコード変換プログラムをプログラミング言語処理系と言います。

この記事の中でも出てきたように、プログラムはコンパイル時もリンク時も、実行時もメモリに上で扱われ、メモリとの関係が欠かせません。

次の記事では、プログラムとメモリとの関係をより詳しく見ていきます。

続きは、プログラムとは何か(4)~メモリ編~

  • このエントリーをはてなブックマークに追加

コメントを残す

*

CAPTCHA