MBR (x86): Difference between revisions

From OSDev.wiki
Jump to navigation Jump to search
[unchecked revision][unchecked revision]
Content added Content deleted
m (added to system initialization category)
No edit summary
 
(21 intermediate revisions by 15 users not shown)
Line 1: Line 1:
A '''Master Boot Record''' ('''MBR''') is the bootsector of a hard disk. That is, it is what the BIOS loads and runs, when it boots a hard disk. The MBR is the very first sector of the hard disk; it contains an MBR Bootstrap program (described below),
"So we have complete control of the computer... what now???"
and a [[Partition Table]]. Devices that emulate a hard disk during system initialization must also contain an MBR, because they
must also have Partition Tables -- even if they are not bootable.


The BIOS will only boot an MBR from a device if that device is in the "boot sequence" stored in [[CMOS]], and if the MBR on the device
That question is usually the main topic in operating system design altogether. Most people that I know will try to approach the design and programming stage like reading a book, cover to cover and in a linear fashion. I have witnessed multitudes of operating system projects that seemed promising turn out to be nothing more than a fancy boot program because this type of design policy.
is formatted correctly. On the other hand, if a device is not in the boot sequence (but has a "drive number"), it is still possible
for a [[Real Mode]] program (such as another MBR or bootloader) to load and boot that device's MBR directly.


==MBR Bootstrap==


An MBR is loaded by the [[BIOS]] at physical address 0x7c00, with DL set to the "drive number" that the MBR was loaded from.
It is my suggestion that you take operating system design and programming as to writing a book, not reading one. When an author writes a book, they most likely worry about the contents first and the cover last. A good author will lay out the base of their book chapter by chapter, making note to finish each chapter first, then they will go back and "per-fect" the contents of each chapter so they flow. When the author is done, a cover is tacked on and the book is complete.
The BIOS then jumps to the very beginning of the loaded MBR (0x7c00), because that part of the MBR contains the "bootstrap" executable code.


Typical MBR bootstrap code will do the following:


* relocate itself away from the 0x7c00 physical address (using a memory copy, and usually a far jump)
Now let's take that book writing process and apply it to operating system design. The designer starts with a base, which most likely will be the minimal code needed to successfully bring the computer to a stable running state, then they add more features and such to make the operating system more stable and user friendly. Once the designer is satisfied with how each part of the operating system interacts in general, they go back to do some code optimization and clean-ups (quite an eternal task). I think the idea behind this method was best stated with this quote, "First make it run, then make it run better." The very last step would be for the designer to program their own MBR program (boot loader), most people in fact use other boot loaders such as GRUB to handle the boot-up task. If you wish to take the GRUB approach, that has been well documented and is beyond the scope of this guide. I will concentrate on the minimal design approach to the MBR.
* determine which partition (or hard disk) to boot from (either by looking for the active partition, or by presenting the user with a selection of installed operating systems to choose from)
** if the user selected an "inactive" partition, then set the selected partition entry to "active", and clear the "active" bits of other partition entries
** use [[ATA in x86 RealMode (BIOS)|BIOS INT 13h]] commands to rewrite the MBR if the partition table entries were modified
* use [[ATA in x86 RealMode (BIOS)|BIOS INT 13h]] commands to load the [[Volume Boot Record]] (VBR, the "bootsector" of the bootloader) from the beginning of the selected partition to physical address 0x7c00
* set DS:SI pointing to the selected partition table entry
* jump to 0x7c00 (with CS set to 0, and DL set to the "drive number")


Note: it is intended for the value of DL, and the DS:SI pointer to be passed all the way into the kernel, for the kernel's use.
This also means that the relocated MBR should not be overwritten during the boot process -- because the DS:SI pointer is aimed
at a partition table entry inside that MBR, and needs to remain valid.


== How the MBR is Loaded ==
==MBR Format==
First thing for you to understand is where the MBR program is loaded to in memory. Much like the special "0x55AA" pattern, the location in memory for the MBR is just a means to create a standard. All x86 based processors load the 512 Byte MBR program to memory at 0000:7C00. That address format (0000:7C00) is nothing more than the adaptation of the old 16-bit "segment:offset" format back when memory could only be addressed 64 Kilobytes at a time. The true format of that memory location would have been 07C0:0000, which basically states that the 64 Kilobyte region starts at 0000:7C00 and stops after 64 K (1000:7C00 or 0x17C00). The trick was for a program to change the contents of the segment registers so they would essientally state "I'm accessing this 64 Kilobyte segment of the total memory". With the 32-bit processor series (Intel 80386 and above), this memory addressing limitation was expanded to a total of 4 Gigabytes when using 32-bit Protected Mode, so we do not have to worry about changing segment registers to access different parts of memory.


