2014年9月30日火曜日

[OS作成]30日でできる!OS自作入門 3日目 (7)

32bitモードへの移行と、C言語で書いたOS本体の実行を実装する。

3日目では、32bitモードへの移行に関する詳細な解説は無く、5日目以降になっている。
C言語とアセンブリ言語で作成したオブジェクトファイルの扱いにおいては、 筆者独自のツールを使っており、標準ツールに置き換えるには、どのようにすれば良いのか、今ひとつ分からない。

他の人のブログを参考にして、乗り切ることにしよう。

コードを読んでいて、どうして1MB以上のメモリにアクセスするようにするために、キーボード関連のコードが出てくるのかと思っていたら、ここに 書いてあった。
---引用---
これに対する互換性のために、IBMのエンジニアはA20アドレスライン(8086はA0からA19までのアドレスラインしか持たない)にキーボードコントローラを通して信号を送り、A20互換モードを有効化/無効化できるようなメカニズムを提供した。なぜキーボードコントローラなのかと不思議に思うだろう。答えは未使用のピンがあったからである。

きっと有名な仕様なんだろうな。こういう話は面白い。

Ubuntu14.04の64bit版を使っているので、gccやldはデフォルトで64bit版のコードに対応するため、 gccやldに32bit用のコードを生成するオプションを付ける必要があった。
こちらのブログでは、hrbファイルのヘッダファイルの固定値を直接レジスタに代入していたが、なるべくオリジナルのコードのままにしたかったので、bootpack以降に、ヘッダに相当するデータを定義して、それを参照するようにした。

以下にMakefile, boot.s, tinyos.c のソースを示す。

Makefile

image_file=tinyos.img

ipl.bin: ipl.s ipl_lnk.ls
 gcc -nostdlib -o $@ -Tipl_lnk.ls ipl.s
 gcc -Tipl_lnk.ls -c -g -Wa,-a,-ad ipl.s > ipl.lst

tinyos.sys: boot.s boot_lnk.ls
 gcc boot.s -nostdlib -o $@ -Tboot_lnk.ls -o boot.bin
# gcc boot.s -m32 -nostdlib -o $@ -Tboot_lnk.ls -o boot.bin
 gcc -Tboot_lnk.ls -c -g -Wa,-a,-ad boot.s > boot.lst
 gcc *.c -m32 -c -nostdlib -Wl,--oformat=binary -o tinyos.o
# gcc *.c -m32 -S -c -nostdlib -Wl,--oformat=binary -o tinyos.o
# gcc -c -g -Wa,-a,-ad tinyos.s > tinyos.lst
 ld -m elf_i386 -o tinyos.bin -e Main --oformat=binary tinyos.o
 cat boot.bin tinyos.bin > $@

image_file: ipl.bin tinyos.sys
 mformat -f 1440 -B ipl.bin -C -i ${image_file} ::
 mcopy tinyos.sys -i ${image_file} ::

img:
 make image_file

run:
 qemu-system-i386 -m 32 -localtime -vga std -fda ${image_file}

debug:
 qemu-system-i386 -m 32 -localtime -vga std -fda ${image_file} \
  -gdb tcp::10000 \
  -S

clean:
 rm tinyos.sys

boot.s

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

// BOOT_INFO
.set CYLS, 0x0ff0 # シリンダ数
.set LEDS,0x0ff1 # LEDの状態
.set VMODE, 0x0ff2 # ビデオモード
.set SCRNX, 0x0ff4 # 解像度X
.set SCRNY, 0x0ff6 # 解像度Y
.set VRAM, 0x0ff8 # VRAMの開始アドレス

.text
.code16

 // ビデオモードを変更する
 movb $0x00, %ah # VGA Graphics 320x200x8bit
 movb $0x13, %al
 int $0x10

 // 画面の状態を記録する
 movb $8, (VMODE)
 movw $320, (SCRNX)
 movw $200, (SCRNY)
 movl $0x000a0000, (VRAM)

 // LEDの状態を記録する
 movb $0x02, %ah
 int $0x16
 movb %al, (LEDS)

 // PICが割り込みを受け付けないようにする
 movb $0xff, %al
 outb %al, $0x21
 nop   # outは連続して使用しない
 outb %al, $0xa1
 cli   # CPUでも割り込み禁止

 // A20互換モードを無効にして1MB以上のアドレスにアクセスできるようにする
 call waitkbdout
 movb $0xd1, %al
 outb %al, $0x64
 call waitkbdout
 movb $0xdf, %al # A20を有効にする
 outb %al, $0x60
 call waitkbdout

 // プロテクトモードに移行する
.arch i486
 lgdt (GDTR0)
 movl %cr0, %eax
 andl $0x7fffffff, %eax # ページング禁止
 orl $0x00000001, %eax # プロテクトモード移行
 movl %eax, %cr0
 jmp pipelineflush
pipelineflush:
 movw $1*8, %ax
 movw %ax, %ds
 movw %ax, %es
 movw %ax, %fs
 movw %ax, %gs
 movw %ax, %ss

 // bootpackを転送する
 movl $bootpack, %esi # 転送元
 movl $BOTPAK, %edi # 転送先
 movl $512*1024/4, %ecx # 4で割っているのは4バイト単位で処理するため
 call memcpy

 // ディスクイメージを本来の位置へ転送する

 // ブートセクタ
 movl $0x7c00, %esi
 movl $DSKCAC, %edi
 movl $512/4, %ecx
 call memcpy
 
 // 残り
 movl $DSKCAC0+512, %esi
 movl $DSKCAC+512, %edi
 movl $0, %ecx
 movb (CYLS), %cl   # 読み込んだシリンダ数
 imull $512*18*2/4, %ecx # 1シリンダあたりのバイト数/4を掛ける
 sub $512/4, %ecx      # IPL分を引く
 call memcpy

 // bootpackを起動する
 movl $BOTPAK, %ebx
 movl 16(%ebx), %ecx
 addl $3, %ecx
 shrl $2, %ecx
 jz skip  # 転送するものがない
 movl 20(%ebx), %esi  # .dataのアドレス
 addl %ebx, %esi
 movl 12(%ebx), %edi # .data転送先
 call memcpy
