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
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.
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
.
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.jmp
to a label that immediately follows the jmp
instruction.gdt
.[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