2017年11月27日の日記です


リロケータブル  2017-11-27 14:09:53  コンピュータ

2年ほど前に、LSI-Cの作者さんの訃報に触れ、追悼文を日記に書いた


LSI-C というのは、コンピュータープログラムに使われる「C」という言語の亜種。(実装の一つ)

作者さんに面識はなかったが、昔お世話になったので追悼文を書いたものだった。


で、そちらの日記を見た方が、Twitter に感想を書いてくださっていた。


#時々エゴサーチしてますよ


MS-DOS には、COM と EXE と呼ばれる、二つの実行形式ファイルがあった。

プログラム言語の話なので実行ファイルとも関連が深く、Cでもこの二つのファイルが作れたことを書いたところ、「初めて知った」と素直に感想をくださったのだ。



あー、…ごめん。

知識を得て喜んでくれたところ申し訳ないのだけど、あちらのページに書いたのは正確ではない。


以前書いたことの概要を示そう。


MS-DOS のC言語には「メモリモデル」という概念があり、プログラムを作るときにはメモリモデルを選ぶ必要があった。


このうち、プログラムとデータをすべてひっくるめて 64Kbyte 以内で作れるものは、COM ファイルになる。

それ以外は EXE ファイルだ。



…でも、これは「C言語で、64Kbyte 以内のメモリモデルを選ぶと COM ファイルを作れた」というだけで、COM ファイルの要件ではない。

COM ファイルで 64Kbyte を超えるデータを扱うことも可能だったし、COM と EXE の違いは、プログラムやデータのサイズよりも大切なことがある。


嘘を書いてしまったみたいで申し訳ないので、もう少しちゃんと書いておこう。




メモリの話なので、簡単な基礎知識から。


メモリは「アドレス」を使って参照される。

アドレス=住所、の名前の通り、1つのメモリ(記憶)には、1つのアドレスが対応している。


1000番地に 123という数値を入れておけば、1000番地を読みだしたときには、必ず123が入っている。

1001番地に 45 という数値を入れても、1000番地の内容には影響がない。

あいかわらず 1000番地は 123 だし、1001番地は 45 だ。


当たり前の話なのだけど、この当たり前が成立するから、安心してプログラムも作れるし、計算もできるんだ。

基本はしっかり抑えておきたい。


コンピューターの中では、文字も、プログラムも、すべて数値で表現される。

だから、メモリの中に文章データも、プログラムも、もちろん計算結果も、すべて置いておける。



もう一つ、以下の説明で使うので「レジスタ」というものを覚えてほしい。

これは、CPU の中にある、特別なメモリ。

アドレスは割り振られておらず、CPU が特別な意味を持って利用する。




さて、MS-DOS に COM ファイルの実行を指示すると、OS は空きメモリを見つけ出して、ファイルの中身をそのままメモリに読み込む。

その後、OS は CPU の中にある「データの場所」と「プログラムの場所」を示すレジスタを、読み込んだ場所にセットする。


そして、読み込んだデータを実行する。

これが、COM ファイル実行の仕組みだ。



「データの場所」と「プログラムの場所」のレジスタとは何だろうか?


MS-DOS が使えるパソコンは、8086 という 16bit CPU を使っていた。

この CPU は、8bit CPU の 8080 の後継機種で、8080 のプログラムを「簡単に移植」できるように作ってあった。


8bit CPU では、メモリアドレスは 16bit (8bit 2個分)あった。これで、64Kbyte のメモリを扱える。

プログラムの中でアドレスを扱うレジスタも、当然 16bit だった。


これが 8086 になると、16bit なのだからメモリアドレスが 32bit になったか…というと、そうでもない。

中途半端に見えるかもしれないけど、20bit になった。

そして、アドレスを扱うレジスタは、あいかわらず 16bit だった。


これは、8bit CPU 8080 のプログラムを移植しやすいようにするためだ。



でも、メモリアドレスは 20bit なのに、レジスタが 16bit では全部を扱えない。

