LoginSignup
4
3

More than 3 years have passed since last update.

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

Last updated at Posted at 2018-09-29

はじめに

前回: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を書いていきましょう。

elf.h
/* 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;
};
types.h
typedef unsigned int   uint;
typedef unsigned short ushort;
typedef unsigned char  uchar;
typedef uint pde_t;
bootmain.c
#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)です。
alt
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バイトごとにアライメント、リトルエンディアンになっていることに注意。
後はコメントの通りの処理をしています。

bootasm.s(続き)
.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

その他

これでブートセクタのプログラム作成は完了です。
あとはコンパイルして、リンクして、ちょこっと手を加えてブートセクタのバイナリが出来上がります。

...なんですが、リンクの部分で説明がまた多そうだったので力尽きました...
コンパイルやリンクなどは次回に回したいと思います。

4
3
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
4
3