Windows 7/8/8.1 MBR Examination Part 2

The second (and the last so far) article about Master Boot Record and Windows boot process describes what the code stored in this special sector actually does.

MBR Boot Code

The boot code is executed in the real mode of the processor which implies non-flat addressing. Every address consists of two parts: segment and offset. Both parts are two byte long. Physical address corresponding to a given segment:offset pair can be calcualted by the following formula:

PhysicalAddress = Segment * 16 + Offset

Since the sizes of both parts allow to address 65536 segments, each 64 KB in length, and since the fact that the formula suggests 16 bytes long segments, the individual segments of the address space actually overlap with their neighbors. For a given physical address there must exist multiple segment:offset pairs that map to it. For example, “virtual” addresses of 0x0060:0, 0x0006:0x5A0 and 0x0000:0x600 map to the physical address 0x600.

Code Relocation

The BIOS loads the Master Boot Record to physical address 0x7C00 and jumps to the first byte of the sector. The main task of the boot code is to load the first sector of the active partition (Volume Boot Record) to 0x7C00 and execute it. The same physical address for the MBR and the VBR makes the initial boot process more transparent; boot code in the VBR might safely think that it is the first code executed after initialization performed by the BIOS (which can be ture since the VBR might be the first sector of the disk. Such disks are called dynamic disks). In case of extended partitions, VBR code may behave similarly to the MBR one – it just finds bootable logical partition, loads its first sector and executes it. Theoreticaly, such a chain might be very long because each of its links can assume to be the first one.

However, the need of loading the VBR into the same address as the MBR forces the initial boot code to copy the whole sector to another location before doing anything else. The code performs this within its first block of instrluctions, displayed in Listing 1. It copies the whole Master Boot Record to buffer starting at physical address 0x600.

At first, the code fills segment registers with zeroes and sets the stack pointer SP to 0x7C00. This is OK since the stack grows to lower addresses, hence it cannot override the boot code starting at the same address.

Listing 1: The first instructions of the boot code

0x7C00: xor ax, ax
0x7C02: mov ss, ax
0x7C04: mov sp, 7C00h
0x7C07: mov es, ax
0x7C09: mov ds, ax
0x7C0B: mov si, 7C00h
0x7C0E: mov di, 600h
0x7C11: mov cx, 200h
0x7C14: cld
0x7C15: rep movsb
0x7C17: push ax
0x7C18: push InitBootPartitionSearch
0x7C1B: retf

The MOVSB instruction is used to copy 512 bytes from 0x0000:0x7C00 to 0x0000:0x600. Then, the code jumps to 0x0000:0x61C, which is just behind the RETF instruction shown in Listing 1. After the jump, the boot code can now start to care about its main task – loading and executing Volume Boot Record of the active partition.

Active Partition Recognition and Error Message Printing

As shown in Listing 2, the boot code searches for active partition in a straightforward manner. At first, it sets the BP register to point to the boot flags field of the first partition entry. Then, it determines value of the most signifficant bit (MSB) of the field. If the bit is set, the code assumes the partition is active, and jumps to the CheckLBAExtension label. That effectively stops the search operation. If the MSB is not set and the boot flags field is nonzero, the code treats the partition entry as invalid, uses the InvalitPartitionTableError label to display appropriate error message and stops the processor. Zero value of the field means inactive partition; the code advances to the next entry by increasing the BP register.

Listing 2: Searching for active partition

0x061C: sti
0x061D: mov cx, 4
0x0620: mov bp, 7BEh
0x0623: cmp byte ptr [bp+0], 0
0x0627: jl short CheckLBAExtension
0x0629: jnz InvalitPartitionTableError
0x062D: add bp, 10h
0x0630: loop SearchPartitionTable
0x0632: int 18h

In case no active partition is found, which is indicated by the fact the CX register reaching zero so the LOOP instruction does not jump, the interrupt 0x18 is invoked. This causes the processor to either stop or reset.

Error messages are printed through the service 0xE of the interrupt 0x10. The service displays character stored in the AL register to screen. Error messages are null terminated strings. The print operation, represented by the PrintErrorMessageCharacter loop, continues until the null character is reached. Position in the string is marked by the DS:SI register pair. When the end of the message is found, the boot code starts to execute the HTL instruction in an infinite loop. The code is shown in Listing 3.

Listing 3: Displaying error messages

0x073E: xor ah, ah
0x0740: add ax, 700h
0x0743: mov si, ax
0x0745: lodsb
0x0746: cmp al, 0
0x0748: jz  short Halt
0x074A: mov bx, 7
0x074D: mov ah, 0Eh
0x074F: int 10h
0x0751: jmp short PrintErrorMessageCharacter
0x0753: hlt
0x0754: jmp short Halt

