Instruction after paging is enabled doesn't appear to execute

2

When I have to setup the EIP register, the program doesn't jump to the correct position. I'm expecting that jmp *%ecx jumps at the right spot in memory setting EIP to around 0xC0100000 (label: StartInHigherHalf) using LEA. I don't think that kmain is necessary because the problem is before it is called. I'm going to post it anyway.

I've tried to debug it with the -d cpu flag on QEMU, and before the jump (blocked with HLT) says that ECX is not loaded with the LEA function. Is it possible for LEA instruction not to execute? Why can that happen? How can I fix it?

Boot.S:

.set ALIGN,    1<<0                                       
.set MEMINFO,  1<<1                               
.set FLAGS,    ALIGN | MEMINFO                         
.set MAGIC,    0x1BADB002                      
.set CHECKSUM, -(MAGIC + FLAGS)            

.set KERNEL_VIRTUAL_BASE, 0xC0000000                       
.set KERNEL_PAGE_NUMBER, (KERNEL_VIRTUAL_BASE >> 22)

.section .multiboot
.align 4
.long MAGIC
.long FLAGS
.long CHECKSUM

.section .data
.align 0x1000
BootPageDirectory:

.quad 0x00000083

.rept KERNEL_PAGE_NUMBER - 1
.quad 0        
.endr

.quad 0x00000083
.rept 0x400 - KERNEL_PAGE_NUMBER - 1
.quad 0                    
.endr

.set STACKSIZE, 0x4000

.global __start__
.set __start__, (setup)
setup: 
    mov $(BootPageDirectory - KERNEL_VIRTUAL_BASE), %ecx
    mov %ecx, %cr3 

    mov %cr4, %ecx
    or $0x00000010, %ecx 
    mov %ecx, %cr4    

    mov %cr0, %ecx
    or $0x80000000, %ecx
    mov %ecx, %cr0

    lea StartInHigherHalf, %ecx
    jmp *%ecx      

StartInHigherHalf:
    movl $0, (BootPageDirectory)
    invlpg (0)

    mov $(stack + STACKSIZE), %esp                 
    push %eax                                        

    push %ebx

    call _init

    call kmain

    cli
1:  hlt
    jmp 1

.section .bss
.align 32

.lcomm stack, STACKSIZE    
.global gdtFlush            
.extern gp               

gdtFlush:
    lgdt (gp)

    mov $0x10, %eax
    mov %eax, %ds               
    mov %eax, %es
    mov %eax, %gs
    mov %eax, %fs
    mov %eax, %ss

    ljmp $0x08, $setcs                              

setcs:
    ret

linker.ld:

ENTRY(__start__)
OUTPUT_FORMAT(elf32-i386)

SECTIONS {
    . = 0xC0100000;

    .text  ALIGN(0x1000) : AT(ADDR(.text) - 0xC0000000) {
        *(.multiboot)
        *(.text)
    }
    .rodata ALIGN(0x1000) : AT(ADDR(.rodata) - 0xC0000000) {
        *(.rodata*)
    } 
    .data ALIGN(0x1000) : AT(ADDR(.data) - 0xC0000000) {
        *(.data)
    }
    .bss ALIGN(0x1000) : AT(ADDR(.bss) - 0xC0000000) {
        _sbss = .;
        *(COMMON)
        *(.bss)
        _ebss = .;
    }
}

kmain.c:

#include <kernel/tty.h>
#include <kernel/log.h>
#include <kernel/stack-protector.h>
#include <kernel/gdt.h>

__attribute__ ((noreturn));
    void kmain(void) {
    gdtInstall();

    initializeTerminal();
    char c;
    char *buffer = &c;
    char *start = buffer;

    char str[] = "Hello, kernel World!";

    atomicallyLog(1, 1, str, buffer);
    kprintf(start, 1);
}
assembly
x86
paging
bootloader
osdev
asked on Stack Overflow Oct 5, 2018 by Marco Mer • edited Sep 16, 2019 by Michael Petch

1 Answer

2

If we assume for a moment that your code actually reaches lea StartInHigherHalf, %ecx, and don't think it actually executed this instruction - The most obvious issue would be paging related. The LEA instruction happens to be the first instruction to execute after the paging bit is set. If paging is wrong then likely the next instruction will fault.

Reviewing your page directory, I noticed that you build it using .quad (8 byte type) instead of .int (4-byte type). Each entry in the page directory is 4 bytes, not 8. This likely is the main cause of your problems. You can avoid the .rept macro by just using the .fill directive:

.fill repeat , size , value

result, size and value are absolute expressions. This emits repeat copies of size bytes. Repeat may be zero or more. Size may be zero or more, but if it is more than 8, then it is deemed to have the value 8, compatible with other people's assemblers. The contents of each repeat bytes is taken from an 8-byte number. The highest order 4 bytes are zero. The lowest order 4 bytes are value rendered in the byte-order of an integer on the computer as is assembling for. Each size bytes in a repetition is taken from the lowest order size bytes of this number. Again, this bizarre behavior is compatible with other people's assemblers.

size and value are optional. If the second comma and value are absent, value is assumed zero. If the first comma and following tokens are absent, size is assumed to be 1.

Your Boot Page Directory could be written as:

.align 0x1000
BootPageDirectory:    
.int 0x00000083    
.fill KERNEL_PAGE_NUMBER - 1, 4, 0    
.int 0x00000083
.fill 0x400 - KERNEL_PAGE_NUMBER - 1, 4, 0

.rept works as well:

