assemblyx86nasmbootloaderprotected-mode

Segment:Offset instead of org 0x7C00 directive


Recently I tried to implement the bootloader from scratch on x86 assembly using NASM. I am aware that the BIOS loads the bootsector to address 0x7C00 and for that reason all the offsets and memory references should be calculated relative to that address. NASM provides a special directive called ORG SOME_ADDRESS without which all offsets would be calculated relative to zero. However It is possible to achieve the similar effect using segment:offset combination. Instead of using org directive, I choose to go with second approach:

As you can see from code block below I substituted the org 0x7C00 with _init block. There are also START_LETTER and PROTECTED_LETTER to indicate that segment:offset is calculated correctly and new code segment is loaded aswell.

[bits 16]


_init:
    mov ax, 0x07C0
    mov ds, ax
    jmp 0x07C0:_main

_main:
    mov ah, 0x0e
    mov al, [START_LETTER]
    int 0x10

load_kernel:
    mov ah, 0x02    ; BIOS function to read sectors
    mov al, 0x0F    ; Number of sectors to read 
    mov ch, 0x00    ; Cylinder 0
    mov cl, 0x02    ; Starting Sector 2
    mov dh, 0x00    ; Head number
    mov dl, 0x80    ; Hard disk drive
    mov bx, 0
    mov es, bx
    mov bx, KERNEL_ADDR
    int 0x13
enable_a20:
    in al, 0x92
    or al, 2
    out 0x92, al
    jmp enable_prtc_mode

enable_prtc_mode:
    mov ah, 0x0e
    mov al, [PROTECTED_LETTER]
    int 0x10

    cli
    lgdt [gtd_descriptor]
    mov eax, cr0
    or eax, 1
    mov cr0, eax
    jmp 0x08:start_ptct_mode

[bits 32]
start_ptct_mode:
    mov ax, 0x10
    mov ds, ax
    mov ss, ax
    mov fs, ax
    mov gs, ax
    mov esp, STACK_ADDR
    mov ebp, esp

begin:
    mov al, 'A'
    mov ah, 0x0f
    mov [0xb8000], ax
    jmp $  

loop:
    jmp loop




KERNEL_ADDR equ 0x7E00
STACK_ADDR equ 0x7C00

gtd_start:
gtd_null:
    dd 0x00000000
    dd 0x00000000
gtd_code:
    dw 0xFFFF
    dw 0x0000
    db 0x00
    db 10011011b
    db 11001111b 
    db 0x00
gtd_data:
    dw 0xFFFF
    dw 0x0000
    db 0x00
    db 10010010b
    db 11001111b
    db 0x00
gtd_end:

gtd_descriptor:
    dw gtd_end - gtd_start - 1
    dd gtd_start

START_LETTER: db 'F'
PROTECTED_LETTER: db 'P'

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


and then compiled with:

nasm -f bin boot.asm -o boot.bin
qemu-system-i386 -hda ./boot.bin

Everything worked fine untill it was time to make far jump to load code selector onto segment register (jmp 0x08:start_ptct_mode). The qemu crashes fails to properly enter the protected mode. However if I remove instructions under _init block and use [org 0x7C00] directive instead everything works as expected:

[bits 16]
[org 0x7C00]

_main:
    mov ah, 0x0e
    mov al, [START_LETTER]
    int 0x10

load_kernel:
    mov ah, 0x02    ; BIOS function to read sectors
    mov al, 0x0F    ; Number of sectors to read 
    mov ch, 0x00    ; Cylinder 0
    mov cl, 0x02    ; Starting Sector 2
    mov dh, 0x00    ; Head number
    mov dl, 0x80    ; Hard disk drive
    mov bx, 0
    mov es, bx
    mov bx, KERNEL_ADDR
    int 0x13
enable_a20:
    in al, 0x92
    or al, 2
    out 0x92, al
    jmp enable_prtc_mode

enable_prtc_mode:
    mov ah, 0x0e
    mov al, [PROTECTED_LETTER]
    int 0x10

    cli
    lgdt [gtd_descriptor]
    mov eax, cr0
    or eax, 1
    mov cr0, eax
    jmp 0x08:start_ptct_mode

[bits 32]
start_ptct_mode:
    mov ax, 0x10
    mov ds, ax
    mov ss, ax
    mov fs, ax
    mov gs, ax
    mov esp, STACK_ADDR
    mov ebp, esp

