Search code examples
assemblyx86nasmldbootloader

How do I properly set up a linker script with a boot loader?


I have a 2 stage bootloader and a c kernel function. I am trying to do this without using an initial file system.

The problem is that it's not making it to the second stage for entry and also not making it to the c function or the kernel. I put a char test in both the first and second stage, in order to test it by output, but its not printing the second stage char.

This lets me know there is an issue with the jmp from the first to the second stage because its not even making it to the first label in the second stage. There is also a print test in the c kernel function named _core and its not printing output either. Which of course it would not work, because control is lost before the first label in the second stage, so there's no reason to believe it would make it to the kernel call which is after.

The first stage is a regular .bin file and since I used the extern directive, the second stage, is an .o file. I separated them like its suppose to but I am not sure how everything is suppose to connect.

I mean the osboot.asm is getting assembled alone so there doesn't seem to be a connection to the linker, even doe I have a jmp problem, with no address to jmp to. Then how does the linker work, with the second stage .o file and the c file .o together, in order to call the kernel? What is the proper way of handling this?

I am assume the linker should be providing all the addresses needed.

osboot.asm - first stage

[BITS 16]
org 0x7C00

osboot:
     mov ah, 0x0e        ; BIOS teletype function
     mov al, 'B'         ; Character to print
     int 0x10            ; Call BIOS interrupt

     call read_osload

     jmp 0:0x7e00

%include "read_osload.inc"

times 510-($-$$) db 0       ; Limit the sector to 510 bytes
dw 0xAA55                   ; Boot signature and last 2 bytes of the first sector

osload.asm - second stage

[BITS 16]
section .osload
global _start

_start: jmp switch_pmode

%include "gdt.inc"

switch_pmode:
    mov ah, 0x0e        ; BIOS teletype function
    mov al, 'L'         ; Character to print
    int 0x10            ; Call BIOS interrupt

    cli
    lgdt [gdt_descriptor]

    mov eax, cr0
    or eax, 1
    mov cr0, eax

    jmp code_seg:p_mode

[BITS 32]
p_mode:
    mov ax, cs
    mov ds, ax
    mov ss, ax
    mov es, ax
    mov fs, ax
    mov gs, ax

    mov ebp, 0x90000
    mov esp, ebp

    [EXTERN _core]
    call _core
    hlt

core.c - kernel

void _core() 
{
    char* video_memory = (char*)0xb8000;

    video_memory[0] = 'E';
    video_memory[1] = 'L';
    video_memory[2] = 'L';
    video_memory[3] = 'E';

    video_memory[4] = 'O';
    video_memory[5] = 'S';
}

read_osload.inc

[BITS 16]

read_osload:
    mov ah, 0        ; reset the drive function
    mov dl, 0        ; drive 0 is the floppy drive
    int 0x13          ; call the BIOS interrupt to reset drive

    mov ax, 0x7E00   ; read address 0x7E00 so we can jmp to the second stage
    mov es, ax
    xor bx, bx

    mov ah, 0x02     ; read sector function
    mov al, 1        ; read 1 sector
    mov ch, 0        ; 0 = track 1  (18 sectors per track) no need to move from track 0
    mov cl, 2        ; read sector 2
    mov dh, 0        ; head number
    mov dl, 0        ; drive number. drive 0 is the floppy drive
    int 0x13          ; call the BIOS interrupt to read drive
    ret

gdt.inc

; null segment descriptor
gdt_start:
    dq 0x0

; code segment descriptor
gdt_code:
    dw 0xffff    ; segment length, bits 0-15
    dw 0x0       ; segment base, bits 0-15
    db 0x0       ; segment base, bits 16-23
    db 10011010b ; flags (8 bits)
    db 11001111b ; flags (4 bits) + segment length, bits 16-19
    db 0x08       ; segment base, bits 24-31

; data segment descriptor
gdt_data:
    dw 0xffff    ; segment length, bits 0-15
    dw 0x0       ; segment base, bits 0-15
    db 0x0       ; segment base, bits 16-23
    db 10010010b ; flags (8 bits)
    db 11001111b ; flags (4 bits) + segment length, bits 16-19
    db 0x10       ; segment base, bits 24-31

gdt_end:

; GDT descriptor
gdt_descriptor:
    dw gdt_end - gdt_start - 1 ; size (16 bit)
    dd gdt_start ; address (32 bit)

code_seg equ gdt_code - gdt_start
data_seg equ gdt_data - gdt_start

