ホームに戻る
 OSを作成

0、はじめに

OS作成にあたり以下の著書を参考にします。

「30日でできる!OS自作入門」川合秀美・著

本の内容はほどほどにC言語が書けて、
アセンブラは初心者という人でもなんとかなる内容です。

難しい説明を後回しにしておいて、
先に達成感が得られるような作りになっているので、
挫折しない工夫としては秀逸だと思います。

いくぶん話言葉すぎるのが苦手な人や、
後回しにする手順が気にいらない人には向かないと思います。
また詳細な説明を完全に省いてしまっている部分も多くあり、
完璧に理解しきるにはこの1冊では不充分です。
そのあたりは著者自身も本文の最後のところで認めています。
本がぶ厚いのは内容が豊富だからというよりは、
説明が丁寧すぎることによるものだと思われます。

ともあれ手順通りにやれば30日も夢では無いです。
こんなに親切でわかりやすい本は無いので、
関係者のかたにはぜひとも購入をお勧めします。

ほかに「はじめて読む486」といった本も参照しています。
「はじめて読む486」はしっかりとした文章やデータが書かれているものの、
説明の仕方が難しいので取り付きにくいのが問題点です。

以下は書籍を読んだ内容についての自分なりの解釈により、
実際にオリジナルのOSを作成してみたものです。

1、準備するもの

qemu・・・x86CPU のエミュレータ(フリー)。
nasm・・・アセンブラ(フリー)。
stirling・・・バイナリエディタ。なんでも良い。

qemu のコマンドは例えば以下のように使います。

qemu.exe -L . -m 128 -fda os.img

OS本では nask というのを使っていますが、
ここでは nasm を使うことにします。

2、フロッピーディスク関連