skip: 
 movl 12(%ebx), %esp  # スタック初期値
 ljmpl $2*8, $0x0000001b
 
waitkbdout:
 inb $0x64, %al
 andb $0x02, %al
 inb $0x60, %al # 元のソースにはないコード
 jnz waitkbdout
 ret

memcpy:
 movl (%esi), %eax
 addl $4, %esi
 movl %eax, (%edi)
 addl $4, %edi
 subl $1, %ecx
 jnz memcpy
 ret

.align 16
 
GDT0:
 // GDTの構成
 // short limit_low, base_low
 // char base_mid, access_right
 // char limit_high, base_high
 
 // null selector
 .skip 8, 0x00
 // base=0x00000000 limit=0xcfffff access_right=0x92
 .word 0xffff, 0x0000, 0x9200, 0x00cf
 // base=0x00280000 limit=0x47ffff access_right=0x9a
 .word 0xffff, 0x0000, 0x9a28, 0x0047
 .word 0x0000
 
GDTR0:
 .word 8 * 3 - 1 # GDTのサイズ?
 .int GDT0

.align 16
bootpack:
 # + 0 : stack+.data+heap の大きさ(4KBの倍数)
 .int 0x00
 # + 4 : シグネチャ "Hari"
 .ascii "Tiny"
 # + 8 : mmarea の大きさ(4KBの倍数)
 .int 0x00
 # +12 : スタック初期値&.data転送先
 .int 0x00310000
 # +16 : .dataのサイズ
 .int 0x11a8
 # +20 : .dataの初期値列がファイルのどこにあるか
 .int 0x10c8
 # +24 +28 のセットで 1bからの命令が E9 XXXXXXXX (JMP)になり、C言語のエントリポイントにJMPするようだ
 # +24 : 0xe9000000
 .int  0xe9000000
 # +28 : エントリアドレス-0x20
 .int 0x00
 # +32 : heap領域(malloc領域)開始アドレス
 .int 0x00

tinyos.c

void Main(void) {
 fin:
  __asm__("hlt\n\t");
  goto fin;
}

gdbを使い、正しく動いているのか確認してみた。
bootpackへのジャンプ
 ljmpl $2*8, $0x0000001b
の実行前に 0x00280000番地からメモリをダンプして bootpackの内容になっていることを確認後、 ステップ実行して、0x1bにジャンプし、 0番地からのメモリをダンプしてみたが、 bootpackの内容にはなっていなかった。
ljmplで0x1bにジャンプしても、0x00280000番地が0番地に割り当てられていないようである。

いろいろ調べてみたが、原因が分からず、今だC言語で書いた関数の呼び出しができない。

Ubuntuの64bit環境だと、いろいろと手間取ることが多いので、Debianの32bit環境で試してみよう。

参考

2014年9月22日月曜日

[OS作成]30日でできる!OS自作入門 3日目 (6)

やっとOS本体のコードを実行できた。
前回まで動かなかった原因は、1セクタ分しかメモリに読み込んでおらず、 OS本体が読み込まれていなかったことが原因だった。 こんなコードを書いていた。
 .set MAX_SECTOR, 18 # 最大セクタ数
..略..
 add $1, %cl
 cmp MAX_SECTOR, %cl ←◆ここ
 jbe readloop # セクタは 1 〜 MAX_SECTOR
これでMAX_SECTOR(18)とCLレジスタを比較しているつもりだったが、 これは間違いだった。 即値と比較するには、以下のコードのように識別子の頭に$を付けなければならなかった。 ($を付けないと、間接アドレシングになるのかな?)
 .set MAX_SECTOR, 18 # 最大セクタ数
..略..
 add $1, %cl
 cmp $MAX_SECTOR, %cl ←◆$を付ける
 jbe readloop # セクタは 1 〜 MAX_SECTOR
同様にして、以下の識別子を参照している箇所は、即値となるように$を付けた。
 .set MAX_RETRY, 5 # 再読み込み最大回数
 .set MAX_SECTOR, 18 # 最大セクタ数
 .set MAX_HEAD, 2 # 最大ヘッド数
 .set MAX_CYLINDER, 10 # 最大シリンダ数
イメージを作成して、qemuで動作確認すると、画面は黒くなり、ビデオモードの変更を確認できた。
ここまでのコードは以下の通り。

ipl.s

/*
 ipl.s
*/
 .text 
 .code16
 jmp entry
 .byte 0x90
 .ascii "TINY_IPL" # ブートセクタの名前
 .word 512  # 1セクタのバイト数
 .byte 1  # クラスタの数
 .word 1  # FAT開始セクタ
 .byte 2  # FATの個数
 .word 224  # ルートディレクトリ領域のエントリ数
 .word 2880  # ドライブのセクタ数
 .byte   0xF0  # メディアタイプ
 .word 9  # FAT領域のセクタ数
 .word 18  # 1トラックのセクタ数
 .word 2  # ヘッド数
 .int 0  # ?
 .int 2880  # ドライブのセクタ数
 .byte 0, 0, 0x29 # ?
 .int 0xFFFFFFFF # ボリュームシリアル番号
 .ascii "TINY-OS    " # ディスクの名前
 .ascii "FAT12   " # フォーマットの名前
 .space 18

// プログラム
 .set MAX_RETRY, 5 # 再読み込み最大回数
 .set MAX_SECTOR, 18 # 最大セクタ数
 .set MAX_HEAD, 2 # 最大ヘッド数
 .set MAX_CYLINDER, 10 # 最大シリンダ数

entry:
 movw $0, %ax
 movw %ax, %ss
 movw $0x7C00, %sp
 movw %ax, %ds

 // 2セクタから1セクタ分読み込む
 movw $0x0820, %ax
 movw %ax, %es
 movb $0, %ch  # シリンダ0
 movb $0, %dh  # ヘッド0
 movb $2, %cl  # セクタ2