link.ld

ENTRY(_core)
OUTPUT_FORMAT("binary")
OUTPUT(core.bin)

SECTIONS
{
    . = 0x7C00;                  /* osboot.asm starts at 0x7C00*/
    .osboot : AT(0x7C00)
    {
        *(.text)
        *(.rodata)
        *(.data)
        _bss_start = .;
        *(.bss)
        *(COMMON)
        _bss_end = .;
    }

    . = 0x7E00;                 /* osload.asm starts at 0x7E00? we need a place to jmp to from osboot */
    .osload : AT(0x7E00)
    {
        *(.text)
        *(.rodata)
        *(.data)
        _bss_start = .;
        *(.bss)
        *(COMMON)
        _bss_end = .;
    }

    . = 0x1000;                /* core.c starts at 0x1000? we need a place to call kernel _core */
    ._core : AT(0x1000)
    {
        *(.text)
        *(.rodata)
        *(.data)
        _bss_start = .;
        *(.bss)
        *(COMMON)
        _bss_end = .;
    }

    kernel_sectors = (SIZEOF(._core) + 511) / 512;

    . = ALIGN(4); /* Ensure alignment between sections */

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

makefile

all: elle.flp

osboot.bin: osboot.asm
    nasm -f bin osboot.asm -o osboot.bin

osload.o: osload.asm
    nasm osload.asm -f elf32 -o osload.o

core.o: core.c
    gcc -m32 -fno-pie -Wall -ffreestanding --no-builtin -c core.c -o core.o

core.bin: core.o osload.o
    ld -T link.ld -melf_i386 -o core.bin core.o osload.o 

elle.flp: osboot.bin core.bin
    cat osboot.bin core.bin > elle.flp

clean:
    rm -f osboot.bin osload.o core.o load_core.o core.bin elle.flp

I have tried altering the link.ld in many different ways. I have tried altering the make file in many ways. I've tried compiling the osload.o and core.o, separately then combining them into a .bin, before linking them. I've tried compiling the osload.o and core.o separately then linking them separately.


Solution

  • It is possible to arrange the code using the linker script and have it place the boot signature in the appropriate place. The following linker script with some cleaned up code to fix a variety of bugs is below and assumes the kernel is loaded starting at physical address 0x8000. There were also issues with:

    • Your GDT has incorrect values for the segment bases (bits 24-31).
    • You only read one sector so none of the kernel is loaded
    • You read the sectors from the disk into the wrong memory address in ES:BX.
    • You didn't properly set up the segment registers at the beginning of your bootloader.
    • You used the code selector in protected mode to initialize data segments.
    • The drive number that was booted from is passed in register DL so you can use that instead of using drive 0 when doing disk accesses.

    link.ld:

    ENTRY(_core)
    OUTPUT_FORMAT(elf32-i386)
    
    SECTIONS
    {
        . = 0x7C00;                  /* osboot.asm starts at 0x7C00*/
        .osboot : AT(0x7C00) SUBALIGN(4)
        {
            osboot.o(.text)
            osboot.o(.*)
        }
    
        . = 0x7DFE;
        .bootsig : {
            SHORT(0xaa55);
        }
    
        . = 0x7E00;                 /* osload.asm starts at 0x7E00? we need a place to jmp to from osboot */
        .osload : AT(0x7E00) SUBALIGN(4)
        {
            osload.o(.text)
            osload.o(.*)
        }
    
        . = 0x8000;                /* core.c starts at 0x1000? we need a place to call kernel _core */
        ._core : SUBALIGN(4)
        {
            *(.text*)
            *(.rodata*)
            *(.data*)
        }
    
        .bss : SUBALIGN(4) {
            __bss_start = .;
            *(COMMON);
            *(.bss*);
        }
        . = ALIGN(4);              /* Round BSS up to 4 byte boundary */
        __bss_end = .;
    
        kernel_sectors = (SIZEOF(._core) + 511) / 512;
    
        /DISCARD/ : {
            *(.eh_frame)
            *(.comment)
        }
    }
    

    gdt.inc:

    ; null segment descriptor
    gdt_start:
        dq 0x0
    
    ; code segment descriptor
    gdt_code:
        dw 0xffff    ; segment length, bits 0-15
        dw 0x0       ; segment base, bits 0-15
        db 0x0       ; segment base, bits 16-23
        db 10011010b ; flags (8 bits)
        db 11001111b ; flags (4 bits) + segment length, bits 16-19
        db 0x00      ; segment base, bits 24-31
    
    ; data segment descriptor
    gdt_data:
        dw 0xffff    ; segment length, bits 0-15
        dw 0x0       ; segment base, bits 0-15
        db 0x0       ; segment base, bits 16-23
        db 10010010b ; flags (8 bits)
        db 11001111b ; flags (4 bits) + segment length, bits 16-19
        db 0x00       ; segment base, bits 24-31
    
    gdt_end:
    
    ; GDT descriptor
    gdt_descriptor:
        dw gdt_end - gdt_start - 1 ; size (16 bit)
        dd gdt_start ; address (32 bit)
    
    code_seg equ gdt_code - gdt_start
    data_seg equ gdt_data - gdt_start
    

    read_osload.inc:

    [BITS 16]
    
    read_osload:
        mov ah, 0        ; reset the drive function
    ;    mov dl, 0        ; drive in DL passed by the BIOS is the boot drive
        int 0x13          ; call the BIOS interrupt to reset drive
    
        xor ax, ax
        mov es, ax
        mov bx, 0x7E00   ; read address 0x7E00 so we can jmp to the second stage
    
        mov ah, 0x02     ; read sector function
        mov al, 15       ; read 15 sectors - this will need to be redone when the
                         ;     kernel gets larger
        mov ch, 0        ; 0 = track 1  (18 sectors per track) no need to move from track 0
        mov cl, 2        ; read sector 2
        mov dh, 0        ; head number
    ;    mov dl, 0        ; drive in DL passed by the BIOS is the boot drive
        int 0x13          ; call the BIOS interrupt to read drive
        ret
    

    osboot.asm:

    [BITS 16]
    extern stage2
    
    osboot:
         xor ax, ax          ; Initialize seg regsisters to 0 since
                             ; linker script will be placing bootloader at
                             ; org 0x7c00
         mov ds, ax
         mov es, ax
         mov ss, ax
         mov sp, 0x7c00      ; Place stack just below bootloader
                             ; at 0x0000:0x7c00
    
         mov ah, 0x0e        ; BIOS teletype function
         mov al, 'B'         ; Character to print
         int 0x10            ; Call BIOS interrupt
    
         call read_osload
    
         jmp 0:stage2
    
    %include "read_osload.inc"
    

    osload.asm:

    [BITS 16]
    global stage2
    
    stage2: jmp switch_pmode
    
    %include "gdt.inc"
    
    switch_pmode:
        mov ah, 0x0e        ; BIOS teletype function
        mov al, 'L'         ; Character to print
        int 0x10            ; Call BIOS interrupt
    
        cli
        lgdt [gdt_descriptor]
    
        mov eax, cr0
        or eax, 1
        mov cr0, eax
        jmp code_seg:p_mode
    
    [BITS 32]
    p_mode:
        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
    
        [EXTERN _core]
        call _core
        hlt
    

    core.c:

    void _core()
    {
        volatile char* video_memory = (volatile char*)0xb8000;
    
        video_memory[0] = 'E';
        video_memory[2] = 'L';
        video_memory[4] = 'L';
        video_memory[6] = 'E';
    
        video_memory[8] = 'O';
        video_memory[10] = 'S';
    }
    

    Makefile:

    all: elle.flp
    
    osboot.o: osboot.asm
            nasm -f elf32 osboot.asm -o osboot.o
    
    osload.o: osload.asm
            nasm osload.asm -f elf32 -o osload.o
    
    core.o: core.c
            gcc -m32 -fno-pie -Wall -ffreestanding --no-builtin -c core.c -o core.o
    
    core.bin: core.o osload.o osboot.o
            ld -T link.ld -melf_i386 -o core.elf core.o osload.o osboot.o
            objcopy -O binary core.elf core.bin
    
    elle.flp: core.bin
            dd if=/dev/zero of=elle.flp bs=1K count=1440
            dd if=core.bin of=elle.flp conv=notrunc
    
    clean:
            rm -f osboot.o osload.o core.o load_core.o core.bin elle.flp core.elf
    

    Notes

    • As your kernel grows in size, you will eventually have to use a more appropriate method of reading a larger number of sectors into memory. I have some example code that reads floppy media using logical block addressing in this Stackoverflow answer.
    • I have general bootloader tips and answers to a variety of boot related problems in this Stackoverflow answer.