C言語による常駐プログラム作成技法

この文書について

この文書は Computer fan No. 6 (1994年) に掲載されたものに加筆、一部修正してHTML形式にしたものです。

概要

常駐プログラムといえばアセンブリ言語で記述するのが常識とされていましたが、 最近のCコンパイラには常駐プログラムの作成を容易にするための 様々な機能があります。

また、下手にアセンブリ言語で作成するより、 C言語で作成した方がプログラム全体の見通しもよくなり、 デバッグなどが容易です。

そこでBORLAND C++によって常駐プログラムの作成を容易にするための関数を 作成してみました。 プログラムはC言語で記述してありますが、 中身はほとんどインラインアセンブラです。 これは細かな制御を行うためとプログラムサイズのためです。

なお、動作確認はPC-9801上で行いましたが、 palpal.exe以外はMS-DOSで動作すると思われます。(未確認)

常駐プログラムとは

常駐 (TSR; terminate and stay resident) プログラムとはプログラム終了後もメモリに残るタイプのプログラムです。

有名なものにコンソール出力の履歴をいつでも参照できるxscript、 コマンドラインの編集を可能にするKI-Shellなど (これらはフリーソフトウェアです)、 そして標準でMS-DOSに付属のものにprint.exeやmouse.comなどがあります。

プログラミングのための基礎知識

MCB

MS-DOSはMCB (memory control block) によってメモリーが管理されています。 MCBは次のような16バイトのブロックです。

+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|+0|+1|+2|+3|+4|+5|+6|+7|+8|+9|+A|+B|+C|+D|+E|+F|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 1|  2  |  3  |    4   |           5           |
+--+-----+-----+--------+-----------------------+

1: id (BYTE)       | 'M'または'Z'
                   | ('M':次のメモリーブロックあり, 'Z':最終メモリーブロック)
2: owner(WORD)     | そのメモリーブロックを使用しているプログラム
                   | のPSPセグメント(オフセットは0000H)
