ブートローダ編① bootasm.S (Xv6を読む~OSコードリーディング~)

前回
jupiteroak.hatenablog.com




bootasm.S
https://github.com/mit-pdos/xv6-public/blob/master/bootasm.S#L10

#include "asm.h"
#include "memlayout.h"
#include "mmu.h"

# Start the first CPU: switch to 32-bit protected mode, jump into C.
# The BIOS loads this code from the first sector of the hard disk into
# memory at physical address 0x7c00 and starts executing in real mode
# with %cs=0 %ip=7c00.

.code16                       # Assemble for 16-bit mode
.globl start
start:
  cli                         # BIOS enabled interrupts; disable

  # 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

  # Physical address line A20 is tied to zero so that the first PCs 
  # with 2 MB would run software that assumed 1 MB.  Undo that.
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

  # 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<<3), $start32

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

  # Set up the stack pointer and call into C.
  movl    $start, %esp
  call    bootmain

  # If bootmain 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

# Bootstrap GDT
.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


処理の内容

ハードウェア割り込みを禁止する

.code16
.globl start
 start:
 cli


.code16 
このソースファイルをアセンブルするGNU Assembler(gas)が16bit用の機械語コードを出力するように、.code16(code16ディレクティブ)を指定します。.code16(code16ディレクティブ)を指定することにより、指定箇所以降のアセンブリ言語命令は16bit用の機械語命令にアセンブルされるようになります。
GNU Assembler(gas)はデフォルトで32bitの機械語コードを出力しますが、レガシーBIOSの場合、x86CPUは16bitモード(リアルモードモード)で起動を開始します。そのため、GNU Assembler(gas)が16bit用の機械語コードを出力するように、.code16(code16ディレクティブ)を指定する必要があります。


.globl start
.globl(globlディレクティブ)を使って、startラベルを外部のファイルから参照できるようにします。


start:
cli命令が配置されているアドレスにstartラベル(シンボル名、名前)を付けます。


cli
リアルモードの間は周辺機器から割り込み信号が生じるので、cli命令によって割り込み信号を無効化します。

During the time the CPU remains in Real
Mode, IRQ0 (the clock) will fire repeatedly, and the hardware that is used to boot the PC (floppy, hard disk, CD, Network card, USB) will also generate IRQs.

https://wiki.osdev.org/Memory_Map_(x86)#Real_mode_address_space_.28.3C_1_MiB.29

CSレジスタ以外のセグメントレジスタ(DS, ES, SSレジスタ)を初期化する

  xorw    %ax,%ax             # Set %ax to zero
  movw    %ax,%ds             # -> Data Segment
  movw    %ax,%es             # -> Extra Segment
  movw    %ax,%ss             # -> Stack Segment


xorw %ax,%ax
第1オペランドのAXレジスタの値と第2オペランドのAXレジスタの値をXOR演算し、その結果を第2オペランドのAXレジスタにセットします。
第1オペランドのAXレジスタと第2オペランドのAXレジスタの各bit同士でXOR演算を行うので、第2オペランドのAXレジスタには0がセットされます。このAXレジスタにセットされた0の値を使って、DS, ES, SSレジスタを初期化していきます。


movw %ax,%ds 
AXレジスタからDSレジスタへ値をコピーします(AXレジスタにセットされた0の値でDSレジスタを初期化します)。
セグメント方式は利用していません(メモリ領域をコードセグメント、データセグメント、スタックセグメントに3分割するような使い方はしていません)。


movw %ax,%es 
AXレジスタからESレジスタへ値をコピーします(AXレジスタにセットされた0の値でESレジスタを初期化します)。


movw %ax,%ss 
AXレジスタからSSレジスタへ値をコピーします(AXレジスタにセットされた0の値でSSレジスタを初期化します)。
セグメント方式は利用していません(メモリ領域をコードセグメント、データセグメント、スタックセグメントに3分割するような使い方はしていません)。

A20アドレスピンを有効化する(プロテクトモード移行準備①)

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

