ELF形式のOSを起動してみた。
こんにちは、にわかです。 タイトル通りなのですが、ELF形式のOSを起動してみます。 この記事を読んでくれたら、ELF形式のOSを起動できるようになります。注意として、UEFIの力を借りてOSをブートするというものになっておらず、
「30日でできる! OS自作入門」と似たようなやり方でブートします。(「30日でできる! OS自作入門」では、自力でリアルモードからプロテクトモードに移行する際に、OSの起動の手続きも行なっていた。)
前回、ELF形式のヘッダ情報を解析するプログラム記事を載せたのですが、その続編に当たります。
ELF形式のOSをロードする方法を紹介します。
とりあえず、起動できた後の画面を載せておきます。
赤くするだけです笑(赤くするだけなので、OSではないのですが、ここではOSということにしましょう。)
UEFIがイケイケなので、リアルモードでOSを起動する記事の需要がない気もしました。。。
1つだけ、この記事のメリットを上げるとすれば、UEFIを知らなくてもELF形式のOSを起動できるってことですかね。。(30日でできる! OS自作入門の読者の方はむしろ今回の記事の方が理解できるかも?)
ELFのおさらい
ELFの記事は前回投稿したものを確認してほしいのですが、簡単におさらいしておきます。ELF(Executable and Linkable Format)は、Linuxでデフォルトになっているオブジェクトファイルと実行ファイルのフォーマトです。
EXEフォーマットの仲間みたいなもんです。
実行ファイルもオブジェクトファイルもどちらもELFフォーマットとして扱うことができるのが特徴の1つです。
ELFで、プログラムをロードする際に参照するヘッダが2種類存在します。
1つ目がELFヘッダで、2つ目がプログラムヘッダです。
ELFヘッダとは、ファイル全体に関する情報が格納されています。(ビッグインディアンなのか?リトルインディアンなのか?エントリポイントはいくつ?など)
プログラムヘッダとは、セグメントに関する情報が格納されています。セグメントは普通複数存在するはずなので、プログラムヘッダは複数存在することになります。(プログラムヘッダが、配列上に並んでいるので、プログラムヘッダテーブルと呼ぶそうです。)
OSはセグメント単位で、実行可能ファイルをRAMにロードします。(OSをロードするプログラムも同様にOSをセグメント単位でロードする。)
その際にセグメントをどこに配置すべきなのか?セグメントの大きさは?エントリポイントは?などといった気になる情報が、ELFヘッダとプログラムヘッダには書かれています。今回の記事でも、ヘッダ情報を参照しつつRAMにOSを展開していきます。
ELFヘッダとプログラムヘッダのどこをみたらいいのか?
以下にELFヘッダとプログラムヘッダの構造体を載せます。
ELFヘッダ
typedef struct elf_header{
char e_ident[16];
short e_type;
short e_machine;
int e_version;
int e_entry;
int e_phoff;
int shoff;
int e_flags;
short e_ehsize;
short e_pehtsize;
short e_phnum;
short e_shetsize;
short e_shnum;
short e_shstrndx;
}ELF_HEADER;
プログラムヘッダ
typedef struct ph_header{
int p_type;
int p_offset;
int p_vaddr;
int p_paddr;
int filesz;
int p_memmsz;
int p_flags;
int p_align;
}PH_HEADER;
どこを見なきゃならんかというと、
e_entry、e_phoff、p_offset、p_vaddr、fileszを最低限見る必要があるかなと。。
ELFヘッダで使用する要素は、e_entry、e_phoffです。
e_entryは、エントリポイント
e_phoffは、プログラムヘッダテーブルの先頭のオフセットアドレス(ファイルの先頭からのオフセット)
プログラムヘッダで使用する要素は、p_offset、p_vaddr、fileszです。
p_offsetは、プログラムヘッダに対応するセグメントのオフセット(ファイルの先頭からのオフセット)
p_vaddrは、プログラムヘッダに対応するセグメントの先頭の論理番地
fileszは、ファイル上のプログラムヘッダに対応するファイル上のサイズ
今回紹介するファイル
ファイル名 | 役割 |
---|---|
ipl.asm | protect_on.asmをロードしたり、ビデオモードを設定する |
protect_on.asm | プロテクトモード移行したり、niwaka-os.binを起動する |
niwaka-os.c | OS本体 |
asmlib.asm | OSで使用するアセンブリ言語で書かれた関数ライブラリ |
※ipl.asmをアセンブルしたファイル名をipl.binとします。
※protect_on.asmをアセンブルしたファイル名をprotect_on.binとします。
※niwaka-os.cとasmlib.asmは、1つのELF形式の実行ファイルniwaka-os.binとなります。
OSを起動する流れ
OSを起動するまでの流れを紹介します。 ディスクはフロッピーディスクとします。 ディスクのフォーマットは、FAT12とします。1.BIOSがipl.binを0x7c00番地以降に読み込む
2.ipl.binが、フロッピーディスクの先頭から(22018*512byte分の内容を0x8000番地以降にロードする。
3.ロード後、0xc200番地にジャンプして、protect_on.binのコードが実行される。(なぜ0xc200番地かと言うと、FAT12の先頭から0x4200~0x43ff番地にprotect_on.binが存在してます。なので、0x8000+0x4200=0xc200番地にprotect_on.binが存在することになります。)
3.protect_on.binで、プロテクトモードに移行してniwaka-os.binのコードセグメントの先頭の番地にジャンプする。
4.niwaka-os.bin起動
上のブートの流れを見てもらうと分かりますが、はりぼてOSと似たような方法でブートしています。
リンカスクリプト
OSのためのリンカスクリプトを紹介します。ENTRY(niwaka_main);
SECTIONS{
. = 0x0c000000 + SIZEOF_HEADERS;
.text : {*(.text)}
. = 0x0c004000;
.data : {*(.data)*(.rodata*)}
. = 0x0c006000;
.bss : {*(.bss)}
}
textとdataとbssが、見てとれますよね。
それぞれ、コードセグメント、dataセグメント、bssセグメントに対応します。
これらを適切な位置にprotect_on.binが展開していきます。(bssに関しては、今回何もしません。)
niwaka-os.binの先頭の論理番地を0x0c000000番地としました。
最低限、コードセグメントは、(0x0c000000+ヘッダの大きさ)番地以降に配置されること、データセグメントは0x0c004000番地以降に配置されることを知っていただければOKです。
リンカスクリプトに記載されている通りにRAMにniwaka-os.binを展開した図が以下になります。
図2(赤い矢印の先はdataセグメントのつもりです。。。。修正は気が向いたら)
図2の矢印の意味は、各ヘッダの格納されているオフセット値(ファイルの先頭からの)が示している場所です。
ipl.bin
これは、ほぼ「30日できる!OS自作入門」のプログラムをほぼ真似ています。(フロッピーディスクをロードする処理が違うだけでやっていることはほとんど同じ)bits 16
ORG 0x7c00 ;アセンブラに対しての命令
SECTORS EQU 19 ;実際は、18だが、条件式の都合により1プラスしてる。
CYLINDERS EQU 20 ;
HEADS EQU 2 ;ヘッドの数
JMP init ;register_initにジャンプする
DB 0x90
DB "NIWAKA- "
DW 512
DB 1
DW 1
DB 2
DW 224
DW 2880
DB 0xf0
DW 9
DW 18
DW 2
DD 0
DD 2880
DB 0,0,0x29
DD 0xffffffff
DB "NIWAK "
DB "FAT12 "
RESB 18
;初期化
init:
MOV AX, 0x00
MOV BX, 0 ;BXアドレスは0番地で固定する
MOV SS, AX
MOV SP, 0x7c00
MOV DS, AX
MOV AX, 0x0800 ;バッファアドレスの開始番地を0x8200番地に設定する
MOV ES, AX
MOV DH, 0 ;ヘッド番号
MOV DL, 0 ;ドライブ番号
MOV CH, 0 ;シリンダ番号
MOV CL, 1 ;セクタ番号
JMP read ;readラベルに飛ぶ
;FDからデータを読み込んでいく
read:
MOV AH, 0x02 ;読み込みモード
MOV AL, 0x01 ;1セクタ分読み込む
INT 0x13 ;割り込み番号13を呼び出す
JC error ;ソフトウェア割り込み失敗時、ジャンプする
MOV AX, ES ;バッファアドレス1足す
ADD AX, 0x20
MOV ES, AX
ADD CL, 1 ;セクタ番号を1足す
CMP CL, SECTORS ;20とCL(セクタ番号を比較する)
JE count_head ;20と等しい場合、ジャンプする
JMP read
count_head:
MOV CL, 0x01 ;セクタ番号を1にする
ADD DH, 0x01 ;ヘッド番号を1足す
CMP DH, HEADS ;ヘッド番号が2の場合、ジャンプする。
JE count_cylinder
JMP read
count_cylinder:
MOV DH, 0x00 ;ヘッド番号を0にする
ADD CH, 0x01
CMP CH, CYLINDERS ;シリンダ番号が5まできたら終わり
JE video_mode ;シリンダ番号が、5のとき終了する
JMP read
video_mode:
MOV AH, 0x00
MOV AL, 0x13
INT 0x10
jmp_protect_on:
JMP 0xc200 ;0xc200番地へジャンプする
;このエラー処理適当すぎますが、許してください。
error:
HLT
JMP error
RESB 0x1fe-($-$$)
DB 0x55, 0xaa
ipl.binの処理が終了して、0xc200番地にジャンプする時点のRAMメモリマップを以下に載せました。(数値の単位は番地です。)
図3
niwaka-os.binの先頭の番地が、なぜ0xc400番地かと言いますと
FAT12フォーマットされたディスク上では0x4400番地以降に配置されているので、0x4400+0x8000(0x8000番地を足す理由は、0x8000番地以降にフロッピーディスクの内容をロードしているから)
上の図を適宜見つつ、下以降の解説を読んでみてください。
protect_on.bin
protect_on.binが肝となるプログラムです。まずは基本アイデアとなるものを図で描きました。
図3(プログラムヘッダを転送しないような表記になっており、後日修正します。)
図3の通り、0xc400番地以降に存在するELFヘッダとプログラムヘッダテーブルとコードセグメント、dataセグメント、bssセグメントを0x0c000000番地に転送します。
以下が、protect_on.binになります。(MOV命令とADD命令やJMP命令ぐらいしか使っておらず、単純なプログラムになっています。)
;プロテクトモードオンにする
bits 16
ORG 0xc200 ;このプログラムは、0xc200番地に読み込まれる
;もうここで一気にページング入ろうか迷う
NIWAKA EQU 0x0c000000 ;niwaka-osのロード先
EntryPoint EQU 0x00006000 ;エントリポイント
ProgramHeader EQU 0x00006004 ;プログラムヘッダテーブルの先頭の番地
CodeFileSize EQU 0x00006008 ;コードセグメントのファイル上のサイズ
DataFileSize EQU 0x0000600c ;データセグメントのファイル上のサイズ
BssMemSize EQU 0x00006010 ;BSS領域のメモリ上のサイズ
;ヘッダ情報を指定した番地に書き込む
write_header:
MOV EAX, [0xc400+0x18]
MOV [EntryPoint], EAX
MOV EAX, [0xc400+0x1c]
MOV [ProgramHeader], EAX
MOV EAX, 0xc400+16
MOV EBX, [ProgramHeader]
ADD EBX, EAX
MOV ESI, [EBX]
MOV [CodeFileSize], ESI
MOV EAX, 0xc400+4
MOV EBX, [ProgramHeader]
ADD EBX, EAX
MOV ESI, [EBX]
ADD [CodeFileSize], ESI
MOV EAX, 0xc400+32+16
MOV EBX, [ProgramHeader]
ADD EBX, EAX
MOV ESI, [EBX]
MOV [DataFileSize], ESI ;32+16
;割り込み禁止する
CLI
; enable A20
CALL waitkbdout
MOV AL,0xd1
OUT 0x64,AL
CALL waitkbdout
MOV AL,0xdf
OUT 0x60,AL
CALL waitkbdout
GDT_SET:
LGDT [GDTR_INIT]
MOV EAX, CR0
OR EAX, 1
MOV CR0, EAX
JMP flash
flash:
MOV AX, 0x08
MOV DS, AX
MOV ES, AX
MOV FS, AX
MOV GS, AX
MOV SS, AX
;コード領域のロード
CodeRegion_Transfer:
MOV ESI, 0xc400 ;転送元
MOV EDI, NIWAKA ;転送先
MOV ECX, [CodeFileSize] ;セグメントサイズ
CALL memcpy
;data領域のロード
DataRegion_Transfer:
MOV EAX, [ProgramHeader];programヘッダのオフセットアドレス
MOV EDX, 0xc400+32+4
ADD EAX, EDX
MOV ESI, [EAX]
ADD ESI, 0xc400
MOV EAX, [ProgramHeader] ;プログラムヘッダテーブルの先頭のオフセット番地
MOV EDX, 0xc400+32+8
ADD EAX, EDX
MOV EDI, [EAX]
MOV ECX, [DataFileSize] ;セグメントサイズ
CALL memcpy
MOV ESP, 0x0c000000 ;スタック初期値
MOV EAX, [EntryPoint]
MOV [offset_addr], EAX
DB 0x66, 0xea
offset_addr:
DB 0x00, 0x00, 0x00, 0x00
DB 0x10, 0x00
GDT_INIT:
RESB 8
DB 0xff, 0xff, 0x00, 0x00, 0x00, 0x92, 0xcf, 0x00
DB 0xff, 0xff, 0x00, 0x00, 0x00, 0x9a, 0xcf, 0x00
GDTR_INIT:
DW 23 ;GDTの大きさ-1
DD GDT_INIT ;GDT_INITのアドレス
waitkbdout:
IN AL,0x64
AND AL,0x02
JNZ waitkbdout ; ANDの結果が0でなければwaitkbdoutへ
RET
memcpy:
MOV AL, [ESI]
ADD ESI, 1
MOV [EDI], AL
ADD EDI, 1
SUB ECX, 1
JNZ memcpy
RET
まずは、序盤のコードの解説をします。
;ヘッダ情報を指定した番地に書き込む
write_header:
MOV EAX, [0xc400+0x18]
MOV [EntryPoint], EAX
MOV EAX, [0xc400+0x1c]
MOV [ProgramHeader], EAX
MOV EAX, 0xc400+16
MOV EBX, [ProgramHeader]
ADD EBX, EAX
MOV ESI, [EBX]
MOV [CodeFileSize], ESI
MOV EAX, 0xc400+4
MOV EBX, [ProgramHeader]
ADD EBX, EAX
MOV ESI, [EBX]
ADD [CodeFileSize], ESI
MOV EAX, 0xc400+32+16
MOV EBX, [ProgramHeader]
ADD EBX, EAX
MOV ESI, [EBX]
MOV [DataFileSize], ESI
①では、エントリーポイントとプログラムヘッダの番地を、EntryPointとProgramHeader番地にそれぞれ書き込んでます。それぞれファイルの先頭から0x18番地と0x1c番地に配置されていますので、ファイルの先頭の番地0x8000+0x4400番地を足すことで、EntryPointとProgramHeaderにアクセスできます。
②では、コードセグメントのファイル上でのサイズとヘッダのサイズを足し合わせています。上のコードがわかりづらいと思うので、以下に図を載せました。。。
ヘッダ部分もロード先に読み込むために、コードセグメントのファイル上のサイズとヘッダ部分のサイズを足し合わせる必要があったんです。。そのために、p_offsetをコードセグメントのファイル上のサイズに足しました。
③では、データセグメントの大きさを求めているのですが、ヘッダ部分を足し合わせないということ以外は、②と基本的に同じなのです。しかし、一点だけ述べておきたいことがあって、0xc400+16に余計に32を足し合わせている理由について述べておきたいです。プログラムヘッダテーブルの先頭を1番目すると、データセグメントは2番目に配置されるはずです。(え?という方は、リンカスクリプトをもう一度見てください。)なので、プログラムヘッダの大きさ32byte分足す必要あったのです。
今回のOSでは、コートセグメントをRAM全体を1つのセグメントとみなすことにします。次回の記事以降ページング実装したときのために、セグメント機能を実質的にオフにしておきたいのです。セグメントもページングも利用するメモリ管理は複雑すぎるのでセグメントを実質的にオフにします。
OSのコード用のセグメントディスクリプタの値を
DB 0xff, 0xff, 0x00, 0x00, 0x00, 0x9a, 0xcf, 0x00
としました。はりぼてOSとは異なる値になっています。
セグメントディスクリプタがよく分からなくても問題ないです。(ELF起動が目的の記事なので)
次に各セグメントを指定した番地以降に転送する処理を解説します。protect_on.binで該当するコードは以下になります。
はりぼてOSでは、転送処理を関数化しています。
以下に該当するコードを載せます。
memcpy:
MOV AL, [ESI]
ADD ESI, 1
MOV [EDI], AL
ADD EDI, 1
SUB ECX, 1
JNZ memcpy
RET
はりぼてOSと違うのは、1byte単位で転送する点です。(こっちの方が分かりやすいかなあと)
ESIに転送元の番地
EDIに転送先の番地
ECXに転送する大きさ(単位はbyte)
を設定して、memcpyをcallします。
実際にmemcpyを利用してセグメントの転送を行なったコードが下になります。(memcpyものせておきました。)
;コード領域のロード
CodeRegion_Transfer:
MOV ESI, 0xc400 ;転送元
MOV EDI, NIWAKA ;転送先
MOV ECX, [CodeFileSize] ;セグメントサイズ
CALL memcpy
;data領域のロード
DataRegion_Transfer:
MOV EAX, [ProgramHeader];programヘッダのオフセットアドレス
MOV EDX, 0xc400+32+4
ADD EAX, EDX
MOV ESI, [EAX]
ADD ESI, 0xc400
MOV EAX, [ProgramHeader] ;プログラムヘッダテーブルの先頭のオフセット番地
MOV EDX, 0xc400+32+8
ADD EAX, EDX
MOV EDI, [EAX]
MOV ECX, [DataFileSize] ;セグメントサイズ
CALL memcpy
;memcpyコードはprotect_on.binの下の方にあります。
memcpy:
MOV AL, [ESI]
ADD ESI, 1
MOV [EDI], AL
ADD EDI, 1
SUB ECX, 1
JNZ memcpy
RET
コードセグメントの転送に関しては、見ていただければお分かりになっていただけるかと思います。
解説したいのは、dataセグメントの転送についてです。転送元の番地と転送先の番地の計算が、コードセグメントに比べてややこしいものになっています。
まずは、転送元の番地を解説したいです。
データセグメントのアドレス(転送元)を求めるために、ファイル上でのオフセットアドレスを求めます。
セグメントに関する情報は、それに対応するプログラムヘッダのp_offset(対応するプログラムヘッダの先頭から4byte目に格納されている。)に格納されています。
そのオフセットと0xc400番地(RAM上でのファイルの先頭の番地)を足したものが、RAM上のデータセグメントの先頭の番地です。
具体的には、下のようなコードになります。
MOV EAX, [ProgramHeader] ;programヘッダテーブルのファイルからのオフセット
MOV EDX, 0xc400+32+4
ADD EAX, EDX ;p_offsetが格納されている番地
MOV ESI, [EAX] ;p_offset自体をESIに代入
ADD ESI, 0xc400 ;RAM上でのセグメントのアドレス(絶対番地)
です。これにより、転送元の番地を指定することが可能です。上のコードが分かりづらければ、図で書くと分かりやすいかと思います。
次に、転送先の番地を求める必要がありますが、ほとんど同じです。転送先の番地はプログラムヘッダのp_vaddrに格納されており、これをEDIにセットします。(p_vaddrは論理番地)
BSS領域の初期化はしないことにしました(手抜き)。というのも、ファイルの中にはBSS領域の実体は存在しないのでロードする必要はないわけですし、起動する分には支障は特にないんですよね。(もちろんメモリ管理をする際、RAMに配置したBSS領域の場所と大きさは把握しておかないといけないですよ。)
転送に関する説明は以上です。
次に実際にniwaka-os.binのコードセグメントにジャンプする処理の解説をします。
niwaka-os.binがどこから始まるのはどこで分かるかというと、
ELFヘッダのe_entryです。
なので、e_entryが示す番地にジャンプすればいいのですが、プロテクトモードに移行するためにfar jumpする必要があることには注意です。
ジャンプするために、far jumpをする必要があるのですが、それがちょっとややこしいものになっています。ここが終われば、実演に移れるので、一緒に頑張っていきましょう。。。。
far jump命令で指定できるオフセットアドレスは、"ラベル"もしくは"即値"しか指定できません。なので、
JUMP DWORD 16:[ENTRY_POINT]
のような書き方はできない訳です。(こういう書き方もできたらいいのに…)
ELFヘッダで示されているエントリポイントをどのように記述しようか迷ったのですが、以下のような記述でfar jumpすることにしました。最初の2行の命令によって、offset_addr番地以降の4byte分は、Entry_Pointで示される番地が書き込まれることになります。
MOV EAX, [ENTRY_POINT]
MOV [offset_addr], EAX
;以下の命令はfar jump命令
DB 0x66, 0xEA ;jump命令
offset_addr:
DB 0x00, 0x00, 0x00, 0x00 ;オフセットアドレス
DB 0x10, 0x00 ;セグメントセレクタ値
最初、普通にreadelfコマンドでエントリポイントを確認して、確認した値を即値とすることにしようか迷ったのですが、エントリポイントは、リンカスクリプトやniwaka-os.cを更新するたびに変わる恐れがあるので毎回確認しないといけなくなります。そういう面倒なことはできるだけ避けたいです。
far jumpしてしまったら、protect_on.binの役目は終わりです。次は、niwaka-os.binの出番です。
niwaka-os.bin(OS本体)
OS本体では、何をしようか迷ったんですけど、画面を赤くするだけにしました。
void hlt();
//以下のグローバル変数はData領域が配置されたかどうかの確認用
int a=0xffffffff;
int b=0x000000cc;
int c;
void niwaka_main(){
int i;
c = 0xffffffff;//0x0c006000番地に書き込まれているかのチェック
char *vram=0xa0000;
for(i=0; i < 0x10000; i++){
*(vram+i) = 12;
}
for(;;){
hlt();
}
}
niwaka-os.binで使用するhlt関数
bits 32
global hlt
section .text
hlt:
HLT
RET
niwaka-os.cとasmlib.asmに関して言うことがなく、初期化したグローバル変数と初期化していないグローバル変数を確認用に配置したことぐらいですかね。
実演
今回使用するプログラムをもう一度載せておきます。コピペして、手順通りにコンパイルなりアセンブルなりすれば動くはずです。
bits 16
ORG 0x7c00 ;アセンブラに対しての命令
SECTORS EQU 19 ;実際は、18だが、条件式の都合により1プラスしてる。
CYLINDERS EQU 20 ;
HEADS EQU 2 ;ヘッドの数
JMP init ;register_initにジャンプする
DB 0x90
DB "NIWAKA- "
DW 512
DB 1
DW 1
DB 2
DW 224
DW 2880
DB 0xf0
DW 9
DW 18
DW 2
DD 0
DD 2880
DB 0,0,0x29
DD 0xffffffff
DB "NIWAK "
DB "FAT12 "
RESB 18
;初期化
init:
MOV AX, 0x00
MOV BX, 0 ;BXアドレスは0番地で固定する
MOV SS, AX
MOV SP, 0x7c00
MOV DS, AX
MOV AX, 0x0800 ;バッファアドレスの開始番地を0x8200番地に設定する
MOV ES, AX
MOV DH, 0 ;ヘッド番号
MOV DL, 0 ;ドライブ番号
MOV CH, 0 ;シリンダ番号
MOV CL, 1 ;セクタ番号
JMP read ;readラベルに飛ぶ
;FDからデータを読み込んでいく
read:
MOV AH, 0x02 ;読み込みモード
MOV AL, 0x01 ;1セクタ分読み込む
INT 0x13 ;割り込み番号13を呼び出す
JC error ;ソフトウェア割り込み失敗時、ジャンプする
MOV AX, ES ;バッファアドレス1足す
ADD AX, 0x20
MOV ES, AX
ADD CL, 1 ;セクタ番号を1足す
CMP CL, SECTORS ;20とCL(セクタ番号を比較する)
JE count_head ;20と等しい場合、シリンダ番号とセクタ番号とヘッド番号の調整を行う。 JE → A==Bのとき、ラベルに示すアドレスにジャンプする。
JMP read
count_head:
MOV CL, 0x01 ;セクタ番号を1にする
ADD DH, 0x01 ;ヘッド番号を1足す
CMP DH, HEADS ;ヘッド番号が2の場合、ジャンプする。
JE count_cylinder
JMP read
count_cylinder:
MOV DH, 0x00 ;ヘッド番号を0にする
ADD CH, 0x01
CMP CH, CYLINDERS ;シリンダ番号が5まできたら終わり
JE video_mode ;シリンダ番号が、5のとき終了する
JMP read
video_mode:
MOV AH, 0x00
MOV AL, 0x13
INT 0x10
jmp_protect_on:
JMP 0xc200 ;0xc200番地へジャンプする
;このエラー処理適当すぎますが、許してください。
error:
HLT
JMP error
RESB 0x1fe-($-$$)
DB 0x55, 0xaa
;プロテクトモードオンにする
bits 16
ORG 0xc200 ;このプログラムは、0xc200番地に読み込まれる
;もうここで一気にページング入ろうか迷う
NIWAKA EQU 0x0c000000 ;niwaka-osのロード先
EntryPoint EQU 0x00006000 ;エントリポイント
ProgramHeader EQU 0x00006004 ;プログラムヘッダテーブルの先頭の番地
CodeFileSize EQU 0x00006008 ;コードセグメントのファイル上のサイズ
DataFileSize EQU 0x0000600c ;データセグメントのファイル上のサイズ
BssMemSize EQU 0x00006010 ;BSS領域のメモリ上のサイズ
;ヘッダ情報を指定した番地に書き込む
write_header:
MOV EAX, [0xc400+0x18]
MOV [EntryPoint], EAX
MOV EAX, [0xc400+0x1c]
MOV [ProgramHeader], EAX
MOV EAX, 0xc400+16
MOV EBX, [ProgramHeader]
ADD EBX, EAX
MOV ESI, [EBX]
MOV [CodeFileSize], ESI
MOV EAX, 0xc400+4
MOV EBX, [ProgramHeader]
ADD EBX, EAX
MOV ESI, [EBX]
ADD [CodeFileSize], ESI
MOV EAX, 0xc400+32+16
MOV EBX, [ProgramHeader]
ADD EBX, EAX
MOV ESI, [EBX]
MOV [DataFileSize], ESI ;32+16
;割り込み禁止する
CLI
; enable A20
CALL waitkbdout
MOV AL,0xd1
OUT 0x64,AL
CALL waitkbdout
MOV AL,0xdf
OUT 0x60,AL
CALL waitkbdout
GDT_SET:
LGDT [GDTR_INIT]
MOV EAX, CR0
OR EAX, 1
MOV CR0, EAX
JMP flash
flash:
MOV AX, 0x08
MOV DS, AX
MOV ES, AX
MOV FS, AX
MOV GS, AX
MOV SS, AX
;コード領域のロード
CodeRegion_Transfer:
MOV ESI, 0xc400 ;転送元
MOV EDI, NIWAKA ;転送先
MOV ECX, [CodeFileSize] ;セグメントサイズ
CALL memcpy
;data領域のロード
DataRegion_Transfer:
MOV EAX, [ProgramHeader];programヘッダのオフセットアドレス
MOV EDX, 0xc400+32+4
ADD EAX, EDX
MOV ESI, [EAX]
ADD ESI, 0xc400
MOV EAX, [ProgramHeader] ;プログラムヘッダテーブルの先頭のオフセット番地
MOV EDX, 0xc400+32+8
ADD EAX, EDX
MOV EDI, [EAX]
MOV ECX, [DataFileSize] ;セグメントサイズ
CALL memcpy
MOV ESP, 0x0c000000 ;スタック初期値
MOV EAX, [EntryPoint]
MOV [offset_addr], EAX
DB 0x66, 0xea
offset_addr:
DB 0x00, 0x00, 0x00, 0x00
DB 0x10, 0x00
GDT_INIT:
RESB 8
DB 0xff, 0xff, 0x00, 0x00, 0x00, 0x92, 0xcf, 0x00
DB 0xff, 0xff, 0x00, 0x00, 0x00, 0x9a, 0xcf, 0x00
GDTR_INIT:
DW 23 ;GDTの大きさ-1
DD GDT_INIT ;GDT_INITのアドレス
waitkbdout:
IN AL,0x64
AND AL,0x02
JNZ waitkbdout ; ANDの結果が0でなければwaitkbdoutへ
RET
memcpy:
MOV AL, [ESI]
ADD ESI, 1
MOV [EDI], AL
ADD EDI, 1
SUB ECX, 1
JNZ memcpy
RET
void hlt();
//以下のグローバル変数はData領域が配置されたかどうかの確認用
int a=0xffffffff;
int b=0x000000cc;
int c;
void niwaka_main(){
int i;
c = 0xffffffff;//0x0c006000番地に書き込まれているかのチェック
char *vram=0xa0000;
for(i=0; i < 0x10000; i++){
*(vram+i) = 12;
}
for(;;){
hlt();
}
}
bits 32
global hlt
section .text
hlt:
HLT
RET
ENTRY(niwaka_main);
SECTIONS{
. = 0x0c000000 + SIZEOF_HEADERS;
.text : {*(.text)}
. = 0x0c004000;
.data : {*(.data)*(.rodata*)}
. = 0x0c006000;
.bss : {*(.bss)}
}
早速コンパイルなりリンクなりアセンブルしていきましょう。
ここでの想定環境は、MacOSとqemu-system-i386とmtoolsとx86用にビルドされたgccとbinutilsです。
まずは、qemuとmtoolsとnasmとgccとbinutilsをインストールしましょう。
brew install qemu
brew install i386-elf-gcc
brew install i386-elf-binutils
brew install mtools
brew install nasm
次に、紹介したファイル達のコンパイル、アセンブルしていきます。
nasm -o ipl.bin ipl.asm
nasm -o protect_on.bin protect_on.asm
nasm -f elf32 -o asmlib.o asmlib.asm
i386-elf-gcc -O0 -c -m32 -fno-pic -o niwaka-os.o niwaka-os.c -Wall
次に、niwaka-os.binを生成するためにasmlib.oとniwaka-os.oをリンクします。
i386-elf-ld -O0 -nostdlib -nostartfiles -e niwaka_main -o niwaka-os.bin -T niwaka-os.ls niwaka-os.o asmlib.o
以上の手順を踏めば、ipl.binとprotect_on.binとniwaka-os.binが生成されます。
次に、FAT12でフォーマットされたディスクイメージを生成します。
mformat -f 1440 -C -B ipl.bin -i os.img
mcopy -i os.img protect_on.bin ::protec_on.bin
mcopy -i os.img niwaka-os.bin ::niwaka-os.bin
mformatは、ディスクをFAT12でフォーマットしてくれます。(オプションでfat32なんかもある。)
mcopyのオプションの説明をします。
mcopy -i ディスクイメージ コピー元ファイル名 ::コピー先ファイル名
となっております。このコマンドによって、ファイルをそのディスクイメージのフォーマットに適した形でディスクイメージに配置をしてくれます。
qemuでos.imgを実行します。
/usr/local/bin/qemu-system-i386 -m 4G -fda os.img -boot a
エミュレート起動できました!!(いつか実機でも起動できるようにしたい)
変数が適切な場所にロードされているかもチェックしましょう。
qemuには、RAMの中身をのぞくコマンドが用意されています。
qemuの画面で、
crtl+alt+2を同時に押してください。(さっき赤い画面を表示させましたよね、その画面上で押してください。)
このターミナル上で、
xp /100xb メモリ番地
を入力すると、指定したメモリ番地から100byte分画面に表示されます。
これを利用して、変数が正しく配置されているかをチェックしましょう。
そのためには、変数の論理番地を把握しておく必要があります。
これは、readelfコマンドを利用して確認します。
readelf -a niwaka-os.bin
とうつと、niwaka-os.binに関する様々な情報が確認できます。一番下の方に変数に関する情報が載っていて、そこで変数の番地を確認してみましょう。
上の画像によると、変数aは、0x0c004000番地から4byte分
変数bは、0x0c004004番地から4byte分
変数cは、0x0c006000番地から4byte分
に展開されていなければ、プログラムが正しく動作しない模様です。
変数の配置も適切なようなので、うまくいきました!!!
(変数の配置が、リトルインディアンになっていることに注意してください。IntelのCPUは、リトルインディアンのため)
最後に
僕の記事を読んで、ELFのことを少しでも理解できたという人がいたら嬉しいです。次回は、ページング移行してみたという記事を出そうかなと。(移行するってだけの内容なんですけど、需要ありそうなら記事にしてみようと思います。)
もしくは、30日でできる!OS自作入門がもうすぐ読み終わるので、それに関する感想文を記事にしようかなと。。
OSに関する知識が断片的すぎるので、体系的に学べる機会が欲しい。。。
それではまた
追記(2019/11/17)
誤字などがあった場合は、コメントで指摘してくださるとありがたいです。
また、気楽に質問してくださるとありがたいです。
不可解な現象(知っている方は教えてください。。。)
※誰か助けてください。なぜなのでしょうか…ディレクトリ領域には、64byte分の領域がmcopyによって書き込まれるはずが、なぜか128byte分書き込まれていました。なぜなのでしょうか…
図5(os.imgのディレクトリ領域付近をダンプした画像)
追記(2019/11/18)
不可解な現象は解決しました!!
ディレクトリ領域に書き込めるファイル名の長さは
ファイル名(8文字)+拡張子(3文字)の計11文字なのですが、
niwaka-os.binは、12文字となっています。。。(protect_on.binも同様)
そのため、拡張するために128byteになったようです。
参考文献
CPUとOSがどう協力しあうのかを知りたい方必見(紙媒体は絶版となっているので、入手が困難) [32ビットコンピュータをやさしく語る はじめて読む486 ](https://www.amazon.co.jp/32ビットコンピュータをやさしく語る-はじめて読む486-アスキー書籍-蒲地輝尚-ebook/dp/B00OCF5YUA/ref=sr_1_1?__mk_ja_JP=カタカナ&keywords=初めて読む486&qid=1573736828&s=books&sr=1-1)この記事書いておいてなんですが、これは読んでる途中です。。。(25日くらいから、ページングやフロッピーディスクのデバイスドライバやELFに興味を持ち出しました。いい加減読まなきゃ)
30日でできる! OS自作入門
下の本はとにかく分かりやすいです。ELFヘッダやプログラムヘッダについてもっと知りたいという方にオススメです。
リンカ・ローダ実践開発テクニック―実行ファイルを作成するために必須の技術
ELFヘッダの構造体の各要素のバイトサイズなんかを参考にしました。ELFとは何か?を知らずに下を見ても訳がわからないと思います。。。。。
Executable and Linkable Format (ELF)