そこで、プログラムの中では「メモリの開始位置」をずらせるようになっていた。


たとえば、「データの場所」として、1000番地を指定したとしよう。

すると、プログラムの中では「234 番地」を指定した場合に、自動的に 1000 + 234 で 1234 番地が使われる。


データの場所と、プログラムの場所は、別々に指定ができた。

だから、最大で 128Kbyte のメモリが扱える。


もしそれ以上のメモリを扱いたいときには、プログラムの中で「データの場所」「プログラムの場所」を示すレジスタを書き替えればいい。



しかし、MS-DOS の COM ファイルは、データもプログラムも同じ場所にあり、さらに場所の書き替えは行わない、という前提で作られる。

最初に書いたように、場所をセットするのは OS の役目で、プログラムはその「場所」で動くことになる。




ここで一つ説明が必要になると思う。


なんで、プログラムやデータを示す「場所」のレジスタをセットする、という必要があるのか。

1234 番地を使いたいのなら、プログラムの中で 1234番地、って書いとけばいいのに。



この答えは単純で、プログラムが同時に複数読み込まれるからだ。


あるプログラムが 1234 番地を使う、と指定していたとしたら、別のプログラムで 1234番地を使ってはならない。

でも、プログラムを作るときに、他の「知り得ない」プログラムが、どこのメモリを使っているかなんてわからないのだ。


プログラムの中では 234番地、とだけ指定して置けば、この問題を解決できる。


2つのプログラムが、それぞれ 234番地をつかおうとしていても、OS が片方は「1000番地から」、もう片方は「2000番地から」使うように指示する。

そうすれば、片方は実際には 1234番地、もう片方は 2234 番地を使うことになり、同時に使って問題ない。



そもそも、8086 が 20bit のアドレスを 16bit のレジスタとの組み合わせで示す、という変なアドレス指定方式を採用しているのは、このような使用方法を想定していたためだ。

MS-DOS の COM ファイルは、素直にそのやり方を実現したプログラム形式なのだ。




でも、これでは大きなプログラムを作れない。COM ファイルの限界を無くしたいときには、EXE ファイルを使う。



COM ファイルは、単純にプログラムを入れてあるだけのファイルだった。

でも、EXE ファイルの中は構造になっていて、少なくとも次の3つのものが入っている。


・プログラム

・データ

・再配置情報


COM ファイルでは、プログラムとデータは混然一体となっていた。

でも、EXE では分離してある。


…まぁ、もちろん混然一体にしてもかまわない。

でも、先に書いたように 8086 は「プログラムの場所」と「データの場所」を別々に指定できる仕組みを持っている。


だから、分離しておけば、読み込み時点で OS が適切にメモリに配置してくれる。



最後に残る「再配置情報」っていうのは何かというと、プログラムを実行する間に OS が書き替えるためのヒントだ。


しつこく繰り返すことになるが、8086 には「データの場所」「プログラムの場所」を、それぞれ 64Kbyte 確保する仕組みがある。

それ以上のメモリアクセスが必要なら、これらの「場所」を示すレジスタを書き替えないといけない。


でも、ここで先に書いた問題が再燃する。

OS が「1000番地から」使うように指示をくれていたのに、勝手に「2000番地から」使うように書き替えることは許されないのだ。

そんなことをしたら、他のプログラムとぶつかってしまう。


そこで、プログラムを作る際には、仮に「0番地から」メモリアドレスを始めて、場所を示すレジスタも、自由に書き替えてよいことにする。

ただし、プログラム中でレジスタを変更する場所は、すべて「再配置情報」に記しておく。


OS は、ファイルからプログラムを読み込み、メモリに配置する際に「再配置情報」を参照する。

そして、書かれた数値に、適切な数値を足していくのだ。


1000番地から使用するのであれば「0番地から」になっている部分に 1000を足し「1000番地から」に変える。

アドレスを指定している個所をすべて変更すれば、そのプログラムはメモリのどこに置かれても、正しく動作する。




