flat assembler
Message board for the users of flat assembler.

Index > OS Construction > Did I enter protected mode right?

Goto page 1, 2  Next
Author
Thread Post new topic Reply to topic
IsaacZ



Joined: 05 Dec 2025
Posts: 12
IsaacZ 08 May 2026, 14:29
I've been trying to develop a very simple, and solid 32-Bit Operating System. I am having a struggle figuring out if I entered protected mode right. Do ignore the kernel, I'm not fixing that now until the bootloader is solid. I would like to know anything I did wrong and how to fix it. I also recognise the IDT isn't finished, but like I said, I'm focusing on bootloader.

Code:
;16-bit Main registers include AX(Accumulator), BX(Base), CX(Count), DX(Data). While 8-bit values are AH(high 8-bit) and AL(low 8-bit), etc.
;16-bit Index registers include SI(Source Index), DI(Destination Index), BP(Base Pointer), and SP(Stack Pointer).
;Segment registers include CS(Code Segment), DS(Data Segment), ES(Extra Segment), FS, GS, SS(Stack Segment)
;The pointer regusters are IP(Instruction Pointer), SP(Stack Pointer), and BP(Base Pointer)
;pusha pushes in this order: AX, CX, DX, BX, SP, BP, SI, DI. And is popped (pop) in reverse
;Overflow Flag, Direction Flag, Inturrupt Flag, Trap Flag, Sign Flag, Zero Flag, Adjust Flag, Parity Flag, and Carry Flag are bits within 16-bit Status registers
;cmp compares two values, and is usually followed by a jump
;Jumps include jmp(Unconditional Jump), jne(Jump if not equal to), jg(Jump if greater than), jl(Jump if less than), jge(Jump if greater than, or equal to), jle(Jump if less than, or equal to)
;$ is the current memory adress, $$ is the start of the current memory adress.
;ah=0e(hex)and then calling Inturrupt 10(hex) switches computer to printing mode. (Turn on your digital typewriter, starring BIOS!)
;ah=0 and then calling inturrupt 16 makes computer wait for keypress.
;int(Inturrupt) is the CPU's responce to an event that needs attention
;hlt(Halt) halts the CPU until the next inturrupt
;0x* and *h are both specifying hexadecimal values
;Define diractives are db(Define Byte), dw(Define Word), dd(Define Doubleword), dq(Define Quadword), dt(Define Ten Bytes)
;inc(Increment) is like add 1 to a value, exept inc preserves Carry flag while add sets all the flags. Example(inc al == add al, 1)
;0xAA55 or 0x55, 0xAA  is the boot signature, literally makes the computer boot.

;Memory Map:
;0000:7C00 - Bootloader
;0000:7E00 = Kernel

;MUST keep DS at 0 while in bootloader, 0x1000 while in kernel.
;MUST keep ES at 0 while in bootloader, 0x1000 while reading from disk, and in kernel.
;When jump to kernel, CS becomes 0x1000

;PROTECTED MODE STRUCTURE
;[Y] CLEAR INTERRUPTS
;[Y] ENABLE A20 LINE
;[Y] LOAD IN GDT (Global Descriptor Table)
;[?] LOAD IN IDT (Interrupt Descriptor Table)

org 0x7C00
use16

bootloader:
    cli
    cld
    xor ax, ax
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov bp, 0x7000
    mov sp, bp
    mov [boot_drive], dl

;Set video mode 0x03
    mov ax, 0x0003
    int 0x10

enable_a20:
    in al, 0x92
    or al, 0x2
    out 0x92, al
    sti

read_disk:
    mov ah, 0x02
    mov al, 5
    mov ch, 0
    mov cl, 2
    mov dh, 0
    mov dl, [boot_drive]
    mov bx, 0x7E00
    int 0x13
    jc disk_error

CODE_OFFSET equ gdt_code - gdt_start ;Also a synonym to 0x08
DATA_OFFSET equ gdt_data - gdt_start ;Also a synonym to 0x10

load_gdt:
    cli
    lgdt [gdt_descriptor]
    mov eax, cr0
    or eax, 1
    mov cr0, eax
    jmp CODE_OFFSET:start_protected

