LoginSignup
9
8

More than 3 years have passed since last update.

UnixライクなOSを作成する.ブート編(その1)

Last updated at Posted at 2018-09-27

メモ用

設計

  • x86アーキテクチャを対象にする。
  • C言語(C11) + GNU Assembler(GAS/AT&T)を使用する。
  • Legacy BIOSを用いてHDDイメージ(.img)からのbootを想定する。
  • カーネル部分のフォーマットは(現段階では)ELFを採用する。
  • Bochs上での動作確認を行う。
  • https://github.com/mix64/mixv6

※注意※
Legacy BIOSは2020年でサポートが終了します。
http://www.uefi.org/sites/default/files/resources/Brian_Richardson_Intel_Final.pdf

今回はBochs上でのみ動作させる予定なのでUEFIに対応する予定はありませんが、
実機で動かしたい場合などはUEFIで作成したほうが良いかもしれません。

参考になるサイト

  • OSDev.org ... OS開発では定番のサイトらしい。情報が英語な点を除けばほぼ全てを網羅していて完璧。わからなかった時はまずここを見る。
  • 0から作るソフトウェア開発 ... 調べた感じだと、日本語のサイトではここが一番丁寧だった。メモリマップやIA32(x86)汎用命令一覧もある。
  • xv6-public ... MITが開発した教育用のOS(x86)。教育用と言ってもチープなものではなく、UnixV6をベースにマルチタスクやファイル管理、コンソールやパイプラインなどの基本的な要素は網羅している。今回のブートプロセスはここから該当部分を取って来て改変したものである。
  • Intel® 64 and IA-32 architectures software developer's manuals ... Intelの鈍器。全てがここに載っている。5000ページほどの英文による解説書。

最初の一歩

PCの電源ボタンを押すと、ROMに格納されたBIOSが起動します。
BIOSは二次記憶デバイスを順番に見て行き、最初のセクタ(512バイト)の末尾2バイトが"0x55AA"である場合、そのデバイスがブート可能なデバイスと判断します。

その後ブート可能なデバイスの最初のセクタ(ブートセクタ/ブートブロック/IPL/MBRとも言う?)をメモリ上の0x7C00に読み出し、プログラムカウンタを0x7C00にセットします。そしてここからブートが始まっていきます。

なぜ0x7C00にロードされるのかはここが丁寧に解説しています。
Assembler/なぜx86ではMBRが"0x7C00"にロードされるのか?(完全版)

ブートセクタを作る

さてここからブートセクタの中身を書いていきます。
序盤ではC言語では使えない命令を多用するので基本的にx86アセンブリで書いていきます。
x86アセンブリには複数種類があるのですが主にNASM(Netwide Asemmbler)かGAS(GNU Assembler)を使うことになると思います。

アセンブリの構文には主に2種類あり、AT&T構文とIntel構文に分けられます。
参考リンク:アセンブラに手を出してみる

デフォルトではNASMはIntel構文で、GASはAT&T構文となっています。
AT&T構文はヘイトが高いイメージがあって人気がないのか、GASでは最近Intel構文もサポートされるようになりました。
私はAT&T構文の方が慣れているので今回はAT&T構文を採用します。

余談ですが、ここでは
言語を"アセンブリ"、
コンパイラを"アセンブラ"、
コンパイルを"アセンブル"
としています。

何はともあれ習うより慣れろ。まずは最初の部分を書いてみましょう。

bootasm.S
#include "asm.h"

.code16                       # Assemble for 16-bit mode
.globl start
start:
  cli
  # Zero data segment registers DS, ES, and SS.
  xorw    %ax, %ax             # Set %ax to zero
  movw    %ax, %ds             # -> Data Segment
  movw    %ax, %es             # -> Extra Segment
  movw    %ax, %ss             # -> Stack Segment

asm.hは後で解説します。
起動時は過去のプロセッサとの互換性のために16ビットモードで動作しています。
.code16 というのは16ビットモードでアセンブルしてもらうための記述子といった感じでしょうか?
次にcli命令で割り込みを防止しています。起動時にキーボード割り込みとかされてもしょうがないですからね。
次にレジスタを初期化します。xor命令で0にセットするのは有名なテクニックですね。
mov命令より早いらしいですが、正直よくわからないです()

A20ゲートの開放

A20ゲートというものが出てきました。
これは簡単に言っちゃうとアドレスバスの20番目以降を使用可能にするという事です。
CPUからメモリにアクセスする際にアドレスを渡すためのワイヤですね。
参考:http://caspar.hazymoon.jp/OpenBSD/annex/gate_a20.html

まぁ開放の仕方はいろいろあるのですが、今回はxv6を真似てキーボードコントローラから開放します。

bootasm.S(続き)
seta20.1:
  inb     $0x64, %al               # Wait for not busy
  testb   $0x2, %al
  jnz     seta20.1

  movb    $0xd1, %al               # 0xd1 -> port 0x64
  outb    %al, $0x64

seta20.2:
  inb     $0x64, %al               # Wait for not busy
  testb   $0x2, %al
  jnz     seta20.2

  movb    $0xdf, %al               # 0xdf -> port 0x60
  outb    %al, $0x60

in命令とout命令が出てきました。
先程のcli命令もそうですが、これらの命令はC言語で書くことが出来ません。

CPUには沢山のデバイスが接続されています。そのデバイスがCPUのどこに接続されているかを抽象化するために"ポート"という概念が生まれました。特に今回のようなポートは"I/Oポート"などと呼ばれますね。

UnixV6が使われていた時代、PDP-11などではメモリマップドI/Oというシステムで、メモリの番地にアクセスするようなコードで、I/Oデバイスのレジスタにアクセス出来るようなものも存在します。