readloop: 
 movw $0, %si  # 失敗回数

retry:
 movb $0x02, %ah # Read sector(s) into memory
 movb $1, %al  # 1セクタ読み込む
 movw $0, %bx  # ES:BX Data buffer(0x8200に読み込む)
 movb $0x00, %dl # Aドライブ
 int $0x13  # BIOS interrupt call
 jnc next  # 読み込みOK でnextへ

 add $1, %si
 cmp $MAX_RETRY, %si
 jae error  # SI >= MAX_RETRY でerrorへ

 movb $0x00, %ah # Reset disk system
 movb $0x00, %dl # Aドライブ
 int $0x13
 jmp retry

next:
 movw %es, %ax # ES = ES + 0x20(512バイト)
 add $0x20, %ax
 movw %ax, %es
 // セクタ
 add $1, %cl
 cmp $MAX_SECTOR, %cl
 jbe readloop # セクタは 1 〜 MAX_SECTOR
 movb $1, %cl
 // ヘッド
 add $1, %dh
 cmp $MAX_HEAD, %dh
 jb readloop # ヘッドは 0 〜 MAX_HEAD - 1
 movb $0, %dh
 // シリンダ
 add $1, %ch
 cmp $MAX_CYLINDER, %ch
 jb readloop # シリンダは 0 〜 MAX_CYLINDER - 1
 jmp 0xC200  # 0x8000 + 0x4200 = 0xC200
 
error: 
 movw $msg, %si
putloop:
 movb (%si), %al
 add $1, %si
 cmp $0, %al
 je fin  # メッセージの後ろの0x00で終了する
 movb $0x0E, %ah # Write Character in TTY Mode
 movw $15, %bx # カラーコード
 int $0x10  # BIOS interrupt call
 jmp putloop
fin:
 hlt
 jmp fin

// メッセージ 
msg: 
 .string "\n\nload error\n"

 .org 0x1FE
 .byte 0x55, 0xAA # 55AAでブートセクタ

2014年9月21日日曜日

[OS作成]30日でできる!OS自作入門 3日目 (5)

まだまだ3日目だ。

今度は、OS本体起動時に、ビデオモードを設定してみる。

tinyos.s

.text
.code16 
 movb $0x00, %ah
 movb $0x13, %al
 int $0x10
fin: 
 hlt
 jmp fin
正しく実行できれば、真っ暗な画面が表示されるはず。
だが、試してみると動かない。

原因としては、次ののどちらか。
jmp 0xC200 で 0xC200にジャンプしていない。
FDからOS本体のイメージを正しく読んでいない。

前日に、jmp 0xC200 で実行したところ、正しく動作せず、
jmp *0xC200 にしたら、見掛け上は動いているようだったので
安心していたが、このコードでは、
0xC200に書いてあるアドレスにジャンプする命令だったようだ。

ソースとリンカスクリプトを再チェック、ビルド方法の確認をしてみたが、今のところお手上げ。
仕様がないので、qemu上のコードをGDBでデバッグすることにした。

Makefile に デバッグ用のターゲットを追加する。

Makefile

debug:
 qemu-system-i386 -m 32 -localtime -vga std -fda ${image_file} \
  -gdb tcp::10000 \
  -S
GDBは、起動時に実行するスクリプトファイルを指定できる。
今後の開発で、多分、実行したいコマンドが増えると思うので、作成しておく。

gdb.scr

target remote localhost:10000
make debug で qemuを起動した後に、gdbを-xオプションでスクリプトファイルを指定して起動する。
$ gdb -x gdb.scr
ブレークポイントを0x7c00に設定する。
(gdb) b *0x7c00
Breakpoint 5 at 0x7c00
ブレークポイントまで実行する。
(gdb) c
Continuing.

Breakpoint 5, 0x00007c00 in ?? ()
止まった。 ジャンプする直前で止めたいので、lstファイルを確認すると、該当箇所は0x00acなので、 0x7cacにブレークポイントを設定し、continueする。
  80                            // シリンダ
  81 00a3 80C501                add     $1, %ch
  82 00a6 3A2E0A00              cmp     MAX_CYLINDER, %ch
  83 00aa 72B9                  jb      readloop        # シリンダは 0 〜 MAX_CYLINDER - 1
  84 00ac E90000                jmp     0xC200          # 0x8000 + 0x4200 = 0xC200  ← ◆ここ
(gdb) b *0x7cac
Breakpoint 2 at 0x7cac
(gdb) c
Continuing.

Breakpoint 2, 0x00007cac in ?? ()
レジスタ値を確認する。
(gdb) i all-registers 
eax            0x840 2112
ecx            0x101 257
edx            0x0 0
ebx            0x0 0
esp            0x7c00 0x7c00
ebp            0x0 0x0
esi            0x0 0
edi            0x0 0
eip            0x7cac 0x7cac
eflags         0x202 [ IF ]
cs             0x0 0
ss             0x0 0
ds             0x0 0
es             0x840 2112 ← ◆これ
fs             0x0 0
gs             0x0 0
あれ?ESレジスタが1回分しか0x20が加算されておらず、0x840になっている。 停止させたアドレスが間違っているのかと思って、continueしたけど
(gdb) c
Continuing.
このまま止まらなかった。 イメージ読み込みがうまく動いていないのか。
ソースをチェックすることにしよう...

参考

[OS作成]30日でできる!OS自作入門 3日目 (4)

いよいよOS本体を起動する処理の作成だ。

IPLとOS本体を含むイメージファイルを作成するには、筆者作成のedimg.exeを使うと簡単であるが、ここ  や ここ  を参考にすると、UbuntuやDebianのmtoolsパッケージに含まれるmformatコマンドを使ってFDイメージを作成しているので、同様の手順で作成することにした。

