Closed38

Legacy BIOSで32bit OSのブートローダを作る時のメモ

🤨🤔😑😨😱🤨🤔😑😨😱

以下のアセンブリから理解をしていく。

ORG 0x7c00
BITS 16

start:
    mov ah, 0eh
    mov al, 'A'
    int 10h

    jmp $

times 510-($-$$) db 0
dw 0xaa55
🤨🤔😑😨😱🤨🤔😑😨😱

GPT-4に聞いてみた。


各行の意味は以下のとおりです。

ORG 0x7c00

ORGはORiGinの略で、アセンブリ言語におけるプログラムがメモリ上のどのアドレスから実行されるかを指定する命令です。この例であれば、プログラムはメモリ内の0x7c00アドレスから開始されます。このアドレスは、一般的にPCがBIOSから制御を引き継いだときの起動セクタの開始アドレスです。

BITS 16

BITSディレクティブは、アセンブラの動作モードを切り替えます。BITS 16という指定で、この後に続くコードは16ビットモードで動作することを示しています。8086以降のx86アーキテクチャは当初16ビットをベースとしていたため、このモードではそのような互換モード下で動作します。

start:

ラベル"start"を定義しています。これはプログラムコード内の特定の位置を指します。この場合、このラベルはORG命令で指定されたアドレス、つまり、0x7c00に対応します。

    mov ah, 0eh
    mov al, 'A'
    int 10h

1つ目のmov命令は、16ビットレジスタAXの上位8ビット(AH)に値0x0Eを移動します。つまり、BIOSの文字表示サービス(Teletype出力)を指定します。2つ目のmov命令は、下位8ビット(AL)にASCII文字'A'の値を設定します。int命令は、ソフトウェア割り込みを発生させ、ここではBIOSの割り込みサービスを呼び出して画面に'A'を表示します。

    jmp $

無限ループを作成します。$は現在のアセンブリの位置を示す特別な記号です。この命令により、プロセッサはこの命令を永遠に実行し続けます。

times 510-($-$$) db 0

times命令は指定した数だけ前の命令を繰り返します。この場合、510 - (現在のアドレス - プログラム開始アドレス)回だけ0を出力します。つまり、ブートセクタはちょうど512バイト(標準のディスクセクタサイズ)になるようにパディングするためのコードです。

dw 0xaa55

dwはDefine Word(ワード定義)を意味します。これにより16ビットの値0xAA55が生成され、これがブートセクタの末尾に配置されます。この値は、PCがこれが有効なブート可能なセクタであると判断するためのマジックナンバー、ブートシグネチャと呼ばれます。

🤨🤔😑😨😱🤨🤔😑😨😱

以下のコード以外の部分はBIOSのおまじないだと思う。

    mov ah, 0eh
    mov al, 'A'
    int 10h

int 10hを実行した時に、
ah (= raxの下位8bit) に 0eh (=14)が代入されていれば、
al (=raxの下位8bitから下位16bit) に代入された文字 'A' を画面に表示する。

int 10hという命令はBIOSの機能として準備されている特殊な命令である。

🤨🤔😑😨😱🤨🤔😑😨😱

上記のコードをnasm -f bin ./boot.asm -o ./boot.binでコンパイルする。

ndisasm boot.binで内容を確認する。

00000000  B40E              mov ah,0xe
00000002  B041              mov al,0x41
00000004  CD10              int 0x10
00000006  EBFE              jmp short 0x6
00000008  0000              add [bx+si],al
...
000001FC  0000              add [bx+si],al
000001FE  55                push bp
000001FF  AA                stosb
🤨🤔😑😨😱🤨🤔😑😨😱

Hello, World!を表示するプログラム。

ORG 0x7c00
BITS 16

start:
    mov si, message
    call print
    jmp $

print:
    mov bx, 0
.loop:    
    lodsb
    cmp al, 0
    je .done
    mov ah, 0eh
    int 10h
    jmp .loop
.done:
    ret

message db "Hello, World!", 0

times 510-($-$$) db 0
dw 0xaa55

mov si, messageは、messageのメモリ位置をsiに格納する。
lodsbは、siに格納された1bytesを取り出しalに格納し、その後siを1bytesずらす。

