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環境で試してみよう。

参考