use32
start_protected:
    mov ax, DATA_OFFSET
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax
    mov esp, 0x90000

    mov edi, 0xB8000
    mov al, '?'
    mov ah, 0x0F
    mov [edi], ax

    jmp 0x08:0x7E00

gdt_start:
gdt_null:
    dd 0x0
    dd 0x0
gdt_code:
    dw 0xFFFF
    dw 0
    db 0
    db 10011010b
    db 11001111b
    db 0
gdt_data:
    dw 0xFFFF
    dw 0
    db 0
    db 10010010b
    db 11001111b
    db 0
gdt_end:
gdt_descriptor:
    dw gdt_end - gdt_start - 1
    dd gdt_start

disk_error:
    mov ah, 0x0E
    mov al, '!'
    mov bh, 0
    int 0x10

    hlt
    jmp $

boot_drive db 0x00

times 510-($-$$) db 0
dw 0xAA55

org 0x7E00
use32
kernel_start:
    mov ax, 0x10
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax
    mov esp, 0x90000

;IDT
load_idt:
    lidt [idt_descriptor]

;MAIN FOR NOW
    mov ebx, test_msg
    call print_string32

    jmp $

print_string32:
    pusha
    mov edx, 0xB8000
.loop:
    mov al, [ebx]
    cmp al, 0
    je .done

    mov ah, 0x0F
    mov [edx], ax
    add ebx, 1
    add edx, 2
    jmp .loop
.done:
    popa
    ret

test_msg db 'PROTECTED AND 32-BITS.', 0

isr_default:
    iret
idt_start:
    dw isr_default and 0xFFFF
    dw 0x08
    db 0
    db 10001110b
    dw isr_default shr 0x10
    times 255 dq 0
idt_end:
idt_descriptor:
    dw idt_end - idt_start - 1
    dd idt_start
    

Thanks for reading Surprised

_________________
Developing a new Operating System...
Post 08 May 2026, 14:29
View user's profile Send private message Visit poster's website Reply with quote
SeproMan



Joined: 11 Oct 2009
Posts: 76
Location: Belgium
SeproMan 09 May 2026, 23:02
Looks ok, but following are some observations.

Quote:
;0000:7E00 = Kernel
;MUST keep DS at 0 while in bootloader, 0x1000 while in kernel.


Here's a conflict about where the kernel resides!

Code:
    mov ss, ax
    mov bp, 0x7000
    mov sp, bp    


Always load SP directly beneath loading SS. Then you don't need those CLI and STI instructions. So better write:
Code:
    mov bp, 0x7000
    mov ss, ax
    mov sp, bp    


Code:
    hlt
    jmp $    


In order for HLT to stay in effect, the better choice here would be:

Code:
    cli
    hlt
    jmp $-2    


Loading those 5 disk sectors could go wrong for any odd reason. Therefore it is better to give it a few tries (eg. 5) and not give up on the very first error. And to avoid some potential errors, it is more robust to read the 5 successive sectors one by one (AL=1) instead of all together (AL=5).

The code at disk_error should carry a USE16 predicate.

The code at kernel_start should not be repeating setting up the segment registers.

_________________
Real Address Mode.
Post 09 May 2026, 23:02
View user's profile Send private message Reply with quote
IsaacZ



Joined: 05 Dec 2025
Posts: 12
IsaacZ 10 May 2026, 21:30
Thanks for bringing those problems to my attention @SeproMan, I'm currently fixing those problems. I also found another potential problem, which I fixed by adding this:
Code:
org 0x7C00
use16

jmp 0x0000:start ;Some BIOSes would jump to 07C0:0000 which would make CS be 0x07C0.

start:
    ;Setup registers
    
Post 10 May 2026, 21:30
View user's profile Send private message Visit poster's website Reply with quote
Core i7



Joined: 14 Nov 2024
Posts: 172
Location: Socket on motherboard
Core i7 11 May 2026, 06:34
IsaacZ wrote:
I would like to know anything I did wrong and how to fix it.

1. Load the kernel at the lowest available address to get more real-mode kernel space. Address 0x0600 is preferred, rather than 0x7E00, as you currently have, which is specified in int-13h.