To display any error message and halt the processor, the boot code just needs to set the AL register to “point” to the message string (the “pointer” is actually an offset relative to the current address of the boot code – 0x0000:0x600), and jump to PrintErrorMessage label. This scenario holds for every error message, including report of invalid partition table (see Listing 4).

Listing 4: Printing error message for invalid partition entry

0x073B: mov al, 63h
0x0763: db 'Invalid partition table',0

Reading the Volume Boot Record

As mentioned above, there exist at least two methods how to read data from a hard drive: via CHS address and through the LBA extension. The LBA extension was added later and it is a good practice to check whether it is supported by the hard drive. The CHS method is always present, however, its addressing capabilities are very limited, as discussed previously.

The code related to reading of the hard drive starts at the CheckLBAExtension label (Listing 5). At first, presence of the LBA extension is checked. The code uses service 0x41 of the interrupt 0x13. The BIOS returns information about available extensions in the CX register. If bit 0 of the register is set, the LBA extension is present. The boot code saves the state of the extension to [BP+0x10]. As can be seen in Listing 5, the code also sets [BP+0x11] to 5. It is the number of attempts the it attempts to read Volume Boot Record of the active partition.

Listing 5: Checking for presence of LBA extension

0x0634: mov [bp+0], dl
0x0637: push bp
0x0638: mov byte ptr [bp+11h], 5
0x063C: mov byte ptr [bp+10h], 0
0x0640: mov ah, 41h
0x0642: mov bx, 55AAh
0x0645: int 13h 
0x0647: pop bp
0x0648: jb short ReadVBR
0x064A: cmp bx, 0AA55h
0x064E: jnz short ReadVBR
0x0650: test cx, 1
0x0654: jz short ReadVBR
0x0656: inc byte ptr [bp+10h]

If the LBA extension check fails (BIOS returns an error), the boot code assumes the LBA extension is not present, and continues the execution.

After the extension check, the boot code attempts to read the Volume Boot Record of the active partition (see Listing 6). It selects the read method according to the value stored at [BP+0x10]. In case of the CHS mode, the CHS address of the Volume Boot Record is loaded from the partition entry, and the value in the boot flags field is used to identify the hard drive (0x80 menas the first hard drive). When the LBA method is selected, LBA address of the VBR is loaded from the partition entry. The boot code always stores the data of the VBR at 0x7C00.

Listing 6: Reading the Volume Boot Record

0x0659: pushad
0x065B: cmp byte ptr [bp+10h], 0
0x065F: jz short ReadVBRCHS
0x0661: push large 0                ; LBA address (high 32 bits)
0x0667: push large dword ptr [bp+8] ; LBA address (low 32 bits)
0x066B: push 0                      ; Memory address (segment)
0x066E: push 7C00h                  ; Memory address (offset)
0x0671: push 1                      ; Number of sectors
0x0674: push 10h                    ; Extended structure size
0x0677: mov ah, 42h
0x0679: mov dl, [bp+0]
0x067C: mov si, sp
0x067E: int 13h 
0x0680: lahf
0x0681: add sp, 10h
0x0684: sahf                                 
0x0685: jmp short CheckSuccess
0x0687: mov ax, 201h
0x068A: mov bx, 7C00h
0x068D: mov dl, [bp+0]
0x0690: mov dh, [bp+1]
0x0693: mov cl, [bp+2]
0x0696: mov ch, [bp+3]
0x0699: int 13h 

After the read operation is performed, time comes to check whether it succeeded. The operation is performed by the code displayed in Listing 7. It starts at the CheckSuccess label. If a failure is detected, the boot code resets the hard drive (see the ResetDiskSystem label) and makes another attempt to read the VBR. Value at [BP+0x11] is decremented after each unsuccessful attempt. When the value reaches zero, the boot code gives up and uses the ErrorLoadingOperatingSystem label to display appropriate error message and halt the processor.

Listing 7: Checking success of the read operation

0x069B: popad
0x069D: jnb short KeyboardInit
0x069F: dec byte ptr [bp+11h]
0x06A2: jnz short ResetDiskSystem
0x06A4: cmp byte ptr [bp+0], 80h
0x06A8: jz ErrorLoadingOperatingSystem
0x06AC: mov dl, 80h
0x06AE: jmp short CheckLBAExtension
0x06B0: push bp
0x06B1: xor ah, ah
0x06B3: mov dl, [bp+0]
0x06B6: int 13h
0x06B8: pop bp
0x06B9: jmp short ReadVBR
0x06BB: cmp ds:7DFEh, 0AA55h
0x06C1: jnz short MBRSignatureInvalid
0x0731: mov al, 9Ah
0x0734: jmp short PrintErrorMessage
0x0736: mov al, 7Bh
0x0739: jmp short PrintErrorMessage
0x077B: db 'Error loading operating system', 0
0x079A: db 'Missing operating system', 0

