汇编语言-实现一个简单的主引导记录(MBR)引导用户程序
本文参考李忠老师的《X86汇编语言:实模式到保护模式》
前言
自己手动实现一个简单的主引导记录来引导用户程序,有助于了解
- 主引导程序的工作流程
- 在汇编代码层面如何调用函数(函数调用的原理)
- 在汇编代码层面如何读写硬盘(CPU与外围设备的交互)
内容虽然不多,但能够综合运用到多方面的知识。
主引导记录
以下内容参考自wiki
主引导记录(Master Boot Record,缩写:MBR),又叫做主引导扇区,是计算机开机后访问硬盘时所必须要读取的首个扇区,它在硬盘上的三维地址为(柱面,磁头,扇区)=(0,0,1)
系统开机的过程:
- BIOS 加电自检,对系统硬件(包括内存)进行检查
- 读取主引导记录。BIOS 将磁盘第一个扇区(也就是 MBR 扇区)读入内存地址 0000:7C00H 处
- 根据 MBR 中的引导代码启动引导程序
- 引导程序加载操作系统内核
修改主引导记录
我们可以将自己写的机器指令写入到主引导扇区,这样 BIOS 自检后就会执行我们指定的程序。就像这样:
示例汇编代码 demo.asm:
mov ax, 0x1234jmp $;--------------------------------------------------------------; $ 表示当前行的汇编地址; $$ 表示第一行的汇编地址; $-$$得到前面代码占用的字节数times (512-2-($-$$)) db 0 ;主引导扇区总长度必须512字节,用0占位db 0x55, 0xaa ;主引导扇区最后两个字节必须是0x55aa
使用 nasm 进行编译得到 demo.bin 文件
用李忠老师的虚拟硬盘写入工具 Vhd writer 将 demo.bin 文件中的内容写入到 Vhd 虚拟硬盘的主引导扇区中
启动 bochsdbg 虚拟机进行调试,因为主引导记录将被加载到物理地址 0x7c00 处,我们使用命令b 0x7c00
、c
连续执行指令,直到 ip 寄存器指向0x7c00时暂停运行
使用s
命令进行单步调试,使用r
命令观察寄存器的值
主引导记录引导用户程序
我们自己编写主引导程序,就可以让其加载其他内容(将控制权让出),比如某个用户程序。要加载用户程序,有一些必要的信息要给主引导程序:用户程序的总长度、程序的执行入口等。因此,就如同计算机世界中的各种协议头部一样,在用户程序的最前面,需要加上一个头部段。
头部段包含以下内容:
- 程序的总长度,占一个 double word,也即 4B
- 入口点段内偏移地址,占一个 word,也即 2B
- 入口点段汇编地址,占一个 double word,也即 4B
- 重定位表项数,占一个 word,也即 2B
- 与之对应的n项表项,每个表项占一个double word,也即 4B
用户程序代码示例代码:
;------------------------------------------------------
SECTION head_section vstart=0;用户程序头部段app_length: dd app_endentry_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_loopmov ah,0x0f ;字体格式mov [es:di],axadd di,2inc sijmp @1
end_loop:pop ax ;恢复寄存器retstart:;设置用户程序的栈空间xor ax,axmov sp,axxor ax,[stack_sec_base_addr]mov ss,ax;程序主体部分,显示数据段文本mov ax,[data_sec_base_addr]mov ds,axxor si,simov ax,0xb800mov es,axxor di,dicall 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_sectionapp_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,要读写的端口号存放在 dx 中,数据存放在 ax/al 中
读写硬盘的最小单位是扇区,因此硬盘是典型的块设备。读写硬盘有两种模式:
- CHS模式(Cylinder Head Sector)柱面-磁头-扇区三元组
- LBA模式(Logic Block Address)逻辑扇区号
读取硬盘的基本步骤:
- 将要读取的扇区数量写入 0x1f2 端口
mov dx,0x1f2mov al,0x1out dx,al
- 将选择的硬盘(主/从)、读写模式(CHS/LBA)以及扇区号共32位数据写入0x1f3、0x1f4、0x1f5、0x1f6端口
1010 0000 00000000 00000000 00000000
共写入32位,低28位表示扇区号
第29和31位固定为1
第28位为0表示读写主硬盘,为1表示读写从硬盘
第30位为0表示CHS读写模式,为1表示LBA读写模式在·
mov dx,0x1f2mov al,0x2out dx,al;0000 0002inc dx xor al,alout dx,al;0000 0000inc dx out dx,al;0000 0000inc dx inc dx mov al,0xe0out dx,al;1110 0000inc dx ;11100000 00000000 00000000 00000002;选择LBA读写模式,读写主硬盘的2号逻辑扇区
- 向端口 0x1f7 写入 0x20 命令,请求读硬盘
mov dx,0x1f7mov al,0x20out dx,al
- 判断硬盘是否已经准备好被读取
从 0x1f7 端口读取 1B 数据
mov dx,0x1f7in al,dx
00000000
共读取8位
第7位为1表示硬盘正忙
第3位为1表示硬盘已经可以交换数据
轮询等待,直到可以读取数据
mov dx,0x1f7
.waits:in dx,aland al,0x88 ;取出第3位和第7位cmp al,0x08jnz .waits
- 硬盘准备好被读取后,从数据端口 0x1f0 读取数据
;假定DS:BX已经指向数据要存放的逻辑地址mov dx,0x1f0mov cx,256 ;512B=256word
.readlo:in dx,axmov [bx],ax add bx,2loop .readlo
加载用户程序
明白怎么读取硬盘后,接下来的目标是加载整个用户程序。首先要将用户程序头部段加载入内存中,并解析头部段中的信息。
我们在前面已经做好如下约定:
- 用户程序在磁盘第 100 个扇区中顺序存储
- 主引导程序始终将用户程序加载到物理内存 0x10000 处
因此,我们将逻辑扇区号定义为常数,而物理地址因为后面需要访问,因此存放到一个4字节内存中:
disk_lba_address equ 100 ;定义常数,约定的逻辑扇区号phy_base_address : dd 0x10000 ;约定的用户程序要存放的物理地址
接下来,我们先将第 100 号扇区(包含了头部段)读入内存,并根据头部段中的第一个双字——程序的总长度来读入剩余的全部扇区
具体步骤如下:
- 读入第 100 号扇区(包含了头部段),并解析程序的总长度。一个扇区512B,总长度/512就是扇区个数,需要注意的是,无法整除时,还需要一个扇区。
mov ax,[cs:phy_base_address]mov dx,[cs:phy_base_address+2]mov bx,16div bx ;左移四位得到逻辑段地址mov ds,ax ;令ds指向将要载入的逻辑段地址xor bx,bx ;对应read_hard_disk的输入mov si,disk_lba_address ;对应read_hard_disk的输入xor di,dicall read_hard_disk ;读出头部段mov ax,[0x00] ;dx:ax = 程序总长度mov dx,[0x02]mov bx,512div bx ;得到用户程序占用的扇区数cmp dx,0jz @1 ;占用整数个扇区inc ax ;不是刚好占用整数个扇区,需要继续读取一个扇区才能读完
@1:dec ax ;已经读了一个扇区,减去
- 使用loop循环来读取剩余扇区
mov cx,ax ;需要读取的扇区数量
@2:mov ax,ds;add ax,0x20 ;0x20<<4=0x200=512B,得到下一个512B的逻辑段地址mov ds,axxor bx,bxinc si ;读下一个逻辑扇区call read_hard_diskloop @2 ;循环读取剩余扇区
用户程序的重定位
我们已经成功的将整个用户程序加载入内存之中,接下来的任务是将控制权交给用户程序,显然需要修改 cs:ip 寄存器为用户程序入口点的逻辑地址。
如何计算呢?在头部段中有两个信息:
用户程序的入口点 段汇编地址。要通过该值计算出 cs,也即段基地址的值,其实很简单,用户程序被加载的起始物理地址是 phy_base_address,只需两者相加就是用户程序入口点的段实际物理地址,左移四位即得到段基地址 cs 的值
用户程序的入口点 段内偏移地址。直接对应了 ip 的值
计算出结果后,为了后续使用方便,我们直接将结果回填到 头部段中 用户程序的入口点段汇编地址 处。很“巧合”的是,在头部段偏移地址为 [0x04] 处存放的是 ip 的值,在头部段偏移地址为 [0x06] 处存放的是 cs 的值,直接使用jmp far
指令即可跳转到用户程序入口点执行。
jmp far [0x04]
在用户程序中,还有多个SECTION,如果不知道它们的段基地址,就难以使用。这时,重定位表项就派上用场了——我们按照上面的计算过程,将重定位表项中所有SECTION的段基地址计算出来,并一一回填到对应的内存空间中,这样,要访问这些SECTION的时候,只需要将段基地址设置为对应的值即可。
;计算用户程序入口点逻辑段地址mov ax,[0x06]mov dx,[0x08]call cal_segment_basemov [0x06],ax ;将计算得到的用户程序入口点段基地址回填到头部段[0x06]处mov cx,[0x0a] ;重定位表项数mov bx,[0x0c] ;第一项
@3:mov ax,[bx]mov dx,[bx+0x02]call cal_segment_basemov [bx],ax ;将计算得到的段基地址回填到重定位表项处add bx,4loop @3
主引导程序拿回控制权【扩展】
用户程序执行完之后,肯定需要将控制权交回给上级,否则只能运行一个程序了,原理很简单,只需保存各个寄存器的值并恢复即可,但是对于cs/ip/ss/sp寄存器的保存和恢复需要特别小心,比如修改了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 bxpush cxpush dxpush dspush espush dipush sipush cs ;【拓展内容】用户程序执行完之后将控制权还给主引导程序call get_ip ;等价于push ip
get_ip:pop ax ;得到ip的值add ax,delta_2-get_ippush axjmp far [0x04] ;==执行用户程序==
delta_2: ;【拓展内容】主引导程序拿回控制权之后应该从此处开始执行pop sipop dipop espop dspop dxpop cxpop bxpop ax ;恢复环境
主引导程序拿回控制权代码(用户程序部分):
mov si,ss ;【拓展内容】记录主引导程序的栈空间mov di,sp;设置用户程序的栈空间xor ax,axmov sp,axxor ax,[stack_sec_base_addr]mov ss,axpush dipush si ;【拓展内容】记录主引导程序的栈空间;用户程序代码部分;..............;..............;用户程序代码部分pop si ;【拓展内容】用户程序执行完之后将控制权还给主引导程序pop dimov ss,simov sp,diretf ;= pop ip & pop cs
However,总感觉控制权需要用户程序主动配合交出怪怪的… …
验证
主引导程序完整汇编代码 my_mbr.asm:
disk_lba_address equ 100 ;定义常数,约定的逻辑扇区号
SECTION mbr vstart=0x7c00;初始化栈寄存器xor ax,axmov ss,axmov sp,axmov ax,[cs:phy_base_address]mov dx,[cs:phy_base_address+2]mov bx,16div bx ;左移四位得到逻辑段地址mov ds,ax ;令ds指向将要载入的逻辑段地址xor di,dimov 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,512div bx ;得到用户程序占用的扇区数cmp dx,0jz @1 ;占用整数个扇区inc ax ;不是刚好占用整数个扇区,需要继续读取一个扇区才能读完
@1:dec ax ;已经读了一个扇区,减去cmp ax,0jz direct ;无扇区需要读了push ds ;保存用户程序的逻辑段地址mov cx,ax;
@2:mov ax,dsadd ax,0x20 ;0x20<<4=0x200=512B,得到下一个512B的逻辑段地址mov ds,axxor bx,bxinc si ;读下一个逻辑扇区call read_hard_diskloop @2 ;循环读取剩余扇区pop ds ;恢复用户程序的逻辑段地址direct:;计算用户程序入口点逻辑段地址mov ax,[0x06]mov dx,[0x08]call cal_segment_basemov [0x06],ax ;将计算得到的用户程序入口点段基地址回填到头部段[0x06]处mov cx,[0x0a] ;重定位表项数mov bx,0x0c ;第一项的地址
@3:mov ax,[bx]mov dx,[bx+0x02]call cal_segment_basemov [bx],ax ;将计算得到的段基地址回填到重定位表项处add bx,4loop @3push ax ;保存环境push bxpush cxpush dxpush dspush espush dipush sipush cs ;【拓展内容】用户程序执行完之后将控制权还给主引导程序call get_ip ;等价于push ip
get_ip:pop ax ;得到ip的值add ax,delta_2-get_ippush axjmp far [0x04]
delta_2: ;【拓展内容】主引导程序拿回控制权之后应该从此处开始执行pop sipop dipop espop dspop dxpop cxpop bxpop ax ;恢复环境jmp $
;------------------------------------------------------------------------
cal_segment_base:;input: dx:ax = 段汇编地址;output:ax = 段基地址push dxadd ax,[cs:phy_base_address]adc dx,[cs:phy_base_address+2] ;带进位加法;ds:ax=段逻辑地址,要取出第4-19位shr ax,4ror dx,4or ax,dxpop dxret
;------------------------------------------------------------------------
read_hard_disk:;从硬盘读入一个扇区;input: DI:SI = 扇区号; DS:BX = 物理内存push di ;保存相关寄存器push sipush dspush bxpush axpush dxpush cxmov dx,0x1f2mov al,1out dx,alinc dxmov ax,si ;写入扇区号、读写模式、硬盘out dx,alinc dxshr ax,8out dx,alinc dxmov ax,diout dx,alinc dxmov al,0xe0out dx,alinc dx ;发送请求读命令mov al,0x20out dx,al.waits: ;轮询硬盘是否可读in al,dxand al,0x88cmp al,0x08jnz .waitsmov dx,0x1f0 ;开始读硬盘mov cx,256
.readlo:in ax,dxmov [bx],axadd bx,2loop .readlopop cxpop dxpop axpop bxpop dspop sipop 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_endentry_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_loopmov ah,0x0f ;字体格式mov [es:di],axadd di,2inc sijmp @1
end_loop:pop ax ;恢复寄存器retstart:mov si,ss ;【拓展内容】记录主引导程序的栈空间mov di,sp;设置用户程序的栈空间xor ax,axmov sp,axxor ax,[stack_sec_base_addr]mov ss,axpush dipush si ;【拓展内容】记录主引导程序的栈空间mov ax,[data_sec_base_addr] ;程序主体部分,显示数据段文本mov ds,axxor si,simov ax,0xb800mov es,axxor di,dicall put_stringpop si ;【拓展内容】用户程序执行完之后将控制权还给主引导程序pop dimov ss,simov sp,diretf ;= 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_sectionapp_end:
分别将他们编译,生成 my_mbr.asm 和 my_app.asm,使用李忠老师的虚拟硬盘写入工具将它们写入到虚拟硬盘
执行virtual box虚拟机,可以看到正常显示期望的文本内容,证明我们的主引导程序正确的引导了用户程序。
还可以利用Bochsdbg进行调试,观察程序的执行过程,尤其是控制权来回切换时,各个寄存器以及栈空间的状态变化。
小结
真实的主引导程序引导操作系统肯定没这么简单,不过把这个实验写下来也能对相关过程有个大致的了解。与纸上谈兵相比,实践环节会遇到很多细节问题,我本以为一天就能完成这个实验,结果花了三四天,有些问题是由于没有彻底理解之前所学的知识,有些知识理解了,但在使用时又疏忽了。学技术,多实践,才能做到查漏补缺和加深记忆的效果。
汇编语言-实现一个简单的主引导记录(MBR)引导用户程序相关推荐
- 主引导扇区及主引导记录MBR的详细说明
引导扇区在每个分区里都存在,但是我们常说的*主引导扇区*是硬盘的 第一物理扇区.它由两个部分组成:即主引导记录MBR和硬盘分区表DPT.在 总共512字节的主引导分区里其中MBR占446个字节(偏移0 ...
- 硬盘结构,主引导记录MBR,硬盘分区表DPT,主分区、扩展分区和逻辑分区,电脑启动过程...
filex的文件系统看的云里雾里,还是先总结下FAT的一些基本知识吧. 硬盘结构 硬盘有很多盘片组成,每个盘片的每个面都有一个读写磁头.如果有N个盘片.就有2N个面,对应2N个磁头(Heads),从0 ...
- 硬盘主引导记录MBR
主引导记录:(MBR,Main Boot Record)是位于磁盘最前边的一段引导(Loader)代码.它负责磁盘操作系统(DOS)对磁盘进行读写时分区合法性的判别.分区引导信息的定位,它由磁盘操作系 ...
- 主引导记录(MBR)的反汇编分析
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; ; 主引导记录(MBR)的反汇编分析 ; ;;;;;;; ...
- 主引导记录MBR/硬盘分区表DPT/主分区、扩展分区和逻辑分区/电脑启动过程
主引导扇区 主引导扇区位于整个硬盘的0柱面0磁头1扇区{(柱面,磁头,扇区)|(0,0,1)},bios在执行自己固有的程序以后就会jump到MBR中的第一 条指令.将系统的控制权交由mbr来执行.主 ...
- 硬盘结构,主引导记录MBR,硬盘分区表DPT,主分区、扩展分区和逻辑分区
filex的文件系统看的云里雾里,还是先总结下FAT的一些基本知识吧. 硬盘结构 硬盘有很多盘片组成,每个盘片的每个面都有一个读写磁头.如果有N个盘片.就有2N个面,对应2N个磁头(Heads),从0 ...
- 硬盘主引导记录(MBR)及其结构详解
硬盘的0柱面.0磁头.1扇区称为主引导扇区,FDISK程序写到该扇区的内容称为主引导记录(MBR).该记录占用512个字节,它用于硬盘启动时将系统控制权交给用户指定的,并在分区表中登记了的某个操作系统 ...
- 硬盘主引导记录(MBR)及其结构
硬盘的0柱面.0磁头.1扇区称为主引导扇区,FDISK程序写到该扇区的内容称为主引导记录(MBR).该记录占用512个字节,它用语硬盘启动时将系统控制权交给用户指定的,并在分区表中登记了的某个操作系统 ...
- 重装系统时启动失败,引导信息有错误,修复磁盘的主引导记录MBR方法
如果要修复这个磁盘的主引导记录MBR,必须在PE下才能进行,下面以通用PE工具箱来制作PE启动U盘. 先从网上把这个工具下载下来,安装到电脑上,先打开安装包,启动后,点"安装"即可 ...
最新文章
- Postman接口调试神器-Chrome浏览器插件
- Windows 7 蓝屏代码大全 amp; 蓝屏全攻略
- Asp.Net Core 发布和部署(Linux + Jexus )
- Spark入门(二)多主standalone安装
- java 条码识别_条码识别示例代码
- word 插入代码_突破Word页码困境,这招简单又实用的自动更新法,90%的人还不会!...
- canoe知识点查阅
- win下 git gui 使用教程
- 【11】 Express安装入门与模版引擎ejs
- chrome fiddler 重定向 https 请求
- Pulseaudio之meson编译(十二)
- 设计模式之适配器与外观模式(二)
- GPU cuda驱动安装
- 量子计算(四):量子力学的发展史
- 敢不敢用一年时间改变你自己?
- Linux Shell中的简单命令组合使用
- Auto.js蚂蚁森林自动偷能量脚本
- STM32CubeMX学习笔记(50)——USB接口使用(DFU固件升级)
- java.sql.SQLException: Invalid utf8 character string: 'ACED00'
- 使用Hbuilder开发python
热门文章
- Java运用jna、vlcj实现音乐和视频的播放器1-主界面设计
- 发那科机器人override指令_发那科机器人程序是如何编写的呢——发那科机器人...
- 2022年最新全国各省五级行政区划代码及名称数据(省-市-区县-乡镇-村)
- 关店歇业?当黄金时代成为历史,快时尚品牌的花式自救
- 【转载】根据已知点通过COORD七参数计算
- python3 中英文标点转换
- vue将数字转成中文大写,一二三四五
- 临床辅助系统CDSS程序
- win7 64位系统PSD缩略图补丁预览PSD Mystic Thumbs免费版
- Allwinner T3 汽车级处理器为工业级 SoM 提供动力