なんとなく思い出したことを書き並べてみました。 今となっては実用性絶無なんで悪影響が出てくることはないと思うんですが... きっかけはこちら
ハードウエアの状態が変化したとか致命的なエラーが起こったとかいうときに 発生する割込み、ですが、ユーザ側からサービス(システムコールや BIOS機能など)を呼び出すときにも割込みを使います。ここらへんはDOSでも UNIXでもだいたい同じらしいす (「ソフトウエア割込み」などと呼ぶことに)
割り込みには番号がついており、 DOSの機能(ファイルを開くとか閉じるとか文字を表示するとか)は 私の使ってたPC98では0x21番目の割込みを呼び出す、といったことをしていました。 VB Magazine関連でよくみかける イントツーワンという会社の[int21]も たぶんここから来たんだと思いますが
割込みというのは、実際には
void (*call_vector)()[256];みたいな呼び出しテーブル(割込みベクタテーブル)に従って、それに対応する アドレスを呼び出すなんてことをしているわけです。 当然ですが1つの割込みで登録できるベクタは一つだけです。 call_vector[0x21]にはMS-DOSのシステムコール(ファンクションリクエストと 呼んでいたように思う)が実装されている場所(エントリポイント)の アドレスが入っています。
本物の割り込みには引数も戻り値もないです。 いつ発生するか分からない割込みでレジスタやスタックの状態を 事前に変化させたり、戻ってきたら変化していたり、というのでは 困ります。 ソフトウエア割込みの場合は、少なくとも呼び出しタイミングは 使う側が把握しているわけですから、呼び出し時のパラメータや戻り値には レジスタを使います。なので、使う側としては、 どのレジスタに何を入れればいいのか、と、呼び出した後のレジスタは どうなるのか(どれが破壊されてどれが戻り値でどれが不変なのか)といった あたりを把握しておかなきゃいけなかったりします。が、それはまた別の話。
ファンクションリクエスト(int 21h)の場合は、AH(AXレジスタの上位8ビット)に 機能番号(0なら文字出力で1なら...といった感じ)が入ります。 各割込みベクタの値は(AH = 35h, AL = 割込み番号)で得ることが できます。どのレジスタに帰ってくるのかは忘れました(^^; 同じように(AH = 25h、AL = 割込み番号)で、割り込みベクタに値を セットすることができます。つまりint 21h(に限ったことではないが)が 呼び出されたときに呼び出される関数(といっていい?)を変更することが できます。(25hと35hは逆だったかも...)
各ベクタはセグメント:オフセットの4バイトで構成されていますので、 ベクタ変更前に割込みを禁止しておかないと、どちらか一方が書換わったときに その割込みが発生すると困ったことになりますがそれもまた余談ではあります。
まあそんなわけで、本来動いていた機能を「横取り」することができます (割り込みのフックという)。これによって、例えば
が、自分勝手にフックしただけでは、本来するべきキーボード割り込みの処理が お留守になってしまい、キー入力がまったくできない、といったような システムが完成してしまいます。そこで、自分のやりたいことが終わったら (または自分のやりたいことをする前に)、自分のルーチンがフックする前に 割り込みベクタに入っていたアドレスを呼び出して本来の処理をしてもらおう というふうになります。そのアドレス自身も誰かが フックしたものかもしれませんが、みんなが行儀よく呼び出してゆけば いつか「本来の処理」に行きつくはずです。中には故意に割り込みをブロック するためにフックしている場合もありますから必ずそうとは言えませんが。
そんなわけで、流れとしては
※1 mov [word 1234:5678], ベクタのオフセットアドレス mov [word 1234:567a], ベクタのセグメントアドレス ※2 call [dword 1234:5678]などという処理が必要になります。1234:5678には4バイトのメモリが 確保され、また※2ではそのアドレスを指すためのコード、つまり
そこで考えられたのが以下のような方法です
※1 mov [word label1], ベクタのオフセットアドレス mov [word label1 + 2], ベクタのセグメントアドレス ※2 db 9ah label1: dd 0hcall far が 9ahだったかどうかよく覚えてないのですが、ともかく db 9ah が callの命令本体(op-code)であるとしますと、 その後に続く4バイトが実際の呼び出し先のアドレスとなります。 そこに直接、割込みベクタに元あったアドレスを入れておけば、 call命令は「割り込みベクタを保存してあるアドレス」を間接的に 示す必要がなくなりますので、※2の部分のメモリは必要なくなります。
つまり、動いているコード内の「飛ぶべきアドレス」を直接いじって しまうことで、メモリを節約することができるのでした。 状況によって、※2の部分がcall 1234:5678 になったり、 call 2345:6789 になったり、いろいろするわけです。
今では何の価値があるかというようなテクニックですが...
なお↑のアセンブラのコードはいい加減な上にTASM方言である「Ideal文法」 なのですが今となってはあまり気にする必要もないような気がします
....
MASMは今ではダウンロ〜ド可能なんだから試してみればいいんだよな。 というわけで試してみます:
Microsoft (R) Macro Assembler Version 6.13.8204 asm1.asm Page 1 - 1 .model small 0000 .code .startup 0017 2E: C7 06 0041 R mov word ptr [vector1 + 0], 5678h 5678 001E 2E: C7 06 0043 R mov word ptr [vector1 + 2], 1234h 1234 0025 E8 0018 call func1 0028 C7 06 0000 R 6789 mov word ptr [vector2 + 0], 6789h 002E C7 06 0002 R 2345 mov word ptr [vector2 + 2], 2345h 0034 E8 000F call func2 0037 9A ---- 004B R call far ptr func3 .exit 0040 func1 proc near 0040 9A db 9ah 0041 00000000 vector1 dd 00h 0045 C3 ret 0046 func1 endp 0046 func2 proc near 0046 FF 1E 0000 R call dword ptr [vector2] 004A C3 ret 004B func2 endp 004B func3 proc far 004B CB ret 004C func3 endp 0000 .data 0000 00000000 vector2 dd ? endfunc3はとりあえず関係ないので放置です(call farが本当に9ahが 知りたかっただけ)。これを見てみると、コード直接書換えのほう(func1)は
.startup/.exit疑似命令は過去に使った記憶がないなあ。6で新設されたのかな (ごまかし)
...でもCOM(Component Object Modelでないほう)を作ると仮定すれば ASSUMEでCS=DS=SSが確定するのでセグメントオーバーライドプリフィックス 必要なくなるような気が。.model tinyってTASMでしか使えなかった 記憶があるのですが、MASM6では使えるのかなあ...いずれにしろそこを クリアしたところで1バイト→3バイトになるだけですが。