2. There are also errors in the descriptor formatting. For example, the GDT should be aligned on a 4KB boundary in virtual memory, but since you haven't implemented paging yet, define it at least at 16 bytes.

Also, pay attention to the "Accessed" bit in the descriptor's Type field. To avoid a GP# exception, it's best to set it to 1 beforehand. Even though the CPU automatically sets this bit the first time it accesses the code/data segment and updates the descriptor, the GDT may not be writable, and then a GP# exception will occur. While you're in real mode, this isn't critical, but after switching to protected mode, the error will be difficult to detect.

For example, here are the WinDbg debugger logs for a GDT x32 table dump request - note the flags in the last column:
Code:
kd> dg 0 80
                                     P  Si  Gr  Pr  Lo
Sel     Base     Limit      Type     l  ze  an  es  ng   Flags
----  -------- --------  ----------  -  --  --  --  --  --------
0000  00000000 00000000  <Reserved>  0  Nb  By  Np  Nl  00000000
0008  00000000 ffffffff  Code RE Ac  0  Bg  Pg  P   Nl  00000c9b
0010  00000000 ffffffff  Data RW Ac  0  Bg  Pg  P   Nl  00000c93
0018  00000000 ffffffff  Code RE Ac  3  Bg  Pg  P   Nl  00000cfb
0020  00000000 ffffffff  Data RW Ac  3  Bg  Pg  P   Nl  00000cf3
0028  80042000 000020ab  TSS32 Busy  0  Nb  By  P   Nl  0000008b
0030  ffdff000 00001fff  Data RW Ac  0  Bg  Pg  P   Nl  00000c93
0038  7ffde000 00000fff  Data RW Ac  3  Bg  By  P   Nl  000004f3
0040  00000400 0000ffff  Data RW     3  Nb  By  P   Nl  000000f2
0048  00000000 00000000  <Reserved>  0  Nb  By  Np  Nl  00000000
0050  8054a100 00000068  TSS32 Avl   0  Nb  By  P   Nl  00000089
0058  8054a168 00000068  TSS32 Avl   0  Nb  By  P   Nl  00000089
0060  00022f40 0000ffff  Data RW Ac  0  Nb  By  P   Nl  00000093
0068  000b8000 00003fff  Data RW     0  Nb  By  P   Nl  00000092
0070  ffff7000 000003ff  Data RW     0  Nb  By  P   Nl  00000092
0078  80400000 0000ffff  Code RE     0  Nb  By  P   Nl  0000009a
0080  80400000 0000ffff  Data RW     0  Nb  By  P   Nl  00000092    

Now let's request the value of the GDTR register, and immediately output a dump of this GDT table:
Code:
kd> r @gdtr
       gdtr = 8003f000    <--------- 4Kb padding