3: size(WORD)      | パラグラフ(16バイト)単位のサイズ
4: reserve(BYTEx3) | 予約領域(未使用)
5: name(BYTEx8)    | プログラム名(MS-DOSバージョン4未満では未使用

また、MCBは次のようにつながっています。

+---+----+-----+--------+-----------------------+
|'M'|    |size0|        |                       | MCB 0
+---+----+-----+--------+-----------------------+
|メモリ領域0                                    | size0 [パラグラフ]
+---+----+-----+--------+-----------------------+
|'M'|    |size1|        |                       | MCB 1
+---+----+-----+--------+-----------------------+
|メモリ領域1                                    | size1 [パラグラフ]
+---+----+-----+--------+-----------------------+
|'M'|    |size2|        |                       |
+---+----+-----+--------+-----------------------+
                        :
                        :
+---+----+-----+--------+-----------------------+
|'Z'|    |sizeZ|        |                       | MCB Z
+---+----+-----+--------+-----------------------+
|メモリ領域Z                                    | sizeZ [パラグラフ]
-------------------------------------------------

先頭のMCBブロックのセグメント (オフセットは0000H) を取得するにはMS-DOS 非公開システムコール52Hを使用します。 このシステムコールで取得した ES:[BX - 2] に先頭のMCBのセグメントが格納されています(サイズはWORD)。[1, 2, 3, 5]

つぎにMCBをたどるプログラム例を示します。

#include <stdio.h>
#include <stdlib.h>
#include <dos.h>

typedef unsigned char BYTE;
typedef unsigned short int WORD;

int main(void)
{
    struct mcb {
        BYTE id;
        WORD owner;
        WORD size;
        BYTE reserve[3];
        char name[8];
    }
    far *pmcb;

    asm MOV AH, 52H;
    asm INT 21H;
    pmcb = MK_FP(*((WORD far *)MK_FP(_ES, _BX - 2)), 0x0000);

    for(;;) {
        printf("%04X owner = %04X, size = %X(%ld)\n",
               FP_SEG(pmcb), pmcb->owner,
               pmcb->size, (long)pmcb->size*16);

        if(pmcb->id == 'Z')
            break;
        else
            pmcb = MK_FP(FP_SEG(pmcb) + pmcb->size + 1, 0x0000);
    }

    return EXIT_SUCCESS;
}

PSP

PSP (program segment prefix)はプログラムがロードされると その先頭に作成される256バイトの領域で、次のような構造をしています。[1,9]

PSPの構造
オフセット サイズ 内容
00H WORD プログラムの終了命令
02H WORD 割り当てメモリーブロックの直後のセグメント
04H BYTE 予約
05H BYTEx5 DOS機能のfarコールのための5バイト
0AH DWORD INT 22Hのコピー
0EH DWORD INT 23Hのコピー
12H DWORD INT 24Hのコピー
16H〜41H 非公開: ただし2CH (DWORD): 環境変数領域のセグメントのみ公開
50H BYTEx3 INT 21H, RETF
53H BYTEx9 予約
5CH BYTEx16 第1FCB (file control block)
6CH BYTEx16 第2FCB (file control block)
80H BYTE コマンドラインの文字数
81H BYTEx127 コマンドライン (0DHでターミネイト)

80H以降はデフォルトのDTA (disk transfer area)

C言語のメモリ管理

C言語(BORLAND C++, small model)で作成されたプログラムはロード直後は 次のようになっています。[1,13]

-------------------------------------------------
|MCB                                            |
-------------------------------------------------  ← _psp:0x0000
|PSP                                            |
-------------------------------------------------  ← CS:0x0000
|_TEXTクラス (CODE)                             |
-------------------------------------------------  ← DS:0x0000
|_DATAクラス (DATA)                             |
-------------------------------------------------
|_BSSクラス (BSS)                               |
-------------------------------------------------  ← DS:__heapbase
|nearヒープ                                     |
------------------------↓-----------------------  ← DS:__brklvl
|<free>                                         |
------------------------↑-----------------------  ← SS:SP
|スタック                                       |
-------------------------------------------------
|farヒープ                                      |
-------------------------------------------------
|<free>                                         |
-------------------------------------------------
|                                               |
:                                               :
:                                               :

注: レジスタ (CS, DS, SS, SP) をC言語からアクセスするには _CS のようにして擬似変数を使用します。 変数はC言語レベルで記述してあるので アセンブリ言語からアクセスするには先頭に _ を付加する必要があります。

注: __heapbase,__brklvl は非公開変数です。[1]

nearヒープはmallocで取得するメモリーのための領域、スタックは一時的な データを格納するための領域、farヒープはfarmallocで取得するメモリーのた めの領域です。

ただし、ヒープ (near・farとも)・スタックは常駐後には必要の無いもので す。(malloc・farmallocでメモリを取得していれば別ですが、常駐プログラ ムではほとんど使用しないと思います)

したがって、nearヒープの直前までを常駐させればよいことがわかります。 これでも必要の無い部分 (初期化部分など) が常駐してしまいますが、C言語で 記述する以上はこれ以上は非常に難しくなります。

プログラム

常駐するだけのプログラム

常駐するだけで何もしないプログラムは次のようになります。

#include <stdio.h>
#include <stdlib.h>
#include <dos.h>

extern void _restorezero(void);

typedef unsigned char BYTE;
typedef unsigned short int WORD;

int main(void)
{
    WORD size;

    puts("常駐プログラムサンプル");

    _restorezero();

    size = _SS - _psp + (_SP/16) + 20;  /* 一番安全なサイズ */

    asm MOV AH, 31H;
    asm MOV AL, 0;               /* EXIT_SUCCESS */
    asm MOV DX, size;
    asm INT 21H;

    return EXIT_SUCCESS;        /* dummy */
}

_restorezero 関数は非公開のライブラリ関数で割込ベクタ 00H, 04H, 05H, 06Hを元に戻します。[1]

メインメモリの情報を表示させると次のようになり、 常駐した事がわかります。 以下はmsによる画面表示です。

 MCB  PSP  Size      Owner name         Devices / Parameters(max 4
---- ---- ------- ----------------- ------------------------------
0587 0588   2624  melemm            (dv) EMMXXXX0
062C 0000     64  <free>
0631 0632     64  rpal98
0636 0682   1184  tsr_temp.exe(env) ←環境変数領域
0681 0682  69488  tsr_temp.exe      ←プログラム本体
1779 17C4   1168  <free>
17C3 17C4 558000  <free>

このプログラムではスタックの直前まで常駐させているのでnearヒープを (常駐部から) 使用している場合でも正常に動作します。 ただし、非常に常駐サイズが大きくなっているので実用的ではありません。

常駐解除可能なプログラム

常駐しているかしていないかを判断するには、メモリのどこかに 「常駐している」という目印を付けておかなければなりません。[5] よく使用される領域に次のようなものがあります。

この中で固定領域はプログラム作成は簡単ですが、 他のプログラムによって使用される可能性があります。

ワークエリアの未使用領域は将来のバージョンにおいて 使用される可能性が非常に高く、 text, grph-vramの未使用領域は規格の拡張 (30行BIOS, 98MATEの640x480表示) によって未使用領域ではなくなっています。

また、割込ベクターを1つ占有するのは割込ベクタという 資源を無駄に消費するので望ましくありません。 INT 2FHを使用する多重割込を使用する方法もありますが、 他のプログラムとの競合は避けられません。

可変領域のMCBの予約領域も将来のバージョンの事を考えると 使用しない方が無難です。 現にDOSバージョン4未満の時に開発された常駐プログラムの中には MCBのオフセット8--15を使用し、 DOSバージョン5で使用するとメモリーマップの表示が乱れるものもあります。 (もっとも、この場合は画面表示の乱れだけで済んだので良かったのですが...)

そこで、今回はPSP領域に目印を付ける事にしました。 ただし、PSP領域でも破壊してはいけない領域もあります。 安全と思われるコマンドラインがコピーされる位置で、 デフォルトのDTA (disk transfer area: ディスク転送領域) となるオフセット0080Hからを使用する事にしました。 常駐プログラムからは基本的にディスク関連を含むほとんどの DOSシステムコールを使用できない事になっているので特に支障はないでしょう。

今度のプログラムはpalpal.exeがサンプルになります。

palpal.exe

ということで実際の (常駐解除可能な) 常駐プログラムのサンプルpalpal.exeです。 これだけはPC-9801専用です。 このプログラムは常駐して次々とパレット0を滑らか (?) に変更していきます。 タイマ割込を使用しているのでそれを使用する プログラムとの相性は最悪でしょう。 環境変数領域を開放しているので少しは常駐量が減っています。

オプションは-sで常駐、-rで開放、-?、その他でヘルプ表示です。

タイマBIOSについては [4, 6] が参考になりました。

30行BIOSで行数を少なく (14行) に設定したときは暴走しました。 これはpalpal自体の問題と考えられます。

中にはライブラリにあるような関数が結構あるのですが、 これは常駐サイズを少しでも減らそうとして記述したからです。 その後に変数__heapbaseの存在を知ってからは意味がなく、 また、どれだけサイズを減らせたのかは疑問ですが (逆にサイズが増加したかもしれません)、 一応自作の関数の方を残す事にしました。 今まで説明した事はすべてこの中の関数を使用すれば簡単に記述できます。

DOSバージョン5以降ではlhコマンドまたはloadhighコマンドで UMBにロードし、開放する事ができます。 (バージョン5未満ではmelemm.386,umbload.com (MELCO) などを使用すればUMBへのロードは可能と思われます。 ただし、開放は不可能と思われます。(未確認))

プログラム内の関数の使用

常駐プログラムの作成には dos.c, dos.h, tsr.c, tsr.h, yasu.h を使用します。 このとき、ヘッダーファイルはカレントディレクトリに置いて下さい。 (または、絶対・相対パスを用いて明示的に指定して下さい。) BORLAND C++には同名のファイル (dos.h) が存在するのでこのような制限があります。 問題になる場合はdos.hをmsdos.hに変更するなどして下さい。

また、このヘッダーファイルではANSIで予約とされている識別名を定義して います。(__TSR_H__など) 問題になる場合 __TSR_H__TSR_H に変更するなどして下さい。

プログラムを常駐させるにはtsr_is_stayedまたはtsr_get_stayed_seg で常駐済みかを確認し、tsr_stayで常駐、tsr_removeで開放します。 環境変数領域はプログラマの責任で開放しなければなりません (dos_remove_env)。

また、マクロ展開されるものがいくつかありますが、 関数名を "()"で括る事により明示的に "関数" を呼び出す事ができます。 これは関数名の後のホワイトスペース (空白,TAB,改行) 以外の文字が "(" 以外であると関数型マクロの展開が行われないからです。

例:

プログラム例を以下に示します。 (奇数回目の起動で常駐し、偶数回目の起動で開放します)

#include <stdio.h>
#include <stdlib.h>
#include "dos.h"
#include "tsr.h"
#include "yasu.h"

#define IDOFS 0x0080                /* 常駐idのオフセット (推奨値) */
#define IDSTR "$$TSRSAMPLE$$"       /* 常駐id */

extern WORD __heapbase;
extern WORD __brklvl;

int main(void)
{
    if(tsr_is_stayed(IDOFS, IDSTR)) /* 常駐しているか? */
        tsr_remove(IDOFS, IDSTR);   /* ならば開放 */
    else {
        dos_remove_env();           /* 環境変数領域開放 */
        tsr_stay(EXIT_SUCCESS, IDOFS, IDSTR,         /*常駐 */
                 _DS - _psp+(__heapbase/16 + 16));   /* ←注 */
    }

    return EXIT_SUCCESS;
}

"注" の部分はコード+データ領域を常駐するときの数字です。 常駐させたい領域にあわせて次のように指定するとよいでしょう。 (αは適当な数字を指定して下さい。私は念のため16程度にしています。)

動作確認

以下の環境で動作を確認しました。

著作権・利用条件

Copyright © 1994 HIRATA Yasuyuki, all rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

This software is provided by the author (HIRATA Yasuyuki) "as is" and any express or implied warranties, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose are disclaimed.

In no event shall the author or contributors be liable for any direct, indirect, incidental, special, exemplary, or consequential damages (including, but not limited to, procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage.

注意: このライセンスはComputer fanの記事中に書かれたライセンスとは異なります。 Computer fanに収録されているファイルは、 記事中のライセンスと上記のBSDスタイルのライセンスの いずれか一方のライセンスを選択することができます。 ただし、ここから入手したファイルは選択することができません。

ファイル

参考文献


© 1994, 2001 HIRATA Yasuyuki <yasu@asuka.net>, all rights reserved.