mformatコマンドを使うとイメージファイルを次のコマンドで作成できる。
$ mformat -f 1440 -B ブートセクタファイル -C -i イメージファイル名 ::
ipl.binはブートセクタのファイルをBオプションで指定できる。
なお、man mformatを見ても、iオプションの説明は無かった。
OS本体をイメージファイルにコピーするには、
$ mcopy OS本体 -i イメージファイル名 ::
とする。

それぞれのコマンドの後ろにtarget directoryとして :: としているが、イメージファイルの場合は、これを指定するようだ。

Makefileは以下のようになった。

Makefile

image_file=tinyos.img

ipl.bin: ipl.s lnk.ls
    gcc -nostdlib -o $@ -Tlnk.ls ipl.s
    gcc -Tlnk.ls -c -g -Wa,-a,-ad ipl.s > ipl.lst

tinyos.sys: tinyos.s lnk.ls
    gcc -nostdlib -o $@ -Tlnk.ls tinyos.s
    gcc -Tlnk.ls -c -g -Wa,-a,-ad tinyos.s > tinyos.lst

image_file: ipl.bin tinyos.sys
    mformat -f 1440 -B ipl.bin -C -i ${image_file} ::
    mcopy tinyos.sys -i ${image_file} ::

img:
    make image_file

run:
    qemu-system-i386 -m 32 -localtime -vga std -fda ${image_file}

書籍通りに、まずは簡単なコードで確認する。

tinyos.s

fin:   
    hlt
    jmp fin

make img でイメージを作成してみる。

なお、tinyos.sのリスティングファイル(アセンブル結果)は以下の通り。

tinyos.lst

   1                  fin:   
   2 0000 F4               hlt
   3 0001 EBFD             jmp fin

書籍の通りに、0x2600にファイル情報がある。
00002600: 5449 4e59 4f53 2020 5359 5320 1800 e9b5  TINYOS  SYS ....
00002610: 3445 3445 0000 e9b5 3445 0200 0300 0000  4E4E....4E......
0x4200からOS本体のコードがある。
00004200: f4eb fd00 0000 0000 0000 0000 0000 0000  ................
イメージを0x8000から読み込ませているので、
イメージの0x4200からのコードを実行するには、
0x8000 + 0x4200 = 0xC200 になる。

ipl.s を修正して、FDの読み込み処理後に
    jmp    0xC200        # 0x8000 + 0x4200 = 0xC200
を追加した。

IPL用のリンカスクリプトとは別に、OS本体用のリンカスクリプトも必要だ。

tinyos_lnk.ls

OUTPUT_FORMAT("binary");

SECTIONS {
     . = 0xC200;
}
Makefileを、これに合わせて修正する。
image_file=tinyos.img

ipl.bin: ipl.s lnk.ls
 gcc -nostdlib -o $@ -Tipl_lnk.ls ipl.s
 gcc -Tipl_lnk.ls -c -g -Wa,-a,-ad ipl.s > ipl.lst

tinyos.sys: tinyos.s lnk.ls
 gcc -nostdlib -o $@ -Ttinyos_lnk.ls tinyos.s
 gcc -Ttinyos_lnk.ls -c -g -Wa,-a,-ad tinyos.s > tinyos.lst

image_file: ipl.bin tinyos.sys
 mformat -f 1440 -B ipl.bin -C -i ${image_file} ::
 mcopy tinyos.sys -i ${image_file} ::

img:
 make image_file

run:
 qemu-system-i386 -m 32 -localtime -vga std -fda ${image_file}

だんだん汚なくなってきた。そのうち綺麗に書き直そう。

make imgしてイメージを作成後、qemuで試してみる。
が、正しく動かない。load errorと出てしまう。
JMPがすっとばされているのか?
リスティングファイルを確認する。

ipl.lst

  83 00ac E90000                jmp     0xC200          # 0x8000 + 0x4200 = 0xC200
どこにもC200が無い。これでは駄目だ。

いろいろ調べて、試行錯誤してみると、

ipl.s

    jmp    *0xC200        # 0x8000 + 0x4200 = 0xC200

ipl.lst

  83 00ac FF2600C2              jmp     *0xC200         # 0x8000 + 0x4200 = 0xC200
うーむ。*0xC200と書くのが正しいのか。
(実は、最初は指定アドレスは即値なので$0xC200と書いて怒られた。)
きちんとGASのリファレンスを読んだほうが良さそうだな。

これで試してみると、一応見かけ上は動いているみたい。
今日はここまで。

9/21 追記
この後、ビデオモードを変更する処理を書いてみたが、どうも上のコードでは、0xC200へjmpしていないようだ。
0xC200へJMPさせるには、どう書けば良いのか?うーむ。

参考


2014年9月20日土曜日

[OS作成]30日でできる!OS自作入門 3日目 (3)

課題は、18セクタ分を読み込む処理を書き,
その後に、それをベースに、10シリンダ分を読み込む処理を書くこと。
/*
 ipl.s
*/
 .code16
 jmp entry
 .byte 0x90
 .ascii "TINY_IPL" # ブートセクタの名前
 .word 512  # 1セクタのバイト数
 .byte 1  # クラスタの数
 .word 1  # FAT開始セクタ
 .byte 2  # FATの個数
 .word 224  # ルートディレクトリ領域のエントリ数
 .word 2880  # ドライブのセクタ数
 .byte   0xF0  # メディアタイプ
 .word 9  # FAT領域のセクタ数
 .word 18  # 1トラックのセクタ数
 .word 2  # ヘッド数
 .int 0  # ?
 .int 2880  # ドライブのセクタ数
 .byte 0, 0, 0x29 # ?
 .int 0xFFFFFFFF # ボリュームシリアル番号
 .ascii "TINY-OS    " # ディスクの名前
 .ascii "FAT12   " # フォーマットの名前
 .space 18

// プログラム
 .set MAX_RETRY, 5 # 再読み込み最大回数
 .set MAX_SECTOR, 18 # 最大セクタ数
 .set MAX_HEAD, 2 # 最大ヘッド数
 .set MAX_CYLINDER, 10 # 最大シリンダ数

