|
ブートローダその7 メモリアクセス
|
前回までの内容
これまでで、
- メモリはセグメントとうい単位に区切られている
- セグメントレジスタCS、DS、SS、ES、FS、GSでセグメントの開始位置を指定できる
- セグメントの開始位置はセグメントレジスタの値×0x10
- アクセスできる最大のアドレスは約1MB
ということがわかりました。それでは前回の続きでセグメントにアクセスする方法について説明していきます
がその前にセグメントレジスタの種類について
セグメントレジスタの種類
セグメントレジスタにはCS、DS、SS、ES、FS、GSがありました
このレジスタはそれぞれに違う役割を持っています
大きくわけると、
- コードセグメント(CS):プログラムの命令が置いていある場所を指定するセグメント
- データセグメント(DS、ES、FS、GS):データが置いてある場所を指定するセグメント
- スタックセグメント(SS):スタックが置いてある場所を指定するセグメント(重要です、後述します)
となります。各セグメントレジスタとレジスタが指定するセグメントについては下記図のようになります
セグメントレジスタとセグメントとアセンブラ言語
これまでセグメントレジスタとセグメントの関係を見てきました
ではアセンブラ言語とセグメントの関係はどうなっているのでしょうか
この前作りました最初のブートローダをもう一度見てみましょう
;/********************************************************************************
; File:boot.asm
; Description:Bootloader
;
;********************************************************************************/
[BITS 16]
ORG 0x7C00
;==================================================================================
; BIOS parameter blocks(FAT12)
;==================================================================================
JMP BOOT ;BS_jmpBoot
BS_OEMName DB "MyOS "
BPB_BytsPerSec DW 0x0200 ;BytesPerSector
BPB_SecPerClus DB 0x01 ;SectorPerCluster
BPB_RsvdSecCnt DW 0x0001 ;ReservedSectors
BPB_NumFATs DB 0x02 ;TotalFATs
BPB_RootEntCnt DW 0x00E0 ;MaxRootEntries
BPB_TotSec16 DW 0x0B40 ;TotalSectors
BPB_Media DB 0xF0 ;MediaDescriptor
BPB_FATSz16 DW 0x0009 ;SectorsPerFAT
BPB_SecPerTrk DW 0x0012 ;SectorsPerTrack
BPB_NumHeads DW 0x0002 ;NumHeads
BPB_HiddSec DD 0x00000000 ;HiddenSector
BPB_TotSec32 DD 0x00000000 ;TotalSectors
BS_DrvNum DB 0x00 ;DriveNumber
BS_Reserved1 DB 0x00 ;Reserved
BS_BootSig DB 0x29 ;BootSignature
BS_VolID DD 0x20120627 ;VolumeSerialNumber 日付を入れました
BS_VolLab DB "MyOS " ;VolumeLabel
BS_FilSysType DB "FAT12 " ;FileSystemType
;/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
;
; BOOT
;
;/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
BOOT:
CLI
HLT
TIMES 510 - ($ - $$) DB 0
DW 0xAA55
この前のアセンブラソースコードを載せてみました
起動直後はCS、DS、SS、ES、FS、GSの値はすべて0x0000として考えてみましょう
まず最初に
ORG 0x7C00
が出てきました。これはこのプログラムが0x7C00であることを意味していました
JMP BOOT ;BS_jmpBoot
次にジャンプ命令がありました。このJMP命令はどこに置かれるのでしょうか?
ブートローダは0x7C00にコピーされるのでこのプログラムも0x7C00をベースアドレスとして作りました
そのプログラムの第0番目がこのJMP命令となります
ベースアドレスが0x7C00ですのでこのJMP命令は0x7C00+0番目のバイト=0x7C00に
置かれることを想定しています。違うアドレスに置いても命令自体は実行されますが、
BOOTのアドレスは0x7C00をベースアドレスにしたアドレスになっています
(この理由については少し下で説明いたします)
さてここでコードセグメントCSの値は0x0000と考えてみました。
するとコードセグメントの開始アドレスは0x00000となります
コードセグメントの大きさは0xFFFFですので0x7C00はコードセグメント内に入っています
ではどうやってコードセグメント内の0x7C00にアクセスしているのでしょうか?
コードセグメント内のアドレスアクセス
今まで説明してきたレジスタ以外に更に特殊なレジスタがあります
IP(Instruction Pointer)という命令があるアドレスを指すレジスタで命令ポインタといいます
0x00000から始まるコードセグメント内の0x7C00にあるこのJMP命令にアクセスするために
IPの値は0x7C00になっています。これで0x7C00のJMP命令を実行することができます
(BIOSからJMP命令か何かでこのアドレスに飛んでくるのでEIPの値が0x7C00になっています)
EIPの値はソフトウェアが故意に書き換えることはできません
その行の命令が終わればハードウェアが自動的に
次の命令のアドレスに書き換えてくれますので、特に意識する必要はありません
が、プログラムがどのセグメントにあるかをコードセグメントで指定する必要がありますので
CSの値には注意が必要です
データセグメント内のアドレスアクセス
コードセグメントにあるデータへのアクセスについてはDSレジスタを使います(もちろんほかのデータセグメントを表す
セグメントレジスタでもできます。DSレジスタ以外はアセンブラ言語で明示的に指定する必要がありますが)
ここでの例ではDSレジスタの値は0x0000で0x0000から始まるセグメントでした
ではそのセグメント内のデータにアクセスするにはどうすればよいのでしょうか
これまで出てきたように0x7C00がベースアドレスとなります
例えばワードサイズ(ワードは16ビットのサイズでした)の”BPB_BytsPerSec” にアクセスするときは
MOV AX, WORD [BPB_BytsPerSec]
と記述できます。この例では”BPB_BytsPerSec”の値をAXレジスタに格納しています
この命令を上記の”CLI”命令の前に書いてアセンブルしてみてください
アセンブルされたバイナリをバイナリエディタで開いてみてください
すると下記図のように表示されます
(PCはリトルエンディアンなので最小位のデータが先に配置されますので注意してください)
バイナリで見ると”BPB_BytsPerSec”は0x000Bにあります
0x000Bにありますが実際には0x7C00に配置されますので実行するときは0x7C0Bに配置されます
MOV命令の引数を見てみますと”BPB_BytsPerSec”のアドレスとして0x7C0Bが指定されています
(リトルエンディアンなので最小位のバイトが先にきて0x0B7Cに見えます)
0x7C00がベースアドレスなので0x7C00に0x000Bを足して0x7C0Bが”BPB_BytsPerSec”の
DSセグメント内のアドレスになります
セグメントとセグメント内のアドレス
セグメントとそのセグメント内のアドレスを記述するときには次のように記述します
(セグメント内のアドレスはオフセットといいます)
セグメントレジスタの値:オフセット
ですので、このBPB_BytsPerSecのアドレスを表したいときには
DSセグメントレジスタの値:オフセット(BPB_BytsPerSecの場所)=0x0000:0x7C0B
と記述します。また同じ場所を示すのにセグメントの開始アドレスを変えて
0x07C0:0x000B
と表記しても同様に0x7C0Bのアドレスを意味します
(このとき左側の値はセグメントレジスタの値で0x10を掛ける前の値です)
アドレスの表記の方法がわかりました。DSレジスタで指定するセグメント以外のセグメントには
どうやってアクセスするのでしょうか
ES、FS、GSレジスタのセグメントへのアクセス
DSレジスタで指定するセグメントは一番使用するセグメントですので、
特にDSレジスタを指定するすることはありません
その他のデータセグメントレジスタであるES、FS、GSのセグメントはアクセスするには
明示的にレジスタを指定しなければなりません。
例えばESレジスタが指定するセグメントにアクセスするときは
MOV [ES:BX], AX
と書きます。ここでアクセスするアドレスは
[セグメントレジスタの値:オフセット]
と記述します。これでDSレジスタで指定するセグメント以外のセグメントにもアクセスできます
また、オフセットとして汎用レジスタを指定することができます
そして、汎用レジスタは命令によって役割が当てられると説明したかと思います。
その役割がこのデータセグメントのオフセットとしての役割をはたします
特にSI、DI、BPレジスタは
- SI:Source Index 読み込むデータのオフセットを指定します
- DI:Destination Index 出力するデータのオフセットを指定します
- BP:スタックポインターのベースアドレスとして働きます。この場合SPがオフセットとなります。
論理アドレスとリニアアドレス
[セグメントレジスタの値:オフセット]の組み合わせのことを論理アドレス(Logical Address)といいます
対して論理アドレスから、セグメントの開始アドレス+オフセット=アクセスしたいアドレス
を計算できました。この計算したアドレスをリニアアドレス(Linear Address)といいます
スタックセグメント内のアドレスアクセス
スタックセグメント内のアドレスアクセスもコードセグメント、データセグメントと同じようにやります
単純にセグメントレジスタSSに値を入れてオフセット(スタックセグメント内の場所)を指定してやればいいだけです
スタックセグメントのオフセットは汎用レジスタのSPレジスタで指定します
SPレジスタはスタックポインタといい関数コールしたときの呼び出し元アドレスを保存したり、
PUSH命令、POP命令などのスタックを操作するときに使用する極めて重要なレジスタです
(SPレジスタは汎用レジスタですが主にスタックの操作をする専用レジスタと考えたほうがいいと思います)
スタックは大きいアドレス(ここではSPレジスタの値が0xFFFCとしました)から使っていきます
(関数コール、PUSH命令のたびに0xFFFCから値を引いていき使いすぎると最終的には0x0000
まで行ってしまいます)
ですので使用できるメモリのなるべく大きなところを今回使用しています
さてここまでで、セグメントを操作することができるようになりました
それではセグメントを操作するブートローダを作ってみましょう
起動直後のセグメントレジスタの初期化
ではセグメントレジスタを初期化する処理を追加してみます
;/********************************************************************************
; File:boot.asm
; Description:Bootloader
;
;********************************************************************************/
[BITS 16]
ORG 0x7C00
;==================================================================================
;
; BIOS parameter blocks(FAT12)
;
;==================================================================================
JMP BOOT ;BS_jmpBoot
BS_OEMName DB "MyOS "
BPB_BytsPerSec DW 0x0200 ;BytesPerSector
BPB_SecPerClus DB 0x01 ;SectorPerCluster
BPB_RsvdSecCnt DW 0x0001 ;ReservedSectors
BPB_NumFATs DB 0x02 ;TotalFATs
BPB_RootEntCnt DW 0x00E0 ;MaxRootEntries
BPB_TotSec16 DW 0x0B40 ;TotalSectors
BPB_Media DB 0xF0 ;MediaDescriptor
BPB_FATSz16 DW 0x0009 ;SectorsPerFAT
BPB_SecPerTrk DW 0x0012 ;SectorsPerTrack
BPB_NumHeads DW 0x0002 ;NumHeads
BPB_HiddSec DD 0x00000000 ;HiddenSector
BPB_TotSec32 DD 0x00000000 ;TotalSectors
BS_DrvNum DB 0x00 ;DriveNumber
BS_Reserved1 DB 0x00 ;Reserved
BS_BootSig DB 0x29 ;BootSignature
BS_VolID DD 0x20120627 ;VolumeSerialNumber 日付を入れました
BS_VolLab DB "MyOS " ;VolumeLabel
BS_FilSysType DB "FAT12 " ;FileSystemType
;/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
;
; BOOT
;
;/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
BOOT:
CLI
; Initialize Data Segment
XOR AX, AX
MOV DS, AX
MOV ES, AX
MOV FS, AX
MOV GS, AX
XOR BX, BX
XOR CX, CX
XOR DX, DX
; Initialize Stack Segment and Stack Poiniter
MOV SS, AX
MOV SP, 0xFFFC
HLT
TIMES 510 - ($ - $$) DB 0
DW 0xAA55
これでセグメントレジスタの初期化ができました
(コードセグメントは明示的に初期化してません。ちょっと容量を減らすために。。。)
Vritual Boxで起動できましたでしょうか
画面的には前回まででと何らかわりません。。。。
XOR AX, AX
この命令はAXレジスタどうしで排他的論理和をとっています
排他的論理和は同じ値のものを0、違う値のものを1にする性質があります
ですので、AXレジスタどうしは同じ値なので結局AXレジスタの値は0にクリアされることになります
これはほんのりちょっとしたテクニックで、MOV命令などでAXレジスタに0x0000を入れてもいいのですが
MOV命令では0x0000という値をメモリからとってきます
XORはメモリにアクセスせずにAXレジスタにアクセスするだけで0クリアできるので、少し動作が速くなります
次の行から0クリアしたAXレジスタの値をDS、ES、FS、GSに入れてセグメントレジスタに入れておきます
(Tips:この場合はレジスタどうしのデータ移動なのでメモリアクセスはしません)
そしてついでにBX、CS、DX汎用レジスタも後々使うので変数の初期化と同様に初期化しておきます
次の命令はSSレジスタとSPレジスタを初期化しています
次回はスタックについてもうちょっとだけ説明いたします