プロテクトモードへ移行する準備として、KBC(キーボードコントローラ)からA20回路を制御し、A20アドレスピンを有効化します。
KBCは入力装置であるキーボードを制御する回路ですが、KBCの出力ポートの1つ(A20gate)がA20回路につながっているので、KBCからA20回路を制御することができます。

KBCへコマンドを送る

KBCの状態を確認する

seta20.1: 
inb命令が配置されているアドレスにseta20.1ラベル(シンボル、名前)を付けます。


inb $0x64,%al
in命令(ポート入力命令)でI/Oポートアドレス0x64を指定することにより、KBC(キーボードコントローラ)が持つステータスレジスタから1バイトの値を読み取ります。読み取ったKBCのステータスレジスタ1バイトの値はALレジスタにセットされます。
ステータスレジスタは、KBCの状態を示すために使用されているレジスタです。


testb $0x2,%al
第1オペランドの0x2と第2オペランドのALレジスタの値をAND演算します。演算結果が0の場合、EFLGASレジスタのZFフラグを1にセットします。
具体的には、0x2(0b 0000 0010)を使って、読み取ったKBCのステータスレジスタの値(ALレジスタにセットされいる)をマスク処理し、ステータスレジスタのbit1(入力バッファフル)の値を取り出しています。
演算結果が1の場合→ステータスレジスタのbit1(入力バッファフル)が1の場合は、EFLGASレジスタZFフラグは0のままです。
また、ステータスレジスタのbit1(入力バッファフル)が1の時は、KBCの入力バッファ内にデータがあることを示しています。
演算結果が0の場合→ステータスレジスタのbit1(入力バッファフル)が0の場合は、EFLGASレジスタZFフラグに1がセットされます。
また、ステータスレジスタのbit1(入力バッファフル)が0の時は、KBCの入力バッファ内にデータがないことを示しています。


jnz seta20.1
jnz命令は、EFLGASレジスタのZFフラグが0の時、第1オペランドで指定されたアドレスにジャンプする命令です。
seta20.1のように、jnz命令で参照されたラベル名(シンボル名)はそのラベルが付けられたアドレスの値になります。そのため、このjnz命令は、EFLGASレジスタのZFフラグが0の時、seta20.1のラベルが付けられたアドレスにジャンプする命令となります。
EFLGASレジスタZFフラグが0の場合→前のtest命令の演算結果が1の場合→ステータスレジスタのbit1(入力バッファフル)が1の場合は、KBCの入力バッファ内にデータがある状態なので、seta20.1ラベルが付けられたアドレスにジャンプし、KBCの入力バッファ内にデータがない状態になるまで一連の処理を繰り返します。
EFLGASレジスタZFフラグが1の場合→前のtest命令の演算結果が0の場合→ステータスレジスタのbit1(入力バッファフル)が0の場合は、KBCの入力バッファ内にデータがない状態なので、seta20.1ラベルが付けられたアドレスにジャンプせずに、次の処理へ進みます。

KBCのコマンドレジスタに書き込みを行う

movb $0xd1,%al 
0xd1をALレジスタにセットします。


outb %al,$0x64
out命令(ポート出力命令)でI/Oポートアドレス0x64を指定することにより、KBCが持つコマンドレジスタへの書き込みを行っています。
書き込む値はALレジスタにセットされた値0xd1です。
0xd1をコマンドレジスタに書き込んだ後に、KBCが持つ入力レジスタへ書き込んだ値が出力ポートの設定(A20gateの設定を含む)として反映されます。

KBCの出力ポートの設定を行う

KBCの状態を確認する

seta20.2: inb $0x64,%al 
inb $0x64,%al で説明した処理と同様です。


testb $0x2,%al
testb $0x2,%al で説明した処理と同様です。


jnz seta20.2
jnz seta20.1 で説明した処理と同様です。

KBCの入力レジスタに書き込みを行う

movb $0xdf,%al
0xdfをALレジスタにセットします。