.align 0x1000
BootPageDirectory:
.int 0x00000083    
.rept KERNEL_PAGE_NUMBER - 1
.int 0
.endr
.int 0x00000083
.rept 0x400 - KERNEL_PAGE_NUMBER - 1
.int 0
.endr

Other Suggestions

Your code seems to be a variant of the OSDev Higher Half x86 Bare Bones kernel. The main difference appears to be that you converted from NASM to GNU Assembler. I've written about a major deficiency of this code on the OSDev Forum. The way it is structured, all the addresses are generated assuming everything is higher half, rather than splitting the lower half that is at 0x100000 from what is in the higher half at 0xC0100000. This design leads to using Mulitboot in a way that isn't really defined by the Multiboot specification.

You can resolve this by using the linker script to separate the two and put appropriate sections in boot.S. A linker script (linker.ld) could look like:

ENTRY(setup)
OUTPUT_FORMAT(elf32-i386)

KERNEL_VIRTUAL_BASE = 0xC0000000;

SECTIONS {
   /* The multiboot data and code will exist in low memory
      starting at 0x100000 */

   . = 0x00100000;

   .lowerhalf ALIGN(0x1000) : {
       *(.lowerhalf.data)
       *(.lowerhalf.text)
   }

   /* The kernel will live at 3GB + 1MB in the virtual
      address space, which will be mapped to 1MB in the
      physical address space. */

   . += KERNEL_VIRTUAL_BASE;
   .text ALIGN(0x1000) : AT(ADDR(.text) - KERNEL_VIRTUAL_BASE) {
       *(.text)
   }

   .data ALIGN (0x1000) : AT(ADDR(.data) - KERNEL_VIRTUAL_BASE) {
       *(.data)
       *(.rodata*)
   }

   .bss ALIGN (0x1000) : AT(ADDR(.bss) - KERNEL_VIRTUAL_BASE) {
       _sbss = .;
       *(COMMON)
       *(.bss)
       _ebss = .;
   }

   /DISCARD/ : {
       *(.eh_frame);
       *(.comment*);
   }
}

Your boot.S file could be written like:

.set ALIGN,    1<<0
.set MEMINFO,  1<<1
.set FLAGS,    ALIGN | MEMINFO
.set MAGIC,    0x1BADB002
.set CHECKSUM, -(MAGIC + FLAGS)

.set KERNEL_VIRTUAL_BASE, 0xC0000000
.set KERNEL_PAGE_NUMBER, (KERNEL_VIRTUAL_BASE >> 22)

.section .lowerhalf.data,"aw",@progbits
.align 4
.long MAGIC
.long FLAGS
.long CHECKSUM

.align 0x1000
BootPageDirectory:
.int 0x00000083
.fill KERNEL_PAGE_NUMBER - 1, 4, 0    
.int 0x00000083
.fill 0x400 - KERNEL_PAGE_NUMBER - 1, 4, 0

.set STACKSIZE, 0x4000

.section .lowerhalf.text,"axw",@progbits
.global setup
setup:
    mov $BootPageDirectory, %ecx
    mov %ecx, %cr3

    mov %cr4, %ecx
    or $0x00000010, %ecx
    mov %ecx, %cr4

    mov %cr0, %ecx
    or $0x80000000, %ecx
    mov %ecx, %cr0

    jmp StartInHigherHalf

.section .text
StartInHigherHalf:
    movl $0, (BootPageDirectory)
    invlpg (0)

    mov $(stack + STACKSIZE), %esp
    push %eax

    push %ebx

    #call _init

    call kmain

    cli
1:  hlt
    jmp 1

/*
.global gdtFlush
.extern gp

gdtFlush:
    lgdt (gp)

    mov $0x10, %eax
    mov %eax, %ds
    mov %eax, %es
    mov %eax, %gs
    mov %eax, %fs
    mov %eax, %ss

    ljmp $0x08, $setcs

setcs:
    ret
*/

.section .bss
.align 32

.lcomm stack, STACKSIZE

A do nothing kernel (kernel.c) could look like:

volatile unsigned short int *const video_mem = (unsigned short int *)0xc00b8000;
void kmain(void) {
    /* print MDP in upper left of display using white on magenta */
    video_mem[0] = (0x57 << 8) | 'M';
    video_mem[1] = (0x57 << 8) | 'D';
    video_mem[2] = (0x57 << 8) | 'P';

    while (1) __asm__ ("hlt");
}

You can generate an ELF executable with:

i686-elf-gcc -c -g -m32 boot.S -o boot.o
i686-elf-gcc -c -g -m32 -O3 kernel.c -o kernel.o -ffreestanding -std=gnu99 \
    -mno-red-zone -fno-exceptions -Wall -Wextra

i686-elf-gcc -nostdlib -Wl,--build-id=none -T linker.ld boot.o kernel.o -lgcc -o kernel.elf

Comments:

  • I prefer using a GCC cross compiler, although you can use your native host compiler as well.
  • The file kernel.elf can be loaded by GRUB (in an ISO file) or directly with QEMU's -kernel option.
  • I simplified the JMP by jumping to the label StartInHigherHalf directly. You can still use the LEA / JMP method but it doesn't gain anything.
  • You have mistakenly placed gdtFlush in the .bss section. It needs to be moved into the .text section. I have done so in the code, but I have also commented it out to get this do nothing kernel working with paging. You will need to add it back in to work with your codebase.
  • I have temporarily commented out the call _init to get this do nothing kernel working. You will need to remove the comment to integrate it into your codebase.
answered on Stack Overflow Oct 6, 2018 by Michael Petch • edited Oct 7, 2018 by Michael Petch

User contributions licensed under CC BY-SA 3.0