Search code examples
cmakefilex86ldosdev

How to properly link 16 and 32 bit .o files?


I switched my computer recently and since then, my makefile chain spits out a 512 byte binary with only 0x00s or the bootloader, but without everything else. I created the following as MRE:

boot.asm:

BITS 16
SECTION boot
GLOBAL _entry
EXTERN _start

_entry:
mov [disk],dl
mov ah, 0x2 ; read sectors
mov al, 6   ; amount = 6
mov ch, 0   ; zylinder = 0
mov cl, 2   ; first sector to read = 2
mov dh, 0   ; head = 0 (up)
mov dl, [disk]  ; disk
mov bx, _start  ; segment:offset address
int 0x13

cli
lgdt [GDT_POINTER]

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

mov ax, DATA_SEGMENT
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
jmp CODE_SEGMENT:_start

disk: DB 0x00

GDT_POINTER:
DW GDT_EXIT - GDT_ENTRY
DD GDT_ENTRY

CODE_SEGMENT EQU GDT_CODE - GDT_ENTRY
DATA_SEGMENT EQU GDT_DATA - GDT_ENTRY

GDT_ENTRY:
DQ 0x00

GDT_CODE:
DW 0xffff
DW 0x0000
DB 0x00
DB 0x9a
DB 0xcf
DB 0x00
    
GDT_DATA:
DW 0xffff
DW 0x0000
DB 0x00
DB 0x92
DB 0xcf
DB 0x00

GDT_EXIT:

TIMES 510 - ($ - $$) DB 0x00
DW 0xAA55

kernel.c:

int _main() {
    while(1) {}
}

linker16.ld:

ENTRY(_entry);
OUTPUT_FORMAT(elf32-i386);
OUTPUT_ARCH(i386);
SECTIONS
{
    . = 0x7C00;

    .text : AT(0x7C00)
    {
        *(boot)
        *(.text)
    }
    
    .data :
    {
        *(.bss);
        *(.bss*);
        *(.data);
        *(.rodata*);
        *(COMMON);
    }  
    /DISCARD/ :
    {
        *(.note*);
        *(.iplt*);
        *(.igot*);
        *(.rel*);
        *(.comment);  
    }
}

linker32.ld:

ENTRY(_main);
OUTPUT_FORMAT(elf32-i386);
OUTPUT_ARCH(i386);
SECTIONS
{
    . = 0x7E00;

    .text : AT(0x7E00)
    {
        *(.text)
    }
    
    .data :
    {
        *(.bss);
        *(.bss*);
        *(.data);
        *(.rodata*);
        *(COMMON);
    }  
    /DISCARD/ :
    {
        *(.note*);
        *(.iplt*);
        *(.igot*);
        *(.rel*);
        *(.comment);  
    }
}

Makefile:

all:
    nasm -O32 -f elf -o boot.o boot.asm
    gcc -m32 -c -g -ffreestanding -nostdlib -nostdinc -Wall -Werror -o kernel.o kernel.c
    ld -static -nostdlib -build-id=none -relocatable -T linker16.ld -o boot.elf boot.o
    ld -static -nostdlib -build-id=none -relocatable -T linker32.ld -o kernel.elf kernel.o
    objcopy -O binary boot.elf boot.bin
    objcopy -O binary kernel.elf kernel.bin
    cat boot.bin kernel.bin > sys.bin~
    rm *.o
    rm *.elf
    rm *.bin
    cat sys.bin~ > sys.bin
    rm sys.bin~
    qemu-system-i386 sys.bin
    
    
qemu:
    qemu-system-i386 sys.bin

The expected output is a blank screen, with a GDT set a few bytes after 0x7C00 when looked into compat monitor ("info registers" output). Instead it is stuck in a bootloop, since the bootloader is correctly compiled but everything after it (the while loop) is missing. Until the .o file, everything is as expected but the .elf and .bin are too short. Does someone have a solution? The versions i use are:

NASM version 2.14.02
gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
GNU ld & objcopy (GNU Binutils for Ubuntu) 2.34

EDIT: The updated code instead produces a mess of zeros, 60 times the size it should be. The magic number is placed correctly but the kernel part is still unusable.

EDIT 2: I found out by trial and error that removing the -relocatable argument for the linker clears out most of the zeros, yet it still doesn't work as expected and sticks in a bootloop.

EDIT 3: If anyone gets the same problem as i did, i want the code to actually work. In the above code i fixed the GDT, since i made a mistake in it. I narrowed all DBs down to DD, but forgot that little endian reverses all bytes in it, therefore the used bit in all GDT descriptors was set to zero, making the jump impossible. In combination with fuz's answer, it is possible to get this nightmare running now.


Solution

  • There's quite a bit strange stuff going on with your program, so instead of trying to fix this, I'll go ahead and start from scratch with something correct.

    Your bootloader is mostly fine. As you already noticed, you cannot reference symbols from your kernel in your bootloader. The default solution is to just jump to a known location in your kernel (e.g. the beginning) and arrange things for the kernel to have its entry point there. So we change boot.asm and remove EXTERN _start, replacing it with

    _start  EQU 0x7e00
    

    To have the kernel reliably be enterable at 0x7e00, there is a trick. In the linker script, we put the following lines into the beginning of the .text section in linker32.ld:

    .text : AT(0x7E00)
    {
        _start = .;
        BYTE(0xE9);
        LONG(_main - _start - 5);
    

    This makes .text begin with a JMP instruction that jumps to _main, which is exactly what we want.

    Next is the issue of random junk being appended to the kernel. This is because you don't discard enough crap. The easiest way is to just discard everything (i.e. *(*)) and explicitly list the sections you want to keep. You need to be careful though; the compiler may decide to put extra junk into weird sections that is needed to keep the kernel working. Alternatively, accept that the compiler does whatever it wants and eat up the larger kernel size. The final linker script linker32.ld is this:

    OUTPUT_FORMAT(elf32-i386);
    OUTPUT_ARCH(i386);
    SECTIONS
    {
        . = 0x7E00;
    
        .text : AT(0x7E00)
        {
            _start = .;
            BYTE(0xE9);
            LONG(_main - _start - 5);
            *(.text);
            *(.text.*);
        }
        
        .data :
        {
            *(.bss);
            *(.bss*);
            *(.data);
            *(.rodata*);
            *(COMMON);
        }  
        /DISCARD/ :
        {
        *(*);
        }
    }
    

    You can fix the discarded sections similarly in linker16.ld.

    Next is the build script. I'll not discuss this in detail, but you can check the changes I made yourself. The two important ones are (a) removing -relocatable (this is absolutely not what you want) and (b) adding -fno-pic -no-pie so the compiler doesn't get any weird ideas.

    all:
        nasm -f elf32 boot.asm
        gcc -m32 -c -g -fno-pic -no-pie -ffreestanding -nostdlib -nostdinc -Wall -Werror -o kernel.o kernel.c
        ld -static -nostdlib -build-id=none -T linker16.ld -o boot.elf boot.o
        ld -static -nostdlib -build-id=none -T linker32.ld -o kernel.elf kernel.o
        objcopy -O binary boot.elf boot.bin
        objcopy -O binary kernel.elf kernel.bin
        cat boot.bin kernel.bin > sys.bin
        qemu-system-i386 sys.bin
    
    qemu:
        qemu-system-i386 sys.bin
    

    It should work like this, assuming the boot loader is correct (I don't have QEMU on this computer).