Notes:
== Memory Address Basics ==
If you already know the basics of the memory address format and the hexidecimal number system, skip this next paragraph. If you do not, let's quickly break down the memory address format so it can be understood better. Think of the format like you do the decimal number system, each digit is a place holder. The only difference is that each place holder is in hexidecimal format, which means as we move place holders from right to left they become significantly larger and represent a much larger value. For example, 0x00000001 is at one byte, 0x00000010 is at 16 bytes, 0x00000100 is at 256 bytes, 0x00001000 is at 4 Kilobytes, 0x00010000 is at 64 Kilobytes, 0x00100000 is at one Megabyte, 0x01000000 is at 16 Megabytes and 0x10000000 is at 256 Megabytes. The maximum address value is 0xFFFFFFFF, which is at the 4 Gigabyte limit. If you noticed, the place holders from right to left grow by a factor of 16, and hexidecimal is a 16-base number system... yes... number systems are that easy.


* See the [[Partition Table]] article for the format of each partition table entry field.
== Memory Segmentation ==
* It is important to remember that the partition table entries are <b>not</b> aligned on 32-bit boundaries.
In real mode, you get one megabyte of memory space. Thus, a maximum of FFFFF. People looked at this and thought, "How are we going to be able to access all this memory?" Since their registers only went up to 16 bits, (maximum of FFFF), they found that they needed to address any memory location with two registers - a segment and an offset.
* Naming the partition table entries as "First" through "Fourth" is for convenience only. The partition table entries are <b>not</b> required to be in any kind of order.
<pre>
* <b>Any</b> one of the partitions may be "active".
12F3:4B27
* There is supposed to be one active partition table entry, at most.
^ ^
* Windows seems to verify and require that the partition it boots from be marked "active".
Segment Offset
* Most other operating systems don't seem to care about the "active" bit in the partition table entry.
</pre>


{| {{wikitable}}
How does it work? The exact memory location is found with the following simple equation:
! Offset
<pre>MemoryPlace = Segment * 16 + Offset</pre>
! Size (bytes)
Thus, 12F3:4B27 would be the exact memory location of 17A57.
! Description
People good at math would recognize that an exact memory location can be found with different Segments and Offsets. This is true.
|-
For example, exact memory location 00210 can be 0020:0010, 0000:0210, and 0021:0000.
| 0x000
| 440<sup>1</sup>
| MBR <b>Bootstrap</b> (flat binary executable code)
|-
| 0x1B8
| 4
| Optional "Unique Disk ID / Signature"<sup>2</sup>
|-
| 0x1BC
| 2
| Optional, reserved 0x0000<sup>3</sup>
|-
| 0x1BE
| 16
| First partition table entry
|-
| 0x1CE
| 16
| Second partition table entry
|-
| 0x1DE
| 16
| Third partition table entry
|-
| 0x1EE
| 16
| Fourth partition table entry
|-
| 0x1FE
| 2
| (0x55, 0xAA) "Valid bootsector" signature bytes
|}


<sup>1</sup> This can be extended to 446 bytes if you omit the next 2 optional fields: Disk ID and reserved.
== Assumptions ==
Now for the part that really matters, the MBR structure. Since we will be using the computer from an "uninitialized" state, we have to make some assumptions.


<sup>2</sup> The 4 byte "Unique Disk ID" is used by recent Linux and Windows systems to identify the drive.
"Unique" in this case means that the IDs of all the drives attached to a particular system
are distinct.


<sup>3</sup> The 2 byte reserved is usually 0x0000. 0x5A5A means read-only according to https://neosmart.net/wiki/mbr-boot-process/.
=== 16-bit Real Mode ===
The first assumption for x86 based processors is that we are in standard starting mode, or the initial processor state, called 16-bit Real Mode (details will be discussed later).


==Partition table entry format==
{| {{wikitable}}
!Offset<sup>1</sup>
!Size (bytes)
!Description
|-
|0x00
|1
|Drive attributes (bit 7 set = active or bootable)
|-
|0x01
|3
|CHS Address of partition start
|-
|0x04
|1
|Partition type
|-
|0x05
|3
|CHS address of last partition sector
|-
|0x08
|4
|LBA of partition start
|-
|0x0C
|4
|Number of sectors in partition
|}


<sup>1</sup> Relative to the start of the partition entry.
=== Stack ===
For the next assumption we will introduce you to the stack. Think of the stack as one giant variable in memory used for the quick storage and loading of data. The stack segment (SS) and stack pointer (SP) are 16-bit registers that combine to specify a 20-bit memory address in real mode, which is the "top" of the stack. When the stack is used to store data, the amount of data stored is subtracted from SP and then is stored into memory using the SS and SP registers as a memory location (SS:SP). When the stack grows, it will grow downwards in memory, not up. Since you are required to set up the SS and SP registers, it is probably a good idea to set SS to 0x0000 and SP to 0x7C00 because we know everything between the beginning of memory and up to 0000:7C00 is pretty much unused since we are in an uninitialized state (we will expand on this later), and should be big enough for our needs.


==Traditional MBR==