begin:
    mov al, 'A'
    mov ah, 0x0f
    mov [0xb8000], ax
    jmp $  

loop:
    jmp loop




KERNEL_ADDR equ 0x7E00
STACK_ADDR equ 0x7C00

gtd_start:
gtd_null:
    dd 0x00000000
    dd 0x00000000
gtd_code:
    dw 0xFFFF
    dw 0x0000
    db 0x00
    db 10011011b
    db 11001111b 
    db 0x00
gtd_data:
    dw 0xFFFF
    dw 0x0000
    db 0x00
    db 10010010b
    db 11001111b
    db 0x00
gtd_end:

gtd_descriptor:
    dw gtd_end - gtd_start - 1
    dd gtd_start

START_LETTER: db 'F'
PROTECTED_LETTER: db 'P'

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


Here is disassebmly as well for the case if you need it:

ndisasm -b 16 boot.bin

; WITH _init
00000000  B8C007            mov ax,0x7c0
00000003  8ED8              mov ds,ax
00000005  EA0A00C007        jmp 0x7c0:0xa
0000000A  B40E              mov ah,0xe
0000000C  A08A00            mov al,[0x8a]
0000000F  CD10              int 0x10
00000011  B402              mov ah,0x2
00000013  B003              mov al,0x3
00000015  B500              mov ch,0x0
00000017  B102              mov cl,0x2
00000019  B600              mov dh,0x0
0000001B  B280              mov dl,0x80
0000001D  BB0000            mov bx,0x0
00000020  8EC3              mov es,bx
00000022  BB007E            mov bx,0x7e00
00000025  CD13              int 0x13
00000027  E492              in al,0x92
00000029  0C02              or al,0x2
0000002B  E692              out 0x92,al
0000002D  EB00              jmp short 0x2f
0000002F  B40E              mov ah,0xe
00000031  A08B00            mov al,[0x8b]
00000034  CD10              int 0x10
00000036  FA                cli
00000037  0F01168400        lgdt [0x84]
0000003C  0F20C0            mov eax,cr0
0000003F  6683C801          or eax,byte +0x1
00000043  0F22C0            mov cr0,eax
00000046  EA4B000800        jmp 0x8:0x4b
0000004B  66B810008ED8      mov eax,0xd88e0010
00000051  8ED0              mov ss,ax
00000053  8EE0              mov fs,ax
00000055  8EE8              mov gs,ax
00000057  BC007C            mov sp,0x7c00
0000005A  0000              add [bx+si],al
0000005C  89E5              mov bp,sp
0000005E  B041              mov al,0x41
00000060  B40F              mov ah,0xf
00000062  66A30080          mov [0x8000],eax
00000066  0B00              or ax,[bx+si]
00000068  EBFE              jmp short 0x68
0000006A  EBFE              jmp short 0x6a
0000006C  0000              add [bx+si],al
0000006E  0000              add [bx+si],al
00000070  0000              add [bx+si],al
00000072  0000              add [bx+si],al
00000074  FF                db 0xff
00000075  FF00              inc word [bx+si]
00000077  0000              add [bx+si],al
00000079  9BCF              wait iret
0000007B  00FF              add bh,bh
0000007D  FF00              inc word [bx+si]
0000007F  0000              add [bx+si],al
00000081  92                xchg ax,dx
00000082  CF                iret
00000083  0017              add [bx],dl
00000085  006C00            add [si+0x0],ch
00000088  0000              add [bx+si],al
0000008A  46                inc si
0000008B  50                push ax
0000008C  0000              add [bx+si],al
0000008E  0000              add [bx+si],al
00000090  0000              add [bx+si],al
00000092  0000              add [bx+si],al
00000094  0000              add [bx+si],al
00000096  0000              add [bx+si],al
00000098  0000              add [bx+si],al
0000009A  0000              add [bx+si],al
0000009C  0000              add [bx+si],al
...
000001F6  0000              add [bx+si],al
000001F8  0000              add [bx+si],al
000001FA  0000              add [bx+si],al
000001FC  0000              add [bx+si],al
000001FE  55                push bp
000001FF  AA                stosb


