2

I just started my first operating system (I'm a beginner in this field). I wanted to do this in both assembly and C, but I'm having trouble calling C functions in assembly code.

boot.asm:

BITS 32
global start
extern dmain

start: 
    call dmain
    
    jmp $

times 510 - ($- $$) db 0
dw 0xAA55

main.c:

#define NULL 0

void clear();

void clear() // clear entire screen
{
    char *mem = (char*)(0xb8000);
    while (*mem != NULL){
        *mem = NULL;
        mem++;
    }
}
void dmain(void *mbr, unsigned int magic) {
    clear();
}

Makefile:

main:
    gcc -m32 -ffreestanding -c main.c -o main.o
    nasm -f win32 boot.asm -o boot.o
    ld -m i386pe -T NUL -o main.tmp -Ttext 0x100000 boot.o main.o
    objcopy -O binary main.tmp main.img

    qemu-system-x86_64 main.img

This is my output:

gcc -m32 -ffreestanding -c main.c -o main.o
nasm -f win32 boot.asm -o boot.o
ld -m i386pe -T NUL -o main.tmp -Ttext 0x100000 boot.o main.o
boot.o:boot.asm:(.text+0x1): undefined reference to `dmain'
make: *** [Makefile:4: main] Error 1
  • 3
    Aside: I'd expect `while (*mem != NULL){ *mem = NULL; mem++; }` to be more like `while (mem != (char *) 0xC0000){ *mem = 0; mem++; }`. – chux - Reinstate Monica May 23 '23 at 11:46
  • @chux-ReinstateMonica The error is in assembly code, because the compiler doesn't find the 'dmain' function. – Federico Occhiochiuso May 23 '23 at 11:48
  • You have more than1 error. I've pointed out 1 and yes, your " doesn't find the 'dmain'" remains. – chux - Reinstate Monica May 23 '23 at 11:49
  • @chux-ReinstateMonica what mistakes? – Federico Occhiochiuso May 23 '23 at 11:51
  • 2
    Check the file names in the question. You show a file `assembly.asm` but the commands do not match this name. Please show the symbols of the object files: `nm boot.o` and `nm main.o`. You might have to prefix symbols with an underscore, see https://stackoverflow.com/q/62753691/10622916 – Bodo May 23 '23 at 12:05
  • 1
    OT: don't use `NULL` for anything else than null pointers. In this case you should use `0`: `*mem = NULL;` -> `*mem = 0;`. But anyway your `clear()` is wrong as pointed out by the first comment. – Jabberwocky May 23 '23 at 12:08
  • 1
    It's normally recommended to use an ELF toolchain for osdev stuff, not Windows PE object files. I don't know exactly what problems you'd run into, though, or if you can just use name-mangling (leading underscores) and calling-convention/ABI to match compilers that target Windows. – Peter Cordes May 23 '23 at 12:09
  • @PeterCordes I tried to change like this: nasm -f elf boot.asm -o boot.o But it doesn't work the same – Federico Occhiochiuso May 23 '23 at 12:24
  • 1
    What `gcc` version are you using? Is it making ELF `.o` files? If so, then I'd definitely recommend `ld -m elf_i386`. If your linker doesn't understand ELF object files, then like I said, you'd need an ELF toolchain (gcc and binutils) if you wanted to use ELF object files generated by NASM. – Peter Cordes May 23 '23 at 12:29
  • 1
    Unrelated to your linker error, but I hope this is a simplified version of your boot sector since you don't change into protected mode at all and don't pass any arguments either. – Jester May 23 '23 at 12:37
  • @PeterCordes like this: ld -m elf_i386 -T NUL -o main.tmp -Ttext 0x100000 boot.o main.o? – Federico Occhiochiuso May 23 '23 at 12:49

1 Answers1

4

If you want to write 32-bit code (as indicated by the BITS32), you will have to set up a protected mode environment. Right now, the CPU is in real mode and has to be put in protected mode.

I see that you are trying to link the boot sector and the boot loader executable into one file. This may be possible with a proper linker script, but GCC cannot fix-up 16-bit addresses if you are still in real mode and does not support segmented memory models, which means that protected mode is a necessity. You are also generating a PE executable with the boot sector, which will certainly not work. The BIOS will just load 512 bytes and jump to it (DL is set to the boot disk by the way).

A better way to do this is to build the boot loader as a separate binary and load it from the disk using INT 13h. Enter the second stage in a 32-bit protected mode environment with the global descriptor table set up for the flat model. The second stage loader can be a flat binary built to run at a specific load address.

Because all your addresses are going to be fixed up by the linker to wherever you want to load your second stage boot loader, it is necessary to subtract the base address when accessing memory outside the loader (e.g video memory).

#define LOAD_POINT 0x7E00 /* Right after the boot sector */
#define PHYS(a) (void*(a-LOAD_POINT))

Do note that if you decide to enter a protected mode environment (which is highly recommended when using GCC), BIOS services will NOT be available unless you set up virtual 8086 mode and a task state segment.

Entering the Protected Mode Environment

BITS 32 is an assembler directive and does not actually change the CPU state. You must set the first bit of CR0 to enter, load the GDT with flat model.

mov  eax,cr0
inc  ax
mov  cr0,eax
lgdt [gdtr]

Even then, you are still only in 16-bit protected mode. It is necessary to jump to the code segment which is marked as 32-bit.

The Global Descriptor Table

Each entry is 8 bytes long. The first is the NULL segment and must be zeroed. The GDT can be placed anywhere in memory. To load the GDT, we must set the GDTR with LGDT.

The following sample shows the relationship between the GDTR and the GDT, as well as flat model segments.

org 7C00h

_GDTR:
        DW      _GDT_end - _GDT - 1     ; GDT limit in bytes (highest offset)
        DD      _GDT                    ; Linear address of GDT
_GDT:
        DQ  0 ; NULL segment
        ; Flat code segment (selector 0x8)
        DB      0FFh,0FFh,0,0,0,1_00_11010b,11_001111b,0
        ; Flat data segment (selector 0x10)
        DB      0FFh,0FFh,0,0,0,1_00_10010b,11_001111b,0
_GDT_end:

The exact structure of the GDT is specified here: https://i.stack.imgur.com/KNZeO.gif

GDT entries are referenced with a selector. Bit 0 and 1 are the privilege level requested (zero for out purposes), bit 2 is GDT/LDT (also zero here for GDT). The rest is a 13-bit index to the table. Basically, the segments are referenced by simply shifting the index by three in this case.

The Interrupt Descriptor Table

If you want to receive interrupts in protected mode, the IDT is essential. It contains a code segment selector and a 32-bit address. This can be done in the boot loader environment.

Reprogramming the PIC to a proper base vector is required so that they do not collide with x86 exceptions as they do in real mode.

Here is a hardware specification of the 8259 PIC (the standard PC interrupt controller) https://stanislavs.org/helppc/8259.html

An example in C: https://wiki.osdev.org/8259_PIC#Code_Examples

Virtual 8086 Mode

Virtual 8086 is a complex topic, so I will give some brief details that should help avoid pitfalls. V86 mode allows for running real mode code in protected mode. Using it requires an IDT with at least a #GP handler.

V86 automatically sets the current privilege level to 3 for user. Any privileged instructions or the INT instruction will cause a general protection fault so that the monitor can emulate it. On the stack of the #GP, the saved instruction pointer points to the exact instruction that caused it, so be sure to increment it after emulating the instruction.

BIOS routines are sure to have IO instructions, so make sure that the TSS has an IO permission bitmap and set all the bits to zero so that all ports are allowed.

The TSS also has a field called ESP0 which is the stack pointer value that will be loaded to ESP when entering ring 3. This has to be set before entering ring 3 or V86. An assembly routine for entering V86 can set this to the current stack pointer.

See the OSDev article (which I have contributed to a while ago) about it for more specific details: https://wiki.osdev.org/Virtual_8086_Mode

Further Reading and Recommendations

Here is an excellent resource for the specifics of the IA32 architecture. This is the official Intel manual for the i386.

https://pdos.csail.mit.edu/6.828/2014/readings/i386/toc.htm

The following topics should be of interest:

  • Virtual 8086 mode
  • IDT/GDT/TSS
  • Exceptions
Sep Roland
  • 33,889
  • 7
  • 43
  • 76
Joey L. Q.
  • 41
  • 1
  • `gcc -m16` will make code that still uses 32-bit addresses but is assembled for 16-bit mode. So should work on a 386 or later. (The bootloader would still have to use `BITS 16`, and the compiler-generated code would have to be *in* the bootloader before the `0xaa55` MBR signature, or the MBR would have to load an extra sector.) – Peter Cordes May 23 '23 at 14:14
  • Qemu has a weird/non-standard "multi-boot stub" where it'll start a module as if the multiboot spec was followed (ie. as 32 bit code), bypassing all the normal stuff (BIOS or UEFI firmware, loading something like GRUB, ...). – Brendan May 23 '23 at 14:18