entry:
 movw $0, %ax
 movw %ax, %ss
 movw $0x7C00, %sp
 movw %ax, %ds

 // 2セクタから1セクタ分読み込む
 movw $0x0820, %ax
 movw %ax, %es
 movb $0, %ch  # シリンダ0
 movb $0, %dh  # ヘッド0
 movb $2, %cl  # セクタ2

readloop: 
 movw $0, %si  # 失敗回数

retry:
 movb $0x02, %ah # Read sector(s) into memory
 movb $1, %al  # 1セクタ読み込む
 movw $0, %bx  # ES:BX Data buffer(0x8200に読み込む)
 movb $0x00, %dl # Aドライブ
 int $0x13  # BIOS interrupt call
 jnc next  # 読み込みOK でnextへ

 add $1, %si
 cmp MAX_RETRY, %si
 jae error  # SI >= MAX_RETRY でerrorへ

 movb $0x00, %ah # Reset disk system
 movb $0x00, %dl # Aドライブ
 int $0x13
 jmp retry

next:
 movw %es, %ax # ES = ES + 0x20(512バイト)
 add $0x20, %ax
 movw %ax, %es
 // セクタ
 add $1, %cl
 cmp MAX_SECTOR, %cl
 jbe readloop # セクタは 1 〜 MAX_SECTOR
 movb $1, %cl
 // ヘッド
 add $1, %dh
 cmp MAX_HEAD, %dh
 jb readloop # ヘッドは 0 〜 MAX_HEAD - 1
 movb $0, %dh
 // シリンダ
 add $1, %ch
 cmp MAX_CYLINDER, %ch
 jb readloop # シリンダは 0 〜 MAX_CYLINDER - 1

fin:
 hlt
 jmp fin

error: 
 movw $msg, %si
putloop:
 movb (%si), %al
 add $1, %si
 cmp $0, %al
 je fin  # メッセージの後ろの0x00で終了する
 movb $0x0E, %ah # Write Character in TTY Mode
 movw $15, %bx # カラーコード
 int $0x10  # BIOS interrupt call
 jmp putloop

// メッセージ 
msg: 
 .string "\n\nload error\n"

 .org 0x1FE
 .byte 0x55, 0xAA # 55AAでブートセクタ
プログラムにバグが無ければ、FDから読み込んだ (18 * 2 * 10 - 1) * 512 バイト分のデータが0x8200以降に格納されているはず。

FDを作って実機で動かせば、FDのデータを読み込んで様子が分かるのだろうけど、今のパソコンにFDDは付いていない。
qemuだけだと、正しく動いているのか、暴走しているのか良く分からない。
gdbでデバッグできるようだけど、まだ試していない。

今日はここまで。次はOS本体を書き始めるようだ。

2014年9月17日水曜日

[OS作成]30日でできる!OS自作入門 3日目 (2)

ファイル読み込みに失敗したら、最大5回リトライする処理。
読み込み失敗時は、ディスクシステムのリセットをするのがポイント。

最大リトライ回数の定義に、.setディレクティブを使ってみた。

変更部分は以下の通り。
// プログラム
 .set MAX_RETRY, 5 # 再読み込み最大回数

entry:
 movw $0, %ax
 movw %ax, %ss
 movw $0x7C00, %sp
 movw %ax, %ds

 // 2セクタから1セクタ分読み込む
 movw $0x0820, %ax
 movw %ax, %es
 movb $0, %ch  # シリンダ0
 movb $0, %dh  # ヘッド0
 movb $2, %cl  # セクタ2

 movw $0, %si  # 失敗回数

retry:
 movb $0x02, %ah # Read sector(s) into memory
 movb $1, %al  # 1セクタ読み込む
 movw $0, %bx  # ES:BX Data buffer(0x8200に読み込む)
 movb $0x00, %dl # Aドライブ
 int $0x13  # BIOS interrupt call
 jnc fin  # 読み込みOK でfinへ

 add $1, %si
 cmp MAX_RETRY, %si
 jae error  # SI >= MAX_RETRY でerrorへ

 movb $0x00, %ah # Reset disk system
 movb $0x00, %dl # Aドライブ
 int $0x13
 jmp retry

fin:
 hlt
 jmp fin

一応、うまく動作しているようだ。

2014年9月16日火曜日

[OS作成]30日でできる!OS自作入門 3日目 (1)

次の課題は、ブートセクタの次のセクタを読み込む処理。

シリンダ、ヘッド、セクタを指定して int 0x13 を呼び出す。
エラー発生時はCF=1となるので、その場合はエラーメッセージを表示する。

今日からイメージファイル名をtinyos.imgにした。
リンカスクリプトは変更なし。

Makefile

image_file=tinyos.img

ipl.bin: ipl.s lnk.ls
 gcc -nostdlib -o $@ -Tlnk.ls ipl.s
 gcc -Tlnk.ls -c -g -Wa,-a,-ad ipl.s > ipl.lst

zero.img:
# 1474560 - 512 = 1474048
 dd if=/dev/zero of=zero.img ibs=512 count=2879

image_file: ipl.bin zero.img
 cat ipl.bin zero.img > ${image_file}

img:
 make image_file

run:
 qemu-system-i386 -m 32 -localtime -vga std -fda ${image_file}

ipl.s

/*
 ipl.s
*/
 .code16
 jmp entry
 .byte 0x90
 .ascii "TINY_IPL" # ブートセクタの名前
 .word 512  # 1セクタのバイト数
 .byte 1  # クラスタの数
 .word 1  # FAT開始セクタ
 .byte 2  # FATの個数
 .word 224  # ルートディレクトリ領域のエントリ数
 .word 2880  # ドライブのセクタ数
 .byte   0xF0  # メディアタイプ
 .word 9  # FAT領域のセクタ数
 .word 18  # 1トラックのセクタ数
 .word 2  # ヘッド数
 .int 0  # ?
 .int 2880  # ドライブのセクタ数
 .byte 0, 0, 0x29 # ?
 .int 0xFFFFFFFF # ボリュームシリアル番号
 .ascii "TINY-OS    " # ディスクの名前
 .ascii "FAT12   " # フォーマットの名前
 .space 18

