Search code examples
assemblykernelnasmqemuprotected-mode

Infinite loop when i try to compile a C program, with self made kernel,in protected mode, using nasm and qemu


My operating system is Ubuntu. I am trying to follow through a tutorial building a kernel..Even though i ve built the kernel, when i try to compile a simple C program that prints an 'X' on the top left corner of the screen in qemu,the screen just loops infinitely between some Real-Mode print commands, without ever landing in protected mode.. The program includes loading some extra sectors with disk_load.asm, then switching to 32 bit protected mode with switch_to_pm, and then executing the C code

Here is the code.

boot_sect.asm

    ;boot sector that boots a C kernel in 32-bit protected mode
[org 0x7c00]
KERNEL_OFFSET equ 0x1000 ;This is the memory offset to which we will load our kernel
mov [BOOT_DRIVE],dl ;BIOS stores our boot drive in DL,so it's
        ;best to remember this for later.
mov bp, 0x9000;Set-up the stack.  
mov sp,bp
mov bx, MSG_REAL_MODE ;Announce that we are starting
call print_string      ;booting from 16-bit real mode
call load_kernel ;Load our kernel
call switch_to_pm;Switch to protected mode,from which
     ;we will not return
jmp $

Include our useful,hard-earned routines
%include "print_string.asm"
%include "disk_load.asm"
%include "gdt.asm"
;%include "print_string_pm.asm"
%include "switch_to_pm.asm"
[bits 16]
load_kernel
load_kernel:
;mov bx, MSG_LOAD_KERNEL ;Print a message to say we are loading the kernel
                      ;call print_string
mov bx, KERNEL_OFFSET   ;Set-up parameters for our disk_load routine,so
mov dh, 15              ;that we load the first 15 sectors(excluding
mov dl, [BOOT_DRIVE]    ;the boot sector)from the boot disk(i.e.our
call disk_load          ;kernel code)to address KERNEL_OFFSET
ret

[bits 32]
;This is where we arrive after switching to and initialising protected mode


BEGIN_PM:
;mov ebx, MSG_PROT_MODE  ;Use our 32-bit print routine to
;call print_string_pm    ;announce we are in protected mode 
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.
;Globalvariables
BOOT_DRIVE      db 0
MSG_REAL_MODE   db "Started in 16-bit Real Mode", 0
;MSG_PROT_MODE   db "Successfully landed in 32-bit Protected Mode", 0
MSG_LOAD_KERNEL db "Loading kernel into memory.", 0
;Bootsectorpadding
times 510-($-$$) db 0
dw 0xaa55

disk_load.asm

;load DH sectors to ES:BX from drive DL
;works for floppies

disk_load:
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 mean time
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.carryflagset)
pop dx                  ;Restore DX from thestack
cmp dh, al;ifAL(sectorsread)!=DH(sectorsexpected) 
jne disk_error          ;display error message
ret
disk_error :
mov bx, DISK_ERROR_MSG
call print_string
jmp $                   ;Variables
DISK_ERROR_MSG db "Disk read error!", 0

switch_to_pm.asm

switch_to_pm:
cli        ;We must switch of interrupts until we have  cli->clear interrupt
       ;set-up the protected mode interrupt vector
       ;otherwise interrupts will run riot

lgdt [gdt_descriptor] ;Load our global descriptor table,which defines
          ;the protected mode segments(e.g.for code and data)
mov eax,cr0            ;To make the switch to protected mode,we set
or eax, 0x1            ;the first bit of CR0,a control register
mov cr0,eax           ;update register, we cannot set the bit directly on the register
jmp CODE_SEG:init_pm ;Make a far jump(i.e.to a new segment)to our 32-bit
         ;code.This also forces the CPU to flush its cache of
                 ;pre-fetched and real mode decoded instructions,which can
             ;cause problems. we can use the or
         ;instruction to include certain bits into a value (i.e. without
         ;disturbing any other bits that, for some important reason, may have been set 
                 ;already in the control register)
[bits 32]
             ;Initialise registers and the stack once in PM.
init_pm:
mov ax, DATA_SEG ;Now in PM,our old segments are meaningless,
mov ds,ax        ;so we point our segment registers to the
mov ss,ax        ;data selector we defined in our GDT
mov es,ax
mov fs,ax
mov gs,ax
mov ebp, 0x90000    ;position so it is right
mov esp,ebp      ;at the top of the free space.
call BEGIN_PM    ;Finally,call somewell-known label7

the GDT code, that contains the gdt_descriptor

gdt.asm

