OSを自作してみる12 ~メモリ管理2:kernelのHigher Half化~



時間なくて、ちょっとサボり気味だった。。。

※過去回の記事とソースコードは下記から入手できる。
https://github.com/Shadow5523/osdev/releases
また、ここで使用するコードはGitHubからダウンロードできる。
https://github.com/Shadow5523/osdev/releases/tag/version0.10.1

目次

 
 

概要

前回までは、物理メモリを4kb単位でメモリを割り当て/開放を行えるようにし、ページングを有効化するところまでを行った。
今回はカーネルを0xC0000000で動作させるように変更を行っていく。boot.sについては詳しい解説等をやらなかったため、本記事ではその解説も行う。

この自作OSのカーネルは0x00100000にイメージがロードされるようになっており、カーネルイメージのすぐ後ろにユーザ空間が来るような構造となっている。このような場合、カーネルなどに機能を追加するなどによって、カーネル空間が大きくなってしまうと、ユーザ空間の開始アドレスなどを変更する必要があり、その都度ユーザ空間内で動作するアプリケーションのアドレスをずらす必要があるので管理がとても面倒になる。

一方、現在の主なOS(LinuxやWindowsなど)では仮想アドレスの0x00000000 – 0xBFFFFFFFがユーザ空間(ユーザランドとも言う)、仮想アドレス0xC0000000 – 0xFFFFFFFFはカーネル空間(カーネルランドとも言う)で動作している。このように区別することでカーネル空間の大きさが変わってもユーザ空間へ影響を与えることがないので管理が簡単になる。

カーネルを0xC0000000で動作させるには、仮想アドレスを0x00000000と0xC0000000を物理メモリの最初の部分とマッピングさせたあと、ページングを有効化する。有効化したあとカーネルは0xC0000000にあり、そのカーネルへジャンプしたあとにマッピングを解除し、Cのコードを呼び出す。

 
 

boot.s

1.boot.sを編集していく。最初にboot.sの全体のコードを載せてみる。

 
 
2.上から順に見ていく。まずは変数を定義する。GASでは以下のような構文で変数を宣言し初期化する。
.set [変数名], [値]

名変数の役割は以下の通り。この5つの変数はマルチブートヘッダの設定を行っている。

変数名 役割
ALIGN ロードされたモジュールのメモリをページ境界(4KB境界)にあわせる。
MEMINFO GRUBから multiboot info構造体を取得するときに使用可能なメモリの総量の値をセットする。
FLAGS GRUBが参照するヘッダーのフラグを設定する。ALIGNとMEMINFOのフラグをセットする。
MAGIC 0x1BADB002で初期化。これはマルチブートのマジックナンバーなのでこれで固定。
CHECKSUM チェックサムを設定。

以下の変数は、ページングを行うために初期化する。

変数名 役割
KERNEL_VBASE カーネルをロードする仮想アドレスを設定。値は0xC0000000。
KERNEL_PNUM ページディレクトリのインデックスを設定。ページのサイズは1ページ4MBで計算する。

最後の変数で、カーネルのスタックのサイズを設定したりする。

変数名 役割
STACKSIZE カーネルスタックのサイズを設定。サイズは16KB。
loader リンカのエントリポイントを設定する。

 
 
3. .dataセクションを編集していく。まず、アライメントを4kb単位に設定する。GASでは、.sectionディレクティブの後ろにセクション名を指定することで、そのあとの処理はそのセクション名の領域に配置される。領域は次の.sectionディレクティブまで。.balignは指定した値でその領域(ここでは.dataセクション)のデータをアライメントしてくれる。(.alignディレクティブも存在するが、アーキテクチャによって少し動作が異なるためここでは使用しない)

セクションの宣言
.section [セクション名]

アライメントを指定
.balign [size]

 
 
4.ページングを有効化するためにページディレクトリの作成を行う。_boot_pdというラベルを宣言して、その直後にページングの処理を行っていく。仮想メモリの最初の4MBと仮想メモリ0xC0000000から4MBの部分を、物理メモリの最初の4MBへとマッピングする。ディレクティブの構文は以下の通り。

ラベルの宣言
ラベル名:

整数型のデータを配置する
.long [値]

繰り返しデータを配置する
.fill [繰り返す値], [サイズ(bytes)], [値]

0x00000083はページディレクトリエントリに設定フラグとなっていて、有効にしているフラグの役割は以下の通り。

ビット番号 役割
bit0 Pフラグ。1をセットすると、そのページは物理メモリにロードされている。
bit1 RWフラグ。1をセットすると、そのページは読み書きできるようになる。
bit7 PSフラグ。1をセットすると、ページサイズは4MB。

今回は上記以外のbitは0で設定する。
 
 
5.マルチブートに関する設定をしていく。.multibootセクションを作成し、項番2で作成した値を配置する。

 
 
6..textセクションを編集していく。ページングを使用するために、CR3レジスタにページディレクトリのアドレスを設定する。ただ、ページングを有効化する前に設定を行うのでページディレクトリの物理アドレス(0xC0000000を引いた値)を設定する。

 
 
7.今回のページングでは、1ページ4MBでページングするためCR4レジスタのPSEフラグ(bit4)を有効にする。まずCR4レジスタの元の値をecxレジスタに一時的に退避させたあとに、bit4にフラグをセットし、再度CR4レジスタに格納する。

 
 
8.次の部分で0x80000000をCR0レジスタにセットし、ページングを有効化していく。項番6と8の部分は前回ページングを有効化(OSを自作してみる11 ~メモリ管理1:物理メモリ管理の実装とページングの有効化~)のところとほぼ同じ。

 
 