kd> dqs 8003f000 L5
8003f000   00000000`00000000
8003f008   00cf9b00`0000ffff    <--- Code Ring(0)
8003f010   00cf9300`0000ffff    <--- Data Ring(0)

8003f018   00cffb00`0000ffff    <--- Code Ring(3)
8003f020   00cff300`0000ffff    <--- Data Ring(3)    

Here you can see that bit (A) is set to 1 (see bytes 0x9b and 0x93).
But if you decode the values ​​from your table, bit (A) is cleared everywhere—here's the code:
Code:
format  pe64 console
include 'win64ax.inc'
entry start
;//------------------
section '.data' data readable writeable
gdtCode    dw  0xffff,0
           db  0, 10011010b, 11001111b, 0

gdtData    dw  0xffff,0
           db  0, 10010010b, 11001111b, 0
;//------------------
section '.text' code readable executable
start:  sub     rsp,8
        mov     rax,qword[gdtCode]
        mov     rbx,qword[gdtData]
       cinvoke  printf,<10,' Code desc: 0x%p',\
                        10,' Data desc: 0x%p',0>,rax,rbx
       cinvoke  getch
       cinvoke  exit,0
;//------------------
section '.idata' import data readable writeable
library  msvcrt,'msvcrt.dll'
import   msvcrt,printf,'printf',getch,'_getch',exit,'exit'


;//------- RESULT ------------//
Code desc: 0x00CF9A000000FFFF
Data desc: 0x00CF92000000FFFF    

So your table should look like this:
Code:
align 16            ;<------------ Padding
gdt_start:  dq  0 
gdt_code0   dq  0x00cf9b000000ffff
gdt_data0   dq  0x00cf93000000ffff
gdt_code3   dq  0x00cffb000000ffff
gdt_data3   dq  0x00cff3000000ffff

gdt_desc:   dw  $ - gdt_start -1
            dd  gdt_start    


Last edited by Core i7 on 11 May 2026, 12:40; edited 1 time in total
Post 11 May 2026, 06:34
View user's profile Send private message Reply with quote
Core i7



Joined: 14 Nov 2024
Posts: 172
Location: Socket on motherboard
Core i7 11 May 2026, 08:16
Moreover, while you're in RM, don't rush to switch to PM, but gather as much system information as possible using interrupts. In protected mode, you won't have BIOS services, and you'll have to do everything manually.

For example, for virtual memory with PAGE_TABLE, you'll need to know the size of physical memory above 1MB. Of course, you can read the SMBIOS table in RM, but it only shows the size of installed DDR-SDRAM. However, using the int-15h AX=0xE820 interrupt, you can get a full map of free/used memory, and then create a PAGE_TABLE based on that.

In addition to memory, you need to know the number of CPU cores (if you plan on multitasking), collect information about physical devices on the motherboard using "PCI-Config-Spase", determine the hard drive type ATA/SATA/SSD/NVMe (you'll need to write a driver for PM), and much more. Once finished, collect all this data into a single structure somewhere in memory, and only then switch to protected mode. This will save you a lot of time and frustration in the future.

Here are the sources for the OS I recently wrote: https://board.flatassembler.net/topic.php?t=23907
Post 11 May 2026, 08:16
View user's profile Send private message Reply with quote
IsaacZ



Joined: 05 Dec 2025
Posts: 12
IsaacZ 11 May 2026, 13:23
Thanks @Core i7, I'm fixing some of those problems right now, such as the GDT
Core i7 wrote:
Moreover, while you're in RM, don't rush to switch to PM, but gather as much system information as possible using interrupts. In protected mode, you won't have BIOS services, and you'll have to do everything manually.

I will, but at the moment, I'm trying to make my current code as solid as possible before moving on, Thanks again Smile

_________________
Developing a new Operating System...
Post 11 May 2026, 13:23
View user's profile Send private message Visit poster's website Reply with quote
IsaacZ



Joined: 05 Dec 2025
Posts: 12
IsaacZ 19 May 2026, 21:09
UPDATE: I spend a week doing some fixes, it should be alot more solid, if I did anything wrong or need to improve anything, please let me know. (I'm a beginner but slowly making progress learning Cool )

Here's the updated bootsector:
Code:
org 0x7C00
use16

jmp 0x0000:start ;Some BIOSes would jump to 07C0:0000 which would make CS be 0x07C0.

start:
    cli
    cld
    xor ax, ax
    mov ds, ax
    mov es, ax
    mov bp, 0x7000
    mov ss, ax
    mov sp, bp ;Load SP right after loading SS
    mov [boot_drive], dl
    mov di, 5

;Set video mode 0x03
    mov ax, 0x0003
    int 0x10

enable_a20:
    in al, 0x92
    or al, 0x2
    out 0x92, al
    sti

read_disk:
    mov ah, 0x02
    mov al, 0x01
    mov ch, 0x00
    mov cl, 0x02
    mov dh, 0x00
    mov dl, [boot_drive]
    mov bx, 0x7E00
    int 0x13
    jnc read_successfully

    mov ax, 0 ;Reset Disk
    int 0x13
    dec di
    jnz read_disk
    jmp disk_error

CODE_OFFSET equ gdt_code - gdt_start ;Also a synonym to 0x08
DATA_OFFSET equ gdt_data - gdt_start ;Also a synonym to 0x10

read_successfully:
load_gdt:
    cli
    lgdt [gdt_descriptor]
    mov eax, cr0
    or eax, 1
    mov cr0, eax
    jmp CODE_OFFSET:start_protected

use32
start_protected:
    mov ax, DATA_OFFSET
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax
    mov esp, 0x90000

    mov edi, 0xB8000
    mov al, '?'
    mov ah, 0x0F
    mov [edi], ax

    jmp 0x08:0x7E00

align 16
gdt_start:
gdt_null:
    dd 0x0
    dd 0x0
gdt_code:
    dw 0xFFFF
    dw 0
    db 0
    db 10011011b ;Changed "Accessed" bit to 1 to avoid #GP exception
    db 11001111b
    db 0
gdt_data:
    dw 0xFFFF
    dw 0
    db 0
    db 10010011b ;Changed "Accessed" bit to 1 to avoid #GP exception
    db 11001111b
    db 0
gdt_end:
gdt_descriptor:
    dw gdt_end - gdt_start - 1
    dd gdt_start

disk_error:
    mov ah, 0x0E
    mov al, '!'
    mov bh, 0
    int 0x10

    cli
    hlt
    jmp $-2

boot_drive db 0x00

times 510-($-$$) db 0
dw 0xAA55
    

_________________
Developing a new Operating System...
Post 19 May 2026, 21:09
View user's profile Send private message Visit poster's website Reply with quote
bzt



Joined: 09 Nov 2018
Posts: 109
bzt 25 Jun 2026, 12:56
Here's my feedback, I hope you find it useful.

1. the first jump MUST BE a short jump, otherwise some BIOSes won't boot your boot sector
2. enable_a20: is usually ok, but won't work on some older models which need retriggering, for compatibility it's better to just use BIOS here too (AX=2401h/INT15h)
3. read_disk: you should use AH=42h/INT13h because modern devices (disks, SSDs, USB sticks, etc.) won't work with CHS, only with LBA
4. gdt_null / gdt_descriptor: that's not a descriptor, but value, and you can put the latter in the former (saves precious space)
5. you should disable PIC and NMI before switching CPU modes (according to the spec, omitting this unlikely would cause trouble but it could. Better to be safe than sorry)

Here's an example in fasm (and source). This is extremely minimal, so should be easy to read and use as a skeleton. It also sets up long mode and parses PE/COFF, ignore those parts. You're basically interested in lines 47 to 106. This code is verified and reported to work on all virtual machines and on real hardware too.

Another example boot sector written in fasm that you can use as a skeleton. It's a bit more complex as it loads the kernel file from Minix3 file system, and also supports long mode kernels, ignore those parts.
Post 25 Jun 2026, 12:56
View user's profile Send private message Reply with quote
Tomasz Grysztar



Joined: 16 Jun 2003
Posts: 8556
Location: Kraków, Poland
Tomasz Grysztar 16 Aug 2026, 08:45
bzt wrote:
1. the first jump MUST BE a short jump, otherwise some BIOSes won't boot your boot sector
Such assumption could be justified in context of a boot sector for a FAT partition, because there a jump is necessary to skip over the FAT header structures. But it would be a wrong assumption for an MBR (which is not tied to a file system). All the non-code structures of MBR (including the standard signature) come at the end of the sector and there was no need to skip over anything in the beginning. With my old boot manager I included a source code for standard MBR from the era (I believe it was initialized this way by MS-DOS 6.22) and you can check that there is no JMP there. If any BIOS did not work with MS-DOS, I would certainly consider it a bug in the BIOS, not the other way around. Wink

Another case is booting from floppy, and you could think that this would be like a boot sector for a FAT partition, because floppies normally were formatted as such. But floppies could also be formatted in non-standard ways, and I never had any trouble booting a non-FAT floppies on machines from the '90s. This is how I tested TetrOS on such machines. So if any BIOS made assumptions about a floppy boot sector other than 0AA55h signature, I would also consider it a bad BIOS.
Post 16 Aug 2026, 08:45
View user's profile Send private message Visit poster's website Reply with quote
revolution
When all else fails, read the source


Joined: 24 Aug 2004
Posts: 21079
Location: In your JS exploiting you and your system
revolution 16 Aug 2026, 09:49
Bad BIOS, yes indeed, they exist. But it doesn't change the problem space. Some BIOSes are bad, and there is very little any normal person can do about it. They will just shrug and say the program is broken.

Unless space is too tight to fit in the JMP, I would just put it in anyway. Where's the harm? The extra 1 nanosecond of execution time is inconsequential.
Post 16 Aug 2026, 09:49
View user's profile Send private message Visit poster's website Reply with quote
Tomasz Grysztar



Joined: 16 Jun 2003
Posts: 8556
Location: Kraków, Poland
Tomasz Grysztar 16 Aug 2026, 10:02
revolution wrote:
Unless space is too tight to fit in the JMP, I would just put it in anyway. Where's the harm? The extra 1 nanosecond of execution time is inconsequential.
In our size competitions these two bytes can make a huge difference. And even outside of that niche, a time to care about working around a specific bug is once it actually becomes a problem. I have yet to see a machine that would refuse to boot an MBR initialized by MS-DOS. And if someone would consider MS-DOS to be at fault in such case, I would see no problem for my program to be considered the same, it would be in a good company.
Post 16 Aug 2026, 10:02
View user's profile Send private message Visit poster's website Reply with quote
macomics



Joined: 26 Jan 2021
Posts: 1235
Location: Russia
macomics 16 Aug 2026, 10:22
If the BIOS loaded the MBR before the SBR and such a warning appears, then the fault may be this MBR, not the BIOS. It's just that the MBR implements incorrect checks for the SBR. This is usually eliminated by fully formatting the disk with the creation of a new partition table. And the BIOS has nothing to do with it.
Post 16 Aug 2026, 10:22
View user's profile Send private message Reply with quote
bzt



Joined: 09 Nov 2018
Posts: 109
bzt 18 Aug 2026, 16:32
Tomasz Grysztar wrote:
bzt wrote:
1. the first jump MUST BE a short jump, otherwise some BIOSes won't boot your boot sector
Such assumption could be justified in context of a boot sector for a FAT partition, because there a jump is necessary to skip over the FAT header structures. But it would be a wrong assumption for an MBR (which is not tied to a file system).
You got it backwards: the jump check was first, the MBR come afterwards.

Originally the storage was small, there was no point in partitioning, so the original BIOS was designed to load FAT boot sectors only (which always had that jump, not just the 0AA55h magic). This was the same with floppies and with the early XT disks as well.

The MBR come many years later when storage capacity skyrocketed (in the AT era that followed XT), and it was a painful ugly hack to allow booting from partitioned disks without the need to change the already existing IBM PC BIOS (they couldn't care about all possible clones, the MBR was developed with the original IBM PC BIOS in mind).

Since many IBM PC clones were just rough copies of the original IBM PC, their BIOS developers simply did not know how to do a proper bootable check and they couldn't possibly foresee MBR chainloading, hence the buggy BIOSes with the jump check.

Tomasz Grysztar wrote:
If any BIOS did not work with MS-DOS, I would certainly consider it a bug in the BIOS, not the other way around. Wink
The thing is, all MS-DOS boot sectors have always been started with such a jump, so it worked even on the buggy BIOSes. (Yes, this is a bug in BIOS.)

I had such buggy machines in real life, one was a Compaq and the other one an Olivetti, both machines were famous to be just "somewhat" IBM PC compatible. Those refused to boot without the jump instruction for sure.

As far as I know, the most common BIOS rewrites from the AT era (AMI BIOS and Award BIOS), had no such check issues, because these were created after MBR already become a widespread thing. But this doesn't mean there are no buggy BIOSes out there.
Post 18 Aug 2026, 16:32
View user's profile Send private message Reply with quote
Tomasz Grysztar



Joined: 16 Jun 2003
Posts: 8556
Location: Kraków, Poland
Tomasz Grysztar 18 Aug 2026, 17:19
bzt wrote:
Tomasz Grysztar wrote:
If any BIOS did not work with MS-DOS, I would certainly consider it a bug in the BIOS, not the other way around. Wink
The thing is, all MS-DOS boot sectors have always been started with such a jump, so it worked even on the buggy BIOSes. (Yes, this is a bug in BIOS.)
The boot sector of FAT systems obviously needed to start with a jump, but I was talking about MBR, not a volume boot record. And the standard MBR initialized by MS-DOS did not start with a jump (see also: STANDARD.ASM in my boot manager package).

Your explanation clarifies a lot, though. I did not even consider pre-MBR systems. Since this thread is about 32-bit protected mode OSes, were are firmly in the MBR era.

BTW, why do you consider MBR "painful and ugly"? In my eyes it has always been a simple and clean design.

Interestingly, a FAT boot record has three bytes reserved for the initial jump. Since the jump has always been short, the third byte was filled with 90h (NOP). But it seems the intention from the beginning was to have space for a 3-byte near jump, just in case it was ever needed. No place for a direct far jump, though.
Post 18 Aug 2026, 17:19
View user's profile Send private message Visit poster's website Reply with quote
revolution
When all else fails, read the source


Joined: 24 Aug 2004
Posts: 21079
Location: In your JS exploiting you and your system
revolution 19 Aug 2026, 02:07
Tomasz Grysztar wrote:
Interestingly, a FAT boot record has three bytes reserved for the initial jump. Since the jump has always been short, the third byte was filled with 90h (NOP). But it seems the intention from the beginning was to have space for a 3-byte near jump, just in case it was ever needed. No place for a direct far jump, though.
The reason is because early assemblers were very dumb.

Pass 1: See a forward jmp, it has no idea how far it is so the allocate the maximum three bytes in case all are needed.

Pass 2: Compute the jmp distance as < 128 so it only needs two bytes. But there are no more passes to re-assemble and adjust all the addresses, so the address will never change. the third byte is now redundant, might as well make it nop.
Post 19 Aug 2026, 02:07
View user's profile Send private message Visit poster's website Reply with quote
bzt



Joined: 09 Nov 2018
Posts: 109
bzt 19 Aug 2026, 22:32
Tomasz Grysztar wrote:
Your explanation clarifies a lot, though.
You're welcome!

Tomasz Grysztar wrote:
BTW, why do you consider MBR "painful and ugly"? In my eyes it has always been a simple and clean design.
Because they have kept the 1 sector limit, which resulted in some bad design decisions. I mean 446 bytes is not much, 4 partitions either, and 1 byte system identifier turned out to be painfully insufficient. Then come "logical extended partitions" to try to solve the limitations, another headache.

They could have used more sectors for the partitioning table and the MBR code safely (even if the first sector would have to load the rest). Should they've used 16 sectors for example, 8192 bytes would be more than plenty, and it still wouldn't cross the first track therefore would work equally no matter the disk geometry (typical AT hard disks at that time had at least 16 sectors on their first cylinder, see for example here). I mean just like how GPT done it (yeah, UEFI is complicated, but GPT in itself isn't, just a simple table on multiple continuous sectors, leaving the first sector entirely for the code).
Post 19 Aug 2026, 22:32
View user's profile Send private message Reply with quote
Tomasz Grysztar



Joined: 16 Jun 2003
Posts: 8556
Location: Kraków, Poland
Tomasz Grysztar 20 Aug 2026, 08:18
revolution wrote:
The reason is because early assemblers were very dumb.
Yes, I know why old assemblers generated NOP after a short jump, but having the system label start at offset +3 seems like a deliberate choice. But maybe you are right and they were just looking at what their assembler was able to generate, and extracted the field offsets from a FAT boot record template made by the assembler.
bzt wrote:
They could have used more sectors for the partitioning table and the MBR code safely (even if the first sector would have to load the rest). Should they've used 16 sectors for example, 8192 bytes would be more than plenty, and it still wouldn't cross the first track therefore would work equally no matter the disk geometry (typical AT hard disks at that time had at least 16 sectors on their first cylinder, see for example here). I mean just like how GPT done it (yeah, UEFI is complicated, but GPT in itself isn't, just a simple table on multiple continuous sectors, leaving the first sector entirely for the code).
I'm willing to be more forgiving here. Multiple sectors would take more time to access, the handling code would be more complex, this all would be a penalty for old systems just to accommodate potential needs of the future ones. After all, we got GPT finally, and it could be designed after gathering enough experience to know what is really necessary. Trying to predict all future needs from the beginning would likely result in many misses.

Should GPT have come a bit sooner than it did? Maybe. Protective MBR is a natural extendability pattern, it could have allowed to move to new structures even back then.
Post 20 Aug 2026, 08:18
View user's profile Send private message Visit poster's website Reply with quote
bzt



Joined: 09 Nov 2018
Posts: 109
bzt 20 Aug 2026, 11:09
Tomasz Grysztar wrote:
Multiple sectors would take more time to access, the handling code would be more complex, this all would be a penalty for old systems just to accommodate potential needs of the future ones.
Yes, but I'm not talking about future needs, the limitations were already too strict at the time. Also it wasn't the first, other solutions were not unheard of when MBR was created; Solaris for example was loading the first 64k of the disk, and BSD slices were lot more capable back then. Even the Amiga RDB - which is widely considered a poor design - was a lot more flexible back then. It could be placed anywhere in the first 16 sectors, leaving as much space for the boot code as needed and did not have the "4 partitions only" limitation, and had a few ASCII bytes for identification, not just one byte.
Tomasz Grysztar wrote:
Trying to predict all future needs from the beginning would likely result in many misses.
What I'm trying to say is, the aforementioned solutions all predate MBR so they fulfill needs that already existed at the time. I obviously don't expect the MBR to solve all possible future needs, but just relaxing the limitations a bit would have made a huge difference.

At a bare minimum, they should have used 2 sectors: 512 bytes for the code, 512 for the table. Loading one more sector from the MBR code isn't complex (the sector load routine was there anyway to load the VBR) nor a real overhead on the boot time. It could use 32 bytes per partition (double the size of MBR's) and could store 16 partitions easily (no need for logical partitions).
Post 20 Aug 2026, 11:09
View user's profile Send private message Reply with quote
Tomasz Grysztar



Joined: 16 Jun 2003
Posts: 8556
Location: Kraków, Poland
Tomasz Grysztar 20 Aug 2026, 12:47
bzt wrote:
Also it wasn't the first, other solutions were not unheard of when MBR was created; Solaris for example was loading the first 64k of the disk, and BSD slices were lot more capable back then.
OK, I understand where you're coming from and why we're looking at this so differently. If you consider systems like BSD or Solaris, then we're thinking of completely different worlds. Personally, I really liked the bareness and minimalism of MS-DOS, and in this context MBR appeared elegant and clean. All the sounds of the hard drive were accounted for. But this only made sense for a specific environment of an offline home PC, where these bare structures were more than enough. It was a great platform for learning, when everything was so plain!
Post 20 Aug 2026, 12:47
View user's profile Send private message Visit poster's website Reply with quote
bzt



Joined: 09 Nov 2018
Posts: 109
bzt 20 Aug 2026, 16:30
Tomasz Grysztar wrote:
OK, I understand where you're coming from and why we're looking at this so differently. If you consider systems like BSD or Solaris, then we're thinking of completely different worlds.
Yes, but I have also referred to the Amiga, another popular home computer back then. But yeah, UNIX in general was another league, although I'm sure the devs at MS knew about it.

Tomasz Grysztar wrote:
Personally, I really liked the bareness and minimalism of MS-DOS, and in this context MBR appeared elegant and clean.
Agreed! I have only realized MBR's limitations when I started to use Linux (around '96 or '97), not before. One partition was enough for MS-DOS, but a UNIX clone like Linux usually needs multiple partitions (root, home, usr, var, etc.).

Tomasz Grysztar wrote:
It was a great platform for learning, when everything was so plain!
Totally agree! I loved that I could get full control of the machine. Also MS-DOS was a bug-free system, not like the ones these days (software were smaller and a bug usually meant crashing the whole machine for good, so devs had to and could do proper testing. Back then all release were a rock-solid stable release, ah, good ol' times!).
Post 20 Aug 2026, 16:30
View user's profile Send private message Reply with quote
Display posts from previous:
Post new topic Reply to topic

Jump to:  
Goto page 1, 2  Next

< Last Thread | Next Thread >
Forum Rules:
You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot vote in polls in this forum
You cannot attach files in this forum
You can download files in this forum


Copyright © 1999-2026, Tomasz Grysztar. Also on GitHub, YouTube.

Website powered by rwasa.