Wednesday, August 31, 2022

[SOLVED] Why does my inline assembly code cause a triple fault?

Issue

I compile my code using GCC with the -masm=intel option. My kernel is loaded by a Multiboot loader like GRUB.

I want to load the address of my GDT and then I reload all segment registers but this causes a triple fault (virtual machine restarts). This code works if I use it in native assembly (in a .asm file).

gdt.c:

#include "gdt.h"

GDT gdtp;

uint64_t gdt[GDT_ENTRIES];

void set_gdt_entry(int x, unsigned int base, unsigned int limit, int flags) {
    gdt[x] = limit & 0xffffLL;
    gdt[x] |= (base & 0xffffffLL) << 16;
    gdt[x] |= ((flags >> 4) & 0xffLL) << 40;
    gdt[x] |= ((limit >> 16) & 0xfLL) << 48;
    gdt[x] |= (flags & 0xfLL) << 52;
    gdt[x] |= ((base >> 24) & 0xffLL) << 56;
}

void gdt_init() {
    gdtp.limit = GDT_ENTRIES * 8 - 1;
    gdtp.pointer = gdt;

    set_gdt_entry(0, 0, 0, 0);
    set_gdt_entry(1, 0, 0xFFFFFFFF, GDT_SIZE | GDT_EXECUTABLE | GDT_SEGMENT | GDT_PRESENT);
    set_gdt_entry(2, 0, 0xFFFFFFFF, GDT_SIZE | GDT_SEGMENT | GDT_PRESENT);
    set_gdt_entry(3, 0, 0xFFFFFFFF, GDT_SIZE | GDT_EXECUTABLE | GDT_SEGMENT | GDT_PRESENT | GDT_RING3);
    set_gdt_entry(4, 0, 0xFFFFFFFF, GDT_SIZE | GDT_SEGMENT | GDT_PRESENT | GDT_RING3);

    asm volatile(
        "lgdt %0\n"
        "mov eax, 0x10\n"
        "mov ss, eax\n"
        "mov es, eax\n"
        "mov ds, eax\n"
        "mov gs, eax\n"
        "mov fs, eax\n"
        "jmp 0x08:1f\n"
        "1:\n"
        : : "m" (gdtp) : "eax"
    );
}

This is my gdt.h:

#include <stdint.h>

#define GDT_ENTRIES 7

typedef enum {
    GDT_AVAILABLE = 0x1,
    GDT_LONG_MODE = 0x2,
    GDT_SIZE = 0x3,
    GDT_GRANULARITY = 0x8,
    GDT_ACCESSED = 0x010,
    GDT_READ_WRITE = 0x020,
    GDT_CONFORMING = 0x040,
    GDT_EXECUTABLE = 0x080,
    GDT_SEGMENT = 0x100,
    GDT_RING1 = 0x200,
    GDT_RING2 = 0x400,
    GDT_RING3 = 0x600,
    GDT_PRESENT = 0x800
} GDT_FLAGS;

typedef struct {
    uint16_t limit;
    void *pointer;
}__attribute__((packed)) GDT;

void set_gdt_entry(int, unsigned int, unsigned int, int);
void gdt_init();

What can I do to get it work?


Solution

The problem isn't in the inline assembly code, however there are the things I see wrong in the code snippets you added to the question:

  • This GDT_FLAGS entry:

      GDT_SIZE = 0x3
    

    Should be:

      GDT_SIZE = 0x4
    
  • You are using a Multiboot loader and you will be accessing memory above 0x100000. Your GDT entries do not have the GDT_GRANULARITY bit set so you are constrained to the lower 1MiB of memory. As well you haven't marked any of your descriptors with the GDT_READ_WRITE bit. The GDT initialization should be:

      void gdt_init() {
          gdtp.limit = GDT_ENTRIES * 8 - 1;
          gdtp.pointer = gdt;
    
          set_gdt_entry(0, 0, 0, 0);
          set_gdt_entry(1, 0, 0xFFFFFFFF, GDT_GRANULARITY | GDT_READ_WRITE \
                        | GDT_SIZE | GDT_EXECUTABLE | GDT_SEGMENT | GDT_PRESENT);
          set_gdt_entry(2, 0, 0xFFFFFFFF, GDT_GRANULARITY | GDT_READ_WRITE \
                        | GDT_SIZE | GDT_SEGMENT | GDT_PRESENT);
          set_gdt_entry(3, 0, 0xFFFFFFFF, GDT_GRANULARITY | GDT_READ_WRITE \
                        | GDT_SIZE | GDT_EXECUTABLE | GDT_SEGMENT | GDT_PRESENT | GDT_RING3);
          set_gdt_entry(4, 0, 0xFFFFFFFF, GDT_GRANULARITY | GDT_READ_WRITE \
                        | GDT_SIZE | GDT_SEGMENT | GDT_PRESENT | GDT_RING3);
    
          asm volatile(
              "lgdt %0\n"
              "mov eax, 0x10\n"
              "mov ss, eax\n"
              "mov es, eax\n"
              "mov ds, eax\n"
              "mov gs, eax\n"
              "mov fs, eax\n"
              "jmp 0x08:1f\n"
              "1:\n"
              : : "m" (gdtp) : "eax", "memory"
          );
      }
    

Recommendations

  • When debugging GDT code and interrupts early on in OS development, I find it useful to use the BOCHS emulator. It will dump processor state information when a problem occurs (ie. triple fault), and has info gdt and info idt commands that will dump these tables to the console. To use BOCHS for OS development you can generate an ISO image and boot as a CD-ROM.

  • You correctly coded your set_gdt_entry in such a way that only the lower 20 bits are used and the upper 12 bits are thrown away. To make things more readable, I recommend specifying the limit with a value between 0x00000 and 0xFFFFF (inclusive). When using GDT_GRANULARITY the limit value is shifted left 12 bits by the CPU and the lower 12 bits are set to 0xFFF. When GDT_GRANULARITY is set the limit is treated as 4KiB pages rather than bytes.

When GDT_GRANULARITY isn't set it the limit value is simply a 20-bit value between 0x00000 and 0xFFFFF specifying the limit as bytes rather than 4KiB pages.



Answered By - Michael Petch
Answer Checked By - Terry (WPSolving Volunteer)