This article refers to Mr. Li Zhong's "X86 Assembly Language: Real Mode to Protected Mode"
foreword
Manually implement a simple master boot record to guide the user program, which is helpful to understand
- Workflow of the main bootloader
- How to call functions at the assembly code level (the principle of function calls)
- How to read and write hard disk at the assembly code level (interaction between CPU and peripheral devices)
Although the content is not much, it can comprehensively apply various knowledge.
master boot record
The following content is referenced from the wiki
Master Boot Record (Master Boot Record, abbreviation: MBR), also known as the master boot sector, is the first sector that must be read when the computer is turned on to access the hard disk. Its three-dimensional address on the hard disk is (cylinder, head, sector) = (0, 0, 1)
The process of system booting:
- BIOS power-on self-test, which checks the system hardware (including memory)
- Read the master boot record. The BIOS reads the first sector of the disk (that is, the MBR sector) into the memory address 0000:7C00H
- Start the bootloader according to the boot code in the MBR
- The bootloader loads the operating system kernel
modify master boot record
We can write our own machine instructions into the main boot sector, so that the BIOS will execute the program we specify after self-test. like this:
Sample assembly code demo.asm:
mov ax, 0x1234
jmp $
;--------------------------------------------------------------
; $ 表示当前行的汇编地址
; $$ 表示第一行的汇编地址
; $-$$得到前面代码占用的字节数
times (512-2-($-$$)) db 0 ;主引导扇区总长度必须512字节,用0占位
db 0x55, 0xaa ;主引导扇区最后两个字节必须是0x55aa
Compile with nasm to get the demo.bin file
Use teacher Li Zhong's virtual hard disk writing tool Vhd writer to write the contents of the demo.bin file into the main boot sector of the Vhd virtual hard disk
Start the bochsdbg virtual machine for debugging, because the master boot record will be loaded to the physical address 0x7c00, we use commands b 0x7c00
, c
execute instructions continuously, and stop running until the ip register points to 0x7c00
Use s
commands for single-step debugging, and use r
commands to observe the value of registers
Master Boot Record Boot User Program
If we write the main boot program ourselves, we can let it load other content (give up control), such as a user program. To load the user program, there are some necessary information to the main bootloader: the total length of the user program, the execution entry of the program, etc. Therefore, just like various protocol headers in the computer world, a header section needs to be added at the front of the user program.
The header section contains the following:
- The total length of the program occupies a double word, which is 4B
- The offset address within the entry point segment, occupying one word, that is, 2B
- The assembly address of the entry point segment occupies a double word, which is 4B
- The number of relocation entries, occupying one word, that is, 2B
- The corresponding n entries, each entry occupies a double word, that is, 4B
User program code sample code:
;------------------------------------------------------
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:
With the header segment, we read the user program into the memory and give it control (modify the segment address register) to complete the loading work. For simplicity, we make the following conventions:
- User programs are stored sequentially in the 100th sector of the disk
- The main bootloader always loads the user program into physical memory at 0x10000
Read and write hard disk
The CPU and peripheral I/O devices need to use the I/O interface to transmit data. The I/O interface integrates some registers, which we call ports. Different ports have different functions, such as sending commands and sending data. The CPU reads and writes data from ports and interacts with peripheral devices. We read and write data from the port with the in
and commands:out
in
instruction:
;一般使用ax和dx寄存器
;选择ax还是al取决于端口的位宽
mov dx,0xc30 ;从0x3c0端口读取数据
in ax,dx
;in al,dx
out
instruction:
mov dx,0xc30
out dx,ax ;往0x3c0端口写入数据
;out dx,al
In our computer, the hard disk interface has eight ports, which are 0x1f0 to 0x1f7 in turn. The port number to be read and written is stored in dx, and the data is stored in ax/al
The smallest unit of reading and writing hard disk is sector, so hard disk is a typical block device. There are two modes for reading and writing hard disk:
- CHS mode (Cylinder Head Sector) cylinder-head-sector triplet
- LBA mode (Logic Block Address) logical sector number
Basic steps to read hard disk:
- Write the number of sectors to read to the 0x1f2 port
mov dx,0x1f2
mov al,0x1
out dx,al
- Write the 32-bit data of the selected hard disk (master/slave), read/write mode (CHS/LBA) and sector number to ports 0x1f3, 0x1f4, 0x1f5, and 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号逻辑扇区
- Write 0x20 command to port 0x1f7, request to read hard disk
mov dx,0x1f7
mov al,0x20
out dx,al
- Determine whether the hard disk is ready to be read
Read 1B data from port 0x1f7
mov dx,0x1f7
in al,dx
00000000
共读取8位
第7位为1表示硬盘正忙
第3位为1表示硬盘已经可以交换数据
Polling waits until data can be read
mov dx,0x1f7
.waits:
in dx,al
and al,0x88 ;取出第3位和第7位
cmp al,0x08
jnz .waits
- After the hard disk is ready to be read, read data from the data port 0x1f0
;假定DS:BX已经指向数据要存放的逻辑地址
mov dx,0x1f0
mov cx,256 ;512B=256word
.readlo:
in dx,ax
mov [bx],ax
add bx,2
loop .readlo
load user program
After understanding how to read the hard disk, the next goal is to load the entire user program. Firstly, the header section of the user program should be loaded into the memory, and the information in the header section should be parsed.
We have made the following agreement before:
- User programs are stored sequentially in the 100th sector of the disk
- The main bootloader always loads the user program into physical memory at 0x10000
Therefore, we define the logical sector number as a constant, and the physical address is stored in a 4-byte memory because it needs to be accessed later:
disk_lba_address equ 100 ;定义常数,约定的逻辑扇区号
phy_base_address : dd 0x10000 ;约定的用户程序要存放的物理地址
Next, we first read sector 100 (including the header segment) into memory, and read all remaining sectors according to the first double word in the header segment—the total length of the program
Specific steps are as follows:
- Read sector 100 (including the header), and parse the total length of the program. For one sector 512B, the total length/512 is the number of sectors. It should be noted that if it cannot be divisible, one more sector is required.
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 ;已经读了一个扇区,减去
- Use loop to read remaining sectors
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 ;循环读取剩余扇区
Relocation of user programs
We have successfully loaded the entire user program into the memory, and the next task is to hand over control to the user program. Obviously, it is necessary to modify the cs:ip register to the logical address of the user program entry point.
How to calculate it? In the header section there are two pieces of information:
-
The assembly address of the entry point segment of the user program. It is actually very simple to calculate cs, that is, the value of the base address of the segment through this value. The starting physical address of the user program loaded is phy_base_address, and only the addition of the two is the actual physical address of the segment at the entry point of the user program. Shift four bits to get the value of segment base address cs
-
The offset address within the segment of the entry point of the user program. directly corresponds to the value of ip
After calculating the result, for the convenience of subsequent use, we directly backfill the result to the assembly address of the entry point segment of the user program in the header segment. It is "coincidentally" that the value of ip is stored at the offset address of the header segment [0x04], and the value of cs is stored at the offset address of the header segment [0x06]. You can use the command jmp far
directly Jump to the user program entry point for execution.
jmp far [0x04]
In the user program, there are multiple SECTIONs, and it is difficult to use if you do not know their segment base addresses. At this time, the relocation entry comes in handy—we calculate the segment base addresses of all SECTIONs in the relocation entry according to the above calculation process, and backfill them into the corresponding memory space one by one. In this way, When accessing these SECTIONs, you only need to set the segment base address to the corresponding value.
;计算用户程序入口点逻辑段地址
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
The main bootloader takes back control [Extended]
After the user program is executed, it is necessary to return the control right to the superior, otherwise only one program can be run. The principle is very simple, just save the value of each register and restore it, but for the cs/ip/ss/sp register Special care is required for saving and restoring. For example, cs is modified, but ip has not been modified. At this time, it will directly point to other commands (cs:ip has changed).
In addition, the value of the ip register cannot be read and written directly, remember the call instruction? Its principle is to push the value of the ip register onto the stack, so we can indirectly get the value of the ip register through the call instruction, like this:
push cs
call get_ip ;等价于push ip
get_ip:
pop ax ;得到ip的值
push ax
It seems seamless, but the ip saved in this way is wrong, because the ip at this time points to the pop ax
starting address of the instruction. Obviously, what we want is push ax
the starting address of the next instruction. There are still many problems like this, which will be encountered in the process of hands-on practice.
The main bootloader gets back the control code (main bootloader part):
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 ;恢复环境
The main boot program gets back the control code (user program part):
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
However, I always feel that the control right needs the active cooperation of the user program to hand over the strange...
verify
The complete assembly code of the main bootloader 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
User program complete assembly code 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:
Compile them separately to generate my_mbr.asm and my_app.asm, and write them to the virtual hard disk using Mr. Li Zhong’s virtual hard disk writing tool
Execute the virtual box virtual machine, and you can see that the expected text content is displayed normally, which proves that our main boot program has correctly guided the user program.
You can also use Bochsdbg to debug and observe the execution process of the program, especially the state changes of each register and stack space when the control right is switched back and forth.
summary
The real main boot program booting the operating system is definitely not that simple, but writing down this experiment can also give you a general understanding of the related process. Compared with talking on paper, there will be many detailed problems in practice. I thought I could complete this experiment in one day, but it took three or four days. Some problems are due to not fully understanding the knowledge I have learned before. Negligence in use. Only by learning technology and practicing more can we achieve the effect of checking for gaps and filling in gaps and deepening memory.