MBR (Master Boot Record)

When a computer boots, the first program run is the BIOS, which stands for Basic Input/Output System. BIOS performs integrity checks of hardware, such as ensuring memory cards are properly slotted and readable, motherboard connection, etc.

BIOS are typically very standard and provide facilities or auxiliary functions for the Bootloader. The BIOS will check for a bootable device by looking in the first sector (sector 0) and checking byte 510 and 511 (the 511th and 512th byte, respectively) for the boot signature: 0x55AA.

On little Endian systems, this will be 0xAA55

If the boot signature is found, the BIOS will load the MBR (Master Boot Record) to address 0x7C00. The MBR is responsible for loading the second part of the bootloader.

When the MBR is loaded, the computer is running in Real-Mode, which is a 16-bit mode that uses segmented memory (not paging) and provides no memory protection. The objective is to switch to protected mode, which is 32 or 64-bit and provides memory protection.

Segment Registers

  • Code Segment -> CS

  • Data Segment -> CS

  • Stack Segment -> SS

  • Extra Segment -> ES

Example MBR

Below is a simple bootloader. I will cover a few important sections, however I'm assuming familiarity with assembly.

```asm-intel-x86-generic
ORG 0x7c00                          ;; ORG is really the offset of: Address * 16 + offset
BITS 16                             ;; real-mode is constrained to 16-bit code

CODE_SEG equ gdt_code - gdt_start   ;; Code segment (.text)
DATA_SEG equ gdt_data - gdt_start   ;; Data Segment (.data)


_start:
    jmp short start
    nop


times 33 db 0                       ;; 33 bytes after short jump for BIOS Parameter Block
                                    ;; https://wiki.osdev.org/FAT#Boot_Record
                                    ;; If BIOS fills in parameter block values, it won't 
                                    ;; corrupt our code


start:
    jmp 0:present                   ;; forces code segment to 0x7c0 (0x7c0 * 16) = 0x7c00
                                    ;; this give our code the best chance of booting on any system
                                    ;; cs replaced with 0x7C0
                                    ;; USB/HD that are partitioned will have a boolader in the first sector
                                    ;; each partition will have a volumbe boot record


init_segments:
    cli                             ;; clear interrupt flags, meaning ignore maskable interrupts
    mov ax, 0x00    
    mov ds, ax         
    mov es, ax          
    mov ss, ax
    mov sp, 0x7c00
    sti                             ;; enable interrupts

    ret


present:
    call init_segments              ;; mask interrupts for safe operation


.protected:                         ;; Entering protected mode
    cli                             ;; Mask interrupts
    lgdt[gdt_descripter]            ;; Load GDT register with start address of Global descriptor table
    mov eax, cr0                    ;; cr0 is control register 0
    or  eax, 0x1                    ;; set PE (protection Enable) bit in CR0
    mov cr0, eax
                                    ;; Perform a far jump to selector 08h (offset into GDT, pointing at 32-bit PM code segment descriptor)
                                    ;; to load the CS with proper PM32 descriptor. Recall that in protected mode, segment registers become
                                    ;; selectors
    jmp CODE_SEG:load32             ;; Switch to by replaceing CODE_SET with GDT[Entry[1]], then jump to load32.


;; GDT Section
gdt_start:                          ;; Global Descriptor Table


;; offset 0                         ;; GDT Null Descriptor
gdt_null:
    dd 0x0
    dd 0x0


;; offset 0x8                       ;; Entry 1
gdt_code:                           ;; CS points here
    dw 0xffff                       ;; Limit GDT[15:0]
    dw 0                            ;; Base GDT[15:0]
    db 0                            ;; Base GDT[16:23]
    db 0x9a                         ;; Access Byte GDT[7:0]
    db 11001111b                    ;; High 4 bit flags and low 4 bit flags Limit[19:16] and flags[3:0]
    db 0                            ;; Base [31:24]


;; offset 0x10                      ;; Entry 2
gdt_data:                           ;; DS, SS, FS, GS, ES 
    dw 0xffff                       ;; Limit GDT[15:0]
    dw 0                            ;; Base GDT[15:0]
    db 0                            ;; Base GDT[16:23]
    db 0x92                         ;; Access Byte GDT[7:0]
    db 11001111b                    ;; High 4 bit flags and low 4 bit flags Limit[19:16] and flags[3:0]
    db 0                            ;; Base [31:24]


;; end gdt
gdt_end:                            ;; Marks the end of GDT descriptor


;; gdt descripter                   ;; Computing size gives us the entire GDT
gdt_descripter:
    dw gdt_end - gdt_start-1        ;; size of descriptor
    dd gdt_start


;; 32-Bt code                       ;; Start 32-bit Mode, and switch from selector memory to paging                     
[BITS 32]                           ;; Start 32-bit mode
load32:
    mov ax, DATA_SEG
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax
    mov ebp, 0x00200000
    mov esp, ebp
    jmp $


times 510-($ - $$) db 0             ;; pad zeros to get 510 bytes
dw 0xAA55                           ;; 511th and 512th byte boot signature dw = 2 bytes
```

