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.
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).