Real mode to protected mode inside the timer ISR

This rocks:

http://svn.navi.cx/misc/trunk/metalkit/lib/bios.c

/* -*- Mode: C; c-basic-offset: 3 -*-
 *
 * bios.c - Make real-mode BIOS calls from protected mode.
 *          For simplicity and small size, this implementation
 *          switches back to real-mode rather than using virtual 8086
 *          mode. A v86 mode implementation may be more robust.
 *
 * This file is part of Metalkit, a simple collection of modules for
 * writing software that runs on the bare metal. Get the latest code
 * at http://svn.navi.cx/misc/trunk/metalkit/
 *
 * Copyright (c) 2008-2009 Micah Dowty
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */

#include "bios.h"
#include "boot.h"
#include "intr.h"


/*
 * BIOSCallInternal --
 *
 *    Internal implementation of BIOS_Call. This is a C function which
 *    wraps the assembly-language internal implementation. The only
 *    reason to use C here, really, is so we can easily calculate
 *    offsets into our shared C structures.
 *
 *    This function must not make any function calls, since we need to
 *    be able to trust the value of %esp. This is also why it must not
 *    be inlined into BIOS_Call itself.
 */

static __attribute__((noinline)) void
BIOSCallInternal(void)
{
   /*
    * Save registers and stack in a safe place.
    */
   asm volatile ("pusha");
   asm volatile ("mov %%esp, %0" :"=m" (BIOS_SHARED->esp));

   /*
    * Jump the the relocated 16-bit trampoline (source code below).
    */
   asm volatile ("ljmp %0, %1"
                 :: "i" (BOOT_CODE16_SEG), "i" (BIOS_SHARED->trampoline));

   /*
    * This is where we return from the relocated trampoline.
    * We're back in protected mode, but the data segments
    * are still 16-bit. Restore them.
    */

   asm volatile ("BIOSReturn32: \n"
                 "mov %0, %%ax \n"
                 "mov %%ax, %%ss \n"
                 "mov %%ax, %%ds \n"
                 "mov %%ax, %%es \n"
                 "mov %%ax, %%fs \n"
                 "mov %%ax, %%gs \n"
                 :: "i" (BOOT_DATA_SEG));

   /*
    * Restore our stack and saved registers.
    * Now we can safely execute C code again.
    */

   asm volatile("mov %0, %%esp" ::"m" (BIOS_SHARED->esp));
   asm volatile ("popa");

   /*
    * Return here. The rest of this code is never run directly,
    * but we need to prevent GCC from optimizing it out.
    */
   asm volatile("jmp BIOSTrampolineEnd\n");

   /*
    * This is a 16-bit assembly-language trampoline, relocated at
    * runtime to low memory, which actually makes the BIOS call. It
    * handles saving/restoring registers, and it switches in and out
    * of real mode.
    *
    * This code is never run directly.
    */

   asm volatile("BIOSTrampoline: .code16");

    /*
     * Switch to our 16-bit data segment.
     */
   asm volatile("movw %0, %%ax \n"
                "movw %%ax, %%ds \n"
                "movw %%ax, %%es \n"
                "movw %%ax, %%ss \n"
                :: "i" (BOOT_DATA16_SEG));

   /*
    * Disable protected mode.
    */
   asm volatile("movl %cr0, %eax \n"
                "andl $(~1), %eax \n"
                "movl %eax, %cr0 \n");

   /*
    * Do another long jump to reset the real-mode %cs
    * register to a valid paragraph number. Right now
    * it's still a protected-mode-style selector index.
    *
    * XXX: I'm not sure how to do this address calculation cleanly.
    *      Currently I'm hardcoding the address of the relocated trampoline.
    */
   asm volatile("ljmp $0, $(BIOSTrampolineCS16 - BIOSTrampoline + 0x7C00)\n"
                "BIOSTrampolineCS16: \n");

   /*
    * Set up the real-mode stack and %cs register.
    */
   asm volatile("xorw %%ax, %%ax \n"
                "mov %%ax, %%ss \n"
                "mov %0, %%esp \n"
                :: "i" (&BIOS_SHARED->stackTop[-sizeof(Regs)]));

   /*
    * Pop Regs off the stack.
    */
   asm volatile("pop %ds \n"
                "pop %es \n"
                "pop %eax \n"   // Ignore EFLAGS value.
                "popal \n");

   /*
    * This interrupt instruction is a placeholder that gets
    * patched at runtime (after relocation) to point to the
    * right interrupt vector.
    */
   asm volatile("BIOSTrampolineVector: \n"
                "int $0xFF");

   /*
    * Some BIOS routines will turn interrupts back on. If an interrupt
    * occurs while we're partway through our 16-to-32 transition
    * below, any interrupts will cause the ISR to fault, so we must
    * make sure interrupts are still off.
    */
   asm volatile("cli");

   /*
    * Push Regs back onto the stack.
    */
   asm volatile("pushal \n"
                "pushfl \n"
                "push %es \n"
                "push %ds \n");

   /*
    * Enable protected mode.
    */
   asm volatile("movl %cr0, %eax \n"
                "orl $1, %eax \n"
                "movl %eax, %cr0 \n");

   /*
    * Return via a long 16-to-32 bit jump.
    */
   asm volatile("data32 ljmp %0, $BIOSReturn32 \n"
                :: "i" (BOOT_CODE_SEG));

   asm volatile("BIOSTrampolineEnd: .code32 \n");
}

extern struct {
   uint16 limit;
   uint32 base;
} PACKED IDTDesc;

/*
 * BIOS_Call --
 *
 *    Make BIOS calls after boot, by temporarily switching
 *    back into real mode.
 *
 *    This function relocates the trampoline and stack into
 *    real-mode-addressable low memory, then makes a 32-to-16-bit jump
 *    into the trampoline.
 */