プログラムをメモリのどこにおいても大丈夫なように作る…このようなプログラムを「リロケータブル」という。

英語で書けば relocatable。re(再び)locate(配置)able(可能)で、日本語訳は「再配置可能」。


COM ファイルは、そのままでリロケータブルだ。「場所」を示すレジスタさえ正しく整えれば、どこにおいてもそのまま動く。


EXE ファイルのプログラムは、読み込むアドレスを「仮に固定して」作られている。リロケータブルではない。

でも、再配置情報を使って、OS が書き替えを行うことで、リロケータブルになっている。

利用者にとっては気にする必要はないだろう。どちらもリロケータブルだ。



さて、ここでやっと、最初の問題に戻れる。


僕は、COM ファイルの要件を「プログラム、データ含めて 64Kbyte 以内のもの」というように書いたのだけど、実はこれは嘘だ。

COM ファイルはサイズが問題なのではなく、「そのままでリロケータブル」なプログラムを意味している。


メモリのどこに読み込んでも実行できる。

MS-DOS の仕組みでは、「読み込み時点」では、64Kbyte 以内に収まることが大切だ。

これは、ファイルサイズが 64Kbyte 以内であること、と同じ意味になる。


実行された後に、空きメモリを探して確保して、外部データファイルを読み込むのは構わない。

こうすれば、COM ファイルでも 64Kbyte を超えるデータを扱える。




歴史的には、MS-DOS の元になった CP/M という 8bit OS があり、その OS の実行ファイルが COM だった。

8bit なので、同時にいくつものプログラムを読み込むことはできない。


MS-DOS では、8086 の仕組みを利用して、COM を複数同時に読み込めるようにした。

CP/M にはできない芸当で、便利だった。


でも、16bit らしい大きなプログラムは作れなかった。

そこで、MS-DOS の Ver 2 になってから、EXE ファイルの仕組みが拡張された。



先に書いたように、仕組みは違うがどちらも「リロケータブル」なファイルで、ユーザーから見たら特に違いはない。


形式が古く、制限の厳しい COM ファイルを好きこのんで作る理由なんて、どこにもなかった。

DOS 標準プログラムも、ver1 から使われている command.com 以外には、非常に小さなプログラムでもほぼ EXE で作られていたと思う。


しかしまぁ、ファイル構造を示すヘッダがない、構造を解析してパッチを当てる時間が不要、などの理由で COM ファイルは「実行が速い」とされた。

ディスクアクセス時間に比べれば、そんなものは誤差範囲レベルなのだけど。


制限が厳しいからこその腕試し、というような側面も相まって、それなりに COM ファイルは作られていたと思う。




さて、ここからは余談。



EXE ファイルのような、「OS がアドレスを変えることで」どこにでも読み込めるようにする…という形式が、いつ頃考案されたのか僕は知らない。


マルチタスクなら、複数のプログラムがメモリに読み込まれるので、当然「プログラムをどこにおいても良い」、つまりはリロケータブルである必要がある。


マルチタスクの元祖は、Multics か、その前身となる実験だった TSS あたりになるのだけど、今調べたら TSS では当たり前に「リロケータブル」となる仕組みが作られていたようだ。


おそらくは、シングルタスクでも複数のライブラリを組み合わせて使用する場合などで、アドレス調整の必要からリロケータブルの仕組みが作られたように思う。



以前調べた中では、WhirlWind I のライブラリプログラムは、リロケータブルにできるように作られていた。

もっとも、これは OS が仕組みを作っているとかではなくて、ソースコードレベルで「別のアドレスに置くときは、ここの部分を改造する」など、適切なコメントを人間に残す形なのだけど。


ともかく、プログラムを置くアドレスが変わっても、正しく動作するプログラムを作ろう、という意識はあったことになる。

となれば、後はどこかの段階で、機械的なサポート方法が考案されたのだろう。



MS-DOS は、Ver 1 では COM ファイルしか使えない。

