Kernel Implementation: Housekeeping Tasks

There are a few small housekeeping tasks we’ve avoided for a while, but we should now deal with them to make some of our later implementation work go smoothly. I’ll provided code for these tasks so they don’t take too much time, but you will need to understand the code well enough to adopt them in your kernel implementation.

Replacing term_write

We’ve been using the bootloader’s provided terminal code to print things to screen, but this isn’t going to work long-term. The first issue is that the bootloader’s terminal code is in the lower half of the address space, which we are going to use for user programs. A deeper issue is that the terminal printing code actually runs in 32-bit mode instead of 64-bit mode; it works just fine now, but we will be making some changes later that would be more complex if we had to maintain support for running 32-bit code.

Luckily, printing characters to screen is not too complicated; there is a special region of memory (physical address 0xB8000) that controls what text displays on the screen. The terminal, which is 80 characters wide and 25 characters tall, uses an 4000-byte block of memory, with half the bytes set to control what character displays, and the other half to set the foreground and background colors for that text. You can use the code below to write to this terminal:

#define VGA_BUFFER 0xB8000
#define VGA_WIDTH 80
#define VGA_HEIGHT 25

#define VGA_COLOR_BLACK 0
#define VGA_COLOR_BLUE 1
#define VGA_COLOR_GREEN 2
#define VGA_COLOR_CYAN 3
#define VGA_COLOR_RED 4
#define VGA_COLOR_MAGENTA 5
#define VGA_COLOR_BROWN 6
#define VGA_COLOR_LIGHT_GREY 7
#define VGA_COLOR_DARK_GREY 8
#define VGA_COLOR_LIGHT_BLUE 9
#define VGA_COLOR_LIGHT_GREEN 10
#define VGA_COLOR_LIGHT_CYAN 11
#define VGA_COLOR_LIGHT_RED 12
#define VGA_COLOR_LIGHT_MAGENTA 13
#define VGA_COLOR_LIGHT_BROWN 14
#define VGA_COLOR_WHITE 15

// Struct representing a single character entry in the VGA buffer
typedef struct vga_entry {
  uint8_t c;
  uint8_t fg : 4;
  uint8_t bg : 4;
} __attribute__((packed)) vga_entry_t;

// A pointer to the VGA buffer
vga_entry_t* term;

// The current cursor position in the terminal
size_t term_col = 0;
size_t term_row = 0;

// Turn on the VGA cursor
void term_enable_cursor() {
  // Set starting scaline to 13 (three up from bottom)
  outb(0x3D4, 0x0A);
  outb(0x3D5, (inb(0x3D5) & 0xC0) | 13);
 
  // Set ending scanline to 15 (bottom)
  outb(0x3D4, 0x0B);
  outb(0x3D5, (inb(0x3D5) & 0xE0) | 15);
}

// Update the VGA cursor
void term_update_cursor() {
  uint16_t pos = term_row * VGA_WIDTH + term_col;
 
  outb(0x3D4, 0x0F);
  outb(0x3D5, (uint8_t) (pos & 0xFF));
  outb(0x3D4, 0x0E);
  outb(0x3D5, (uint8_t) ((pos >> 8) & 0xFF));
}

// Clear the terminal
void term_clear() {
  // Clear the terminal
  for (size_t i = 0; i < VGA_WIDTH * VGA_HEIGHT; i++) {
    term[i].c = ' ';
    term[i].bg = VGA_COLOR_BLACK;
    term[i].fg = VGA_COLOR_WHITE;
  }

  term_col = 0;
  term_row = 0;

  term_update_cursor();
}

// Write one character to the terminal
void term_putchar(char c) {
  // Handle characters that do not consume extra space (no scrolling necessary)
  if (c == '\r') {
    term_col = 0;
    term_update_cursor();
    return;

  } else if (c == '\b') {
    if (term_col > 0) {
      term_col--;
      term[term_row * VGA_WIDTH + term_col].c = ' ';
    }
    term_update_cursor();
    return;
  }

  // Handle newline
  if (c == '\n') {
    term_col = 0;
    term_row++;
  }

  // Wrap if needed
  if (term_col == VGA_WIDTH) {
    term_col = 0;
    term_row++;
  }

  // Scroll if needed
  if (term_row == VGA_HEIGHT) {
    // Shift characters up a row
    memcpy(term, &term[VGA_WIDTH], sizeof(vga_entry_t) * VGA_WIDTH * (VGA_HEIGHT - 1));
    term_row--;
    
    // Clear the last row
    for (size_t i=0; i<VGA_WIDTH; i++) {
      size_t index = i + term_row * VGA_WIDTH;
      term[index].c = ' ';
      term[index].fg = VGA_COLOR_WHITE;
      term[index].bg = VGA_COLOR_BLACK;
    }
  }

  // Write the character, unless it's a newline
  if (c != '\n') {
    size_t index = term_col + term_row * VGA_WIDTH;
    term[index].c = c;
    term[index].fg = VGA_COLOR_WHITE;
    term[index].bg = VGA_COLOR_BLACK;
    term_col++;
  }

  term_update_cursor();
}

// Initialize the terminal
void term_init() {
  // Get a usable pointer to the VGA text mode buffer
  term = ptov(VGA_BUFFER);

  term_enable_cursor();
  term_clear();
}

