All Articles

Reading xv6OS Thoroughly to Fully Understand the Kernel - Segment Descriptor Initialization Edition -

This page has been machine-translated from the original page.

Inspired by An Introduction to OS Code Reading: Learning Kernel Internals with UNIX V6, I’m reading xv6 OS.

Because UNIX V6 itself does not run on x86 CPUs, I decided to read the source of kash1064/xv6-public: xv6 OS, a fork of the xv6 OS repository that makes UNIX V6 run on the x86 architecture.

In the previous article, I looked at how the lapicinit function called from main configures the Local APIC.

This time, I will trace the behavior of the seginit function.

Table of Contents

The seginit function

The seginit function sets the kernel segment descriptors on the CPU.

seginit();       // segment descriptors

The seginit function is defined in vm.c as follows.

// Set up CPU's kernel segment descriptors.
// Run once on entry on each CPU.
void seginit(void)
{
  struct cpu *c;

  // Map "logical" addresses to virtual addresses using identity map.
  // Cannot share a CODE descriptor for both kernel and user
  // because it would have to have DPL_USR, but the CPU forbids
  // an interrupt from CPL=0 to DPL=3.
  c = &cpus[cpuid()];
  c->gdt[SEG_KCODE] = SEG(STA_X|STA_R, 0, 0xffffffff, 0);
  c->gdt[SEG_KDATA] = SEG(STA_W, 0, 0xffffffff, 0);
  c->gdt[SEG_UCODE] = SEG(STA_X|STA_R, 0, 0xffffffff, DPL_USER);
  c->gdt[SEG_UDATA] = SEG(STA_W, 0, 0xffffffff, DPL_USER);
  lgdt(c->gdt, sizeof(c->gdt));
}

Let’s read through the source code.

First, the cpu structure was defined in proc.h as follows.

// Per-CPU state
struct cpu {
  uchar apicid;                // Local APIC ID
  struct context *scheduler;   // swtch() here to enter scheduler
  struct taskstate ts;         // Used by x86 to find stack for interrupt
  struct segdesc gdt[NSEGS];   // x86 global descriptor table
  volatile uint started;       // Has the CPU started?
  int ncli;                    // Depth of pushcli nesting.
  int intena;                  // Were interrupts enabled before pushcli?
  struct proc *proc;           // The process running on this cpu or null
};

The cpu structure is stored in the array cpus.

As seen in the Multiprocessor Edition, this is an array supporting up to 8 CPUs, as set by NCPU defined in param.h.

Looking up cpus

Let’s look at the section that retrieves the cpu structure from the cpus array.

struct cpu *c;
c = &cpus[cpuid()];

The cpuid function is defined in proc.c as follows.

It returns the value obtained by subtracting cpus from the return value of mycpu (I wonder what exactly it’s doing…).

// Must be called with interrupts disabled
int cpuid() {
  return mycpu()-cpus;
}

// Must be called with interrupts disabled to avoid the caller being
// rescheduled between reading lapicid and running through the loop.
struct cpu* mycpu(void)
{
  int apicid, i;
  
  if(readeflags()&FL_IF) panic("mycpu called with interrupts enabled\n");
  
  apicid = lapicid();
  // APIC IDs are not guaranteed to be contiguous. Maybe we should have
  // a reverse map, or reserve a register to store &cpus[i].
  for (i = 0; i < ncpu; ++i) {
    if (cpus[i].apicid == apicid) return &cpus[i];
  }
  panic("unknown apicid\n");
}

The key part of mycpu is the following code.

apicid = lapicid();
// APIC IDs are not guaranteed to be contiguous. Maybe we should have
// a reverse map, or reserve a register to store &cpus[i].
for (i = 0; i < ncpu; ++i) {
  if (cpus[i].apicid == apicid) return &cpus[i];
}

The lapicid function is defined in lapic.c; it retrieves the APICID from the Local APIC and returns it after right-shifting by 24 bits.