x86はI/OポートマップドI/O(または単にI/Oポート?)、メモリマップドI/Oどちら使用でき、IOポートでが指定のポート番号に対してin/out命令を行うことで周辺デバイスのレジスタにアクセスすることが出来ます。

ちなみにこのポートマッピングは標準化されている(?)ので、一覧表があるはずです。
なぜか見つからないので結構苦労しているので、知ってる人がいたらぜひ教えて下さい。
追記:非公式ですが、ここが見やすいようです。
http://bochs.sourceforge.net/techspec/PORTS.LST

脱線しました。
えーっとキーボードのIOポートは0x60,0x64が割り当てられています。
まぁ後はコメントの通りなんですが、キーボードがbusyじゃないときに指定のデータを書き込めばA20ゲートが開放されます。

GDTの設定

ここの解説がかなり丁寧なので丸投げします
http://softwaretechnique.jp/OS_Development/kernel_loader2.html

まぁ要するに、ページングを使ってメモリ管理するけど、セグメントも設定しないといけないからとりあえず作るよって感じです。基本的に使わないのでベースは0x00000000,リミットは0xffffffffで全範囲に設定します。

bootasm.S(末尾)
.p2align 2                                # force 4 byte alignment
gdt:
  SEG_NULLASM                             # null seg
  SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff)   # code seg
  SEG_ASM(STA_W, 0x0, 0xffffffff)         # data seg

gdtdesc:
  .word   (gdtdesc - gdt - 1)             # sizeof(gdt) - 1
  .long   gdt                             # address gdt
asm.h
#define SEG_NULLASM                                             \
        .word 0, 0;                                             \
        .byte 0, 0, 0, 0

/* The 0xC0 means the limit is in 4096-byte units */
/* and (for executable segments) 32-bit mode. */
#define SEG_ASM(type,base,lim)                                  \
        .word (((lim) >> 12) & 0xffff), ((base) & 0xffff);      \
        .byte (((base) >> 16) & 0xff), (0x90 | (type)),         \
                (0xC0 | (((lim) >> 28) & 0xf)), (((base) >> 24) & 0xff)

#define CR0_PE    0x00000001    /* Protection Enable */
#define SEG_KCODE 0x08          /* kernel code */
#define SEG_KDATA 0x10          /* kernel data+stack */

#define STA_X     0x8           /* Executable segment */
#define STA_W     0x2           /* Writeable (non-executable segments) */
#define STA_R     0x2           /* Readable (executable segments) */

上記のサイトと同じようにフォーマットを作成します。
とりあえずxv6ではNULLセグメント、コードセグメント、データセグメントを作成していました(丸パクリ)。
ちなみにアセンブリで出てくるgdt,gdtdescはラベルであり、アセンブル時に全部アドレスに変換されます。

さて、それではこのgdtdescを読み込んでいきましょう。

bootasm.S
  # Switch from real to protected mode.  Use a bootstrap GDT that makes
  # virtual addresses map directly to physical addresses so that the
  # effective memory map doesn't change during the transition.
  lgdt    gdtdesc
  movl    %cr0, %eax
  orl     $(CR0_PE), %eax
  movl    %eax, %cr0


//PAGEBREAK!
  # Complete the transition to 32-bit protected mode by using a long jmp
  # to reload %cs and %eip.  The segment descriptors are set up with no
  # translation, so that the mapping is still the identity mapping.
  ljmp    $(SEG_KCODE), $start32

.code32  # Tell assembler to generate 32-bit code now.
start32:
  # Set up the protected-mode data segment registers
  movw    $(SEG_KDATA), %ax       # Our data segment selector
  movw    %ax, %ds                # -> DS: Data Segment
  movw    %ax, %es                # -> ES: Extra Segment
  movw    %ax, %ss                # -> SS: Stack Segment
  xorw    %ax, %ax                # Zero segments not ready for use
  movw    %ax, %fs                # -> FS
  movw    %ax, %gs                # -> GS
  movl    $start, %esp            # Set up the stack pointer

  call    bootmain

  # If returns (it shouldn't), trigger a Bochs
  # breakpoint if running under Bochs, then loop.
  movw    $0x8a00, %ax            # 0x8a00 -> port 0x8a00
  movw    %ax, %dx
  outw    %ax, %dx
  movw    $0x8ae0, %ax            # 0x8ae0 -> port 0x8a00
  outw    %ax, %dx

spin:
  jmp spin

lgdt命令でGDTの読み込みをすることが出来ます。
さて、これで32ビットモードへの準備は整いました。あとはCPUの制御レジスタCR0のProtection Enableのビットをオンにしてやると32ビットモードに移行します。

通常のjmp命令は同じセグメント内でジャンプしますが、ljmp命令はセグメントを変えながらジャンプすることが出来ます。
jmp命令を行うことで16ビットモードでパイプラインされていた32ビットモードの命令がフラッシュされ、新規に32ビットモードとして32ビットモードの命令が実行されていきます。
コードセグメントへはljmpで値を入れたので、他のセグメントにも値を入れておきます。

$startは0x7C00のアドレスを指していますが、その上の部分(アドレスが若い方)には少しの未使用領域があるので、ブート中はそこをスタック領域として使用します。
メモリマップ:http://softwaretechnique.jp/OS_Development/kernel_loader1.html
alt

アセンブリでの記述はこれまで。call bootmainにより
次回ではC言語で記述されたbootmain関数を見ていきます。

あと一応何かしらのエラーが発生した場合、無限ループになるようにセットしています。

その他

えーっとですね、実は今回は自分が記述した部分は一つもありません()
全部xv6そのままです。まぁ(ブート部分は似たり寄ったりだから)、多少はね?
次回以降はちゃんと自分で書いてるんで許してくださいなんでも島村卯月がんばります!

9
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
8