outb %al,$0x60
out命令(ポート出力命令)でI/Oポートアドレス0x60を指定することにより、KBCが持つ入力レジスタへの書き込みを行っています。
この命令の前にコマンドレジスタに0xd1を書き込んでいるので、今回入力レジスタへ書き込んだ値は出力ポートの設定(A20gateの設定を含む)として反映されます。
入力レジスタに書き込む値はALレジスタにセットされた値0xdf(0b 1101 1111)なので、入力レジスタの各bitに対応するKBCの出力ポートの設定は次のようになります。

入力レジスタの各bit 対応するKBCの出力ポート 設定値
bit0 システムリセット信号線 1
bit1 A20gate信号線 1
bit2 マウスのクロック信号線 1
bit3 マウスのデータ信号線 1
bit4 キーボードの出力バッファフル信号線 1
bit5 マウスの出力バッファフル信号線 0
bit6 キーボードのクロック信号線 1
bit7 キーボードのデータ信号線 1

bit1に1をセットすることにより、A20ピンの値がそのままCPUに反映されます。

GDT・GDTRを設定する(プロテクトモード移行準備②)

プロテクトモードへ移行する準備として、GDT(グローバルディスクリプタテーブル)の設定とGDTR(グローバルディスクリプタテーブルレジスタ)の設定を行います。まず先に、このソースファイルの終わりに記述されているGDT(グローバルディスクリプタテーブル)の設定から説明します。

GDT(グローバルディスクリプタテーブル)の設定

.p2align 2 
gdt:
SEG_NULLASM 
SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff) 
SEG_ASM(STA_W, 0x0, 0xffffffff)

メインメモリ上にGDT(グローバルディスクリプタテーブル)を定義します。


.p2align 2 
.p2alignn(.p2alignディレクティブ)は、このディレクティブの直後にある命令やデータを、2^nバイト境界のアドレス(2^nバイトの倍数値となるアドレス)に配置させます。
今回の場合、.p2align 2の直後にあるデータはGDT(グローバルディスクリプタテーブル、SEG_NULLASMから始まる)なので、GDTの先頭アドレスは、2^2=4バイト境界(4バイトの倍数値)になります。


gdt:
SEG_NULLASMが配置されているアドレスにラベルを付けます。
gdtラベルからGDTの定義が始まるので、gdtラベルの参照値はGDTの先頭アドレスになります。


SEG_NULLASM
GDTの先頭にある1つ目のディスクリプタを設定するために、SEG_NULLASMマクロを使用しています。
このディスクリプタ(8バイトサイズ)は、セグメントレジスタ(CSレジスタ、DSレジスタなど)を無効にするために使用されます。


SEG_ASM(STA_X | STA_R, 0x0, 0xffffffff) 
GDTの2つ目にあるディスクリプタをコードセグメントに関するディスクリプタとして設定するために、SEG_ASMマクロを使用しています。

第1引数(type)は、セグメントの種類を指定する4bitの値です。
設定値は STA_X | STA_R → 0x0a → 1010なので、GDTの2つ目にあるディスクリプタは、実行と読み出しが可能なコードセグメントに関するディスクリプタになります。

第2引数(base)は、セグメントベース(コードセグメントの先頭アドレス)となる32bitの値です。
設定値は 0x0です(LinuxなどのOSでは、セグメント方式を使ってメモリを区分けするようなことはしていません。CPUがプロテクトモードで動くためにはセグメントディスクリプタを必要としますが、セグメント回路を無効化することができないので便宜的にこのような設定を行なっています)。

第3引数(lim)は、セグメントのサイズを指定するリミット値(セグメントのサイズ-1)を上位20bitに含んだ値です。
設定値は0xffffffffなので、リミット値は0xfffffとなります。
また、SEG_ASMマクロ内でディスクリプタのbit55(Gフラグ)を1に設定しているので、コードセグメントのサイズは4GBとなります。


SEG_ASM(STA_W, 0x0, 0xffffffff) 
GDTの3つ目にあるディスクリプタをデータセグメントに関するディスクリプタとして設定するために、SEG_ASMマクロを使用しています。

