02/11/30 | 簡易OSを作ろう編−2(FAT12/16の解析)[★★★◎] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(気まぐれでかつ自分も調べながら書いてるので、情報が正しくなかったり、続編がなかったりする可能性も有ります) ここの文章は、半分は7月に書いたものです・・・(^^;) 簡単なものとはいえOSを作るのであれば、ファイルシステムの実装はまず必須ではないかと思います。 フォーマットも何にもしてないFDDやHDDは、例えればせいぜい横線の入っているだけのただのノートといえます。 このままでは「何ページ目の何行目」とデータを数値で指定しなければ行けないため、プログラムからは扱いやすいですが、人間が操作するには非常に扱いづらいです。 実際にはOSはノートの一部を目次として使用し、データの名前と、その位置を表すテーブルを持たせることで外部からはデータの名前(=ファイル名)だけで操作することが出来ます。 FATはFile Allocation Table(=ファイル割り当てテーブル)の略な訳で意味的にもそれっぽい感じです。 で。 前回に書いたとおりWindows上ではフロッピーはFAT12と言う形式でファイルを格納しています。 この方式は各クラスタが12bit値を持つことになり、クラスタ数が2^12=4096個弱まで持てることになります。 (後で書きますが、FATテーブルはクラスタのチェイン構造を表すのでクラスタのデータ量12bit=対応できるクラスタ数になります) フロッピーは1クラスタ=1セクタで扱うため、全2880クラスタが存在することになります。そのため、このテーブルには2880cluster*1.5(byte/cluster)=4320byteが必要になります。 これは9セクタ分必要になります。 googleなどで検索すると具体的なフォーマットがある程度わかります。(FAT16やFAT32はありますがFAT12と銘うってるとこはないですね・・・) 実際の配置は以下の様になっているようです。
まずはブートセクタ。 ブートセクタはその名の通り、起動に使用されるセクタです。 BIOSの設定などでFDDからの起動を行えるようにすると、BIOSはFDのブートセクタだけをメモリに読みこみ、その上にあるプログラムを実行します。 このプログラムはIPL(Initial Program Loader)と呼ばれ、小さなプログラムです。 Windows/DOSの場合はこの内部でIO.SYSを探し、実行を行っています。 このIPLの解析結果は次回の戯れ言に載せようかと思います。 Intelの命令セットマニュアルと前回リンクを貼ったファンクションコールのリストがあれば結構簡単に解析できます。 ただ、このブートセクタ上の最初数十バイトにファイル構造を示すデータがあります。 これはFAT12及びFAT16では共通です。(FAT32だと一部メンバが追加されている) HDDの場合は、各パーティションに以下のようなデータが入ります。 HDD全体のMBR(マスターブートレコード。一番最初のセクタ。)では、起動するパーティションを選択し、そのパーティションの最初のセクタを読みこんで実行します。 VB及びCの構造体表記は以下の通りになります。(構造体及びメンバ変数の名前は勝手に名前をつけてます。他のサイトでもバラバラなので) 各値はリトルエンディアン(Pentiumのエンディアンと同じ)で格納されています。
で、各値の解説です。 (A) ジャンプコード(3byte) この部分はプログラムとして実行されます。 このままプログラムを進めると、どんな値が入ってるかわからないこの構造体を命令として読みこんでしまうため、この構造体の後ろまでプログラムカウンタを進めるコードが入ります。 手もとの環境では「EB 3C 90」でしたが、「EB 3C」は「jmp 3Ch」、つまり0x3C=60バイト進む命令です。60バイトと言うとちょうど(構造体のサイズ62byte)-(この命令の2byte)です。 「90」は「NOP(No Operation)」で何も実行しない命令です。 よって、プログラムは構造体を飛び越えて実行して行きます。 (B) OEMラベル(8byte) 通常フォーマットしたOS等が文字列で入ります。 「MSWIN4.1」とか「MSDOS5.0」とか入るようですが、なぜか手元のは「*_-K>IHC」・・・なんだろう? (C) セクタあたりバイト数(2byte) 普通の1.44MBの2HDなFDDなら512byteです。 HDDなら色々あるのかもしれません。 (D) クラスタ当たりセクタ数(1byte) 1つのクラスタが何セクタを担当するかです。 FAT16とFAT32の切り替えの頃は、ここがだいぶ大きくなったのではないでしょうか。 FDは2880セクタしかないので12bitで十分なので、1クラスタ=1セクタです。 (E) 予約セクタ数(2byte) FATの始まるセクタ番号です。 FDではブートレコードの次がもうFATなのでここは1。 (F) FATの数(1byte) FAT12/16/32では、いざと言う時のため予備のFATも持っています。 よって普通は2。 (G) ルート最大エントリ数(2byte) FAT32では関係ありませんが、FAT12/16ではルートディレクトリ用の領域があらかじめ確保されています。 手元のFDではここは224になっていました。 実際、サイズの小さいファイルでも225個以上をルートディレクトリに配置することはできません。 FAT32では可変長です。 (H) 全セクタ数(2byte) そのまんまですね。1.44MBのFDなら2880。 (I) メディアディスクリプタ(1byte) 2HDとか2DDの区別や、3.5インチ・5.25インチFDの区別をします。 通常の2HDの3.5インチ、1.44MBのFDなら0xF0。 (J) FATのセクタ数(2byte) 1つのFATに必要なセクタ数。 FDでは2847エントリ*1.5byte/セクタ=4264バイト必要なので、9セクタ。 (K) トラック当たりセクタ数(2byte) 1つのトラックにあるセクタ数です。1.44MBのFDなら18。 (L) ヘッド数(2byte) FDでは表裏で2です。HDDならもっと多そうです。 (M) 不可視セクタ数(4byte) 論理セクタの先頭位置の物理セクタ上の位置(わかりにく・・・)です。 パーティションを区切ったりしたら関係あると思いますが、FDでは0です。 (N) 全セクタ数(4byte) FAT12/16では特に埋めなくてもいいようです。前に同じ名前のデータあるし。 (O) 予約ドライブ番号(1byte) HDDでは関係あるようですが、FDでは関係なし。 (P) 予約1(1byte) 未使用。 (Q) ブートシグネチャ(1byte) 0x29だとボリュームラベルとか持つらしいです・・・。 (R) ボリュームID(4byte) (S) ボリュームラベル(11byte) IDはフォーマット時に自動的に決定、ラベルは自分で変えることができますね。 (T) ファイルシステム(8byte) FAT12とかFAT16とかが文字列で入ります。 と、こんな感じです。 で、次にFATテーブルです。 FATテーブルは、各ファイルの実際の内容がどのセクタに入っているかを表します。 FATテーブルの各エントリは12bitなので000-FFFまでありますが、000、001、FF8-FFFは通常のエントリとは異なる意味を持ちます。 000は未使用クラスタを意味し、FFFは最終クラスタを意味します。 000、001はともに通常と別の意味を持つため、実際のエントリは002からスタートします。 (以後、上の方に書いた手元の環境でのセクタ数を基準にします。) 上記の通り、ファイルの実際のデータの前にブートセクタ、FATテーブル×2、ルートディレクトリ領域が合計33セクタ=0x4200バイトあるので、エントリ番号は以下のように対応します。 002 - 0x004200〜0x0043FF(34セクタ目) 003 - 0x004400〜0x0045FF(35セクタ目) 004 - 0x004600〜0x0047FF(36セクタ目) ・ ・ ・ B20 - 0x167EFF〜0x167FFF(2880セクタ目) で、各エントリは次に続くエントリ番号を持つリスト構造になっています。 例えば、あるファイルがエントリ100から始まっている場合、エントリ100に101が入っていたら次のエントリは101、101に102が入ってたら102、102に飛んで105が入ってたら次は105・・・・で、最後にFFFが入っていたらそのエントリで終わり。 と言うようになっています。 ファイルを削除した場合は、このエントリは先頭からたどって行ってFFFにぶつかるまでFATテーブルを未使用マークである000で塗りつぶされます。 (削除済みファイルを復活させるソフトは連続で使用してた場合しかだめなのかも) 当然、複数のエントリが同じエントリを指したりしてしまうと不都合が生じます。 スキャンディスクとかはそこらへんをチェックしてるんだと思われます。 あとは、最後にディレクトリやファイルの情報が格納されているかです。 ディレクトリは中にあるファイルや下位ディレクトリの情報を格納しているだけで、基本的には普通のファイルと変わりありません。 ただ、OSが階層構造を持っているように見せてくれるだけです。 各ディレクトリは、そのディレクトリ内のファイルや下位ディレクトリの情報を持っています。 ルートディレクトリは上記の通りもてる数に制限がありますが、下位ディレクトリはその制限はありません。 ファイルや下位ディレクトリの情報は32バイトで格納されます。
(A) (0-7,8byte)ファイル名 ピリオドの前までです。aiueo.exeならAIUEOと大文字で格納されます。 また、削除した場合最初の1バイトは0xE5に置き換えられます。 この時にOSは「削除済みファイル」とみなして存在しないものとします。 削除したファイルを復活させるソフトはDOS時代からありますが、いずれも最初の1文字を入力する可能性がありますが、それはこのためです。 (B) (08-0A,3byte)拡張子 拡張子です。 このため上とあわせてFAT12/16では8.3形式のファイル名になります。 (C) (0B-0B,1byte)ファイル属性 Windowsでも使われる属性の値です。各値の和で属性をあらわします。 01h - 読取専用 02h - 隠しファイル 04h - システムファイル 08h - ボリュームラベル 10h - ディレクトリ 20h - ファイル 10hが入っているときはOSがディレクトリと解釈します。 (D) (0C-0C,1byte)予約領域 FAT32ではここを利用しますが、FAT12/16では関係ありません。 (E) (0D-0D,1byte)ファイル作成時刻 10ms単位でファイルの作成時刻を表します。 後に書く通り、ファイルの作成時刻は2秒単位でしか表せないので、ここで1秒を調整するんでしょう。 ファイル更新時刻にはこれがないので必ず偶数秒単位になります。 (F) (0E-0F,2byte)ファイル作成時刻 (G) (10-11,2byte)ファイル作成日付 (H) (12-13,2byte)ファイルアクセス日付 それぞれエクスプローラでファイルのプロパティで見ることのできる値です アクセスに関しては時刻のデータがないので、プロパティにも表示されません。 時刻は5,6,5bitで時・分・秒を表します。秒が1bit足りないので、実際の値を2で割ったものを格納します。そのため、時刻は2秒単位になります。 日付は7,4,5bitで年(1980年からの経過年数)・月・日で格納されます。 例えば1998/06/12だと、(年は1980年から18年後ということで、) YYYYYYY MMMM DDDDD 0100010 0110 01100 → 24CC実際はリトルエンディアンで格納されるので、18(0x12)バイト目がCC、19(0x13)バイト目が24になります。 (I) (14-15,2byte)先頭クラスタ番号の上位2バイト FAT12/16では後述の下位2バイトで足りるのでここは必要ありません。 FAT32で必要。 (J) (16-17,2byte)ファイル更新時刻 (K) (18-19,2byte)ファイル更新日付 上記の通りに日付・時刻を格納。 時刻は2秒単位でしか保持できません。 (L) (1A-1B,2byte)先頭クラスタ番号の下位2バイト ファイルの先頭のクラスタ番号です。 この番号から前述の通りチェイン構造で各セクタにデータが格納されて行きます。 (M) (1C-1F,4byte)ファイルサイズ ファイルサイズですね。 これで大体構造がわかったかと思います。 これを見ると、ディレクトリが深くなるとその分各セクタをたどって行く必要が出てくるのでアクセスは遅くなります。 次回は、上にある通りブートセクタの内容を逆アセンブル・解析したものを載せる予定です。 |
02/06/25 | 簡易OSを作ろう編−1(FDDの論理セクタ読み書き)[★★★★◎] | |||||||||
(気まぐれでかつ自分も調べながら書いてるので、情報が正しくなかったり、続編がなかったりする可能性も有ります) 一応自分も情報関係の学科にいるわけで、インターネットやらゲームやら流行り(?)のプログラムを組むのもいいですが、根本的にどのようにコンピュータが動作するかと言うのを知っておくのも必要ではないかと言う事で、フロッピー上で動く非常に簡単なOS作りにチャレンジしてみようかと(気まぐれで)思い立ちました。 本当はCPUの回路レベル設計なども一通り経験しとけばいいんですが、昨年の学校のCPU作成実験ではコンパイラ作成係になってしまったので、命令セット作成には関わってもパイプラインやバスの処理には関わってないんですよね。 しかもCPUの設計はハードにしろソフトにしろお金がかかりそうなんであんまりやりたくないです。 (命令セット以上のレベルであればシミュレータとか作ってもいいですが) 似たような情報はネット上を探せばより細かい情報が見つかるので、資料として見るには向かないです(^^;) あくまで暇つぶしの読み物と言う事で。 というわけで、現状のままFD1枚でできるOS作成にチャレンジというわけです。 現在WindowsではHDDはFAT16、FAT32、NTFSあたりのファイルシステムを利用していますが、フロッピーではFAT12と言うものを利用しています。 FAT12はその名の通りクラスタ数が2^12=4096個(正確にはこれより少し少ない)まで持てる形式で、1.44MBのフロッピーではディスク両面*トラック数80*セクタ数18=2880個のセクタがあるため、FDDでは1クラスタ=1セクタの扱いができます。 ちなみに1セクタは通常512バイトです。(2880*512=1474460=1.44M) FAT16では2^16=65536個弱のクラスタが持てますが、1クラスタ=1セクタだと65536*512=32Mまでのディスクしか持てないため、1クラスタ=64セクタ(32KB)などどすること2GBまでサポートしてましたが、これには非常に無駄が多いです。 ここら辺の話はFAT16→FAT32に関するサイトを見れば、プログラム組まない人向けにもわかるように書いてあるサイトが数多くあるのでここらへんにしておきます。 で。 Windowsでプログラム上からファイルアクセスをしようとすると、APIならCreateFile、Cランタイムならopenやfopen、VBならOpenステートメントあたりを使うわけです。 しかしこれらはすべてOSのファイルシステムを経由するため、FAT構造にのっとったファイルアクセス(Win98の環境でやってるので)しか行えません。 最終的にWindowsに依存しないOSを作成するのであればファイルシステムもFATとは別に自分で作成しなければ行けないことになります。 とはいえこれからOSを作るのにそれ用のコンパイラもリンカもないわけで、Windows上で開発を行います。 すると、Windows上で作成したファイルをFAT構造でないFDに読み書きする手段が必要となります。 そこで、まずアセンブラでBIOSにソフトウェア割りこみをかけ、ファンクションコールをすることを考えます。 MS-DOSではint 25h/26h、BIOSではint 13hでFDの読み書きを行うことができます。 両者の違いは、int 25h/26hでは面やトラックを気にせず2880個のセクタを一意に指定しますが、int 13hではトラック番号・セクタ番号を個別に指定します。 最終的に作成するOSはMS-DOSの命令は使えないのでint 13hを使うことになりますが、とりあえず今回はセクタ番号のみでアクセスした方がDOSの形式に合わせやすいのでint 25h/26hを使います。 なお、x86でのDOS/BIOSに関する割りこみ・ファンクションコールのリストはRalf Brown's Fileに有ります。 それによると、int 25h/26hを呼び出す時は各レジスタは以下のように設定します。
セクタ数はCXで指定しますが、これだと16bitまでしか指定できないため、32MBまでのディスクしか扱えません。 そのため、CX=0xFFFFの時は32MB以上の領域を読み書きするために別の方法でより大きいセクタ番号を指定することができますが、ここではFDの読み書きだけを扱うため、その場合には触れません。 そこで、せっかく購入したVC++6で以下の様なコードをインラインアセンブラで書いてみました。 __asm{ mov al, 0 ; ドライブはa: mov bx, buf ; データを保存するバッファ mov cx, 1 ; 読み込むセクタ数 mov dx, 0 ; 読み出すセクタ番号 int 25h ; バッファにデータを読み出す }で、実行したところ・・・ 見事にブルーバック(T-T) 他のサイトでは同じコードでうまく行っていたにもかかわらず、多少パラメータをいじってもだめだったので、思ったのは「Win32用コードではDOSやBIOSのファンクションコールは行えないのでは?」ということで、Win16用コードをはくリンカを探すことにします。 幸いMicroSoftのアセンブラMASM 6.15と、リンカがともにフリーで入手できるようなので、これを利用してみることにしました。 MASM 6.15の入手先→VC++6 Processor Pack リンカの入手先(直に自己解凍形式EXEにリンク)→16ビット用リンカ まぁ16ビット用のリンカを使うとなんとか成功したわけですが・・・ (16bitコードは直にDOS/BIOS呼んでもいいんですかね・・・) 16bit用コードを吐くCコンパイラを持ってないため、このままだと全コードアセンブラで書かないと行けないことになると。 作成するOSのコードはともかく、Win側はある程度プログラムを書きやすくしたいので、C++なりVBなりでWin32から同じ処理を行う方法を探してみます。 (以下Win95/98で利用可能。WinNT/2000では利用できません) MSDNなりインターネットで情報を探すとでてきたのは、vwin32なるデバイスドライバがディスク操作関連のBIOS呼び出しを代わりに行ってくれることが判明。 しかも、これはAPIだけで行えるので、(最初VC++で作りましたが)VBで読み書き部を作成してみる。 ちなみに手順は以下の通り。 CreateFileでファイル名に"\\.\vwin32"を入力することでデバイスハンドルを取得 ↓ 各レジスタの代わりに構造体の対応するメンバを埋める ↓ 引数に VWIN32_DIOC_DOS_INT25 = 2 VWIN32_DIOC_DOS_INT26 = 3 VWIN32_DIOC_DOS_INT13 = 4 のいずれかを入力してDeviceIoControl関数を呼び出す。 なお、実際には以下のコードになります。
上記のコードをセクタ0-2879の全セクタに行うと、無事FDの論理セクタの操作ができることに成ります。 これで得られる情報はファイルシステムとは関係ないので、フォーマットしてないFDからも読みこめるし、ファイルシステムそのものの情報も含んでいるので、フォーマット済みFDから取り出した全セクタのデータを未フォーマットのFDに書きこむとフォーマット済みFDと同じ物ができあがります。 Linuxを使うためRedHatなどインストールする時は、RedHatのCDからdosutil\rawrite.exeなるプログラムでRedHatインストール用のブート用フロッピーを作成することになると思います。 なんでWindowsなのにRedHat用のフォーマット・ファイル書きこみができるんだろうと不思議だったんですが、あれは単に用意された全セクタのデータを論理的に書きこんでいただけのようですね。 実際boot.imgなどのイメージはすべて2880*512バイトですから。 次回はファイルシステムの参考と言う事でFDで使われるFAT12の解析か、コンピュータ・OSの起動に関係するIPL(Initial Program Loader)の解析をしてみる予定です。 ・・・ってこの文章アップロードする前にFAT及びIPLの解析が済んでしまった・・・。 |
02/03/29 | 実験終了編[★★★★◎] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
今回はVBでもCでもないです。 今学期はなかなか重い実験があったんですが、ようやく終了。 数人のグループでCPUを製作、そのCPU用にコンパイラを作成し、レイトレーシングを動かしてその実行速度を競うというもの。 んで、自分はコンパイラ係ということになりました。 コンパイラ作成は授業の形で説明なども行われ、そのとおりに作成すればとりあえず動作最低限のものが動くと言うもの。 で、言語の概要はこんな感じ。 Ocamlなる関数型言語のサブセット。 基本的に変数はすべてレジスタを利用(メモリを使用しない) まぁこれぐらいなんですが。 関数型言語と言うのはLinuxでEmacs等使ってると触れることも有ると思いますが、Lisp等がそれに当たります。 VBやC++、Pascal、Java等の「コンピューターに行わせたい手続きを書いていく」タイプの言語は手続き型言語と言いますが、関数型言語はその名前の通り関数適用を基本にしたプログラムです。 Ocamlと言うのはMLという言語をオブジェクト思考向けにしたものだそうです。 MLはどちらかと言うと実用向けと言うよりは学術用の言語ですが、慣れるとかなりプログラムが速く書けるようです。 実際なんかのプログラムコンテストではML等の関数型言語で作られたプログラムがプログラム作成速度が上位を占めたとか。 ちなみにMLで書かれたコードはこんな感じになります。 (* 階乗を求める関数fact *) let rec fact n = if n <= 0 then 1 else n * fact (n-1) 再帰関数を扱ったプログラムをしたことがあれば関数型言語未経験者でも意味は簡単にわかるでしょう。 あとは「fact 5」なら120のように関数を適用すればいいわけです。 まぁこの程度のプログラムならforループなどのある手続き型言語の方がラクですが。 Lispと違い括弧だらけにならないのはよろしいかと。 で。 コンパイラを作成するとなるとまず必要なのが字句解析と構文解析。 ちなみに昨年まではScheme(Lispの派生言語)用のコンパイラで授業を行っていたらしいです。 Schemeはevalが実装されているので、プログラムをquoteしたものをevalすればいいのでここらへんは処理系任せにできるんですが・・・ まぁC言語には字句解析にはLexやFlex、構文解析にはYaccやBisonと言う汎用プログラムがあるように、Ocamlには幸いOcamlyaccやOcamllexなるものが付属しているのでそれで作成。 しかしこう言うの使うとスクリプトみたいのすぐ作れそうだね。 ほんとにVBYaccとかVBLexが欲しくなってきた。 ちなみに、自分のグループのアーキテクチャー及び命令セットはこんな感じ。
上記の命令を見るとわかるのが、スタック関連の命令がない、除算がない、浮動小数点関連の命令がないと不足してる部分が盛りだくさんです。 ただ、実際Xilinxと言うCADで書いた回路の通り動いてくれるCPUを利用するんですが、かなり古い物を利用するため、そんな回路を詰める容量がないんですね。 一方、コンパイラのサポートする文法は、
んで。 コンパイラは字句解析・構文解析後以下のような処理を行います。
とここまで型に関する処理を行い、以下はプログラムを簡潔にする処理が続く。
んで、ここから最適化。(数回繰り返して行う)
で、いよいよアセンブラ生成に向けて。
と一連の処理を行ってアセンブラが生成されます。 ここまで書いて疲れたのでとりあえずこの辺で。 |
02/01/15 | ホイール+フック+アドイン編[★★★★◎◎] | |
約1年ぶりのプログラムネタで。 いや、「動的配列の構造解析」あたりをここで書いてもよかったんですがあんまり奮闘もせず終わってしまったので今回の更新と同時にVB講座で公開ということで。 今回の戯れ言は今だVB5 ProかEntを利用している人向けです。 タイトルからして大体中身は想像つくでしょう。 VB5のIDE(開発環境の事です)のコードペインではマウスホイールが使えないんですよね。 ちなみに世代的には同じはずのVC++5の開発環境では使えます。 VB6はどうなんだろ。 最近ブラウザでもちょこちょこ文章をスクロールさせるのにホイールを使うことが多く、(まぁうちのマウスは安物のせいか、ホイールはボタン機能がついてないんで使わないんですが。なぜか横についているが使いにく過ぎ。スクロールも速すぎるのでまったく使わないです。)やっぱりこれをプログラム書くときにも使いたいんですよね。 んで。 いつでもホイールを使えるようなオンラインソフトを使用すると言う手もあるんですが、とりあえずマシン全体に負荷(ってほどでもないが)をかけるのもなんなんで、自力作成に挑戦。 で、今の自分に無くて新たに必要な技術を考える。 まずは当たり前だがホイールを回したと言う情報を取得すること。 VB IDE内でホイールが回ったと言う情報をフックすること。 ホイールが回ったらコードペインの現在の行を変えるアドインを作ること。 この3つで、今回のタイトルになるわけです。 んで、まずはホイールの処理。 調べたところ、やはりホイールの処理なんてのはちょっとしたエディタ系アプリでも作れば欲しいものなので簡単に資料が集まる。 とりあえず、API32になったばかりのWin95及びNT3.5時代はホイール付きマウス自体OSサポートが無かったのでおいといて(なんかRegisterWindowMassageを使えばできないことも無い)、OSでサポートされたWin98及びNT4.0で考える。 どうやらWM_MOUSEWHEEL=&H20Aなるメッセージが来るようですね。 当然Win98より前のVB5のAPIビューアーではこの値が出てきませんが。 そこで、(VB講座の部屋にもある)以前作ったウインドウのサブクラス化によるファイルドロップ受け付けルーチンがあるので、ファイルドロップ部をWM_MOUSEWHEELを受け付けるように書き帰ることで成功。 wParamの上位16bitを見ることでどっちがわに回転するかがわかるので、これで適当にスクロールバーでもいじるコードを書いてサクっと完成。ここはすんなり。 次にとりあえずアドインの枠組み。 ただ、これは「新しいプロジェクト」から「アドイン」選べば作成できるので簡単。 今回は専用ウインドウ表示などは必要無いのでフォーム表示部を削除、プロジェクト名やAddToIni関数周りの文字列を書き換えてとりあえず完成。 この程度のものだと簡単ですね。 で、ようやく今回のメイン(?)とも言えるホイールメッセージのフックです。 フックさえできれば、 VBIDE.VBE.ActiveCodePane.TopLine = VBIDE.VBE.ActiveCodePane.TopLine - Sgn(m.wParam \ &H10000) * MoveLine とでもすればMoveLine行分動くはずなので。 まぁm.wParamは別に&H10000で割らなくてもいいんですけどね。(現時点ではWM_MOUSEWHEELは120か-120しか値を取らず、無段階で回転量を受け取ることが出来ないため) んで、フック。 基本的にはメッセージ群に対応したコールバック関数をSetWindowsHookEx APIでインストール(インストールと聞いてなんかヤなイメージを受けたけど実際はフックチェインなる関数リストに追加されるだけっぽい。)するだけなんですが・・・ フックはシステム全体を見るもの(グローバルフック)と自分のスレッドだけいじるもの(ローカルフック)とあるんですが、今回はVB IDEだけ見れればいいのでローカルフックで。 ちなみにグローバルフックを行うにはActiveX DLLでなく普通のDLLを作成して関数アドレスを取得したものをSetWindowsHookExに送る必要があるため、非常にめんどくさい。 特に普通のDLLを作ると言う時点でVBではほぼ不可能(方法が無いわけではない)だし、Cで別に書くのも非常に手間がかかる。 とりあえずマウスメッセージを受け取るためフックの種類をWH_MOUSEとして適当に書いたものを動かす。 何も起きん。 まぁ肝心のコールバック関数部で何もしてないので当然だが。 んで、コールバックではlParamがMOUSEHOOKSTRUCTなんでRtlMoveMemoryでlParamからMOUSEHOOKSTRUCT読みこみ、状況(メッセージとかwParamとか)をファイルに書きこむルーチンを作る。 んで、コンパイル、実行!(と言ってもアドインに追加するだけなんだけど) ・・・。 VBが落ちた。・・・IMEが落ちた。explorerが落ちた。 一気に大量の一般保護エラーで落ちまくり、再起動(T-T) 一体原因は何だとVBを起動したら、アドインの登録は消えていないため、またエラー連発、再起動(T-T) レジストリの情報を書き変えるのも面倒だし、生成したwheel.exeを削除してコードを見る。 何度か試した結果(もちろん何度も再起動)、どうもRtlMoveMemoryでlParamからMOUSEHOOKSTRUCTを読みこもうとしたとき落ちてることが発覚。 書き込みならともかく、なんで読みこみで落ちるのかなぁと疑問に思い、Win32Helpを何度も眺める。 どうもSetWindowsHookEx第4引数のdwThreadIDは0にするとすべてのスレッドを指定したことになると書いてあり、これを「自分のプロセス内のすべてのスレッド」だと思っていたらどうも「(単に)すべてのスレッド」らしく、他のプロセスのメモリにアクセスして落ちたらしい。 (第3引数のインスタンスで自分のプロセスのインスタンスを送ったのでローカルフック扱いされると思ってたんですがどうもそうでは無いようで。) んで、そこをApp.ThreadIDに修正。 んで、実行。 おお、エラー無く動く。 ただ、コールバック関数に入ってきた値がいくつかしかないんだよね。 しかもホイールをぐりぐり動かしたのにWM_MOUSEWHEEL = 0x20Aは1つも入ってない。 まあWM_MOUSEWHEELはWin98から増えたメッセージだししょうがないかなと言う気もする。 ちなみにWin2KやXPではWM_MOUSEWHEELも取れるし、このとき使ったMouseProcより高性能(?)なLowLevelMouseProcが使用できるらしい。(正確にはそのようなコールバック関数に対応したフックができると言うことだが) じゃあ最初はウインドウメッセージの横取りでホイールが実装できたわけだし、WH_MOUSEでなくウインドウメッセージを横取りするWH_CALLWNDPROCにしてみる。 おお、実行するとどうも大量のメッセージがファイルに記録されている。 でもやっぱりWM_MOUSEWHEELは無い。 しかし、WH_CALLWNDPROCより少なめとはいえ、かなりのメッセージが流れるにも関わらず、やはりWM_MOUSEWHEELは見つからず。 んで、次に考えたのはそもそもアドインがVB IDE本体とは別スレッドで実行しているのではないかと言うこと。 SetWindowsHookExの第4引数のdwThreadIDはコールバック関数のあるスレッド(要はApp.ThreadID)を指定すればいいのかと思っていたが、SDKからみるとどうも監視先スレッドを調べれば良い様にも思える。 んで、VC++5付属のSpy++をつかってそもそもアドインのプロセス・スレッドとVB IDEのプロセス・スレッドの親子関係を調べてみると・・・ 別のプロセスだったし(T-T) しかもVBのプロセスはVBのプロセスでスレッドが3つあったし・・・(ただ、全てのウインドウはその中の1つのスレッドにくっついてたのでそれがメインに見えたが) これだとプロセス一覧からVB IDEらしきものを調べ、さらにそのスレッド一覧から・・・ うーん、面倒だ。 と、ふと思ったのは、以前OCXコンポーネント作成機能をいじってたときに、ActiveX EXEはアウトプロセスコンポーネントでActiveX DLLはインプロセスコンポーネントであるということが書いてあったのを思い出す。 確かに現在の設定ではActiveX EXEになっていたので、ActiveX DLLにしてみる。 んで、起動時にApp.ThreadIDの値を見たら見事Spy++で見たVB5のメインらしきスレッドのIDと一致する。 まぁプロセスだけでもVB IDEと一致すれば上出来だろうと思ってたので嬉しかったり。 んで、再びWH_MOUSE、WH_CALLWNDPROCとやっていったがまだ出てこない。 そこでgoogleとかで調べたところ、WM_MOUSEよりGetMessageの時点でフックするWH_GETMESSAGEでいいらしい。 んでWH_GETMESSAGEで行うと・・・おお、WM_MOUSEWHEEL=&H20Aがチャンとフック関数を通過している。 ようやくフックがうまくいったので、先ほどのCodePane.TopLineをいじるコードを実装。 おお、動くぞ(T-T) ただ。 動かす行数はインターフェイス作成するの面倒だからレジストリに直書きしてもらってGetSettingで読んでるんだけど、なぜか2倍動く・・・ MoveLineが3なのに6行。 レジストリからうまく読めてないのか・・・? そこで VBIDE.VBE.ActiveCodePane.TopLine = VBIDE.VBE.ActiveCodePane.TopLine - Sgn(m.wParam \ &H10000) * MoveLine の最後に「\ 2」を付けてみるこれだとMoveLine=3で原因が他なら\2して1になるわけで2行動くはず。 MoveLineそのものがMoveLine=6になってしまってるんなら3行になるはずだし。 で、2行しか動かなかった。 当然残るは「その部分が2回実行されている」と言うことなんで、さらにSDKのGetMsgProcの部分を読んでみる。 どうもGetMassageでメッセージキューから削除される前と削除されるときと2回呼ばれているみたいなので、削除される前、つまりwParam = PM_NOMOREMOVEの時は行が移らないようにして・・・ 完成(^^) 無謀な挑戦も久々に無事成功でした。 (ってこれやった3日間で何十回再起動したか・・・特にグローバルフックしちゃってた時) 以下最終的に書いたコールバック関数。
今後のネタとしては・・・数式解析とかもやったしVBでNFA(非決定性有限オートマトン)使った自前正規表現でもやってみようかなと。 ただこれエスケープ文字・文字クラス展開まで2日で一気にやったんだけど、NFA生成及びNFA受理の処理が面倒で書く気にならない・・・むむむ。 |