This code should replace the term_write function we received from the bootloader; you’ll need to update your kprint functions to use term_putchar instead of term_write. Make sure you call term_init before printing anything or you will end up crashing your kernel.

Note that the ptov function in my code converts a physical address to a usable virtual address in the higher-half direct map. You’ll need to edit the code to call your physical-to-virtual conversion function. Make sure that function is usable before you call term_init()! You may have to change the order things are initialized in your _start function to make sure the HHDM pointer is set before initializing the terminal, and the terminal is initialized before you print anything.

Unmap all lower-half pages

The next housekeeping task we’ll tackle is to unmap all pages in the lower half of our kernel’s address space. We’re going to reserve this area for user code and data, so we don’t want the kernel to use any of it. To do this correctly, you’re going to need to make sure your page table entry struct includes some additional fields we did not discuss in class. Here is the updated struct I am using, which includes some bit fields we grouped together as “unused” space:

typedef struct page_table_entry {
  bool present : 1;
  bool writable : 1;
  bool user : 1;
  bool write_through : 1;
  bool cache_disable : 1;
  bool accessed : 1;
  bool dirty : 1;
  bool page_size : 1;
  uint8_t _unused0 : 4;
  uintptr_t address : 40;
  uint16_t _unused1 : 11;
  bool no_execute : 1;
} __attribute__((packed)) pt_entry_t;

The most important field you should make sure you include is the page_size field. If this bit is set to one, the page table provides a page larger than 4KB; setting this bit to one in the level 2 page table creates a 2MB page (the page offset occupies the final 21 bits of the address), and setting this bit to one in the level 3 page table creates a 1GB page (the page offset grows to 30 bits).

Once you’ve updated that struct in your implementation you can add the following code to your project. I run this code right after adding all the free physical memory to the physical page freelist; it’s important that this code runs after you’ve set up your higher-half direct map pointer. Just as in the example above, this code depents on ptov, which converts a physical address to a usable virtual address in the higher-half direct map.

// Unmap everything in the lower half of an address space with level 4 page table at address root
void unmap_lower_half(uintptr_t root)
  // We can reclaim memory used to hold page tables, but NOT the mapped pages
  pt_entry_t* l4_table = ptov(root);
  for (size_t l4_index = 0; l4_index < 256; l4_index++) {

    // Does this entry point to a level 3 table?
    if (l4_table[l4_index].present) {

      // Yes. Mark the entry as not present in the level 4 table
      l4_table[l4_index].present = false;

      // Now loop over the level 3 table
      pt_entry_t* l3_table = ptov(l4_table[l4_index].address << 12);
      for (size_t l3_index = 0; l3_index < 512; l3_index++) {

        // Does this entry point to a level 2 table?
        if (l3_table[l3_index].present && !l3_table[l3_index].page_size) {

          // Yes. Loop over the level 2 table
          pt_entry_t* l2_table = ptov(l3_table[l3_index].address << 12);
          for (size_t l2_index = 0; l2_index < 512; l2_index++) {

            // Does this entry point to a level 1 table?
            if (l2_table[l2_index].present && !l2_table[l2_index].page_size) {

              // Yes. Free the physical page the holds the level 1 table
              pmem_free(l2_table[l2_index].address << 12);
            }
          }

          // Free the physical page that held the level 2 table
          pmem_free(l3_table[l3_index].address << 12);
        }
      }

      // Free the physical page that held the level 3 table
      pmem_free(l4_table[l4_index].address << 12);
    }
  }

  // Reload CR3 to flush any cached address translations
  write_cr3(read_cr3());
}

You may already have these functions, but here are the implementations for read_cr3 and `write_cr3 just in case:

uintptr_t read_cr3() {
  uintptr_t value;
  __asm__("mov %%cr3, %0" : "=r" (value));
  return value;
}

void write_cr3(uint64_t value) {
  __asm__("mov %0, %%cr3" : : "r" (value));
}

Once you make this change you may find that your kernel crashes or fails with a page fault. This will happen if you use physical addresses directly in your code instead of translating them up to the higher-half direct map. Any time you access data startin with its physical address you will need to conver that physical address to a usable virtual address in the higher-half direct map.

Invalidating cached address translations

Finally, we have a missing step we need to deal with in our vm_map, vm_protect, and vm_unmap functions. The processor has a special cache called a Translation Lookaside Buffer (TLB) that remembers translations from virtual addresses to physical addresses. Any time you modify a page table structure it is a good idea to notify the TLB that it should discard any cached translations for the mapping you modified. The code above does this for the whole address space by reloading the cr3 register, which is what points to the top-level page table. But when we modify a small part of an address space we can invalidate TLB entries with finer granularity. To do this, call the following function with the virtual address you’ve modified in each of your functions that modify the page table structure:

void invalidate_tlb(uintptr_t virtual_address) {
   __asm__("invlpg (%0)" :: "r" (virtual_address) : "memory");
}

You likely won’t see any change in behavior when you add this to your vm_map function (that seems to work even without this step) but if you’ve implemented vm_unmap you’ve likely noticed that you can access unmapped pages without causing a page fault; this should fix that issue.