第1引数(type)は、セグメントの種類を指定する4bitの値です。
設定値はSTA_W → 0x2 → 0010なので、GDTの3つ目にあるディスクリプタは、読み書き可能なデータセグメントに関するディスクリプタになります。

第2引数(base)は、セグメントベース(データセグメントの先頭アドレス)となる32bitの値です。
設定値は 0x0です(LinuxなどのOSでは、セグメント方式を使ってメモリを区分けするようなことはしていません。CPUがプロテクトモードで動くためにはセグメントディスクリプタを必要としますが、セグメント回路を無効化することができないので便宜的にこのような設定を行なっています)。

第3引数(lim)は、セグメントのサイズを指定するリミット値(セグメントのサイズ-1)を上位20bitに含んだ値です。
設定値は0xffffffffなので、リミット値は0xfffffとなります。
また、SEG_ASMマクロ内でディスクリプタのbit55(Gフラグ)を1に設定しているので、データセグメントのサイズは4GBとなります。

GDTR(グローバルディスクリプタテーブルレジスタ)の設定

 lgdt    gdtdesc
gdtdesc:
  .word   (gdtdesc - gdt - 1)             # sizeof(gdt) - 1
  .long   gdt                             # address gdt

メインメモリ上にGDTR(グローバルディスクリプタテーブルレジスタ)の構造と同じデータ構造を定義し、lgdt命令によりそのデータ構造の値をGDTRにロードします。


lgdt gdtdesc
lgdt命令により、gdtdescラベルが付けられたアドレスに配置されている48bitのデータ構造の値(GDTRの構造と同じデータ構造の値)をGDTRにロードします。


gdtdesc:
.word (gdtdesc - gdt - 1)が配置されているアドレスにラベルを付けます。
gdtdescラベルから、GDTRの構造と同じデータ構造(48bit)の定義が始まります。


.word (gdtdesc - gdt - 1)
.word(wordディレクティブ)は16bitの整数値を定義するために使用されるディレクティブです。
この16bitのデータ領域にGDTのサイズ-1の値(バイト単位)を格納します。
下位アドレス側のgdt(グローバルディスクリプタテーブルの先頭の先頭アドレス)から、上位アドレス側のgdtdesc(GDTR様のデータ構造の先頭アドレス)まで、8バイトのディスクリプタが3つ連続で並んでいます。
よって、設定される値は gdtdesc - gdt - 1 = 24(0x18) -1 = 23(0x17) となります。

gdtdescやgdtのアドレス値からも確認することができます。

## Global Destriptor table(8byte*3entries)
00007c60 <gdt>:

## put the LGDT register(2+4byte)
00007c78 <gdtdesc>:

xv6実装の詳解(boot処理編: segmentationとpagingを中心に) - Qiita
https://gist.github.com/knknkn1162/9ba537b49b10e77f39462a30b274689e#file-bootblock-asm-L144 より引用


.long gdt
.long(longディレクティブ)は32bitの整数値を定義するために使用されるディレクティブです。
この32bitのデータ領域にGDTの先頭アドレスの値(gdtラベルが付けられたアドレス)を格納します。

CPUをプロテクトモードへ移行する

  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<<3), $start32

A20ピンを有効化し、GDT・GDTRの設定が終わったら、CR0(コントロールレジスタ0)のbit0(PEフラグ Protection Enable フラグ)を1にセットしてプロテクトモード(32bitモード)へ移行します。


movl %cr0, %eax
CR0の値をeaxレジスタにコピーします。


orl $CR0_PE, %eax
第1オペランドの値CR0_PE→0x0000 0001と第2オペランドのeaxの値をOR演算した結果を第2オペランドのeaxにセットします。
or命令を使うのはCR0のbit0(PEフラグ)以外のbitの値が変更されないようにするためです。
また、or命令のオペランドにCR0を指定することはできないので、前の命令でCR0の値をeaxレジスタにコピーしています。