// プログラム
entry:
 movw $0, %ax
 movw %ax, %ss
 movw $0x7C00, %sp
 movw %ax, %ds

 // 2セクタから1セクタ分読み込む
 movw $0x0820, %ax
 movw %ax, %es
 movb $0, %ch  # シリンダ0
 movb $0, %dh  # ヘッド0
 movb $2, %cl  # セクタ2

 movb $0x02, %ah # ディスク読み込み
 movb $1, %al  # 1セクタ読み込む
 movw $0, %bx  # ES:BX Data buffer(0x8200に読み込む)
 movb $0x00, %dl # Aドライブ
 int $0x13  # BIOS interrupt call
 jc error

fin:
 hlt
 jmp fin

error: 
 movw $msg, %si
putloop:
 movb (%si), %al
 add $1, %si
 cmp $0, %al
 je fin  # メッセージの後ろの0x00で終了する
 movb $0x0E, %ah # Write Character in TTY Mode
 movw $15, %bx # カラーコード
 int $0x10  # BIOS interrupt call
 jmp putloop

// メッセージ 
msg: 
 .string "\n\nload error\n"

 .org 0x1FE
 .byte 0x55, 0xAA # 55AAでブートセクタ

$ make img
$ make run

で実行したが、エラーメッセージは出ていない。
指定したセクタを正しく読めているようであるが、本当にそうなのかは、画面からは分からない。

今日はここまで。

2014年9月15日月曜日

[OS作成]30日でできる!OS自作入門 2日目 (2)

書籍では、BIOSの解説ページ
http://community.osdev.info/?(AT)BIOS
が紹介されているが、SPAM業者?か何かに書き換えられているようで、
変なリンクがたくさん表示されおり、肝心の情報は何もない。

うーむ。どこを見れば良いかな?
探してみた。

このページが良いかな?
Interrupt Jump Table

知りたい割り込み番号をクリックすると詳細を表示できる。
例えば、今回、使っている int $0x10 は、
http://www.ctyme.com/intr/int-10.htm
であり、AX=$0x0Eについては、
http://www.ctyme.com/intr/rb-0106.htm
が、詳細情報である。

書籍と同様に、開発しやすくなるように、Makefileを作ることにしよう。

疑問点が一つある。
ブートセクタ以降に、以下のコードを書いていたが、これは何だろう。
以下のように編集して、ブートセクタ以降を全て0x00にしたイメージを使い、qemuで動作確認してみたが、問題なく動いた。
 .org 0x1FE
 .byte 0x55, 0xAA # 55AAでブートセクタ

// ブートセクタ以降
 
// .byte 0xF0, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00
 .byte 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
 .space 4600
// .byte 0xF0, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00
 .byte 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
 .space 1469432
とりあえず気にせずに、このまま進めよう。

作成したMakefileは以下の通り。
image_file=helloos.img

ipl.bin: ipl.s lnk.ls
 gcc -nostdlib -o $@ -Tlnk.ls ipl.s
 gcc -Tlnk.ls -c -g -Wa,-a,-ad ipl.s > ipl.lst

zero.img:
# 1474560 - 512 = 1474048
 dd if=/dev/zero of=zero.img ibs=512 count=2879

image_file: ipl.bin zero.img
 cat ipl.bin zero.img > ${image_file}

img:
 make image_file

run:
 qemu-system-i386 -m 32 -localtime -vga std -fda ${image_file}

リスティングファイル(アセンブル結果)を出力するには、gccの-Wa,-aオプションを渡して、asコマンドのリスティング出力を有効にする。

書籍では、独自コマンドを使って、イメージを作成していたが、
catコマンドで
(1)gccで作成した512バイトのブートセクタイメージ
(2)1.44Mバイト-512バイト(ブートセクタ分)のサイズを0で埋めたファイル
を連結して作成している。


これで、次のターゲットを利用できるようになった。

イメージファイルを作成する。
$ make img

作成したイメージをqemuを起動する。
$ make run

参考


[OS作成]30日でできる!OS自作入門 2日目 (1)

今度はプログラム部分を分かり易く置き換えていく。

helloos3.s

/*
 helloos3.s
*/
 .code16
 jmp entry
 .byte 0x90
 .ascii "HELLOIPL" # ブートセクタの名前
 .word 512  # 1セクタのバイト数
 .byte 1  # クラスタの数
 .word 1  # FAT開始セクタ
 .byte 2  # FATの個数
 .word 224  # ルートディレクトリ領域のエントリ数
 .word 2880  # ドライブのセクタ数
 .byte   0xF0  # メディアタイプ
 .word 9  # FAT領域のセクタ数
 .word 18  # 1トラックのセクタ数
 .word 2  # ヘッド数
 .int 0  # ?
 .int 2880  # ドライブのセクタ数
 .byte 0, 0, 0x29 # ?
 .int 0xFFFFFFFF # ボリュームシリアル番号
 .ascii "HELLO-OS   " # ディスクの名前
 .ascii "FAT12   " # フォーマットの名前
 .space 18

// プログラム
entry:
 movw $0, %ax
 movw %ax, %ss
 movw $0x7C00, %sp
 movw %ax, %ds
 movw %ax, %es

 movw $msg, %si
putloop:
 movb (%si), %al
 add $1, %si
 cmp $0, %al
 je fin  # メッセージの後ろの0x00で終了する
 movb $0x0E, %ah # Write Character in TTY Mode
 movw $15, %bx # カラーコード
 int $0x10  # BIOS interrupt call
 jmp putloop
fin:
 hlt
 jmp fin

// メッセージ 
msg: 
 .string "\n\nhello, world\n"

 .org 0x1FE
 .byte 0x55, 0xAA # 55AAでブートセクタ