int lapicid(void)
{
  if (!lapic) return 0;
  return lapic[ID] >> 24;
}

The variable lapic holds the address of the Local APIC, which was stored in mp.c as seen in the Multiprocessor Edition.

According to Intel’s multiprocessor specification (section 5-1), the Local APIC is placed at the base memory address 0x0FEE00000, and Local APIC IDs are assigned consecutively starting from 0 on the hardware.

Reference: INTEL MULTIPROCESSOR SPECIFICATION Pdf Download | ManualsLib

When I confirmed this in the debugger, the value of lapic[ID] was 0 on the first call.

Therefore the return value of lapicid is also 0.

This means apicid = lapicid(); stores 0 in apicid.

I also confirmed with the debugger that cpus[i].apicid is also 0 and matches apicid, so &cpus[i] returns &cpus[0].

for (i = 0; i < ncpu; ++i) {
  if (cpus[i].apicid == apicid) return &cpus[i];
}

Therefore return mycpu()-cpus; is also 0, and I confirmed that the return value of the first cpuid call is 0.

This means c = &cpus[cpuid()]; becomes c = &cpus[0]; on the first run.

Setting the GDT

So the following lines set values in the gdt[NSEGS] element of &cpus[0].

// Map "logical" addresses to virtual addresses using identity map.
// Cannot share a CODE descriptor for both kernel and user
// because it would have to have DPL_USR, but the CPU forbids
// an interrupt from CPL=0 to DPL=3.
c = &cpus[cpuid()];
c->gdt[SEG_KCODE] = SEG(STA_X|STA_R, 0, 0xffffffff, 0);
c->gdt[SEG_KDATA] = SEG(STA_W, 0, 0xffffffff, 0);
c->gdt[SEG_UCODE] = SEG(STA_X|STA_R, 0, 0xffffffff, DPL_USER);
c->gdt[SEG_UDATA] = SEG(STA_W, 0, 0xffffffff, DPL_USER);
lgdt(c->gdt, sizeof(c->gdt));

&cpus[0] is the cpu structure described above, and gdt is defined as follows.

struct segdesc gdt[NSEGS];   // x86 global descriptor table

The segdesc structure is a structure defined in mmu.h.

#ifndef __ASSEMBLER__
// Segment Descriptor
struct segdesc {
  uint lim_15_0 : 16;  // Low bits of segment limit
  uint base_15_0 : 16; // Low bits of segment base address
  uint base_23_16 : 8; // Middle bits of segment base address
  uint type : 4;       // Segment type (see STS_ constants)
  uint s : 1;          // 0 = system, 1 = application
  uint dpl : 2;        // Descriptor Privilege Level
  uint p : 1;          // Present
  uint lim_19_16 : 4;  // High bits of segment limit
  uint avl : 1;        // Unused (available for software use)
  uint rsv1 : 1;       // Reserved
  uint db : 1;         // 0 = 16-bit segment, 1 = 32-bit segment
  uint g : 1;          // Granularity: limit scaled by 4K when set
  uint base_31_24 : 8; // High bits of segment base address
};

Incidentally, NSEGS is also defined as the constant 6 in mmu.h.

// cpu->gdt[NSEGS] holds the above segments.
#define NSEGS     6

The segdesc structure defined here is a segment descriptor.

2022/02/image-4.png

Reference image: Intel SDM vol3

A segment descriptor is the data structure that serves as an entry in the GDT and LDT, which I briefly touched on in Notes on x86 CPU memory protection mechanisms (GDT and LDT).

A segment descriptor notifies the CPU of the segment’s size, address, access permissions, and state.

On x86 CPUs, this mechanism is used to implement memory protection.

The segment selectors such as SEG_KCODE and the permissions assigned to them were also confirmed when reading the bootstrap, so I will omit them in this article.

Reference: Reading xv6OS Thoroughly to Fully Understand the Kernel - Bootstrap Edition -

Summary

I have initialized the segment descriptors on the kernel side.

Next time, I will start with the picinit function…

Reference Books