Search code examples
c++x86operating-systemvirtualboxi386

VirtualBox crashing when assembly "sti" is executed when testing Operating System


I'm currently attempting to build my own Operating System and have run into an issue when trying to test out my kernel code using VirtualBox.

The real issue arises when I call the assembly instruction sti as I'm currently attempting to implement an interrupt descriptor table and communicate with the PICs.

Here is the code that calls it. It's a function called kernel_main that is called from another assembly file. That file simply sets up the stack before executing any code from the OS, but there hasn't been any issues there, and everything works fine until I add the instruction asm("sti"); to the following code:

/*   main function of our kernal 
 *   accepts the pointer to multiboot and the magic code (no particular reason to take the magic number)
 * 
 *      use extern C to prevent gcc from changing the name
 */

extern "C" void kernel_main(void *multiboot_structure, uint32_t magic_number)
{

    // can't use the standard printf as we're outside an OS currently
    // we don't have access to glibc so we write our own printf
    printf_boot_message("kernel.....\n");

    // create the global descriptor table
    GlobalDescriptorTable gdt;

    // create the interrupt descriptor table
    InterruptHandler interrupt_handler(&gdt);

    // enable interrupts (test)
    asm("sti");    // <- causes crash 

    // random debug printf
    printf_boot_message("sti called\n");

    // kernal never really stops, inf loop
    while (1)
        ;
}

Below is the virtual box debug output, I've googled around for VINF_EM_TRIPLE_FAULT but mostly found RAM related issues that I don't think apply to me. The printf calls in the above code execute as expected followed by the VM immediately crashing stating the following:

Link to output as it's too large to post here: https://pastebin.com/jfPfhJUQ

Here is my interrupt handling code:

 * Implementations of the interrupt handling routines in sys_interrupts.h
 */

#include "sys_interrupts.h"
#include "misc.h"


//handle() is used to take the interrupt number, 
//i_number, and the address to the current CPU stack frame.
uint32_t InterruptHandler::handle(uint8_t i_number, uint32_t crnt_stkptr)
{

    // debug
    printf(" INTERRUPT");

    // after the interrupt code has been executed,
    // return the stack pointer so that the CPU can resume
    // where it left off.

    // this works for now as we do not have multiple
    // concurrent processes running, so there is no issue
    // of handling the threat number.
    return crnt_stkptr;
}


// define the global descriptor table
InterruptHandler::_gate_descriptor InterruptHandler::interrupt_desc_table[N_ENTRIES];



// define the constructor. Takes a pointer to the global 
// descriptor table
InterruptHandler::InterruptHandler(GlobalDescriptorTable* global_desc_table)
{

    // grab the offset of the usable memory within our global segment
    uint16_t seg = global_desc_table->CodeSegmentSelector();

    // set all the entries in the IDT to block request initially
    for (uint16_t i = 0; i < N_ENTRIES; i++)
    {
        // create an a gate for a system level interrupt, calling the block function (does nothing) using seg as its memory.
        create_entry(i, seg, &block_request, PRIV_LVL_KERNEL, GATE_INTERRUPT);
    }



    // create a couple interrupts for 0x00 and 0x01, really 0x20 and 0x21 in memory
    //create_entry(BASE_I_NUM + 0x00, seg, &isr0x00, PRIV_LVL_KERNEL, GATE_INTERRUPT);
    //create_entry(BASE_I_NUM + 0x01, seg, &isr0x01, PRIV_LVL_KERNEL, GATE_INTERRUPT);



    // init the PICs
    pic_controller.send_master_cmd(PIC_INIT);
    pic_controller.send_slave_cmd(PIC_INIT);

    // tell master pic to add 0x20 to any interrupt number it sends to CPU, while slave pic sends 0x28 + i_number
    pic_controller.send_master_data(PIC_OFFSET_MASTER);
    pic_controller.send_slave_data(PIC_OFFSET_SLAVE);


    // set the interrupt vectoring to cascade and tell master that there is a slave PIC at IRQ2
    pic_controller.send_master_data(ICW1_INTERVAL4);
    pic_controller.send_slave_data(ICW1_SINGLE);


    // set the PICs to work in 8086 mode
    pic_controller.send_master_data(ICW1_8086);
    pic_controller.send_slave_data(ICW1_8086);

    // send 0s
    pic_controller.send_master_data(DEFAULT_MASK);
    pic_controller.send_slave_data(DEFAULT_MASK);


    // tell the cpu to use the table
    interrupt_desc_table_pointerdata idt_ptr;

    //set the size
    idt_ptr.table_size = N_ENTRIES * sizeof(_gate_descriptor) - 1;

    // set the base address
    idt_ptr.base_addr = (uint32_t)interrupt_desc_table;

    // use lidt instruction to load the table 
    // the cpu will map interrupts to the table
    asm volatile("lidt %0" : : "m" (idt_ptr));
    
    
    // issue debug print
    printf_boot_message("   2: Created Interrupt Desc Table...\n");
}

// define the destructor of the class
InterruptHandler::~InterruptHandler()
{
}