message db "Hello, World!", 0は、db "Hello, World!", 0という宣言にmessageというラベル付けを行っている。db "Hello, World!", 0はBIOS上に1bytesずつデータを並べているだけ。

🤨🤔😑😨😱🤨🤔😑😨😱

バイナリで見ると命令とデータが一緒の部分においてあって不思議な気持ちになるが、これがノイマン型コンピュータということなんだろう。

🤨🤔😑😨😱🤨🤔😑😨😱

16bit時代のCPUについて

BIOSではBITS 16と宣言している。これはintel 8086の互換モードとしてCPUを動作させることを意味している。

https://ja.wikipedia.org/wiki/Intel_8086

これをリアルモードと言ったりする。

https://ja.wikipedia.org/wiki/リアルモード

🤨🤔😑😨😱🤨🤔😑😨😱

一つのレジスタに16bitしか保存できないと、65536個の値しか表現できない。
レジスタはメモリのアドレスの番地を指定するのに使う。
16bitでは、最大で65536 bytes = 64 KiBのメモリしか扱うことはできない。

intel 8086ではこの問題を解決するために、普通のレジスタの他にセグメントレジスタというものが用意された。メモリのアドレスを計算する場合は、普通のレジスタの値(この文脈でこの値のことをオフセットと呼ぶ)に、セグメントレジスタを16倍した値を足し、合計で20bitでメモリのアドレスを指定する。20bitあれば1MiBのアドレスを扱うことができる。(この辺の説明はwikiに詳しく書いてある。)

🤨🤔😑😨😱🤨🤔😑😨😱

Intel 8086のレジスタ

🤨🤔😑😨😱🤨🤔😑😨😱

AX, BX, CX, DX: 汎用レジスタ

SI, DI: 文字列操作用の特殊レジスタ
IP: 次のCPU命令のアドレスを保持する特殊レジスタ
BP, SP: ベースポインタとスタックポインタ

Flag: 前回のCPU命令の結果を示すフラグ

🤨🤔😑😨😱🤨🤔😑😨😱

Intel 8086にはセグメントレジスタがある。それぞれ以下のように使う。

CS (Code Segment): IPと組み合わせて使う。次のCPU命令のアドレス位置のセグメントが保存される。
DS (Data Segment): 汎用レジスタと組み合わせて使う。
SS (Stack Segment): SPまたはBPと組み合わせて使う。
ES (Extra Segment): SIやDIと組み合わせて使うことが多い。

🤨🤔😑😨😱🤨🤔😑😨😱

割り込みベクタテーブル

🤨🤔😑😨😱🤨🤔😑😨😱

x86のリアルモードではメモリの最初の1KB(0x0000から0x03FF)が割り込みベクターテーブルとなっている。4Bytesごとに256個のエントリがあり、それぞれ割り込みに対応している。

🤨🤔😑😨😱🤨🤔😑😨😱

割り込みベクターテーブルを定義してみる。

ORG 0
BITS 16

start:
    jmp 0x7c0:step2

print_A:
    mov ah, 0eh
    mov al, 'A'
    int 10h
    iret

print_B:
    mov ah, 0eh
    mov al, 'B'
    int 10h
    iret

step2:
    mov ax, 0
    mov ss, ax

    mov word[ss:0x00], print_A
    mov word[ss:0x02], 0x7c0

    mov word[ss:0x04], print_B
    mov word[ss:0x06], 0x7c0

    int 1
    int 0

times 510-($-$$) db 0
dw 0xaa55

word は x86アセンブリ言語におけるデータサイズの指定子で、16ビット(2バイト)のデータを表す。

🤨🤔😑😨😱🤨🤔😑😨😱

diskからメモリにデータを読み出す方法。

ah = 2でint 13を呼び出せばdiskからメモリにデータを読み出せるみたい。

https://www.ctyme.com/intr/rb-0607.htm

🤨🤔😑😨😱🤨🤔😑😨😱

以下の命令で、読み出せた。

    mov ah, 2
    mov al, 1
    mov ch, 0
    mov cl, 2
    mov dh, 0
    mov bx, buffer
    int 0x13

以下のようにSIレジスタにbufferというラベルを紐づければ、文字列が読み出せる。

    mov si, buffer
🤨🤔😑😨😱🤨🤔😑😨😱

プロテクトモード

https://ja.wikipedia.org/wiki/プロテクトモード

