気まぐれな戯れ言 バックナンバー5


気まぐれな戯れ言バックナンバーです。全体の一覧はこちら

02/11/30 簡易OSを作ろう編−2
02/06/25 簡易OSを作ろう編−1
02/03/29 実験終了編
02/01/15 ホイール+フック+アドイン編

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と銘うってるとこはないですね・・・)
実際の配置は以下の様になっているようです。

開始セクタ番号−終了セクタ番号セクタ数中身
0-01ブートセクタ・FAT12の情報
1-99FAT12テーブル
10-189FAT12テーブル(予備)
19-3214ルートディレクトリテーブル
33-28792847ファイルデータ


まずはブートセクタ。
ブートセクタはその名の通り、起動に使用されるセクタです。
BIOSの設定などでFDDからの起動を行えるようにすると、BIOSはFDのブートセクタだけをメモリに読みこみ、その上にあるプログラムを実行します。
このプログラムはIPL(Initial Program Loader)と呼ばれ、小さなプログラムです。
Windows/DOSの場合はこの内部でIO.SYSを探し、実行を行っています。
このIPLの解析結果は次回の戯れ言に載せようかと思います。
Intelの命令セットマニュアルと前回リンクを貼ったファンクションコールのリストがあれば結構簡単に解析できます。

ただ、このブートセクタ上の最初数十バイトにファイル構造を示すデータがあります。
これはFAT12及びFAT16では共通です。(FAT32だと一部メンバが追加されている)

HDDの場合は、各パーティションに以下のようなデータが入ります。
HDD全体のMBR(マスターブートレコード。一番最初のセクタ。)では、起動するパーティションを選択し、そのパーティションの最初のセクタを読みこんで実行します。

VB及びCの構造体表記は以下の通りになります。(構造体及びメンバ変数の名前は勝手に名前をつけてます。他のサイトでもバラバラなので)
各値はリトルエンディアン(Pentiumのエンディアンと同じ)で格納されています。

意味 バイト数VB表記 C表記
Type BPB_FAT16struct BPB_FAT16 {
(A)ジャンプコード3     Jumpcode(2) As Byte    CHAR Jumpcode[3]
(B)OEMラベル8     OEMLabel(7) As Byte    CHAR OEMLabel[7]
(C)セクタあたりバイト数2     BytesPerSector As Integer    WORD BytesPerSector
(D)クラスタ当たりセクタ数1     SectorsPerCluster As Byte    CHAR SectorsPerCluster
(E)予約セクタ数2     ReservedSectors As Integer    WORD ReservedSectors
(F)FATの数1     NumberFATs As Byte    CHAR NumberFATs
(G)ルート最大エントリ数2     MaxRootEntries As Integer    WORD MaxRootEntries
(H)全セクタ数2     NumberSectors As Integer    WORD NumberSectors
(I)メディアディスクリプタ1     MediaDescriptor As Byte    CHAR MediaDescriptor
(J)FATのセクタ数2     SectorsPerFAT As Integer    WORD SectorsPerFAT
(K)トラック当たりセクタ数2     SectorsPerTrack As Integer    WORD SectorsPerTrack
(L)ヘッド数2     NumberHeads As Integer    WORD NumberHeads
(M)不可視セクタ数4     HiddenSectors As Long    DWORD HiddenSectors
(N)全セクタ数4     LargeSectors As Long    DWORD LargeSectors
(O)予約ドライブ番号1     DriveNumber As Byte    CHAR DriveNumber
(P)予約11     Reserved1 As Byte    CHAR Reserved1
(Q)ブートシグネチャ1     BootSignature As Byte    CHAR BootSignature
(R)ボリュームID4     VolumeID(3) As Byte    CHAR VolumeID[4]
(S)ボリュームラベル11     VolumeLabel(10) As Byte    CHAR VolumeLabel[11]
(T)ファイルシステム8     FileSystemLabel(7) As Byte    CHAR FileSystemLabel[8]
End Type}