ORG Form

In segmented memory, addresses are calculated using the following formula:

Address * 16 + offset , where address is the address of one of the segment registers.

The multiplier of 16 is used because real-mode is restricted to 16-bit address, necessitating alignment with 16. The offset, in this example, is ORG.

Admittedly, this isn't the best way to boot; starting at 0x0000 is better, but I'll discuss that later.

Code & Data Segment

Next, you'll see two variables created: CODE_SEG and DATA_SEG. These represent the segments of memory that will have an entry in the Global Descriptor Table. Read the linked post to learn more about that. For now, just know that, in addition to the defined (User) code and data segment, a kernel code and data segment is also defined.

These segments define the boundaries for Kernel space and User space in memory and the GDT (analogous to the IVT/IDT) describes this layout to the CPU.

FAT Partitioning

If the media in which the MBR resides is not partitioned, loading is simple enough. However, if it is partitioned (e.g. Hard Disk), the beginning of the media will contain MBR or other partition information in what's known as a Volume Boot Record. The BIOS parameter block is used to store this information, and is 33 bytes.

times 33 db 0

The code above ensures 33 bytes are reserved, just in case the bootloader is stored on a Hard Drive. Remember, the goal of the MBR is to function on as many different media types and for as many different manufacturers as possible.

If you omit those bytes because you're only planning to boot from non-partitioned media, you run the risk of your MBR code being overwritten in the event that it's ever used on partitioned media.

Masking Interrupts

The code below masks interrupts and sets the segment registers to 0x00; except for the Stack Segment register, which is set ot 0x7C00. Why? Earlier, the formula for calculating memory addresses in segmented memory was introduced as: address * 16 + offset. If for whatever reason, the value in one of the segment addresses is not zero, the system may compute an erroneous address using that formula. Imagine if instead of 0x00 * 16 + 0x7C00 was used to start the MBR, 0x7D * 16 + 0x7C00 was used.

This is, in part, why it's best to start the bootloader with ORG 0x0000 instead of 0x7C00.

```asm-intel-x86-generic
init_segments:
    cli                             ;; clear interrupt flags, meaning ignore maskable interrupts
    mov ax, 0x00    
    mov ds, ax         
    mov es, ax          
    mov ss, ax
    mov sp, 0x7c00
    sti                             ;; enable interrupts

    ret

```

Global Descriptor Table

This deserves a more thorough explanation, which I will provide in the GDT post.

Boot Signature

The MBR ends with a calculation that guarantees the size of our MBR. An MBR is 512 bytes, and byte 510 and 511 (the 511th and 512th byte, respectively) hold the boot signature. The formula 510 - ($ - $$) subtracts the start address (denoted by the _start label) from the current address ($). That result is then subtracted from 510, to get the total size of the program. If it is not equal to 510 (the size needed so that the boot signature makes up the 511th and 512th byte), zero padded bytes will be used to fill the remaining space.

times 510 - ($ - $$)

Finally, the boot signature is added.

dw 0xAA55

This boot signature is written in Little-endian byte order to match the target system.

Last updated