The DOS FDISK program was the first to ever use an MBR, so that MBR became the de-facto standard.
=== BIOS ===
It also automatically became the standard for the minimum level of functionality of an MBR. It was never
Our final assumption will be that we need to load a program from a disk drive. Here I will introduce you to the Real Mode BIOS interrupt routine 0x13, which are disk drive access routines. BIOS interrupt routines rely on data in the registers to specify the action to be taken. In the case of an interrupt routine 0x13 read operation, AH being the value of 2 specifies this operation type. AL represents how many 512-byte sectors (physical design standard) we want to read. ES and BX form the memory address location ES:BX, which is the destination of the sectors to be read. The next 3 structures combine to create a physical offset on the floppy disk drive. CH is the track on which to start reading from (equivalent of a cylinder on a hard disk). CL is the sector on which to start reading from. DH is the head on which to start reading from. The last structure is DL, which represents the drive to be used. In this case DL is set to 0, which represents the first floppy disk drive (A:). Finally we call the 0x13 interrupt routine which executes everything we just setup. Now that our program is loaded into memory, we transfer execution control to it by simply jumping to its base address specified earlier by ES:BX.
changed after it was first introduced.


The whole point of the FDISK program is to manipulate the MBRs of the hard disks attached to a system.
== Finishing Touches ==
When FDISK partitions a blank disk, it writes an MBR to sector 0 of the drive. When FDISK adds a new partition
We are almost finished with the basic MBR, only one more thing to do. We need to make sure the MBR program is 512 bytes long with the last 2 bytes being the special boot pattern (0xAA55). How this is done depends on your programming method. Here is an example in [http://en.wikipedia.org/wiki/NASM NASM] syntax to help paint a picture.
to a disk, it adds an entry into the Partition Table in the MBR. When FDISK makes a partition "active",
it sets the "flag" byte in the Partition Table entry to 0x80.


The MBR that FDISK uses is coded to:


* relocate itself to <tt>0x0000:0x0600</tt>
<pre>
* examine the byte at offset 0x1be, 0x1ce, 0x1de, and 0x1ee to determine the active partition
TIMES 510-($-$$) DB 0x00 ;Pad the rest of the boot sector upto byte 510 with zeros
* load only the first sector of the active partition (which is expected to contain a DOS bootsector) to <tt>0x0000:0x7c00</tt> (hence the previous relocation)
SIGNATURE DW 0xAA55 ;Create the special pattern
* set SI
</pre>
* jump to 0x7c00 -- transferring control to the DOS bootsector.


<!--Commented out because rant
It is extremely unfortunate and stupid that this MBR loads only one sector of the booted partition. There is no
additional complication to load more than one sector in the code; but doing so makes writing bootloaders <b>much</b> simpler
if, perhaps, 8 sectors are loaded instead. So it may be wise to always replace this old MBR with a custom MBR that loads
more than one sector, for your OS. DOS/Windows will still boot correctly if more than one sector is loaded.
-->


==Dual Booting==
We can observe the usage of NASM's "TIMES" and "$" directives. TIMES simply states to excute the following code as many times as specified (i.e. TIMES NUMBER_OF_TIMES INSTRUCTION_TO_EXECUTE). The "$" character states the current address in the program, putting two "$" together as such "$$" states the program's beginning address. On the next line is the "SIGNATURE" directive, which in itself is nothing more than a way for us to see if a data structure is particulary special. The line could easily have been "DW 0xAA55" and the end result would be the same.


A typical system may have several hard disks on it, and each hard disk can have 4 standard partitions (without
One thing you may have noticed when looking at the MBR in a hex editor or debugger, is that the special pattern looks backwards, it should be 0xAA55 right? Well yes, when loaded into the register that is precisely what it should be, but when stored in the program file, it looks like 0x55AA as the Intel x86 series register/memory operations work by loading or storing the least significant byte first. This form of "byte swapping" is called [http://en.wikipedia.org/wiki/Endianness#Endianness_in_computers Little Endian] order. NASM data instructions (DB/DW/DD/etc...) store to the program file in the same way as if you were storing data from a register, because that is the most common use for such data in Assembly Language (wysiwyg). When the BIOS program looks for that special pattern, it sets a pointer to that location in memory and loads that WORD value into a register in order to check it for the value of 0xAA55.
going into the extra complication of Extended Partitions). Each of those partitions could theoretically hold
its own distinct bootable OS, and filesystem.


However, the standard x86 boot sequence will only ever boot the MBR from the "C:" disk
Please note that the above paragraph is something you do not need to be readily concerned with, but it is good information to keep in mind when using hex editors and disassemblers. This also goes for using debuggers, as whatever code/data/information that is stored in the program file, is loaded into memory in exactly the same order (no byte-swapping like register/memory operations).
(the first disk found during the disk detection phase). And the standard MBR will only allow a single active
partition on that disk, and will only ever boot that one partition. This is really completely inadequate.


It is not possible to modify the BIOS/CMOS boot sequence -- but you <i>can</i> change the MBR. So, one solution is to replace the standard MBR
== Code Examples ==
with a "custom, Dual Booting" MBR. (For a good example, see John Fine's SMBMBR in the [[#External Links|External Links]], below.)


A simple dual booting MBR will allow the user to select any partition on the current drive, to boot. A more
=== [http://en.wikipedia.org/wiki/NASM NASM] ===
complicated dual booting MBR will also allow the user to select other hard disks, or even to specifically
<pre>
choose other partitions on other hard disks, to boot.
[BITS 16] ;Tell NASM that we wish to use 16-bit Addressing and Indexing for Real Mode
ORG 0x00007C00 ;Tell NASM the base address location of this program, as it is in RAM


For one MBR to load and run a different MBR off a different drive is called "chain loading". If all the MBRs
;Setup the initial Stack and all the Segment Registers
are dual booting, then the user can cycle through all the disks, and choose to boot the "correct" partition from
cli ;Disable Interrupts to avoid corrupting the stack
the "correct" drive.
xor ax,ax ;AX = 0
mov es,ax ;We're making sure all the segment registers are cleared to 0.
mov fs,ax ; ES, FS, GS, and DS are being cleared, as CS will be
mov gs,ax ; ...
mov ds,ax ; ...
mov ss,ax ;Stack Segment = 0
mov sp,0x7C00 ;Stack Pointer = 0x7C00
sti ;Enable Interrupts


A dual booting MBR is a huge improvement over the standard MBR. The only problem is that an MBR bootstrap is limited
jmp 0:flush_cs ;Here we are jumping to flush_cs in memory segment 0,
to a little over 400 bytes of code. Such a tiny program is insufficient to create a "nice" user-friendly dual
; to make sure CS is set to 0. The reason this needs
booting system that has commercial appeal. It <b>is</b> enough space to hack up an interface that is good enough
; to be done, is that some computers load the MBR
for the person who wrote it.
; into memory at 07C0:000, which is another valid way
; of saying 0000:7C00 in the segement:offset address
; format. This could create problems with certain
; instructions that assume CS to be 0 instead of
; 0x07C0. By using "ORG" above and this instruction,
; we are ensuring that program is offered the address
; space format it is expecting... preventing crashes.
flush_cs:


One way around this size limitation is to note that a bootloader can contain a <b>lot</b> more code than an MBR. So a custom
;Load Kernel from Floppy Disk to RAM
MBR can try to load a "preferred" bootloader (if it can find one on some partition, somewhere) -- and that preferred
;-Interrupt 0x13 (BIOS Common Disk Access), Function 0x02 (Read Sectors into Memory)
bootloader can have a very pretty, user-friendly interface that allows the user to select any partition
;-AH = BIOS Interrupt Function
off any drive, to boot.
;-AL = Number of 512-byte Sectors to load
;-CX = Cylinder & Sector Offsets
;-DX = Head Offset & Drive Number
;-ES:BX = Segment:Offset of Address Destination in RAM


It is also nice, if there is only one partition on only one drive (ie. there is no choice for a user to make), if the
mov ah,0x02 ;BIOS Interrupt Function 0x02 (Read Sectors into Memory)
MBR will boot that one partition automatically -- without bothering the user with prompts.
mov al,0x01 ;Load 1 sector from floppy
mov bx,0x7E00 ;Set BX offset to our desired load location, ES already set from above
mov cx,0x0002 ;Set Cylinder Offset to 0, Set Sector offset to 2
xor dx,dx ;Set Head Offset to 0, Set Drive Number to 0 (First Floppy Disk)
int 0x13 ;Execute BIOS Interrupt


==Generic MBRs==
;Jump to newly loaded Kernel
jmp 0x0000:0x7E00 ;Sets CS to 0x0000 and the IP to 0x7E00, execution continues at that address


As said above, the MBR that is written to the disk by the DOS FDISK program is "the most generic" one. But every disk partitioning
;Fill the rest of the MBR with Zeroes, and write the "Magic" Boot Signature
app must write some sort of MBR to the disk, and every generic bootloader may well need a custom MBR.
TIMES 510-($-$$) DB 0x00
SIGNATURE DW 0xAA55


Fortunately, there was a standard for them to conform to (the FDISK MBR), and they all did conform to it. Some of them may be
</pre>
Dual Booting, or have other features -- but they will all end up loading your bootloader at the standard address (0x7c00),
with the DL register set to the boot "drive number", and DS:SI pointing at the correct partition table entry of the correct
MBR.


==Building a Custom MBR Bootstrap program==
== Conclusion ==
That's it, our simple MBR is complete, now we just have to write it to the floppy disk. To write the MBR we must use special I/O tools. If you are using a POSIX compliant operating system then you should have access to "dd". If you are using Windows, you will have to obtain the [http://www.chrysocome.net/dd Windows equivalent of "dd"] or a program such as [http://www.chrysocome.net/rawwrite "RawWrite"]. Once the MBR is written to the floppy disk, the disk is considered unformatted by Windows since the MBR also acts as holder for partition information, so don't try to access it with anything but RawWrite. If you wrote a MBR program verbatim from this chapter, you can go ahead and boot from the disk, but don't expect much more than a freeze-up since we haven't put any program in the sectors that the simple MBR program loads into memory.


When the BIOS transfers control to the MBR bootstrap code, the system is in Real mode. The MBR will probably also run entirely
[[Category:System Initialization (x86)]]
in Real mode, so it is important to understand [[Real Mode#Real Mode Memory Addressing|Real mode addressing]].

Self-relocation is one of the [[C#Things C can't do|things C can't do]], and most modern C compilers can't create code that is
compatible with Real mode, anyway. So an MBR must be written in [[Assembly]].

It is necessary to build an MBR that is exactly 512 bytes long. How this is done depends on your assembler and linker.
The last 2 bytes must be the special boot pattern (0x55 followed by 0xAA), and as said above, the bootstrap portion of the
MBR must be at most 446 bytes long. You must also put at least one partition table entry in the Partition Table
portion of the MBR -- otherwise some BIOSes will refuse to boot from the disk. This includes most UEFI firmware in BIOS-compatibility
mode.

===Initial Environment===

When the BIOS loads and runs an MBR, it is loaded into memory at physical address 0x7c00. This is ''usually'' <tt>0x0000:0x7c00</tt>
(CS = 0, offset address 0x7c00). However, some BIOSes load
to <tt>0x7c0:0x0000</tt> (CS = 0x07c0, offset address 0) -- which resolves to the same physical address, but can cause problems.
A good practice is to enforce CS:IP with a far jump near the beginning of your bootstrap code.
The MBR will probably need to immediately relocate itself anyway, and that is a good time to enforce CS:IP.

The BIOS passes very little useful information directly to the MBR. In fact, the only important number is the value in DL -- the "drive
number". It needs to be passed to all later calls to BIOS function INT 13h, so that byte in DL probably needs to be saved carefully.

The values in all the other registers, and in most of memory, are undefined.

===Immediate Priorities===

It is important to immediately set up a [[stack]], and also to set the rest of the CPU's segment registers
(DS, ES, FS, GS) properly. Setting up a stack involves pointing the SS:SP pair of registers at some memory that is not being used for anything
else (and the address must be even). The other segment registers should usually be set to 0.

See the [[Memory Map (x86)]] article to understand what memory is available during boot (low memory from 0x500 to 0x7ffff, generally).

In general, you do not want to fragment your available memory, or the memory of each 64K "page", if possible. The MBR needs a stack,
and a place to relocate itself to. The MBR will be loading a bootloader at 0x7c00, so it is reasonably convenient for the MBR to
relocate itself either to somewhere around the 0x500 to 0x600 address range, or to 0x7a00 (ie. just below 0x7c00). The stack can then
point to 0x7c00 (if the MBR is not at 0x7a00), or the stack can be just below the relocated MBR.

==Storing an MBR to the disk==

To write an MBR to the first sector of a disk, you must use special disk I/O tools, because the MBR (by definition) is not inside any disk partition.
The MBR only exists on the "raw device". There are quite a few "disk editing" tools available; some are listed in
[[:Category:Disk Image Utilities|Disk Image Utilities]].

==x86 Examples==
Assuming one knows how to read sectors from disk and has setup a function allowing them to do so, and assuming the inputs to said function are:
* EBX - 32-bit LBA Address
* CX - Sector Count
* ES:DI - Buffer
* BYTE [bootDrive] - Drive Number<br />
An extremely simple MBR complying to the standard could look like:
<syntaxhighlight lang="asm">
[bits 16]
[org 0x0600]

start:
cli ; We do not want to be interrupted
xor ax, ax ; 0 AX
mov ds, ax ; Set Data Segment to 0
mov es, ax ; Set Extra Segment to 0
mov ss, ax ; Set Stack Segment to 0
mov sp, ax ; Set Stack Pointer to 0
.CopyLower:
mov cx, 0x0100 ; 256 WORDs in MBR
mov si, 0x7C00 ; Current MBR Address
mov di, 0x0600 ; New MBR Address
rep movsw ; Copy MBR
jmp 0:LowStart ; Jump to new Address

LowStart:
sti ; Start interrupts
mov BYTE [bootDrive], dl ; Save BootDrive
.CheckPartitions: ; Check Partition Table For Bootable Partition
mov bx, PT1 ; Base = Partition Table Entry 1
mov cx, 4 ; There are 4 Partition Table Entries
.CKPTloop:
mov al, BYTE [bx] ; Get Boot indicator bit flag
test al, 0x80 ; Check For Active Bit
jnz .CKPTFound ; We Found an Active Partition
add bx, 0x10 ; Partition Table Entry is 16 Bytes
dec cx ; Decrement Counter
jnz .CKPTloop ; Loop
jmp ERROR ; ERROR!
.CKPTFound:
mov WORD [PToff], bx ; Save Offset
add bx, 8 ; Increment Base to LBA Address
.ReadVBR:
mov EBX, DWORD [bx] ; Start LBA of Active Partition
mov di, 0x7C00 ; We Are Loading VBR to 0x07C0:0x0000
mov cx, 1 ; Only one sector
call ReadSectors ; Read Sector

.jumpToVBR:
cmp WORD [0x7DFE], 0xAA55 ; Check Boot Signature
jne ERROR ; Error if not Boot Signature
mov si, WORD [PToff] ; Set DS:SI to Partition Table Entry
mov dl, BYTE [bootDrive] ; Set DL to Drive Number
jmp 0x7C00 ; Jump To VBR

times (218 - ($-$$)) nop ; Pad for disk time stamp

DiskTimeStamp times 8 db 0 ; Disk Time Stamp

bootDrive db 0 ; Our Drive Number Variable
PToff dw 0 ; Our Partition Table Entry Offset

times (0x1b4 - ($-$$)) nop ; Pad For MBR Partition Table

UID times 10 db 0 ; Unique Disk ID
PT1 times 16 db 0 ; First Partition Entry
PT2 times 16 db 0 ; Second Partition Entry
PT3 times 16 db 0 ; Third Partition Entry
PT4 times 16 db 0 ; Fourth Partition Entry

dw 0xAA55 ; Boot Signature
</syntaxhighlight>
Of course this is an extremely simplified MBR, and is only able to load the [first] active partition.

==Comments==

==See Also==

===Articles===

* [[System Initialization (x86)]]

===External Links===
* [http://www.osdever.net/downloads/bootsectors/smbmbr03.zip Fine's MBR code on osdever]
* [http://www.nondot.org/sabre/os/articles/TheBootProcess/ OSRC] -- for more boot sector info
* [http://www.ranish.com/part/ Ranish Partition Manager] -- Disk partitioning software

[[Category:X86]]
[[Category:Booting]]
[[Category:Bootloaders]]

[[de:Master Boot Record]]

Latest revision as of 18:08, 9 July 2023

A Master Boot Record (MBR) is the bootsector of a hard disk. That is, it is what the BIOS loads and runs, when it boots a hard disk. The MBR is the very first sector of the hard disk; it contains an MBR Bootstrap program (described below), and a Partition Table. Devices that emulate a hard disk during system initialization must also contain an MBR, because they must also have Partition Tables -- even if they are not bootable.

The BIOS will only boot an MBR from a device if that device is in the "boot sequence" stored in CMOS, and if the MBR on the device is formatted correctly. On the other hand, if a device is not in the boot sequence (but has a "drive number"), it is still possible for a Real Mode program (such as another MBR or bootloader) to load and boot that device's MBR directly.

MBR Bootstrap

An MBR is loaded by the BIOS at physical address 0x7c00, with DL set to the "drive number" that the MBR was loaded from. The BIOS then jumps to the very beginning of the loaded MBR (0x7c00), because that part of the MBR contains the "bootstrap" executable code.

Typical MBR bootstrap code will do the following:

  • relocate itself away from the 0x7c00 physical address (using a memory copy, and usually a far jump)
  • determine which partition (or hard disk) to boot from (either by looking for the active partition, or by presenting the user with a selection of installed operating systems to choose from)
    • if the user selected an "inactive" partition, then set the selected partition entry to "active", and clear the "active" bits of other partition entries
    • use BIOS INT 13h commands to rewrite the MBR if the partition table entries were modified
  • use BIOS INT 13h commands to load the Volume Boot Record (VBR, the "bootsector" of the bootloader) from the beginning of the selected partition to physical address 0x7c00
  • set DS:SI pointing to the selected partition table entry
  • jump to 0x7c00 (with CS set to 0, and DL set to the "drive number")

Note: it is intended for the value of DL, and the DS:SI pointer to be passed all the way into the kernel, for the kernel's use. This also means that the relocated MBR should not be overwritten during the boot process -- because the DS:SI pointer is aimed at a partition table entry inside that MBR, and needs to remain valid.

MBR Format

Notes:

  • See the Partition Table article for the format of each partition table entry field.
  • It is important to remember that the partition table entries are not aligned on 32-bit boundaries.
  • Naming the partition table entries as "First" through "Fourth" is for convenience only. The partition table entries are not required to be in any kind of order.
  • Any one of the partitions may be "active".
  • There is supposed to be one active partition table entry, at most.
  • Windows seems to verify and require that the partition it boots from be marked "active".
  • Most other operating systems don't seem to care about the "active" bit in the partition table entry.
Offset Size (bytes) Description
0x000 4401 MBR Bootstrap (flat binary executable code)
0x1B8 4 Optional "Unique Disk ID / Signature"2
0x1BC 2 Optional, reserved 0x00003
0x1BE 16 First partition table entry
0x1CE 16 Second partition table entry
0x1DE 16 Third partition table entry
0x1EE 16 Fourth partition table entry
0x1FE 2 (0x55, 0xAA) "Valid bootsector" signature bytes

1 This can be extended to 446 bytes if you omit the next 2 optional fields: Disk ID and reserved.

2 The 4 byte "Unique Disk ID" is used by recent Linux and Windows systems to identify the drive. "Unique" in this case means that the IDs of all the drives attached to a particular system are distinct.

3 The 2 byte reserved is usually 0x0000. 0x5A5A means read-only according to https://neosmart.net/wiki/mbr-boot-process/.

Partition table entry format

Offset1 Size (bytes) Description
0x00 1 Drive attributes (bit 7 set = active or bootable)
0x01 3 CHS Address of partition start
0x04 1 Partition type
0x05 3 CHS address of last partition sector
0x08 4 LBA of partition start
0x0C 4 Number of sectors in partition

1 Relative to the start of the partition entry.

Traditional MBR

The DOS FDISK program was the first to ever use an MBR, so that MBR became the de-facto standard. It also automatically became the standard for the minimum level of functionality of an MBR. It was never changed after it was first introduced.

The whole point of the FDISK program is to manipulate the MBRs of the hard disks attached to a system. When FDISK partitions a blank disk, it writes an MBR to sector 0 of the drive. When FDISK adds a new partition to a disk, it adds an entry into the Partition Table in the MBR. When FDISK makes a partition "active", it sets the "flag" byte in the Partition Table entry to 0x80.

The MBR that FDISK uses is coded to:

  • relocate itself to 0x0000:0x0600
  • examine the byte at offset 0x1be, 0x1ce, 0x1de, and 0x1ee to determine the active partition
  • load only the first sector of the active partition (which is expected to contain a DOS bootsector) to 0x0000:0x7c00 (hence the previous relocation)
  • set SI
  • jump to 0x7c00 -- transferring control to the DOS bootsector.


Dual Booting

A typical system may have several hard disks on it, and each hard disk can have 4 standard partitions (without going into the extra complication of Extended Partitions). Each of those partitions could theoretically hold its own distinct bootable OS, and filesystem.

However, the standard x86 boot sequence will only ever boot the MBR from the "C:" disk (the first disk found during the disk detection phase). And the standard MBR will only allow a single active partition on that disk, and will only ever boot that one partition. This is really completely inadequate.

It is not possible to modify the BIOS/CMOS boot sequence -- but you can change the MBR. So, one solution is to replace the standard MBR with a "custom, Dual Booting" MBR. (For a good example, see John Fine's SMBMBR in the External Links, below.)

A simple dual booting MBR will allow the user to select any partition on the current drive, to boot. A more complicated dual booting MBR will also allow the user to select other hard disks, or even to specifically choose other partitions on other hard disks, to boot.

For one MBR to load and run a different MBR off a different drive is called "chain loading". If all the MBRs are dual booting, then the user can cycle through all the disks, and choose to boot the "correct" partition from the "correct" drive.

A dual booting MBR is a huge improvement over the standard MBR. The only problem is that an MBR bootstrap is limited to a little over 400 bytes of code. Such a tiny program is insufficient to create a "nice" user-friendly dual booting system that has commercial appeal. It is enough space to hack up an interface that is good enough for the person who wrote it.

One way around this size limitation is to note that a bootloader can contain a lot more code than an MBR. So a custom MBR can try to load a "preferred" bootloader (if it can find one on some partition, somewhere) -- and that preferred bootloader can have a very pretty, user-friendly interface that allows the user to select any partition off any drive, to boot.

It is also nice, if there is only one partition on only one drive (ie. there is no choice for a user to make), if the MBR will boot that one partition automatically -- without bothering the user with prompts.

Generic MBRs

As said above, the MBR that is written to the disk by the DOS FDISK program is "the most generic" one. But every disk partitioning app must write some sort of MBR to the disk, and every generic bootloader may well need a custom MBR.

Fortunately, there was a standard for them to conform to (the FDISK MBR), and they all did conform to it. Some of them may be Dual Booting, or have other features -- but they will all end up loading your bootloader at the standard address (0x7c00), with the DL register set to the boot "drive number", and DS:SI pointing at the correct partition table entry of the correct MBR.

Building a Custom MBR Bootstrap program

When the BIOS transfers control to the MBR bootstrap code, the system is in Real mode. The MBR will probably also run entirely in Real mode, so it is important to understand Real mode addressing.

Self-relocation is one of the things C can't do, and most modern C compilers can't create code that is compatible with Real mode, anyway. So an MBR must be written in Assembly.

It is necessary to build an MBR that is exactly 512 bytes long. How this is done depends on your assembler and linker. The last 2 bytes must be the special boot pattern (0x55 followed by 0xAA), and as said above, the bootstrap portion of the MBR must be at most 446 bytes long. You must also put at least one partition table entry in the Partition Table portion of the MBR -- otherwise some BIOSes will refuse to boot from the disk. This includes most UEFI firmware in BIOS-compatibility mode.

Initial Environment

When the BIOS loads and runs an MBR, it is loaded into memory at physical address 0x7c00. This is usually 0x0000:0x7c00 (CS = 0, offset address 0x7c00). However, some BIOSes load to 0x7c0:0x0000 (CS = 0x07c0, offset address 0) -- which resolves to the same physical address, but can cause problems. A good practice is to enforce CS:IP with a far jump near the beginning of your bootstrap code. The MBR will probably need to immediately relocate itself anyway, and that is a good time to enforce CS:IP.

The BIOS passes very little useful information directly to the MBR. In fact, the only important number is the value in DL -- the "drive number". It needs to be passed to all later calls to BIOS function INT 13h, so that byte in DL probably needs to be saved carefully.

The values in all the other registers, and in most of memory, are undefined.

Immediate Priorities

It is important to immediately set up a stack, and also to set the rest of the CPU's segment registers (DS, ES, FS, GS) properly. Setting up a stack involves pointing the SS:SP pair of registers at some memory that is not being used for anything else (and the address must be even). The other segment registers should usually be set to 0.

See the Memory Map (x86) article to understand what memory is available during boot (low memory from 0x500 to 0x7ffff, generally).

In general, you do not want to fragment your available memory, or the memory of each 64K "page", if possible. The MBR needs a stack, and a place to relocate itself to. The MBR will be loading a bootloader at 0x7c00, so it is reasonably convenient for the MBR to relocate itself either to somewhere around the 0x500 to 0x600 address range, or to 0x7a00 (ie. just below 0x7c00). The stack can then point to 0x7c00 (if the MBR is not at 0x7a00), or the stack can be just below the relocated MBR.

Storing an MBR to the disk

To write an MBR to the first sector of a disk, you must use special disk I/O tools, because the MBR (by definition) is not inside any disk partition. The MBR only exists on the "raw device". There are quite a few "disk editing" tools available; some are listed in Disk Image Utilities.

x86 Examples

Assuming one knows how to read sectors from disk and has setup a function allowing them to do so, and assuming the inputs to said function are:

  • EBX - 32-bit LBA Address
  • CX - Sector Count
  • ES:DI - Buffer
  • BYTE [bootDrive] - Drive Number

An extremely simple MBR complying to the standard could look like:

[bits 16]
[org 0x0600]

start:
  cli                         ; We do not want to be interrupted
  xor ax, ax                  ; 0 AX
  mov ds, ax                  ; Set Data Segment to 0
  mov es, ax                  ; Set Extra Segment to 0
  mov ss, ax                  ; Set Stack Segment to 0
  mov sp, ax                  ; Set Stack Pointer to 0
  .CopyLower:
    mov cx, 0x0100            ; 256 WORDs in MBR
    mov si, 0x7C00            ; Current MBR Address
    mov di, 0x0600            ; New MBR Address
    rep movsw                 ; Copy MBR
  jmp 0:LowStart              ; Jump to new Address

LowStart:
  sti                         ; Start interrupts
  mov BYTE [bootDrive], dl    ; Save BootDrive
  .CheckPartitions:           ; Check Partition Table For Bootable Partition
    mov bx, PT1               ; Base = Partition Table Entry 1
    mov cx, 4                 ; There are 4 Partition Table Entries
    .CKPTloop:
      mov al, BYTE [bx]       ; Get Boot indicator bit flag
      test al, 0x80           ; Check For Active Bit
      jnz .CKPTFound          ; We Found an Active Partition
      add bx, 0x10            ; Partition Table Entry is 16 Bytes
      dec cx                  ; Decrement Counter
      jnz .CKPTloop           ; Loop
    jmp ERROR                 ; ERROR!
    .CKPTFound:
      mov WORD [PToff], bx    ; Save Offset
      add bx, 8               ; Increment Base to LBA Address
  .ReadVBR:
    mov EBX, DWORD [bx]       ; Start LBA of Active Partition
    mov di, 0x7C00            ; We Are Loading VBR to 0x07C0:0x0000
    mov cx, 1                 ; Only one sector
    call ReadSectors          ; Read Sector

  .jumpToVBR:
    cmp WORD [0x7DFE], 0xAA55 ; Check Boot Signature
    jne ERROR                 ; Error if not Boot Signature
    mov si, WORD [PToff]      ; Set DS:SI to Partition Table Entry
    mov dl, BYTE [bootDrive]  ; Set DL to Drive Number
    jmp 0x7C00                ; Jump To VBR

times (218 - ($-$$)) nop      ; Pad for disk time stamp

DiskTimeStamp times 8 db 0    ; Disk Time Stamp

bootDrive db 0                ; Our Drive Number Variable
PToff dw 0                    ; Our Partition Table Entry Offset

times (0x1b4 - ($-$$)) nop    ; Pad For MBR Partition Table

UID times 10 db 0             ; Unique Disk ID
PT1 times 16 db 0             ; First Partition Entry
PT2 times 16 db 0             ; Second Partition Entry
PT3 times 16 db 0             ; Third Partition Entry
PT4 times 16 db 0             ; Fourth Partition Entry

dw 0xAA55                     ; Boot Signature

Of course this is an extremely simplified MBR, and is only able to load the [first] active partition.

Comments

See Also

Articles

External Links