この記事は、Li Zhong 氏の「X86 アセンブリ言語: リアル モードからプロテクト モードへ」を参考にしています。
序文
ユーザー プログラムをガイドする単純なマスター ブート レコードを手動で実装します。これは理解に役立ちます。
- メイン ブートローダーのワークフロー
- アセンブリ コード レベルで関数を呼び出す方法 (関数呼び出しの原則)
- アセンブリコードレベルでハードディスクを読み書きする方法(CPUと周辺機器の相互作用)
内容は多くはありませんが、様々な知識を総合的に応用できます。
マスター ブート レコード
以下内容はwikiより引用
マスター ブート レコード (マスター ブート レコード、略称: MBR) は、マスター ブート セクターとも呼ばれ、ハード ディスクにアクセスするためにコンピューターの電源を入れたときに読み取る必要がある最初のセクターです。 is (シリンダー、ヘッド、セクター) = (0, 0, 1)
システム起動のプロセス:
- システムハードウェア (メモリを含む) をチェックする BIOS パワーオンセルフテスト
- マスター ブート レコードを読み取ります。BIOS は、ディスクの最初のセクター (つまり、MBR セクター) をメモリ アドレス 0000:7C00H に読み取ります。
- MBR のブート コードに従ってブートローダーを開始します。
- ブートローダーはオペレーティング システムのカーネルをロードします
マスター ブート レコードの変更
BIOS がセルフテスト後に指定したプログラムを実行するように、独自のマシン命令をメイン ブート セクタに書き込むことができます。このような:
サンプル アセンブリ コード demo.asm:
mov ax, 0x1234
jmp $
;--------------------------------------------------------------
; $ 表示当前行的汇编地址
; $$ 表示第一行的汇编地址
; $-$$得到前面代码占用的字节数
times (512-2-($-$$)) db 0 ;主引导扇区总长度必须512字节,用0占位
db 0x55, 0xaa ;主引导扇区最后两个字节必须是0x55aa
nasm でコンパイルして、demo.bin ファイルを取得します。
Li Zhong 先生の仮想ハード ディスク書き込みツール Vhd ライターを使用して、demo.bin ファイルの内容を Vhd 仮想ハード ディスクのメイン ブート セクターに書き込みます。
デバッグのために bochsdbg 仮想マシンを起動します。これは、マスター ブート レコードが物理アドレス 0x7c00 に読み込まれるためb 0x7c00
ですc
。
コマンドを使用してs
シングル ステップ デバッグを行い、r
コマンドを使用してレジスタの値を観察する
マスター ブート レコード ブート ユーザー プログラム
メインのブート プログラムを自分で作成すると、ユーザー プログラムなどの他のコンテンツをロードする (制御を放棄する) ことができます。ユーザー プログラムをロードするには、メイン ブートローダに必要な情報がいくつかあります。ユーザー プログラムの全長、プログラムの実行エントリなどです。したがって、コンピューターの世界のさまざまなプロトコル ヘッダーと同様に、ユーザー プログラムの先頭にヘッダー セクションを追加する必要があります。
ヘッダー セクションには、次のものが含まれます。
- プログラム全体の長さは 4B のダブルワードを占めます。
- エントリ ポイント セグメント内のオフセット アドレス。1 ワード、つまり 2B を占めます。
- エントリ ポイント セグメントのアセンブリ アドレスは、4B であるダブル ワードを占有します。
- 1 ワード、つまり 2B を占める再配置エントリの数
- 対応する n エントリ。各エントリはダブル ワード、つまり 4B を占有します。
ユーザー プログラム コードのサンプル コード:
;------------------------------------------------------
SECTION head_section vstart=0
;用户程序头部段
app_length: dd app_end
entry_offset: dw start ;用户程序入口点段内偏移地址
entry_sec_addr: dd section.code_section.start ;用户程序入口点段汇编地址
realloc_tbl_num: dw (head_sec_end-code_sec_base_addr)/4 ;每项4字节
code_sec_base_addr: dd section.code_section.start ;代码段汇编地址
data_sec_base_addr: dd section.data_section.start ;数据段汇编地址
stack_sec_base_addr: dd section.stack_section.start ;栈段汇编地址
head_sec_end:
;------------------------------------------------------
SECTION code_section align=16 vstart=0 ;代码段
put_string:
;input: ds:si 源字符串起始地址
; es:di 目标位置起始地址
push ax ;保存寄存器
@1:
mov al,[ds:si]
cmp al,0 ;字符串结尾
je end_loop
mov ah,0x0f ;字体格式
mov [es:di],ax
add di,2
inc si
jmp @1
end_loop:
pop ax ;恢复寄存器
ret
start:
;设置用户程序的栈空间
xor ax,ax
mov sp,ax
xor ax,[stack_sec_base_addr]
mov ss,ax
;程序主体部分,显示数据段文本
mov ax,[data_sec_base_addr]
mov ds,ax
xor si,si
mov ax,0xb800
mov es,ax
xor di,di
call put_string
;------------------------------------------------------
SECTION data_section align=16 vstart=0 ;数据段
db 'Hello World! ___Written by cy.',0
;------------------------------------------------------
SECTION stack_section align=16 vstart=0 ;栈段
resb 256
;------------------------------------------------------
SECTION end_section
app_end:
ヘッダー セグメントを使用して、ユーザー プログラムをメモリに読み込み、制御を与え (セグメント アドレス レジスタを変更)、ロード作業を完了します。簡単にするために、次の規則を作成します。
- ユーザー プログラムは、ディスクの 100 番目のセクターに順番に格納されます。
- メイン ブートローダは、常にユーザー プログラムを物理メモリの 0x10000 にロードします。
ハードディスクの読み書き
CPU と周辺 I/O デバイスは、I/O インターフェースを使用してデータを送信する必要があります. I/O インターフェースには、ポートと呼ばれるいくつかのレジスタが統合されています. 異なるポートには、コマンドの送信やデータの送信などの異なる機能があります. CPU は、ポートからデータを読み書きし、周辺機器と対話します。in
およびコマンドを使用して、out
ポートからデータを読み書きします。
in
命令:
;一般使用ax和dx寄存器
;选择ax还是al取决于端口的位宽
mov dx,0xc30 ;从0x3c0端口读取数据
in ax,dx
;in al,dx
out
命令:
mov dx,0xc30
out dx,ax ;往0x3c0端口写入数据
;out dx,al
私たちのコンピュータでは、ハードディスク インターフェイスには 0x1f0 から 0x1f7 まで順番に 8 つのポートがあり、読み書きするポート番号は dx に格納され、データは ax/al に格納されます。
ハードディスクの読み書きの最小単位はセクタなので、ハードディスクは典型的なブロックデバイスです。ハードディスクの読み取りと書き込みには、次の 2 つのモードがあります。
- CHS モード (シリンダー ヘッド セクター) シリンダー ヘッド セクター トリプレット
- LBA モード (Logic Block Address) 論理セクタ番号
ハードディスクを読み取るための基本的な手順:
- 読み取るセクタ数を 0x1f2 ポートに書き込みます
mov dx,0x1f2
mov al,0x1
out dx,al
- 選択したハードディスク (マスター/スレーブ) の 32 ビット データ、読み取り/書き込みモード (CHS/LBA)、およびセクター番号をポート 0x1f3、0x1f4、0x1f5、および 0x1f6 に書き込みます。
1010 0000 00000000 00000000 00000000
共写入32位,低28位表示扇区号
第29和31位固定为1
第28位为0表示读写主硬盘,为1表示读写从硬盘
第30位为0表示CHS读写模式,为1表示LBA读写模式在·
mov dx,0x1f2
mov al,0x2
out dx,al;0000 0002
inc dx
xor al,al
out dx,al;0000 0000
inc dx
out dx,al;0000 0000
inc dx
inc dx
mov al,0xe0
out dx,al;1110 0000
inc dx
;11100000 00000000 00000000 00000002
;选择LBA读写模式,读写主硬盘的2号逻辑扇区
- ポート 0x1f7 に 0x20 コマンドを書き込み、ハードディスクの読み取りを要求します
mov dx,0x1f7
mov al,0x20
out dx,al
- ハードディスクの読み取り準備が整っているかどうかを確認する
ポート 0x1f7 から 1B データを読み取る
mov dx,0x1f7
in al,dx
00000000
共读取8位
第7位为1表示硬盘正忙
第3位为1表示硬盘已经可以交换数据
ポーリングは、データが読み取れるまで待機します
mov dx,0x1f7
.waits:
in dx,al
and al,0x88 ;取出第3位和第7位
cmp al,0x08
jnz .waits
- ハードディスクの読み取り準備が整ったら、データ ポート 0x1f0 からデータを読み取ります。
;假定DS:BX已经指向数据要存放的逻辑地址
mov dx,0x1f0
mov cx,256 ;512B=256word
.readlo:
in dx,ax
mov [bx],ax
add bx,2
loop .readlo
ユーザープログラムをロード
ハードディスクの読み取り方法を理解したら、次の目標はユーザー プログラム全体をロードすることです。最初に、ユーザー プログラムのヘッダー セクションをメモリにロードし、ヘッダー セクション内の情報を解析する必要があります。
以前に次の契約を締結しました。
- ユーザー プログラムは、ディスクの 100 番目のセクターに順番に格納されます。
- メイン ブートローダは、常にユーザー プログラムを物理メモリの 0x10000 にロードします。
したがって、論理セクター番号を定数として定義し、後でアクセスする必要があるため、物理アドレスは 4 バイトのメモリに格納されます。
disk_lba_address equ 100 ;定义常数,约定的逻辑扇区号
phy_base_address : dd 0x10000 ;约定的用户程序要存放的物理地址
次に、最初にセクター 100 (ヘッダー セグメントを含む) をメモリに読み取り、ヘッダー セグメントの最初のダブル ワード (プログラムの全長) に従って残りのすべてのセクターを読み取ります。
具体的な手順は次のとおりです。
- セクター 100 (ヘッダーを含む) を読み取り、プログラムの全長を解析します。1セクタ512Bは全長/512がセクタ数であり、割り切れない場合はさらに1セクタ必要となる。
mov ax,[cs:phy_base_address]
mov dx,[cs:phy_base_address+2]
mov bx,16
div bx ;左移四位得到逻辑段地址
mov ds,ax ;令ds指向将要载入的逻辑段地址
xor bx,bx ;对应read_hard_disk的输入
mov si,disk_lba_address ;对应read_hard_disk的输入
xor di,di
call read_hard_disk ;读出头部段
mov ax,[0x00] ;dx:ax = 程序总长度
mov dx,[0x02]
mov bx,512
div bx ;得到用户程序占用的扇区数
cmp dx,0
jz @1 ;占用整数个扇区
inc ax ;不是刚好占用整数个扇区,需要继续读取一个扇区才能读完
@1:
dec ax ;已经读了一个扇区,减去
- ループを使用して残りのセクターを読み取る
mov cx,ax ;需要读取的扇区数量
@2:
mov ax,ds;
add ax,0x20 ;0x20<<4=0x200=512B,得到下一个512B的逻辑段地址
mov ds,ax
xor bx,bx
inc si ;读下一个逻辑扇区
call read_hard_disk
loop @2 ;循环读取剩余扇区
ユーザープログラムの移動
ユーザー プログラム全体をメモリに正常にロードしたので、次のタスクはユーザー プログラムに制御を渡すことです.明らかに、cs:ip レジスタをユーザー プログラム エントリ ポイントの論理アドレスに変更する必要があります。
計算方法は?ヘッダー セクションには、次の 2 つの情報があります。
-
ユーザー プログラムのエントリ ポイント セグメントのアセンブリ アドレス。cs、つまりこの値からセグメントのベースアドレスの値を計算するのは実際には非常に簡単です.ロードされたユーザープログラムの開始物理アドレスはphy_base_addressであり、2つを加算するだけで実際の物理アドレスになります.ユーザプログラムのエントリポイントのセグメント セグメントベースアドレス cs の値を取得するために 4 ビットをシフトします
-
ユーザープログラムのエントリポイントのセグメント内のオフセットアドレス。ip の値に直接対応します
結果を計算した後、後で使用できるように、ヘッダー セグメント内のユーザー プログラムのエントリ ポイント セグメントのアセンブリ アドレスに結果を直接バックフィルします。ip の値がヘッダー セグメントのオフセット アドレス [0x04] に格納され、cs の値がヘッダー セグメントのオフセット アドレス [0x06] に格納されているのは "偶然" です. コマンドを直接使用することができますjmp far
Jump実行のためのユーザープログラムのエントリポイントに。
jmp far [0x04]
ユーザープログラムには複数の SECTION があり、それらのセグメントのベースアドレスが分からないと使いづらいです。このとき、再配置エントリが役に立ちます. 上記の計算プロセスに従って、再配置エントリ内のすべての SECTION のセグメント ベース アドレスを計算し、対応するメモリ空間に 1 つずつ埋め戻します. SECTION では、セグメント ベース アドレスを対応する値に設定するだけで済みます。
;计算用户程序入口点逻辑段地址
mov ax,[0x06]
mov dx,[0x08]
call cal_segment_base
mov [0x06],ax ;将计算得到的用户程序入口点段基地址回填到头部段[0x06]处
mov cx,[0x0a] ;重定位表项数
mov bx,[0x0c] ;第一项
@3:
mov ax,[bx]
mov dx,[bx+0x02]
call cal_segment_base
mov [bx],ax ;将计算得到的段基地址回填到重定位表项处
add bx,4
loop @3
メイン ブートローダが制御を取り戻す [拡張]
ユーザープログラム実行後は、制御権を上位に戻す必要があり、そうしないと1つのプログラムしか実行できず、原理は非常に単純で、各レジスタの値を保存して復元するだけですが、cs/ipの場合は/ss/sp register 保存と復元には細心の注意が必要.たとえば、cs は変更されていますが、ip は変更されていません.このとき、他のコマンドを直接指します (cs:ip が変更されています)。
さらに、ip レジスタの値を直接読み書きすることはできません。call 命令を覚えていますか? その原則は、ip レジスタの値をスタックにプッシュすることです。そのため、次のように call 命令を介して間接的に ip レジスタの値を取得できます。
push cs
call get_ip ;等价于push ip
get_ip:
pop ax ;得到ip的值
push ax
シームレスに見えますが、この時点での IP は命令の開始アドレスを指しているため、この方法で保存された IP は間違っています。pop ax
明らかに、必要なのはpush ax
次の命令の開始アドレスです。このような問題はまだまだ多く、実践の過程で遭遇するでしょう。
メイン ブートローダは制御コードを取得します (メイン ブートローダ部分):
push ax ;保存环境
push bx
push cx
push dx
push ds
push es
push di
push si
push cs ;【拓展内容】用户程序执行完之后将控制权还给主引导程序
call get_ip ;等价于push ip
get_ip:
pop ax ;得到ip的值
add ax,delta_2-get_ip
push ax
jmp far [0x04] ;==执行用户程序==
delta_2: ;【拓展内容】主引导程序拿回控制权之后应该从此处开始执行
pop si
pop di
pop es
pop ds
pop dx
pop cx
pop bx
pop ax ;恢复环境
メイン ブート プログラムは、制御コード (ユーザー プログラム部分) を取得します。
mov si,ss ;【拓展内容】记录主引导程序的栈空间
mov di,sp
;设置用户程序的栈空间
xor ax,ax
mov sp,ax
xor ax,[stack_sec_base_addr]
mov ss,ax
push di
push si ;【拓展内容】记录主引导程序的栈空间
;用户程序代码部分
;..............
;..............
;用户程序代码部分
pop si ;【拓展内容】用户程序执行完之后将控制权还给主引导程序
pop di
mov ss,si
mov sp,di
retf ;= pop ip & pop cs
しかし、私は常々、制御権が奇妙なものを引き渡すためにユーザー プログラムの積極的な協力を必要としていると感じています...
確認
メイン ブートローダー my_mbr.asm の完全なアセンブリ コード:
disk_lba_address equ 100 ;定义常数,约定的逻辑扇区号
SECTION mbr vstart=0x7c00
;初始化栈寄存器
xor ax,ax
mov ss,ax
mov sp,ax
mov ax,[cs:phy_base_address]
mov dx,[cs:phy_base_address+2]
mov bx,16
div bx ;左移四位得到逻辑段地址
mov ds,ax ;令ds指向将要载入的逻辑段地址
xor di,di
mov si,disk_lba_address ;对应read_hard_disk的输入
xor bx,bx ;对应read_hard_disk的输入
call read_hard_disk ;读出头部段
mov ax,[0x00] ;dx:ax = 程序总长度
mov dx,[0x02]
mov bx,512
div bx ;得到用户程序占用的扇区数
cmp dx,0
jz @1 ;占用整数个扇区
inc ax ;不是刚好占用整数个扇区,需要继续读取一个扇区才能读完
@1:
dec ax ;已经读了一个扇区,减去
cmp ax,0
jz direct ;无扇区需要读了
push ds ;保存用户程序的逻辑段地址
mov cx,ax;
@2:
mov ax,ds
add ax,0x20 ;0x20<<4=0x200=512B,得到下一个512B的逻辑段地址
mov ds,ax
xor bx,bx
inc si ;读下一个逻辑扇区
call read_hard_disk
loop @2 ;循环读取剩余扇区
pop ds ;恢复用户程序的逻辑段地址
direct:
;计算用户程序入口点逻辑段地址
mov ax,[0x06]
mov dx,[0x08]
call cal_segment_base
mov [0x06],ax ;将计算得到的用户程序入口点段基地址回填到头部段[0x06]处
mov cx,[0x0a] ;重定位表项数
mov bx,0x0c ;第一项的地址
@3:
mov ax,[bx]
mov dx,[bx+0x02]
call cal_segment_base
mov [bx],ax ;将计算得到的段基地址回填到重定位表项处
add bx,4
loop @3
push ax ;保存环境
push bx
push cx
push dx
push ds
push es
push di
push si
push cs ;【拓展内容】用户程序执行完之后将控制权还给主引导程序
call get_ip ;等价于push ip
get_ip:
pop ax ;得到ip的值
add ax,delta_2-get_ip
push ax
jmp far [0x04]
delta_2: ;【拓展内容】主引导程序拿回控制权之后应该从此处开始执行
pop si
pop di
pop es
pop ds
pop dx
pop cx
pop bx
pop ax ;恢复环境
jmp $
;------------------------------------------------------------------------
cal_segment_base:
;input: dx:ax = 段汇编地址
;output:ax = 段基地址
push dx
add ax,[cs:phy_base_address]
adc dx,[cs:phy_base_address+2] ;带进位加法
;ds:ax=段逻辑地址,要取出第4-19位
shr ax,4
ror dx,4
or ax,dx
pop dx
ret
;------------------------------------------------------------------------
read_hard_disk:
;从硬盘读入一个扇区
;input: DI:SI = 扇区号
; DS:BX = 物理内存
push di ;保存相关寄存器
push si
push ds
push bx
push ax
push dx
push cx
mov dx,0x1f2
mov al,1
out dx,al
inc dx
mov ax,si ;写入扇区号、读写模式、硬盘
out dx,al
inc dx
shr ax,8
out dx,al
inc dx
mov ax,di
out dx,al
inc dx
mov al,0xe0
out dx,al
inc dx ;发送请求读命令
mov al,0x20
out dx,al
.waits: ;轮询硬盘是否可读
in al,dx
and al,0x88
cmp al,0x08
jnz .waits
mov dx,0x1f0 ;开始读硬盘
mov cx,256
.readlo:
in ax,dx
mov [bx],ax
add bx,2
loop .readlo
pop cx
pop dx
pop ax
pop bx
pop ds
pop si
pop di ;恢复相关寄存器
ret
;------------------------------------------------------------------------
phy_base_address : dd 0x10000 ;约定的用户程序要存放的物理地址
times (512-2-($-$$)) db 0 ;主引导扇区总长度必须512字节,用0占位
db 0x55, 0xaa ;主引导扇区最后两个字节必须是0x55aa
ユーザー プログラムの完全なアセンブリ コード my_app.asm:
;------------------------------------------------------
SECTION head_section vstart=0
;用户程序头部段
app_length: dd app_end
entry_offset: dw start ;用户程序入口点段内偏移地址
entry_sec_addr: dd section.code_section.start ;用户程序入口点段汇编地址
realloc_tbl_num: dw (head_sec_end-code_sec_base_addr)/4 ;每项4字节
code_sec_base_addr: dd section.code_section.start ;代码段汇编地址
data_sec_base_addr: dd section.data_section.start ;数据段汇编地址
stack_sec_base_addr: dd section.stack_section.start ;栈段汇编地址
head_sec_end:
;------------------------------------------------------
SECTION code_section align=16 vstart=0 ;代码段
put_string:
;input: ds:si 源字符串起始地址
; es:di 目标位置起始地址
push ax ;保存寄存器
@1:
mov al,[ds:si]
cmp al,0 ;字符串结尾
je end_loop
mov ah,0x0f ;字体格式
mov [es:di],ax
add di,2
inc si
jmp @1
end_loop:
pop ax ;恢复寄存器
ret
start:
mov si,ss ;【拓展内容】记录主引导程序的栈空间
mov di,sp
;设置用户程序的栈空间
xor ax,ax
mov sp,ax
xor ax,[stack_sec_base_addr]
mov ss,ax
push di
push si ;【拓展内容】记录主引导程序的栈空间
mov ax,[data_sec_base_addr] ;程序主体部分,显示数据段文本
mov ds,ax
xor si,si
mov ax,0xb800
mov es,ax
xor di,di
call put_string
pop si ;【拓展内容】用户程序执行完之后将控制权还给主引导程序
pop di
mov ss,si
mov sp,di
retf ;= pop ip & pop cs
;------------------------------------------------------
SECTION data_section align=16 vstart=0 ;数据段
db 'Hello World! ___Written by cy.',0
;------------------------------------------------------
SECTION stack_section align=16 vstart=0 ;用户栈段
resb 256 ;为栈空间预留256B
;------------------------------------------------------
SECTION end_section
app_end:
それらを個別にコンパイルして my_mbr.asm と my_app.asm を生成し、Li Zhong 氏の仮想ハードディスク書き込みツールを使用して仮想ハードディスクに書き込みます。
仮想ボックス仮想マシンを実行すると、予期したテキスト コンテンツが正常に表示されることがわかります。これは、メインのブート プログラムがユーザー プログラムを正しく誘導したことを証明しています。
また、Bochsdbg を使用して、プログラムの実行プロセス、特に制御権が前後に切り替えられたときの各レジスタおよびスタック空間の状態変化をデバッグおよび観察することもできます。
まとめ
オペレーティング システムを起動する実際のメイン ブート プログラムはそれほど単純ではありませんが、この実験を書き留めておけば、関連するプロセスの概要を理解することもできます。紙で話すよりも、実際には細かい問題が多く、1日で終わると思っていた実験が、3、4日かかってしまい、今までの知識を十分に理解できていないことが問題になっています。使用中で。テクノロジーを学び、さらに練習することによってのみ、ギャップをチェックし、ギャップを埋め、記憶を深める効果を得ることができます.