// function to make entries in the IDT
// takes the interrupt number as an index, the segment offset it used to specify which memory segment to use
// a pointer to the function to call, the flags and access level.
void InterruptHandler::create_entry(uint8_t i_number, uint16_t segment_desc_offset, void (*isr)(), uint8_t priv_lvl, uint8_t desc_type)
{

    // set the i_number'th entry to the given params

    // take the lower bits of the pointer
    interrupt_desc_table[i_number].handler_lower_bits = ((uint32_t)isr) & 0xFFFF;
    
    // take the upper bits
    interrupt_desc_table[i_number].handler_upper_bits = (((uint32_t)isr) >> 16) & 0xFFFF;

    // calculate the privilage byte, setting the correct bits
    interrupt_desc_table[i_number].priv_lvl = 0x80 | ((priv_lvl & 3) << 5) | desc_type;


    interrupt_desc_table[i_number].segment_desc_offset = segment_desc_offset;


    // reserved byte is always 0
    interrupt_desc_table[i_number].reserved_byte = 0;

}



// need a function to block or ignore any requests
// that we dont want to service. Requests could be caused
// by devices we haven't yet configured when testing the os.
void InterruptHandler::block_request()
{
    // do nothing
}


// function to tell the CPU to send interrupts
// to this table
void InterruptHandler::set_active()
{

    // call sti assembly to start interrup poling at the CPU level
    asm volatile("sti");    // <- calling this crashes the kernel
    

    // issue debug print
    printf_boot_message("   4: Activated sys interrupts...\n");
}

And here is the code for my GDT, I followed the os dev wiki guide for this:

#include "global_desc_table.h"

/**
 * A code segment is identified by flag 0x9A, cannot write to a code segment
 * while a data segment is identified by flag 0x92
 * 
 * Based on the C code present on OSDEV Wiki
 */


GlobalDescriptorTable::GlobalDescriptorTable() : nullSegmentSelector(0, 0, 0),
                                                 unusedSegmentSelector(0, 0, 0),
                                                 codeSegmentSelector(0, 64*1024*1024, 0x9A),
                                                 dataSegmentSelector(0, 64*1024*1024, 0x92)
{
    
    //8 bytes defined, but processor expects 6 bytes only 
    uint32_t i[2];

    //first 4 bytes is address of table
    i[0] = (uint32_t)this;

    //second 4 bytes, the high bytes,  are size of global desc table
    i[1] = sizeof(GlobalDescriptorTable) << 16;


    // tell processor to use this table using its ldgt function
    asm volatile("lgdt (%0)" : : "p"  (((uint8_t *) i) + 2));


    // issue debug print
    printf_boot_message("   1: Created Global Desc Table...\n");
}

// function to get the offset of the datasegment selector
uint16_t GlobalDescriptorTable::DataSegmentSelector()
{
    // calculate the offset by subtracting the table's address from the datasegment's address
    return (uint8_t *) &dataSegmentSelector - (uint8_t*)this;
}

// function to get the offset of the code segment 
uint16_t GlobalDescriptorTable::CodeSegmentSelector()
{
    // calculate the offset by subtracting the table's address from the code segment's address
    return (uint8_t *) &codeSegmentSelector - (uint8_t*)this;
}

// default destructor
GlobalDescriptorTable::~GlobalDescriptorTable()
{
}

/**
 * The constructor to create a new entry segment, set the flags, determine the formatting for the limit, and set the base
 */
GlobalDescriptorTable::SegmentDescriptor::SegmentDescriptor(uint32_t base, uint32_t limit, uint8_t flags)
{
    uint8_t* target = (uint8_t*)this;

    //if 16 bit limit
    if (limit <= 65536)
    { 
        // tell processor that this is a 16bit entry
        target[6] = 0x40;
    
    } else {
        
        // if the last 12 bits of limit are not 1s
        if ((limit & 0xFFF) != 0xFFF)
        {
            limit = (limit >> 12) - 1;
            
        } else {
            limit >>= 12;
        }
        
        // indicate that there was a shift of 12 done
        target[6] = 0xC0;

    }

    // set the lower and upper 2 lowest bytes of limit
    target[0] = limit & 0xFF;
    target[1] = (limit >> 8) & 0xFF;

    //the rest of limit must go in lower 4 bit of byte 6, and byte 5
    target[6] |= (limit >> 16) & 0xF;

    //encode the pointer
    target[2] =  base & 0xFF;
    target[3] = (base >> 8) & 0xFF;
    target[4] = (base >> 16) & 0xFF;
    target[7] = (base >> 24) & 0xFF;

    // set the flags 
    target[5] = flags;
    
}


/**
 * Define the methods to get the base pointer from an segment and 
 * the limit for a segment, taken from os wiki
 */

uint32_t GlobalDescriptorTable::SegmentDescriptor::Base()
{
    // simply do the reverse of wht was done to place the pointer in
    
    uint8_t* target = (uint8_t*) this;
    uint32_t result = target[7];
    result = (result << 8) + target[4];
    result = (result << 8) + target[3];
    result = (result << 8) + target[2];

    return result;
}

uint32_t GlobalDescriptorTable::SegmentDescriptor::Limit()
{
    uint8_t* target = (uint8_t *)this;
    uint32_t result = target[6] & 0xF;
    result = (result << 8) + target[1];
    result = (result << 8) + target[0];
    
    //check if there was a shift of 12
    if (target[6] & 0xC0 == 0xC0)
    {
        result = (result << 12) & 0xFFF;
    }
    
    return result;
}

Solution

  •     i[0] = (uint32_t)this;
    
        //second 4 bytes, the high bytes,  are size of global desc table
        i[1] = sizeof(GlobalDescriptorTable) << 16;
    

    I've had the same problem, just swap the 0 and 1 in between:

        i[1] = (uint32_t)this;
        //second 4 bytes, the high bytes,  are size of global desc table
        i[0] = sizeof(GlobalDescriptorTable) << 16;
    

    That's the problem if you are following the same tutorial and I think you do if you came here.