; GDT
gdt_start:
gdt_null: ;the mandatory null descriptor
dd 0x0  ;'dd'means define double word(i.e.4bytes)
dd 0x0
gdt_code: ;the code segment descriptor
;base=0x0,limit=0xfffff ,
;1st flags:(present)1(privilege)00(descriptortype)1->1001b 
;type flags:(code)1(conforming)0(readable)1(accessed)0->1010b
;2nd flags:(granularity)1(32-bitdefault)1(64-bitseg)0(AVL)0->1100b
dw 0xffff ;Limit(bits0-15)
dw 0x0  ;Base(bits0-15)
db 0x0  ;Base(bits16-23)
db 10011010b    ;1st flags,type flags
db 11001111b    ;2n dflags,Limit(bits16-19)
db 0x0       ;Base(bits24-31)
gdt_data:    ;the data segment descriptor
         ;Same as code segment except for the type flags:
   ;type flags:(code)0(expanddown)0(writable)1(accessed)0->0010b
dw 0xffff ;Limit(bits0-15)
dw 0x0  ;Base(bits0-15)
db 0x0  ;Base(bits16-23)
db 10010010b    ;1stflags,type flags
db 11001111b    ;2ndflags,Limit(bits16-19)
db 0x0      ;Base(bits24-31)
gdt_end:  ;The reason for putting a label at the end of the
      ;GDT is so we can have the assembler calculate
   ;the size of the GDT for the GDT decriptor(below)
 ;GDT descriptior
gdt_descriptor:
dw gdt_end - gdt_start - 1  ;Size of our GDT,always less one
            ;of the true size 
dd gdt_start            ;Start address of our GDT

;Define some handy constants for the GDT segment descriptor offsets,which
;are what segment registers must contain when in protected mode.For example,
;when we set DS=0x10 in PM,the CPU knows that we mean it to use the
;segment described at offset 0x10(i.e.16bytes)in our GDT,which in our
;case is the DATA segment(0x0->NULL;0x08->CODE;0x10->DATA)
CODE_SEG equ gdt_code - gdt_start -10;-10 so it is withing memory limits 
DATA_SEG equ gdt_data - gdt_start -10;

print_string.asm

print_string:
pusha
mov ah, 0x0e

loop:
    mov al, [bx]
    cmp al, 0
    je return
    int 0x10
    inc bx
    jmp loop

return:
    popa
    ret

That is the C program that prints an 'X'

kernel.c

void main() {
//Create a pointer to a char,and point it to the first text cell of
//video memory(i.e.the top-left of the screen)
char* video_memory = (char *) 0xb800;
//At the address pointed to by video_memory,store the character'X'
//(i.e.display 'X' in the top-left of the screen).
*video_memory = 'X';
}

In order to make the C code, raw machine code we type in the directory we have saved the kernel.c file

$gcc -ffreestanding -c kernel.c -o kernel.o
$ld -o kernel.bin -Ttext 0x1000 kernel.o --oformat binary

then in order to create the binary file for qemu

$nasm boot_sect.asm -f bin  -o boot_sect.bin

and finally

$cat bootsect.bin kernel.bin > os-image

to execute the code as a floppy

$qemu -fda os-iamge

and now instead of printing an 'X' it just loops infinitely, printing only the Real Mode strings("Started in 16 bit Real-Mode"and "Loading Kernel into memory.", and not entering 32bit protected mode. I tried running every piece of code in nasm one by one,and then adding every individual piece of code together, and it seemed like the loop began when the switch_to_pm piece of code was added.Why is that? (i think something is faulty with switch_to_pm code) Thank you a lot.


Solution

  • This is advanced programming. As such, it has some prerequisites, for example being able to debug your program. I have done that for you now, but you shouldn't come to SO every time you hit a problem.

    The main issue is in your gdt.asm, namely these lines:

    CODE_SEG equ gdt_code - gdt_start -10;-10 so it is withing memory limits 
    DATA_SEG equ gdt_data - gdt_start -10;
    

    I can't even guess what you wanted to do there with the -10 and the comment doesn't help one bit. The correct definition is:

    CODE_SEG equ gdt_code - gdt_start
    DATA_SEG equ gdt_data - gdt_start
    

    The cpu uses the segment selectors (with the bottom 3 bits masked) to address the GDT. If you want to modify the linear address covered, you need to edit the GDT entry, you can't just mess with the selector.

    Fixing this, your code will no longer crash, but won't work either. The reason being that the VGA text memory is at real-mode segment 0xb800, but that's of course physical address 0xb8000 (one more zero). You need to change that in your kernel.c. Furthermore, since you are only writing a single byte, you are not touching the attribute byte. You should do that too, to make sure your X is displayed no matter what the current contents of screen memory are.