2

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
20
  • 3
    As stack grows downwards and there is a free memory space between 0x500 and 0x7BFF I thought it won't cause any conflicts at initial stages Commented Jun 7 at 13:06
  • 2
    @ecm got it right. It has to contain GDT linear address. If you set your code to start at segment 0 (as it is with org 0x7c00) then symbol offset relative to that segment happens to coincide with linear address of the same symbol. At segment 0x7C0 - not so much. Commented Jun 7 at 16:01
  • 1
    @Zero I meant your GDT is being loaded from the wrong address, before you even switch to PM. But also yes, any absolute addresses in protected mode must also be linear because your code/data descriptors are 0-based. That includes start_ptct_mode in that far jump, and anything else you might add later like data references (not near jumps and calls though, as those are pc-relative). This idea of offsetting labels - its kind of error prone (and nasm sucks at helping with that; I couldn't make either multisection nor org work properly). Commented Jun 8 at 4:15
  • 2
    No, lgdt [gdt_descriptor] is fine because it uses the ds to load the GDTR from the memory operand. But instead of dd gtd_start you need dd gtd_start + 7C00h As the page at pushbx.org/ecm/doc/insref.htm#insLGDT says the GDTR.base (from the dd in your source text) is a linear address so it needs the + 7C00h if you org 0. The memory operand to lgdt uses ds for a segmented memory address. Commented Jun 8 at 9:38
  • 1
    @ecm you were right! it makes perfect sense. I edited the code and it worked! thank you Commented Jun 8 at 11:54

1 Answer 1

4

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

  • Write the equates early on so the reader of your code doesn't have to go look for them.
  • Setup SS:SP for the real mode code too. The x86 architecture gives special attention to the mov to SS / pop to SS instructions, so that no interrupt can happen between the change to SS and the very next instruction. Therefore, if we change the companion register SP in the immediately following instruction, we assure ourselves of a flawless modification of SS:SP.
  • Instead of a hard-coded number, use the drive number that BIOS gave you in the DL register.
  • Unless actually needed, omit the far jump to establish a certain CS for your real mode code.
  • Don't jmp to a label that immediately follows the jmp instruction.
  • Do check the CF after invoking selected BIOS (or DOS) functions.
  • When setting up the segment registers in protected mode, do it in a logical order so that you don't forget one, like the ES that is missing from your program.
  • Correct the typo's for Global Descriptor Table: gdt.
  • There's no need for 2 infinite loops at the end.
  • Prefer a comment over a label if you want to document what the code does. NASM has to do a tiny bit more work to manage these excess labels.
  • You can safely re-purpose the gdt's null-descriptor to hold the gdt-descriptor. This shaves off 6 bytes; sometimes that makes all the difference in a bootloader!
[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
Sign up to request clarification or add additional context in comments.

1 Comment

You did comment on keeping mov ss and mov sp together but I think you could list the reason for this in the text more explicitly too. That is, that writing to ss locks out interrupts until after the next instruction and you should make use of this to change ss:sp together in one atomic operation.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.