プリント基板設計・シミュレーション

TOP > アポロレポート > コラム > 第56回 プログラミングについて『DOSのプログラム2』
コラム
2024/01/23

第56回 プログラミングについて『DOSのプログラム2』

アポロレポート

 DOS用のプログラムは、UNIX用のプログラムと比べて簡単な部分も確かにありますが、かえって難しい部分があります。「かたがパソコン用のプログラムだ!」などと考えていたら大間違いです。それはメモリーの使い方で、UNIXでは湯水の様にメモリーやスタックを消費しても結構動作してくれるのですが、DOSではこのようにはいきません。前回もお話したように、64Kバイトのセグメントが10個しかないのです。スタックはどんなに頑張っても64Kバイトより大きくはできず、コードもスタックも含めて、最大でも640Kバイトしか使えないのです。その上、このメモリーの領域に常駐しているプログラムがあったときには悲しいくらいに小さくなってしまいます。
 実際に、私はVMSやUNIXで御機嫌な環境でプログラムを作っていましたので、DOS用のプログラムを書いたときには、この制限は知っていたものの実際に作ってみるとものすごく窮屈な環境でプログラムを動作させなければならないことが身にしみて教えられました。

 余談ですが、VAX/VMSでのプログラミングは非常にやりやすいものでした。VAXはバイトマシンですので、構造体に穴があくこともなく、ポインタは32ビットのみで、リトルエンディアンであるので関数の引数のやり取りのときの整数のサイズをあまり気にしなくても動作してしまいました。
 実際にはCPUのアーキテクチュアにもよりますが、バイトマシンであるということは、メモリー上の値のアラインメントはどこにあってもいいので、次のようなプログラムは何事もなく実行できます。

 #include <stdio.h>

 main()
 {
  char  data[9];
  double v1;

  v1=123.456;

  move(&v1,data+1);

  printf("%f\n",*(double *)(data+1));
 }

 move(from,to)
 char *from,*to;
 {
  int i;

  for(i=0;i<=7;i++)to[i]=from[i];
 }

ところが、これをHPのワークステーションで実行するとコアダンプしてしまいます。試しにPC98で実行すると問題ありませんでした。確かにプログラム上での理屈では問題はないのです。エンディアンの問題では、

 #include <stdio.h>

 main()
 {
  short c;

  c=123;

  disp(&c);
 }
 disp(c)
 char *c;
 {
  printf("%d\n",*c);
 }

はVAX/VMSでは正しい結果を表示しますが、HPのワークステーションではこれもうまくいきません。
 厳密に言うとあまりこれらのようなプログラムを書いてはいけないのですが、VAX/VMSの扱いやすさは今から思うと格別だったような気がします。またその頃はプログラムを移植する必要もなかったので、FORTRANの拡張機能を使いまくっていました。

 UNIXでプログラムを書いていた人がDOS用のプログラムを書くときに、最初に突き当たり、最後まで苦労する壁は、なんといってもメモリーです。大きな配列は作れないし、スタックがすぐにオーバーフローしてしまうのです。ですので、UNIX用のプログラムをDOS用に移植するときなどはコンパイルをやり直しただけでうまく動作することはあまりありません。コンパイルすらうまくいかないことも多いのです。

 その辺を詳しく見てみましょう(DOSエクステンダーのことは考えずに、あくまでもノーマルな状態での環境とします)。DOSでは64Kバイトというサイズが1つメモリーのブロックになっているので、プログラムによっては数種類のメモリーのモデルを考えることができ、データとコードの制限がそれに伴って制限があります。呼び名はコンパイラによって違うかも知れませんが、ここではマイクロソフトのコンパイラでの呼び方を使います。

  モデル   | コード | データ |データ配列
 -------------+---------+---------+-----------
 タイニー   |<64K |<64K |<64K
 スモール   | 64K | 64K | 64K
 メィディアム | 無制限 | 64K | 64K
 コンパクト  | 64K | 無制限 | 64K
 ラージ    | 無制限 | 無制限 | 64K
 ヒュージ   | 無制限 | 無制限 | 無制限

 タイニーモデルは、コードもデータもまたその合計も64Kバイトを越えることはできません。スモールモデルはコードとデータそれぞれに64Kバイト以内です。メィディアムモデルはコードの制限はありませんがデータは64Kバイト。コンパクトモデルはその反対。ラージモデルはコード、データとも制限はありませんが、配列は64Kバイトを越えることはできません。ヒュージモデルはすべてが無制限です。無制限と言っても640Kバイトが限界です。

 もしUNIXなどの環境で動いていたプログラムを移植するときには、ラージモデルかヒュージモデルを普通は選択することと思います。運が良ければコンパイルも難無く通過し簡単に動作します。移植を意識して作成されたものか、小さなメモリーしか使用しないプログラム以外はまず簡単に移植できません。上の表のデータや配列の無制限という魅力的な制限も、実はスタック上に作れるもの(自動変数)の制限ではなく、静的なものに限られるのです。ということは、

 abc()
 {
  short data[4000];
     :
     :
 }