CPU がサポートする機能を使ってリロケータブルを実現してはいるが、「OS の機能」として、制限の少ないリロケータブルをサポートするのは Ver 2 以降だ。


大型機で使われていた技術を取り入れたものだろう。




X68000 という 16bit パソコンがあった。

CPU に 68000 という、昔の Apple Macintosh や、セガの「メガドライブ」に使われたものと同じ LSI を使用していた。


この CPU では、MS-DOS は動かない。

だけど、MS-DOS を参考にしたと思われる、Human68k という OS を標準搭載していた。



実行ファイルも、MS-DOS のように2種類あった。x 形式と r 形式だ。

x 形式は、EXE と同じように、実行前にプログラムが OS によって書き替えられる。


r 形式は書き替えずにそのまま実行する。この点は COM ファイルと同じだ。

でも、68000 には MS-DOS のような「メモリの始まる場所」を示すような数値はなかった。


それでも、リロケータブルなプログラムを作ることができた。




8086 では、アドレスを表すのに3つの方法があった。


・今実行中のアドレスに、1byte 分、-128~127 の数値を加える。

・今セットされている「場所を示すレジスタ」を先頭として、2byte 分、0~65535 の数値を加える。

・場所を示すレジスタも書き替えて、全メモリ(20bit)を指定する。



これに対して、68000 では、大きく分類して2つのアドレス指定方法があった。


・今実行中のアドレスに、2byte 分 -32768~32767 の数値を加える。

・全メモリ (24bit) を指定する。



8086 の COM ファイルは、「場所を示すレジスタ」を OS がセットしてくれるのを前提として、リロケータブルになっている。


それに対し、68000 では、レジスタの助けを無しにリロケータブルにできる。

アドレスの範囲は同じ 2byte に見えるが、常に「実行中のアドレス」を中心とするため、作り方次第で 64Kbyte を超えるプログラムも作れる。


68000 の「リロケータブル」は、8086 の「リロケータブル」よりも、もっと制限が少ないのだ。



でも、X68k では、「 r 形式」としてリロケータブルなプログラムを実行できるようになっていたにも関わらず、開発のためのサポートはなかった。


MS-DOS なら、C言語でメモリモデルを選択すれば作れた。

だけど、X68k のC言語は x 形式しか作ることができず、r 形式を作るには、アセンブラですべてを書くしかなかった。



先に、MS-DOS でも COM を作るのは腕試しの側面があった…というようなことを書いた。

とはいえ、C言語でも簡単に作れる。


でも、X68k の r 形式はすべてをアセンブラで書く必要がある。

「適切に書かれれば」という前提付きだけど、C言語の生成するコードよりも効率が良く、高速動作する…ことも考えられる。


だから、r 形式のプログラムは「速い」という神話が生まれた。

いや、ただの神話で、r だから速いなんてことは全然ないのだけど。


僕もあこがれて、いくつかの小さなプログラムを r 形式になるようにアセンブラで書いていた。

まぁ、習作だよね。特に発表はしてなかったと思う。


#特に、シングルタスクの OS 上でマルチタスクな動作を実現する「常駐プログラム」はアセンブラで書く必然性があったので、そういうものを試作していたはず。


大学2年生の時に作った、BANDITS も r 形式ではなかったかな。

ただ、このゲームは未完成。習作何本か作っていい気になって大作に挑み、失敗した。


技術が伴わないのに大きな目標を立ててはいけませんね。




同じテーマの日記(最近の一覧)

コンピュータ

関連ページ

【追悼】森公一郎さん (LSI-C の作者)【日記 15/01/20】

Programming Tips

別年同日の日記

03年 ピザ

06年 クリスマス飾り

15年 エイダ・ラブレイス 命日(1852)

15年 NTT工事延期

16年 セガ・サターン復活

18年 闘龍伝説エランドール


申し訳ありませんが、現在意見投稿をできない状態にしています


戻る
トップページへ

-- share --

2000

-- follow --




- Reverse Link -