; WITH ORG
00000000  B40E              mov ah,0xe
00000002  A0807C            mov al,[0x7c80]
00000005  CD10              int 0x10
00000007  B402              mov ah,0x2
00000009  B003              mov al,0x3
0000000B  B500              mov ch,0x0
0000000D  B102              mov cl,0x2
0000000F  B600              mov dh,0x0
00000011  B280              mov dl,0x80
00000013  BB0000            mov bx,0x0
00000016  8EC3              mov es,bx
00000018  BB007E            mov bx,0x7e00
0000001B  CD13              int 0x13
0000001D  E492              in al,0x92
0000001F  0C02              or al,0x2
00000021  E692              out 0x92,al
00000023  EB00              jmp short 0x25
00000025  B40E              mov ah,0xe
00000027  A0817C            mov al,[0x7c81]
0000002A  CD10              int 0x10
0000002C  FA                cli
0000002D  0F01167A7C        lgdt [0x7c7a]
00000032  0F20C0            mov eax,cr0
00000035  6683C801          or eax,byte +0x1
00000039  0F22C0            mov cr0,eax
0000003C  EA417C0800        jmp 0x8:0x7c41
00000041  66B810008ED8      mov eax,0xd88e0010
00000047  8ED0              mov ss,ax
00000049  8EE0              mov fs,ax
0000004B  8EE8              mov gs,ax
0000004D  BC007C            mov sp,0x7c00
00000050  0000              add [bx+si],al
00000052  89E5              mov bp,sp
00000054  B041              mov al,0x41
00000056  B40F              mov ah,0xf
00000058  66A30080          mov [0x8000],eax
0000005C  0B00              or ax,[bx+si]
0000005E  EBFE              jmp short 0x5e
00000060  EBFE              jmp short 0x60
00000062  0000              add [bx+si],al
00000064  0000              add [bx+si],al
00000066  0000              add [bx+si],al
00000068  0000              add [bx+si],al
0000006A  FF                db 0xff
0000006B  FF00              inc word [bx+si]
0000006D  0000              add [bx+si],al
0000006F  9BCF              wait iret
00000071  00FF              add bh,bh
00000073  FF00              inc word [bx+si]
00000075  0000              add [bx+si],al
00000077  92                xchg ax,dx
00000078  CF                iret
00000079  0017              add [bx],dl
0000007B  00627C            add [bp+si+0x7c],ah
0000007E  0000              add [bx+si],al
00000080  46                inc si
00000081  50                push ax
00000082  0000              add [bx+si],al
00000084  0000              add [bx+si],al
00000086  0000              add [bx+si],al
00000088  0000              add [bx+si],al
0000008A  0000              add [bx+si],al
0000008C  0000              add [bx+si],al
0000008E  0000              add [bx+si],al
00000090  0000              add [bx+si],al
00000092  0000              add [bx+si],al
...
000001F4  0000              add [bx+si],al
000001F6  0000              add [bx+si],al
000001F8  0000              add [bx+si],al
000001FA  0000              add [bx+si],al
000001FC  0000              add [bx+si],al
000001FE  55                push bp
000001FF  AA                stosb


What did I do wrong? Any help would be appreciated.


UPDATE: Thanks to @ecm and @Andrey Turkin the error was fixed. I leave the updated version of the code below if the someone else would have the similar question:

[bits 16]

_init:
    mov ax, 0x07C0
    mov ds, ax
    jmp 0x07C0:_main

_main:
    mov ah, 0x0e
    mov al, [START_LETTER]
    int 0x10

load_kernel:
    mov ah, 0x02    ; BIOS function to read sectors
    mov al, 0x0F    ; Number of sectors to read 
    mov ch, 0x00    ; Cylinder 0
    mov cl, 0x02    ; Starting Sector 2
    mov dh, 0x00    ; Head number
    mov dl, 0x80    ; Hard disk drive
    mov bx, 0
    mov es, bx
    mov bx, KERNEL_ADDR
    int 0x13
enable_a20:
    in al, 0x92
    or al, 2
    out 0x92, al
    jmp enable_prtc_mode

enable_prtc_mode:
    mov ah, 0x0e
    mov al, [PROTECTED_LETTER]
    int 0x10

    cli
    lgdt [gdt_descriptor]
    mov eax, cr0
    or eax, 1
    mov cr0, eax
    jmp 0x08:start_ptct_mode + 0x7C00

[bits 32]
start_ptct_mode:
    mov ax, 0x10
    mov ds, ax
    mov ss, ax
    mov fs, ax
    mov gs, ax
    mov esp, STACK_ADDR
    mov ebp, esp

begin:
    mov al, 'A'
    mov ah, 0x0f
    mov [0xb8000], ax
    jmp $  

loop:
    jmp loop




