How to execute C code on barebones correctly? [duplicate] - c

I'm trying to make my own custom OS and I need some help with my code.
This is my bootloader.asm:
[ORG 0x7c00]
start:
cli
xor ax, ax
mov ds, ax
mov ss, ax
mov es, ax
mov [BOOT_DRIVE], dl
mov bp, 0x8000
mov sp, bp
mov bx, 0x9000
mov dh, 5
mov dl, [BOOT_DRIVE]
call load_kernel
call enable_A20
call graphics_mode
lgdt [gdtr]
mov eax, cr0
or al, 1
mov cr0, eax
jmp CODE_SEG:init_pm
[bits 32]
init_pm:
mov ax, DATA_SEG
mov ds, ax
mov ss, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ebp, 0x90000
mov esp, ebp
jmp 0x9000
[BITS 16]
graphics_mode:
mov ax, 0013h
int 10h
ret
load_kernel:
; load DH sectors to ES:BX from drive DL
push dx ; Store DX on stack so later we can recall
; how many sectors were request to be read ,
; even if it is altered in the meantime
mov ah , 0x02 ; BIOS read sector function
mov al , dh ; Read DH sectors
mov ch , 0x00 ; Select cylinder 0
mov dh , 0x00 ; Select head 0
mov cl , 0x02 ; Start reading from second sector ( i.e.
; after the boot sector )
int 0x13 ; BIOS interrupt
jc disk_error ; Jump if error ( i.e. carry flag set )
pop dx ; Restore DX from the stack
cmp dh , al ; if AL ( sectors read ) != DH ( sectors expected )
jne disk_error ; display error message
ret
disk_error :
mov bx , ERROR_MSG
call print_string
hlt
[bits 32]
; prints a null - terminated string pointed to by EDX
print_string :
pusha
mov edx , VIDEO_MEMORY ; Set edx to the start of vid mem.
print_string_loop :
mov al , [ ebx ] ; Store the char at EBX in AL
mov ah , WHITE_ON_BLACK ; Store the attributes in AH
cmp al , 0 ; if (al == 0) , at end of string , so
je print_string_done ; jump to done
mov [edx] , ax ; Store char and attributes at current
; character cell.
add ebx , 1 ; Increment EBX to the next char in string.
add edx , 2 ; Move to next character cell in vid mem.
jmp print_string_loop ; loop around to print the next char.
print_string_done :
popa
ret ; Return from the function
[bits 16]
; Variables
ERROR_MSG db "Error!" , 0
BOOT_DRIVE: db 0
VIDEO_MEMORY equ 0xb8000
WHITE_ON_BLACK equ 0x0f
%include "a20.inc"
%include "gdt.inc"
times 510-($-$$) db 0
db 0x55
db 0xAA
I compile it with this:
nasm -f bin -o boot.bin bootloader.asm
This is kernel.c:
call_main(){main();}
void main(){}
I compile it with this:
gcc -ffreestanding -o kernel.bin kernel.c
and then:
cat boot.bin kernel.bin > os.bin
I want to know what I am doing wrong because when I test with QEMU it doesn't work. Can someone give some tips to improve kernel.c so I don't have to use the call_main() function?
When testing I use:
qemu-system-i386 -kernel os.bin
My Other Files
a20.inc:
enable_A20:
call check_a20
cmp ax, 1
je enabled
call a20_bios
call check_a20
cmp ax, 1
je enabled
call a20_keyboard
call check_a20
cmp ax, 1
je enabled
call a20_fast
call check_a20
cmp ax, 1
je enabled
mov bx, [ERROR]
call print_string
enabled:
ret
check_a20:
pushf
push ds
push es
push di
push si
cli
xor ax, ax ; ax = 0
mov es, ax
not ax ; ax = 0xFFFF
mov ds, ax
mov di, 0x0500
mov si, 0x0510
mov al, byte [es:di]
push ax
mov al, byte [ds:si]
push ax
mov byte [es:di], 0x00
mov byte [ds:si], 0xFF
cmp byte [es:di], 0xFF
pop ax
mov byte [ds:si], al
pop ax
mov byte [es:di], al
mov ax, 0
je check_a20__exit
mov ax, 1
check_a20__exit:
pop si
pop di
pop es
pop ds
popf
ret
a20_bios:
mov ax, 0x2401
int 0x15
ret
a20_fast:
in al, 0x92
or al, 2
out 0x92, al
ret
[bits 32]
[section .text]
a20_keyboard:
cli
call a20wait
mov al,0xAD
out 0x64,al
call a20wait
mov al,0xD0
out 0x64,al
call a20wait2
in al,0x60
push eax
call a20wait
mov al,0xD1
out 0x64,al
call a20wait
pop eax
or al,2
out 0x60,al
call a20wait
mov al,0xAE
out 0x64,al
call a20wait
sti
ret
a20wait:
in al,0x64
test al,2
jnz a20wait
ret
a20wait2:
in al,0x64
test al,1
jz a20wait2
ret
gdt.inc:
gdt_start:
dd 0 ; null descriptor--just fill 8 bytes dd 0
gdt_code:
dw 0FFFFh ; limit low
dw 0 ; base low
db 0 ; base middle
db 10011010b ; access
db 11001111b ; granularity
db 0 ; base high
gdt_data:
dw 0FFFFh ; limit low (Same as code)
dw 0 ; base low
db 0 ; base middle
db 10010010b ; access
db 11001111b ; granularity
db 0 ; base high
end_of_gdt:
gdtr:
dw end_of_gdt - gdt_start - 1 ; limit (Size of GDT)
dd gdt_start ; base of GDT
CODE_SEG equ gdt_code - gdt_start
DATA_SEG equ gdt_data - gdt_start