// ブートセクタ以降
 
 .byte 0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00
 .space 4600
 .byte 0xF0, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00
 .space 1469432
起動時はリアルモードなので、.code16で、16bitコードを生成するように指定する。

ELFヘッダをスキップしたイメージを作るには、リンカスクリプトを使えば良いことが分かった。
NASMのORGに相当するものはGASには無いので、ORGで指定するアドレスはリンカスクリプトで設定する。

lnk.ls

OUTPUT_FORMAT("binary");

IPL_BASE = 0x7C00;

SECTIONS {
  . = IPL_BASE;
}
今までは、asコマンドとddコマンドでイメージを作成していたが、これからは、gccコマンドでイメージを作成できる。
$ gcc -nostdlib -o helloos3.img -Tlnk.ls helloos3.s
今まで通り、qemuで確認してOK。
$ qemu-system-i386 -m 32 -localtime -vga std -fda helloos3.img
ここを参考にすると、リンカスクリプトを
OUTPUT_FORMAT("binary");
IPLBASE = 0x7c00;

SECTIONS {
        . = IPLBASE;
        .text        : {*(.text)}
        .data        : {*(.data)}
        . = IPLBASE + 510;
        .sign        : {SHORT(0xaa55)}
}
のように書くと 0x1FEからの0xAA,0x55も定義できるようだけど、試していない。
今のところ、.text, .dataの設定は不要だった。
そのうち必要になるのかな?

Z80のアセンブラには慣れているせいか、GASの標準であるAT&T構文ではsourceとdestinationが逆なのは、ちょっと気持ち悪く感じる。

参考


2014年9月14日日曜日

[OS作成]30日でできる!OS自作入門 1日目 (3)

書籍と同様に、ディレクティブを使って、もう少し分かりやすく書き直した。
/*
 helloos2.s
*/
 .byte 0xEB, 0x4E, 0x90
 .ascii "HELLOIPL" # ブートセクタの名前
 .word 512  # 1セクタのバイト数
 .byte 1  # クラスタの数
 .word 1  # FAT開始セクタ
 .byte 2  # FATの個数
 .word 224  # ルートディレクトリ領域のエントリ数
 .word 2880  # ドライブのセクタ数
 .byte   0xF0  # メディアタイプ
 .word 9  # FAT領域のセクタ数
 .word 18  # 1トラックのセクタ数
 .word 2  # ヘッド数
 .int 0  # ?
 .int 2880  # ドライブのセクタ数
 .byte 0, 0, 0x29 # ?
 .int 0xFFFFFFFF # ボリュームシリアル番号
 .ascii "HELLO-OS   " # ディスクの名前
 .ascii "FAT12   " # フォーマットの名前
 .space 18

// プログラム

 .byte 0xB8, 0x00, 0x00, 0x8E, 0xD0, 0xBC, 0x00, 0x7C
 .byte 0x8E, 0xD8, 0x8E, 0xC0, 0xBE, 0x74, 0x7C, 0x8A
 .byte 0x04, 0x83, 0xC6, 0x01, 0x3C, 0x00, 0x74, 0x09
 .byte 0xB4, 0x0E, 0xBB, 0x0F, 0x00, 0xCD, 0x10, 0xEB
 .byte 0xEE, 0xF4, 0xEB, 0xFD

// メッセージ 

 .string "\n\nhello, world\n"

 .org 0x1FE
 .byte 0x55, 0xAA # 55AAでブートセクタ

// ブートセクタ以降
 
 .byte 0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00
 .space 4600
 .byte 0xF0, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00
 .space 1469432
動作確認。
$ as --32 -o helloos2 helloos2.s
$ dd if=helloos2 of=helloos2.img skip=52 ibs=1 count=1474560
$ qemu-system-i386 -m 32 -localtime -vga std -fda helloos2.img

GASを使っていて、分かったこと

・コメントは3種類の書き方がある。
// コメント
/* コメント */
# コメント
・.stringディレクティブを使うと文字列の最後に0x00を付ける。
・.string,.asciiの文字列にはエスケープシーケンスが使える。

その他、分かったこと

・最初のセクタの最後の2バイトが0x55,0xAAの場合に、ブートセクタと判断し、プログラムを実行する。

ふーん。そういうふうに起動ディスクか判断していたのね。
ハードディスクの場合も同様かな?

やっと一日目が完了。先は長いな...

2014年9月13日土曜日

[OS作成]30日でできる!OS自作入門 1日目 (2)

数日経ったが、まだ1日目である。むぅぅぅ...

「30日でできる! OS自作入門」のサポートページにはLinux用のツールが公開されているが
筆者の独自ツールを使っているようだった。
あまり独自ツールは使いたくなかったので、いろいろと検索してみると、GASとGCCでも開発できるようだったので、
こちらを試してみた。
OS自作入門では、NASKというNASMベースのアセンブラを使っているが、GASとは構文が全く違うので、
書籍のコードを、いちいちGAS用に変換しなければならず、少し面倒だ。

NASM Intel構文
GAS AT&T構文

GASでは .intel_syntax ディレクティブを使うと、Intel構文にできるようであるが、とりあえずデフォルトのAT&T構文で開発を進めよう。
と言うことで、作成したのがこれ。
helloos1.s
.byte 0xEB, 0x4E, 0x90, 0x48, 0x45, 0x4C, 0x4C, 0x4F
.byte 0x49, 0x50, 0x4C, 0x00, 0x02, 0x01, 0x01, 0x00
.byte 0x02, 0xE0, 0x00, 0x40, 0x0B, 0xF0, 0x09, 0x00
.byte 0x12, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00
.byte 0x40, 0x0B, 0x00, 0x00, 0x00, 0x00, 0x29, 0xFF
.byte 0xFF, 0xFF, 0xFF, 0x48, 0x45, 0x4C, 0x4C, 0x4F
.byte 0x2D, 0x4F, 0x53, 0x20, 0x20, 0x20, 0x46, 0x41
.byte 0x54, 0x31, 0x32, 0x20, 0x20, 0x20, 0x00, 0x00