KERNEL_ADDR equ 0x7E00
STACK_ADDR equ 0x7C00

gdt_start:
gdt_null:
    dd 0x00000000
    dd 0x00000000
gdt_code:
    dw 0xFFFF
    dw 0x0000
    db 0x00
    db 10011011b
    db 11001111b 
    db 0x00
gdt_data:
    dw 0xFFFF
    dw 0x0000
    db 0x00
    db 10010010b
    db 11001111b
    db 0x00
gdt_end:

gdt_descriptor:
    dw gdt_end - gdt_start - 1
    dd gdt_start + 0x7C00

START_LETTER: db 'F'
PROTECTED_LETTER: db 'P'

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

Solution

  • NASM provides a special directive called ORG SOME_ADDRESS without which all offsets would be calculated relative to zero.

    If you don't use an ORG directive then NASM secretly uses the ORG 0 setting.

    However It is possible to achieve the similar effect using segment:offset combination.

    Not related to having an ORG directive or not! The segment:offset combination that you're talking about redundantly sets CS equal to DS. But your bootloader program can work perfectly fine without knowing its CS.

    As you can see from code block below I substituted the org 0x7C00 with _init block.

    This is not a substitution at all! Even in the absence of ORG would you still need to setup at least the DS segment register, and preferably also setup for a valid SS:SP because you have no idea where the stack currently is.

    The essential repair

    jmp 0x08:start_ptct_mode
    

    Without an ORG the value of (the label) start_ptct_mode will be a very small number somewhere in the middle of the range [0,511]. Since the linear base address mentioned in the CODE selector is zero, and our bootloader program is located at linear address 00007C00h, the offset part for this jmp is clearly too small. The correct instruction will be: jmp 0x08:0x7C00 + start_ptct_mode

    dd gtd_start
    

    For exactly the same reason as above, this will have to become: dd 0x7C00 + gtd_start.

    The further improvements

    [bits 16]                     ; No `ORG` eqv `ORG 0`
    
    KERNEL_ADDR equ 0x7E00
    STACK_ADDR  equ 0x7C00
    
        mov  ax, 0x07C0           ; Matches `ORG 0`
        mov  ds, ax
        xor  ax, ax
        mov  es, ax
        mov  ss, ax               ; Keep these two
        mov  sp, STACK_ADDR       ; close together
    
        mov  ah, 0x0E
        mov  al, [START_LETTER]
        int  0x10
    
    ; load_kernel
        mov  ah, 0x02    ; BIOS function to read sectors
        mov  al, 0x0F    ; Number of sectors to read 
        mov  ch, 0x00    ; Cylinder 0
        mov  cl, 0x02    ; Starting Sector 2
        mov  dh, 0x00    ; Head number
        mov  bx, KERNEL_ADDR
        int  0x13
        jc   ???????????????????
    ; enable_a20
        in   al, 0x92
        or   al, 2
        out  0x92, al
    
        mov  ah, 0x0E
        mov  al, [PROTECTED_LETTER]
        int  0x10
    
        cli
        lgdt [gdt_descriptor]
        mov  eax, cr0
        or   eax, 1
        mov  cr0, eax
        jmp  0x08:0x7C00 + start_ptct_mode
    
    [bits 32]
    start_ptct_mode:
        mov  ax, 0x10
        mov  ds, ax
        mov  es, ax
        mov  fs, ax
        mov  gs, ax
        mov  ss, ax          ; Even though CLI disabled interrupts, 
        mov  esp, STACK_ADDR ; it remains a good habit to 1st change 
        mov  ebp, esp        ; SS, then immediately after change ESP
    
    ; begin
        mov  al, 'A'
        mov  ah, 0x0f
        mov  [0x000B8000], ax
    
        cli
        hlt
        jmp  $-2
    
        ALIGN 4
    gdt_start:
    gdt_descriptor:
        dw gdt_end - gdt_start - 1
        dd 0x7C00 + gdt_start
        dw 0
    ; gdt_code
        dw 0xFFFF
        dw 0x0000
        db 0x00
        db 10011011b
        db 11001111b 
        db 0x00
    ; gdt_data
        dw 0xFFFF
        dw 0x0000
        db 0x00
        db 10010010b
        db 11001111b
        db 0x00
    gdt_end:
    
    START_LETTER      db 'F'
    PROTECTED_LETTER  db 'P'
    
    times 510 - ($ - $$) db 0x00
    dw 0xAA55