ブートには2HDのフロッピーディスクを用います。
フロッピーディスクを入れて起動することで、
ブートセクタ(いちばん最初のセクタ 512 bytes を、
自動的に 0x00007c00-0x00007dff に読み込みます。
そして 0x00007c00 よりプログラムの実行が開始します。

ブートセクタ以外の部分はブートセクタ内のプログラムで
メモリ上に読み出す必要があります。

以下、そのための資料。

1セクタ=512バイト

18(1-18)セクタ×80(0-79)シリンダ×両面(0-1)

512*18*80*2=1474560Bytes=1440KB

AH=0x02 読み込み
AH=0x03 書き込み
AH=0x04 ベリファイ
AH=0x0c シーク
AL=処理するセクタ数
CH=シリンダ番号 & 0xff
CL=セクタ番号(bit 0-5) | (シリンダ番号 & 0x300) >> 2
DH=ヘッド番号
DL=ドライブ番号
ES:BX=バッファアドレス
 戻り値
FLAGS.CF==0:エラーなし、AH==0
FLAGS.CF==1:エラーあり、AHにエラーコード

INT 0x13 でディスク処理

エラー時には AH=0x00 DL=0x00 とし INT 0x13 として、
ドライブにリセットをかける必要がある。

以下、ディスクイメージを作るコードです。

; ディスク 10 シリンダぶんを読み込み 0x08200〜0x34fff を埋める
; 本文はディスクイメージの 0x4200 から置く
; メモリでは 0xc200 へ読み込まれるので、
; 処理を 0xc200 へ飛ばす

CYLS EQU 10

ORG 0x7c00

JMP entry ; いきなりプログラム本体へ飛ぶ

DB 0x90
DB "NMIPL   " ; ブートセクタの名前
DW 512        ; 1セクタの大きさ
DB 1          ; クラスタの大きさ 1 セクタ
DW 1          ; FAT が 1 セクタめから始まる
DB 2          ; FAT の個数 2
DW 224        ; ルートディレクトリ領域の大きさ
DW 2880       ; ドライブの大きさ 2880 セクタ
DB 0xf0       ; メディアのタイプ
DW 9          ; FAT 領域の長さ 9 セクタ
DW 18         ; 1トラックのセクタ数 18
DW 2          ; 面の数 2
DD 0          ; パーテションを使わないので 0
DD 2880       ; ドライブの大きさ 2880 セクタ
DB 0, 0, 0x29 ; 不明
DD 0xffffffff ; 不明
DB "NM-OS      " ; ディスクの名前
DB "FAT12   " ; フォーマットの名前

%assign i 0 
%rep    18
DB 0
%assign i i+1 
%endrep

; プログラム本体

entry:
MOV AX, 0
MOV SS, AX
MOV SP, 0x7c00
MOV DS, AX
MOV AX, 0x0820
MOV ES, AX
MOV CH, 0
MOV DH, 0
MOV CL, 2
readloop:
MOV SI, 0
retry:
MOV AH, 0x02
MOV AL, 1
MOV BX, 0
MOV DL, 0x00
INT 0x13
JNC next
ADD SI, 1
CMP SI, 5
JAE error
MOV AH, 0x00
MOV DL, 0x00
INT 0x13
JMP retry
next:
MOV AX, ES
ADD AX, 0x0020
MOV ES, AX
ADD CL, 1
CMP CL, 18
JBE readloop
MOV CL, 1
ADD DH, 1
CMP DH, 2
JB readloop
MOV DH, 0
ADD CH, 1
CMP CH, CYLS
JB readloop
MOV [0x0ff0], CH
JMP 0xc200
error:
HLT
JMP error

%assign i 0 
%rep    329
DB 0
%assign i i+1 
%endrep

DB 0x55, 0xaa ; ブートセクタは必ず 0x55, 0xaa で終わる

DB 0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00

%assign i 0 
%rep    4600
DB 0
%assign i i+1 
%endrep

DB 0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00

%assign i 0 
%rep    1469432
DB 0
%assign i i+1 
%endrep

3、デバグ用命令

実行が正しく行われているか確認するための命令。

; 文字 A を書く

MOV AH, 0x0e
MOV AL, 0x41  ; A=0x41
MOV BL, 15
MOV BH, 0
INT 0x10

; 点を (16, 0) に白で描画

MOV ECX, 0xa0010 ; 0xa0000=グラフィックバッファの先頭
MOV BYTE [ECX], 15

; 文字色を変える

CLI
MOV AL, 0x01     ; 0-255 最初のパレット番号
OUT 0x03c8, AL   ; 得る場合は 0x03c7
MOV AL, 0xfb     ; R
OUT 0x03c9, AL
MOV AL, 0xfb     ; G
OUT 0x03c9, AL
MOV AL, 0xfb     ; B
OUT 0x03c9, AL
STI

4、32ビットモードへ

これから作成するバイナリデータは、
ブートディスクイメージの 0x4200 以降に上書きします。
するとブートセクタ上のプログラムによって、
0xc200 以降にロードされ実行が移ります。

以下は、

画面モードの設定、
キーボードの状態を記録
A20の壁(1MB以降のアクセス)の解除
セグメント方式とプロテクトモードの設定
OS本文の転送と実行の以降

をやっています。

OS本文では割り込みの設定などやっていこうと思いますが、
とりあえずそこまでの記述になります。

BOTPAK  EQU    0x00280000    ; bootpackのロード先
DSKCAC  EQU    0x00100000    ; ディスクキャッシュの場所
DSKCAC0  EQU    0x00008000    ; ディスクキャッシュの場所(リアルモード)

CYLS  EQU    0x0ff0      ; ブートセクタが設定する
LEDS  EQU    0x0ff1
VMODE  EQU    0x0ff2      ; 色数に関する情報。何ビットカラーか?
SCRNX  EQU    0x0ff4      ; 解像度のX
SCRNY  EQU    0x0ff6      ; 解像度のY
VRAM  EQU    0x0ff8      ; グラフィックバッファの開始番地

ORG    0xc200

; VGA グラフィックス(320x200x8bitカラー)

MOV AL, 0x13
MOV AH, 0x00
INT 0x10
MOV BYTE [VMODE], 8          ; 8bit
MOV WORD [SCRNX], 320        ; screen_width
MOV WORD [SCRNY], 200        ; screen_height
MOV DWORD [VRAM], 0x000a0000 ; vram

; キーボードの LED 状態を調べる

MOV AH, 0x02
INT 0x16
MOV [LEDS], AL

; PIC の割り込みを禁止する

MOV AL, 0xff
OUT 0x21, AL
NOP          ; OUT命令を連続させるとうまくいかない機種がある
OUT 0xa1, AL

; CPU での割り込み禁止

CLI

CALL waitkbdout
MOV  AL, 0xd1
OUT  0x64, AL
CALL aitkbdout
MOV  AL, 0xdf ; enable A20
OUT  0x60, AL
CALL waitkbdout

; プロテクトモード移行

;%NASK [INSTRSET "i486p"]   ; 486の命令を使用

LGDT [GDTR0]         ; GDT を設定 

MOV  EAX, CR0
AND  EAX, 0x7fffffff ; bit31を0に ページング禁止
OR   EAX, 0x00000001 ; bit0を1に プロテクトモードへの移行
MOV  CR0, EAX
JMP  pipelineflash   ; パイプラインのクリア
pipelineflash:
MOV AX, 1*8          ;  読み書き可能セグメント32bit
MOV DS, AX
MOV ES, AX
MOV FS, AX
MOV GS, AX
MOV SS, AX

; bootpackの転送

MOV  ESI, bootpack   ; 転送元
MOV  EDI, BOTPAK     ; 転送先
MOV  ECX, 512*1024/4
CALL memcpy

; ディスクデータの転送

MOV  ESI, 0x7c00 ; 転送元
MOV  EDI, DSKCAC ; 転送先
MOV  ECX, 512/4
CALL memcpy

; ディスクデータの転送

MOV  ESI, DSKCAC0+512 ; 転送元
MOV  EDI, DSKCAC+512  ; 転送先
MOV  ECX, 0
MOV  CL, [CYLS]
IMUL ECX, 512*18*2/4  ; シリンダ数からバイト数/4に変換
SUB  ECX, 512/4       ; IPLの分だけ差し引く
CALL memcpy

; bootpackの起動

MOV  EBX, BOTPAK

MOV  ESP, 0x00310000 ; スタック初期値
JMP  DWORD 2*8:0x00000000

; キーボードのから読み

waitkbdout:
IN  AL, 0x64
AND AL, 0x02
IN  AL, 0x60     
JNZ waitkbdout
RET

; メモリのコピー

memcpy:
MOV EAX, [ESI]
ADD ESI, 4
MOV [EDI], EAX
ADD EDI, 4
SUB ECX, 1
JNZ memcpy
RET

ALIGN 16, DB 0

GDT0:
DW 0x0000, 0x0000, 0x0000, 0x0000
DW 0xffff, 0x0000, 0x9200, 0x00cf  ; 読み書き可能セグメント32bit
DW 0xffff, 0x0000, 0x9a28, 0x0047  ; 実行可能セグメント32bit(bootpack用)

DW 0
GDTR0:
DW 8*3-1 ; ディスクリプタテーブルのサイズ - 1
DD GDT0

ALIGN 16, DB 0

bootpack:

; JMP  DWORD 2*8:0x00000000
; によって処理がここに飛びますが、
; ここからは別セグメントに飛んだことになるので
; アドレスを 0 からと考えることになります。
; 以降は別でアセンブルして継ぎ足すことにします。
;
; 以降は32ビットコードなので、
; [BITS 32]
; といった記述が必要になります。

5、GDT

ディスクリプタテーブルは、

リミットサイズ:16bit ディスクリプタテーブルのサイズ - 1
ディスクリプタテーブルの先頭アドレス:32bit

以上を用意し、

LGDT [GDTR0]

という特殊な命令で設定します。
コードでいうと以下のようになります。。

GDT0:
DW 0x0000, 0x0000, 0x0000, 0x0000
DW 0xffff, 0x0000, 0x9200, 0x00cf  ; 読み書き可能セグメント32bit
DW 0xffff, 0x0000, 0x9a28, 0x0047  ; 実行可能セグメント32bit(bootpack用)

DW 0
GDTR0:
DW 8*3-1 ; ディスクリプタテーブルのサイズ - 1
DD GDT0

テーブルのデータの並び方は独特で複雑です。

サイズ、アドレス、属性 を例えば以下のようにしたとき、

0x11234455, 0x66778899, 0xabc

以下のように並びかわります。

DW 0x4455, 0x8899, 0xbc77, 0x66a3

属性に関しては以下の通り、

GDRAPDDSTTTA の順で上から、

G:Gビット。1ならサイズを4K倍して解釈する。
D:コードセグメントなら0=16ビット、1=32ビット。
   データセグメントならリミットが0xffffを越えるなら必ず1にすべき。
R:リザーブ。必ず0。
A:OS製作者が自由に使用可能。
P:1でセグメントが物理メモリ上にあることを示す。
D:特権レベル(0-3)。
S:1であればセグメントを示す。
T:セグメントタイプ。
   0:読み出し専用
   1:読み書き可能
   2:読み出し専用(スタック)
   3:読み書き可能(スタック)
   4:実行専用
   5:実行および読み書き可能
   6:実行専用コンフォーミングコード
   7:実行および読み書き可能コンフォーミングコード
A:定期的に0に、アクセスすると1に。アクセス状況調査用。

本のほうは属性を以下のようにしています。

#define AR_DATA32_RW 0x492 ; データセグメント
#define AR_CODE32_ER 0x49a ; コードセグメント
#define AR_LDT       0x082 ; タスク用データセグメント(後述)
#define AR_TSS32     0x089 ; タスク切り替え用(後述)
#define AR_INTGATE32 0x08e ; 割り込み用(後述)

6、IDT

IDTもGDTと同じような設定のやりかたです。

LIDT [IDTR0]

IDT0:
%assign i 0 
%rep    32
DW 0x0000, 0x0000, 0x0000, 0x0000
%assign i i+1 
%endrep

DW 0x0000, 0x0000, 0x0000, 0x0000 ; IRQ 0 :INT 0x20
DW 0x0005, 0x0010, 0x8e00, 0x0000 ; IRQ 1 :INT 0x21

DW 0
IDTR0:
DW 8*34-1 ; ディスクリプタテーブルのサイズ - 1
DD IDT0

割り込み関数のアドレス、セグメント(2*8など)、属性
を例えば以下のようにしたとき、

0x11223344, 0x5566, 0x7788

DW 0x3344, 0x5566, 0x8877, 0x1122

属性の値は 0x08e としておけばよいようです。

7、PIC

PICは割り込みのスイッチです。
IDTを設定しただけでは割り込みは発生しません。

PIC は PIC0 と PIC1 の2つが存在し、
ひとつの PIC に8つの割り込みが可能です。
ただしPIC0 の番号 2 の割り込みは PIC1 の割り込みを受けます。

実際に値をセットするには、

MOV AL, 0xff
OUT 0x21, AL

などとやります。

実際指定できるのは PIC0_ICW2 と PIC1_ICW2 の2つ。
他のパラメータは特にこのままで問題ありません。
ポート番号の重複が気になるところですが、
この順番で出力すれば正しく動作します。
(OUT が連続する場合は間に NOP を置くと良い)

PIC0_IMR  0x21 0xff ; すべての割り込みをオフ
PIC1_IMR  0xa1 0xff ; すべての割り込みをオフ

PIC0_ICW1 0x20 0x11 ; エッジトリガモード
PIC0_ICW2 0x21 0x20 ; IRQ0-7 は INT20-27 で受ける。(INT0-1b はシステムが使用)
PIC0_ICW3 0x21 1 << 2 ; PIC1 は IRQ2 に接続
PIC0_ICW4 0x21 0x01 ; ノンバッファモード

PIC1_ICW1 0xa0 0x11 ; エッジトリガモード
PIC1_ICW2 0xa1 0x28 ; IRQ8-15 は INT28-2f で受ける
PIC1_ICW3 0xa1 2 ; PIC1 は IRQ2 に接続
PIC1_ICW4 0xa1 0x01 ; ノンバッファモード

PIC0_IMR  0x21 0xfb ; PIC1 以外はすべて禁止
PIC1_IMR  0xa1 0xff ; すべての割り込みをオフ

以上で初期化は終わりです。

STI

でCPUの割り込みを有効にします。

例えばキーボード割り込みを有効にしたい場合、
キーボード割り込みは IRQ1 なので、

MOV AL, 0xf9
OUT 0x21, AL

とします。

すると INT21 での割り込みが可能になり、
IDTでの21番の割り込み処理へ飛ばすことができるわけです。
(この操作は必ず STI した後に行うこと)

なお、割り込みを受けた場合には、
割り込み先で割り込み再開の処理をかける必要があります。

PIC0_OCW2  0x20 0x60+IRQ番号
PIC1_OCW2  0xa0 0x60+IRQ番号

例えば IRQ1 の割り込みを再開するには、

MOV AL, 0x61
OUT 0x20, AL

とする必要があります。

PIC1 の割り込み再開の場合には、

MOV AL, 0x64
OUT 0xa0, AL
MOV AL, 0x62
OUT 0x20, AL

PIC0 の番号 2 への割り込み再開も必要になります。

8、割り込み処理

例えば IRQ 21 で キー入力を受けることを考えます。
キーデータは押したときと離したときの割り込みが発生します。
そのときに飛ばされる処理は以下のようにしてみました。

_inthandler21:
PUSH ES
PUSH DS
PUSHAD

XOR EAX, EAX
MOV EDX, 0x60
IN AL, DX ; キー入力を得る

MOV AL, 0x61
OUT 0x20, AL ; 割り込み状態復帰

MOV [KEY_BUF], EAX ; キー情報を記録

POPAD
POP DS
POP ES
IRETD

割り込みは、得たキーデータを保存するのみとなっています。
割り込みの処理はできるだけ短くが鉄則です。

OS側でキーデータを調べる場合はいったん、

CLI

で割り込みを禁止します。

もしキーデータが無ければ、

_stihlt:
STI
HLT
RET

以上を CALL _stlhlt として割り込み再開します。

キーデータがあればキーデータを取り出し、
そのまま

STI

とやればOKです。

キーデータの保存はリングバッファを用いたほうが良いでしょう。
以下、だいたいの流れ。

cli;
if(バッファにデータがあるか?){
  // ある場合
  データを取り出す;
  sti;
  データを用いた処理;
}
else{
  // 無い場合
  stihlt; 
}

尚、割り込みは 0x30 から 0xff までフリーであり、
例えば INT 0x40 などでソフトウェア側から呼び出すことも可能である。
この方法でプロセスの立ち上げやAPIの呼び出しを作成する
というような工夫も可能である。

例外の割り込みは 0x0d であり、
メモリの不正アクセスがあれば割り込まれる。

注意として、
どこから割り込んだか?
によって DS などが異なることがある。
もしOS以外のセグメントから割り込んだ場合、
これらを適切にOSのものと一時置きかえる必要がある。

9、PIT

プログラマブル インターバル タイマーのこと。
要はタイマー割り込みです。

割り込み周期は 11932 を設定すれば 100Hz になるらしいので、
16進数にすると 0x2e9c を設定するようです。

まず ATI し、次に、

MOV AL, 0x34 ; PITの割り込み周期変更
OUT 0x43, AL
MOV AL, 0x9c
OUT 0x40, AL
MOV AL, 0x2e
OUT 0x40, AL

上の設定をしPICの割り込みを有効にします。

10、TTS

TTSはマルチタスクの管理をします。
TTSはGDTの領域に登録しますが、
GDTとは全く性質のことなるものです。

TSSはひとつのタスクにつき次のようなデータ領域を必要とします

TSS32:
DD 0x00000000, 0x00000000, 0x00000000, 0x00000000 ; backlink, esp0, ss0, esp1
DD 0x00000000, 0x00000000, 0x00000000, 0x00000000 ; ss1, esp2, ss2, cr3
DD 0x00000000, 0x00000000, 0x00000000, 0x00000000 ; eip, eflags, eax, ecx
DD 0x00000000, 0x00000000, 0x00000000, 0x00000000 ; edx, ebx, esp, ebp
DD 0x00000000, 0x00000000                         ; esi, edi
DD 0x00000000, 0x00000000, 0x00000000, 0x00000000 ; es, cs, ss, ds
DD 0x00000000, 0x00000000                         ; fs, gs
DD 0x00000000, 0x00000000                         ; ldtr, iomap

補足:
backlink: タスク切り替え時に元のタスクのセレクタ値をCPUが保存
esp0, ss0: 特権レベル 0 での ESP と SS を保存しておく
esp1, ss1: 特権レベル 1 の場合
esp2, ss2: 特権レベル 2 の場合
ldtr: 後述
iomap: I/O の許可をタスクごとに設定できる

GDTへの登録ですが新たなセグメントの場合は、

サイズ、アドレス、属性

でしたが、TTSの場合は、

サイズ、データ領域のアドレス、属性

となります。

サイズは 26 * 4 - 1 なので 103 で固定です。
データ領域のアドレスは上の例なら [TSS32] などです。
属性はTSSの場合 0x089 で良いようです。

例えば 3*8 でGDTに登録された場合、

LTR [TR]

TR:
DW 0x0018

とすることでTRレジスタに現在のタスクを知らせます。

これを 4*8 のタスクに切り替える場合は、

JMP 4*8:0

とし、TRレジスタは自動的に切り替わります。
(このとき :0 は無視されます。)

切り替える前に以下の要素は設定が必要です。

eflags = 0x00000202; ; ↓現在のタスクの初期値
eax = 0;
ecx = 0;
edx = 0;
ebx = 0;
ebp = 0;
esi = 0;
edi = 0;
es = 1 * 8;
cs = 2 * 8;
ss = 1 * 8;
ds = 1 * 8;
fs = 1 * 8;
gs = 1 * 8;
idtr = 0;
iomap = 0x40000000;

esp = タスクで使用するスタックポインタの位置
eip = タスク切り替え時に移動するポイントのアドレス

特にスタックはタスクごとに新たな領域が必要だし、
タスクが切り替わってどこに飛ぶか?が重要です。

11、LDT

LDTはタスクごとにローカルなセグメントを切り替える方法です。
主な用途はタスク間での領域の保護になります。

LDTはTSSが1つにつき1つ存在します。

まずLDT領域を用意します。
(パラメータはとりあえずすべて 0 にしています)

LDT0:
DW 0x0000, 0x0000, 0x0000, 0x0000 ; コードセグメント用
DW 0x0000, 0x0000, 0x0000, 0x0000 ; データセグメント用

GDTにはサイズ、LDTのアドレス、属性を書きます。
サイズはコードとデータの2つなので 2*8-1 になると思います。
属性はLDTの場合 0x082 で良いようです。

TSSの idtr には対応するGDT内のLDTの番号を書きます。

次にLDTにコードセグメントとデータセグメントの設定を行う。

となります。

LDTの場合はタスク内で 0*8+4 と 1*8+4 でアクセスします。
4 を足すことでGDTと区別されます。

各タスクごとに各タスクのLDTが存在することになり、
各タスクで 4 というセグメントでアクセスしたとしても、
各タスクごとのセグメントにアクセスすることができ、
逆に他のタスクのセグメントを保護することになります。

12、セキュリティ

アプリケーションは特権レベル 3 に設定します。
するとアプリケーションからOSのメモリにアクセスすると、
INT 0x0d の例外を発生させます。
アプリケーションからは HLT や CLI などを使用しても例外を発生させます。
この仕組みでアプリケーションからOSを保護します。
逆に言うと、OSからアプリケーションへの移行をし、安全を確保します。
ただ、OSは特権レベル 3 に far-CALL や JMP できません。
OS本では以下のようなトリッキーな方法で特権レベル3のアプリに移行しています。

特権0(OS)から特権3(アプリ)への移動

_f
PUSHAD
tss.esp0 にOSの ESP を保存
tss.ss0 にOSの SS を保存
MOV ES, アプリのデータセグメント
MOV DS, アプリのデータセグメント
MOV FS, アプリのデータセグメント
MOV GS, アプリのデータセグメント
PUSH アプリの SS+3
PUSH アプリの ESP
PUSH アプリの CS+3
PUSH アプリの EIP
RETF

戻るときはAPI割り込み中に、

MOV ESP, tss.esp0
POPAD
RET

これで _f の呼び出し元に返ります。

↑ということですがこの方法を試したらスタックセグメントが切り変わらなかった。
自分は「はじめて読む486」を参考にしてタスクによる切り替えをしました。
「はじめて読む486」ではレベルの移動にコールゲートという方法を使っています。
特権レベル 0 に戻る場合もコールゲートを使ったほうが良さそうです。

割り込みに関しても IDT に特権レベル 3 を設定します。
すると特権レベル 3 からでも割り込みを呼べます。
重要なキー割り込みなどは特権レベル 0 のままのほうが良いでしょう。

アプリケーション間のメモリ保護はLDTによって行います。

13、サンプルコード

以下の4ファイルで構成されています。

a.nas:ディスク読み出しのディスクイメージ全体
b.nas:OSの設定と32ビットモードへ移行部分
c.nas:OS本体部分
d.nas:アプリケーション部分

それぞれ nasm でアセンブルし、
b から d はできたバイナリを b c d の順にくっつけます。
これを a のディスクイメージの 0x4200 からの部分に貼り付けます。
挿入するのではなくきちんと置き換えてください。

直アドレスをコード中に直接書いてしまっている部分もあり、
このサンプルに少しでも変更を加えるだけで動かなくなる可能性があります。

とりあえず

quem からのディスクイメージの読み出し
ディスクリプタテーブルの設定と32ビットモードへの移行
タスク切り替えによる特権レベル3のLDTへの移行
特権レベル3からのキー割り込みの発生

以上に成功し動作確認をとりました。

;
; a.nas
;

; ディスク 10 シリンダぶんを読み込み 0x08200〜0x34fff を埋める
; 本文 (ディスクイメージ 0x4200 メモリで 0xc200) へ処理を飛ばす

CYLS EQU 10

ORG 0x7c00

JMP entry

DB 0x90
DB "HELLOIPL"
DW 512
DB 1
DW 1
DB 2
DW 224
DW 2880
DB 0xf0
DW 9
DW 18
DW 2
DD 0
DD 2880
DB 0, 0, 0x29
DD 0xffffffff
DB "NM-OS      "
DB "FAT12   "

%assign i 0 
%rep 18
DB 0
%assign i i+1 
%endrep

entry:
MOV AX, 0
MOV SS, AX
MOV SP, 0x7c00
MOV DS, AX
MOV AX, 0x0820
MOV ES, AX
MOV CH, 0
MOV DH, 0
MOV CL, 2
readloop:
MOV SI, 0
retry:
MOV AH, 0x02
MOV AL, 1
MOV BX, 0
MOV DL, 0x00
INT 0x13
JNC next
ADD SI, 1
CMP SI, 5
JAE error
MOV AH, 0x00
MOV DL, 0x00
INT 0x13
JMP retry
next:
MOV AX, ES
ADD AX, 0x0020
MOV ES, AX
ADD CL, 1
CMP CL, 18
JBE readloop
MOV CL, 1
ADD DH, 1
CMP DH, 2
JB readloop
MOV DH, 0
ADD CH, 1
CMP CH, CYLS
JB readloop
MOV [0x0ff0], CH
JMP 0xc200
error:
HLT
JMP error

%assign i 0 
%rep 329
DB 0
%assign i i+1 
%endrep

DB 0x55, 0xaa

DB 0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00

%assign i 0 
%rep 4600
DB 0
%assign i i+1 
%endrep

DB 0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00

%assign i 0 
%rep 1469432
DB 0
%assign i i+1 
%endrep

;
; b.nas
;

BOTPAK  EQU 0x00280000 ; bootpackのロード先

CYLS  EQU 0x0ff0 ; ブートセクタが設定する
LEDS  EQU 0x0ff1 ; LED の状態を記録
VMODE EQU 0x0ff2 ; 色数に関する情報。何ビットカラーか?
SCRNX EQU 0x0ff4 ; 解像度のX
SCRNY EQU 0x0ff6 ; 解像度のY
VRAM  EQU 0x0ff8 ; グラフィックバッファの開始番地

ORG 0xc200

; VGA グラフィックス(320x200x8bitカラー)

MOV AL, 0x13
MOV AH, 0x00
INT 0x10
MOV BYTE [VMODE], 8          ; 8bit
MOV WORD [SCRNX], 320        ; screen_width
MOV WORD [SCRNY], 200        ; screen_height
MOV DWORD [VRAM], 0x000a0000 ; vram

; キーボードの LED 状態を調べる

MOV AH, 0x02
INT 0x16
MOV [LEDS], AL

; PIC の割り込みを禁止する

MOV AL, 0xff
OUT 0x21, AL
NOP          ; OUT命令を連続させるとうまくいかない機種がある
OUT 0xa1, AL

; CPU での割り込み禁止

CLI

; 1MB以降のメモリへのアクセスを有効に

CALL waitkbdout
MOV  AL,0xd1
OUT  0x64,AL
CALL waitkbdout
MOV  AL,0xdf ; enable A20
OUT  0x60,AL
CALL waitkbdout

; 文字 A を表示するテスト

MOV AH, 0x0e
MOV AL, 0x41
MOV BL, 15
MOV BH, 0
INT 0x10

; プロテクトモード移行

;%NASK [INSTRSET "i486p"] ; 486の命令を使用

LGDT [GDTR0]         ; GDT を設定 

MOV EAX, CR0
AND EAX, 0x7fffffff ; bit31を0に ページング禁止
OR  EAX, 0x00000001 ; bit0を1に プロテクトモードへの移行
MOV CR0, EAX
JMP pipelineflash   ; パイプラインのクリア
pipelineflash:
MOV AX, 1*8         ;  読み書き可能セグメント32bit
MOV DS, AX
MOV ES, AX
MOV FS, AX
MOV GS, AX
MOV SS, AX

; bootpackの転送

MOV  ESI, bootpack   ; 転送元
MOV  EDI, BOTPAK     ; 転送先
MOV  ECX, 512*1024/4
CALL memcpy

; bootpackの起動

MOV EBX, BOTPAK

; 1つめの点を描画

MOV ECX, 0xa0200
MOV BYTE [ECX], 15

LIDT [IDTR0]        ; IDT を設定

LTR [TR]            ; TR を設定

; PIC の初期化

MOV AL, 0xff
OUT 0x21, AL ; すべての割り込みをオフ
MOV AL, 0xff
OUT 0xa1, AL ; すべての割り込みをオフ

MOV AL, 0x11
OUT 0x20, AL ; エッジトリガモード
MOV AL, 0x20
OUT 0x21, AL ; IRQ0-7 は INT20-27 で受ける。(INT0-1b はシステムが使用)
MOV AL, 0x04
OUT 0x21, AL ; PIC1 は IRQ2 に接続
MOV AL, 0x01
OUT 0x21, AL ; ノンバッファモード

MOV AL, 0x11
OUT 0xa0, AL ; エッジトリガモード
MOV AL, 0x28
OUT 0xa1, AL ; IRQ8-15 は INT28-2f で受ける
MOV AL, 0x02
OUT 0xa1, AL ; PIC1 は IRQ2 に接続
MOV AL, 0x01
OUT 0xa1, AL ; ノンバッファモード

MOV AL, 0xfb
OUT 0x21,AL ; PIC1 以外はすべて禁止
MOV AL, 0xff
OUT 0xa1, AL ; すべての割り込みをオフ

MOV ESP, 0x00310000 ; スタック初期値
JMP DWORD 2*8:0x00000000

; キーボードのから読み

waitkbdout:
IN  AL, 0x64
AND AL, 0x02
IN  AL, 0x60     
JNZ waitkbdout
RET

; メモリのコピー

memcpy:
MOV EAX, [ESI]
ADD ESI, 4
MOV [EDI], EAX
ADD EDI, 4
SUB ECX, 1
JNZ memcpy
RET

ALIGN 16, DB 0

GDT0:
DW 0x0000, 0x0000, 0x0000, 0x0000
DW 0xffff, 0x0000, 0x9200, 0x00cf  ; 読み書き可能セグメント32bit
DW 0xffff, 0x0000, 0x9a28, 0x0040  ; 実行可能セグメント32bit(bootpack用)
DW 0x0067, 0xc570, 0x8900, 0x0000  ; TSS32_0
DW 0x0067, 0xc5d8, 0x8900, 0x0000  ; TSS32_1
DW 0x000f, 0xc640, 0x8200, 0x0000  ; LDT_1

DW 0
GDTR0:
DW 8*6-1 ; ディスクリプタテーブルのサイズ - 1
DD GDT0

ALIGN 16, DB 0

IDT0:
%assign i 0 
%rep    12
DW 0x0000, 0x0000, 0x0000, 0x0000
%assign i i+1 
%endrep

DW 0x0005, 0x0010, 0x8e00, 0x0000 ; 0x0c
DW 0x002c, 0x0010, 0x8e00, 0x0000 ; 0x0d

%assign i 0 
%rep    18
DW 0x0000, 0x0000, 0x0000, 0x0000
%assign i i+1 
%endrep

DW 0x0000, 0x0000, 0x0000, 0x0000
DW 0x0053, 0x0010, 0x8e00, 0x0000 ; 0x21

%assign i 0 
%rep    30
DW 0x0000, 0x0000, 0x0000, 0x0000
%assign i i+1 
%endrep

DW 0x007b, 0x0010, 0xee00, 0x0000 ; 0x40

DW 0
IDTR0:
DW 8*65-1 ; ディスクリプタテーブルのサイズ - 1
DD IDT0

ALIGN 16, DB 0

TR:
DW 0x0018

ALIGN 16, DB 0

TSS32_0:
DD 0x00000000, 0x00000000, 0x00000000, 0x00000000 ; backlink, esp0, ss0, esp1
DD 0x00000000, 0x00000000, 0x00000000, 0x00000000 ; ss1, esp2, ss2, cr3
DD 0x00000000, 0x00000202, 0x00000000, 0x00000000 ; eip, eflags, eax, ecx
DD 0x00000000, 0x00000000, 0x00000000, 0x00000000 ; edx, ebx, esp, ebp
DD 0x00000000, 0x00000000                         ; esi, edi
DD 0x00000008, 0x00000010, 0x00000008, 0x00000008 ; es, cs, ss, ds
DD 0x00000008, 0x00000008                         ; fs, gs
DD 0x00000000, 0x40000000                         ; ldtr, iomap

TSS32_1:
DD 0x00000000, 0x00310000, 0x00000008, 0x00000000 ; backlink, esp0, ss0, esp1
DD 0x00000000, 0x00000000, 0x00000000, 0x00000000 ; ss1, esp2, ss2, cr3
DD 0x00000000, 0x00000202, 0x00000000, 0x00000000 ; eip, eflags, eax, ecx
DD 0x00000000, 0x00000000, 0x00000fff, 0x00000000 ; edx, ebx, esp, ebp
DD 0x00000000, 0x00000000                         ; esi, edi
DD 0x00000007, 0x0000000f, 0x00000007, 0x00000007 ; es, cs, ss, ds
DD 0x00000007, 0x00000007                         ; fs, gs
DD 0x00000028, 0x40000000                         ; ldtr, iomap

ALIGN 16, DB 0

LDT1:
DW 0xffff, 0x0000, 0xf239, 0x0040 ; スタックセグメント用
DW 0xffff, 0x0000, 0xfa38, 0x0040 ; コードセグメント用

ALIGN 16, DB 0

bootpack:

;
; c.nas
;

APP EQU 0x00380000 ; appのロード先

[BITS 32]

ORG 0

JMP main

interrupt0c: ; スタック例外割り込み

STI
PUSH DS
PUSH ES
PUSHAD
MOV EAX, ESP
PUSH EAX
MOV AX, SS
MOV DS, AX
MOV ES, AX

MOV ECX, 0xa0000 ; 0xa0000=グラフィックバッファの先頭
ADD ECX, 0x0400
MOV BYTE [ECX], 15

POP EAX
POPAD
POP ES
POP DS
ADD ESP, 4
IRETD

interrupt0d: ; 例外割り込み

STI
PUSH DS
PUSH ES
PUSHAD
MOV EAX, ESP
PUSH EAX
MOV AX, SS
MOV DS, AX
MOV ES, AX

MOV ECX, 0xa0000 ; 0xa0000=グラフィックバッファの先頭
ADD ECX, 0x0300
MOV BYTE [ECX], 6

POP EAX
POPAD
POP ES
POP DS
ADD ESP, 4
IRETD

interrupt21: ; キー割り込み

PUSH DS
PUSH ES
PUSHAD
MOV EAX, ESP
PUSH EAX
MOV AX, SS
MOV DS, AX
MOV ES, AX

MOV AL, 0x61
OUT 0x20, AL ; 割り込み状態復帰

XOR EAX, EAX
MOV EDX, 0x00000060
IN AL, DX ; キー入力を得る

MOV ECX, 0xa0000 ; 0xa0000=グラフィックバッファの先頭
ADD ECX, EAX
MOV BYTE [ECX], 15

POP EAX
POPAD
POP ES
POP DS
IRETD

interrupt40: ; 自作割り込み

STI
PUSH DS
PUSH ES
PUSHAD
MOV EAX, ESP
PUSH EAX

XOR EAX, EAX
MOV AX, SS
MOV DS, AX
MOV ES, AX

; 3つめの点を描画

MOV ECX, 0xa0000 ; 0xa0000=グラフィックバッファの先頭
ADD ECX, 0x0204
MOV BYTE [ECX], 15

POP EAX
POPAD
POP ES
POP DS
IRETD

main:

MOV  ESI, app ; 転送元
MOV  EDI, APP ; 転送先
MOV  ECX, 512/4

memcpy:
MOV EAX, [CS:ESI]
ADD ESI, 4
MOV [EDI], EAX
ADD EDI, 4
SUB ECX, 1
JNZ memcpy

STI

; キー割り込みを許可

MOV AL, 0xf9
OUT 0x21, AL

; 2つめの点を描画

MOV ECX, 0xa0000 ; 0xa0000=グラフィックバッファの先頭
ADD ECX, 0x0202
MOV BYTE [ECX], 15

JMP 4*8:0

ALIGN 16, DB 0

app:

;
; d.nas
;

[BITS 32]

ORG 0

main:

INT 0x40

fin:
JMP fin

ALIGN 16, DB 0

inserted by FC2 system