9.命令ポインタ(EIP)が物理アドレスを保持しているので、leal命令で_startのラベルのアドレス(この中にカーネルを呼び出す処理が入っている)をecxレジスタへ格納し、格納されたアドレスへジャンプしていくことによって正しい仮想アドレス(0xC0000000)へジャンプする。動きがほぼmov命令と同じであるが、こちらはフラグレジスタに影響を与えず、完全にアドレスのみをロードする命令である。その後、ecxレジスタに格納されたアドレスへジャンプし、処理を_startラベルへと移行する。ecxレジスタの先頭に*がついているが、これはレジスタのアドレスを指定しているという意味になる。

直接_startのアドレスへ飛ばずに一旦ecxレジスタへ退避しているのは、CPUのパイプライン処理で先にある命令を解読し、アクセスするアドレスに不整合が生じて想定していない実行結果となることを防ぐためである。
(jmp命令を実行すると先取りしているjmp命令以後の命令を破棄する
 
 
8._startシンボルのタイプを”関数”として設定する。また、カーネルは見かけ上0xC0000000にあり、最初に設定したページディレクトリのマッピング 情報は必要ないので解除する。使用するディレクティブの構文は以下の通り。

シンボルのタイプを指定する
.type [シンボル名], @[function | object]
  
TLBをフラッシュする
invlpg [メモリアドレス]

 
 
9.mov命令でespレジスタにスタックの一番上位のアドレスを入れて、ebxにマルチブートの情報(Higher Half化されたもの)をマッピングする。その後、ebxにマルチブートの情報を、eaxにはマルチブートのマジックナンバーを渡す。

 
 
10.ここでC言語で書いたkernel本体をcall命令を使用して呼び出す。制御をカーネルに渡したあとはcli命令で外部からの割り込みが行われないようにし、システム側でhlt命令が来るまで無限ループする。

関数を呼び出す
call [関数名]

特定のアドレスにジャンプする(ラベルを指定することも出来る)
jmp [アドレス]

 
 
11.bssセクションを編集し、中に16KBスタックを作成する。.skip 命令は、指定したバイト数分メモリを空ける事が出来る。

指定したバイト数分メモリを空ける
.skip [size],( [value])

 
 

linker.ld

1.次にリンカスクリプト書いていく。全体は以下通り。

 
 
2.ENTRY()でプログラムの開始アドレス指定する。ページング開始処理等を書いているのが_loaderラベルからになっているのでこれを指定。また、OUTPUT_FORMATでは、生成する実行ファイルフォーマットを指定する。elf32-i386を指定。

 
 
3.SECTIONSコマンド記述する。このブロックの中に.bss .text .data等のセクションの配置場所指定する。.で現在のアドレスを指定することが出来、ここでは仮想アドレス 0xC0100000をカーネルの開始アドレスとして指定している。しかし実際には物理メモリ1MBにマッピングしている。

 
 
4..textセクションが配置される物理アドレスを定義する。ALIGN(4K)でセクションを4KB配置されるように指定し、AT()ではは物理アドレスを指定する。.textで動作するアドレスを算出するにはまず、ADDR()でそのセクションが動作するアドレスを取得し(ここでは仮想アドレスが返される)、そこから0xC0000000を引くことによって物理アドレスを求める。

「*(.multiboot)」や「*(.text)」ではリンカスクリプトで定義したセクションを指定する。こうすることによって.textセクションの領域にはGASで記述した「.text」と「.miltiboot」が配置される。
「__kernel_start」にはカーネルの開始アドレスが格納される。
 
 
5.同様に.rodata, .data, .bssの各セクションが配置される物理アドレスを定義する。

.rodataセクションはRedHatのGCCを使用している場合、名前が拡張されたセクション名が使用されるので「*(.rodata*)」のようにワイルドカードを使用する。.bssセクション内に配置されている「COMMON」セクションは、.bssセクションと同様に初期化されていない変数が格納されるが、.bssセクションとの違いは、変数が重複して定義されている場合は「COMMON」セクションに配置され、どこでも定義されていない場合は「.bss」 セクションに配置される。「 __kernel_end = .;」ではカーネルの終了アドレスを取得している。

リンカスクリプトの編集はこれで完了。これでkernelは0xC0000000に配置され動作するようになるが、これによってテキストバッファのアドレスや、ultiboot_info構造体のアドレス情報があるメモリアドレスが変更になるため、次にこれらを修正する。

 
 

Cのソースファイルを変更

1.drivers/terminal.cを開き、テキストバッファのアドレスを変更する。

 
 
2.multiboot_info_t構造体のアドレス情報へもアクセスできなくなるので、0xC0000000を足し、仮想アドレスでアクセスする。
また、仮想アドレスで動作するようになると、ポインタを使用してstrcpyやmemcpyは使用できなくなる。(まだメモリアロケータは作っていないので。。。)なので、下のコードで使用しているポインタの宣言を配列の宣言へと変更する。

 
 
3.最後にkernel/kernel.cを開き、前回行った物理メモリの初期化、ページングの有効化を行う処理の部分をコメントアウトする。また、前に作成した 「getmmap()」とmultibootのマジックナンバーを取得し、表示してみる。

 
 

動作確認

OSを起動し、cr0レジスタが0x80000011になってればページングは有効になっている。また、変数を宣言し、そのアドレスを見てみると、0xC010〜から始まるアドレスに配置されている。

 
 
次回はPDE, PTEを使用した4kbページングを有効化したいと思う。脱線して他のこともやるかも…。

Leave a Reply

Your email address will not be published. Required fields are marked *