There are a number of issues, but in general your assembly code does work. I have written a StackOverflow answer that has tips for general bootloader development.
Don't Assume the Segment Registers are Set Properly
The original code in your question didn't set the SS stack segment register. Tip #1 I give is:
When the BIOS jumps to your code you can't rely on CS,DS,ES,SS,SP
registers having valid or expected values. They should be set up
appropriately when your bootloader starts.
If you need ES it should be set as well. Although in your code it doesn't appear to be the case (except in the print_string function which I'll discuss later).
Properly Define the GDT
The single largest bug that would have prevented you from getting far into protected mode was that you set up the global descriptor table (GDT) in gdt.inc starting with:
gdt_start:
dd 0 ; null descriptor--just fill 8 bytes dd 0
Each global descriptor needs to be 8 bytes but dd 0 defines just 4 bytes (double word). It should be:
gdt_start:
dd 0 ; null descriptor--just fill 8 bytes
dd 0
It actually appears that the second dd 0 was accidentally added to the end of the comment on the previous line.
When in 16-bit Real Mode Don't Use 32-bit Code
You have written some print_string code but it is 32-bit code:
[bits 32]
; prints a null - terminated string pointed to by EBX
print_string :
pusha
mov edx , VIDEO_MEMORY ; Set edx to the start of vid mem.
print_string_loop :
mov al , [ ebx ] ; Store the char at EBX in AL
mov ah , WHITE_ON_BLACK ; Store the attributes in AH
cmp al , 0 ; if (al == 0) , at end of string , so
je print_string_done ; jump to done
mov [edx] , ax ; Store char and attributes at current
; character cell.
add ebx , 1 ; Increment EBX to the next char in string.
add edx , 2 ; Move to next character cell in vid mem.
jmp print_string_loop ; loop around to print the next char.
print_string_done :
popa
ret ; Return from the function
You call print_string as an error handler in 16-bit code so what you are doing here will likely force a reboot of the computer. You can't use the 32-bit registers and addressing. The code can be made 16-bit with some adjustments:
; prints a null - terminated string pointed to by EBX
print_string :
pusha
push es ;Save ES on stack and restore when we finish
push VIDEO_MEMORY_SEG ;Video mem segment 0xb800
pop es
xor di, di ;Video mem offset (start at 0)
print_string_loop :
mov al , [ bx ] ; Store the char at BX in AL
mov ah , WHITE_ON_BLACK ; Store the attributes in AH
cmp al , 0 ; if (al == 0) , at end of string , so
je print_string_done ; jump to done
mov word [es:di], ax ; Store char and attributes at current
; character cell.
add bx , 1 ; Increment BX to the next char in string.
add di , 2 ; Move to next character cell in vid mem.
jmp print_string_loop ; loop around to print the next char.
print_string_done :
pop es ;Restore ES that was saved on entry
popa
ret ; Return from the function
The primary difference (in 16-bit code) is that we no longer use EAX and EDX 32-bit registers. In order to access the video ram # 0xb8000 we need to use a segment:offset pair that represents the same thing. 0xb8000 can be represented as segment:offset 0xb800:0x0 (Computed as (0xb800<<4)+0x0) = 0xb8000 physical address. We can use this knowledge to store b800 in the ES register and use DI register as the offset to update video memory. We now use:
mov word [es:di], ax
To move a word into video ram.
Assembling and Linking the Kernel and Bootloader
One of the issues you have in building your Kernel is that you don't properly generate a flat binary image that can be loaded into memory directly. Rather than using gcc -ffreestanding -o kernel.bin kernel.c I recommend doing it this way:
gcc -g -m32 -c -ffreestanding -o kernel.o kernel.c -lgcc
ld -melf_i386 -Tlinker.ld -nostdlib --nmagic -o kernel.elf kernel.o
objcopy -O binary kernel.elf kernel.bin
This assembles kernel.c to kernel.o with debugging info (-g). The linker then takes kernel.o (32-bit ELF binary) and produces an ELF executable called kernel.elf (this file will be handy if you want to debug your kernel). We then use objcopy to take the ELF32 executable file kernel.elf and convert it into a flat binary image kernel.bin that can be loaded by the BIOS. A key thing to note is that with -Tlinker.ld option we are asking the LD(linker) to read options from the file linker.ld . This is a simple linker.ld you can use to get started:
OUTPUT_FORMAT(elf32-i386)
ENTRY(main)
SECTIONS
{
. = 0x9000;
.text : { *(.text) }
.data : { *(.data) }
.bss : { *(.bss) *(COMMON) }
}
The thing to note here is that . = 0x9000 is telling the linker that it should produce an executable that will be loaded at memory address 0x9000 . 0x9000 is where you seem to have placed your kernel in your question. The rest of the lines make available the C sections that will need to be included into your kernel to work properly.
I recommend doing something similar when using NASM so rather than doing nasm -f bin -o boot.bin bootloader.asm do it this way:
nasm -g -f elf32 -F dwarf -o boot.o bootloader.asm
ld -melf_i386 -Ttext=0x7c00 -nostdlib --nmagic -o boot.elf boot.o
objcopy -O binary boot.elf boot.bin
This is similar to compiling the C kernel. We don't use a linker script here, but we do tell the linker to produce our code assuming the code (bootloader) will be loaded at 0x7c00 .
For this to work you will need to remove this line from bootloader.asm :
[ORG 0x7c00]
Cleanup The Kernel (kernel.c)
Modify your kernel.c file to be:
/* This code will be placed at the beginning of the object by the linker script */
__asm__ (".pushsection .text.start\r\n" \
"jmp main\r\n" \
".popsection\r\n"
);
/* Place main as the first function defined in kernel.c so
* that it will be at the entry point where our bootloader
* will call. In our case it will be at 0x9000 */
int main(){
/* Do Stuff Here*/
return 0; /* return back to bootloader */
}
In bootloader.asm we should be calling the main function (that will be placed at 0x9000) rather than jumping to it. Instead of:
jmp 0x9000
Change it to:
call 0x9000
cli
loopend: ;Infinite loop when finished
hlt
jmp loopend
The code after the call will be executed when C function main returns. It is a simple loop that will effectively halt the processor and remain that way indefinitely since we have no where to go back to.
Code After Making All Recommended Changes
bootloader.asm:
[bits 16]
global _start
_start:
cli
xor ax, ax
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x8000 ; Stack pointer at SS:SP = 0x0000:0x8000
mov [BOOT_DRIVE], dl; Boot drive passed to us by the BIOS
mov dh, 17 ; Number of sectors (kernel.bin) to read from disk
; 17*512 allows for a kernel.bin up to 8704 bytes
mov bx, 0x9000 ; Load Kernel to ES:BX = 0x0000:0x9000
call load_kernel
call enable_A20
; call graphics_mode ; Uncomment if you want to switch to graphics mode 0x13
lgdt [gdtr]
mov eax, cr0
or al, 1
mov cr0, eax
jmp CODE_SEG:init_pm
graphics_mode:
mov ax, 0013h
int 10h
ret
load_kernel:
; load DH sectors to ES:BX from drive DL
push dx ; Store DX on stack so later we can recall
; how many sectors were request to be read ,
; even if it is altered in the meantime
mov ah , 0x02 ; BIOS read sector function
mov al , dh ; Read DH sectors
mov ch , 0x00 ; Select cylinder 0
mov dh , 0x00 ; Select head 0
mov cl , 0x02 ; Start reading from second sector ( i.e.
; after the boot sector )
int 0x13 ; BIOS interrupt
jc disk_error ; Jump if error ( i.e. carry flag set )
pop dx ; Restore DX from the stack
cmp dh , al ; if AL ( sectors read ) != DH ( sectors expected )
jne disk_error ; display error message
ret
disk_error :
mov bx , ERROR_MSG
call print_string
hlt
; prints a null - terminated string pointed to by EDX
print_string :
pusha
push es ;Save ES on stack and restore when we finish
push VIDEO_MEMORY_SEG ;Video mem segment 0xb800
pop es
xor di, di ;Video mem offset (start at 0)
print_string_loop :
mov al , [ bx ] ; Store the char at BX in AL
mov ah , WHITE_ON_BLACK ; Store the attributes in AH
cmp al , 0 ; if (al == 0) , at end of string , so
je print_string_done ; jump to done
mov word [es:di], ax ; Store char and attributes at current
; character cell.
add bx , 1 ; Increment BX to the next char in string.
add di , 2 ; Move to next character cell in vid mem.
jmp print_string_loop ; loop around to print the next char.
print_string_done :
pop es ;Restore ES that was saved on entry
popa
ret ; Return from the function
%include "a20.inc"
%include "gdt.inc"
[bits 32]
init_pm:
mov ax, DATA_SEG
mov ds, ax
mov ss, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ebp, 0x90000
mov esp, ebp
call 0x9000
cli
loopend: ;Infinite loop when finished
hlt
jmp loopend
[bits 16]
; Variables
ERROR db "A20 Error!" , 0
ERROR_MSG db "Error!" , 0
BOOT_DRIVE: db 0
VIDEO_MEMORY_SEG equ 0xb800
WHITE_ON_BLACK equ 0x0f
times 510-($-$$) db 0
db 0x55
db 0xAA
gdt.inc:
gdt_start:
dd 0 ; null descriptor--just fill 8 bytes
dd 0
gdt_code:
dw 0FFFFh ; limit low
dw 0 ; base low
db 0 ; base middle
db 10011010b ; access
db 11001111b ; granularity
db 0 ; base high
gdt_data:
dw 0FFFFh ; limit low (Same as code)
dw 0 ; base low
db 0 ; base middle
db 10010010b ; access
db 11001111b ; granularity
db 0 ; base high
end_of_gdt:
gdtr:
dw end_of_gdt - gdt_start - 1 ; limit (Size of GDT)
dd gdt_start ; base of GDT
CODE_SEG equ gdt_code - gdt_start
DATA_SEG equ gdt_data - gdt_start
a20.inc:
enable_A20:
call check_a20
cmp ax, 1
je enabled
call a20_bios
call check_a20
cmp ax, 1
je enabled
call a20_keyboard
call check_a20
cmp ax, 1
je enabled
call a20_fast
call check_a20
cmp ax, 1
je enabled
mov bx, [ERROR]
call print_string
enabled:
ret
check_a20:
pushf
push ds
push es
push di
push si
cli
xor ax, ax ; ax = 0
mov es, ax
not ax ; ax = 0xFFFF
mov ds, ax
mov di, 0x0500
mov si, 0x0510
mov al, byte [es:di]
push ax
mov al, byte [ds:si]
push ax
mov byte [es:di], 0x00
mov byte [ds:si], 0xFF
cmp byte [es:di], 0xFF
pop ax
mov byte [ds:si], al
pop ax
mov byte [es:di], al
mov ax, 0
je check_a20__exit
mov ax, 1
check_a20__exit:
pop si
pop di
pop es
pop ds
popf
ret
a20_bios:
mov ax, 0x2401
int 0x15
ret
a20_fast:
in al, 0x92
or al, 2
out 0x92, al
ret
[bits 32]
[section .text]
a20_keyboard:
cli
call a20wait
mov al,0xAD
out 0x64,al
call a20wait
mov al,0xD0
out 0x64,al
call a20wait2
in al,0x60
push eax
call a20wait
mov al,0xD1
out 0x64,al
call a20wait
pop eax
or al,2
out 0x60,al
call a20wait
mov al,0xAE
out 0x64,al
call a20wait
sti
ret
a20wait:
in al,0x64
test al,2
jnz a20wait
ret
a20wait2:
in al,0x64
test al,1
jz a20wait2
ret
kernel.c:
/* This code will be placed at the beginning of the object by the linker script */
__asm__ (".pushsection .text.start\r\n" \
"jmp main\r\n" \
".popsection\r\n"
);
/* Place main as the first function defined in kernel.c so
* that it will be at the entry point where our bootloader
* will call. In our case it will be at 0x9000 */
int main(){
/* Do Stuff Here*/
return 0; /* return back to bootloader */
}
linker.ld
OUTPUT_FORMAT(elf32-i386)
ENTRY(main)
SECTIONS
{
. = 0x9000;
.text : { *(.text.start) *(.text) }
.data : { *(.data) }
.bss : { *(.bss) *(COMMON) }
}
Create Disk Image Using DD / Debugging with QEMU
If you use the files above, and produce the required bootloader and kernel files using these commands (as mentioned previously)
nasm -g -f elf32 -F dwarf -o boot.o bootloader.asm
ld -melf_i386 -Ttext=0x7c00 -nostdlib --nmagic -o boot.elf boot.o
objcopy -O binary boot.elf boot.bin
gcc -g -m32 -c -ffreestanding -o kernel.o kernel.c -lgcc
ld -melf_i386 -Tlinker.ld -nostdlib --nmagic -o kernel.elf kernel.o
objcopy -O binary kernel.elf kernel.bin
You can produce a disk image (in this case we'll make it the size of a floppy) with these commands:
dd if=/dev/zero of=disk.img bs=512 count=2880
dd if=boot.bin of=disk.img bs=512 conv=notrunc
dd if=kernel.bin of=disk.img bs=512 seek=1 conv=notrunc
This creates a zero filled disk image of size 512*2880 bytes (The size of a 1.44 megabyte floppy). dd if=boot.bin of=disk.img bs=512 conv=notrunc writes boot.bin to the first sector of the file without truncating the disk image. dd if=kernel.bin of=disk.img bs=512 seek=1 conv=notrunc places kernel.bin into the disk image starting at the second sector. The seek=1 skips over the first block (bs=512) before writing.
If you wish to run your kernel you can launch it as floppy drive A: (-fda) in QEMU like this:
qemu-system-i386 -fda disk.img
You can also debug your 32-bit kernel using QEMU and the GNU Debugger (GDB) with the debug information we generated when compiling/assembling the code with the instructions above.
qemu-system-i386 -fda disk.img -S -s &
gdb kernel.elf \
-ex 'target remote localhost:1234' \
-ex 'layout src' \
-ex 'layout reg' \
-ex 'break main' \
-ex 'continue'
This example launches QEMU with the remote debugger and emulating a floppy disk using the file disk.img(that we created with DD). GDB launches using kernel.elf (a file we generated with debug info), then connects to QEMU, and sets a breakpoint at function main() in the C code. When the debugger finally is ready you'll be prompted to press <return> to continue. With any luck you should be viewing function main in the debugger.

Related

Call extern C function from assembly [duplicate]

I'm trying to make my own custom OS and I need some help with my code.
This is my bootloader.asm:
[ORG 0x7c00]
start:
cli
xor ax, ax
mov ds, ax
mov ss, ax
mov es, ax
mov [BOOT_DRIVE], dl
mov bp, 0x8000
mov sp, bp
mov bx, 0x9000
mov dh, 5
mov dl, [BOOT_DRIVE]
call load_kernel
call enable_A20
call graphics_mode
lgdt [gdtr]
mov eax, cr0
or al, 1
mov cr0, eax
jmp CODE_SEG:init_pm
[bits 32]
init_pm:
mov ax, DATA_SEG
mov ds, ax
mov ss, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ebp, 0x90000
mov esp, ebp
jmp 0x9000
[BITS 16]
graphics_mode:
mov ax, 0013h
int 10h
ret
load_kernel:
; load DH sectors to ES:BX from drive DL
push dx ; Store DX on stack so later we can recall
; how many sectors were request to be read ,
; even if it is altered in the meantime
mov ah , 0x02 ; BIOS read sector function
mov al , dh ; Read DH sectors
mov ch , 0x00 ; Select cylinder 0
mov dh , 0x00 ; Select head 0
mov cl , 0x02 ; Start reading from second sector ( i.e.
; after the boot sector )
int 0x13 ; BIOS interrupt
jc disk_error ; Jump if error ( i.e. carry flag set )
pop dx ; Restore DX from the stack
cmp dh , al ; if AL ( sectors read ) != DH ( sectors expected )
jne disk_error ; display error message
ret
disk_error :
mov bx , ERROR_MSG
call print_string
hlt
[bits 32]
; prints a null - terminated string pointed to by EDX
print_string :
pusha
mov edx , VIDEO_MEMORY ; Set edx to the start of vid mem.
print_string_loop :
mov al , [ ebx ] ; Store the char at EBX in AL
mov ah , WHITE_ON_BLACK ; Store the attributes in AH
cmp al , 0 ; if (al == 0) , at end of string , so
je print_string_done ; jump to done
mov [edx] , ax ; Store char and attributes at current
; character cell.
add ebx , 1 ; Increment EBX to the next char in string.
add edx , 2 ; Move to next character cell in vid mem.
jmp print_string_loop ; loop around to print the next char.
print_string_done :
popa
ret ; Return from the function
[bits 16]
; Variables
ERROR_MSG db "Error!" , 0
BOOT_DRIVE: db 0
VIDEO_MEMORY equ 0xb8000
WHITE_ON_BLACK equ 0x0f
%include "a20.inc"
%include "gdt.inc"
times 510-($-$$) db 0
db 0x55
db 0xAA
I compile it with this:
nasm -f bin -o boot.bin bootloader.asm
This is kernel.c:
call_main(){main();}
void main(){}
I compile it with this:
gcc -ffreestanding -o kernel.bin kernel.c
and then:
cat boot.bin kernel.bin > os.bin
I want to know what I am doing wrong because when I test with QEMU it doesn't work. Can someone give some tips to improve kernel.c so I don't have to use the call_main() function?
When testing I use:
qemu-system-i386 -kernel os.bin
My Other Files
a20.inc:
enable_A20:
call check_a20
cmp ax, 1
je enabled
call a20_bios
call check_a20
cmp ax, 1
je enabled
call a20_keyboard
call check_a20
cmp ax, 1
je enabled
call a20_fast
call check_a20
cmp ax, 1
je enabled
mov bx, [ERROR]
call print_string
enabled:
ret
check_a20:
pushf
push ds
push es
push di
push si
cli
xor ax, ax ; ax = 0
mov es, ax
not ax ; ax = 0xFFFF
mov ds, ax
mov di, 0x0500
mov si, 0x0510
mov al, byte [es:di]
push ax
mov al, byte [ds:si]
push ax
mov byte [es:di], 0x00
mov byte [ds:si], 0xFF
cmp byte [es:di], 0xFF
pop ax
mov byte [ds:si], al
pop ax
mov byte [es:di], al
mov ax, 0
je check_a20__exit
mov ax, 1
check_a20__exit:
pop si
pop di
pop es
pop ds
popf
ret
a20_bios:
mov ax, 0x2401
int 0x15
ret
a20_fast:
in al, 0x92
or al, 2
out 0x92, al
ret
[bits 32]
[section .text]
a20_keyboard:
cli
call a20wait
mov al,0xAD
out 0x64,al
call a20wait
mov al,0xD0
out 0x64,al
call a20wait2
in al,0x60
push eax
call a20wait
mov al,0xD1
out 0x64,al
call a20wait
pop eax
or al,2
out 0x60,al
call a20wait
mov al,0xAE
out 0x64,al
call a20wait
sti
ret
a20wait:
in al,0x64
test al,2
jnz a20wait
ret
a20wait2:
in al,0x64
test al,1
jz a20wait2
ret
gdt.inc:
gdt_start:
dd 0 ; null descriptor--just fill 8 bytes dd 0
gdt_code:
dw 0FFFFh ; limit low
dw 0 ; base low
db 0 ; base middle
db 10011010b ; access
db 11001111b ; granularity
db 0 ; base high
gdt_data:
dw 0FFFFh ; limit low (Same as code)
dw 0 ; base low
db 0 ; base middle
db 10010010b ; access
db 11001111b ; granularity
db 0 ; base high
end_of_gdt:
gdtr:
dw end_of_gdt - gdt_start - 1 ; limit (Size of GDT)
dd gdt_start ; base of GDT
CODE_SEG equ gdt_code - gdt_start
DATA_SEG equ gdt_data - gdt_start
There are a number of issues, but in general your assembly code does work. I have written a StackOverflow answer that has tips for general bootloader development.
Don't Assume the Segment Registers are Set Properly
The original code in your question didn't set the SS stack segment register. Tip #1 I give is:
When the BIOS jumps to your code you can't rely on CS,DS,ES,SS,SP
registers having valid or expected values. They should be set up
appropriately when your bootloader starts.
If you need ES it should be set as well. Although in your code it doesn't appear to be the case (except in the print_string function which I'll discuss later).
Properly Define the GDT
The single largest bug that would have prevented you from getting far into protected mode was that you set up the global descriptor table (GDT) in gdt.inc starting with:
gdt_start:
dd 0 ; null descriptor--just fill 8 bytes dd 0
Each global descriptor needs to be 8 bytes but dd 0 defines just 4 bytes (double word). It should be:
gdt_start:
dd 0 ; null descriptor--just fill 8 bytes
dd 0
It actually appears that the second dd 0 was accidentally added to the end of the comment on the previous line.
When in 16-bit Real Mode Don't Use 32-bit Code
You have written some print_string code but it is 32-bit code:
[bits 32]
; prints a null - terminated string pointed to by EBX
print_string :
pusha
mov edx , VIDEO_MEMORY ; Set edx to the start of vid mem.
print_string_loop :
mov al , [ ebx ] ; Store the char at EBX in AL
mov ah , WHITE_ON_BLACK ; Store the attributes in AH
cmp al , 0 ; if (al == 0) , at end of string , so
je print_string_done ; jump to done
mov [edx] , ax ; Store char and attributes at current
; character cell.
add ebx , 1 ; Increment EBX to the next char in string.
add edx , 2 ; Move to next character cell in vid mem.
jmp print_string_loop ; loop around to print the next char.
print_string_done :
popa
ret ; Return from the function
You call print_string as an error handler in 16-bit code so what you are doing here will likely force a reboot of the computer. You can't use the 32-bit registers and addressing. The code can be made 16-bit with some adjustments:
; prints a null - terminated string pointed to by EBX
print_string :
pusha
push es ;Save ES on stack and restore when we finish
push VIDEO_MEMORY_SEG ;Video mem segment 0xb800
pop es
xor di, di ;Video mem offset (start at 0)
print_string_loop :
mov al , [ bx ] ; Store the char at BX in AL
mov ah , WHITE_ON_BLACK ; Store the attributes in AH
cmp al , 0 ; if (al == 0) , at end of string , so
je print_string_done ; jump to done
mov word [es:di], ax ; Store char and attributes at current
; character cell.
add bx , 1 ; Increment BX to the next char in string.
add di , 2 ; Move to next character cell in vid mem.
jmp print_string_loop ; loop around to print the next char.
print_string_done :
pop es ;Restore ES that was saved on entry
popa
ret ; Return from the function
The primary difference (in 16-bit code) is that we no longer use EAX and EDX 32-bit registers. In order to access the video ram # 0xb8000 we need to use a segment:offset pair that represents the same thing. 0xb8000 can be represented as segment:offset 0xb800:0x0 (Computed as (0xb800<<4)+0x0) = 0xb8000 physical address. We can use this knowledge to store b800 in the ES register and use DI register as the offset to update video memory. We now use:
mov word [es:di], ax
To move a word into video ram.
Assembling and Linking the Kernel and Bootloader
One of the issues you have in building your Kernel is that you don't properly generate a flat binary image that can be loaded into memory directly. Rather than using gcc -ffreestanding -o kernel.bin kernel.c I recommend doing it this way:
gcc -g -m32 -c -ffreestanding -o kernel.o kernel.c -lgcc
ld -melf_i386 -Tlinker.ld -nostdlib --nmagic -o kernel.elf kernel.o
objcopy -O binary kernel.elf kernel.bin
This assembles kernel.c to kernel.o with debugging info (-g). The linker then takes kernel.o (32-bit ELF binary) and produces an ELF executable called kernel.elf (this file will be handy if you want to debug your kernel). We then use objcopy to take the ELF32 executable file kernel.elf and convert it into a flat binary image kernel.bin that can be loaded by the BIOS. A key thing to note is that with -Tlinker.ld option we are asking the LD(linker) to read options from the file linker.ld . This is a simple linker.ld you can use to get started:
OUTPUT_FORMAT(elf32-i386)
ENTRY(main)
SECTIONS
{
. = 0x9000;
.text : { *(.text) }
.data : { *(.data) }
.bss : { *(.bss) *(COMMON) }
}
The thing to note here is that . = 0x9000 is telling the linker that it should produce an executable that will be loaded at memory address 0x9000 . 0x9000 is where you seem to have placed your kernel in your question. The rest of the lines make available the C sections that will need to be included into your kernel to work properly.
I recommend doing something similar when using NASM so rather than doing nasm -f bin -o boot.bin bootloader.asm do it this way:
nasm -g -f elf32 -F dwarf -o boot.o bootloader.asm
ld -melf_i386 -Ttext=0x7c00 -nostdlib --nmagic -o boot.elf boot.o
objcopy -O binary boot.elf boot.bin
This is similar to compiling the C kernel. We don't use a linker script here, but we do tell the linker to produce our code assuming the code (bootloader) will be loaded at 0x7c00 .
For this to work you will need to remove this line from bootloader.asm :
[ORG 0x7c00]
Cleanup The Kernel (kernel.c)
Modify your kernel.c file to be:
/* This code will be placed at the beginning of the object by the linker script */
__asm__ (".pushsection .text.start\r\n" \
"jmp main\r\n" \
".popsection\r\n"
);
/* Place main as the first function defined in kernel.c so
* that it will be at the entry point where our bootloader
* will call. In our case it will be at 0x9000 */
int main(){
/* Do Stuff Here*/
return 0; /* return back to bootloader */
}
In bootloader.asm we should be calling the main function (that will be placed at 0x9000) rather than jumping to it. Instead of:
jmp 0x9000
Change it to:
call 0x9000
cli
loopend: ;Infinite loop when finished
hlt
jmp loopend
The code after the call will be executed when C function main returns. It is a simple loop that will effectively halt the processor and remain that way indefinitely since we have no where to go back to.
Code After Making All Recommended Changes
bootloader.asm:
[bits 16]
global _start
_start:
cli
xor ax, ax
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x8000 ; Stack pointer at SS:SP = 0x0000:0x8000
mov [BOOT_DRIVE], dl; Boot drive passed to us by the BIOS
mov dh, 17 ; Number of sectors (kernel.bin) to read from disk
; 17*512 allows for a kernel.bin up to 8704 bytes
mov bx, 0x9000 ; Load Kernel to ES:BX = 0x0000:0x9000
call load_kernel
call enable_A20
; call graphics_mode ; Uncomment if you want to switch to graphics mode 0x13
lgdt [gdtr]
mov eax, cr0
or al, 1
mov cr0, eax
jmp CODE_SEG:init_pm
graphics_mode:
mov ax, 0013h
int 10h
ret
load_kernel:
; load DH sectors to ES:BX from drive DL
push dx ; Store DX on stack so later we can recall
; how many sectors were request to be read ,
; even if it is altered in the meantime
mov ah , 0x02 ; BIOS read sector function
mov al , dh ; Read DH sectors
mov ch , 0x00 ; Select cylinder 0
mov dh , 0x00 ; Select head 0
mov cl , 0x02 ; Start reading from second sector ( i.e.
; after the boot sector )
int 0x13 ; BIOS interrupt
jc disk_error ; Jump if error ( i.e. carry flag set )
pop dx ; Restore DX from the stack
cmp dh , al ; if AL ( sectors read ) != DH ( sectors expected )
jne disk_error ; display error message
ret
disk_error :
mov bx , ERROR_MSG
call print_string
hlt
; prints a null - terminated string pointed to by EDX
print_string :
pusha
push es ;Save ES on stack and restore when we finish
push VIDEO_MEMORY_SEG ;Video mem segment 0xb800
pop es
xor di, di ;Video mem offset (start at 0)
print_string_loop :
mov al , [ bx ] ; Store the char at BX in AL
mov ah , WHITE_ON_BLACK ; Store the attributes in AH
cmp al , 0 ; if (al == 0) , at end of string , so
je print_string_done ; jump to done
mov word [es:di], ax ; Store char and attributes at current
; character cell.
add bx , 1 ; Increment BX to the next char in string.
add di , 2 ; Move to next character cell in vid mem.
jmp print_string_loop ; loop around to print the next char.
print_string_done :
pop es ;Restore ES that was saved on entry
popa
ret ; Return from the function
%include "a20.inc"
%include "gdt.inc"
[bits 32]
init_pm:
mov ax, DATA_SEG
mov ds, ax
mov ss, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ebp, 0x90000
mov esp, ebp
call 0x9000
cli
loopend: ;Infinite loop when finished
hlt
jmp loopend
[bits 16]
; Variables
ERROR db "A20 Error!" , 0
ERROR_MSG db "Error!" , 0
BOOT_DRIVE: db 0
VIDEO_MEMORY_SEG equ 0xb800
WHITE_ON_BLACK equ 0x0f
times 510-($-$$) db 0
db 0x55
db 0xAA
gdt.inc:
gdt_start:
dd 0 ; null descriptor--just fill 8 bytes
dd 0
gdt_code:
dw 0FFFFh ; limit low
dw 0 ; base low
db 0 ; base middle
db 10011010b ; access
db 11001111b ; granularity
db 0 ; base high
gdt_data:
dw 0FFFFh ; limit low (Same as code)
dw 0 ; base low
db 0 ; base middle
db 10010010b ; access
db 11001111b ; granularity
db 0 ; base high
end_of_gdt:
gdtr:
dw end_of_gdt - gdt_start - 1 ; limit (Size of GDT)
dd gdt_start ; base of GDT
CODE_SEG equ gdt_code - gdt_start
DATA_SEG equ gdt_data - gdt_start
a20.inc:
enable_A20:
call check_a20
cmp ax, 1
je enabled
call a20_bios
call check_a20
cmp ax, 1
je enabled
call a20_keyboard
call check_a20
cmp ax, 1
je enabled
call a20_fast
call check_a20
cmp ax, 1
je enabled
mov bx, [ERROR]
call print_string
enabled:
ret
check_a20:
pushf
push ds
push es
push di
push si
cli
xor ax, ax ; ax = 0
mov es, ax
not ax ; ax = 0xFFFF
mov ds, ax
mov di, 0x0500
mov si, 0x0510
mov al, byte [es:di]
push ax
mov al, byte [ds:si]
push ax
mov byte [es:di], 0x00
mov byte [ds:si], 0xFF
cmp byte [es:di], 0xFF
pop ax
mov byte [ds:si], al
pop ax
mov byte [es:di], al
mov ax, 0
je check_a20__exit
mov ax, 1
check_a20__exit:
pop si
pop di
pop es
pop ds
popf
ret
a20_bios:
mov ax, 0x2401
int 0x15
ret
a20_fast:
in al, 0x92
or al, 2
out 0x92, al
ret
[bits 32]
[section .text]
a20_keyboard:
cli
call a20wait
mov al,0xAD
out 0x64,al
call a20wait
mov al,0xD0
out 0x64,al
call a20wait2
in al,0x60
push eax
call a20wait
mov al,0xD1
out 0x64,al
call a20wait
pop eax
or al,2
out 0x60,al
call a20wait
mov al,0xAE
out 0x64,al
call a20wait
sti
ret
a20wait:
in al,0x64
test al,2
jnz a20wait
ret
a20wait2:
in al,0x64
test al,1
jz a20wait2
ret
kernel.c:
/* This code will be placed at the beginning of the object by the linker script */
__asm__ (".pushsection .text.start\r\n" \
"jmp main\r\n" \
".popsection\r\n"
);
/* Place main as the first function defined in kernel.c so
* that it will be at the entry point where our bootloader
* will call. In our case it will be at 0x9000 */
int main(){
/* Do Stuff Here*/
return 0; /* return back to bootloader */
}
linker.ld
OUTPUT_FORMAT(elf32-i386)
ENTRY(main)
SECTIONS
{
. = 0x9000;
.text : { *(.text.start) *(.text) }
.data : { *(.data) }
.bss : { *(.bss) *(COMMON) }
}
Create Disk Image Using DD / Debugging with QEMU
If you use the files above, and produce the required bootloader and kernel files using these commands (as mentioned previously)
nasm -g -f elf32 -F dwarf -o boot.o bootloader.asm
ld -melf_i386 -Ttext=0x7c00 -nostdlib --nmagic -o boot.elf boot.o
objcopy -O binary boot.elf boot.bin
gcc -g -m32 -c -ffreestanding -o kernel.o kernel.c -lgcc
ld -melf_i386 -Tlinker.ld -nostdlib --nmagic -o kernel.elf kernel.o
objcopy -O binary kernel.elf kernel.bin
You can produce a disk image (in this case we'll make it the size of a floppy) with these commands:
dd if=/dev/zero of=disk.img bs=512 count=2880
dd if=boot.bin of=disk.img bs=512 conv=notrunc
dd if=kernel.bin of=disk.img bs=512 seek=1 conv=notrunc
This creates a zero filled disk image of size 512*2880 bytes (The size of a 1.44 megabyte floppy). dd if=boot.bin of=disk.img bs=512 conv=notrunc writes boot.bin to the first sector of the file without truncating the disk image. dd if=kernel.bin of=disk.img bs=512 seek=1 conv=notrunc places kernel.bin into the disk image starting at the second sector. The seek=1 skips over the first block (bs=512) before writing.
If you wish to run your kernel you can launch it as floppy drive A: (-fda) in QEMU like this:
qemu-system-i386 -fda disk.img
You can also debug your 32-bit kernel using QEMU and the GNU Debugger (GDB) with the debug information we generated when compiling/assembling the code with the instructions above.
qemu-system-i386 -fda disk.img -S -s &
gdb kernel.elf \
-ex 'target remote localhost:1234' \
-ex 'layout src' \
-ex 'layout reg' \
-ex 'break main' \
-ex 'continue'
This example launches QEMU with the remote debugger and emulating a floppy disk using the file disk.img(that we created with DD). GDB launches using kernel.elf (a file we generated with debug info), then connects to QEMU, and sets a breakpoint at function main() in the C code. When the debugger finally is ready you'll be prompted to press <return> to continue. With any luck you should be viewing function main in the debugger.

My bootloader works, but doesn't load my kernel into memory

I've looked more into creating my own bootloader, rather than using grub. I soon came up with this: It takes care of switching to 32bit pm, it loads my kernel from the disk & it jumps to it to execute it.
I'm catting my kernel & my bootloader like this: cat boot.bin kernel > img.bin
I'm assembling my bootloader like this: nasm -f bin boot.s -o boot.bin
i686-elf-ld -o kernel -Ttext=0x1000 kernel_entry.bin kernel.bin --oformat binary
I'm compiling my kernel like this: i686-elf-gcc *.o -Ttext=0x1000 -o kernel.bin -ffreestanding -O2 -nostdlib -lgcc
(*.o are all compiled C files which I compile like this: i686-elf-gcc -c file.c -o file.o -std=gnu99 -ffreestanding -O2 -Wall -Wextra
[org 0x7c00]
[bits 16]
xor ax, ax
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x7c00
jmp 0:skip ; far jump
skip:
; load kernel
mov bx, 0x1000
mov dh, 17 ; reading 20 sectors should be enough ._.
mov dl, [BOOT_DRIVE]
call dsk_load
call load_kernel
dsk_load:
mov [SECTORS], dh
mov ch, 0x00 ; C = 0
mov dh, 0x00 ; H = 0
mov cl, 0x02 ; S = 2
next_group:
mov di, 5 ; retry 5 times
again:
mov ah, 0x02
mov al, [SECTORS]
int 0x13
jc maybe_retry
sub [SECTORS], al ; set remaining sectors
jz done
mov cl, 0x01 ; read sector 1
xor dh, 1 ; next head
jnz next_group
inc ch ; next cylinder
jmp next_group
maybe_retry:
mov ah, 0x00 ; reset drive
int 0x13
dec di
jnz again
jmp dsk_err ; we've tried too many times, give up
dsk_err:
mov bx, BOOTLOADER_SIG
call print
mov bx, DISK_READ_FAIL
call print
jmp $
done:
ret
; print string
print:
; print loop
print_loop:
mov ah, 0x0e
mov al, [bx] ; load current character
cmp al, 0
je print_return ; return when finished
int 0x10 ; print character
inc bx ; next character
jmp print_loop
print_return:
ret
load_kernel:
; If all that went well, we can switch to protected mode
cli
lgdt [gdt_descriptor]
mov eax, cr0
or eax, 0x1
mov cr0 , eax
jmp CODE_SEG:init_32_pm ; make a far jump
[bits 32]
init_32_pm:
set_up_stack:
mov esp, stack_end
mov ax, DATA_SEG
mov ds, ax
mov ss, ax
mov es, ax
mov fs, ax
mov gs, ax
jmp 0x1000 ; jump to kernel_entry.s
; our beloved gdt
gdt_start:
gdt_null: ; null descriptor
dd 0x0
dd 0x0
gdt_code: ; code segment descriptor
dw 0xffff ; limit (bits 0-15)
dw 0x0 ; base (bits 0-15)
db 0x0 ; base (bits 16-23)
db 10011010b ; 1st flags, type flags
db 11001111b ; 2nd flags, Limit (bits 16-19)
db 0x0 ; base (bits 24 - 31)
gdt_data: ; data segment descriptor
dw 0xffff ; limit (bits 0-15)
dw 0x0 ; base (bits 0-15)
db 0x0 ; base (bits 16 -23)
db 10010010b ; 1st flags, type flags
db 11001111b ; 2nd flags, Limit (bits 16-19)
db 0x0 ; base (bits 24 - 31)
gdt_end:
gdt_descriptor:
dw gdt_end - gdt_start - 1 ; gdt size
dd gdt_start ; gdt start address
; some handy constants
CODE_SEG equ gdt_code - gdt_start
DATA_SEG equ gdt_data - gdt_start
BOOT_DRIVE db 0
SECTORS db 0
BOOTLOADER_SIG db "------ bootloader ------", 0x0d, 0xa, 0
DISK_READ_FAIL db "An error occurred while loading the kernel! Please restart your computer.", 0x0d, 0xa, 0
times 510-($-$$) db 0
dw 0xaa55
section .bss
stack_begin:
resb 4096 ; 4kib stack
stack_end:
; 9 sectors
The code that resides over at 0x1000 is this:
; kernel_entry.s
[bits 32]
[extern kmain]
call kmain
jmp $
times 510-($-$$) db 0
dw 0xaa55
; 1 sectors
My bootloader doesn't crash but it does not load my kernel, which should print some things to the screen.
This is the kmain function:
void kmain(void)
{
/* Initialize terminal */
tty_init();
tty_puts("Hello kernel!", VGA_COLOR_LIGHT_CYAN);
}
Assume the tty functions are working, since they were doing just fine when testing with grub instead of my own bootloader. Does anyone know what's going on? (Testing in bochs shows no errors)

Why does this bootloader crash?

I've looked more into creating my own bootloader, rather than using grub. I soon came up with this:
It takes care of switching to 32bit pm, it loads my kernel from the disk & it jumps to it to execute it.
I'm catting my kernel & my bootloader like this: cat boot.bin kernel.bin > img.bin
I'm assembling my bootloader like this: nasm -f bin kernel/arch/$ARCH_TARGET/boot/boot.s -o boot.bin
I'm compiling my kernel like this: i686-elf-gcc *.o -Ttext=0x0 -o kernel.bin -ffreestanding -O2 -nostdlib -lgcc
(*.o are all compiled C files which I compile like this: i686-elf-gcc -c file.c -o file.o -std=gnu99 -ffreestanding -O2 -Wall -Wextra
This is my bootloader code:
[org 0x7c00]
[bits 16]
xor ax, ax
mov ds, ax
mov es, ax
; load kernel
mov bx, 0x1000
mov dh, 10 ; reading 15 sectors should be enough ._.
mov dl, [BOOT_DRIVE]
call dsk_load
call load_kernel
dsk_load:
mov [SECTORS], dh
mov ch, 0x00 ; C = 0
mov dh, 0x00 ; H = 0
mov cl, 0x02 ; S = 2
next_group:
mov di, 5 ; retry 5 times
again:
mov ah, 0x02
mov al, [SECTORS]
int 0x13
jc maybe_retry
sub [SECTORS], al ; set remaining sectors
jz done
mov cl, 0x01 ; read sector 1
xor dh, 1 ; next head
jnz next_group
inc ch ; next cylinder
jmp next_group
maybe_retry:
mov ah, 0x00 ; reset drive
int 0x13
dec di
jnz again
jmp dsk_err ; we've tried too many times, give up
dsk_err:
mov bx, BOOTLOADER_SIG
call print
mov bx, DISK_READ_FAIL
call print
jmp $
done:
ret
; print string
print:
; print loop
print_loop:
mov ah, 0x0e
mov al, [bx] ; load current character
cmp al, 0
je print_return ; return when finished
int 0x10 ; print character
inc bx ; next character
jmp print_loop
print_return:
ret
load_kernel:
; If all that went well, we can switch to protected mode
cli
lgdt [gdt_descriptor]
mov eax, cr0
or eax, 0x1
mov cr0 , eax
jmp CODE_SEG:init_32_pm ; make a far jump
[bits 32]
init_32_pm:
mov ax, DATA_SEG
mov ds, ax
mov ss, ax
mov es, ax
mov fs, ax
mov gs, ax
jmp 0x1000 ; call kernel
jmp $
; our beloved gdt
gdt_start:
gdt_null: ; null descriptor
dd 0x0
dd 0x0
gdt_code: ; code segment descriptor
dw 0xffff ; limit (bits 0-15)
dw 0x0 ; base (bits 0-15)
db 0x0 ; base (bits 16 -23)
db 10011010b ; 1st flags, type flags
db 11001111b ; 2nd flags, Limit (bits 16-19)
db 0x0 ; base (bits 24 - 31)
gdt_data: ; data segment descriptor
dw 0xffff ; limit (bits 0-15)
dw 0x0 ; base (bits 0-15)
db 0x0 ; base (bits 16 -23)
db 10010010b ; 1st flags, type flags
db 11001111b ; 2nd flags, Limit (bits 16-19)
db 0x0 ; base (bits 24 - 31)
gdt_end:
gdt_descriptor:
dw gdt_end - gdt_start - 1 ; gdt size
dd gdt_start ; gdt start address
; some handy constants
CODE_SEG equ gdt_code - gdt_start
DATA_SEG equ gdt_data - gdt_start
BOOT_DRIVE db 0
SECTORS db 0
BOOTLOADER_SIG db "------ NubelaOS bootloader ------", 0x0d, 0xa, 0
DISK_READ_FAIL db "An error occurred while loading the kernel! Please restart your computer.", 0x0d, 0xa, 0
times 510-($-$$) db 0
dw 0xaa55
Booting it into Qemu makes it go in a "boot loop", by rebooting constantly

External C code not executing from two stage x86 bootloader?

I want to make a toy os but i am stuck on the bootloader stage.
I just wanted to write a very simple bootloader that loads my main C code, i don't like x86 assembly all that much, what happens is once in the second stage bootloader, after switching to protected/32bit mode, i call my kmain() c function using the extern keyword it seems to go into some kind of infinite rebooot loop OR qemu crashes with this error "Trying to execute code outside RAM or ROM at 0x000a0000 "
I also have some x86 assembler based functions to print text to the screen, those work well if used, but calling the extern c function causes the above mentioned arrors
the "flow" is like this
bootloader.asm(first stage bootloader)->Stage2.asm(2nd stage bootloader)->kmain.cpp
GDT, stack, A20 are setup in the Stage2.asm to start protected mode/32 bit mode, and then the extern kmain function is called.The kmain() c++ file only tries to write one character with a color to the VGA buffer
I am on an UBuntu 18.04 host, using nasm for assembly, gcc for linking and c compiling and qemu for testing this so-called OS
These are the commands i use to build and run
nasm -f elf bootload.asm -o bootload.o
nasm -f elf Stage2.asm -o stage2.o
gcc -m32 stage2.o bootload.o kmain.cpp -o kernel.bin -g -nostdlib -ffreestanding -std=c++11 -mno-red-zone -fno-exceptions -nostdlib -fno-rtti -Wall -Wextra -Werror -T linker.ld
qemu-system-i386 -fda kernel.bin
This is my bootloader.asm
[bits 16]
section .boot
global boot
boot:
hello: db "Hello world!",0
mov si,hello
mov ah,0x0e
.loop:
lodsb
or al,al
jz diskboot
int 0x10
jmp .loop
diskboot:
mov ax,0x800
mov es,ax
xor bx,bx
mov ah,0x2
mov al,0x1
mov ch,0x0
mov cl,0x2
mov dh,0x0
int 0x13
jmp 0x800:0
Stage2.asm
section .kernel
bits 16
jmp main ; go to start
;*******************************************************
; Preprocessor directives
;*******************************************************
%include "stdio.inc" ; basic i/o routines
%include "Gdt.inc" ; Gdt routines
%include "A20.inc" ; A20 enabling
;*******************************************************
; Data Section
;*******************************************************
LoadingMsg db 0x0D, 0x0A, "Searching for Operating System...", 0x00
main:
cli ; clear interrupts
xor ax, ax ; null segments
mov ds, ax
mov es, ax
mov ax, 0x9000 ; stack begins at 0x9000-0xffff
mov ss, ax
mov sp, 0xFFFF
sti ; enable interrupts
call InstallGDT ; install our GDT
call EnableA20_KKbrd_Out
mov si, LoadingMsg
call Puts16
EnterStage3:
cli ; clear interrupts
mov eax, cr0 ; set bit 0 in cr0--enter pmode
or eax, 1
mov cr0, eax
jmp CODE_DESC:Stage3 ; far jump to fix CS
bits 32
Stage3:
mov ax, DATA_DESC
mov ds, ax
mov ss, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ebp, 0x90000
mov esp, ebp
extern kmain
call kmain
cli
hlt
msg db 0x0A, "<[ OS Development Series Tutorial 10 ]>", 0x0A, 0
A20.inc
;********************************************
; Enable A20 address line
;
; OS Development Series
;********************************************
%ifndef __A20_INC_67343546FDCC56AAB872_INCLUDED__
%define __A20_INC_67343546FDCC56AAB872_INCLUDED__
bits 16
;----------------------------------------------
; Enables a20 line through keyboard controller
;----------------------------------------------
EnableA20_KKbrd:
cli
push ax
mov al, 0xdd ; send enable a20 address line command to controller
out 0x64, al
pop ax
ret
;--------------------------------------------
; Enables a20 line through output port
;--------------------------------------------
EnableA20_KKbrd_Out:
cli
pusha
call wait_input
mov al,0xAD
out 0x64,al ; disable keyboard
call wait_input
mov al,0xD0
out 0x64,al ; tell controller to read output port
call wait_output
in al,0x60
push eax ; get output port data and store it
call wait_input
mov al,0xD1
out 0x64,al ; tell controller to write output port
call wait_input
pop eax
or al,2 ; set bit 1 (enable a20)
out 0x60,al ; write out data back to the output port
call wait_input
mov al,0xAE ; enable keyboard
out 0x64,al
call wait_input
popa
sti
ret
; wait for input buffer to be clear
wait_input:
in al,0x64
test al,2
jnz wait_input
ret
; wait for output buffer to be clear
wait_output:
in al,0x64
test al,1
jz wait_output
ret
;--------------------------------------
; Enables a20 line through bios
;--------------------------------------
EnableA20_Bios:
pusha
mov ax, 0x2401
int 0x15
popa
ret
;-------------------------------------------------
; Enables a20 line through system control port A
;-------------------------------------------------
EnableA20_SysControlA:
push ax
mov al, 2
out 0x92, al
pop ax
ret
%endif
Gdt.inc
;*************************************************
; Gdt.inc
; -GDT Routines
;
; OS Development Series
;*************************************************
%ifndef __GDT_INC_67343546FDCC56AAB872_INCLUDED__
%define __GDT_INC_67343546FDCC56AAB872_INCLUDED__
bits 16
;*******************************************
; InstallGDT()
; - Install our GDT
;*******************************************
InstallGDT:
cli ; clear interrupts
pusha ; save registers
lgdt [toc] ; load GDT into GDTR
sti ; enable interrupts
popa ; restore registers
ret ; All done!
;*******************************************
; Global Descriptor Table (GDT)
;*******************************************
gdt_data:
dd 0 ; null descriptor
dd 0
; gdt code: ; code descriptor
dw 0FFFFh ; limit low
dw 0 ; base low
db 0 ; base middle
db 10011010b ; access
db 11001111b ; granularity
db 0 ; base high
; gdt data: ; data descriptor
dw 0FFFFh ; limit low (Same as code)10:56 AM 7/8/2007
dw 0 ; base low
db 0 ; base middle
db 10010010b ; access
db 11001111b ; granularity
db 0 ; base high
end_of_gdt:
toc:
dw end_of_gdt - gdt_data - 1 ; limit (Size of GDT)
dd gdt_data ; base of GDT
; give the descriptor offsets names
%define NULL_DESC 0
%define CODE_DESC 0x8
%define DATA_DESC 0x10
%endif ;__GDT_INC_67343546FDCC56AAB872_INCLUDED__
linker.ld
ENTRY(boot)
OUTPUT_FORMAT("binary")
SECTIONS {
. = 0x7c00;
.boot :
{
*(.boot)
}
. = 0x7dfe;
.sig : {
SHORT(0xaa55);
}
. = 0x8000;
.kernel : AT(0x7e00) /* place immediately after the boot sector */
{
*(.kernel)
*(.text)
*(.rodata)
*(.data)
*(.bss)
*(COMMON)
}
kernel_sectors = (SIZEOF(.kernel) + 511) / 512;
/DISCARD/ : {
*(.eh_frame)
}
}
kmain.cpp
extern "C" void kmain()
{
unsigned char* vga = (unsigned char*) 0xb8000;
vga[0] = 'S';
vga[1] = 0x09;
for(;;);
}
EDIT : added stdio.inc
%ifndef __STDIO_INC_67343546FDCC56AAB872_INCLUDED__
%define __STDIO_INC_67343546FDCC56AAB872_INCLUDED__
;==========================================================
;
; 16 Bit Real Mode Routines
;==========================================================
;************************************************;
; Puts16 ()
; -Prints a null terminated string
; DS=>SI: 0 terminated string
;************************************************;
bits 16
Puts16:
pusha ; save registers
.Loop1:
lodsb ; load next byte from string from SI to AL
or al, al ; Does AL=0?
jz Puts16Done ; Yep, null terminator found-bail out
mov ah, 0eh ; Nope-Print the character
int 10h ; invoke BIOS
jmp .Loop1 ; Repeat until null terminator found
Puts16Done:
popa ; restore registers
ret ; we are done, so return
;==========================================================
;
; 32 Bit Protected Mode Routines
;==========================================================
bits 32
%define VIDMEM 0xB8000 ; video memory
%define COLS 80 ; width and height of screen
%define LINES 25
%define CHAR_ATTRIB 63 ; character attribute (White text on light blue background)
_CurX db 0 ; current x/y location
_CurY db 0
;**************************************************;
; Putch32 ()
; - Prints a character to screen
; BL => Character to print
;**************************************************;
Putch32:
pusha ; save registers
mov edi, VIDMEM ; get pointer to video memory
;-------------------------------;
; Get current position ;
;-------------------------------;
xor eax, eax ; clear eax
;--------------------------------
; Remember: currentPos = x + y * COLS! x and y are in _CurX and _CurY.
; Because there are two bytes per character, COLS=number of characters in a line.
; We have to multiply this by 2 to get number of bytes per line. This is the screen width,
; so multiply screen with * _CurY to get current line
;--------------------------------
mov ecx, COLS*2 ; Mode 7 has 2 bytes per char, so its COLS*2 bytes per line
mov al, byte [_CurY] ; get y pos
mul ecx ; multiply y*COLS
push eax ; save eax--the multiplication
;--------------------------------
; Now y * screen width is in eax. Now, just add _CurX. But, again remember that _CurX is relative
; to the current character count, not byte count. Because there are two bytes per character, we
; have to multiply _CurX by 2 first, then add it to our screen width * y.
;--------------------------------
mov al, byte [_CurX] ; multiply _CurX by 2 because it is 2 bytes per char
mov cl, 2
mul cl
pop ecx ; pop y*COLS result
add eax, ecx
;-------------------------------
; Now eax contains the offset address to draw the character at, so just add it to the base address
; of video memory (Stored in edi)
;-------------------------------
xor ecx, ecx
add edi, eax ; add it to the base address
;-------------------------------;
; Watch for new line ;
;-------------------------------;
cmp bl, 0x0A ; is it a newline character?
je .Row ; yep--go to next row
;-------------------------------;
; Print a character ;
;-------------------------------;
mov dl, bl ; Get character
mov dh, CHAR_ATTRIB ; the character attribute
mov word [edi], dx ; write to video display
;-------------------------------;
; Update next position ;
;-------------------------------;
inc byte [_CurX] ; go to next character
; cmp byte [_CurX], COLS ; are we at the end of the line?
; je .Row ; yep-go to next row
jmp .done ; nope, bail out
;-------------------------------;
; Go to next row ;
;-------------------------------;
.Row:
mov byte [_CurX], 0 ; go back to col 0
inc byte [_CurY] ; go to next row
;-------------------------------;
; Restore registers & return ;
;-------------------------------;
.done:
popa ; restore registers and return
ret
;**************************************************;
; Puts32 ()
; - Prints a null terminated string
; parm\ EBX = address of string to print
;**************************************************;
Puts32:
;-------------------------------;
; Store registers ;
;-------------------------------;
pusha ; save registers
push ebx ; copy the string address
pop edi
.loop:
;-------------------------------;
; Get character ;
;-------------------------------;
mov bl, byte [edi] ; get next character
cmp bl, 0 ; is it 0 (Null terminator)?
je .done ; yep-bail out
;-------------------------------;
; Print the character ;
;-------------------------------;
call Putch32 ; Nope-print it out
;-------------------------------;
; Go to next character ;
;-------------------------------;
inc edi ; go to next character
jmp .loop
.done:
;-------------------------------;
; Update hardware cursor ;
;-------------------------------;
; Its more efficiant to update the cursor after displaying
; the complete string because direct VGA is slow
mov bh, byte [_CurY] ; get current position
mov bl, byte [_CurX]
call MovCur ; update cursor
popa ; restore registers, and return
ret
;**************************************************;
; MoveCur ()
; - Update hardware cursor
; parm/ bh = Y pos
; parm/ bl = x pos
;**************************************************;
bits 32
MovCur:
pusha ; save registers (aren't you getting tired of this comment?)
;-------------------------------;
; Get current position ;
;-------------------------------;
; Here, _CurX and _CurY are relitave to the current position on screen, not in memory.
; That is, we don't need to worry about the byte alignment we do when displaying characters,
; so just follow the forumla: location = _CurX + _CurY * COLS
xor eax, eax
mov ecx, COLS
mov al, bh ; get y pos
mul ecx ; multiply y*COLS
add al, bl ; Now add x
mov ebx, eax
;--------------------------------------;
; Set low byte index to VGA register ;
;--------------------------------------;
mov al, 0x0f
mov dx, 0x03D4
out dx, al
mov al, bl
mov dx, 0x03D5
out dx, al ; low byte
;---------------------------------------;
; Set high byte index to VGA register ;
;---------------------------------------;
xor eax, eax
mov al, 0x0e
mov dx, 0x03D4
out dx, al
mov al, bh
mov dx, 0x03D5
out dx, al ; high byte
popa
ret
;**************************************************;
; ClrScr32 ()
; - Clears screen
;**************************************************;
bits 32
ClrScr32:
pusha
cld
mov edi, VIDMEM
mov cx, 2000
mov ah, CHAR_ATTRIB
mov al, ' '
rep stosw
mov byte [_CurX], 0
mov byte [_CurY], 0
popa
ret
;**************************************************;
; GotoXY ()
; - Set current X/Y location
; parm\ AL=X position
; parm\ AH=Y position
;**************************************************;
bits 32
GotoXY:
pusha
mov [_CurX], al ; just set the current position
mov [_CurY], ah
popa
ret
%endif ;__STDIO_INC_67343546FDCC56AAB872_INCLUDED__
stdio.inc had functions that were already writing to the VGA framebuffer using x86 assembler in 32 bit protected mode, these functions were conflicting with my C code that was also trying to write to the framebuffer.
removing stdio.inc and all references to its functions has solved the issue
I can now write to the VGA Buffer and have colored text display on the screen using C code.

Far jump after switching from real to protected mode

According to this tutorial it is sufficient to create a simple operating system with switching to protected mode as simple as the following code without the need for other well known actions such as enabling A20...
Anyway, I am newbie to this domain, I wrote the following code as they mentioned exactly with the modification inspired from this SO.
Code Structure:
This simple operating system should load briefly as follows:
Load/read 15 sectors
Enable GDT
Switch to protected mode (And print "Successfully landed in 32-bit Protected Mode").
Load kernel and print "X"
However the emulator is still rebooting. Please find enclosed the entire code.
bootloader.asm
[bits 16]
[org 0x7C00]
KERNEL_OFFSET equ 0x1000
xor ax, ax
mov ds, ax
mov es, ax
mov [BOOT_DRIVE], dl
mov ax, 0x07E0 ; End of stack
cli
mov ss, ax
mov sp, 0x1200 ; Size of Stack. By this, we assume that stack starts at 9000h
; of size 1200h and ends at 7E00h to avoid being overwritten.
sti
call load_kernel
call switch_to_pm
jmp $
%include "src/functions/disk_load.asm"
load_kernel:
mov bx, KERNEL_OFFSET
mov dh, 15
mov dl, [BOOT_DRIVE]
call disk_load
ret
; Global variables
BOOT_DRIVE db 0
SECTORS db 0
MSG_PROT_MODE db "Successfully landed in 32-bit Protected Mode" , 0
%include "src/functions/gdt.asm"
%include "src/functions/switch_to_pm.asm"
[ bits 32]
; This is where we arrive after switching to and initialising protected mode.
BEGIN_PM:
mov ebx , MSG_PROT_MODE
call print_string_pm ; Use our 32 - bit print routine.
;call KERNEL_OFFSET ; Now jump to the address of our loaded
; kernel code , assume the brace position ,
; and cross your fingers. Here we go !
jmp $ ; Hang.
%include "src/functions/writing_video_mode.asm"
; Bootsector padding
times 510-($-$$) db 0
dw 0xAA55
; 15 sector padding
times 15*256 dw 0xDADA
disk_load.asm
disk_load:
mov [SECTORS], dh
mov ch, 0x00 ;C=0
mov dh, 0x00 ;H=0
mov cl, 0x02 ;S=2
next_group:
mov di, 5 ;Max 5 tries
again:
mov ah, 0x02 ;Read sectors
mov al, [SECTORS]
int 0x13
jc maybe_retry
sub [SECTORS], al ;Remaining sectors
jz ready
mov cl, 0x01 ;Always sector 1
xor dh, 1 ;Next head on diskette!
jnz next_group
inc ch ;Next cylinder
jmp next_group
maybe_retry:
mov ah, 0x00 ;Reset diskdrive
int 0x13
dec di
jnz again
jmp disk_error
ready:
ret
disk_error:
mov ah, 0x0e
mov al, 'Y'
int 0x10
jmp $
DISK_ERROR_MSG db "Disk read error!", 0
gdt.asm
gdt_start:
gdt_null:
dd 0x0 ; ’ dd ’ means define double word ( i.e. 4 bytes )
dd 0x0
gdt_code:
dw 0xffff
dw 0x0
db 0x0
db 10011010b ; 1 st flags , type flags
db 11001111b ; 2 nd flags , Limit ( bits 16 -19)
db 0x0
gdt_data:
dw 0xffff
dw 0x0
db 0x0
db 10010010b ; 1 st flags , type flags
db 11001111b ; 2 nd flags , Limit ( bits 16 -19)
db 0x0
gdt_end:
gdt_descriptor:
dw gdt_end - gdt_start - 1
dd gdt_start
CODE_SEG equ gdt_code - gdt_start
DATA_SEG equ gdt_data - gdt_start
switch_to_pm.asm
[ bits 16 ]
switch_to_pm:
cli
lgdt [ gdt_descriptor ]
mov eax , cr0
or eax , 0x1
mov cr0 , eax
jmp CODE_SEG:init_pm
[ bits 32 ]
init_pm:
mov ax, DATA_SEG
mov ds, ax
mov ss, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ebp , 0x90000
mov esp , ebp
call BEGIN_PM
And in order to make sure that we landed in the protected mode:
writing_video_mode.asm
[ bits 32]
VIDEO_MEMORY equ 0xb8000
WHITE_ON_BLACK equ 0x0f
print_string_pm:
push eax
push ebx
push edx
mov edx , VIDEO_MEMORY ; Set edx to the start of vid mem.
print_string_pm_loop:
mov al, [ebx]
mov ah, WHITE_ON_BLACK
cmp al, 0
je print_string_pm_done
mov [edx], ax
add ebx, 1
add edx, 2
jmp print_string_pm_loop
print_string_pm_done:
pop edx
pop ebx
pop eax
ret
kernel.c
void main () {
char * video_memory = (char *) 0xb8000;
*video_memory = 'X';
}
By the way, I am using this Makefile:
all: bootloader.bin kernel.bin
bootloader.bin: src/bootloader.asm
nasm src/bootloader.asm -f bin -o output/bootloader.bin
kernel.o: src/kernel/kernel.c
gcc -ffreestanding -c src/kernel/kernel.c -o output/kernel.o -m32
kernel.bin: kernel.o
ld -o output/kernel.bin -Ttext 0x1000 --oformat binary output/kernel.o -melf_i386
clean:
rm -f output/*.* output/*
and in order to move it into the flash memory, I use these commands:
cat output/bootloader.bin output/kernel.bin > os-image
sudo dd if=os-image of=/dev/sdb bs=512 conv=notrunc && sync
In order to run it, I am using qemu with this command:
qemu-system-i386 -hda /dev/sdb
Noting that /dev/sdb is my flash memory drive.
Problem:
Indeed, the code is landing into the protected mode (I.e. Printing "Successfully landed in 32-bit Protected Mode") just when disabling/commenting the call KERNEL_OFFSET in the bootloader.asm. However when enabling this line it starts booting and rebooting.
I hope I have provided every needed information. It seems for me that the far jump should not be done this way. Any comment is appreciated.
Just remove
times 15*256 dw 0xDADA
(btw, why DADA?)
then compile your kernel, after that
cat output/bootloader.bin output/kernel.bin > os-image
and somehow make your os image 8192 byte long (16 sectors, bootloader + 15). I'm not Linux/Unix fan (even can't use them), but I think dd command (something like dd if=dev\zero of=temp_file count=(8192 - file actual size), and then cat os-image temp-file > os-image) should do the job. I'm also not sure is this compilation command correct (only not sure). I would remove "-melf_i386" from linker command, but idk, I have only used MinGW on Windows (it's only similar to GCC).
Sorry for my bad English, I hope I helped.

Resources