Assembly language - implement a simple master boot record (MBR) boot user program

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:

  1. BIOS power-on self-test, which checks the system hardware (including memory)
  2. 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
  3. Start the bootloader according to the boot code in the MBR
  4. 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, cexecute instructions continuously, and stop running until the ip register points to 0x7c00

insert image description here

Use scommands for single-step debugging, and use rcommands to observe the value of registers

insert image description here

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:

  1. The total length of the program occupies a double word, which is 4B
  2. The offset address within the entry point segment, occupying one word, that is, 2B
  3. The assembly address of the entry point segment occupies a double word, which is 4B
  4. The number of relocation entries, occupying one word, that is, 2B
  5. The corresponding n entries, each entry occupies a double word, that is, 4B

insert image description here

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:

  1. User programs are stored sequentially in the 100th sector of the disk
  2. 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 inand commands:out

ininstruction:

    ;一般使用ax和dx寄存器
    ;选择ax还是al取决于端口的位宽
    mov dx,0xc30    ;从0x3c0端口读取数据
    in ax,dx
    ;in al,dx

outinstruction:

    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:

  1. CHS mode (Cylinder Head Sector) cylinder-head-sector triplet
  2. LBA mode (Logic Block Address) logical sector number

Basic steps to read hard disk:

  1. Write the number of sectors to read to the 0x1f2 port
    mov dx,0x1f2
    mov al,0x1
    out dx,al
  1. 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号逻辑扇区
  1. Write 0x20 command to port 0x1f7, request to read hard disk
    mov dx,0x1f7
    mov al,0x20
    out dx,al
  1. 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
  1. 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:

  1. User programs are stored sequentially in the 100th sector of the disk
  2. 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:

  1. 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                          ;已经读了一个扇区,减去
  1. 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:

  1. 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

  2. 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 fardirectly 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 axstarting address of the instruction. Obviously, what we want is push axthe 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

insert image description here

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.

insert image description here

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.

Guess you like

Origin blog.csdn.net/hesorchen/article/details/128847119