movl %eax, %cr0
eaxレジスタの値($CR0_PEの値 と 元のCR0の値 の論理和)をCR0にセットします。
この命令により、CR0のbit0(PEフラグ)に1がセットされ、次の命令からはプロテクトモードで実行されます。


ljmp $(SEG_KCODE<<3), $start32
第1オペランドセレクタ値SEG_KCODE<<3に対応するセグメントベースと第2オペランドのオフセットアドレス値start32(ラベル名)を足し合わせたアドレスへ無条件ジャンプします。
SEG_KCODE → 0b 0000 0000 0000 0001 → 1 を3bit左シフト演算することで、セグメントディスクリプタを指定するセレクタ値SEG_KCODE<<3 → 0b 0000 0000 0000 1000 → 8を得ることができます。
このセレクタ値8に対応するセグメントディスクリプタは、GDTの2つ目にあるディスクリプタ SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff) です。このディスクリプタからはコードセグメントのセグメントベース0x0000 0000を取得することができます。
また、プロテクトモードに移行した直後はCPUのパイプライン内にリアルモードで解釈された命令が残存しているので、CPUのパイプライン内をフラッシュするためにjmp命令を実行しています。

プロテクトモードにおいてCSレジスタ以外のセグメントレジスタを初期化する

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

プロテクトモードに移行したら、各種のセグメントレジスタを初期化します。


.code32
このソースファイルをアセンブルするGNU Assembler(gas)が32bit用の機械語コードを出力するように、.code32(code32ディレクティブ)を指定します。.code32(code32ディレクティブ)を指定することにより、指定箇所以降のアセンブリ言語命令は32bit用の機械語命令にアセンブルされるようになります。


start32:
movw命令が配置されているアドレスにstartラベル(シンボル名、名前)を付けます。


movw $(SEG_KDATA<<3), %ax
SEG_KDATA → 0b 0000 0000 0000 0010 → 2を3bit左シフト演算した値SEG_KDATA<<3 → 0b 0000 0000 0001 0000 → 16をAXレジスタにコピーしています。このAXレジスタにセットされた16の値は、セレクタ値として、次のDSレジスタ、ESレジスタ、SSレジスタにセットされます。

movw %ax, %ds
AXレジスタにセットされているセレクタ値16をDSレジスタにコピーしています。
このセレクタ値16に対応するセグメントディスクリプタは、GDTの3つ目にあるセグメントディスクリプタSEG_ASM(STA_W, 0x0, 0xffffffff) です。セグメント方式は利用していません。


movw %ax, %es
AXレジスタにセットされているセレクタ値16をESレジスタにコピーしています。


movw %ax, %ss
AXレジスタにセットされているセレクタ値16をSSレジスタにコピーしています。
このセレクタ値16に対応するセグメントディスクリプタは、GDTの3つ目にあるセグメントディスクリプタSEG_ASM(STA_W, 0x0, 0xffffffff) です。セグメント方式は利用していません。


movw %ax, %es
axレジスタにセットされているセレクタ値16をESレジスタにコピーしています。


movw $0, %ax
即値0をAXレジスタにコピーしています。
このAXレジスタにセットされた0をFSレジスタ、GSレジスタの初期化に使用します。


movw %ax, %fs 
AXレジスタにセットされた0でFSレジスタを初期化しています。


movw %ax, %gs
AXレジスタにセットされた0でGSレジスタを初期化しています

bootmainへ処理を移す

movl    $start, %esp
call    bootmain

最後に、bootmain.cファイルのbootmain(void)へ処理を移します。


movl $start, %esp
startラベルのアドレス値(0x7c00)をESPレジスタ(スタックポインタ)に設定しています。
0x7c00から下位方向へのメモリ領域をスタック領域として利用します。


call bootmain
第一オペランドで指定したアドレス(bootmainのアドレス)から始まるサブルーチン(bootmain.cファイルのbootmain(void))へ処理を移します。




次回
jupiteroak.hatenablog.com