0123456789ABCDEF
(A)(B)(C)(D)(E)
(F)(G)(H)(I)(J)(K)(L)(M)
(N)(O)(P)(Q)(R)(S)
(S)(T)

で、各値の解説です。
(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バイトで格納されます。

0123456789ABCDEF
(A)(B)(C)(D)(E)(F)
(G)(H)(I)(J)(K)(L)(M)

(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を呼び出す時は各レジスタは以下のように設定します。
AL ドライブ番号(A:0 , B:1 ...)
BX 読み書き先バッファアドレス
CX 読み書きするセクタ数(0xFFFFは不可)
DX 読み書き開始セクタ番号

セクタ数は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関数を呼び出す。

なお、実際には以下のコードになります。


Public Declare Function CreateFile Lib "kernel32" Alias "CreateFileA" (ByVal lpFileName As String, ByVal dwDesiredAccess As Long, ByVal dwShareMode As Long, ByVal lpSecurityAttributes As Long, ByVal dwCreationDisposition As Long, ByVal dwFlagsAndAttributes As Long, ByVal hTemplateFile As Long) As Long
Public Declare Function CloseHandle Lib "kernel32" (ByVal hObject As Long) As Long
Public Declare Function DeviceIoControl Lib "kernel32" (ByVal hDevice As Long, ByVal dwIoControlCode As Long, lpInBuffer As Any, ByVal nInBufferSize As Long, lpOutBuffer As Any, ByVal nOutBufferSize As Long, lpBytesReturned As Long, ByVal lpOverlapped As Long) As Long
Public Declare Sub RtlMoveMemory Lib "kernel32" (Destination As Any, Source As Any, ByVal Length As Long)

Public Const VWIN32_STRING = "\\.\vwin32"
Public Const FILE_FLAG_DELETE_ON_CLOSE = &H4000000
Public Const VWIN32_DIOC_DOS_INT25 = 2
Public Const VWIN32_DIOC_DOS_INT26 = 3
Public Const VWIN32_DIOC_DOS_DRIVEINFO = 6
Public Const CARRY_FLAG = 1

Public Type DIOC_REGISTERS
 reg_EBX As Long
 reg_EDX As Long
 reg_ECX As Long
 reg_EAX As Long
 reg_EDI As Long
 reg_ESI As Long
 reg_Flags As Long
End Type


Public Const SURFACES = 2 '両面
Public Const TRACKS = 80 'トラック
Public Const SECTORS = 18 'セクタ
Public Const SECTORSIZE = 512 'セクタあたりのサイズ



'----------------------------------------------------------------------
'ReadLogicalFDSectors (hDev, bDrive, dwStartSector, wSectors, lpSectBuff)
'
' 論理FDドライブからセクタを読み取ります。Int 25h を使用します。
'パラメータ:
' hDev - VWIN32 を示すハンドル
' bDrive - MS-DOSの論理ドライブ番号です。 0 = A, 1 = B.
' dwStartSector - 最初に読み取りを行う論理セクタ
' wSectors - 読み取るセクタの数
' lpSectBuff - セクタ データを格納するためのバッファです
'
'戻り値:
' 成功すると 1 を返します。それ以外は 0 を返します。
'------------------------------------------------------------------*/
Public Function ReadLogicalFDSectors(ByVal hDev As Long, ByVal bDrive As Byte, _
ByVal dwStartSector As Long, ByVal wSectors As Integer, ByRef lpSectBuff() As Byte) As Long

Dim fResult&, cb&
Dim reg As DIOC_REGISTERS

'領域チェック
If dwStartSector < 0 Then Exit Function
If dwStartSector + wSectors - 1 >= SURFACES * TRACKS * SECTORS Then Exit Function

If UBound(lpSectBuff) - LBound(lpSectBuff) + 1 < wSectors * SECTORSIZE Then Exit Function


'レジスタセット
With reg
 reg.reg_EAX = bDrive
 reg.reg_EBX = VarPtr(lpSectBuff(LBound(lpSectBuff)))
 reg.reg_ECX = wSectors
 reg.reg_EDX = dwStartSector
End With

fResult = DeviceIoControl(hDev, VWIN32_DIOC_DOS_INT25, reg, Len(reg), reg, Len(reg), cb, 0)

fResult = fResult And (Not (reg.reg_Flags And CARRY_FLAG))
ReadLogicalFDSectors = fResult

End Function



'----------------------------------------------------------------------
'WriteLogicalFDSectors (hDev, bDrive, dwStartSector, wSectors, lpSectBuff)
'
' 論理FDドライブのセクタへ書きこみます。Int 26h を使用します。
'パラメータ:
' hDev - VWIN32 を示すハンドル
' bDrive - MS-DOSの論理ドライブ番号です。 0 = A, 1 = B.
' dwStartSector - 最初に読み取りを行う論理セクタ
' wSectors - 読み取るセクタの数
' lpSectBuff - セクタ データを格納するためのバッファです
'
'戻り値:
' 成功すると 1 を返します。それ以外は 0 を返します。
'------------------------------------------------------------------*/
Public Function WriteLogicalFDSectors(ByVal hDev As Long, ByVal bDrive As Byte, _
ByVal dwStartSector As Long, ByVal wSectors As Integer, ByRef lpSectBuff() As Byte) As Long
Dim fResult&, cb&
Dim reg As DIOC_REGISTERS

'領域チェック
If dwStartSector < 0 Then Exit Function
If dwStartSector + wSectors - 1 >= SURFACES * TRACKS * SECTORS Then Exit Function

If UBound(lpSectBuff) - LBound(lpSectBuff) + 1 < wSectors * SECTORSIZE Then Exit Function

'レジスタセット
With reg
 reg.reg_EAX = bDrive
 reg.reg_EBX = VarPtr(lpSectBuff(LBound(lpSectBuff)))
 reg.reg_ECX = wSectors
 reg.reg_EDX = dwStartSector
End With

fResult = DeviceIoControl(hDev, VWIN32_DIOC_DOS_INT26, reg, Len(reg), reg, Len(reg), cb, 0)

fResult = fResult And (Not (reg.reg_Flags And CARRY_FLAG))
WriteLogicalFDSectors = fResult

End Function


上記のコードをセクタ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が欲しくなってきた。

ちなみに、自分のグループのアーキテクチャー及び命令セットはこんな感じ。

CPU16bit
動作周波数6MHz
パイプライン3段
レジスタ16bit*16
命令長16bit
オペランド数2(一部3)
CPI1(メモリアクセス系命令は2)
遅延スロット1
RAMプログラム用メモリ64KB、データ用メモリ64KB(実際は計256KBまで使用可だが、これで足りたので)
フラグNegative , Zero , Equal , Carryの4つ
整数はビッグエンディアン形式。
浮動小数点はポインタを持ちまわすことで利用。メモリ内はIEEE準拠の32bit形式で保持。
乗算は外部ROM(128KB)に8bit*8bitの16bit乗算テーブルを置く。
メモリの0番地とのLoad,Storeで端末とのデータのやり取りが可能。(Memory-Mapped I/O)

命令一覧
レジスタ代入mov
即値代入movi,sethi
整数演算addi,subi,add,sub,addc,subc,mul
ビット演算xor,and,or,not
シフトshr1,shr8,shl8,swp,shrc
比較cmp
分岐b,be,bne,bl,ble,bg,bge,bc
メモリ読み書きld,st
その他nop

上記の命令を見るとわかるのが、スタック関連の命令がない、除算がない、浮動小数点関連の命令がないと不足してる部分が盛りだくさんです。
ただ、実際Xilinxと言うCADで書いた回路の通り動いてくれるCPUを利用するんですが、かなり古い物を利用するため、そんな回路を詰める容量がないんですね。

一方、コンパイラのサポートする文法は、
整数演算+ - *
小数演算+. -. *. /.(平方根などは外部関数として定義)
変数定義let a = .... in .....
関数定義let rec f a b c ... = .... and g x y z .... = ..... in ......
関数適用f a b c ....
条件分岐if b then .... else ....
配列a.(x) a[x]
配列代入a.(x) <- y
タプル(a,b,c...)
複文.... ; ....
整数比較== != <= >=
その他サポート
静的配列mem l d
型推論(テキトーな多相型サポート)
グローバル変数をメモリに格納
汎用レジスタはr0 - r12の13個。r13はスタックポインタ、r14はヒープポインタ、r15はその他一時用

んで。
コンパイラは字句解析・構文解析後以下のような処理を行います。

型推論(各変数の型をチェック)
グローバル変数処理(グローバル変数をメモリに変換)
多相型解決(Alpha型を入力に持つ関数名を型に応じて関数名変換)
浮動小数点ライブラリとの連携(上記の通り小数はポインタで扱うので、「a *. b」 (= fmul a b) と言う小数演算があったら 「let c = 0.0 in fmul a b c ; c」に変換。今回はレイトレが末尾再帰以外の再帰をしていないためこれでよいが、本当はよくない)
型情報の除去(最適化のジャマになるため)

とここまで型に関する処理を行い、以下はプログラムを簡潔にする処理が続く。

K正規型変換(1 * 2 + 3 * 4 と言う文は a = 1 * 2 , b = 3 * 4 , a + bとCPUの扱える単純な計算に分解)
α変換(同一の変数名を別々の名前に付けかえる)
β簡約(let a = b in N と言う文があったらN中のaをすべてbに置き換える。関数型言語では一般的に参照透明性が保たれているので問題ない)
η簡約(let a = N in a と言う文があったらNに置き換える)
letの結合に関する簡約(let a = let b = M1 in M2 in M3 と言う文があったら let b = M1 in let a = M2 in M3と平らにする。bのスコープが多少変化する(M2内だけだったのがM2,M3になる)がα変換を正しく行っていれば問題ない)

んで、ここから最適化。(数回繰り返して行う)

インライン展開(これはC等でも出てくるのでいいでしょう)
整数畳み込み(計算値のあらかじめわかるものは計算)
不要な変数の除去(使わない変数をなくす)

で、いよいよアセンブラ生成に向けて。

Closure変換(外部変数を関数呼び出しの段階で値を確保しておく)
レジスタ割り当て(2オペランドに対応する処理も含む)
アセンブリ生成(当初SPARC用アセンブリを吐いたがその後自分のグループ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日間で何十回再起動したか・・・特にグローバルフックしちゃってた時)


以下最終的に書いたコールバック関数。

Function RoleWheel(ByVal ncode As Long, ByVal wParam As Long, ByVal lParam As Long) As Long
'ホイール入力のチェック
On Error Resume Next
Dim m As MSG

If ncode < 0 Then
  RoleWheel = CallNextHookEx(Hookid, ncode, wParam, lParam)

Else
  'ホイールの場合
  Call CopyPtrtoVal(m, lParam, Len(m))
  's = s + Hex(m.message) + vbCrLf
  If m.message = WM_MOUSEWHEEL And wParam = PM_REMOVE Then
    Set CP = VBIE.ActiveCodePane
    CP.TopLine = CP.TopLine - Sgn(m.wParam \ &H10000) * MoveLine
  End If
  
  RoleWheel = CallNextHookEx(Hookid, ncode, wParam, lParam)
End If
End Function
あ、CopyPtrToValってのはRtlMoveMemoryをByVal,ByRefをいじってAlias指定したもの。

今後のネタとしては・・・数式解析とかもやったしVBでNFA(非決定性有限オートマトン)使った自前正規表現でもやってみようかなと。
ただこれエスケープ文字・文字クラス展開まで2日で一気にやったんだけど、NFA生成及びNFA受理の処理が面倒で書く気にならない・・・むむむ。


一覧へ戻る