.space 0x10

.byte 0xB8, 0x00, 0x00, 0x8E, 0xD0, 0xBC, 0x00, 0x7C
.byte 0x8E, 0xD8, 0x8E, 0xC0, 0xBE, 0x74, 0x7C, 0x8A
.byte 0x04, 0x83, 0xC6, 0x01, 0x3C, 0x00, 0x74, 0x09
.byte 0xB4, 0x0E, 0xBB, 0x0F, 0x00, 0xCD, 0x10, 0xEB
.byte 0xEE, 0xF4, 0xEB, 0xFD, 0x0A, 0x0A, 0x68, 0x65
.byte 0x6C, 0x6C, 0x6F, 0x2C, 0x20, 0x77, 0x6F, 0x72
.byte 0x6C, 0x64, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00

.space 368

.byte 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0xAA
.byte 0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00

.space 4600

.byte 0xF0, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00

.space 1469432
参考にしたBlogでは .skipディレクティブを使っていたが、GASのマニュアルを参照すると、

.skip
.skip is recognized on the 680x0 platform as a synonym for .space.
.space
Syntax: .space size[, fill]
This directive emits size bytes, each of value fill. Both size and fill are absolute expressions. If the comma and fill are omitted, fill is assumed to be zero.

ということなので、RESBに対応するディレクティブは.skipではなく、.spaceを使用した。

$ as --32 -o helloos1 helloos1.s

--32 を付けて 32ビットコードを生成するようにした。

30日でできる!OS自作入門を Linux & GAS で行う (1日目) を参考に、同様にして、ELFヘッダのサイズを調べる。
$ readelf -h helloos1
ELF ヘッダ:
  マジック:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 
  クラス:                            ELF32
  データ:                            2 の補数、リトルエンディアン
  バージョン:                        1 (current)
  OS/ABI:                            UNIX - System V
  ABI バージョン:                    0
  型:                                REL (再配置可能ファイル)
  マシン:                            Intel 80386
  バージョン:                        0x1
  エントリポイントアドレス:               0x0
  プログラムの開始ヘッダ:          0 (バイト)
  セクションヘッダ始点:          1474664 (バイト)
  フラグ:                            0x0
  このヘッダのサイズ:                52 (バイト)  ← ◆これ
  プログラムヘッダサイズ:            0 (バイト)
  プログラムヘッダ数:                0
  セクションヘッダ:                  40 (バイト)
  セクションヘッダサイズ:            7
  セクションヘッダ文字列表索引:      4

ヘッダのサイズが52バイトなので、ddコマンドで52バイトスキップしてイメージを作成する。
$ dd if=helloos1 of=helloos1.img skip=52 ibs=1 count=1474560
1474560+0 レコード入力
2880+0 レコード出力
1474560 バイト (1.5 MB) コピーされました、 7.76736 秒、 190 kB/秒
最初に作った helloos0.imgと比較する。
$ cmp helloos0.img helloos1.img

qemuで動作確認。
$ qemu-system-i386 -m 32 -localtime -vga std -fda helloos1.img
違いが無いので、当然動く。問題無し。

GASやGCCでの開発方法をBlogで公開してくださった方々に感謝。

参考


2014年9月11日木曜日

[OS作成]30日でできる!OS自作入門 1日目 (1)

書籍ではWindowsを使っているが、
VirtualBox上のUbuntu14.04で開発することにした。

最初はバイナリエディタを使って、FDイメージを作成し、このディスクを使ってマシンを起動すると、
hello, world!
を表示できるようにするのが課題。

VirutalBoxでもqemuは動くのだろうか。
まずはqemuをインストールだ。

$ sudo apt-get install qemu

コンソールベースのバイナリエディタは何か良いのないかなと探してみたら、
Emacsのhexl-modeが、それらしい。
さっそく試してみる。

$ echo "hoge" > hoge.txt

emacs から M-x hexl-find-file で hoge.txt を開く。



標準で、こんな機能があったのか。
もっと早く知りたかった...

FDの容量の1.44MBまで00を埋めるのは面倒なので、ddコマンドで作ってしまおう。

$ dd ibs=1024 count=1440 if=/dev/zero of=helloos0.img
1440+0 レコード入力
2880+0 レコード出力
1474560 バイト (1.5 MB) コピーされました、 0.0289785 秒、 50.9 MB/秒

Emacsでhelloos.imgを開いて、hexl-modeで編集する。
このhexl-modeは1バイトをhexで入力するには、いちいちC-M-xで入力しないと駄目なのね...
面倒くさいと思いつつ、書籍の通りに入力。
ダンプリストを打ち込むなんて何年ぶりだろう。



qemuで起動してみる。

$ qemu-system-i386 -m 32 -localtime -vga std -fda helloos0.img



動いた!
hello, worldが表示された。

参考

バイナリファイルを編集するには

2014年9月10日水曜日

[OS作成]30日でできる!OS自作入門 0日目

30日でできる!OS自作入門という書籍がある。
発売当初は気になっていたのだけど、すっかり存在を忘れていた。
先日、マイナビ Booksを見ていたら、電子書籍化されていることが分かり、
また、キャンペーンらしく、少し安くなっていたので、購入してみた。

書籍紹介の内容を引用すると
プログラミングの基礎からはじめて、30日後にはウィンドウシステムを有する32bitマルチタスクOSをフルスクラッチで作り上げるという入門書。
一からOSを作ろうという内容。

章立ては Chapter0(0日目) から Chapter31(31日目) まである。
まずは、最初の0日目からスタート。

OSとは何?
何を作っていくかの説明。

文体はくだけた感じで、気軽に読める。
最初はアセンブラを使っていくようだ。
アセンブラを使うのは久しぶりだ。

投げ出さずに最後まで行けるかな?
mrubyTinySchemeを動くようにすると楽しそうだ。

参考

30日でできる!OS自作入門