はどうやっても無理なのです。ですので、これを static にするか、関数外の変数にしなければなりません。

 純粋にDOS用のプログラムとして開発を行なうときには、最初からそれ用と分かっているのでメモリーについては最初から考慮して作成するのは当たり前のことです。このとき、どのメモリーモデルを使用するのかは作成するプログラムの性質に依存します。例えば、とにかく小さいプログラムで、標準入力から読み込んだ文字の文字ケースを変えるといったたぐいのコードもメモリーの使用量も少ないものでは、タイニーモデルかスモールモデルで十分です。タイニーモデルの場合、ファイルの拡張子がCOMという実行ファイルを作成できます。拡張子がEXEのものとの違いは、EXEのヘッダー部分がないもので、通常のプログラムとして使用できる他、絶対アドレスに置くプログラムにしたり、メモリーに常駐するプログラムにすることができます。
 勿論小さいプログラムの場合はどのメモリーモデルを使用しても構いませんが、ラージモデルなどの場合はポインタがFAR属性を持ったものになりますので、同一のセグメントのデータを扱うにもかかわらず、32ビット分のポインタの計算をしてしまいますので実行速度が落ちる可能性があります(とはいっても現在のパソコンはメチャクチャ速いのでその差はあまり分かりませんが...)。

 メモリーモデルがスモールだからと言って、64Kバイトしかメモリーを使用できないということではありません。この64Kバイトというのは、静的な変数とスタック上に確保される動的な変数の総バイト数のことを意味していますので、OSにメモリーの確保を要求する場合は別になります。
 DOS用のコンパイラにはFAR領域(640Kバイト内のどこでも)のメモリーを確保してくれるものがあります。次の例では _fmalloc を使っていったいどれくらいのメモリーを確保できるかを調べてみましょう。

 #include <stdio.h>
 #include <string.h>
 #include <malloc.h>

 #define SIZE 10

 main()
 {
  long i;

  i=0;
  while(1){
   if(_fmalloc(SIZE)==NULL)break;
   i++;
  }

  printf("%2d %6ld %6ld\n",SIZE、i,i*SIZE);
 }

 このプログラムを SIZE を1~20まで変えて実行してみると私のパソコンでは、

1 126888 126888
2 126888 253776
3 84594 253782
4 84594 338376
5 63444 317220
6 63444 380664
7 50754 355278
8 50754 406032
9 42293 380637
10 42293 422930
11 36250 398750
12 36250 435000
13 31718 412334
14 31718 444052
15 28193 422895
16 28193 451088
17 25373 431341
18 25373 456714
19 23064 438216
20 23064 461280

となりました。これはスモールモデルで実行しましたので、64Kバイトよりも大きいメモリーを確保できていることが分かります。上の結果を見てみると、ちょっと不思議なことが分かると思います。SIZE が 2のときと3のときでは確保できたバイト数に違いが殆どありません。また、SIZE が 9のときには8のときよりも確保できたバイト数が減り、10になると9よりも多くなっています。また SIZE が大きくなると確保できたバイト数が増えています。
 この理由を詳しく調べてみると、 _fmalloc 関数は確保したメモリーのアドレスを返すのですが、このアドレスの2バイト前の位置に確保したメモリーのサイズが書き込まれていました。ということは、

    プログラム                  OS

 10バイトの確保を要求    →   確保したメモリーのサイズを書き込むために
                   要求されたサイズ+2バイトのメモリーを確
                   保する。

 確保したアドレスを受け取る  ←   最初の2バイトにメモリーのサイズを書き込
                   み、確保したアドレス+2バイトのアドレス
                   を要求側に返す。

ということになっているのです。またアラインメントの関係で、要求したサイズによってはメモリー上に連続して確保することができずにまったく使用しないメモリーができてしまうのです。DOSに限ったことではありませんが、このようにメモリーを動的に確保する場合、確保するサイズには気をつけなければならないことに注意して下さい。

 スタックサイズも小さいのでオーバーフローしないように気を付けなければなりません。次の例は再帰呼び出しをどれくらい深く呼び出せるかを実験するものです。

#include <stdio.h>

 int depth=0;

 main()
 {
  abc();
 }
 abc()
 {
  printf("%d\n",++depth);
  abc();
 }

これを実行してみると 119 回目でスタックがオーバーフローしてしまいました。次に、関数 abc に int i; を追加し、

 #include <stdio.h>

 int depth=0;

 main()
 {
  abc();
 }
 abc()
 {
  int i;

  printf("%d\n",++depth);
  abc();
 }

として実行してみると、99 回目でオーバーフローしました。悪乗りして、int i; を配列にして、

 #include <stdio.h>

 int depth=0;

 main()
 {
  abc();
 }
 abc()
 {
  int i[100];

  printf("%d\n",++depth);
  abc();
 }

として実行してみると、5 回目でオーバーフローしました。スタックの設定が小さいこともあるのですが、悲しいくらいにすぐオーバーフローしてしまうことが分かると思います。
 これではどうにもならないので、int i[100]; をブロックを新たに作って実験してみました。

 #include <stdio.h>

 int depth=0;

 main()
 {
  abc();
 }
 abc()
 {
  {
   int i[100];
  }

  printf("%d\n",++depth);
  abc();
 }

理屈ではブロックが終了した時点で配列 i は消滅するはずなのですが、やはり 5 回目でオーバーフローしてしまいました。多分これはコンパイラの問題で、このブロックの終了時にスタックを開放していないのだと考えられます。そこで、

 #include <stdio.h>

 int depth=0;

 main()
 {
  abc();
 }
 abc()
 {
  int i[100],j[100];

  printf("%d\n",++depth);
  abc();
 }

とすると、2 回目でオーバーフローしてしまうのですが、

#include <stdio.h>

 int depth=0;

 main()
 {
  abc();
 }
 abc()
 {
  {
   int i[100];
  }

  {
   int i[100];
  }

  printf("%d\n",++depth);
  abc();
 }

とすると、5 回目でオーバーフローしました。

 ということは、スタックを少しでも節約するためには、自動変数はブロック内でこまめに宣言した方が良いということになります。

 ということでまた次回。


そのお悩み、
アポロ技研に話してみませんか?

アポロ技研でワンランク上の物創りへ!
そのお悩み、アポロ技研に話してみませんか?