LoginSignup
35
25

More than 3 years have passed since last update.

ELF形式のOSを起動してみた。

Last updated at Posted at 2019-11-15

ELF形式のOSを起動してみた。

こんにちは、にわかです。
タイトル通りなのですが、ELF形式のOSを起動してみます。
この記事を読んでくれたら、ELF形式のOSを起動できるようになります。

注意として、UEFIの力を借りてOSをブートするというものになっておらず、
「30日でできる! OS自作入門」と似たようなやり方でブートします。(「30日でできる! OS自作入門」では、自力でリアルモードからプロテクトモードに移行する際に、OSの起動の手続きも行なっていた。)

前回、ELF形式のヘッダ情報を解析するプログラム記事を載せたのですが、その続編に当たります。
ELF形式のOSをロードする方法を紹介します。
とりあえず、起動できた後の画面を載せておきます。
OS3.png
赤くするだけです笑(赤くするだけなので、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形式の大雑把な構成図を載せます。
image.png
図1

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が、フロッピーディスクの先頭から(2*20*18*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のためのリンカスクリプトを紹介します。

niwaka-os.ls
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を展開した図が以下になります。

image.png

図2(赤い矢印の先はdataセグメントのつもりです。。。。修正は気が向いたら)

図2の矢印の意味は、各ヘッダの格納されているオフセット値(ファイルの先頭からの)が示している場所です。

ipl.bin

これは、ほぼ「30日できる!OS自作入門」のプログラムをほぼ真似ています。(フロッピーディスクをロードする処理が違うだけでやっていることはほとんど同じ)

ipl.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と等しい場合、ジャンプする
    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メモリマップを以下に載せました。(数値の単位は番地です。)
image.png

図3

niwaka-os.binの先頭の番地が、なぜ0xc400番地かと言いますと
FAT12フォーマットされたディスク上では0x4400番地以降に配置されているので、0x4400+0x8000(0x8000番地を足す理由は、0x8000番地以降にフロッピーディスクの内容をロードしているから)

上の図を適宜見つつ、下以降の解説を読んでみてください。

protect_on.bin

protect_on.binが肝となるプログラムです。

まずは基本アイデアとなるものを図で描きました。

image.png
図3(プログラムヘッダを転送しないような表記になっており、後日修正します。)

図3の通り、0xc400番地以降に存在するELFヘッダとプログラムヘッダテーブルとコードセグメント、dataセグメント、bssセグメントを0x0c000000番地に転送します。

以下が、protect_on.binになります。(MOV命令とADD命令やJMP命令ぐらいしか使っておらず、単純なプログラムになっています。)

protect_on.asm
;プロテクトモードオンにする
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にアクセスできます。

②では、コードセグメントのファイル上でのサイズとヘッダのサイズを足し合わせています。上のコードがわかりづらいと思うので、以下に図を載せました。。。

image.png
図4

ヘッダ部分もロード先に読み込むために、コードセグメントのファイル上のサイズとヘッダ部分のサイズを足し合わせる必要があったんです。。そのために、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本体では、何をしようか迷ったんですけど、画面を赤くするだけにしました。

niwaka-os.c

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関数

asmlib.asm
bits 32
global hlt

section .text

hlt:
    HLT 
    RET

niwaka-os.cとasmlib.asmに関して言うことがなく、初期化したグローバル変数と初期化していないグローバル変数を確認用に配置したことぐらいですかね。

実演

今回使用するプログラムをもう一度載せておきます。コピペして、手順通りにコンパイルなりアセンブルなりすれば動くはずです。

ipl.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

protect_on.asm
;プロテクトモードオンにする
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
niwaka-os.c

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();
    }
}
asmlib.asm
bits 32
global hlt

section .text

hlt:
    HLT 
    RET
niwaka-os.ls
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

結果は、、、、、、
OS3.png

エミュレート起動できました!!(いつか実機でも起動できるようにしたい)

変数が適切な場所にロードされているかもチェックしましょう。
qemuには、RAMの中身をのぞくコマンドが用意されています。

qemuの画面で、
crtl+alt+2を同時に押してください。(さっき赤い画面を表示させましたよね、その画面上で押してください。)

そうすると、下のような画面に切り替わります。
スクリーンショット 2019-11-14 21.38.44.png

このターミナル上で、

xp /100xb メモリ番地

を入力すると、指定したメモリ番地から100byte分画面に表示されます。
これを利用して、変数が正しく配置されているかをチェックしましょう。
そのためには、変数の論理番地を把握しておく必要があります。
これは、readelfコマンドを利用して確認します。

readelf -a niwaka-os.bin

とうつと、niwaka-os.binに関する様々な情報が確認できます。一番下の方に変数に関する情報が載っていて、そこで変数の番地を確認してみましょう。
スクリーンショット 2019-11-14 20.49.45.png
上の画像によると、変数aは、0x0c004000番地から4byte分
変数bは、0x0c004004番地から4byte分
変数cは、0x0c006000番地から4byte分
に展開されていなければ、プログラムが正しく動作しない模様です。

qemuで確認してみると、、、、
スクリーンショット 2019-11-14 20.47.25.png

スクリーンショット 2019-11-14 20.47.39.png
変数の配置も適切なようなので、うまくいきました!!!
(変数の配置が、リトルインディアンになっていることに注意してください。IntelのCPUは、リトルインディアンのため)

最後に

僕の記事を読んで、ELFのことを少しでも理解できたという人がいたら嬉しいです。

次回は、ページング移行してみたという記事を出そうかなと。(移行するってだけの内容なんですけど、需要ありそうなら記事にしてみようと思います。)

もしくは、30日でできる!OS自作入門がもうすぐ読み終わるので、それに関する感想文を記事にしようかなと。。

OSに関する知識が断片的すぎるので、体系的に学べる機会が欲しい。。。
それではまた

追記(2019/11/17)
誤字などがあった場合は、コメントで指摘してくださるとありがたいです。
また、気楽に質問してくださるとありがたいです。

不可解な現象(知っている方は教えてください。。。)


※誰か助けてください。なぜなのでしょうか…

ディレクトリ領域には、64byte分の領域がmcopyによって書き込まれるはずが、なぜか128byte分書き込まれていました。なぜなのでしょうか…スクリーンショット 2019-11-15 18.36.09.png
図5(os.imgのディレクトリ領域付近をダンプした画像)

追記(2019/11/18)
不可解な現象は解決しました!!
ディレクトリ領域に書き込めるファイル名の長さは
ファイル名(8文字)+拡張子(3文字)の計11文字なのですが、
niwaka-os.binは、12文字となっています。。。(protect_on.binも同様)
そのため、拡張するために128byteになったようです。

参考文献

CPUとOSがどう協力しあうのかを知りたい方必見(紙媒体は絶版となっているので、入手が困難)
32ビットコンピュータをやさしく語る はじめて読む486

この記事書いておいてなんですが、これは読んでる途中です。。。(25日くらいから、ページングやフロッピーディスクのデバイスドライバやELFに興味を持ち出しました。いい加減読まなきゃ)
30日でできる! OS自作入門

下の本はとにかく分かりやすいです。ELFヘッダやプログラムヘッダについてもっと知りたいという方にオススメです。
リンカ・ローダ実践開発テクニック―実行ファイルを作成するために必須の技術

ELFヘッダの構造体の各要素のバイトサイズなんかを参考にしました。ELFとは何か?を知らずに下を見ても訳がわからないと思います。。。。。
Executable and Linkable Format (ELF)

35
25
5

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
35
25