Intel 80286以降のCPUに導入されているモード。メモリにアクセス権限を付与する機能。

🤨🤔😑😨😱🤨🤔😑😨😱

クロスコンパイル環境のセットアップ

https://wiki.osdev.org/GCC_Cross-Compiler#Linux_Users_building_a_System_Compiler

これまでM1 Macでやってたけど、qemuやgdbのデバッグ機能を使えないのでUbuntu Desktopでやる。
持っててよかったThinkpad X230。

🤨🤔😑😨😱🤨🤔😑😨😱

Ubuntu 22.04だと以下のコマンドを順番に実行していけば環境は整うと思う。

# Install dependencies

sudo apt install build-essential bison flex libgmp3-dev libmpc-dev libmpfr-dev texinfo  libisl-dev

# Download source code

mkdir ~/src
cd ~/src
wget https://ftp.gnu.org/gnu/binutils/binutils-2.41.tar.gz
tar -xvf binutils-2.41.tar.gz

wget https://ftp.tsukuba.wide.ad.jp/software/gcc/releases/gcc-13.2.0/gcc-13.2.0.tar.gz
tar -xvf gcc-13.2.0.tar.gz

# Set up environment variables

export PREFIX="$HOME/opt/cross"
export TARGET=i686-elf
export PATH="$PREFIX/bin:$PATH"

# Compile binutils

cd $HOME/src
 
mkdir build-binutils
cd build-binutils
../binutils-2.41/configure --target=$TARGET --prefix="$PREFIX" --with-sysroot --disable-nls --disable-werror
make
make install

# Compile GCC

cd $HOME/src
 
# The $PREFIX/bin dir _must_ be in the PATH. We did that above.
which -- $TARGET-as || echo $TARGET-as is not in the PATH
 
mkdir build-gcc
cd build-gcc
../gcc-13.2.0/configure --target=$TARGET --prefix="$PREFIX" --disable-nls --enable-languages=c,c++ --without-headers
make all-gcc
make all-target-libgcc
make install-gcc
make install-target-libgcc

参考:

https://wiki.osdev.org/GCC_Cross-Compiler#Linux_Users_building_a_System_Compiler

🤨🤔😑😨😱🤨🤔😑😨😱

Legacy BIOSからカーネルを起動するのは難しかったので、Grubから起動させてみる。以下がとても参考になった。

https://wiki.osdev.org/Bare_Bones#Booting_the_Operating_System

日本語だと以下が参考になる。

https://qiita.com/sp6/items/228e63030ea0124e42c0

🤨🤔😑😨😱🤨🤔😑😨😱

リアルモードからプロテクトモードに移行してOSを起動する方法は、以下を読めば実装できそうではある。

http://softwaretechnique.web.fc2.com/OS_Development/bootloader1.html

とりあえずgrubから簡単に起動できることと、UEFIになった現代にこれをマスターするのは優先順位低いかな。ただ、Global Descriptor Tableは理解しといた方がいいかもしれない。

🤨🤔😑😨😱🤨🤔😑😨😱

x86でカーネルを0x100000に配置する理由。

🤨🤔😑😨😱🤨🤔😑😨😱

リアルモードでは、セグメントレジスタとオフセットレジスタで合わせて20bitでしかアドレスを指定できない。その最大は0xfffffである。そのため、プロテクトモードに移行した時には、リアルモードの次のアドレスから利用する。

つまり、0xfffff + 1 = 0x100000である。

🤨🤔😑😨😱🤨🤔😑😨😱

dmesgでメモリマップが確認できる。手元のx86_64のLinuxでは以下のようになっていた。

[    0.000000] BIOS-provided physical RAM map:
[    0.000000] BIOS-e820: [mem 0x0000000000000000-0x000000000009d7ff] usable
[    0.000000] BIOS-e820: [mem 0x000000000009d800-0x000000000009ffff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000000e0000-0x00000000000fffff] reserved
[    0.000000] BIOS-e820: [mem 0x0000000000100000-0x000000001fffffff] usable
[    0.000000] BIOS-e820: [mem 0x0000000020000000-0x00000000201fffff] reserved

この中の0x100000(1MB)から0x1fffffff(512MB)の領域の一部がLinuxカーネルに使われていると思う。

このスクラップは2023/11/21にクローズされました