#はじめに
前回:https://qiita.com/mix64/items/0d1e96fca0200b00c3a4
前回bootasm.Sの処理を終え、call bootmainでC言語で書かれたブートセクタの領域にジャンプしてきました。
ここではブートセクタの最後の仕事であるカーネルのロードを行います。
#ELF(Executable and Linking Format)について
参考:https://linuxjm.osdn.jp/html/LDP_man-pages/man5/elf.5.html
参考:http://softwaretechnique.jp/OS_Development/Tips/ELF/elf01.html
カーネル部分のフォーマットにはELFを採用します。
.textや.dataなどのバイナリの前にヘッダを付けたという感じでしょうかね?
ヘッダにはプログラムのサイズや、エントリポイントなどが入っています。
詳しくは参考URLへ(面倒になったので丸投げ)
今回使用する部分のみに関して言及するならば、
ELFヘッダでは「マジックナンバ、プログラムヘッダオフセット、プログラムヘッダ数、エントリポイント」
プログラムヘッダでは「オフセット、物理アドレス、ファイルサイズ、(使用)メモリサイズ」
あたりを確認しておくと良いと思います。
#カーネルローダの作成
何はともあれ、bootmain.cを書いていきましょう。
/* Format of an ELF executable file*/
#define ELF_MAGIC 0x464C457FU /* "\x7FELF" in little endian */
/* File header */
struct elfhdr {
uint magic; /* must equal ELF_MAGIC */
uchar elf[12];
ushort type;
ushort machine;
uint version;
uint entry;
uint phoff;
uint shoff;
uint flags;
ushort ehsize;
ushort phentsize;
ushort phnum;
ushort shentsize;
ushort shnum;
ushort shstrndx;
};
/* Program section header */
struct proghdr {
uint type;
uint off;
uint vaddr;
uint paddr;
uint filesz;
uint memsz;
uint flags;
uint align;
};
typedef unsigned int uint;
typedef unsigned short ushort;
typedef unsigned char uchar;
typedef uint pde_t;
#include "types.h"
#include "elf.h"
#define SECTSIZE 512
void readseg(uchar*, uchar, uint);
void memclr(uchar* addr, uint count);
void bootmain(void)
{
struct elfhdr *elf;
struct proghdr *ph, *eph;
void (*entry)(void);
uchar *pa;
uint offset, count;
elf = (struct elfhdr*)0x7E00; /* scratch space */
/* Read 4KB from disk (next bootsector) */
readseg((uchar*)elf, 8, 1);
/* Is this an ELF executable? */
if(elf->magic != ELF_MAGIC)
return; /* let bootasm.S handle error */
/* Load each program segment (ignores ph flags). */
ph = (struct proghdr*)((uchar*)elf + elf->phoff);
eph = ph + elf->phnum;
for(; ph < eph; ph++){
pa = (uchar*)ph->paddr - (ph->off % SECTSIZE); /* Round down to sector boundary. */
count = (ph->filesz + SECTSIZE-1) / SECTSIZE; /* how many read sectors */
offset = (ph->off / SECTSIZE) + 1; /* Translate from bytes to sectors; kernel starts at sector 1. */
readseg(pa, count, offset);
if(ph->memsz > ph->filesz)
memclr((uchar*)ph->paddr + ph->filesz, ph->memsz - ph->filesz);
}
/* Call the entry point from the ELF header. Does not return! */
entry = (void(*)(void))(elf->entry);
entry();
}
長いですね...ヘッダファイルは定義なので説明は省きます。(xv6から拝借した)
readseg()とmemclr()はアセンブリで定義した関数なので後で説明します。
まずelfヘッダを読み込むメモリ領域を決めましょう。
読み込むヘッダのサイズは4KB(0x1000Byte)です。
0x0500~0x7C00はスタック領域として使っているので、0x7E00~0x8E00でも使いましょうかね(適当)
カーネルロードが終了したらお役御免なので、適当に置いておきます。
readseg()を使って8セクタ(4KB)分をメモリに読み込みます。
ブートセクタが最初(0番目)に入っているので1番目から8番目ですね。
ブートセクタが512バイトしか無いので、ELFのフラグとかを確認する余裕が無いです。
なのでxv6と同じくマジックナンバだけを確認しています。
次にプログラムヘッダーからアドレスやサイズを取得します。
実際の値はバイナリをreadelfコマンドで確認すると良いと思います。
読み込み部はプログラムのコメントそのままなので省略。
メモリサイズがファイルサイズより大きい場合があります。
その差分の領域はbssセクションなので0で初期化する必要があります。
最後にカーネルのエントリポイントへジャンプして、カーネルローダのお仕事は終了です。
お疲れ様でした。
#ATAによるディスク読み書き
参考:https://wiki.osdev.org/ATA_read/write_sectors
参考:https://forum.osdev.org/viewtopic.php?t=12268
ラベルに.globlを付けるとC言語の方から関数のようにアセンブリで書かれた部分を実行できます。
拡張インラインアセンブラを使うのも考えましたが、面倒だったのでカット。カーネル部分では使います。
ディスクの読み書きには2つのモードがあり、LBA(Logical Block Address)モードと、CHS (cylinder,head,sector)モードがあります。
読んで字のごとく、シリンダ・ヘッド・セクタを指定するモードと、ディスク全体を連続した論理アドレス空間として見なすモードがあります。
使いやすいので今回はLBAモードを採用します。
ポート0x1F7はread時にはステータスレジスタ、write時にはコマンドレジスタとして機能します。
あと2バイト以上のポート番号(256~)にアクセスする際は、即値ではなくdxレジスタに入れてin/out命令をする必要があるっぽい(?)
引数は4バイトごとにアライメント、リトルエンディアンになっていることに注意。
後はコメントの通りの処理をしています。
.globl readseg
readseg: # (char *addr, uchar count, uint offset)
push %ebp
movl %esp, %ebp
pushal
call waitdisk
movb 12(%ebp), %al # read sector num.
movw $0x1F2, %dx # sector count, how many sectors to read/write
outb %al, %dx
movw $0x1F3, %dx # Port to send bit 0 - 7 of LBA
movb 16(%ebp), %al # Get bit 1 - 7 in AL
outb %al, %dx
movw $0x1F4, %dx # Port to send bit 8 - 15 of LBA
movb 17(%ebp), %al # Get bit 8 - 15 in AL
outb %al, %dx
movw $0x1F5, %dx # Port to send bit 16 - 23 of LBA
movb 18(%ebp), %al # Get bit 16 - 23 in AL
outb %al, %dx
movw $0x1F6, %dx # Port to send drive and bit 24 - 27 of LBA
movb 19(%ebp), %al # Get bit 24 - 27 in AL
orb $0xE0, %al # Set bit 6 in al for LBA(Logical Block Address) mode
outb %al, %dx
movw $0x1F7, %dx # Command port
movb $0x20, %al # 0x20: Read with retry.
outb %al, %dx
call waitdisk # wait disk read finish
movb $0x80, %dl
movl 8(%ebp), %edi # kernel base address
xorl %eax, %eax
movb 12(%ebp), %al # read sector num. only bit 0 - 7 valid
mulb %dl # sector num(%al) * 0x80(=512/4)(%dl) -> %ax
movl %eax, %ecx
movw $0x1F0, %dx # data register, the bytes are written/read here
rep insl
popal
leave
ret
waitdisk:
movw $0x1F7, %dx
waitdisk.1:
inb %dx, %al # Status register
andb $0xC0, %al # bit7: controller is executing a command
cmpb $0x40, %al # bit6: drive is ready
jnz waitdisk.1 # waiting disk
ret
.globl memclr
memclr: # (char *addr, uint count)
push %ebp
movl %esp, %ebp
pushal
movl 8(%ebp), %edi # base address
movl 12(%ebp), %ecx # write count
movb $0x00, %al # write data (0)
rep stosb
popal
leave
ret
C言語側でどうレジスタを使っているかわからないので、pushalで汎用レジスタを全部スタックに積んでから処理をしています。再開時にはpopalで取り出し。
必要に応じて参照:
insl命令:http://softwaretechnique.jp/OS_Development/Tips/IA32_Instructions/INS.html
leave命令:http://softwaretechnique.jp/OS_Development/Tips/IA32_Instructions/LEAVE.html
memclrはアドレスaddrからcountバイトを0クリアする関数です。
stosb命令を使用することで容量を節約しています。(何度も言ったように512バイトしか無いためキツキツ)
stosb命令:http://softwaretechnique.jp/OS_Development/Tips/IA32_Instructions/STOS.html
#その他
これでブートセクタのプログラム作成は完了です。
あとはコンパイルして、リンクして、ちょこっと手を加えてブートセクタのバイナリが出来上がります。
...なんですが、リンクの部分で説明がまた多そうだったので力尽きました...
コンパイルやリンクなどは次回に回したいと思います。