If the read operation succeeds, the boot code reaches the KeyboardInit label. And if the Volume Boot Record contains signature 0xAA55 at its end, the main task of the MBR is now complete; the first sector of the active partition is stored in memory at address 0x0000:0x7C00. Before executing it, an attempt is made to perform two optional steps: keyboard initialization and VBR content hashing.

Keyboard Initialization

The 8042 keyboard controller is a device that, apart from keyboard services, offers features none would have expected. It can be commanded to reset the processor immediately. And it can enable the famous A20 line that allows real mode code to address physical memory above 1 megabyte. Because the operating system usually wants to boot successfully and not to keep reseting the processor in an infinite loop, the reset ability of the controller is just an interesting feature, at least from this point of view. Situation is different with the A20 line.

The keyboard initialization code is shon in Listing 8. The boot code first checks whether the keyboard is able to receive commands. This is the task for the WaitKeyboardReady function, shown in Listing 9. If the function returns nonzero value, the controller cannot receive commands at the moment. In such a case, the boot code skips the whole initialization and attempts to find Trusted Platform Module (TPM) or a device with similar capabilities.

Keyboard initialization code works with two I/O ports related to the 8042 controller: port 64h, called a command port, and port 60h, a data port. The communication protocol is the following:

  • Send a command to the command port.
  • Wait until the controller is ready to receive data for the command.
  • Send command data to the data port. This step is ommited in case the command does not require any data to be sent.
  • Wait until the keyboard controller processes the command.

The boot code sends command 0xD1 with data byte 11011111b. The command enables keyboard interrupt and the A20 line. Since this time, when a key is pressed or released, the keyboard controller generates an interrupt to report the event. Additionaly, it is possible to address physical memory above 1 megabyte.

Listing 8: Keyboard initialization

0x06C3: push word ptr [bp+0]
0x06C6: call WaitKeyboardReady
0x06C9: jnz short CheckTCGStatus
0x06CB: cli
0x06CC: mov al, D1h
0x06CE: out 64h, al
0x06D0: call WaitKeyboardReady
0x06D3: mov al, 11011111b
0x06D5: out 60h, al
0x06D7: call WaitKeyboardReady
0x06DA: mov al, FFb
0x06DC: out 64h, al
0x06DE: call WaitKeyboardReady
0x06E1: sti

To determine whether the keyboard controller is ready to receive commands, the WaitKeyboardReady routine repeatedly checks keyboard status. This is done by reading a byte from the port 0x64 which is, in the read case, sometimes called a keyboard status. If the bit 1 is set, the controller’s input buffer is full, hence the device cannot accept and process any commands right now. Otherwise, the controller is ready to receive commands and data.

Listing 9: Waiting for the controller

WaitKeyboardReady proc near       
0x0756: sub cx, cx
0x0758: in al, 64h      
0x075A: jmp short $+2    
0x075C: and al, 2
0x075E: loopne ReadKeyboardStatus
0x0760: and al, 2
0x0762: retn

Trusted Platform Module

The code that works with TPM (or another device which supports TCG interface) is quite straightforward. It is displayed in Listing 10. At first, it checks whether a device capable of cryptographics services, is available. If so, the code determines whether the device is able to hash a given area of physical memory. If yes, a hash of the Volume Boot Record is computed and probably stored inside the cryptographic device. Communication with the device is performed via the interrupt 0x1A.

Listing 10: Hashing of the Volume Boot Record

0x06E2: mov ax, 0BB00h
0x06E5: int 1Ah
0x06E7: and eax, eax
0x06EA: jnz short ExecuteVBR
0x06EC: cmp ebx, 'APCT'
0x06F3: jnz short ExecuteVBR
0x06F5: cmp cx, 102h
0x06F9: jb short ExecuteVBR
0x06FB: push large 0BB07h
0x0701: push large 200h 
0x0707: push large 0
0x070D: push ebx     
0x070F: push ebx      
0x0711: push ebp          
0x0713: push large 0       
0x0719: push large 7C00h 
0x071F: popad
0x0721: push 0
0x0724: pop es            
0x0725: int 1Ah
0x0727: pop dx
0x0728: xor dh, dh
0x072A: jmp far ptr 0000:7C00h

When the code finishes with the cryptography, it finally jumps to address 0x7C00 in order to execute the VOlume Boot Record.


Leave a Reply

Your email address will not be published. Required fields are marked *