fastcall void
BIOS_Call(uint8 vector, Regs *regs)
{
   extern uint8 BIOSTrampoline[];
   extern uint8 BIOSTrampolineVector[];
   extern uint8 BIOSTrampolineEnd[];
   const uint32 trampSize = (uint8*)BIOSTrampolineEnd - (uint8*)BIOSTrampoline;
   const uint32 vectorOffset = (uint8*)BIOSTrampolineVector - (uint8*)BIOSTrampoline + 1;

   Bool iFlag = Intr_Save();
   Intr_Disable();

   /*
    * Relocate the trampoline code itself.
    */
   memcpy(BIOS_SHARED->trampoline, BIOSTrampoline, trampSize);

   /*
    * Save the 32-bit IDT descriptor, and set up a legacy 256-entry
    * 16-bit IDT descriptor.
    */
   asm volatile("sidt %0" : "=m" (BIOS_SHARED->idtr32));
   BIOS_SHARED->idtr16.base = 0;
   BIOS_SHARED->idtr16.limit = 0x3ff;
   asm volatile("lidt %0" :: "m" (BIOS_SHARED->idtr16));

   /*
    * Binary-patch the trampoline code with the right interrupt vector.
    */
   BIOS_SHARED->trampoline[vectorOffset] = vector;

   /*
    * Copy Regs onto the top of the 16-bit stack.
    */
   memcpy(&BIOS_SHARED->stackTop[-sizeof *regs], regs, sizeof *regs);

   BIOSCallInternal();

   /* Copy Regs back */
   memcpy(regs, &BIOS_SHARED->stackTop[-sizeof *regs], sizeof *regs);

   /*
    * Back to 32-bit IDT.
    */
   asm volatile("lidt %0" :: "m" (BIOS_SHARED->idtr32));

   Intr_Restore(iFlag);
}

It’s the insane little trampoline I wrote last year in order to make real-mode BIOS calls from my toy protected mode OS, Metalkit. It’s full of all kinds of awesome and scary things.

So today, I just had occasion to try making a BIOS call from inside the timer interrupt, and it works! (Both in a VMware VM and on the physical laptop I tried it on.) Woohoo!

So now I have this spiffy little app that tests VESA BIOS palette manipulation:

http://svn.navi.cx/misc/trunk/metalkit/examples/vbe-palette/main.c

/* -*- Mode: C; c-basic-offset: 3 -*-
 *
 * Metalkit example: Demonstrate color palette manipulation in VBE mode.
 */

#include "vbe.h"
#include "console_vga.h"
#include "timer.h"
#include "intr.h"
#include "math.h"


/*
 * timerISR --
 *
 *    Timer interrupt handler, which animates the palette.
 *
 *    Just for fun, we'll actually use our palette as a low-res 16x16
 *    pixel true-color framebuffer, and well draw some abstract art.
 */

static void
timerISR(int vector)
{
   uint32 palette[256];
   static int tick = 0;
   int i;

   /*
    * Let the phase of each color channel slowly drift around.
    */
   const float rPhase = tick * 0.001;
   const float gPhase = tick * 0.002;
   const float bPhase = tick * 0.003;

   for (i = 0; i < 256; i++) {
      const int x = (i & 0x0F) - 3;
      const int y = (i >> 4) - 3;
      const float t = (x*x + y*y) * 0.05 + tick * 0.02;

      const uint8 r = sinf(t + rPhase) * 0x0f + 0x10;
      const uint8 g = sinf(t + gPhase) * 0x0f + 0x10;
      const uint8 b = sinf(t + bPhase) * 0x0f + 0x10;

      palette[i] = (r << 16) | (g << 8) | b;
   }

   VBE_SetPalette(0, 256, palette);
   tick++;
}


/*
 * drawTestPattern --
 *
 *    Visually present the palette as a 16x16 grid of boxes.
 */

static inline void
drawTestPattern(void)
{
   uint8 *fb = (uint8*) gVBE.current.info.linearAddress;
   int bytesPerLine = gVBE.current.info.bytesPerLine;
   int boxWidth = gVBE.current.info.width >> 4;
   int boxHeight = gVBE.current.info.height >> 4;
   int x, y;

   for (y = 0; y < gVBE.current.info.height; y++) {
      uint8 *line = fb;
      fb += bytesPerLine;

      for (x = 0; x < gVBE.current.info.width; x++) {
         int row = y / boxHeight;
         int col = x / boxWidth;
         int color = (row << 4) | col;

         *(line++) = color;
      }
   }
}


int
main(void)
{
   ConsoleVGA_Init();
   Intr_Init();
   Intr_SetFaultHandlers(Console_UnhandledFault);

   VBE_InitSimple(640, 480, 8);
   drawTestPattern();

   /*
    * Start our timer ISR, which will animate the palette at 60 Hz.
    */
   Timer_InitPIT(PIT_HZ / 60);
   Intr_SetMask(PIT_IRQ, TRUE);
   Intr_SetHandler(IRQ_VECTOR(PIT_IRQ),timerISR);

   /*
    * Sleep between interrupts.
    */
   while (1) {
      Intr_Halt();
   }

   return 0;
}

Here’s a screenshot of it running in VMware and QEMU. It doesn’t work correctly in QEMU. Not sure why yet- it could just be that their VESA BIOS doesn’t support command 0x09.

If you want to try it yourself (on a VM or a physical machine), here’s a 4 kB precompiled binary. You can either use it as a floppy disk image or a GRUB multiboot image.

(Yes, this is a great example of the sort of dorky thing that gets me excited on a regular basis ;)