割り込みベクタに関する小話

なんとなく思い出したことを書き並べてみました。 今となっては実用性絶無なんで悪影響が出てくることはないと思うんですが... きっかけはこちら

ハードウエアの状態が変化したとか致命的なエラーが起こったとかいうときに 発生する割込み、ですが、ユーザ側からサービス(システムコールや 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と※2の部分です。(追記: ですが、 以下の説明は間違いだらけです。あとでまとめて修正します) ※1で保存したメモリの場所が、例えば1234:5678(セグメント:オフセット表記ですが 詳しいことは無視して4バイトのアドレスだと思いましょう)という アドレスを持っていたとすると、おおざっぱに言って
※1
  mov [word 1234:5678], ベクタのオフセットアドレス
  mov [word 1234:567a], ベクタのセグメントアドレス

※2
  call [dword 1234:5678]
などという処理が必要になります。1234:5678には4バイトのメモリが 確保され、また※2ではそのアドレスを指すためのコード、つまり といったメモリ(とコード)消費量になります。また、 call 1234:5678 といったように、アドレスを直接指定して呼び出すときと、 call [1234:5678] といったように、指定したアドレスに入っている値を アドレスにして呼び出すときではcallの命令長が変る(はず)ですので、 ここでもロスが発生します。

そこで考えられたのが以下のような方法です

※1
         mov [word label1], ベクタのオフセットアドレス
         mov [word label1 + 2], ベクタのセグメントアドレス

※2
         db 9ah
label1:  dd 0h
call 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      ?
                                end     
func3はとりあえず関係ないので放置です(call farが本当に9ahが 知りたかっただけ)。これを見てみると、コード直接書換えのほう(func1)は 一方、メモリを確保してそちらに書いとく場合(func2)は ...1バイトしか減らないのか。知らなかった。てっきり4バイトくらいは 節約されてると思ってたのに。そうかvector2がnearだから2バイトで 指せるということをすっかり忘れていた(←…)(長々と書いてこれか)

.startup/.exit疑似命令は過去に使った記憶がないなあ。6で新設されたのかな (ごまかし)

...でもCOM(Component Object Modelでないほう)を作ると仮定すれば ASSUMEでCS=DS=SSが確定するのでセグメントオーバーライドプリフィックス 必要なくなるような気が。.model tinyってTASMでしか使えなかった 記憶があるのですが、MASM6では使えるのかなあ...いずれにしろそこを クリアしたところで1バイト→3バイトになるだけですが。


戻る
Zinnia (zinnia@risky-safety.org)