Skip to content
Masakazu Yonehara edited this page Aug 24, 2026 · 2 revisions

常駐プログラム(TSR)

常駐プログラムは、実行が終わったあともメモリに残り続けるプログラムです。 割り込みベクタに自分の処理を登録しておくと、他のプログラムが動いている間も そこから呼び出されます。FEP や常駐ユーティリティはこの仕組みで作られています。

基本

やることは2つだけです。

  1. 割り込みベクタに自分のハンドラを登録する(INT 21h AH=0x25)
  2. 常駐して終了する(INT 21h AH=0x31)
        mov     ax, 0x2560              ; AH=25h ベクタ設定 / AL=登録する割り込み番号
        mov     dx, handler             ; DS:DX = ハンドラの位置
        int     0x21

        mov     dx, resident_end        ; 常駐させる最後のラベル
        add     dx, 15                  ; パラグラフ単位へ切り上げる
        mov     cl, 4
        shr     dx, cl
        mov     ax, 0x3100              ; AH=31h 常駐終了
        int     0x21

.COM では DS = CS なので、DX にラベルのオフセットをそのまま入れれば DS:DX がハンドラを指します。

前提条件

  • 常駐サイズはパラグラフ(16バイト)単位です。バイト数ではありません。
  • オフセットは CS:0 起点なので、PSP の 256 バイトも自動的に含まれます。 ラベルのオフセットをそのまま使えば辻褄が合います。
  • ハンドラが呼ばれたとき、DS は呼び出し元のものです。 自分のデータを触るなら ハンドラの中で DS を自分のセグメントに入れ直し、抜ける前に戻してください。
handler:
        push    ds
        push    cs
        pop     ds                      ; DS を自分のものにする
        inc     word [counter]
        mov     bx, [counter]
        mov     ax, 0x1234
        pop     ds                      ; 呼び出し元の DS に戻す
        iret

counter:
        dw      0

resident_end:                           ; ここまでが常駐する

常駐サイズはラベルの順序で決まります

resident_end より後ろに書いたものは常駐しません。 常駐後に使うデータやテーブルは、必ずこのラベルより前に置いてください。

%include でテーブルを取り込むときは特に注意が必要です。ファイルの末尾に置くと テーブルが常駐範囲の外に出ます。この間違いはインストール時にはエラーにならず、 別のプログラムを実行するまで表面化しません。

動作確認は「常駐 → 他を実行 → 再使用」で

常駐した直後に呼んで動いても、それだけでは確認になりません。 他のプログラムを実行してから、もう一度呼んでください。 常駐範囲の外に出たコードは、次にプログラムが読み込まれたときに上書きされます。

実測では、正しいサイズで常駐させた場合、

  • 常駐直後に呼ぶ → 動く
  • もう一度呼ぶ → 動く(常駐データのカウンタも増える)
  • 別のプログラムを実行してから呼ぶ → 動く

一方、常駐サイズを小さくしすぎた版では、呼んだ瞬間に Invalid Opcode になりました。 このときハンドラは1命令も実行されていません。

常駐を解除する

解除するには2つやります。

  1. 割り込みベクタを元へ戻す(AH = 0x25)
  2. メモリを解放する(AH = 0x49。ES に解放するセグメント)

そのためには、常駐する側が「元のベクタ」を解除する側から読める場所に 置いておく必要があります。

; 常駐する側: 先頭に目印と元のベクタを置く
        jmp     near start      ; near を明示する(下記)

sig:    db 'MYTSR!!!'           ; 目印
old_off: dw 0                   ; 元のベクタ
old_seg: dw 0
start:
        ...

解除する側は、ベクタから常駐先のセグメントを求め、目印を確かめてから 元のベクタを書き戻し、メモリを解放します。

実測では、解除後に割り込みベクタが常駐前の値に戻り、メモリの解放も成功しました。

目印を確かめずに解放しないでください。 別のプログラムがベクタを 横取りしていた場合、無関係なメモリを解放してしまいます。

jmp near を明示してください

先頭に jmp start と書くと、アセンブラが2バイトの短いジャンプに縮めることがあります。 すると目印の位置が1バイトずれ、解除する側から見つけられなくなります。

実測でこれを踏みました。解除side は「見つからない」と表示し、常駐は残ったままでした。 jmp near start と書けば3バイトに固定されます。

決まった位置に情報を置く設計では、その前にあるものの長さが変わらないようにしてください。

よくある間違い

  • 常駐サイズにバイト数を入れる。 パラグラフ(16バイト)単位です。
  • 常駐させたいデータを resident_end より後ろに置く。 常駐しません。 インストール時はエラーにならないので気づきにくいです。
  • ハンドラの中で DS を直さずに自分のデータを読む。 呼び出し元の DS が 入っているので、まったく別の場所を読みます。
  • 常駐直後に1回呼べただけで動作確認とする。 他のプログラムを実行してから もう一度呼ぶまでは、サイズが正しいかどうか分かりません。
  • 目印を確かめずにメモリを解放する。 別のプログラムがベクタを横取りしていると、 無関係なメモリを解放してしまいます。
  • 先頭の jmp に near を書かない。 縮められて、後ろの目印の位置がずれます。
  • INT 21h をフックしたハンドラの中で int 21h を呼ぶ。 自分のハンドラに 再入します。退避しておいた元のベクタを pushf + call far で直接呼んでください (→ エスケープシーケンス)。

実機との差異

現時点で確認できているものはありません。 ただし 測定は np2kai 上で行っています。 実機での確認は行っていません。

未検証

  • 常駐サイズが「ぎりぎり足りない」場合の挙動
  • EMS / XMS を使って常駐領域を小さくする方法
  • 常駐プログラム同士がベクタを取り合った場合の順序

検証: 2026-08-24 / WebNP2 (np2kai) + FreeDOS(98) / 再現: guest/probes/TSR60.ASM(常駐側), CALL60.ASM(呼び出し側), TSR60R.ASM + UNTSR.ASM(解除), guest/probes/TSR60S.ASM(常駐サイズを小さくした故障注入版)

正常版が「常駐 → 他を実行 → 再使用」を通ること、故障注入版がその検査で Invalid Opcode になることの両方を確認しています。

Clone this wiki locally