<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://twicy.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://twicy.github.io/" rel="alternate" type="text/html" /><updated>2026-10-25T21:42:22-07:00</updated><id>https://twicy.github.io/feed.xml</id><title type="html">Yuchen Tang</title><subtitle>Projects, publications and blog posts on systems, networking and GPU programming.</subtitle><author><name>Yuchen Tang</name></author><entry><title type="html">Paper Notes: OSDI’26</title><link href="https://twicy.github.io/blogs/osdi-2026-notes.html" rel="alternate" type="text/html" title="Paper Notes: OSDI’26" /><published>2026-10-21T00:00:00-07:00</published><updated>2026-10-21T00:00:00-07:00</updated><id>https://twicy.github.io/blogs/osdi-2026-notes</id><content type="html" xml:base="https://twicy.github.io/blogs/osdi-2026-notes.html"><![CDATA[<h1 id="paper-notes-osdi26">Paper Notes: OSDI’26</h1>

<p>Notes on the OSDI’26 papers closest to my own work. I add papers as I read them.</p>

<h2 id="RamRyder">Break on Through to the Other Side: Pooling Memory Elastically with RamRyder</h2>

<p class="paper-meta"><em>Yanbo Zhou and Steven Swanson (UC San Diego), with Shanghai Jiao Tong University and Samsung Semiconductor</em> · <a href="https://www.usenix.org/system/files/osdi26-zhou-yanbo.pdf">Paper</a> · <a href="https://github.com/ramryder-project/ramryder">Code</a></p>

<ul class="paper-tags">
  <li>CXL</li>
  <li>Memory tiering</li>
  <li>Virtualization</li>
</ul>

<p><strong>In one sentence.</strong></p>

<p>Memory bandwidth should be allocated as a cloud resource in its own right, just like CPU and memory capacity.</p>

<p><strong>Background.</strong></p>

<ul>
  <li>Memory allocation in the cloud is not flexible enough: vendors allocate memory capacity in a fixed ratio to vCPUs and offer no way to allocate bandwidth. The authors’ analysis of Alibaba’s cluster traces shows that 90% of servers exhibit average bandwidth utilization below 44.5%.</li>
  <li>Underutilized bandwidth is not necessarily correlated with underutilized capacity, because they exhibit different spatial and temporal patterns.</li>
</ul>

<p><strong>Approach.</strong></p>

<ul>
  <li>RamRyder is a software-defined elastic memory system for cloud VMs that adjusts capacity and bandwidth dynamically, by managing memory at channel granularity.</li>
  <li>Components:
    <ul>
      <li>Host user space resource manager: channel allocation</li>
      <li>Hypervisor: virtualize memory channels</li>
      <li>Modified guest kernel: page-to-channel allocation policies</li>
    </ul>
  </li>
</ul>

<p><strong>Evaluation.</strong></p>

<ul>
  <li>Implemented on unmodified commodity servers
    <ul>
      <li>Disable existing DIMM channel interleaving via BIOS/UEFI</li>
      <li>CXL devices reserved as a separate DAX device in user space with CXL configuration tools</li>
    </ul>
  </li>
  <li>Test setup:
    <ul>
      <li>AMD EPYC Zen 5 with 12 DDR5 channels per socket; each VM’s vCPUs are pinned to their own CCXs, so VMs do not share an L3 cache</li>
      <li>8 of the 12 channels in one socket are used, each populated with one 32 GB DDR5 DIMM, to match compute-optimized cloud instances</li>
      <li>4 × 256 GB Samsung CXL 2.0 memory devices</li>
      <li>Debian with Linux 6.15</li>
    </ul>
  </li>
  <li>RamRyder improves average capacity and bandwidth utilization by 28.6% and 43.2%, respectively, across the cluster while delivering performance comparable to the best case with exclusive access</li>
</ul>

<p><strong>Food for thought.</strong></p>

<ul>
  <li>Limitations: the mapping from physical addresses to memory channels is normally hidden from system software, so deployment means turning off channel interleaving in BIOS/UEFI (a boot-time setting), and the guest kernel must be modified</li>
</ul>

<h2 id="NEMO">Finding NEMO: Nimble and Expressive Memory Observability</h2>

<p class="paper-meta"><em>Shihang Li, Matthew Giordano and Simon Peter (University of Washington), with Meta and Microsoft Azure</em> · <a href="https://www.usenix.org/system/files/osdi26-li-shihang.pdf">Paper</a> · <a href="https://github.com/vic-lsh/nemo">Code</a></p>

<ul class="paper-tags">
  <li>CXL</li>
  <li>Memory tiering</li>
</ul>

<p><strong>In one sentence.</strong></p>

<p>A small, programmable telemetry engine in the memory controller lets the OS count exactly the memory accesses it cares about, at the granularity it chooses, without spending CPU time.</p>

<p><strong>Background.</strong></p>

<ul>
  <li>A long-standing need for easy and efficient hardware and software mechanisms that can track memory usage on multi-tier memory systems.</li>
  <li>Existing mechanisms:
    <ul>
      <li>page level access bit tracking, expensive</li>
      <li>hardware cache miss sampling, reduced precision (HeMem)</li>
    </ul>
  </li>
</ul>

<p><strong>Approach.</strong></p>

<ul>
  <li>NEMO, a hardware-software codesigned memory telemetry engine
    <ul>
      <li>On the hardware side, NEMO is a lightweight extension to memory controllers (MCs)</li>
      <li>On the software side, NEMO exposes an OS-facing programming model and Linux driver</li>
    </ul>
  </li>
  <li>MCs already host QoS and monitoring features; the NEMO pipeline runs alongside, rather than on, the memory datapath.</li>
  <li>To support different granularities of monitoring (cache line to multi-GiB regions), the “match stage” masks and shifts the request’s physical address with OS-set parameters (simple fixed-latency arithmetic, not a hash) and looks the result up in a small translation table to find the counter. For page-hotness tracking, a 2 MiB page granularity is adopted</li>
</ul>

<p><strong>Evaluation.</strong></p>

<ul>
  <li>Altera Agilex 7 FPGA I-Series Development Kit with CXL 2.0 Type-3 support in hard IP</li>
  <li>SRAM per NEMO pipeline: ~150 KiB</li>
  <li>Across the full bitstream, NEMO uses 4.4% of the FPGA’s M20K SRAM and 4.8% of its logic</li>
  <li>Speeds up HeMem’s reaction to hot-set changes by 5×, accelerates THP splitting in MEMTIS by 10.4×, and detects noisy neighbors in Linux with 350× lower overhead. These telemetry improvements yield up to 1.7× higher throughput and 23% lower latency across key-value stores and databases</li>
</ul>

<p><strong>Food for thought.</strong></p>

<p>This paper overlaps a lot with my past work.</p>

<ul>
  <li>One data point from my own experience: scanning the page table (test-and-clear of the accessed bits) for 1 GB of address space (one VMA of 4 KiB pages, memset first so that every PTE really exists) takes about 1 ms, so 1 TB would take about 1 s, long enough for the accessed bits to go stale. And as the authors point out, one bit per page is too coarse-grained.</li>
  <li>2 MiB granularity is a common, wise and practical choice for page temperature tracking.</li>
  <li>The memory controller is a compelling place for hardware event monitoring. The prototype already sits in a CXL memory expander’s controller; I would imagine future CXL devices shipping such engines to hint the OS and other system software.</li>
</ul>

<h2 id="OLDDOG">Teaching The Old Dog New Tricks: Building Efficient Data Pipelines for Large-Scale LLM Pre-training (Operational Systems)</h2>

<p class="paper-meta"><em>Luofan Chen and Chenhan Wang (USTC and ByteDance Seed) and Cheng Li (USTC), with ByteDance Seed, ByteDance and Tsinghua University</em> · <a href="https://www.usenix.org/system/files/osdi26-chen-luofan.pdf">Paper</a></p>

<ul class="paper-tags">
  <li>Storage</li>
  <li>LLM training</li>
</ul>

<p><strong>In one sentence.</strong></p>

<p>ML workloads usually have fixed ranks and predictable access patterns, which creates room for resource pre-allocation and optimizations.</p>

<p><strong>Background.</strong></p>

<ul>
  <li>Pre-training data pipeline:
    <ul>
      <li><em>initialization phase</em>: The training framework issues blocking operations that load full model checkpoints, distributed and replicated across thousands of ranks</li>
      <li><em>iterative training phase</em>: The dataloader prefetches and pre-processes data for forward passes and backward passes. The training framework periodically persists checkpoints to storage</li>
      <li><em>companion evaluation phase</em>: Each evaluation cycle loads the latest checkpoint from storage, executes inference and writes back results to storage</li>
    </ul>
  </li>
  <li>Storage challenges:
    <ul>
      <li><em>companion evaluation phase</em> is blocked by cross-DC I/O</li>
      <li><em>initialization phase</em> is bottlenecked by an “I/O storm” where synchronous checkpoint loading triggers massive data contention on a small set of hot files</li>
      <li>data loading shifts from being I/O-bound to compute-bound due to the high cost of transforming video and image data</li>
    </ul>
  </li>
</ul>

<p><strong>Approach.</strong></p>

<ul>
  <li>Predictive checkpoint replication
    <ul>
      <li>Companion evaluations follow a predictable interval, typically every one thousand training steps; instead of issuing thousands of WAN reads for checkpoint shards, they are batched up and cached in the storage tier of the evaluation cluster.</li>
      <li>Allow the storage scheduler to preempt background traffic to make room for high-priority evaluations.</li>
    </ul>
  </li>
  <li>Proactive Hotspot Prediction
    <ul>
      <li>The training framework explicitly informs the storage system about upcoming access patterns</li>
      <li>The storage tier only increases replication factor for hot metadata and replicated-parameter files</li>
    </ul>
  </li>
  <li>Storage-Side Transformation Offloading
    <ul>
      <li>Multimodal data loading is compute-bound: the training hosts’ CPUs cannot decode video and images fast enough to keep the GPUs busy</li>
      <li>A deterministic dataloader writes the exact order of upcoming samples into per-step files, so storage nodes know what comes next and use their spare CPUs to decode, crop and resize those samples just in time for the next step</li>
    </ul>
  </li>
</ul>

<p><strong>Evaluation.</strong></p>

<p>Reduce wasted GPU hours per evaluation from 16,800 to 4,000, shorten checkpoint loading time at each training start by 40.8%, and reduce training stalls caused by dataloading by 63.2%.</p>

<p><strong>Food for thought.</strong></p>

<p>This work is pretty solid and straightforward. ML workloads tend to be deterministic and that indicates room for prefetching and resource reservation.</p>

<p>Heterogeneous systems + AI storage systems are topics I should dive deeper into.</p>

<h2 id="SBB">SBB: Eliminating Centralized Bottlenecks in Userspace Network Runtime</h2>

<p class="paper-meta"><em>Kang Hu and Diyu Zhou (Peking University), with Zhongguancun Laboratory and Michigan Technological University</em> · <a href="https://www.usenix.org/system/files/osdi26-hu-kang.pdf">Paper</a> · <a href="https://github.com/TELOS-syslab/SBB-Artifact">Code</a></p>

<ul class="paper-tags">
  <li>Network stack</li>
  <li>Kernel bypass</li>
</ul>

<p><strong>In one sentence.</strong></p>

<p>SBB removes every centralized scheduler from a userspace network runtime: each core receives its own timer and NIC interrupts in user space through Intel’s User Interrupts (UINTR) and balances load itself, so throughput scales with the number of cores.</p>

<p><strong>Background.</strong></p>

<ul>
  <li>Userspace network runtimes (Shinjuku, Caladan, etc.) must schedule three things, and every prior design centralizes at least one of them, which becomes the bottleneck as worker cores are added:
    <ul>
      <li>Request preemption: a central timer sends preemption signals (IPIs) to workers when a quantum expires</li>
      <li>CPU allocation (lending idle cores to best-effort jobs): a central monitor polls every worker’s NIC queue for backlog, since a polling app that has given up its core cannot notice new packets</li>
      <li>Request load balancing: RSS suffers from skewed flows; a dispatcher thread polling one NIC queue balances well but caps throughput</li>
    </ul>
  </li>
  <li>Giving these components more cores doesn’t help: the authors measure worse load balancing and no gain elsewhere.</li>
</ul>

<p><strong>Approach.</strong></p>

<ul>
  <li>Each worker thread binds to a core with its own TX/RX queues and <strong>local timer</strong>, and schedules itself:
    <ul>
      <li>Preemption: before each request, the worker arms a one-shot APIC timer that interrupts into user space. With the timer registers mapped into user space, this costs ~50 cycles, versus ~3 µs for <code class="language-plaintext highlighter-rouge">setitimer</code></li>
      <li>CPU allocation: NIC interrupts replace polling. A running latency-critical thread takes them in its user-space handler; otherwise a kernel handler wakes it up, so idle cores can safely run best-effort jobs</li>
      <li>Two-level load balancing: task stealing for temporary imbalance, and flow migration, which reprograms the NIC to move flows off overloaded cores, for persistent imbalance</li>
    </ul>
  </li>
  <li>Both interrupts arrive at the same handler, so SBB enables them in disjoint phases: NIC interrupts only while waiting for packets (which also prevents interrupt storms), the timer only during application-layer work</li>
</ul>

<p><strong>Evaluation.</strong></p>

<ul>
  <li>Server: 2 NUMA nodes, each with a 64-core Intel Xeon Platinum 8592 at 1.9 GHz; 100 Gbps Intel E810 NIC</li>
  <li>Built on DPDK, plus a 2,095-line Linux kernel patch</li>
  <li>Baselines: Shinjuku, Concord and TQ for preemption; Skyloft and Caladan for CPU allocation</li>
  <li>SBB achieves 1.7× to 5.2× higher throughput at 48 cores, under the same tail latency target</li>
</ul>

<p><strong>Food for thought.</strong></p>

<ul>
  <li>UINTR control plane: the kernel is still involved, registering each core’s notification vector and the user-space handler (SBB also gives the NIC queue and APIC timer interrupts that vector).</li>
  <li>UINTR data path: the CPU first matches an incoming interrupt’s vector against the notification vector; on a match, it skips the IDT and runs the user-space handler directly, without entering ring 0.</li>
</ul>

<h2 id="Rakaia">Rakaia: Scalable In-Kernel Scheduling for TCP-Based RPCs</h2>

<p class="paper-meta"><em>Rui Yang, Konstantinos Prasopoulos, and Edouard Bugnion, EPFL</em> · <a href="https://www.usenix.org/system/files/osdi26-yang-rui.pdf">Paper</a></p>

<ul class="paper-tags">
  <li>Network stack</li>
  <li>RPC</li>
</ul>

<p><strong>In one sentence.</strong></p>

<p>For RPCs, the authors created a set of message semantic APIs based on kernel TCP/IP stack that removes extra I/O threads copying and head-of-line blocking.</p>

<p><strong>Background.</strong></p>

<ul>
  <li>gRPC big picture:
    <ul>
      <li>NIC RX DMA into the memory, acked in device driver, handled over to TCP/IP stack</li>
      <li>I/O threads were epoll waiting on the socket and wakes up when there is work. I/O threads call read which copies skbs into a userspace buffer, and parses the HTTP frames.</li>
      <li>The complete message is enqueued to a global FIFO and handed to a worker.</li>
    </ul>
  </li>
  <li>Bottlenecks:
    <ul>
      <li>TCP’s stream-oriented abstraction is a poor fit for message-oriented RPCs: RPCs sharing the same TCP stream are processed sequentially by default, causing HOL blocking</li>
      <li>gRPC must reconstruct message semantics using additional userspace abstractions</li>
    </ul>
  </li>
</ul>

<p><strong>Approach.</strong></p>

<ul>
  <li>Key insight:
    <ul>
      <li>message parsing and scheduling should occur in the same softirq context that processes incoming TCP packets.</li>
      <li>messages must be decoupled from their underlying TCP connections to allow global scheduling</li>
    </ul>
  </li>
  <li>Each application thread only needs a single Rakaia socket to handle all TCP traffic
    <ul>
      <li>Parsing the message from TCP byte stream in the same softirq context</li>
      <li>Logical centralized queue of messages for scheduling</li>
      <li>Work stealing on empty local queue for load balancing</li>
    </ul>
  </li>
</ul>

<p><strong>Evaluation.</strong></p>

<ul>
  <li>Server: CloudLab xl170, one 10-core Intel Xeon E5-2640v4 at 2.4 GHz (20 hyperthreads); open-loop load over 20 to 5,000 connections, with 20 µs and 100 µs service times</li>
  <li>Achieves up to 5× higher throughputunder-SLO than KCM, Linux’s current in-kernel message API over TCP</li>
  <li>Improves gRPC-Go’s throughput-underSLO by up to 1.56×, and gRPC-C++’s by up to 2.69×</li>
  <li>Improves the throughput-under-SLO for real-world applications including Silo running TPC-C and OpenTelemetry Collector by 1.39× and 1.42×, respectively</li>
</ul>

<p><strong>Food for thought.</strong></p>

<p>Thinking about Rakaia for UDP-based RPC systems and DPDK</p>]]></content><author><name>Yuchen Tang</name></author><summary type="html"><![CDATA[Notes on OSDI'26 papers close to my work. RamRyder (UC San Diego, Shanghai Jiao Tong University, Samsung) makes memory bandwidth a resource the cloud can allocate, giving each VM whole DIMM and CXL channels and raising cluster-wide capacity and bandwidth utilization by 28.6% and 43.2%. NEMO (University of Washington, Meta, Microsoft Azure) adds a programmable telemetry pipeline to the memory controller, so the OS can count exactly the memory accesses it cares about. Teaching The Old Dog New Tricks (USTC, ByteDance) exploits how predictable LLM pre-training is: it replicates checkpoints and hot files ahead of time and decodes samples on storage nodes, cutting wasted GPU hours per evaluation from 16,800 to 4,000. SBB (Peking University) removes the central timer, monitor and dispatcher from a userspace network runtime by giving each core its own user interrupts, for 1.7-5.2x higher throughput at 48 cores. Rakaia (EPFL) parses RPC messages out of TCP streams inside the kernel and schedules them from one shared queue, raising gRPC throughput by up to 1.56x (Go) and 2.69x (C++).]]></summary></entry><entry><title type="html">Toy OS 7: Slab and kmalloc</title><link href="https://twicy.github.io/blogs/os/toy-os-7-slab.html" rel="alternate" type="text/html" title="Toy OS 7: Slab and kmalloc" /><published>2026-10-20T00:00:00-07:00</published><updated>2026-10-20T00:00:00-07:00</updated><id>https://twicy.github.io/blogs/os/toy-os-7-slab</id><content type="html" xml:base="https://twicy.github.io/blogs/os/toy-os-7-slab.html"><![CDATA[<h1 id="toy-os-7-slab-and-kmalloc">Toy OS 7: Slab and kmalloc</h1>

<p><em>Toy OS worklog: <a href="/blogs/os/toy-os-1-real-mode.html">1. Real mode</a> · <a href="/blogs/os/toy-os-2-segmentation.html">2. Segmentation</a> · <a href="/blogs/os/toy-os-3-paging.html">3. Paging</a> · <a href="/blogs/os/toy-os-4-kernel.html">4. Kernel</a> · <a href="/blogs/os/toy-os-5-interrupts.html">5. Interrupts</a> · <a href="/blogs/os/toy-os-6-buddy.html">6. Buddy</a> · <strong>7. Slab</strong> · <a href="https://github.com/twicy/toy-os/tree/fc88a1de36e8cd2cf495c46f3e6b36c01fc94f8b">Source</a></em></p>

<p>The buddy allocator (post 6) hands out 4 KiB at a time, but a list node, a cache descriptor or a small buffer is a few dozen bytes. A <em>slab allocator</em> sits on top of it. It takes whole pages, cuts each into equal-size objects, and keeps one cache per object size. This post covers the slab layer (<a href="https://github.com/twicy/toy-os/commit/442dcfe"><code class="language-plaintext highlighter-rouge">442dcfe</code></a>) and <code class="language-plaintext highlighter-rouge">kmalloc</code>/<code class="language-plaintext highlighter-rouge">kfree</code> on top of it (<a href="https://github.com/twicy/toy-os/commit/fc88a1d"><code class="language-plaintext highlighter-rouge">fc88a1d</code></a>), the last commit of this series.</p>

<p>ULK (ch. 8, “Memory Area Management”) describes Linux’s original SLAB allocator. It has cache descriptors, separate slab descriptors, per-object descriptor arrays, coloring and per-CPU caches, after Bonwick’s design for Solaris. toy-os is much smaller, and in two ways it is closer to SLUB, which replaced SLAB as Linux’s default: the slab’s bookkeeping lives in its <code class="language-plaintext highlighter-rouge">struct page</code>, and a free object itself holds the pointer to the next free object.</p>

<h2 id="table-of-contents">Table of Contents</h2>

<ul>
  <li><a href="#one-struct-page-three-owners">One struct page, three owners</a></li>
  <li><a href="#caches-and-slabs">Caches and slabs</a></li>
  <li><a href="#allocating-and-freeing-objects">Allocating and freeing objects</a></li>
  <li><a href="#kmalloc-and-kfree">kmalloc and kfree</a></li>
  <li><a href="#watching-it-work">Watching it work</a></li>
  <li><a href="#whats-next">What’s next</a></li>
  <li><a href="#references">References</a></li>
</ul>

<h2 id="one-struct-page-three-owners">One struct page, three owners</h2>

<p>The slab commit adds three fields to <code class="language-plaintext highlighter-rouge">struct page</code> and one flag:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#define PG_reserved (1u &lt;&lt; 0)
</span><span class="cm">/* Set if it is the first page of a free block in buddy system */</span>
<span class="cp">#define PG_buddy (1u &lt;&lt; 1)
</span><span class="cm">/* Set on every page that is a slab, the slab fields below are valid */</span>
<span class="cp">#define PG_slab (1u &lt;&lt; 2)
</span><span class="cm">/* Set on the first page of a kmalloc too large for a slab, order holds its size */</span>
<span class="cp">#define PG_large (1u &lt;&lt; 3)
</span>
<span class="k">struct</span> <span class="n">page</span> <span class="p">{</span>
    <span class="kt">uint16_t</span> <span class="n">flags</span><span class="p">;</span>
    <span class="kt">uint16_t</span> <span class="n">order</span><span class="p">;</span>
    <span class="kt">int32_t</span> <span class="n">refcount</span><span class="p">;</span>
    <span class="cm">/* buddy: the free list of its order, slab: the partial list of its cache */</span>
    <span class="k">struct</span> <span class="n">list_head</span> <span class="n">lru</span><span class="p">;</span>
    <span class="cm">/* PG_slab only, a slab is exactly one page so these describe the whole slab */</span>
    <span class="k">struct</span> <span class="n">kmem_cache</span> <span class="o">*</span><span class="n">slab_cache</span><span class="p">;</span>
    <span class="kt">void</span> <span class="o">*</span><span class="n">freelist</span><span class="p">;</span>                 <span class="cm">/* first free object, NULL when the slab is full */</span>
    <span class="kt">uint32_t</span> <span class="n">inuse</span><span class="p">;</span>                 <span class="cm">/* objects handed out, 0 is empty, cache-&gt;objects is full */</span>
<span class="p">};</span>
</code></pre></div></div>

<p><img src="/assets/images/posts/toy-os-7-slab/struct-page.svg" alt="Which struct page fields each owner uses: a free buddy block uses flags, order and lru; a slab page uses flags, refcount, lru, slab_cache, freelist and inuse; a large kmalloc uses flags, order and refcount" /></p>

<p><em>The 28 bytes of <code class="language-plaintext highlighter-rouge">struct page</code> and who uses which field.</em></p>

<p>As my commit note says, <code class="language-plaintext highlighter-rouge">lru</code> is “shared” by the buddy and slab layers. A page cannot be free in the buddy system and hold objects in a slab at the same time, so the same <code class="language-plaintext highlighter-rouge">list_head</code> links free blocks in post 6 and partial slabs here. Linux’s <code class="language-plaintext highlighter-rouge">struct page</code> takes this much further, with unions of fields for every possible owner.</p>

<h2 id="caches-and-slabs">Caches and slabs</h2>

<p>A cache is little more than a size and a list:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* Example: struct kmem_cache "kmalloc-1024"
 * size = 1024, objects = 4
 * partial is a linked list whose container is struct page
 */</span>
<span class="k">struct</span> <span class="n">kmem_cache</span> <span class="p">{</span>
    <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">name</span><span class="p">;</span>
    <span class="kt">uint32_t</span> <span class="n">size</span><span class="p">;</span>
    <span class="kt">uint32_t</span> <span class="n">objects</span><span class="p">;</span>
    <span class="cm">/* Non-full slabs, slab = page frame here */</span>
    <span class="k">struct</span> <span class="n">list_head</span> <span class="n">partial</span><span class="p">;</span>
<span class="p">};</span>

<span class="kt">void</span> <span class="nf">kmem_cache_init</span><span class="p">(</span><span class="k">struct</span> <span class="n">kmem_cache</span> <span class="o">*</span><span class="n">cache</span><span class="p">,</span> <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">name</span><span class="p">,</span> <span class="kt">uint32_t</span> <span class="n">size</span><span class="p">,</span> <span class="kt">uint32_t</span> <span class="n">align</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">align</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span>
        <span class="n">align</span> <span class="o">=</span> <span class="k">sizeof</span><span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="p">);</span>
    <span class="n">ASSERT</span><span class="p">(</span><span class="n">is_pow2</span><span class="p">(</span><span class="n">align</span><span class="p">));</span>
    <span class="n">ASSERT</span><span class="p">(</span><span class="n">size</span> <span class="o">&lt;=</span> <span class="n">PAGE_SIZE</span> <span class="o">&amp;&amp;</span> <span class="n">align</span> <span class="o">&lt;=</span> <span class="n">PAGE_SIZE</span><span class="p">);</span>

    <span class="n">size</span> <span class="o">=</span> <span class="n">ALIGN_UP</span><span class="p">(</span><span class="n">MAX</span><span class="p">(</span><span class="n">size</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="p">)),</span> <span class="n">align</span><span class="p">);</span>

    <span class="n">cache</span><span class="o">-&gt;</span><span class="n">name</span> <span class="o">=</span> <span class="n">name</span><span class="p">;</span>
    <span class="n">cache</span><span class="o">-&gt;</span><span class="n">size</span> <span class="o">=</span> <span class="n">size</span><span class="p">;</span>
    <span class="n">cache</span><span class="o">-&gt;</span><span class="n">objects</span> <span class="o">=</span> <span class="n">PAGE_SIZE</span> <span class="o">/</span> <span class="n">size</span><span class="p">;</span>
    <span class="n">INIT_LIST_HEAD</span><span class="p">(</span><span class="o">&amp;</span><span class="n">cache</span><span class="o">-&gt;</span><span class="n">partial</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>A slab is exactly one page. Every object is at least pointer-sized, because a free object stores the pointer to the next free object in its first 4 bytes. <code class="language-plaintext highlighter-rouge">new_slab</code> takes a page from the buddy system and threads that list through it. It walks backwards, so the list starts at object 0 and runs in address order:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* Carve a fresh page */</span>
<span class="k">static</span> <span class="k">struct</span> <span class="n">page</span> <span class="o">*</span><span class="nf">new_slab</span><span class="p">(</span><span class="k">struct</span> <span class="n">kmem_cache</span> <span class="o">*</span><span class="n">cache</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">struct</span> <span class="n">page</span> <span class="o">*</span><span class="n">page</span> <span class="o">=</span> <span class="n">alloc_pages</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
    <span class="kt">char</span> <span class="o">*</span><span class="n">base</span><span class="p">;</span>

    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">page</span><span class="p">)</span>
        <span class="k">return</span> <span class="nb">NULL</span><span class="p">;</span>

    <span class="n">base</span> <span class="o">=</span> <span class="n">page_address</span><span class="p">(</span><span class="n">page</span><span class="p">);</span>
    <span class="n">page</span><span class="o">-&gt;</span><span class="n">freelist</span> <span class="o">=</span> <span class="nb">NULL</span><span class="p">;</span>
    <span class="cm">/* Each slot either holds a valid obj or pointer to the next slot */</span>
    <span class="cm">/* As long as the slab is "partial", it's freelist is bound to point to a
     * free slot which should further point to its next free slot if possible
     */</span>
    <span class="k">for</span> <span class="p">(</span><span class="kt">uint32_t</span> <span class="n">i</span> <span class="o">=</span> <span class="n">cache</span><span class="o">-&gt;</span><span class="n">objects</span><span class="p">;</span> <span class="n">i</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span><span class="o">--</span><span class="p">)</span> <span class="p">{</span>
        <span class="kt">void</span> <span class="o">**</span><span class="n">obj</span> <span class="o">=</span> <span class="p">(</span><span class="kt">void</span> <span class="o">**</span><span class="p">)(</span><span class="n">base</span> <span class="o">+</span> <span class="p">(</span><span class="n">i</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="o">*</span> <span class="n">cache</span><span class="o">-&gt;</span><span class="n">size</span><span class="p">);</span>
        <span class="o">*</span><span class="n">obj</span> <span class="o">=</span> <span class="n">page</span><span class="o">-&gt;</span><span class="n">freelist</span><span class="p">;</span>
        <span class="n">page</span><span class="o">-&gt;</span><span class="n">freelist</span> <span class="o">=</span> <span class="n">obj</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="n">page</span><span class="o">-&gt;</span><span class="n">flags</span> <span class="o">|=</span> <span class="n">PG_slab</span><span class="p">;</span>
    <span class="n">page</span><span class="o">-&gt;</span><span class="n">slab_cache</span> <span class="o">=</span> <span class="n">cache</span><span class="p">;</span>
    <span class="n">page</span><span class="o">-&gt;</span><span class="n">inuse</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="n">list_add</span><span class="p">(</span><span class="o">&amp;</span><span class="n">page</span><span class="o">-&gt;</span><span class="n">lru</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">cache</span><span class="o">-&gt;</span><span class="n">partial</span><span class="p">);</span>
    <span class="k">return</span> <span class="n">page</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The list costs no memory: free objects are unused memory anyway. Once objects are allocated and freed in random order, it no longer runs in address order:</p>

<p><img src="/assets/images/posts/toy-os-7-slab/slab-page.svg" alt="A kmalloc-512 slab with slots 0, 2, 3 in use: struct page freelist points to slot 1, then 4, 5, 6, 7, then NULL" /></p>

<p><em>One slab after four allocations and one free. The free list is a stack (LIFO).</em></p>

<h2 id="allocating-and-freeing-objects">Allocating and freeing objects</h2>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="o">*</span><span class="nf">kmem_cache_alloc</span><span class="p">(</span><span class="k">struct</span> <span class="n">kmem_cache</span> <span class="o">*</span><span class="n">cache</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">struct</span> <span class="n">page</span> <span class="o">*</span><span class="n">page</span><span class="p">;</span>
    <span class="kt">void</span> <span class="o">**</span><span class="n">obj</span><span class="p">;</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">list_empty</span><span class="p">(</span><span class="o">&amp;</span><span class="n">cache</span><span class="o">-&gt;</span><span class="n">partial</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">new_slab</span><span class="p">(</span><span class="n">cache</span><span class="p">))</span>
        <span class="k">return</span> <span class="nb">NULL</span><span class="p">;</span>

    <span class="cm">/* pop the head of the first partial slab's free list */</span>
    <span class="n">page</span> <span class="o">=</span> <span class="n">list_first_entry</span><span class="p">(</span><span class="o">&amp;</span><span class="n">cache</span><span class="o">-&gt;</span><span class="n">partial</span><span class="p">,</span> <span class="k">struct</span> <span class="n">page</span><span class="p">,</span> <span class="n">lru</span><span class="p">);</span>
    <span class="n">obj</span> <span class="o">=</span> <span class="n">page</span><span class="o">-&gt;</span><span class="n">freelist</span><span class="p">;</span>
    <span class="n">page</span><span class="o">-&gt;</span><span class="n">freelist</span> <span class="o">=</span> <span class="o">*</span><span class="n">obj</span><span class="p">;</span>
    <span class="n">page</span><span class="o">-&gt;</span><span class="n">inuse</span><span class="o">++</span><span class="p">;</span>

    <span class="cm">/* a full slab leaves the partial list, kmem_cache_free puts it back */</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">page</span><span class="o">-&gt;</span><span class="n">inuse</span> <span class="o">==</span> <span class="n">cache</span><span class="o">-&gt;</span><span class="n">objects</span><span class="p">)</span>
        <span class="cm">/* Do not worry about losing track of full slabs, see kmem_cache_free() */</span>
        <span class="n">list_del_init</span><span class="p">(</span><span class="o">&amp;</span><span class="n">page</span><span class="o">-&gt;</span><span class="n">lru</span><span class="p">);</span>
    <span class="k">return</span> <span class="n">obj</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The cache tracks only <em>partial</em> slabs, the ones with at least one free object, so allocation never has to search. A full slab simply drops off the list. Nothing needs to find it until one of its objects is freed, and then the object’s address leads straight back to it: <code class="language-plaintext highlighter-rouge">virt_to_page(obj)</code> gives the page, and the page gives its cache.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">kmem_cache_free</span><span class="p">(</span><span class="k">struct</span> <span class="n">kmem_cache</span> <span class="o">*</span><span class="n">cache</span><span class="p">,</span> <span class="kt">void</span> <span class="o">*</span><span class="n">obj</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">struct</span> <span class="n">page</span> <span class="o">*</span><span class="n">page</span> <span class="o">=</span> <span class="n">obj_to_page</span><span class="p">(</span><span class="n">obj</span><span class="p">);</span>

    <span class="n">ASSERT</span><span class="p">((</span><span class="n">page</span><span class="o">-&gt;</span><span class="n">flags</span> <span class="o">&amp;</span> <span class="n">PG_slab</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="n">page</span><span class="o">-&gt;</span><span class="n">slab_cache</span> <span class="o">==</span> <span class="n">cache</span><span class="p">);</span>
    <span class="cm">/* must be the start of an object, not a pointer into the middle of one */</span>
    <span class="n">ASSERT</span><span class="p">(((</span><span class="kt">uintptr_t</span><span class="p">)</span><span class="n">obj</span> <span class="o">-</span> <span class="p">(</span><span class="kt">uintptr_t</span><span class="p">)</span><span class="n">page_address</span><span class="p">(</span><span class="n">page</span><span class="p">))</span> <span class="o">%</span> <span class="n">cache</span><span class="o">-&gt;</span><span class="n">size</span> <span class="o">==</span> <span class="mi">0</span><span class="p">);</span>
    <span class="cm">/* a cheap double free check, it only catches freeing the last freed object again */</span>
    <span class="n">ASSERT</span><span class="p">(</span><span class="n">page</span><span class="o">-&gt;</span><span class="n">inuse</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="n">obj</span> <span class="o">!=</span> <span class="n">page</span><span class="o">-&gt;</span><span class="n">freelist</span><span class="p">);</span>

    <span class="cm">/* a full slab is about to have a free object, so it can serve allocations again */</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">page</span><span class="o">-&gt;</span><span class="n">inuse</span> <span class="o">==</span> <span class="n">cache</span><span class="o">-&gt;</span><span class="n">objects</span><span class="p">)</span>
        <span class="n">list_add</span><span class="p">(</span><span class="o">&amp;</span><span class="n">page</span><span class="o">-&gt;</span><span class="n">lru</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">cache</span><span class="o">-&gt;</span><span class="n">partial</span><span class="p">);</span>

    <span class="cm">/* push onto the free list */</span>
    <span class="o">*</span><span class="p">(</span><span class="kt">void</span> <span class="o">**</span><span class="p">)</span><span class="n">obj</span> <span class="o">=</span> <span class="n">page</span><span class="o">-&gt;</span><span class="n">freelist</span><span class="p">;</span>
    <span class="n">page</span><span class="o">-&gt;</span><span class="n">freelist</span> <span class="o">=</span> <span class="n">obj</span><span class="p">;</span>
    <span class="n">page</span><span class="o">-&gt;</span><span class="n">inuse</span><span class="o">--</span><span class="p">;</span>

    <span class="cm">/* keep one slab even when empty, so allocating and freeing a single
     * object in a loop does not go to the buddy system every time */</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">page</span><span class="o">-&gt;</span><span class="n">inuse</span> <span class="o">==</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">list_is_singular</span><span class="p">(</span><span class="o">&amp;</span><span class="n">cache</span><span class="o">-&gt;</span><span class="n">partial</span><span class="p">))</span>
        <span class="n">free_slab</span><span class="p">(</span><span class="n">page</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Empty slabs go back to the buddy system (<code class="language-plaintext highlighter-rouge">free_slab</code> clears <code class="language-plaintext highlighter-rouge">PG_slab</code>, then calls <code class="language-plaintext highlighter-rouge">free_pages</code>). The exception is a cache’s last slab. Without that exception, a loop of <code class="language-plaintext highlighter-rouge">kmalloc(32)</code>/<code class="language-plaintext highlighter-rouge">kfree</code> would split and merge a buddy block on every iteration.</p>

<h2 id="kmalloc-and-kfree">kmalloc and kfree</h2>

<p><code class="language-plaintext highlighter-rouge">kmalloc</code> picks one of nine caches, <code class="language-plaintext highlighter-rouge">kmalloc-8</code> to <code class="language-plaintext highlighter-rouge">kmalloc-2048</code>, by rounding the size up to a power of two. Anything bigger goes straight to the buddy allocator:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* index of the smallest kmalloc cache holding size bytes, 0 &lt; size &lt;= KMALLOC_MAX_CACHE_SIZE */</span>
<span class="k">static</span> <span class="kr">inline</span> <span class="kt">uint32_t</span> <span class="nf">kmalloc_index</span><span class="p">(</span><span class="kt">uint32_t</span> <span class="n">size</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">size</span> <span class="o">&lt;=</span> <span class="p">(</span><span class="mi">1u</span> <span class="o">&lt;&lt;</span> <span class="n">KMALLOC_MIN_SHIFT</span><span class="p">))</span>
        <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
    <span class="cm">/* 32 - clz(size - 1) is log2 of size rounded up to a power of 2 */</span>
    <span class="k">return</span> <span class="mi">32</span> <span class="o">-</span> <span class="n">__builtin_clz</span><span class="p">(</span><span class="n">size</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="o">-</span> <span class="n">KMALLOC_MIN_SHIFT</span><span class="p">;</span>
<span class="p">}</span>

<span class="kt">void</span> <span class="o">*</span><span class="nf">kmalloc</span><span class="p">(</span><span class="kt">size_t</span> <span class="n">size</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">struct</span> <span class="n">page</span> <span class="o">*</span><span class="n">page</span><span class="p">;</span>
    <span class="kt">uint32_t</span> <span class="n">order</span><span class="p">;</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">size</span> <span class="o">==</span> <span class="mi">0</span> <span class="o">||</span> <span class="n">size</span> <span class="o">&gt;</span> <span class="n">KMALLOC_MAX_SIZE</span><span class="p">)</span>
        <span class="k">return</span> <span class="nb">NULL</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">size</span> <span class="o">&lt;=</span> <span class="n">KMALLOC_MAX_CACHE_SIZE</span><span class="p">)</span>
        <span class="k">return</span> <span class="n">kmem_cache_alloc</span><span class="p">(</span><span class="o">&amp;</span><span class="n">kmalloc_caches</span><span class="p">[</span><span class="n">kmalloc_index</span><span class="p">(</span><span class="n">size</span><span class="p">)]);</span>

    <span class="cm">/* too large for a slab: a buddy block of its own, its first page
     * remembers the order so kfree knows how much to give back */</span>
    <span class="n">order</span> <span class="o">=</span> <span class="n">get_order</span><span class="p">(</span><span class="n">size</span><span class="p">);</span>
    <span class="n">page</span> <span class="o">=</span> <span class="n">alloc_pages</span><span class="p">(</span><span class="n">order</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">page</span><span class="p">)</span>
        <span class="k">return</span> <span class="nb">NULL</span><span class="p">;</span>
    <span class="n">page</span><span class="o">-&gt;</span><span class="n">flags</span> <span class="o">|=</span> <span class="n">PG_large</span><span class="p">;</span>
    <span class="n">page</span><span class="o">-&gt;</span><span class="n">order</span> <span class="o">=</span> <span class="n">order</span><span class="p">;</span>
    <span class="k">return</span> <span class="n">page_address</span><span class="p">(</span><span class="n">page</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th><code class="language-plaintext highlighter-rouge">size</code> (bytes)</th>
      <th>Comes from</th>
      <th>Objects per page</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1 – 8</td>
      <td><code class="language-plaintext highlighter-rouge">kmalloc-8</code></td>
      <td>512</td>
    </tr>
    <tr>
      <td>9 – 16</td>
      <td><code class="language-plaintext highlighter-rouge">kmalloc-16</code></td>
      <td>256</td>
    </tr>
    <tr>
      <td>17 – 32</td>
      <td><code class="language-plaintext highlighter-rouge">kmalloc-32</code></td>
      <td>128</td>
    </tr>
    <tr>
      <td>33 – 64</td>
      <td><code class="language-plaintext highlighter-rouge">kmalloc-64</code></td>
      <td>64</td>
    </tr>
    <tr>
      <td>65 – 128</td>
      <td><code class="language-plaintext highlighter-rouge">kmalloc-128</code></td>
      <td>32</td>
    </tr>
    <tr>
      <td>129 – 256</td>
      <td><code class="language-plaintext highlighter-rouge">kmalloc-256</code></td>
      <td>16</td>
    </tr>
    <tr>
      <td>257 – 512</td>
      <td><code class="language-plaintext highlighter-rouge">kmalloc-512</code></td>
      <td>8</td>
    </tr>
    <tr>
      <td>513 – 1024</td>
      <td><code class="language-plaintext highlighter-rouge">kmalloc-1024</code></td>
      <td>4</td>
    </tr>
    <tr>
      <td>1025 – 2048</td>
      <td><code class="language-plaintext highlighter-rouge">kmalloc-2048</code></td>
      <td>2</td>
    </tr>
    <tr>
      <td>2049 – 4 MiB</td>
      <td><code class="language-plaintext highlighter-rouge">alloc_pages(get_order(size))</code>, marked <code class="language-plaintext highlighter-rouge">PG_large</code></td>
      <td> </td>
    </tr>
    <tr>
      <td>0, or more than 4 MiB</td>
      <td><code class="language-plaintext highlighter-rouge">NULL</code></td>
      <td> </td>
    </tr>
  </tbody>
</table>

<p>There is no <code class="language-plaintext highlighter-rouge">kmalloc-4096</code>. A 4 KiB object would fill a whole page, and a page is what the buddy allocator already hands out. A size of 2049 bytes still costs a whole page, which is the price of power-of-two classes. Every object is aligned to its size (up to <code class="language-plaintext highlighter-rouge">PAGE_SIZE</code>), since a slab starts on a page boundary and objects are packed at multiples of the size.</p>

<p><code class="language-plaintext highlighter-rouge">kfree</code> gets only a pointer. The page’s flags say where it came from, and that is the point of <code class="language-plaintext highlighter-rouge">PG_large</code>. As my commit note says, it is cheap and lets <code class="language-plaintext highlighter-rouge">kmalloc</code> and <code class="language-plaintext highlighter-rouge">kfree</code> pair up:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">kfree</span><span class="p">(</span><span class="k">const</span> <span class="kt">void</span> <span class="o">*</span><span class="n">ptr</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">struct</span> <span class="n">page</span> <span class="o">*</span><span class="n">page</span><span class="p">;</span>
    <span class="kt">uint32_t</span> <span class="n">order</span><span class="p">;</span>

    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">ptr</span><span class="p">)</span>
        <span class="k">return</span><span class="p">;</span>

    <span class="cm">/* the page says which kind of kmalloc it was */</span>
    <span class="n">page</span> <span class="o">=</span> <span class="n">obj_to_page</span><span class="p">(</span><span class="n">ptr</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">page</span><span class="o">-&gt;</span><span class="n">flags</span> <span class="o">&amp;</span> <span class="n">PG_slab</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">kmem_cache_free</span><span class="p">(</span><span class="n">page</span><span class="o">-&gt;</span><span class="n">slab_cache</span><span class="p">,</span> <span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="p">)</span><span class="n">ptr</span><span class="p">);</span>
        <span class="k">return</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="n">ASSERT</span><span class="p">((</span><span class="n">page</span><span class="o">-&gt;</span><span class="n">flags</span> <span class="o">&amp;</span> <span class="n">PG_large</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="n">ptr</span> <span class="o">==</span> <span class="n">page_address</span><span class="p">(</span><span class="n">page</span><span class="p">));</span>
    <span class="n">order</span> <span class="o">=</span> <span class="n">page</span><span class="o">-&gt;</span><span class="n">order</span><span class="p">;</span>
    <span class="n">page</span><span class="o">-&gt;</span><span class="n">flags</span> <span class="o">&amp;=</span> <span class="o">~</span><span class="n">PG_large</span><span class="p">;</span>
    <span class="n">page</span><span class="o">-&gt;</span><span class="n">order</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="n">free_pages</span><span class="p">(</span><span class="n">page</span><span class="p">,</span> <span class="n">order</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p><img src="/assets/images/posts/toy-os-7-slab/kmalloc-flow.svg" alt="kmalloc routes sizes up to 2048 through kmalloc_index to one of nine caches, each with a partial list of slab pages; larger sizes go to alloc_pages with PG_large; kfree looks at the page flags to choose the path back" /></p>

<p><em>The two paths through <code class="language-plaintext highlighter-rouge">kmalloc</code>, and how <code class="language-plaintext highlighter-rouge">kfree</code> finds the right one.</em></p>

<p>The checks catch a lot: a pointer below <code class="language-plaintext highlighter-rouge">0xC0000000</code> or beyond RAM, a pointer into the middle of an object, a page that is neither a slab nor a large allocation, and freeing the same object twice in a row. My commit note also records what they miss. A wrong pointer that happens to land on the start of <em>another</em> object passes every check, and that object is freed while someone still uses it. Linux catches that class of bug with KASAN, which keeps shadow memory to track whether each byte may be accessed.</p>

<p><code class="language-plaintext highlighter-rouge">kmem_cache_create()</code> makes a cache for a specific object type. The <code class="language-plaintext highlighter-rouge">struct kmem_cache</code> itself (20 bytes) comes from <code class="language-plaintext highlighter-rouge">kmalloc</code>, in this case from <code class="language-plaintext highlighter-rouge">kmalloc-32</code>.</p>

<h2 id="watching-it-work">Watching it work</h2>

<p><code class="language-plaintext highlighter-rouge">main()</code> at <a href="https://github.com/twicy/toy-os/commit/fc88a1d"><code class="language-plaintext highlighter-rouge">fc88a1d</code></a> only calls <code class="language-plaintext highlighter-rouge">init_kmalloc()</code>. To see the allocator at work, add a throwaway test after it (it is not part of the commit):</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kt">void</span> <span class="nf">kmalloc_demo</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
    <span class="kt">char</span> <span class="o">*</span><span class="n">a</span> <span class="o">=</span> <span class="n">kmalloc</span><span class="p">(</span><span class="mi">24</span><span class="p">);</span>
    <span class="kt">char</span> <span class="o">*</span><span class="n">b</span> <span class="o">=</span> <span class="n">kmalloc</span><span class="p">(</span><span class="mi">24</span><span class="p">);</span>
    <span class="kt">char</span> <span class="o">*</span><span class="n">c</span> <span class="o">=</span> <span class="n">kmalloc</span><span class="p">(</span><span class="mi">100</span><span class="p">);</span>
    <span class="kt">char</span> <span class="o">*</span><span class="n">big</span> <span class="o">=</span> <span class="n">kmalloc</span><span class="p">(</span><span class="mi">5000</span><span class="p">);</span>

    <span class="n">printk</span><span class="p">(</span><span class="s">"kmalloc(24)   = %p</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">a</span><span class="p">);</span>
    <span class="n">printk</span><span class="p">(</span><span class="s">"kmalloc(24)   = %p</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">b</span><span class="p">);</span>
    <span class="n">printk</span><span class="p">(</span><span class="s">"kmalloc(100)  = %p</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">c</span><span class="p">);</span>
    <span class="n">printk</span><span class="p">(</span><span class="s">"kmalloc(5000) = %p</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">big</span><span class="p">);</span>
    <span class="n">kfree</span><span class="p">(</span><span class="n">b</span><span class="p">);</span>
    <span class="n">printk</span><span class="p">(</span><span class="s">"kfree(%p), then kmalloc(20) = %p</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">b</span><span class="p">,</span> <span class="n">kmalloc</span><span class="p">(</span><span class="mi">20</span><span class="p">));</span>
<span class="p">}</span>
</code></pre></div></div>

<p><img src="/assets/images/posts/toy-os-7-slab/kmalloc-demo.png" alt="Bochs: kmalloc(24) = 0xc0500000, kmalloc(24) = 0xc0500020, kmalloc(100) = 0xc0582000, kmalloc(5000) = 0xc0584000, kfree(0xc0500020), then kmalloc(20) = 0xc0500020" /></p>

<p><em>The test’s output. Every address can be predicted from the free lists in post 6.</em></p>

<ol>
  <li><strong><code class="language-plaintext highlighter-rouge">kmalloc(24)</code></strong> rounds up to <code class="language-plaintext highlighter-rouge">kmalloc-32</code>, which has no slab yet. <code class="language-plaintext highlighter-rouge">new_slab</code> asks for an order-0 page, and the order-0 list holds exactly one: pfn <code class="language-plaintext highlighter-rouge">0x500</code>, the page left over from the loader’s tables. Object 0 is at <code class="language-plaintext highlighter-rouge">0xC0500000</code>.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">kmalloc(24)</code> again</strong> pops object 1 from the same slab, 32 bytes later: <code class="language-plaintext highlighter-rouge">0xC0500020</code>.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">kmalloc(100)</code></strong> goes to <code class="language-plaintext highlighter-rouge">kmalloc-128</code>, which needs a new slab. The order-0 list is now empty, so the buddy allocator splits the order-1 block at <code class="language-plaintext highlighter-rouge">0x582</code>. It returns <code class="language-plaintext highlighter-rouge">0x582</code> and puts <code class="language-plaintext highlighter-rouge">0x583</code> on the order-0 list. Result: <code class="language-plaintext highlighter-rouge">0xC0582000</code>.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">kmalloc(5000)</code></strong> is too big for a slab: <code class="language-plaintext highlighter-rouge">get_order(5000)</code> = 1, two pages. The order-1 list is empty after step 3, so the order-2 block at <code class="language-plaintext highlighter-rouge">0x584</code> splits into <code class="language-plaintext highlighter-rouge">0x584</code> (returned) and <code class="language-plaintext highlighter-rouge">0x586</code> (back on the order-1 list). That is the split pictured in post 6. Result: <code class="language-plaintext highlighter-rouge">0xC0584000</code>, with <code class="language-plaintext highlighter-rouge">PG_large</code> and <code class="language-plaintext highlighter-rouge">order = 1</code> on its first page.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">kfree(b)</code> then <code class="language-plaintext highlighter-rouge">kmalloc(20)</code></strong>: <code class="language-plaintext highlighter-rouge">b</code> goes back on the head of its slab’s free list, and the next <code class="language-plaintext highlighter-rouge">kmalloc-32</code> allocation gets it right back, <code class="language-plaintext highlighter-rouge">0xC0500020</code>. The most recently freed object is the most likely to still be in the CPU cache.</li>
</ol>

<p>All of these are direct-map addresses: physical = virtual − <code class="language-plaintext highlighter-rouge">0xC0000000</code>, and each allocation is physically contiguous. That is what DMA buffers and page tables need, but it is also the weakness of <code class="language-plaintext highlighter-rouge">kmalloc</code>. Once physical memory fragments, a large <code class="language-plaintext highlighter-rouge">kmalloc</code> fails even when there is plenty of free memory in total.</p>

<h2 id="whats-next">What’s next</h2>

<p>That is where this series stops (<a href="https://github.com/twicy/toy-os/commit/fc88a1d"><code class="language-plaintext highlighter-rouge">fc88a1d</code></a>). The commits after it give the kernel <code class="language-plaintext highlighter-rouge">vmalloc</code> (virtually contiguous memory built from single pages, with unmapped guard pages between areas), threads with vmalloc’ed kernel stacks, and a double-fault task gate that catches kernel stack overflows. They may become the next worklogs.</p>

<h2 id="references">References</h2>

<ol>
  <li>D. P. Bovet and M. Cesati, <em>Understanding the Linux Kernel</em>, 3rd ed., ch. 8 “Memory Management”, “Memory Area Management”: the slab allocator, cache and slab descriptors, general and specific caches, and <code class="language-plaintext highlighter-rouge">kmalloc()</code>.</li>
  <li>J. Bonwick, “The Slab Allocator: An Object-Caching Kernel Memory Allocator”, USENIX Summer 1994 Technical Conference.</li>
  <li>J. Corbet, <a href="https://lwn.net/Articles/229984/">“The SLUB allocator”</a>, LWN.net, 2007; Linux source <code class="language-plaintext highlighter-rouge">mm/slub.c</code>.</li>
  <li>The Linux kernel documentation, <a href="https://docs.kernel.org/dev-tools/kasan.html">Kernel Address Sanitizer (KASAN)</a>.</li>
  <li>navi, <a href="https://segmentfault.com/a/1190000040232553">从零开始写 OS 内核 - 实现堆和 malloc</a>, segmentfault. It builds a heap allocator instead of slabs, a useful contrast.</li>
  <li>郑钢, 《操作系统真相还原》, ch. 12 (进一步完善内核): the book’s <code class="language-plaintext highlighter-rouge">sys_malloc</code>, built on arenas of 16–1024-byte blocks.</li>
</ol>]]></content><author><name>Yuchen Tang</name></author><summary type="html"><![CDATA[The seventh toy-os worklog builds small-object allocation on top of the buddy system. A kmem_cache hands out fixed-size objects from one-page slabs. Each free object stores the address of the next, and the slab's state lives in its struct page, sharing the list link the buddy allocator uses, much like Linux's SLUB. kmalloc sends 8 to 2048 bytes to nine power-of-two caches and anything larger straight to the buddy allocator, marked so kfree can tell the two apart. A small test shows real addresses: objects 32 bytes apart, a freed object reused at once, and a buddy split.]]></summary></entry><entry><title type="html">Toy OS 6: Physical Memory, E820 and the Buddy System</title><link href="https://twicy.github.io/blogs/os/toy-os-6-buddy.html" rel="alternate" type="text/html" title="Toy OS 6: Physical Memory, E820 and the Buddy System" /><published>2026-10-18T00:00:00-07:00</published><updated>2026-10-18T00:00:00-07:00</updated><id>https://twicy.github.io/blogs/os/toy-os-6-buddy</id><content type="html" xml:base="https://twicy.github.io/blogs/os/toy-os-6-buddy.html"><![CDATA[<h1 id="toy-os-6-physical-memory-e820-and-the-buddy-system">Toy OS 6: Physical Memory, E820 and the Buddy System</h1>

<p><em>Toy OS worklog: <a href="/blogs/os/toy-os-1-real-mode.html">1. Real mode</a> · <a href="/blogs/os/toy-os-2-segmentation.html">2. Segmentation</a> · <a href="/blogs/os/toy-os-3-paging.html">3. Paging</a> · <a href="/blogs/os/toy-os-4-kernel.html">4. Kernel</a> · <a href="/blogs/os/toy-os-5-interrupts.html">5. Interrupts</a> · <strong>6. Buddy</strong> · <a href="/blogs/os/toy-os-7-slab.html">7. Slab</a> · <a href="https://github.com/twicy/toy-os/tree/fc88a1de36e8cd2cf495c46f3e6b36c01fc94f8b">Source</a></em></p>

<p>So far the kernel lives inside the first MiB, and the only other memory it knows about is the loader’s page tables. Bochs gives it 512 MiB. To hand out page frames, the kernel needs four things, in this order:</p>

<ol>
  <li><strong>What RAM exists</strong>: the BIOS’s E820 map, collected by the loader in real mode.</li>
  <li><strong>A boot-time allocator</strong> to place its own data structures before any real allocator exists (memblock).</li>
  <li><strong>Page tables that map all of RAM</strong>, so that any frame can be reached through a pointer.</li>
  <li><strong>A <code class="language-plaintext highlighter-rouge">struct page</code> for every frame</strong>, and the buddy allocator on top of them.</li>
</ol>

<p>That is the order of <code class="language-plaintext highlighter-rouge">main()</code> (with the work spread over commits <a href="https://github.com/twicy/toy-os/commit/cbd7402"><code class="language-plaintext highlighter-rouge">cbd7402</code></a>, <a href="https://github.com/twicy/toy-os/commit/8625d4f"><code class="language-plaintext highlighter-rouge">8625d4f</code></a>, <a href="https://github.com/twicy/toy-os/commit/7a8ec84"><code class="language-plaintext highlighter-rouge">7a8ec84</code></a>, <a href="https://github.com/twicy/toy-os/commit/cd46473"><code class="language-plaintext highlighter-rouge">cd46473</code></a> and <a href="https://github.com/twicy/toy-os/commit/ab8312c"><code class="language-plaintext highlighter-rouge">ab8312c</code></a>):</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="cm">/* GDT's memory is safely pre-allcoated in BSS section, under _end */</span>
    <span class="cm">/* But kernel PG and PTs are phys_mem_alloc()ed already */</span>
    <span class="n">e820_probe_phys_mem</span><span class="p">();</span>
    <span class="n">init_pd</span><span class="p">();</span>
    <span class="c1">// Hand over physical pages to zone</span>
    <span class="n">init_page_alloc</span><span class="p">();</span>
</code></pre></div></div>

<p>The design follows Linux as ULK describes it (ch. 8, “Page Frame Management”), cut down to one zone.</p>

<h2 id="table-of-contents">Table of Contents</h2>

<ul>
  <li><a href="#e820-asking-the-bios">E820: asking the BIOS</a></li>
  <li><a href="#memblock-a-boot-time-allocator">memblock: a boot-time allocator</a></li>
  <li><a href="#kernel-page-tables-for-all-of-ram">Kernel page tables for all of RAM</a></li>
  <li><a href="#struct-page-and-mem_map">struct page and mem_map</a></li>
  <li><a href="#the-buddy-system">The buddy system</a></li>
  <li><a href="#physical-memory-after-init_page_alloc">Physical memory after init_page_alloc()</a></li>
  <li><a href="#references">References</a></li>
</ul>

<h2 id="e820-asking-the-bios">E820: asking the BIOS</h2>

<p><code class="language-plaintext highlighter-rouge">int 0x15</code> with <code class="language-plaintext highlighter-rouge">EAX = 0xE820</code> returns the memory map one entry per call. The caller passes <code class="language-plaintext highlighter-rouge">EDX = 'SMAP'</code>, an entry size in <code class="language-plaintext highlighter-rouge">ECX</code>, a buffer in <code class="language-plaintext highlighter-rouge">ES:DI</code>, and a continuation value in <code class="language-plaintext highlighter-rouge">EBX</code> (0 the first time). The BIOS hands back the next <code class="language-plaintext highlighter-rouge">EBX</code>, and <code class="language-plaintext highlighter-rouge">EBX = 0</code> means that was the last entry. Each entry is a 64-bit base, a 64-bit length and a 32-bit type (1 usable, 2 reserved, 3 ACPI reclaimable, 4 ACPI NVS, 5 bad). This is a BIOS call, so the loader does it in real mode, before anything else (post 3):</p>

<div class="language-nasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">; Store the BIOS E820 memory map at MEM_MAP_ADDR (layout in boot.inc)</span>
<span class="nl">detect_memory:</span>
    <span class="nf">mov</span> <span class="kt">dword</span> <span class="p">[</span><span class="nv">MEM_MAP_COUNT_ADDR</span><span class="p">],</span> <span class="mi">0</span>
    <span class="c1">; es:di = where the BIOS writes the next entry</span>
    <span class="nf">mov</span> <span class="nb">di</span><span class="p">,</span> <span class="nv">MEM_MAP_ENTRIES_ADDR</span>
    <span class="c1">; continuation value, 0 = start from the first entry</span>
    <span class="nf">xor</span> <span class="nb">ebx</span><span class="p">,</span> <span class="nb">ebx</span>
<span class="nl">.next_entry:</span>
    <span class="nf">mov</span> <span class="nb">eax</span><span class="p">,</span> <span class="mh">0xe820</span>
    <span class="c1">; 'SMAP', set on every call since some BIOSes trash edx</span>
    <span class="nf">mov</span> <span class="nb">edx</span><span class="p">,</span> <span class="mh">0x534d4150</span>
    <span class="nf">mov</span> <span class="nb">ecx</span><span class="p">,</span> <span class="nv">MEM_MAP_ENTRY_SIZE</span>
    <span class="nf">int</span> <span class="mh">0x15</span>
    <span class="c1">; cf set: no E820 on the first call, end of list on later ones</span>
    <span class="nf">jc</span> <span class="nv">.done</span>
    <span class="c1">; eax = 'SMAP' confirms the BIOS understood the call</span>
    <span class="nf">cmp</span> <span class="nb">eax</span><span class="p">,</span> <span class="mh">0x534d4150</span>
    <span class="nf">jne</span> <span class="nv">.done</span>
    <span class="c1">; skip entries with a 0 qword length at offset 8</span>
    <span class="nf">mov</span> <span class="nb">eax</span><span class="p">,</span> <span class="p">[</span><span class="nb">di</span> <span class="o">+</span> <span class="mi">8</span><span class="p">]</span>
    <span class="nf">or</span> <span class="nb">eax</span><span class="p">,</span> <span class="p">[</span><span class="nb">di</span> <span class="o">+</span> <span class="mi">12</span><span class="p">]</span>
    <span class="nf">jz</span> <span class="nv">.skip</span>
    <span class="nf">inc</span> <span class="kt">dword</span> <span class="p">[</span><span class="nv">MEM_MAP_COUNT_ADDR</span><span class="p">]</span>
    <span class="nf">add</span> <span class="nb">di</span><span class="p">,</span> <span class="nv">MEM_MAP_ENTRY_SIZE</span>
    <span class="c1">; stop before the map grows into the loader's stack</span>
    <span class="nf">cmp</span> <span class="kt">dword</span> <span class="p">[</span><span class="nv">MEM_MAP_COUNT_ADDR</span><span class="p">],</span> <span class="nv">MEM_MAP_MAX_ENTRIES</span>
    <span class="nf">jae</span> <span class="nv">.done</span>
<span class="nl">.skip:</span>
    <span class="c1">; ebx = 0: that was the last entry</span>
    <span class="nf">test</span> <span class="nb">ebx</span><span class="p">,</span> <span class="nb">ebx</span>
    <span class="nf">jnz</span> <span class="nv">.next_entry</span>
</code></pre></div></div>

<p>The map goes to physical <code class="language-plaintext highlighter-rouge">0x500</code>: a dword count, then up to 32 entries of 20 bytes, ending at <code class="language-plaintext highlighter-rouge">0x784</code>, just below the loader at <code class="language-plaintext highlighter-rouge">0x800</code> (post 1). Dumping it in Bochs right after the call:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;bochs&gt; xp /28wx 0x500
0x0000000000000500 &lt;bogus+       0&gt;:	0x00000005	0x00000000	0x00000000	0x0009f000
0x0000000000000510 &lt;bogus+      16&gt;:	0x00000000	0x00000001	0x0009f000	0x00000000
0x0000000000000520 &lt;bogus+      32&gt;:	0x00001000	0x00000000	0x00000002	0x000e8000
0x0000000000000530 &lt;bogus+      48&gt;:	0x00000000	0x00018000	0x00000000	0x00000002
0x0000000000000540 &lt;bogus+      64&gt;:	0x00100000	0x00000000	0x1ff00000	0x00000000
0x0000000000000550 &lt;bogus+      80&gt;:	0x00000001	0xfffc0000	0x00000000	0x00040000
0x0000000000000560 &lt;bogus+      96&gt;:	0x00000000	0x00000002	0x00000000	0x00000000
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>#</th>
      <th>Base</th>
      <th>Length</th>
      <th>End</th>
      <th>Type</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0</td>
      <td><code class="language-plaintext highlighter-rouge">0x00000000</code></td>
      <td><code class="language-plaintext highlighter-rouge">0x0009F000</code></td>
      <td><code class="language-plaintext highlighter-rouge">0x0009F000</code></td>
      <td>1, usable (636 KiB)</td>
    </tr>
    <tr>
      <td>1</td>
      <td><code class="language-plaintext highlighter-rouge">0x0009F000</code></td>
      <td><code class="language-plaintext highlighter-rouge">0x00001000</code></td>
      <td><code class="language-plaintext highlighter-rouge">0x000A0000</code></td>
      <td>2, reserved (EBDA)</td>
    </tr>
    <tr>
      <td>2</td>
      <td><code class="language-plaintext highlighter-rouge">0x000E8000</code></td>
      <td><code class="language-plaintext highlighter-rouge">0x00018000</code></td>
      <td><code class="language-plaintext highlighter-rouge">0x00100000</code></td>
      <td>2, reserved (BIOS)</td>
    </tr>
    <tr>
      <td>3</td>
      <td><code class="language-plaintext highlighter-rouge">0x00100000</code></td>
      <td><code class="language-plaintext highlighter-rouge">0x1FF00000</code></td>
      <td><code class="language-plaintext highlighter-rouge">0x20000000</code></td>
      <td>1, usable (511 MiB)</td>
    </tr>
    <tr>
      <td>4</td>
      <td><code class="language-plaintext highlighter-rouge">0xFFFC0000</code></td>
      <td><code class="language-plaintext highlighter-rouge">0x00040000</code></td>
      <td><code class="language-plaintext highlighter-rouge">0x1_00000000</code></td>
      <td>2, reserved (BIOS ROM at the top of 4 GiB)</td>
    </tr>
  </tbody>
</table>

<p><img src="/assets/images/posts/toy-os-6-buddy/e820-space.svg" alt="The physical address space by E820: usable RAM below 0x9F000 and from 1 MiB to 512 MiB, reserved BIOS areas, and large ranges that are not listed at all" /></p>

<p><em>All 4 GiB of physical address space according to E820 (not to scale).</em></p>

<p>Two things to notice. First, the map only lists ranges, so holes such as <code class="language-plaintext highlighter-rouge">[0xA0000, 0xE8000)</code> (video memory, VGA ROM) are simply absent, and so is everything between 512 MiB and the BIOS ROM. Second, entry 4 <em>ends</em> at <code class="language-plaintext highlighter-rouge">0x1_0000_0000</code>, which wraps to 0 in 32 bits. <code class="language-plaintext highlighter-rouge">e820_probe_phys_mem</code> therefore computes in 64 bits and clips to the last page boundary that fits in a <code class="language-plaintext highlighter-rouge">uint32_t</code>:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>        <span class="c1">// 64 bit: the BIOS area entry 0xfffc0000 + 0x40000 = 0x100000000 wraps to 0 in 32 bits</span>
        <span class="kt">uint64_t</span> <span class="n">start64</span> <span class="o">=</span> <span class="p">((</span><span class="kt">uint64_t</span><span class="p">)</span><span class="n">entry</span><span class="o">-&gt;</span><span class="n">base_addr_high</span> <span class="o">&lt;&lt;</span> <span class="mi">32</span><span class="p">)</span> <span class="o">|</span> <span class="n">entry</span><span class="o">-&gt;</span><span class="n">base_addr_low</span><span class="p">;</span>
        <span class="kt">uint64_t</span> <span class="n">end64</span> <span class="o">=</span> <span class="n">start64</span> <span class="o">+</span> <span class="p">(((</span><span class="kt">uint64_t</span><span class="p">)</span><span class="n">entry</span><span class="o">-&gt;</span><span class="n">length_high</span> <span class="o">&lt;&lt;</span> <span class="mi">32</span><span class="p">)</span> <span class="o">|</span> <span class="n">entry</span><span class="o">-&gt;</span><span class="n">length_low</span><span class="p">);</span>
        <span class="p">...</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">entry</span><span class="o">-&gt;</span><span class="n">type</span> <span class="o">!=</span> <span class="n">E820_ARDS_TYPE_Memory</span> <span class="o">||</span> <span class="n">start64</span> <span class="o">&gt;=</span> <span class="n">PHYS_ADDR_LIMIT</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">continue</span><span class="p">;</span>
        <span class="p">}</span>
        <span class="p">...</span>
        <span class="kt">uint32_t</span> <span class="n">entry_start</span> <span class="o">=</span> <span class="n">ALIGN_UP</span><span class="p">((</span><span class="kt">uint32_t</span><span class="p">)</span><span class="n">start64</span><span class="p">,</span> <span class="n">PAGE_SIZE</span><span class="p">);</span>
        <span class="kt">uint32_t</span> <span class="n">entry_end</span> <span class="o">=</span> <span class="n">ALIGN_DOWN</span><span class="p">((</span><span class="kt">uint32_t</span><span class="p">)</span><span class="n">end64</span><span class="p">,</span> <span class="n">PAGE_SIZE</span><span class="p">);</span>

        <span class="n">block_list_add</span><span class="p">(</span><span class="o">&amp;</span><span class="n">ards_mem</span><span class="p">,</span> <span class="n">entry_start</span><span class="p">,</span> <span class="n">entry_end</span><span class="p">);</span>
</code></pre></div></div>

<p>Only type 1 ranges are kept, rounded <em>inward</em> to whole pages, since a partial page cannot be handed out.</p>

<h2 id="memblock-a-boot-time-allocator">memblock: a boot-time allocator</h2>

<p>The page tables and <code class="language-plaintext highlighter-rouge">mem_map</code> need memory before the page allocator exists. Linux solves this with <em>memblock</em> (older kernels used a bitmap-based bootmem allocator): two sorted lists of physical ranges, <em>memory</em> and <em>reserved</em>. A boot-time allocation is a first fit in “memory minus reserved”, and allocating just means adding a reserved range. toy-os does the same with two fixed arrays:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* ascending and non-overlapping */</span>
<span class="k">struct</span> <span class="n">phys_mem_list</span> <span class="p">{</span>
    <span class="k">struct</span> <span class="n">phys_mem_block</span> <span class="n">blocks</span><span class="p">[</span><span class="n">PHYS_MEM_MAX_BLOCKS</span><span class="p">];</span>
    <span class="kt">uint32_t</span> <span class="n">count</span><span class="p">;</span>
<span class="p">};</span>

<span class="cm">/* usable RAM from E820, filled once by e820_probe_phys_mem and never changed after */</span>
<span class="k">static</span> <span class="k">struct</span> <span class="n">phys_mem_list</span> <span class="n">ards_mem</span><span class="p">;</span>
<span class="cm">/* free mem = ards_mem - ards_rsved, will only be used once */</span>
<span class="k">static</span> <span class="k">struct</span> <span class="n">phys_mem_list</span> <span class="n">ards_rsved</span><span class="p">;</span>
</code></pre></div></div>

<ul>
  <li><code class="language-plaintext highlighter-rouge">block_list_add</code> inserts <code class="language-plaintext highlighter-rouge">[start, end)</code> and merges it with every block it overlaps or touches.</li>
  <li><code class="language-plaintext highlighter-rouge">block_list_remove</code> trims a block’s head or tail, deletes a fully covered block, or splits a block in two.</li>
  <li><code class="language-plaintext highlighter-rouge">phys_mem_alloc(size, align)</code> tries aligned candidates in each usable block. When a candidate overlaps a reserved block, it jumps past that block and tries again.</li>
  <li><code class="language-plaintext highlighter-rouge">phys_mem_for_each_free(fn)</code> walks both sorted lists in step and calls <code class="language-plaintext highlighter-rouge">fn</code> on each free gap. That is how everything ends up in the buddy allocator below.</li>
</ul>

<p>Right after probing, two ranges are reserved. The first is everything below <code class="language-plaintext highlighter-rouge">max(1 MiB, _end)</code>: the kernel, the BIOS areas, the dead loader and buffer, the stack. The second is the loader’s page tables at <code class="language-plaintext highlighter-rouge">[0x100000, 0x501000)</code> (post 3). They touch, so the list merges them into one block. Commit <a href="https://github.com/twicy/toy-os/commit/8625d4f"><code class="language-plaintext highlighter-rouge">8625d4f</code></a> printed the lists:</p>

<p><img src="/assets/images/posts/toy-os-6-buddy/memblock.png" alt="Bochs: usable memory 0x0-0x9F000 and 0x100000-0x20000000, reserved 0x0-0x501000, max_pfn 0x20000" /></p>

<p><em><code class="language-plaintext highlighter-rouge">dump_phys_mem()</code> at commit <a href="https://github.com/twicy/toy-os/commit/8625d4f"><code class="language-plaintext highlighter-rouge">8625d4f</code></a>. The 5 E820 entries became 2 usable blocks.</em></p>

<p><code class="language-plaintext highlighter-rouge">max_pfn = 0x20000</code> is one past the highest usable page frame number: 131072 frames of 4 KiB = 512 MiB.</p>

<h2 id="kernel-page-tables-for-all-of-ram">Kernel page tables for all of RAM</h2>

<p>The loader’s tables map only the first MiB. To reach any frame through a C pointer, the kernel maps <em>all</em> RAM at <code class="language-plaintext highlighter-rouge">0xC0000000</code>, the way Linux’s direct mapping (“low memory”) does. For 512 MiB that takes 128 page tables plus one page directory:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">init_pd</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
    <span class="kt">uint32_t</span> <span class="n">n_pts</span> <span class="o">=</span> <span class="n">DIV_ROUND_UP</span><span class="p">(</span><span class="n">max_pfn</span><span class="p">,</span> <span class="n">PT_ENTRIES</span><span class="p">);</span>
    <span class="kt">uint32_t</span> <span class="n">first_pde</span> <span class="o">=</span> <span class="n">PD_INDEX</span><span class="p">(</span><span class="n">KERNEL_OFFSET</span><span class="p">);</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">first_pde</span> <span class="o">+</span> <span class="n">n_pts</span> <span class="o">&gt;</span> <span class="n">PD_ENTRIES</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">PANIC</span><span class="p">(</span><span class="s">"%u MiB of RAM does not fit the kernel's 1 GiB of address space"</span><span class="p">,</span>
            <span class="n">max_pfn</span> <span class="o">&gt;&gt;</span> <span class="p">(</span><span class="mi">20</span> <span class="o">-</span> <span class="n">PAGE_SHIFT</span><span class="p">));</span>
    <span class="p">}</span>

    <span class="cm">/* Create a new kernel page dir, not colliding with loader's */</span>
    <span class="n">kernel_pd</span> <span class="o">=</span> <span class="n">early_alloc</span><span class="p">(</span><span class="n">PAGE_SIZE</span><span class="p">);</span>
    <span class="n">kernel_pts</span> <span class="o">=</span> <span class="n">early_alloc</span><span class="p">(</span><span class="n">n_pts</span> <span class="o">*</span> <span class="n">PAGE_SIZE</span><span class="p">);</span>
    <span class="n">memset</span><span class="p">(</span><span class="n">kernel_pd</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">PAGE_SIZE</span><span class="p">);</span>
    <span class="n">memset</span><span class="p">(</span><span class="n">kernel_pts</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">n_pts</span> <span class="o">*</span> <span class="n">PAGE_SIZE</span><span class="p">);</span>

    <span class="k">for</span> <span class="p">(</span><span class="kt">uint32_t</span> <span class="n">pfn</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">pfn</span> <span class="o">&lt;</span> <span class="n">max_pfn</span><span class="p">;</span> <span class="n">pfn</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">kernel_pts</span><span class="p">[</span><span class="n">pfn</span><span class="p">]</span> <span class="o">=</span> <span class="n">mk_pte</span><span class="p">(</span><span class="n">pfn</span> <span class="o">&lt;&lt;</span> <span class="n">PAGE_SHIFT</span><span class="p">,</span> <span class="n">PG_KERNEL</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">for</span> <span class="p">(</span><span class="kt">uint32_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">n_pts</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">kernel_pd</span><span class="p">[</span><span class="n">first_pde</span> <span class="o">+</span> <span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">mk_pde</span><span class="p">(</span><span class="n">virt_to_phys</span><span class="p">(</span><span class="o">&amp;</span><span class="n">kernel_pts</span><span class="p">[</span><span class="n">i</span> <span class="o">*</span> <span class="n">PT_ENTRIES</span><span class="p">]),</span> <span class="n">PG_KERNEL</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="cm">/* no identity map of low memory, so virtual 0 faults, and no recursive entry:
     * page tables are reached through the direct map from here on */</span>
    <span class="n">asm</span> <span class="k">volatile</span> <span class="p">(</span><span class="s">"mov %0, %%cr3"</span> <span class="o">:</span> <span class="o">:</span> <span class="s">"r"</span><span class="p">(</span><span class="n">virt_to_phys</span><span class="p">(</span><span class="n">kernel_pd</span><span class="p">))</span> <span class="o">:</span> <span class="s">"memory"</span><span class="p">);</span>

    <span class="cm">/* the CPU no longer walks the loader's page directory and page tables, give them back */</span>
    <span class="n">phys_mem_free</span><span class="p">(</span><span class="n">LOADER_PAGE_TABLES_START</span><span class="p">,</span> <span class="n">LOADER_PAGE_TABLES_END</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>There is a chicken-and-egg problem. <code class="language-plaintext highlighter-rouge">phys_mem_alloc</code> returns <code class="language-plaintext highlighter-rouge">0x501000</code>, the first free byte, but the CPU still walks the loader’s tables, which map nothing above 1 MiB. So <code class="language-plaintext highlighter-rouge">memset(kernel_pd, ...)</code> at <code class="language-plaintext highlighter-rouge">0xC0501000</code> would page-fault. <code class="language-plaintext highlighter-rouge">early_alloc</code> maps each new frame first, by writing a PTE into the loader’s tables through the recursive window from post 3:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* A brief recap of loader pt mapping:
 * PD starts at 0x100000, PTs start at 0x101000
 * PDE 0 and both PDE 768 point to PT0, which covers the first 1 MB
 * PDE 769 - 1022 are mapped to PT 769 - 1022, but PTEs are empty
 * PDE 1023 points to PD
 */</span>
<span class="cp">#define LOADER_PTES ((pte_t *)0xFFC00000)
</span>
<span class="cm">/* Mapping kernel_pd and kernel_pts virtual addr, for later arr + index access */</span>
<span class="k">static</span> <span class="kt">void</span> <span class="o">*</span><span class="nf">early_alloc</span><span class="p">(</span><span class="kt">uint32_t</span> <span class="n">size</span><span class="p">)</span> <span class="p">{</span>
    <span class="kt">uint32_t</span> <span class="n">phys</span> <span class="o">=</span> <span class="n">phys_mem_alloc</span><span class="p">(</span><span class="n">size</span><span class="p">,</span> <span class="n">PAGE_SIZE</span><span class="p">);</span>
    <span class="p">...</span>
    <span class="k">for</span> <span class="p">(</span><span class="kt">uint32_t</span> <span class="n">off</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">off</span> <span class="o">&lt;</span> <span class="n">size</span><span class="p">;</span> <span class="n">off</span> <span class="o">+=</span> <span class="n">PAGE_SIZE</span><span class="p">)</span> <span class="p">{</span>
        <span class="kt">uint32_t</span> <span class="n">va</span> <span class="o">=</span> <span class="p">(</span><span class="kt">uint32_t</span><span class="p">)</span><span class="n">phys_to_virt</span><span class="p">(</span><span class="n">phys</span> <span class="o">+</span> <span class="n">off</span><span class="p">);</span>
        <span class="cm">/* All PTs (arry of PTEs) are packed */</span>
        <span class="n">LOADER_PTES</span><span class="p">[</span><span class="n">va</span> <span class="o">&gt;&gt;</span> <span class="n">PAGE_SHIFT</span><span class="p">]</span> <span class="o">=</span> <span class="n">mk_pte</span><span class="p">(</span><span class="n">phys</span> <span class="o">+</span> <span class="n">off</span><span class="p">,</span> <span class="n">PG_KERNEL</span><span class="p">);</span>
        <span class="n">asm</span> <span class="k">volatile</span> <span class="p">(</span><span class="s">"invlpg (%0)"</span> <span class="o">:</span> <span class="o">:</span> <span class="s">"r"</span><span class="p">(</span><span class="n">va</span><span class="p">)</span> <span class="o">:</span> <span class="s">"memory"</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="n">phys_to_virt</span><span class="p">(</span><span class="n">phys</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>VA <code class="language-plaintext highlighter-rouge">0xC0501000</code> falls under PDE 769, and the loader created PT 769 in advance (empty), exactly for cases like this. As my commit note says, adding the new tables to the loader’s tables makes their virtual addresses valid, which saves working with physical addresses. Two details:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">kernel_pts</code> is one contiguous 512 KiB array, so the PTE for frame <code class="language-plaintext highlighter-rouge">pfn</code> is simply <code class="language-plaintext highlighter-rouge">kernel_pts[pfn]</code>.</li>
  <li>The flags are <code class="language-plaintext highlighter-rouge">PG_KERNEL</code>: present and writable, without the user bit the loader used.</li>
</ul>

<p>After <code class="language-plaintext highlighter-rouge">mov cr3</code>, Bochs sees a single mapping:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cr3: 0x000000501000
0x00000000c0000000-0x00000000dfffffff -&gt; 0x000000000000-0x00001fffffff
</code></pre></div></div>

<p><img src="/assets/images/posts/toy-os-6-buddy/kernel-va.svg" alt="The kernel's virtual address space: nothing below 0xC0000000, all 512 MiB of RAM at 0xC0000000-0xDFFFFFFF, nothing above" /></p>

<p><em>The kernel page directory: one direct map, nothing else. Compare with the loader’s six mappings in post 3.</em></p>

<p>The identity map is gone, so a NULL pointer dereference now page-faults, and the fault handler from post 5 names it. The recursive entry is gone too: page tables are now ordinary memory in the direct map. The <code class="language-plaintext highlighter-rouge">PANIC</code> in <code class="language-plaintext highlighter-rouge">init_pd</code> marks this design’s limit. With more than 1 GiB of RAM, the direct map would not fit in the kernel’s 1 GiB of virtual space. Linux stops its direct map at 896 MiB to keep 128 MiB free for vmalloc and temporary mappings, and treats the rest as <em>high memory</em> (ULK ch. 8). toy-os has no high memory.</p>

<h2 id="struct-page-and-mem_map">struct page and mem_map</h2>

<p>The buddy allocator needs a descriptor for every page frame: <code class="language-plaintext highlighter-rouge">struct page</code>, in one array called <code class="language-plaintext highlighter-rouge">mem_map</code>, indexed by page frame number (pfn):</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">page</span> <span class="p">{</span>
    <span class="kt">uint16_t</span> <span class="n">flags</span><span class="p">;</span>
    <span class="kt">uint16_t</span> <span class="n">order</span><span class="p">;</span>
    <span class="kt">int32_t</span> <span class="n">refcount</span><span class="p">;</span>
    <span class="cm">/* buddy: the free list of its order, slab: the partial list of its cache */</span>
    <span class="k">struct</span> <span class="n">list_head</span> <span class="n">lru</span><span class="p">;</span>
    <span class="cm">/* PG_slab only, a slab is exactly one page so these describe the whole slab */</span>
    <span class="k">struct</span> <span class="n">kmem_cache</span> <span class="o">*</span><span class="n">slab_cache</span><span class="p">;</span>
    <span class="kt">void</span> <span class="o">*</span><span class="n">freelist</span><span class="p">;</span>                 <span class="cm">/* first free object, NULL when the slab is full */</span>
    <span class="kt">uint32_t</span> <span class="n">inuse</span><span class="p">;</span>                 <span class="cm">/* objects handed out, 0 is empty, cache-&gt;objects is full */</span>
<span class="p">};</span>

<span class="k">extern</span> <span class="k">struct</span> <span class="n">page</span> <span class="o">*</span><span class="n">mem_map</span><span class="p">;</span>
<span class="cp">#define pfn_to_page(pfn) (mem_map + (pfn))
#define page_to_pfn(page) ((uint32_t)((page) - mem_map))
#define page_address(page) phys_to_virt(page_to_phys(page))
#define virt_to_page(va) pfn_to_page(virt_to_phys(va) &gt;&gt; PAGE_SHIFT)
</span></code></pre></div></div>

<p>In the buddy commit it was 16 bytes: <code class="language-plaintext highlighter-rouge">flags</code>, <code class="language-plaintext highlighter-rouge">order</code>, <code class="language-plaintext highlighter-rouge">refcount</code> and <code class="language-plaintext highlighter-rouge">lru</code>. The slab fields arrive in <a href="/blogs/os/toy-os-7-slab.html">post 7</a>, which makes it 28 bytes, and 131072 × 28 B = 3.5 MiB:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">init_page_alloc</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
    <span class="kt">uint32_t</span> <span class="n">size</span> <span class="o">=</span> <span class="n">max_pfn</span> <span class="o">*</span> <span class="k">sizeof</span><span class="p">(</span><span class="k">struct</span> <span class="n">page</span><span class="p">);</span>
    <span class="kt">uint32_t</span> <span class="n">phys</span> <span class="o">=</span> <span class="n">phys_mem_alloc</span><span class="p">(</span><span class="n">size</span><span class="p">,</span> <span class="n">PAGE_SIZE</span><span class="p">);</span>
    <span class="p">...</span>
    <span class="n">mem_map</span> <span class="o">=</span> <span class="n">phys_to_virt</span><span class="p">(</span><span class="n">phys</span><span class="p">);</span>

    <span class="cm">/* every frame starts reserved: holes, the kernel, page tables and mem_map itself
     * must never be handed out, only usable minus reserved is freed below */</span>
    <span class="k">for</span> <span class="p">(</span><span class="kt">uint32_t</span> <span class="n">pfn</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">pfn</span> <span class="o">&lt;</span> <span class="n">max_pfn</span><span class="p">;</span> <span class="n">pfn</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">struct</span> <span class="n">page</span> <span class="o">*</span><span class="n">page</span> <span class="o">=</span> <span class="n">pfn_to_page</span><span class="p">(</span><span class="n">pfn</span><span class="p">);</span>
        <span class="n">page</span><span class="o">-&gt;</span><span class="n">flags</span> <span class="o">=</span> <span class="n">PG_reserved</span><span class="p">;</span>
        <span class="p">...</span>
    <span class="p">}</span>
    <span class="p">...</span>
    <span class="cm">/* memblock is not used after this, from here on frames come from alloc_pages */</span>
    <span class="n">phys_mem_for_each_free</span><span class="p">(</span><span class="n">free_range</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">phys_mem_alloc</code> returns <code class="language-plaintext highlighter-rouge">0x100000</code> this time. <code class="language-plaintext highlighter-rouge">init_pd</code> has just freed the loader’s page tables, so <code class="language-plaintext highlighter-rouge">mem_map</code> moves into the first free spot: the old page tables. It has to be in the direct map (all of RAM is), because it is reached through a pointer.</p>

<h2 id="the-buddy-system">The buddy system</h2>

<p>The buddy system keeps free memory in blocks of 2<sup>order</sup> pages, order 0 to <code class="language-plaintext highlighter-rouge">MAX_ORDER</code> = 10 (4 KiB to 4 MiB). Every block is aligned to its own size, and there is one free list per order:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* Number of free blocks, not pages */</span>
<span class="k">struct</span> <span class="n">free_block</span> <span class="p">{</span>
    <span class="k">struct</span> <span class="n">list_head</span> <span class="n">free_list</span><span class="p">;</span>
    <span class="kt">uint32_t</span> <span class="n">nr_free</span><span class="p">;</span>
<span class="p">};</span>

<span class="cp">#define MAX_ORDER 10
</span>
<span class="cm">/* Order = pow(2, order) number of pages */</span>
<span class="k">struct</span> <span class="n">zone</span> <span class="p">{</span>
    <span class="k">struct</span> <span class="n">free_block</span> <span class="n">free_blocks</span><span class="p">[</span><span class="n">MAX_ORDER</span> <span class="o">+</span> <span class="mi">1</span><span class="p">];</span>
    <span class="kt">uint32_t</span> <span class="n">free_pages</span><span class="p">;</span>
<span class="p">};</span>
</code></pre></div></div>

<p>Only the <em>first</em> page of a free block is on a list. It carries <code class="language-plaintext highlighter-rouge">PG_buddy</code>, and its <code class="language-plaintext highlighter-rouge">order</code> field records the block size. The list link is <code class="language-plaintext highlighter-rouge">lru</code>, so the free lists take no memory of their own.</p>

<h3 id="who-is-my-buddy">Who is my buddy?</h3>

<p>The commit’s note lists three rules for two blocks to be buddies:</p>

<ol>
  <li>They have the same order.</li>
  <li>They are next to each other in physical memory.</li>
  <li>The lower one is aligned to 2<sup>order+1</sup> pages, so together they form a valid block one order up.</li>
</ol>

<p>Rules 2 and 3 mean the buddy of the block at <code class="language-plaintext highlighter-rouge">pfn</code> differs from <code class="language-plaintext highlighter-rouge">pfn</code> in exactly one bit, bit <code class="language-plaintext highlighter-rouge">order</code>. Flipping it gives the buddy:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* Flipping the bit is a clever trick! */</span>
<span class="k">static</span> <span class="k">struct</span> <span class="n">page</span> <span class="o">*</span><span class="nf">neigh_buddy</span><span class="p">(</span><span class="k">struct</span> <span class="n">page</span> <span class="o">*</span><span class="n">page</span><span class="p">,</span> <span class="kt">uint32_t</span> <span class="n">order</span><span class="p">)</span> <span class="p">{</span>
    <span class="kt">uint32_t</span> <span class="n">buddy_pfn</span> <span class="o">=</span> <span class="n">page_to_pfn</span><span class="p">(</span><span class="n">page</span><span class="p">)</span> <span class="o">^</span> <span class="p">(</span><span class="mi">1u</span> <span class="o">&lt;&lt;</span> <span class="n">order</span><span class="p">);</span>
    <span class="k">return</span> <span class="n">buddy_pfn</span> <span class="o">&lt;</span> <span class="n">max_pfn</span> <span class="o">?</span> <span class="n">pfn_to_page</span><span class="p">(</span><span class="n">buddy_pfn</span><span class="p">)</span> <span class="o">:</span> <span class="nb">NULL</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">static</span> <span class="kt">void</span> <span class="nf">free_one_page</span><span class="p">(</span><span class="k">struct</span> <span class="n">page</span> <span class="o">*</span><span class="n">page</span><span class="p">,</span> <span class="kt">uint32_t</span> <span class="n">order</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">while</span> <span class="p">(</span><span class="n">order</span> <span class="o">&lt;</span> <span class="n">MAX_ORDER</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">struct</span> <span class="n">page</span> <span class="o">*</span><span class="n">buddy</span> <span class="o">=</span> <span class="n">neigh_buddy</span><span class="p">(</span><span class="n">page</span><span class="p">,</span> <span class="n">order</span><span class="p">);</span>

        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">buddy</span> <span class="o">||</span> <span class="o">!</span><span class="p">(</span><span class="n">buddy</span><span class="o">-&gt;</span><span class="n">flags</span> <span class="o">&amp;</span> <span class="n">PG_buddy</span><span class="p">)</span> <span class="o">||</span> <span class="n">buddy</span><span class="o">-&gt;</span><span class="n">order</span> <span class="o">!=</span> <span class="n">order</span><span class="p">)</span>
            <span class="k">break</span><span class="p">;</span>
        <span class="n">remove_from_zone</span><span class="p">(</span><span class="n">buddy</span><span class="p">,</span> <span class="n">order</span><span class="p">);</span>
        <span class="cm">/* the merged block starts at the lower half */</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">buddy</span> <span class="o">&lt;</span> <span class="n">page</span><span class="p">)</span>
            <span class="n">page</span> <span class="o">=</span> <span class="n">buddy</span><span class="p">;</span>
        <span class="n">order</span><span class="o">++</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="n">add_to_zone</span><span class="p">(</span><span class="n">page</span><span class="p">,</span> <span class="n">order</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The test <code class="language-plaintext highlighter-rouge">PG_buddy &amp;&amp; order == order</code> covers rule 1: the buddy must be the head of a free block of the same size, not, say, the middle of a larger free block, or a page in use. Linux’s <code class="language-plaintext highlighter-rouge">__free_one_page</code> does the same with <code class="language-plaintext highlighter-rouge">page_is_buddy()</code>.</p>

<p>Allocation goes the other way. It takes the smallest non-empty order that is big enough, and while the block is too big, it splits it and puts the upper half back on the list one order down:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">page</span> <span class="o">*</span><span class="nf">alloc_pages</span><span class="p">(</span><span class="kt">uint32_t</span> <span class="n">order</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">for</span> <span class="p">(</span><span class="kt">uint32_t</span> <span class="n">o</span> <span class="o">=</span> <span class="n">order</span><span class="p">;</span> <span class="n">o</span> <span class="o">&lt;=</span> <span class="n">MAX_ORDER</span><span class="p">;</span> <span class="n">o</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">struct</span> <span class="n">free_block</span> <span class="o">*</span><span class="n">free_block</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">zone</span><span class="p">.</span><span class="n">free_blocks</span><span class="p">[</span><span class="n">o</span><span class="p">];</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">list_empty</span><span class="p">(</span><span class="o">&amp;</span><span class="n">free_block</span><span class="o">-&gt;</span><span class="n">free_list</span><span class="p">))</span>
            <span class="k">continue</span><span class="p">;</span>

        <span class="k">struct</span> <span class="n">page</span> <span class="o">*</span><span class="n">page</span> <span class="o">=</span> <span class="n">list_first_entry</span><span class="p">(</span><span class="o">&amp;</span><span class="n">free_block</span><span class="o">-&gt;</span><span class="n">free_list</span><span class="p">,</span> <span class="k">struct</span> <span class="n">page</span><span class="p">,</span> <span class="n">lru</span><span class="p">);</span>
        <span class="n">remove_from_zone</span><span class="p">(</span><span class="n">page</span><span class="p">,</span> <span class="n">o</span><span class="p">);</span>
        <span class="k">while</span> <span class="p">(</span><span class="n">o</span> <span class="o">&gt;</span> <span class="n">order</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">o</span><span class="o">--</span><span class="p">;</span>
            <span class="n">add_to_zone</span><span class="p">(</span><span class="n">page</span> <span class="o">+</span> <span class="p">(</span><span class="mi">1u</span> <span class="o">&lt;&lt;</span> <span class="n">o</span><span class="p">),</span> <span class="n">o</span><span class="p">);</span>
        <span class="p">}</span>
        <span class="n">page</span><span class="o">-&gt;</span><span class="n">refcount</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
        <span class="n">zone</span><span class="p">.</span><span class="n">free_pages</span> <span class="o">-=</span> <span class="p">(</span><span class="mi">1u</span> <span class="o">&lt;&lt;</span> <span class="n">order</span><span class="p">);</span>
        <span class="k">return</span> <span class="n">page</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="nb">NULL</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p><img src="/assets/images/posts/toy-os-6-buddy/split-merge.svg" alt="Splitting the order-2 block at pfn 0x584 to allocate order 1, then freeing it: 0x584 ^ 2 = 0x586 is a free order-1 buddy, so they merge; 0x584 ^ 4 = 0x580 is a page table, so merging stops" /></p>

<p><em>A split and a merge, with the real pfns <code class="language-plaintext highlighter-rouge">kmalloc(5000)</code> uses in post 7.</em></p>

<p><code class="language-plaintext highlighter-rouge">free_pages</code> checks what it can before giving a block back: the order is in range, the pfn is aligned to it, the page is not reserved, not already free, and not owned by the slab layer or <code class="language-plaintext highlighter-rouge">kmalloc</code>, and the refcount is exactly 1.</p>

<h3 id="filling-the-free-lists">Filling the free lists</h3>

<p>At boot, each free range from memblock goes through <code class="language-plaintext highlighter-rouge">free_range</code>, which carves it into the largest aligned blocks that fit:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* Only those page frames entirely contained are reclaimed */</span>
<span class="k">static</span> <span class="kt">void</span> <span class="nf">free_range</span><span class="p">(</span><span class="kt">uint32_t</span> <span class="n">start</span><span class="p">,</span> <span class="kt">uint32_t</span> <span class="n">end</span><span class="p">)</span> <span class="p">{</span>
    <span class="kt">uint32_t</span> <span class="n">pfn</span> <span class="o">=</span> <span class="n">PFN_UP</span><span class="p">(</span><span class="n">start</span><span class="p">);</span>
    <span class="kt">uint32_t</span> <span class="n">end_pfn</span> <span class="o">=</span> <span class="n">PFN_DOWN</span><span class="p">(</span><span class="n">end</span><span class="p">);</span>

    <span class="k">while</span> <span class="p">(</span><span class="n">pfn</span> <span class="o">&lt;</span> <span class="n">end_pfn</span><span class="p">)</span> <span class="p">{</span>
        <span class="cm">/* __builtin_ctz = count trailing zeros */</span>
        <span class="kt">uint32_t</span> <span class="n">order</span> <span class="o">=</span> <span class="n">pfn</span> <span class="o">?</span> <span class="n">MIN</span><span class="p">((</span><span class="kt">uint32_t</span><span class="p">)</span><span class="n">__builtin_ctz</span><span class="p">(</span><span class="n">pfn</span><span class="p">),</span> <span class="n">MAX_ORDER</span><span class="p">)</span> <span class="o">:</span> <span class="n">MAX_ORDER</span><span class="p">;</span>
        <span class="k">while</span> <span class="p">(</span><span class="n">pfn</span> <span class="o">+</span> <span class="p">(</span><span class="mi">1u</span> <span class="o">&lt;&lt;</span> <span class="n">order</span><span class="p">)</span> <span class="o">&gt;</span> <span class="n">end_pfn</span><span class="p">)</span>
            <span class="n">order</span><span class="o">--</span><span class="p">;</span>
        <span class="k">for</span> <span class="p">(</span><span class="kt">uint32_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="p">(</span><span class="mi">1u</span> <span class="o">&lt;&lt;</span> <span class="n">order</span><span class="p">);</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span>
            <span class="n">pfn_to_page</span><span class="p">(</span><span class="n">pfn</span> <span class="o">+</span> <span class="n">i</span><span class="p">)</span><span class="o">-&gt;</span><span class="n">flags</span> <span class="o">&amp;=</span> <span class="o">~</span><span class="n">PG_reserved</span><span class="p">;</span>
        <span class="n">zone</span><span class="p">.</span><span class="n">free_pages</span> <span class="o">+=</span> <span class="p">(</span><span class="mi">1u</span> <span class="o">&lt;&lt;</span> <span class="n">order</span><span class="p">);</span>
        <span class="n">free_one_page</span><span class="p">(</span><span class="n">pfn_to_page</span><span class="p">(</span><span class="n">pfn</span><span class="p">),</span> <span class="n">order</span><span class="p">);</span>
        <span class="n">pfn</span> <span class="o">+=</span> <span class="mi">1u</span> <span class="o">&lt;&lt;</span> <span class="n">order</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The number of trailing zero bits of <code class="language-plaintext highlighter-rouge">pfn</code> is the largest order it is aligned to. The inner loop shrinks the block until it fits in what is left. Starting from pfn <code class="language-plaintext highlighter-rouge">0x582</code> (<code class="language-plaintext highlighter-rouge">...0010</code>), that gives order 1, then <code class="language-plaintext highlighter-rouge">0x584</code> order 2, <code class="language-plaintext highlighter-rouge">0x588</code> order 3, and so on, up to <code class="language-plaintext highlighter-rouge">0x800</code>, where order 10 takes over. The comment and my third commit note point out the limit of <code class="language-plaintext highlighter-rouge">PFN_UP</code>/<code class="language-plaintext highlighter-rouge">PFN_DOWN</code>: a range that starts or ends in the middle of a page loses that partial page. With E820 entries and reservations that are all page aligned, nothing is lost here.</p>

<p>I read the resulting free lists straight out of <code class="language-plaintext highlighter-rouge">zone</code> in the Bochs debugger. The struct addresses on the lists convert back to pfns:</p>

<p><img src="/assets/images/posts/toy-os-6-buddy/free-lists.svg" alt="The free lists after boot: one block each for orders 0 to 7 except 8, one order-9 block at 0x600, and 126 order-10 blocks from 0x1FC00 down to 0x800" /></p>

<p><em><code class="language-plaintext highlighter-rouge">zone.free_blocks</code> after <code class="language-plaintext highlighter-rouge">init_page_alloc()</code>. <code class="language-plaintext highlighter-rouge">list_add</code> inserts at the head, so the last block freed, the highest, comes first.</em></p>

<h2 id="physical-memory-after-init_page_alloc">Physical memory after init_page_alloc()</h2>

<p><img src="/assets/images/posts/toy-os-6-buddy/phys-after-buddy.svg" alt="Physical memory: low 1 MiB reserved, mem_map from 1 MiB to 0x480000, 129 free pages up to 0x501000, the kernel PD and 128 PTs up to 0x582000, then free RAM to 512 MiB" /></p>

<p><em>The final physical layout. Everything white is now in the buddy allocator. memblock’s reserved list ends as <code class="language-plaintext highlighter-rouge">[0, 0x480000)</code> and <code class="language-plaintext highlighter-rouge">[0x501000, 0x582000)</code>.</em></p>

<table>
  <thead>
    <tr>
      <th>Physical</th>
      <th>Size</th>
      <th>What</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">0x00000000</code>–<code class="language-plaintext highlighter-rouge">0x000FFFFF</code></td>
      <td>1 MiB</td>
      <td>reserved: BIOS areas, kernel image, stack (post 4)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">0x00100000</code>–<code class="language-plaintext highlighter-rouge">0x0047FFFF</code></td>
      <td>3.5 MiB</td>
      <td><code class="language-plaintext highlighter-rouge">mem_map</code>, where the loader page tables were</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">0x00480000</code>–<code class="language-plaintext highlighter-rouge">0x00500FFF</code></td>
      <td>129 pages</td>
      <td>free: an order-7 block and one order-0 page</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">0x00501000</code>–<code class="language-plaintext highlighter-rouge">0x00501FFF</code></td>
      <td>4 KiB</td>
      <td>kernel page directory (CR3)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">0x00502000</code>–<code class="language-plaintext highlighter-rouge">0x00581FFF</code></td>
      <td>512 KiB</td>
      <td>128 kernel page tables</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">0x00582000</code>–<code class="language-plaintext highlighter-rouge">0x1FFFFFFF</code></td>
      <td>~506 MiB</td>
      <td>free</td>
    </tr>
  </tbody>
</table>

<p>131072 frames − 256 (low MiB) − 896 (<code class="language-plaintext highlighter-rouge">mem_map</code>) − 129 (page tables) = <strong>129791</strong> free pages, which is exactly what the kernel prints:</p>

<p><img src="/assets/images/posts/toy-os-6-buddy/page-alloc.png" alt="Bochs: page allocator: 129791 free pages, mem_map at phys 0x100000" /></p>

<p><em>The second line on the screen.</em></p>

<p>The page allocator only hands out whole pages, though, and most kernel objects are a few dozen bytes. <a href="/blogs/os/toy-os-7-slab.html">Post 7</a> builds slab caches and <code class="language-plaintext highlighter-rouge">kmalloc</code> on top of it.</p>

<h2 id="references">References</h2>

<ol>
  <li>D. P. Bovet and M. Cesati, <em>Understanding the Linux Kernel</em>, 3rd ed., ch. 8 “Memory Management”: page descriptors, memory zones, high memory, and “The Buddy System Algorithm”. Also ch. 2, “Paging in Linux”: the kernel’s final page tables and the 896 MiB limit.</li>
  <li>郑钢, 《操作系统真相还原》, ch. 5 (获取物理内存容量: E820, E801 and 88h) and ch. 8 (内存管理系统). The book manages memory with bitmaps; toy-os follows Linux here.</li>
  <li>navi, <a href="https://segmentfault.com/a/1190000040203948">从零开始写 OS 内核 - 虚拟内存完善</a>, segmentfault.</li>
  <li>Linux source: <code class="language-plaintext highlighter-rouge">mm/memblock.c</code>, and <code class="language-plaintext highlighter-rouge">__free_one_page()</code> and <code class="language-plaintext highlighter-rouge">__find_buddy_pfn()</code> in <code class="language-plaintext highlighter-rouge">mm/page_alloc.c</code>.</li>
  <li>OSDev Wiki, <a href="https://wiki.osdev.org/Detecting_Memory_(x86)">Detecting Memory (x86)</a>.</li>
</ol>]]></content><author><name>Yuchen Tang</name></author><summary type="html"><![CDATA[The sixth toy-os worklog takes over the rest of RAM. The loader asks the BIOS for the E820 memory map, which in Bochs has five entries. A memblock-style boot allocator keeps sorted usable and reserved lists and places the kernel's new page tables, which map all 512 MiB at 0xC0000000. They are mapped first through the loader's recursive page-table window. The loader's 4 MiB of page tables is then freed, and the struct page array lands exactly there. Every free frame goes to a buddy allocator with orders 0 to 10, which finds buddies by flipping one bit of the page frame number. Maps and the free lists, read out of Bochs, show where all of it ends up.]]></summary></entry><entry><title type="html">Toy OS 5: Interrupts, the PIC and the Timer</title><link href="https://twicy.github.io/blogs/os/toy-os-5-interrupts.html" rel="alternate" type="text/html" title="Toy OS 5: Interrupts, the PIC and the Timer" /><published>2026-10-14T00:00:00-07:00</published><updated>2026-10-14T00:00:00-07:00</updated><id>https://twicy.github.io/blogs/os/toy-os-5-interrupts</id><content type="html" xml:base="https://twicy.github.io/blogs/os/toy-os-5-interrupts.html"><![CDATA[<h1 id="toy-os-5-interrupts-the-pic-and-the-timer">Toy OS 5: Interrupts, the PIC and the Timer</h1>

<p><em>Toy OS worklog: <a href="/blogs/os/toy-os-1-real-mode.html">1. Real mode</a> · <a href="/blogs/os/toy-os-2-segmentation.html">2. Segmentation</a> · <a href="/blogs/os/toy-os-3-paging.html">3. Paging</a> · <a href="/blogs/os/toy-os-4-kernel.html">4. Kernel</a> · <strong>5. Interrupts</strong> · <a href="/blogs/os/toy-os-6-buddy.html">6. Buddy</a> · <a href="/blogs/os/toy-os-7-slab.html">7. Slab</a> · <a href="https://github.com/twicy/toy-os/tree/fc88a1de36e8cd2cf495c46f3e6b36c01fc94f8b">Source</a></em></p>

<p>The kernel prints (post 4), but it runs with <code class="language-plaintext highlighter-rouge">IF = 0</code>, the way the loader left it, and it has no IDT. A divide by zero would end in a triple fault and a reboot. This post adds the Interrupt Descriptor Table, a common entry path for all 256 vectors, the 8259A interrupt controller and a 100 Hz timer. (These were the commits <a href="https://github.com/twicy/toy-os/commit/18ae789"><code class="language-plaintext highlighter-rouge">18ae789</code></a>, <a href="https://github.com/twicy/toy-os/commit/14a9b9a"><code class="language-plaintext highlighter-rouge">14a9b9a</code></a> and <a href="https://github.com/twicy/toy-os/commit/4a3564b"><code class="language-plaintext highlighter-rouge">4a3564b</code></a>.)</p>

<h2 id="table-of-contents">Table of Contents</h2>

<ul>
  <li><a href="#vectors">Vectors</a></li>
  <li><a href="#the-idt">The IDT</a></li>
  <li><a href="#256-stubs-and-one-common-path">256 stubs and one common path</a></li>
  <li><a href="#a-first-test-int3">A first test: int3</a></li>
  <li><a href="#the-8259a-pic">The 8259A PIC</a></li>
  <li><a href="#the-pit-and-a-100-hz-tick">The PIT and a 100 Hz tick</a></li>
  <li><a href="#panic-turns-interrupts-off-first">PANIC turns interrupts off first</a></li>
  <li><a href="#references">References</a></li>
</ul>

<h2 id="vectors">Vectors</h2>

<p>Every interrupt or exception arrives as a vector number, 0–255:</p>

<ul>
  <li><strong>0–31: CPU exceptions.</strong> Intel reserves all of them, defined or not. Examples: 0 divide error, 3 breakpoint, 6 invalid opcode, 8 double fault, 13 general protection, 14 page fault. Some of them (8, 10–14, 17, 21) push an <em>error code</em> on the stack. The others do not.</li>
  <li><strong>External interrupts</strong> come from devices through an interrupt controller, which the OS programs to use vectors of its choosing. toy-os uses 0x20–0x2F.</li>
  <li><strong>Software interrupts</strong>: <code class="language-plaintext highlighter-rouge">int n</code> raises vector <em>n</em>. <code class="language-plaintext highlighter-rouge">int3</code> (one byte, <code class="language-plaintext highlighter-rouge">0xCC</code>) is the breakpoint.</li>
</ul>

<p>Most exceptions are either <em>faults</em>, where the saved EIP points at the instruction that faulted so it can be retried (page fault), or <em>traps</em>, where it points at the next instruction (<code class="language-plaintext highlighter-rouge">int3</code>). A few are <em>aborts</em> (double fault, machine check), which cannot be resumed at all.</p>

<h2 id="the-idt">The IDT</h2>

<p>The IDT works like the GDT (post 2): a table of 8-byte descriptors, found through a register, IDTR. Each entry is a <em>gate</em> that holds a <code class="language-plaintext highlighter-rouge">selector:offset</code> pointing at the handler:</p>

<p><img src="/assets/images/posts/toy-os-5-interrupts/gate.svg" alt="An interrupt gate: offset bits 31-16, P, DPL, type 1110, then the code segment selector 0x0008 and offset bits 15-0" /></p>

<p><em>The gate for vector 0x20, as Bochs’s <code class="language-plaintext highlighter-rouge">info idt</code> decodes it below.</em></p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* With only Interrupt Gate Descriptors, nested interrupts cannot happen
 * Trap Gate Descriptors does not clear IF so interrupts are not masked
 */</span>
<span class="k">struct</span> <span class="n">interrupt_descriptor</span> <span class="p">{</span>
    <span class="kt">uint16_t</span> <span class="n">offset_15_0</span><span class="p">;</span>
    <span class="kt">uint16_t</span> <span class="n">segment_selector</span><span class="p">;</span>
    <span class="kt">uint8_t</span> <span class="n">zero</span><span class="p">;</span>
    <span class="kt">uint8_t</span> <span class="n">type_attr</span><span class="p">;</span>
    <span class="kt">uint16_t</span> <span class="n">offset_31_16</span><span class="p">;</span>
<span class="p">}</span> <span class="n">__attribute__</span><span class="p">((</span><span class="n">packed</span><span class="p">));</span>

<span class="cm">/* P = 1, DPL = 00b, Type = 1110b */</span>
<span class="cp">#define INTER_DESC_TYPE_DEFAULT 0x8E
</span>
<span class="kt">void</span> <span class="nf">init_idt</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">IDT_SIZE</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">idt</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">offset_15_0</span> <span class="o">=</span> <span class="n">intr_stubs</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">&amp;</span> <span class="mh">0xFFFF</span><span class="p">;</span>
        <span class="cm">/* Index + TI + DPL */</span>
        <span class="n">idt</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">segment_selector</span> <span class="o">=</span> <span class="mi">0</span><span class="n">b1000</span><span class="p">;</span>
        <span class="n">idt</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">zero</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
        <span class="n">idt</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">type_attr</span> <span class="o">=</span> <span class="n">INTER_DESC_TYPE_DEFAULT</span><span class="p">;</span>
        <span class="n">idt</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">offset_31_16</span> <span class="o">=</span> <span class="n">intr_stubs</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">&gt;&gt;</span> <span class="mi">16</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="k">struct</span> <span class="n">idt_ptr</span> <span class="n">idtr</span><span class="p">;</span>
    <span class="n">idtr</span><span class="p">.</span><span class="n">limit</span> <span class="o">=</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">idt</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span><span class="p">;</span>
    <span class="n">idtr</span><span class="p">.</span><span class="n">base</span> <span class="o">=</span> <span class="p">(</span><span class="kt">uint32_t</span><span class="p">)</span><span class="n">idt</span><span class="p">;</span>
    <span class="n">asm</span> <span class="k">volatile</span> <span class="p">(</span><span class="s">"lidt %0"</span> <span class="o">:</span> <span class="o">:</span> <span class="s">"m"</span> <span class="p">(</span><span class="n">idtr</span><span class="p">));</span>
<span class="p">}</span>
</code></pre></div></div>

<p>All 256 gates are 32-bit <strong>interrupt gates</strong>. Entering one clears IF, so a handler cannot be interrupted, and nesting is impossible for now. A trap gate (type <code class="language-plaintext highlighter-rouge">1111</code>) would leave IF alone. The table is 2 KiB of <code class="language-plaintext highlighter-rouge">.bss</code> at <code class="language-plaintext highlighter-rouge">0xC0007E20</code>:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;bochs&gt; info idt 0 4
Interrupt Descriptor Table (base=0x00000000c0007e20, limit=2047):
IDT[0x00]=32-Bit Interrupt Gate target=0x0008:0xc0004780, DPL=0
IDT[0x01]=32-Bit Interrupt Gate target=0x0008:0xc0004789, DPL=0
IDT[0x02]=32-Bit Interrupt Gate target=0x0008:0xc0004792, DPL=0
...
&lt;bochs&gt; info idt 32 34
IDT[0x20]=32-Bit Interrupt Gate target=0x0008:0xc0004890, DPL=0
IDT[0x21]=32-Bit Interrupt Gate target=0x0008:0xc0004899, DPL=0
</code></pre></div></div>

<p><strong>Two privilege checks</strong>, from my notes on the commit (ULK ch. 4 walks through them):</p>

<ol>
  <li>The handler’s code segment DPL must be ≤ CPL. A handler can never be <em>less</em> privileged than the code it interrupts. All gates use selector <code class="language-plaintext highlighter-rouge">0x08</code>, the ring 0 kernel code, so this always passes.</li>
  <li>For a <em>software</em> interrupt (<code class="language-plaintext highlighter-rouge">int n</code>, <code class="language-plaintext highlighter-rouge">int3</code>, <code class="language-plaintext highlighter-rouge">into</code>), CPL ≤ gate DPL, or the CPU raises #GP instead. Every gate here has DPL 0, so a ring 3 program cannot <code class="language-plaintext highlighter-rouge">int 0x20</code> and pretend to be the timer. A system call gate will be the one gate with DPL 3. Hardware interrupts and CPU exceptions skip this check.</li>
</ol>

<h2 id="256-stubs-and-one-common-path">256 stubs and one common path</h2>

<p>The CPU tells the handler nothing about <em>which</em> vector brought it there, and only some exceptions push an error code. So each vector gets a tiny stub that makes the stack look the same every time. nasm’s <code class="language-plaintext highlighter-rouge">%rep</code> writes all 256:</p>

<div class="language-nasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">%define HAS_ERRCODE(v) ((v) == 8 || ((v) &gt;= 10 &amp;&amp; (v) &lt;= 14) || (v) == 17 || (v) == 21)
</span>
<span class="nf">section</span> <span class="nv">.text</span>

<span class="cp">%assign vec 0
%rep 256
</span><span class="nf">stub_</span><span class="o">%</span><span class="p">[</span><span class="nv">vec</span><span class="p">]:</span>
<span class="c1">; If this vec has no err code, push placeholder</span>
<span class="cp">%if !HAS_ERRCODE(vec)
</span>    <span class="nf">push</span> <span class="mi">0</span>
<span class="cp">%endif
</span>    <span class="nf">push</span> <span class="nv">vec</span>
    <span class="nf">jmp</span> <span class="nv">intr_common</span>
<span class="cp">%assign vec vec + 1
%endrep
</span>
<span class="nl">intr_common:</span>
    <span class="c1">; Predefined order: eax, ecx, edx, ebx, esp, ebp, esi, edi</span>
    <span class="nf">pushad</span>
    <span class="nf">push</span> <span class="nb">esp</span>
    <span class="nf">cld</span>
    <span class="nf">call</span> <span class="nv">intr_dispatch</span>
    <span class="nf">add</span> <span class="nb">esp</span><span class="p">,</span> <span class="mi">4</span>
    <span class="nf">popad</span>
    <span class="c1">; the err code/placeholder + the vec</span>
    <span class="nf">add</span> <span class="nb">esp</span><span class="p">,</span> <span class="mi">8</span>
    <span class="nf">iretd</span>

<span class="nf">section</span> <span class="nv">.rodata</span>
<span class="nl">intr_stubs:</span>
<span class="cp">%assign vec 0
%rep 256
</span>    <span class="kd">dd</span> <span class="nv">stub_</span><span class="o">%</span><span class="p">[</span><span class="nv">vec</span><span class="p">]</span>
<span class="cp">%assign vec vec + 1
%endrep
</span></code></pre></div></div>

<p>A stub is 9 bytes (<code class="language-plaintext highlighter-rouge">push 0</code>, <code class="language-plaintext highlighter-rouge">push imm8</code>, <code class="language-plaintext highlighter-rouge">jmp rel32</code>). The 8 vectors with an error code skip <code class="language-plaintext highlighter-rouge">push 0</code> and take 7. That explains the IDT targets above: <code class="language-plaintext highlighter-rouge">0xC0004780</code> + 24 × 9 + 8 × 7 = <code class="language-plaintext highlighter-rouge">0xC0004890</code> for vector 0x20. <code class="language-plaintext highlighter-rouge">intr_stubs</code> is the table of stub addresses that <code class="language-plaintext highlighter-rouge">init_idt</code> copies into the gates.</p>

<p>When <code class="language-plaintext highlighter-rouge">intr_dispatch</code> starts, the stack holds a complete frame. <code class="language-plaintext highlighter-rouge">push esp</code> passes its address as the argument, and the C struct below describes it:</p>

<p><img src="/assets/images/posts/toy-os-5-interrupts/intr-frame.svg" alt="The kernel stack at intr_dispatch: eflags, cs, eip pushed by the CPU, err_code, vector from the stub, eight registers from pushad, then the argument f and the return address" /></p>

<p><em>Who pushes what. <code class="language-plaintext highlighter-rouge">user_esp</code> and <code class="language-plaintext highlighter-rouge">user_ss</code> are only there when the interrupt came from ring 3, which cannot happen yet.</em></p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">intr_frame</span> <span class="p">{</span>
    <span class="kt">uint32_t</span> <span class="n">edi</span><span class="p">,</span> <span class="n">esi</span><span class="p">,</span> <span class="n">ebp</span><span class="p">,</span> <span class="n">esp_dummy</span><span class="p">,</span> <span class="n">ebx</span><span class="p">,</span> <span class="n">edx</span><span class="p">,</span> <span class="n">ecx</span><span class="p">,</span> <span class="n">eax</span><span class="p">;</span>
    <span class="kt">uint32_t</span> <span class="n">vector</span><span class="p">,</span> <span class="n">err_code</span><span class="p">;</span>
    <span class="kt">uint32_t</span> <span class="n">eip</span><span class="p">,</span> <span class="n">cs</span><span class="p">,</span> <span class="n">eflags</span><span class="p">;</span>
    <span class="kt">uint32_t</span> <span class="n">user_esp</span><span class="p">,</span> <span class="n">user_ss</span><span class="p">;</span>
<span class="p">};</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">cld</code> matters. The C calling convention guarantees the direction flag is clear when a function is called, but an interrupt can arrive in the middle of code that set it, such as a backwards <code class="language-plaintext highlighter-rouge">rep movsb</code>. Without <code class="language-plaintext highlighter-rouge">cld</code>, any <code class="language-plaintext highlighter-rouge">memcpy</code> in a handler would run backwards.</p>

<p>The C side is a table of function pointers, plus a fallback that names the exception and halts:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">intr_dispatch</span><span class="p">(</span><span class="k">struct</span> <span class="n">intr_frame</span> <span class="o">*</span><span class="n">f</span><span class="p">)</span> <span class="p">{</span>
    <span class="cm">/* Fall thru APIC vector range */</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">f</span><span class="o">-&gt;</span><span class="n">vector</span> <span class="o">&gt;=</span> <span class="n">PIC_IRQ_BASE</span> <span class="o">&amp;&amp;</span> <span class="n">f</span><span class="o">-&gt;</span><span class="n">vector</span> <span class="o">&lt;</span> <span class="n">PIC_IRQ_BASE</span> <span class="o">+</span> <span class="n">PIC_IRQ_COUNT</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">pic_ack</span><span class="p">(</span><span class="n">f</span><span class="o">-&gt;</span><span class="n">vector</span> <span class="o">-</span> <span class="n">PIC_IRQ_BASE</span><span class="p">))</span> <span class="p">{</span>
            <span class="k">return</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">intr_handlers</span><span class="p">[</span><span class="n">f</span><span class="o">-&gt;</span><span class="n">vector</span><span class="p">])</span> <span class="p">{</span>
        <span class="n">intr_handlers</span><span class="p">[</span><span class="n">f</span><span class="o">-&gt;</span><span class="n">vector</span><span class="p">](</span><span class="n">f</span><span class="p">);</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="n">unhandled_intr</span><span class="p">(</span><span class="n">f</span><span class="p">);</span>  <span class="c1">// "exception 13 (general protection), err 0x..., eip 0x..."</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="a-first-test-int3">A first test: int3</h2>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kt">void</span> <span class="nf">on_breakpoint</span><span class="p">(</span><span class="k">struct</span> <span class="n">intr_frame</span> <span class="o">*</span><span class="n">f</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">printk</span><span class="p">(</span><span class="s">"int3 at eip %#x</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">f</span><span class="o">-&gt;</span><span class="n">eip</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">...</span>
    <span class="n">register_handler</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span> <span class="n">on_breakpoint</span><span class="p">);</span>
    <span class="n">asm</span> <span class="nf">volatile</span> <span class="p">(</span><span class="s">"int $3"</span><span class="p">);</span>
    <span class="n">printk</span><span class="p">(</span><span class="s">"back from int3</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>
</code></pre></div></div>

<p>It prints <code class="language-plaintext highlighter-rouge">int3 at eip 0xc0001815</code>, yet <code class="language-plaintext highlighter-rouge">int3</code> itself is at <code class="language-plaintext highlighter-rouge">0xC0001814</code>:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>c0001814:	int3
c0001815:	sub    $0xc,%esp
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">int3</code> is a trap, so the saved EIP is the <em>next</em> instruction, and <code class="language-plaintext highlighter-rouge">iretd</code> resumes there: “back from int3”. A debugger that patches <code class="language-plaintext highlighter-rouge">0xCC</code> over an instruction has to back EIP up by one to re-run it.</p>

<h2 id="the-8259a-pic">The 8259A PIC</h2>

<p>Device interrupts go through two Intel 8259A chips. The <em>master</em> takes IRQ 0–7. The <em>slave</em> takes IRQ 8–15 and signals the master on the master’s IRQ 2. The CPU sees one interrupt line (INTR), asks the PIC for a vector, and goes through the IDT:</p>

<p><img src="/assets/images/posts/toy-os-5-interrupts/intr-path.svg" alt="IRQ sources go through the master and slave 8259A to the CPU, which looks up IDT[n] and runs stub_n, intr_common, intr_dispatch and the handler, then returns with iretd" /></p>

<p><em>From a device to a C handler and back.</em></p>

<p>The BIOS leaves the master on vectors 0x08–0x0F. Those are CPU exceptions in protected mode: an untouched timer IRQ 0 would arrive as vector 8, a “double fault”. So the first job is to remap both chips, with four initialization command words (ICW) each:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">init_pic</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
    <span class="cm">/* ICW1 on the command port, ICW2-4 then follow on the data port */</span>
    <span class="n">outb</span><span class="p">(</span><span class="n">PIC_MASTER_CMD</span><span class="p">,</span> <span class="n">ICW1_INIT_ICW4</span><span class="p">);</span>   <span class="c1">// 0x11: init, cascaded, edge, ICW4 follows</span>
    <span class="n">outb</span><span class="p">(</span><span class="n">PIC_SLAVE_CMD</span><span class="p">,</span> <span class="n">ICW1_INIT_ICW4</span><span class="p">);</span>

    <span class="cm">/* ICW2: vector of the chip's IRQ 0, the low 3 bits must be 0 */</span>
    <span class="n">outb</span><span class="p">(</span><span class="n">PIC_MASTER_DATA</span><span class="p">,</span> <span class="n">PIC_IRQ_BASE</span><span class="p">);</span>        <span class="c1">// 0x20</span>
    <span class="n">outb</span><span class="p">(</span><span class="n">PIC_SLAVE_DATA</span><span class="p">,</span> <span class="n">PIC_IRQ_BASE</span> <span class="o">+</span> <span class="mi">8</span><span class="p">);</span>     <span class="c1">// 0x28</span>

    <span class="cm">/* ICW3: the master gets a bitmask of the pins with a slave on them,
     * the slave gets the number of the master pin it hangs off */</span>
    <span class="n">outb</span><span class="p">(</span><span class="n">PIC_MASTER_DATA</span><span class="p">,</span> <span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">IRQ_CASCADE</span><span class="p">);</span>
    <span class="n">outb</span><span class="p">(</span><span class="n">PIC_SLAVE_DATA</span><span class="p">,</span> <span class="n">IRQ_CASCADE</span><span class="p">);</span>

    <span class="n">outb</span><span class="p">(</span><span class="n">PIC_MASTER_DATA</span><span class="p">,</span> <span class="n">ICW4_8086</span><span class="p">);</span>       <span class="c1">// 8086 mode, EOI sent by software</span>
    <span class="n">outb</span><span class="p">(</span><span class="n">PIC_SLAVE_DATA</span><span class="p">,</span> <span class="n">ICW4_8086</span><span class="p">);</span>

    <span class="cm">/* OCW1: mask everything but the cascade, drivers unmask their own IRQ */</span>
    <span class="n">outb</span><span class="p">(</span><span class="n">PIC_MASTER_DATA</span><span class="p">,</span> <span class="p">(</span><span class="kt">uint8_t</span><span class="p">)</span><span class="o">~</span><span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">IRQ_CASCADE</span><span class="p">));</span>
    <span class="n">outb</span><span class="p">(</span><span class="n">PIC_SLAVE_DATA</span><span class="p">,</span> <span class="mh">0xFF</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>(The real code has an <code class="language-plaintext highlighter-rouge">io_wait()</code>, a write to the unused POST port <code class="language-plaintext highlighter-rouge">0x80</code>, after each ICW, because real 8259As need a moment between them.)</p>

<p>After delivering an IRQ, the PIC holds back IRQs of equal or lower priority until it gets an end-of-interrupt (EOI, <code class="language-plaintext highlighter-rouge">0x20</code> on the command port). An IRQ from the slave is in service on <em>both</em> chips, so both need an EOI. <code class="language-plaintext highlighter-rouge">intr_dispatch</code> sends it before calling the handler. That is safe because the interrupt gate has cleared IF: a new IRQ can be raised at the PIC but will not reach the CPU before <code class="language-plaintext highlighter-rouge">iretd</code>.</p>

<p><code class="language-plaintext highlighter-rouge">pic_ack</code> also catches <em>spurious</em> IRQs. If a device drops its line before the CPU acknowledges it, the PIC still has to answer with a vector, so it hands out its lowest-priority one: IRQ 7 (master) or 15 (slave). It does not mark it in service, though, so reading the in-service register tells the two apart:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span> <span class="nf">pic_ack</span><span class="p">(</span><span class="kt">uint8_t</span> <span class="n">irq</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">irq</span> <span class="o">==</span> <span class="mi">7</span> <span class="o">||</span> <span class="n">irq</span> <span class="o">==</span> <span class="mi">15</span><span class="p">)</span> <span class="p">{</span>
        <span class="kt">uint16_t</span> <span class="n">cmd</span> <span class="o">=</span> <span class="n">irq</span> <span class="o">==</span> <span class="mi">7</span> <span class="o">?</span> <span class="n">PIC_MASTER_CMD</span> <span class="o">:</span> <span class="n">PIC_SLAVE_CMD</span><span class="p">;</span>
        <span class="n">outb</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">OCW3_READ_ISR</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="p">(</span><span class="n">inb</span><span class="p">(</span><span class="n">cmd</span><span class="p">)</span> <span class="o">&amp;</span> <span class="mh">0x80</span><span class="p">))</span> <span class="p">{</span>
            <span class="cm">/* The master did see the cascade IRQ 2, only the slave's was fake */</span>
            <span class="k">if</span> <span class="p">(</span><span class="n">irq</span> <span class="o">==</span> <span class="mi">15</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">outb</span><span class="p">(</span><span class="n">PIC_MASTER_CMD</span><span class="p">,</span> <span class="n">OCW2_EOI</span><span class="p">);</span>
            <span class="p">}</span>
            <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="cm">/* An IRQ from the slave is in service on both chips */</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">irq</span> <span class="o">&gt;=</span> <span class="mi">8</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">outb</span><span class="p">(</span><span class="n">PIC_SLAVE_CMD</span><span class="p">,</span> <span class="n">OCW2_EOI</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="n">outb</span><span class="p">(</span><span class="n">PIC_MASTER_CMD</span><span class="p">,</span> <span class="n">OCW2_EOI</span><span class="p">);</span>
    <span class="k">return</span> <span class="mi">1</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="the-pit-and-a-100-hz-tick">The PIT and a 100 Hz tick</h2>

<p>The 8253/8254 programmable interval timer counts down from a divisor at 1.193182 MHz. In mode 2 (rate generator), channel 0 raises IRQ 0 every time it reaches zero and reloads:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#define PIT_INPUT_HZ 1193182
</span><span class="cm">/* Channel 0 (bits 7-6 = 00), low byte then high byte (bits 5-4 = 11),
 * mode 2 rate generator (bits 3-1 = 010), binary count (bit 0 = 0) */</span>
<span class="cp">#define PIT_CMD_CH0_RATE 0x34
</span>
<span class="cm">/* volatile: an IRQ changes it behind the back of any loop reading it */</span>
<span class="k">static</span> <span class="k">volatile</span> <span class="kt">uint32_t</span> <span class="n">ticks</span><span class="p">;</span>

<span class="k">static</span> <span class="kt">void</span> <span class="nf">on_timer</span><span class="p">(</span><span class="k">struct</span> <span class="n">intr_frame</span> <span class="o">*</span><span class="n">f</span><span class="p">)</span> <span class="p">{</span>
    <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="n">f</span><span class="p">;</span>
    <span class="n">ticks</span><span class="o">++</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">ticks</span> <span class="o">%</span> <span class="n">TIMER_HZ</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">printk</span><span class="p">(</span><span class="s">"uptime %u s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">ticks</span> <span class="o">/</span> <span class="n">TIMER_HZ</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kt">void</span> <span class="nf">init_timer</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
    <span class="kt">uint16_t</span> <span class="n">divisor</span> <span class="o">=</span> <span class="n">PIT_INPUT_HZ</span> <span class="o">/</span> <span class="n">TIMER_HZ</span><span class="p">;</span>     <span class="c1">// 11931, i.e. 100.007 Hz</span>

    <span class="n">outb</span><span class="p">(</span><span class="n">PIT_CMD</span><span class="p">,</span> <span class="n">PIT_CMD_CH0_RATE</span><span class="p">);</span>
    <span class="n">outb</span><span class="p">(</span><span class="n">PIT_CHANNEL0</span><span class="p">,</span> <span class="n">divisor</span> <span class="o">&amp;</span> <span class="mh">0xFF</span><span class="p">);</span>
    <span class="n">outb</span><span class="p">(</span><span class="n">PIT_CHANNEL0</span><span class="p">,</span> <span class="n">divisor</span> <span class="o">&gt;&gt;</span> <span class="mi">8</span><span class="p">);</span>

    <span class="n">register_handler</span><span class="p">(</span><span class="n">PIC_IRQ_BASE</span> <span class="o">+</span> <span class="n">IRQ_TIMER</span><span class="p">,</span> <span class="n">on_timer</span><span class="p">);</span>
    <span class="n">pic_unmask</span><span class="p">(</span><span class="n">IRQ_TIMER</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Then, for the first time since the loader’s <code class="language-plaintext highlighter-rouge">cli</code> in post 3:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="n">init_timer</span><span class="p">();</span>
    <span class="cm">/* IF has been 0 since the loader's cli, IRQs only arrive from here on */</span>
    <span class="n">asm</span> <span class="nf">volatile</span> <span class="p">(</span><span class="s">"sti"</span><span class="p">);</span>
    <span class="k">for</span> <span class="p">(;;)</span> <span class="p">{</span>
        <span class="cm">/* sleep until the next interrupt instead of spinning */</span>
        <span class="n">asm</span> <span class="k">volatile</span> <span class="p">(</span><span class="s">"hlt"</span><span class="p">);</span>
    <span class="p">}</span>
</code></pre></div></div>

<p><img src="/assets/images/posts/toy-os-5-interrupts/uptime.png" alt="Bochs: kernel entered, int3 at eip, back from int3, then uptime 1 s to uptime 5 s" /></p>

<p><em>The kernel at commit <a href="https://github.com/twicy/toy-os/commit/14a9b9a"><code class="language-plaintext highlighter-rouge">14a9b9a</code></a>, stopped after the fifth “uptime” line. <code class="language-plaintext highlighter-rouge">main</code> was at <code class="language-plaintext highlighter-rouge">0xC0001520</code> back then.</em></p>

<h2 id="panic-turns-interrupts-off-first">PANIC turns interrupts off first</h2>

<p>The next commit (<a href="https://github.com/twicy/toy-os/commit/4a3564b"><code class="language-plaintext highlighter-rouge">4a3564b</code></a>) added <code class="language-plaintext highlighter-rouge">PANIC</code> and <code class="language-plaintext highlighter-rouge">ASSERT</code>. My note on it: <em>interrupts need to be disabled before PANIC.</em></p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* cli first: with IF set, the next IRQ wakes hlt and its handler keeps
 * printing over the panic message */</span>
<span class="cp">#define PANIC(...) do { \
    asm volatile ("cli"); \
    printk("PANIC at %s:%d %s(): ", __FILE__, __LINE__, __func__); \
    printk(__VA_ARGS__); \
    printk("\n"); \
    for (;;) { \
        asm volatile ("hlt"); \
    } \
} while (0)
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">hlt</code> only sleeps until the next interrupt. With IF still set, the timer would wake the CPU 100 times a second, <code class="language-plaintext highlighter-rouge">on_timer</code> would print “uptime” lines under (and eventually over) the panic message, and the system would look half-alive. With <code class="language-plaintext highlighter-rouge">cli</code>, <code class="language-plaintext highlighter-rouge">hlt</code> really is the end; only an NMI could wake it.</p>

<p><code class="language-plaintext highlighter-rouge">main</code> still has an <code class="language-plaintext highlighter-rouge">ASSERT(1 == 2)</code> right after <code class="language-plaintext highlighter-rouge">sti</code>, as a test. In the current build the compiler knows the assertion always fails, so <code class="language-plaintext highlighter-rouge">sti</code> is followed directly by <code class="language-plaintext highlighter-rouge">cli</code>:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>c000182a:	sti
c000182b:	cli
c000182c:	push   $0xc00062b0
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">sti</code> only takes effect after the <em>next</em> instruction, which is <code class="language-plaintext highlighter-rouge">cli</code>. So no tick ever gets in, and the screen ends like this:</p>

<p><img src="/assets/images/posts/toy-os-5-interrupts/panic.png" alt="Bochs: kernel entered, page allocator line, int3 at eip 0xc0001815, back from int3, PANIC at src/main.c:38 main(): assertion failed: 1 == 2" /></p>

<p><em>toy-os at <a href="https://github.com/twicy/toy-os/commit/fc88a1d"><code class="language-plaintext highlighter-rouge">fc88a1d</code></a>, the end of this series. The “page allocator” line comes from <a href="/blogs/os/toy-os-6-buddy.html">post 6</a>.</em></p>

<h2 id="references">References</h2>

<ol>
  <li>郑钢, 《操作系统真相还原》, ch. 7 (中断): the IDT, the 8259A, interrupt stubs with error-code placeholders, and the 8253 timer.</li>
  <li>navi, <a href="https://segmentfault.com/a/1190000040196631">从零开始写 OS 内核 - 中断处理</a>, segmentfault.</li>
  <li>D. P. Bovet and M. Cesati, <em>Understanding the Linux Kernel</em>, 3rd ed., ch. 4 “Interrupts and Exceptions”: IRQs and the PIC, the IDT, the hardware’s privilege checks, and why Linux sets most gates to DPL 0.</li>
  <li>Intel 64 and IA-32 Architectures Software Developer’s Manual, Vol. 3A, “Interrupt and Exception Handling”: the exception table, error codes, gate descriptors, and the interrupt shadow of <code class="language-plaintext highlighter-rouge">sti</code>.</li>
  <li>OSDev Wiki, <a href="https://wiki.osdev.org/8259_PIC">8259 PIC</a> (including spurious IRQs) and <a href="https://wiki.osdev.org/Programmable_Interval_Timer">Programmable Interval Timer</a>.</li>
</ol>]]></content><author><name>Yuchen Tang</name></author><summary type="html"><![CDATA[The fifth toy-os worklog teaches the kernel to take interrupts. It fills a 256-entry IDT with 32-bit interrupt gates that point at nasm-generated stubs. Each stub pushes a placeholder error code where the CPU does not push one, plus its vector number, so every vector reaches one C dispatcher with the same struct intr_frame on the stack. It then remaps the cascaded 8259A PICs to vectors 0x20-0x2F, programs the 8253 PIT for a 100 Hz tick, and turns interrupts on for the first time since the loader's cli. It also covers the gate privilege checks, int3 as a trap, spurious IRQs, and why PANIC must disable interrupts first.]]></summary></entry><entry><title type="html">Toy OS 4: Loading the Kernel</title><link href="https://twicy.github.io/blogs/os/toy-os-4-kernel.html" rel="alternate" type="text/html" title="Toy OS 4: Loading the Kernel" /><published>2026-10-13T00:00:00-07:00</published><updated>2026-10-13T00:00:00-07:00</updated><id>https://twicy.github.io/blogs/os/toy-os-4-kernel</id><content type="html" xml:base="https://twicy.github.io/blogs/os/toy-os-4-kernel.html"><![CDATA[<h1 id="toy-os-4-loading-the-kernel">Toy OS 4: Loading the Kernel</h1>

<p><em>Toy OS worklog: <a href="/blogs/os/toy-os-1-real-mode.html">1. Real mode</a> · <a href="/blogs/os/toy-os-2-segmentation.html">2. Segmentation</a> · <a href="/blogs/os/toy-os-3-paging.html">3. Paging</a> · <strong>4. Kernel</strong> · <a href="/blogs/os/toy-os-5-interrupts.html">5. Interrupts</a> · <a href="/blogs/os/toy-os-6-buddy.html">6. Buddy</a> · <a href="/blogs/os/toy-os-7-slab.html">7. Slab</a> · <a href="https://github.com/twicy/toy-os/tree/fc88a1de36e8cd2cf495c46f3e6b36c01fc94f8b">Source</a></em></p>

<p>Paging is on and the loader runs in protected mode with a stack at <code class="language-plaintext highlighter-rouge">0xC009F000</code> (post 3). The last thing it does is start the kernel. The kernel is written in C and built as an ordinary ELF executable. An ELF file is not a memory image, though, so the loader has to read the file’s program headers and lay out the memory itself.</p>

<h2 id="table-of-contents">Table of Contents</h2>

<ul>
  <li><a href="#building-the-kernel">Building the kernel</a></li>
  <li><a href="#reading-kernelbin-from-the-disk">Reading kernel.bin from the disk</a></li>
  <li><a href="#copying-the-segments">Copying the segments</a></li>
  <li><a href="#physical-memory-when-main-starts">Physical memory when main() starts</a></li>
  <li><a href="#first-words-from-c-vga-and-printk">First words from C: VGA and printk</a></li>
  <li><a href="#references">References</a></li>
</ul>

<h2 id="building-the-kernel">Building the kernel</h2>

<div class="language-make highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">CFLAGS</span> <span class="o">:=</span> <span class="nt">-m32</span> <span class="nt">-ffreestanding</span> <span class="nt">-nostdinc</span> <span class="nt">-Iinclude</span> <span class="se">\</span>
          <span class="nt">-fno-pie</span> <span class="nt">-fno-stack-protector</span> <span class="nt">-fno-asynchronous-unwind-tables</span> <span class="se">\</span>
          <span class="nt">-Wall</span> <span class="nt">-Wextra</span> <span class="nt">-MMD</span> <span class="nt">-MP</span>

<span class="nl">kernel.bin</span><span class="o">:</span> <span class="nf">$(KERNEL_OBJS)</span>
	ld <span class="nt">-m</span> elf_i386 <span class="nv">$^</span> <span class="nt">-Ttext</span> 0xc0001500 <span class="nt">-e</span> main <span class="nt">-o</span> <span class="nv">$@</span>
</code></pre></div></div>

<ul>
  <li><code class="language-plaintext highlighter-rouge">-m32</code>, <code class="language-plaintext highlighter-rouge">-m elf_i386</code>: 32-bit code and a 32-bit ELF.</li>
  <li><code class="language-plaintext highlighter-rouge">-ffreestanding -nostdinc</code>: there is no C library and no host headers. <code class="language-plaintext highlighter-rouge">include/</code> has its own <code class="language-plaintext highlighter-rouge">stdint.h</code>, <code class="language-plaintext highlighter-rouge">stdarg.h</code> and <code class="language-plaintext highlighter-rouge">string.h</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">-fno-pie</code>: the kernel is linked at a fixed address and nothing will relocate it.</li>
  <li><code class="language-plaintext highlighter-rouge">-fno-stack-protector</code>: otherwise gcc emits canary checks. On i386 the canary is read from <code class="language-plaintext highlighter-rouge">%gs:0x14</code>, a thread-local slot that does not exist here.</li>
  <li><code class="language-plaintext highlighter-rouge">-fno-asynchronous-unwind-tables</code>: no <code class="language-plaintext highlighter-rouge">.eh_frame</code>, since nothing will ever unwind the stack.</li>
  <li><code class="language-plaintext highlighter-rouge">-Ttext 0xc0001500 -e main</code>: <code class="language-plaintext highlighter-rouge">.text</code> starts at <code class="language-plaintext highlighter-rouge">0xC0001500</code>, and the ELF entry point is <code class="language-plaintext highlighter-rouge">main</code>.</li>
</ul>

<p>Why <code class="language-plaintext highlighter-rouge">0xC0001500</code>? <code class="language-plaintext highlighter-rouge">0xC0000000</code> + PA <code class="language-plaintext highlighter-rouge">0x1500</code> puts the kernel in the higher half, in low memory just above the loader (<code class="language-plaintext highlighter-rouge">0x800</code>–<code class="language-plaintext highlighter-rouge">0xFFF</code>). As my commit note says, <code class="language-plaintext highlighter-rouge">ld</code> decides the target virtual addresses, and the ELF program headers record them:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ readelf -l kernel.bin
Entry point 0xc00017b4
There are 5 program headers, starting at offset 52

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  LOAD           0x000000 0xc0001000 0xc0000000 0x000d4 0x000d4 R   0x1000
  LOAD           0x000500 0xc0001500 0xc0001500 0x03cd3 0x03cd3 R E 0x1000
  LOAD           0x005000 0xc0006000 0xc0006000 0x00d6c 0x00d6c R   0x1000
  LOAD           0x005d6c 0xc0007d6c 0xc0007d6c 0x00004 0x010a8 RW  0x1000
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x10

 Section to Segment mapping:
  Segment Sections...
   00
   01     .text
   02     .rodata
   03     .data .bss
</code></pre></div></div>

<ul>
  <li>With 4 KiB alignment, a segment’s file offset and virtual address must be equal modulo 4 KiB. <code class="language-plaintext highlighter-rouge">.text</code> at <code class="language-plaintext highlighter-rouge">0xC0001500</code> therefore sits at file offset <code class="language-plaintext highlighter-rouge">0x500</code>. The first page of the file, <code class="language-plaintext highlighter-rouge">0xC0001000</code>, holds only the ELF header and program headers: segment 00, 212 bytes.</li>
  <li>The entry point <code class="language-plaintext highlighter-rouge">0xC00017B4</code> is not the start of <code class="language-plaintext highlighter-rouge">.text</code>. <code class="language-plaintext highlighter-rouge">gdt.o</code> comes first in the link order, and <code class="language-plaintext highlighter-rouge">-e main</code> just records <code class="language-plaintext highlighter-rouge">main</code>’s address.</li>
  <li>The last segment has <code class="language-plaintext highlighter-rouge">FileSiz</code> 4 but <code class="language-plaintext highlighter-rouge">MemSiz</code> <code class="language-plaintext highlighter-rouge">0x10A8</code>: that is <code class="language-plaintext highlighter-rouge">.bss</code>. It takes up memory but has no bytes in the file.</li>
</ul>

<h2 id="reading-kernelbin-from-the-disk">Reading kernel.bin from the disk</h2>

<p>Before paging (post 3), the loader reads 200 sectors from LBA 9 to a buffer at <code class="language-plaintext highlighter-rouge">0x70000</code>. It uses the same ATA PIO routine as the MBR (post 1), rewritten with 32-bit registers:</p>

<div class="language-nasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="c1">; load kernel.bin into a buffer, it is an ELF file so its code is not at offset 0</span>
    <span class="nf">mov</span> <span class="nb">eax</span><span class="p">,</span> <span class="nv">KERNEL_START_SECTOR</span>    <span class="c1">; 9</span>
    <span class="nf">mov</span> <span class="nb">ebx</span><span class="p">,</span> <span class="nv">KERNEL_BIN_BASE_ADDR</span>   <span class="c1">; 0x70000</span>
    <span class="nf">mov</span> <span class="nb">ecx</span><span class="p">,</span> <span class="nv">KERNEL_SECTOR_COUNT</span>    <span class="c1">; 200</span>
    <span class="nf">call</span> <span class="nv">read_disk</span>
</code></pre></div></div>

<p>200 sectors is 100 KiB, so the buffer is <code class="language-plaintext highlighter-rouge">[0x70000, 0x89000)</code>. Today <code class="language-plaintext highlighter-rouge">kernel.bin</code> is 34,272 bytes (67 sectors). <code class="language-plaintext highlighter-rouge">KERNEL_SECTOR_COUNT</code> must match <code class="language-plaintext highlighter-rouge">count=200</code> in the Makefile, and the kernel file must stay under 100 KiB, or its tail is silently cut off. The first bytes of the buffer show the ELF magic:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;bochs&gt; xp /16bx 0x70000
0x0000000000070000 &lt;bogus+       0&gt;:	0x7f	0x45	0x4c	0x46	0x01	0x01	0x01	0x00
0x0000000000070008 &lt;bogus+       8&gt;:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
</code></pre></div></div>

<h2 id="copying-the-segments">Copying the segments</h2>

<p><code class="language-plaintext highlighter-rouge">kernel_init</code> walks the program header table and handles each <code class="language-plaintext highlighter-rouge">PT_LOAD</code> entry. The offsets are those of <code class="language-plaintext highlighter-rouge">Elf32_Ehdr</code> and <code class="language-plaintext highlighter-rouge">Elf32_Phdr</code>:</p>

<table>
  <thead>
    <tr>
      <th>Field</th>
      <th>Offset</th>
      <th> </th>
      <th>Field</th>
      <th>Offset</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">e_entry</code></td>
      <td>24</td>
      <td> </td>
      <td><code class="language-plaintext highlighter-rouge">p_type</code></td>
      <td>0</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">e_phoff</code></td>
      <td>28</td>
      <td> </td>
      <td><code class="language-plaintext highlighter-rouge">p_offset</code></td>
      <td>4</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">e_phentsize</code></td>
      <td>42</td>
      <td> </td>
      <td><code class="language-plaintext highlighter-rouge">p_vaddr</code></td>
      <td>8</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">e_phnum</code></td>
      <td>44</td>
      <td> </td>
      <td><code class="language-plaintext highlighter-rouge">p_filesz</code> / <code class="language-plaintext highlighter-rouge">p_memsz</code></td>
      <td>16 / 20</td>
    </tr>
  </tbody>
</table>

<div class="language-nasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">kernel_init:</span>
    <span class="nf">...</span>
    <span class="c1">; e_phentsize, namely program header size</span>
    <span class="nf">mov</span> <span class="nb">dx</span><span class="p">,</span> <span class="p">[</span><span class="nv">KERNEL_BIN_BASE_ADDR</span> <span class="o">+</span> <span class="mi">42</span><span class="p">]</span>
    <span class="c1">; e_phoff, offset of the first program header</span>
    <span class="nf">mov</span> <span class="nb">ebx</span><span class="p">,</span> <span class="p">[</span><span class="nv">KERNEL_BIN_BASE_ADDR</span> <span class="o">+</span> <span class="mi">28</span><span class="p">]</span>
    <span class="c1">; e_phnum, number of program header</span>
    <span class="nf">mov</span> <span class="nb">cx</span><span class="p">,</span> <span class="p">[</span><span class="nv">KERNEL_BIN_BASE_ADDR</span> <span class="o">+</span> <span class="mi">44</span><span class="p">]</span>

    <span class="nf">add</span> <span class="nb">ebx</span><span class="p">,</span> <span class="nv">KERNEL_BIN_BASE_ADDR</span>

<span class="nl">.each_segment:</span>
    <span class="c1">; p_type is a dword, only PT_LOAD segments get copied into memory</span>
    <span class="nf">cmp</span> <span class="kt">dword</span> <span class="p">[</span><span class="nb">ebx</span><span class="p">],</span> <span class="nv">PT_LOAD</span>
    <span class="nf">jne</span> <span class="nv">.next_segment</span>

    <span class="c1">; memcpy from bin to the designated place</span>
    <span class="nf">push</span> <span class="kt">dword</span> <span class="p">[</span><span class="nb">ebx</span> <span class="o">+</span> <span class="mi">16</span><span class="p">]</span>           <span class="c1">; p_filesz</span>
    <span class="nf">mov</span> <span class="nb">eax</span><span class="p">,</span> <span class="p">[</span><span class="nb">ebx</span> <span class="o">+</span> <span class="mi">4</span><span class="p">]</span>              <span class="c1">; p_offset</span>
    <span class="nf">add</span> <span class="nb">eax</span><span class="p">,</span> <span class="nv">KERNEL_BIN_BASE_ADDR</span>
    <span class="nf">push</span> <span class="nb">eax</span>
    <span class="nf">push</span> <span class="kt">dword</span> <span class="p">[</span><span class="nb">ebx</span> <span class="o">+</span> <span class="mi">8</span><span class="p">]</span>            <span class="c1">; p_vaddr</span>
    <span class="nf">call</span> <span class="nv">mem_cpy</span>
    <span class="nf">add</span> <span class="nb">esp</span><span class="p">,</span> <span class="mi">12</span>

    <span class="c1">; .bss takes memory but no file bytes (p_memsz &gt; p_filesz), zero</span>
    <span class="c1">; the tail so C globals start at 0</span>
    <span class="nf">mov</span> <span class="nb">eax</span><span class="p">,</span> <span class="p">[</span><span class="nb">ebx</span> <span class="o">+</span> <span class="mi">20</span><span class="p">]</span>
    <span class="nf">sub</span> <span class="nb">eax</span><span class="p">,</span> <span class="p">[</span><span class="nb">ebx</span> <span class="o">+</span> <span class="mi">16</span><span class="p">]</span>
    <span class="nf">push</span> <span class="nb">eax</span>                        <span class="c1">; p_memsz - p_filesz</span>
    <span class="nf">mov</span> <span class="nb">eax</span><span class="p">,</span> <span class="p">[</span><span class="nb">ebx</span> <span class="o">+</span> <span class="mi">8</span><span class="p">]</span>
    <span class="nf">add</span> <span class="nb">eax</span><span class="p">,</span> <span class="p">[</span><span class="nb">ebx</span> <span class="o">+</span> <span class="mi">16</span><span class="p">]</span>
    <span class="nf">push</span> <span class="nb">eax</span>                        <span class="c1">; p_vaddr + p_filesz</span>
    <span class="nf">call</span> <span class="nv">mem_zero</span>
    <span class="nf">add</span> <span class="nb">esp</span><span class="p">,</span> <span class="mi">8</span>

<span class="nl">.next_segment:</span>
    <span class="nf">add</span> <span class="nb">ebx</span><span class="p">,</span> <span class="nb">edx</span>
    <span class="nf">loop</span> <span class="nv">.each_segment</span>
    <span class="nf">ret</span>
</code></pre></div></div>

<p><img src="/assets/images/posts/toy-os-4-kernel/elf-load.svg" alt="kernel.bin in the buffer on the left, the loaded kernel in memory on the right: headers, .text, .rodata and .data copied to their virtual addresses, .bss zeroed" /></p>

<p><em>The file layout (offsets from <code class="language-plaintext highlighter-rouge">0x70000</code>) and the memory layout it turns into. The bands are the four <code class="language-plaintext highlighter-rouge">mem_cpy</code> calls.</em></p>

<p>The <code class="language-plaintext highlighter-rouge">p_vaddr</code> addresses are above <code class="language-plaintext highlighter-rouge">0xC0000000</code>. That works because paging is on and PDE 768 maps VA <code class="language-plaintext highlighter-rouge">0xC0000000</code>–<code class="language-plaintext highlighter-rouge">0xC00FFFFF</code> to the first MiB. As my commit note says, this loader pays for an extra copy: disk → buffer → final place. A smarter loader would read the headers first and then read each segment straight from the disk to its address.</p>

<p>The <code class="language-plaintext highlighter-rouge">.bss</code> zeroing was not there at first. It came with printk (<a href="https://github.com/twicy/toy-os/commit/c91a8c0"><code class="language-plaintext highlighter-rouge">c91a8c0</code></a>), whose <code class="language-plaintext highlighter-rouge">cursor_x</code> and <code class="language-plaintext highlighter-rouge">cursor_y</code> were the first C globals that had to start at 0.</p>

<p>Then the jump:</p>

<div class="language-nasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="nf">call</span> <span class="nv">kernel_init</span>

    <span class="nf">mov</span> <span class="nb">esp</span><span class="p">,</span> <span class="nv">KERNEL_STACK_TOP</span>       <span class="c1">; 0xC009F000</span>
    <span class="c1">; e_entry</span>
    <span class="nf">jmp</span> <span class="p">[</span><span class="nv">KERNEL_BIN_BASE_ADDR</span> <span class="o">+</span> <span class="mi">24</span><span class="p">]</span>
</code></pre></div></div>

<p>The kernel stack is the loader’s stack, reset to its top: the loader’s frames are simply forgotten. At the first instruction of <code class="language-plaintext highlighter-rouge">main</code>:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(0) Breakpoint 7, 0x00000000c00017b4 in ?? ()
(0) [0x0000000017b4] 0008:00000000c00017b4 (unk. ctxt): lea ecx, ss:[esp+4]       ; 8d4c2404
rsp: 00000000_c009f000
rip: 00000000_c00017b4
</code></pre></div></div>

<h2 id="physical-memory-when-main-starts">Physical memory when main() starts</h2>

<p>This is the whole first MiB, plus what lies above it, at the first instruction of <code class="language-plaintext highlighter-rouge">main</code>:</p>

<p><img src="/assets/images/posts/toy-os-4-kernel/low-1mib-kernel.svg" alt="Physical memory when main() starts: IVT, BDA, E820 map at 0x500, the dead loader at 0x800, the kernel image at 0x1000-0x8E13, free space, the dead kernel.bin buffer at 0x70000, the kernel stack below 0x9F000, EBDA, video and BIOS, the loader page tables at 1 MiB, then free RAM" /></p>

<p><em>Physical memory at <code class="language-plaintext highlighter-rouge">main()</code>. Amber regions are done and could be reused. The whole first MiB is also visible at VA <code class="language-plaintext highlighter-rouge">0xC0000000</code> + PA.</em></p>

<table>
  <thead>
    <tr>
      <th>Physical</th>
      <th>Virtual</th>
      <th>What</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">0x00500</code>–<code class="language-plaintext highlighter-rouge">0x00567</code></td>
      <td><code class="language-plaintext highlighter-rouge">0xC0000500</code></td>
      <td>E820 map: count + 5 entries (room for 32, up to <code class="language-plaintext highlighter-rouge">0x784</code>)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">0x00800</code>–<code class="language-plaintext highlighter-rouge">0x00B70</code></td>
      <td> </td>
      <td><code class="language-plaintext highlighter-rouge">loader.bin</code>; its GDT at <code class="language-plaintext highlighter-rouge">0x803</code> is in use until <code class="language-plaintext highlighter-rouge">init_gdt()</code> (post 2)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">0x01000</code>–<code class="language-plaintext highlighter-rouge">0x010D3</code></td>
      <td><code class="language-plaintext highlighter-rouge">0xC0001000</code></td>
      <td>ELF + program headers</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">0x01500</code>–<code class="language-plaintext highlighter-rouge">0x051D2</code></td>
      <td><code class="language-plaintext highlighter-rouge">0xC0001500</code></td>
      <td><code class="language-plaintext highlighter-rouge">.text</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">0x06000</code>–<code class="language-plaintext highlighter-rouge">0x06D6B</code></td>
      <td><code class="language-plaintext highlighter-rouge">0xC0006000</code></td>
      <td><code class="language-plaintext highlighter-rouge">.rodata</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">0x07D6C</code>–<code class="language-plaintext highlighter-rouge">0x08E13</code></td>
      <td><code class="language-plaintext highlighter-rouge">0xC0007D6C</code></td>
      <td><code class="language-plaintext highlighter-rouge">.data</code> (4 bytes) + <code class="language-plaintext highlighter-rouge">.bss</code> (GDT, IDT, allocator state, …)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">0x70000</code>–<code class="language-plaintext highlighter-rouge">0x88FFF</code></td>
      <td> </td>
      <td><code class="language-plaintext highlighter-rouge">kernel.bin</code> as read from the disk</td>
    </tr>
    <tr>
      <td>↓ <code class="language-plaintext highlighter-rouge">0x9F000</code></td>
      <td><code class="language-plaintext highlighter-rouge">0xC009F000</code></td>
      <td>kernel stack, growing down</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">0xB8000</code>–<code class="language-plaintext highlighter-rouge">0xBFFFF</code></td>
      <td><code class="language-plaintext highlighter-rouge">0xC00B8000</code></td>
      <td>text buffer</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">0x100000</code>–<code class="language-plaintext highlighter-rouge">0x500FFF</code></td>
      <td>(recursive window, post 3)</td>
      <td>loader page directory and page tables</td>
    </tr>
  </tbody>
</table>

<p>A few things follow from this layout:</p>

<ul>
  <li>The kernel image overwrites the MBR (<code class="language-plaintext highlighter-rouge">0x7C00</code>–<code class="language-plaintext highlighter-rouge">0x7DFF</code>), which is fine, since the MBR is long dead. The kernel’s GDT (post 2) ends up exactly in the MBR’s last 128 bytes.</li>
  <li>In memory, the kernel can grow up to the buffer at <code class="language-plaintext highlighter-rouge">0x70000</code>, about 444 KiB. The file is the tighter limit: 100 KiB.</li>
  <li>Nothing here is reclaimed yet. Post 6 simply reserves the whole first MiB. With a kernel of about 32 KiB, that is simpler than tracking every gap.</li>
  <li><code class="language-plaintext highlighter-rouge">kernel.h</code> turns the higher-half rule into two macros, valid for anything in the kernel half:</li>
</ul>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* the kernel half starts here, physical address p is at KERNEL_OFFSET + p */</span>
<span class="cp">#define KERNEL_OFFSET 0xC0000000
#define phys_to_virt(x) ((void *)((uintptr_t)(x) + KERNEL_OFFSET))
#define virt_to_phys(x) ((uintptr_t)(x) - KERNEL_OFFSET)
</span></code></pre></div></div>

<h2 id="first-words-from-c-vga-and-printk">First words from C: VGA and printk</h2>

<p>The first <code class="language-plaintext highlighter-rouge">main</code> (<a href="https://github.com/twicy/toy-os/commit/bf3a03a"><code class="language-plaintext highlighter-rouge">bf3a03a</code></a>) was just <code class="language-plaintext highlighter-rouge">while(1);</code>. Once that ran without faulting, the next commit (<a href="https://github.com/twicy/toy-os/commit/c91a8c0"><code class="language-plaintext highlighter-rouge">c91a8c0</code></a>) gave the kernel a voice.</p>

<h3 id="the-text-buffer">The text buffer</h3>

<p>In 80 × 25 color text mode, each screen cell is two bytes in the buffer: the character, then an attribute.</p>

<p><img src="/assets/images/posts/toy-os-4-kernel/vga-cell.svg" alt="A VGA text cell: bit 15 blink, bits 14-12 background, bit 11 bright, bits 10-8 foreground, bits 7-0 the character; 0x0F74 is a white t on black" /></p>

<p><em>One cell. The kernel prints white on black, attribute <code class="language-plaintext highlighter-rouge">0x0F</code>.</em></p>

<p><code class="language-plaintext highlighter-rouge">src/vga.c</code> writes cells at <code class="language-plaintext highlighter-rouge">0xC00B8000</code>, keeps its own cursor, scrolls by copying rows 1–24 up one row, and moves the hardware cursor through the CRTC registers:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* 0xB8000 seen through the kernel alias the loader maps at 0xC0000000 */</span>
<span class="cp">#define VIDEO_SEG_BASE 0xc00b8000
</span>
<span class="kt">void</span> <span class="nf">put_char</span><span class="p">(</span><span class="kt">char</span> <span class="n">c</span><span class="p">,</span> <span class="kt">uint8_t</span> <span class="n">color</span><span class="p">)</span> <span class="p">{</span>
    <span class="kt">uint16_t</span> <span class="o">*</span><span class="n">video_memory</span> <span class="o">=</span> <span class="p">(</span><span class="kt">uint16_t</span> <span class="o">*</span><span class="p">)</span><span class="n">VIDEO_SEG_BASE</span><span class="p">;</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">c</span> <span class="o">==</span> <span class="sc">'\n'</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">cursor_x</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
        <span class="n">cursor_y</span><span class="o">++</span><span class="p">;</span>
    <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">c</span> <span class="o">==</span> <span class="sc">'\r'</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">cursor_x</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="c1">// char is signed, without the cast bytes &gt;= 0x80 sign-extend over the color</span>
        <span class="n">video_memory</span><span class="p">[</span><span class="n">cursor_y</span> <span class="o">*</span> <span class="n">VGA_WIDTH</span> <span class="o">+</span> <span class="n">cursor_x</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="n">color</span> <span class="o">&lt;&lt;</span> <span class="mi">8</span><span class="p">)</span> <span class="o">|</span> <span class="p">(</span><span class="kt">uint8_t</span><span class="p">)</span><span class="n">c</span><span class="p">;</span>
        <span class="n">cursor_x</span><span class="o">++</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="p">...</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The comment marks a real trap. <code class="language-plaintext highlighter-rouge">char</code> is signed on x86, so a byte like <code class="language-plaintext highlighter-rouge">0xB0</code> becomes <code class="language-plaintext highlighter-rouge">0xFFB0</code> when promoted to <code class="language-plaintext highlighter-rouge">int</code>. OR’ed with <code class="language-plaintext highlighter-rouge">color &lt;&lt; 8</code>, that wipes out the attribute byte.</p>

<h3 id="printk-and-varargs">printk and varargs</h3>

<p><code class="language-plaintext highlighter-rouge">printk</code> is a thin wrapper: <code class="language-plaintext highlighter-rouge">vformat()</code> in <code class="language-plaintext highlighter-rouge">lib/stdio.c</code> parses the format and calls a <code class="language-plaintext highlighter-rouge">putc</code> callback for each character. It supports <code class="language-plaintext highlighter-rouge">%d %i %u %o %x %X %c %s %p %%</code>, the flags <code class="language-plaintext highlighter-rouge">- + space # 0</code>, width and precision (<code class="language-plaintext highlighter-rouge">*</code> too), and the length modifiers <code class="language-plaintext highlighter-rouge">hh h l z</code>.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kt">void</span> <span class="nf">vga_putc</span><span class="p">(</span><span class="kt">char</span> <span class="n">c</span><span class="p">,</span> <span class="kt">void</span> <span class="o">*</span><span class="n">arg</span><span class="p">)</span> <span class="p">{</span>
    <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="n">arg</span><span class="p">;</span>
    <span class="n">put_char</span><span class="p">(</span><span class="n">c</span><span class="p">,</span> <span class="n">VGA_COLOR_DEFAULT</span><span class="p">);</span>
<span class="p">}</span>

<span class="cm">/* Characters go straight to the screen, so no buffer limits the length */</span>
<span class="kt">int</span> <span class="nf">printk</span><span class="p">(</span><span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">format</span><span class="p">,</span> <span class="p">...)</span> <span class="p">{</span>
    <span class="kt">va_list</span> <span class="n">ap</span><span class="p">;</span>
    <span class="kt">int</span> <span class="n">len</span><span class="p">;</span>

    <span class="n">va_start</span><span class="p">(</span><span class="n">ap</span><span class="p">,</span> <span class="n">format</span><span class="p">);</span>
    <span class="n">len</span> <span class="o">=</span> <span class="n">vformat</span><span class="p">(</span><span class="n">vga_putc</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">,</span> <span class="n">format</span><span class="p">,</span> <span class="n">ap</span><span class="p">);</span>
    <span class="n">va_end</span><span class="p">(</span><span class="n">ap</span><span class="p">);</span>
    <span class="k">return</span> <span class="n">len</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>With <code class="language-plaintext highlighter-rouge">-nostdinc</code> there is no <code class="language-plaintext highlighter-rouge">&lt;stdarg.h&gt;</code>, but varargs are a compiler feature anyway. <code class="language-plaintext highlighter-rouge">include/stdarg.h</code> just names the builtins:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="n">__builtin_va_list</span> <span class="kt">va_list</span><span class="p">;</span>
<span class="cp">#define va_start(ap, last) __builtin_va_start(ap, last)
#define va_arg(ap, type) __builtin_va_arg(ap, type)
#define va_end(ap) __builtin_va_end(ap)
</span></code></pre></div></div>

<p>On i386, <code class="language-plaintext highlighter-rouge">__builtin_va_list</code> is just a <code class="language-plaintext highlighter-rouge">char *</code> into the caller’s stack frame. The cdecl caller pushes the arguments right to left, so in memory they sit left to right above the return address:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>c00017d2:	push   $0xc00017b4      ; 2nd argument: main
c00017d7:	push   $0xc0006241      ; 1st argument: the format string
c00017dc:	call   c0001adf &lt;printk&gt;
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">va_start(ap, format)</code> points <code class="language-plaintext highlighter-rouge">ap</code> just past <code class="language-plaintext highlighter-rouge">format</code>. Each <code class="language-plaintext highlighter-rouge">va_arg(ap, int)</code> reads 4 bytes and moves on. Arguments narrower than <code class="language-plaintext highlighter-rouge">int</code> (<code class="language-plaintext highlighter-rouge">char</code>, <code class="language-plaintext highlighter-rouge">short</code>) are promoted to <code class="language-plaintext highlighter-rouge">int</code> when passed, which is why <code class="language-plaintext highlighter-rouge">vformat</code> reads <code class="language-plaintext highlighter-rouge">%hhd</code> as <code class="language-plaintext highlighter-rouge">(signed char)va_arg(ap, int)</code>.</p>

<p><img src="/assets/images/posts/toy-os-4-kernel/kernel-entry.png" alt="Bochs: &quot;toy-os kernel entered at 0xc00017b4&quot;" /></p>

<p><em><code class="language-plaintext highlighter-rouge">main</code> clears the screen and prints its own address: <code class="language-plaintext highlighter-rouge">%p</code> of <code class="language-plaintext highlighter-rouge">main</code> = <code class="language-plaintext highlighter-rouge">e_entry</code>.</em></p>

<p>The kernel can talk now, but it cannot listen: any exception would still take the machine down. <a href="/blogs/os/toy-os-5-interrupts.html">Post 5</a> adds an IDT, the 8259A PIC and a timer.</p>

<h2 id="references">References</h2>

<ol>
  <li>郑钢, 《操作系统真相还原》, ch. 5 (加载内核: ELF, the kernel at <code class="language-plaintext highlighter-rouge">0xc0001500</code>, the <code class="language-plaintext highlighter-rouge">0x70000</code> buffer) and ch. 6 (完善内核: printing to the screen).</li>
  <li>navi, <a href="https://segmentfault.com/a/1190000040167201">从零开始写 OS 内核 - 加载并进入 kernel</a> and <a href="https://segmentfault.com/a/1190000040179784">显示与打印</a>, segmentfault.</li>
  <li><code class="language-plaintext highlighter-rouge">man 5 elf</code>: <code class="language-plaintext highlighter-rouge">Elf32_Ehdr</code> and <code class="language-plaintext highlighter-rouge">Elf32_Phdr</code>.</li>
  <li>System V Application Binary Interface, Intel386 Architecture Processor Supplement: the cdecl calling convention and variable argument lists.</li>
  <li>OSDev Wiki, <a href="https://wiki.osdev.org/Text_UI">Text UI</a> and <a href="https://wiki.osdev.org/Text_Mode_Cursor">Text Mode Cursor</a>.</li>
</ol>]]></content><author><name>Yuchen Tang</name></author><summary type="html"><![CDATA[The fourth toy-os worklog gets from the loader into C. The kernel is a freestanding 32-bit ELF linked at 0xC0001500. The loader reads it from disk into a buffer at 0x70000, copies each PT_LOAD segment to its virtual address, zeroes .bss and jumps to e_entry with the stack at 0xC009F000. A map shows every piece of the first MiB at the moment main() starts. The post ends with the first output from C: a VGA text driver and a printk built on the compiler's varargs builtins.]]></summary></entry><entry><title type="html">Toy OS 3: Protected Mode and Paging</title><link href="https://twicy.github.io/blogs/os/toy-os-3-paging.html" rel="alternate" type="text/html" title="Toy OS 3: Protected Mode and Paging" /><published>2026-10-11T00:00:00-07:00</published><updated>2026-10-11T00:00:00-07:00</updated><id>https://twicy.github.io/blogs/os/toy-os-3-paging</id><content type="html" xml:base="https://twicy.github.io/blogs/os/toy-os-3-paging.html"><![CDATA[<h1 id="toy-os-3-protected-mode-and-paging">Toy OS 3: Protected Mode and Paging</h1>

<p><em>Toy OS worklog: <a href="/blogs/os/toy-os-1-real-mode.html">1. Real mode</a> · <a href="/blogs/os/toy-os-2-segmentation.html">2. Segmentation</a> · <strong>3. Paging</strong> · <a href="/blogs/os/toy-os-4-kernel.html">4. Kernel</a> · <a href="/blogs/os/toy-os-5-interrupts.html">5. Interrupts</a> · <a href="/blogs/os/toy-os-6-buddy.html">6. Buddy</a> · <a href="/blogs/os/toy-os-7-slab.html">7. Slab</a> · <a href="https://github.com/twicy/toy-os/tree/fc88a1de36e8cd2cf495c46f3e6b36c01fc94f8b">Source</a></em></p>

<p>The loader at <code class="language-plaintext highlighter-rouge">0x800</code> has a GDT (post 2). It now does five things, in this order:</p>

<ol>
  <li>Ask the BIOS for the memory map (E820). BIOS calls only work in real mode, so this comes first (<a href="/blogs/os/toy-os-6-buddy.html">post 6</a> covers it).</li>
  <li>Switch to 32-bit protected mode.</li>
  <li>Read <code class="language-plaintext highlighter-rouge">kernel.bin</code> from the disk into a buffer (<a href="/blogs/os/toy-os-4-kernel.html">post 4</a>).</li>
  <li>Build page tables and turn on paging, moving itself into the higher half at <code class="language-plaintext highlighter-rouge">0xC0000000</code>.</li>
  <li>Copy the kernel into place and jump to it (post 4).</li>
</ol>

<p>This post covers steps 2 and 4.</p>

<h2 id="table-of-contents">Table of Contents</h2>

<ul>
  <li><a href="#into-protected-mode">Into protected mode</a></li>
  <li><a href="#why-paging">Why paging</a></li>
  <li><a href="#the-loaders-page-tables">The loader’s page tables</a></li>
  <li><a href="#turning-paging-on-and-moving-to-the-higher-half">Turning paging on and moving to the higher half</a></li>
  <li><a href="#the-virtual-address-space-now">The virtual address space now</a></li>
  <li><a href="#the-recursive-entry">The recursive entry</a></li>
  <li><a href="#references">References</a></li>
</ul>

<h2 id="into-protected-mode">Into protected mode</h2>

<div class="language-nasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">loader_entry:</span>
    <span class="c1">; BIOS calls only work in real mode, collect the memory map first</span>
    <span class="nf">call</span> <span class="nv">detect_memory</span>

    <span class="c1">; disable interrupts - no IDT yet, any interrupt in prot mode would triple fault</span>
    <span class="nf">cli</span>

    <span class="c1">; enable A20</span>
    <span class="nf">in</span> <span class="nb">al</span><span class="p">,</span> <span class="mh">0x92</span>
    <span class="nf">or</span> <span class="nb">al</span><span class="p">,</span> <span class="mi">0000</span><span class="nv">_0010b</span>
    <span class="nf">out</span> <span class="mh">0x92</span><span class="p">,</span> <span class="nb">al</span>

    <span class="c1">; load GDT</span>
    <span class="nf">lgdt</span> <span class="p">[</span><span class="nv">GDT_PTR</span><span class="p">]</span>

    <span class="c1">; open protection mode - set cr0 bit 0</span>
    <span class="nf">mov</span> <span class="nb">eax</span><span class="p">,</span> <span class="nb">cr0</span>
    <span class="nf">or</span> <span class="nb">eax</span><span class="p">,</span> <span class="mh">0x00000001</span>
    <span class="nf">mov</span> <span class="nb">cr0</span><span class="p">,</span> <span class="nb">eax</span>

    <span class="c1">; refresh pipeline</span>
    <span class="nf">jmp</span> <span class="kt">dword</span> <span class="nv">SELECTOR_CODE</span><span class="p">:</span><span class="nv">prot_mode_entry</span>
</code></pre></div></div>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">cli</code></strong>. In protected mode, interrupts go through an IDT, and there is none yet (post 5). The real-mode vector table at address 0 means nothing to a protected-mode CPU. The first timer tick would fault, the fault could not be delivered either (double fault), and then the CPU resets (triple fault). IF stays 0 until the kernel runs <code class="language-plaintext highlighter-rouge">sti</code> in post 5.</li>
  <li><strong>A20</strong>. The 8086 wrapped addresses at 1 MiB, and some old software relied on it, so PCs keep address line 20 forced to 0 until someone enables it. With A20 off, every odd MiB is an alias of the even one below it: <code class="language-plaintext highlighter-rouge">0x100000</code> reads <code class="language-plaintext highlighter-rouge">0x000000</code>. Setting bit 1 of port <code class="language-plaintext highlighter-rouge">0x92</code> (the “fast A20” gate) turns it on.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">lgdt</code>, then <code class="language-plaintext highlighter-rouge">CR0.PE = 1</code></strong>.</li>
  <li><strong>The far jump</strong>. Setting PE does not change how the CPU runs the next instruction. CS still holds the descriptor it cached in real mode, and instructions that were already fetched and decoded as 16-bit code are still in the pipeline. A far jump loads CS from GDT entry 1 and discards that prefetched code. <code class="language-plaintext highlighter-rouge">dword</code> makes it a 48-bit <code class="language-plaintext highlighter-rouge">selector:offset32</code> pointer: <code class="language-plaintext highlighter-rouge">66 EA 22 09 00 00 08 00</code>.</li>
</ul>

<p>Bochs makes the in-between state visible. Here are the segment registers right <em>after</em> <code class="language-plaintext highlighter-rouge">mov cr0, eax</code> and <em>before</em> the jump, at <code class="language-plaintext highlighter-rouge">0x8A3</code>:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(0) [0x0000000008a3] 0000:00000000000008a3 (unk. ctxt): jmpf 0x0008:00000922      ; 66ea220900000800
cs:0x0000, dh=0x00009300, dl=0x0000ffff, valid=1
	Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
gdtr:base=0x0000000000000803, limit=0x1f
CR0=0x60000011: pg CD NW ac wp ne ET ts em mp PE
</code></pre></div></div>

<p>PE is set, yet CS is still the 64 KiB real-mode segment. One instruction later, at <code class="language-plaintext highlighter-rouge">prot_mode_entry</code>:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(0) [0x000000000922] 0008:0000000000000922 (unk. ctxt): mov ax, 0x0010            ; 66b81000
cs:0x0008, dh=0x00cf9b00, dl=0x0000ffff, valid=1
	Code segment, base=0x00000000, limit=0xffffffff, Execute/Read, Non-Conforming, Accessed, 32-bit
</code></pre></div></div>

<p>The new code then reloads the other segment registers from the GDT and gets a stack:</p>

<div class="language-nasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">[</span><span class="nf">bits</span> <span class="mi">32</span><span class="p">]</span>
<span class="nl">prot_mode_entry:</span>
    <span class="c1">; segment registers still hold real-mode values, reload with selectors</span>
    <span class="nf">mov</span> <span class="nb">ax</span><span class="p">,</span> <span class="nv">SELECTOR_DATA</span>
    <span class="nf">mov</span> <span class="nb">ds</span><span class="p">,</span> <span class="nb">ax</span>
    <span class="nf">mov</span> <span class="nb">es</span><span class="p">,</span> <span class="nb">ax</span>
    <span class="nf">mov</span> <span class="nb">ss</span><span class="p">,</span> <span class="nb">ax</span>
    <span class="nf">mov</span> <span class="nb">esp</span><span class="p">,</span> <span class="nv">LOADER_STACK_TOP</span>       <span class="c1">; 0x9F000</span>
    <span class="nf">mov</span> <span class="nb">ax</span><span class="p">,</span> <span class="nv">SELECTOR_VIDEO</span>
    <span class="nf">mov</span> <span class="nb">gs</span><span class="p">,</span> <span class="nb">ax</span>
</code></pre></div></div>

<p>Without the BIOS, printing means writing character/attribute pairs straight into the text buffer through <code class="language-plaintext highlighter-rouge">gs</code>. The cursor position also has to be done by hand: it lives in two CRTC registers, index <code class="language-plaintext highlighter-rouge">0x0E</code> (high byte) and <code class="language-plaintext highlighter-rouge">0x0F</code> (low byte), behind ports <code class="language-plaintext highlighter-rouge">0x3D4</code>/<code class="language-plaintext highlighter-rouge">0x3D5</code>. <code class="language-plaintext highlighter-rouge">print_prot_mode</code> reads the position the BIOS left there, writes the string from that point, handles CR and LF, and writes the new position back, so the protected-mode lines continue right under the MBR’s.</p>

<h2 id="why-paging">Why paging</h2>

<p>With flat segments (post 2), segmentation protects nothing. Paging gives every 4 KiB page its own mapping and its own permission bits (present, writable, user). That is what lets a kernel:</p>

<ul>
  <li>live at the same virtual addresses in every process (Linux on 32-bit x86: user space <code class="language-plaintext highlighter-rouge">0</code>–<code class="language-plaintext highlighter-rouge">0xBFFFFFFF</code>, kernel <code class="language-plaintext highlighter-rouge">0xC0000000</code> and up),</li>
  <li>keep user code out of kernel memory, and</li>
  <li>use physical memory wherever it happens to be free.</li>
</ul>

<p>32-bit x86 paging with 4 KiB pages uses two levels. CR3 holds the physical address of a <em>page directory</em> (1024 four-byte PDEs). Each PDE points to a <em>page table</em> (1024 PTEs), and each PTE points to a 4 KiB page frame. A virtual address is split into 10, 10 and 12 bits:</p>

<p><img src="/assets/images/posts/toy-os-3-paging/page-walk.svg" alt="VA 0xC00B8140 split into PD index 768, PT index 184 and offset 0x140, walked through the loader's page directory and PT0 to physical 0xB8140" /></p>

<p><em>Translating VA <code class="language-plaintext highlighter-rouge">0xC00B8140</code>, the text buffer seen through the kernel alias, with the loader’s real table contents.</em></p>

<p><img src="/assets/images/posts/toy-os-3-paging/pte.svg" alt="PDE and PTE format: frame address in bits 31-12, then AVL, G, PS/PAT, D, A, PCD, PWT, U/S, R/W, P" /></p>

<p><em>A PDE, <code class="language-plaintext highlighter-rouge">0x00101007</code>. The low 12 bits are flags, because tables and frames are 4 KiB aligned.</em></p>

<h2 id="the-loaders-page-tables">The loader’s page tables</h2>

<p>The loader puts the page directory at 1 MiB, the first address above the BIOS areas:</p>

<div class="language-nasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">PAGE_DIR_BASE</span><span class="kd"> equ</span> <span class="mh">0x100000</span>
<span class="no">PAGE_TABLE_0_BASE</span><span class="kd"> equ</span> <span class="mh">0x101000</span>
<span class="no">PDE_FLAGS</span><span class="kd"> equ</span> <span class="nv">...</span>       <span class="c1">; U/S | R/W | P = 0x7</span>
<span class="no">PTE_FLAGS</span><span class="kd"> equ</span> <span class="nv">...</span>       <span class="c1">; same</span>
<span class="no">PDE_0</span><span class="kd"> equ</span> <span class="nv">PAGE_TABLE_0_BASE</span> <span class="o">+</span> <span class="nv">PDE_FLAGS</span>
<span class="no">PTE_0</span><span class="kd"> equ</span> <span class="nv">PAGE_0_BASE</span> <span class="o">+</span> <span class="nv">PTE_FLAGS</span>
</code></pre></div></div>

<div class="language-nasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">setup_gpt:</span>
    <span class="nf">xor</span> <span class="nb">eax</span><span class="p">,</span> <span class="nb">eax</span>
<span class="no">PURGE_LEN</span><span class="kd"> equ</span> <span class="p">(</span><span class="mi">1024</span> <span class="o">*</span> <span class="mi">4</span> <span class="o">+</span> <span class="mi">1024</span> <span class="o">*</span> <span class="mi">1024</span> <span class="o">*</span> <span class="mi">4</span><span class="p">)</span>
<span class="nl">.purge_mem:</span>
    <span class="nf">mov</span> <span class="kt">byte</span> <span class="p">[</span><span class="nv">PAGE_DIR_BASE</span> <span class="o">+</span> <span class="nb">eax</span><span class="p">],</span> <span class="mi">0</span>
    <span class="nf">inc</span> <span class="nb">eax</span>
    <span class="nf">cmp</span> <span class="nb">eax</span><span class="p">,</span> <span class="nv">PURGE_LEN</span>
    <span class="nf">jl</span> <span class="nv">.purge_mem</span>

    <span class="c1">; Setting up PDE0 and PT0</span>
    <span class="nf">mov</span> <span class="kt">dword</span> <span class="p">[</span><span class="nv">PAGE_DIR_BASE</span><span class="p">],</span> <span class="nv">PDE_0</span>
    <span class="nf">mov</span> <span class="kt">dword</span> <span class="p">[</span><span class="nv">PAGE_DIR_BASE</span> <span class="o">+</span> <span class="mi">768</span> <span class="o">*</span> <span class="mi">4</span><span class="p">],</span> <span class="nv">PDE_0</span>

    <span class="nf">xor</span> <span class="nb">eax</span><span class="p">,</span> <span class="nb">eax</span>
    <span class="c1">; PTE for frame 0, advanced one frame per entry</span>
    <span class="nf">mov</span> <span class="nb">ebx</span><span class="p">,</span> <span class="nv">PTE_0</span>

<span class="c1">; Map the first 1M of memory</span>
<span class="c1">; VA [0x0, 0x100000) and VA[0xc0000000, 0xc0100000) map to PA [0x0, 0x100000)</span>
<span class="nl">.setup_pt0:</span>
    <span class="nf">mov</span> <span class="kt">dword</span> <span class="p">[</span><span class="nv">PAGE_TABLE_0_BASE</span> <span class="o">+</span> <span class="mi">4</span> <span class="o">*</span> <span class="nb">eax</span><span class="p">],</span> <span class="nb">ebx</span>
    <span class="nf">add</span> <span class="nb">ebx</span><span class="p">,</span> <span class="mi">4096</span>
    <span class="nf">inc</span> <span class="nb">eax</span>
    <span class="nf">cmp</span> <span class="nb">eax</span><span class="p">,</span> <span class="mi">256</span>
    <span class="nf">jl</span> <span class="nv">.setup_pt0</span>

    <span class="c1">; Recursive Mapping of Page Directory</span>
    <span class="nf">mov</span> <span class="kt">dword</span> <span class="p">[</span><span class="nv">PAGE_DIR_BASE</span> <span class="o">+</span> <span class="mi">1023</span> <span class="o">*</span> <span class="mi">4</span><span class="p">],</span> <span class="nv">PAGE_DIR_BASE</span> <span class="o">+</span> <span class="nv">PDE_FLAGS</span>

<span class="c1">; Map other kernel PDEs</span>
    <span class="nf">mov</span> <span class="nb">ecx</span><span class="p">,</span> <span class="mi">769</span>
    <span class="nf">mov</span> <span class="nb">ebx</span><span class="p">,</span> <span class="nv">PAGE_TABLE_0_BASE</span> <span class="o">+</span> <span class="mi">769</span> <span class="o">*</span> <span class="mi">4096</span> <span class="o">+</span> <span class="nv">PDE_FLAGS</span>
<span class="nl">.setup_kernel_pdes:</span>
    <span class="nf">mov</span> <span class="kt">dword</span> <span class="p">[</span><span class="nv">PAGE_DIR_BASE</span> <span class="o">+</span> <span class="nb">ecx</span> <span class="o">*</span> <span class="mi">4</span><span class="p">],</span> <span class="nb">ebx</span>
    <span class="nf">add</span> <span class="nb">ebx</span><span class="p">,</span> <span class="mi">4096</span>
    <span class="nf">inc</span> <span class="nb">ecx</span>
    <span class="nf">cmp</span> <span class="nb">ecx</span><span class="p">,</span> <span class="mi">1023</span>
    <span class="nf">jl</span> <span class="nv">.setup_kernel_pdes</span>
    <span class="nf">ret</span>
</code></pre></div></div>

<p>The page directory gets four kinds of entries:</p>

<ul>
  <li><strong>PDE 0 → PT0</strong> and <strong>PDE 768 → PT0</strong>. PT0 maps frames 0–255, the first MiB. The same 1 MiB appears twice: at VA 0 (identity), so the loader keeps running after paging is on, and at VA <code class="language-plaintext highlighter-rouge">0xC0000000</code> (768 × 4 MiB), where the kernel will live.</li>
  <li><strong>PDE 769–1022 → their own page tables</strong>, all of whose PTEs are zero. Nothing is mapped there yet, but every kernel PDE already exists. A future process’s page directory can copy PDEs 768–1023 once, and the whole kernel half is shared from then on. Post 6 also relies on PDE 769 being there.</li>
  <li><strong>PDE 1023 → the page directory itself</strong>, the recursive entry (below).</li>
</ul>

<p>The page table for PDE <em>i</em> sits at <code class="language-plaintext highlighter-rouge">0x101000 + i × 4 KiB</code>. That needs no allocator, but it leaves slots 1–768 empty: 3 MiB of zeroed, never-used memory. Here is the physical side:</p>

<p><img src="/assets/images/posts/toy-os-3-paging/loader-pt-phys.svg" alt="Physical memory from 0 to 512 MiB: low 1 MiB, the page directory at 0x100000, PT0 at 0x101000, unused slots up to 0x402000, PTs 769-1022 up to 0x500000, an unused slot for 1023, then free RAM" /></p>

<p><em>Physical memory taken by the loader’s paging structures: <code class="language-plaintext highlighter-rouge">[0x100000, 0x501000)</code>, 4 MiB + 4 KiB. Post 6 gives it back.</em></p>

<p>Zeroing that region byte by byte costs four instructions per byte. Bochs counts 7,348,009 instructions when <code class="language-plaintext highlighter-rouge">setup_gpt</code> is called and 24,144,543 at the magic breakpoint after paging is on. The difference, 16.8 million, is almost exactly 4 × <code class="language-plaintext highlighter-rouge">PURGE_LEN</code> (4,198,400 bytes). So two thirds of all instructions before the kernel starts go into this one loop. <code class="language-plaintext highlighter-rouge">rep stosd</code> would be four times fewer iterations. It does not matter in Bochs, but it is a fun number.</p>

<p>The flags are <code class="language-plaintext highlighter-rouge">0x7</code>: present, writable and <strong>user</strong>. With U/S = 1 everywhere, ring 3 code could read and write the kernel. That is harmless while nothing runs in ring 3, and the kernel’s own page tables (post 6) drop the user bit.</p>

<h2 id="turning-paging-on-and-moving-to-the-higher-half">Turning paging on and moving to the higher half</h2>

<div class="language-nasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="nf">call</span> <span class="nv">setup_gpt</span>

<span class="c1">; Move GDT, video segment and stack to their kernel aliases</span>
    <span class="nf">sgdt</span> <span class="p">[</span><span class="nv">GDT_PTR</span><span class="p">]</span>
    <span class="c1">; video desc base 31..24 is the top byte of its high dword: 0xB8000 -&gt; 0xC00B8000</span>
    <span class="nf">or</span> <span class="kt">dword</span> <span class="p">[</span><span class="nv">VIDEO_DESC</span> <span class="o">+</span> <span class="mi">4</span><span class="p">],</span> <span class="mh">0xC0000000</span>
    <span class="c1">; GDTR base 0x803 -&gt; 0xC0000803</span>
    <span class="nf">add</span> <span class="kt">dword</span> <span class="p">[</span><span class="nv">GDT_PTR</span> <span class="o">+</span> <span class="mi">2</span><span class="p">],</span> <span class="mh">0xC0000000</span>
    <span class="nf">add</span> <span class="nb">esp</span><span class="p">,</span> <span class="mh">0xC0000000</span>

<span class="c1">; Enable paging</span>
    <span class="nf">mov</span> <span class="nb">eax</span><span class="p">,</span> <span class="nv">PAGE_DIR_BASE</span>
    <span class="nf">mov</span> <span class="nb">cr3</span><span class="p">,</span> <span class="nb">eax</span>
    <span class="c1">; set cr0 bit 31 (PG)</span>
    <span class="nf">mov</span> <span class="nb">eax</span><span class="p">,</span> <span class="nb">cr0</span>
    <span class="nf">or</span> <span class="nb">eax</span><span class="p">,</span> <span class="mh">0x80000000</span>
    <span class="nf">mov</span> <span class="nb">cr0</span><span class="p">,</span> <span class="nb">eax</span>

    <span class="c1">; GDTR base is a linear address, load the high one only after paging is on</span>
    <span class="nf">lgdt</span> <span class="p">[</span><span class="nv">GDT_PTR</span><span class="p">]</span>
    <span class="c1">; gs still caches the old video base until the selector is reloaded</span>
    <span class="nf">mov</span> <span class="nb">ax</span><span class="p">,</span> <span class="nv">SELECTOR_VIDEO</span>
    <span class="nf">mov</span> <span class="nb">gs</span><span class="p">,</span> <span class="nb">ax</span>
</code></pre></div></div>

<p>The idea, as my commit note puts it: <em>mirror the stack pointer and video segment to kernel space after paging.</em> Everything the CPU finds by <em>linear</em> address has to move to its alias above <code class="language-plaintext highlighter-rouge">0xC0000000</code>, because the identity map will not last (post 6 removes it):</p>

<ul>
  <li><strong>The GDTR base is a linear address.</strong> After paging, the GDT is still reachable at <code class="language-plaintext highlighter-rouge">0x803</code> through the identity map, but the kernel should not depend on that. The new base, <code class="language-plaintext highlighter-rouge">0xC0000803</code>, is loaded with <code class="language-plaintext highlighter-rouge">lgdt</code> only once paging is on. Before that, <code class="language-plaintext highlighter-rouge">0xC0000803</code> would be a physical address, and there is no RAM there.</li>
  <li><strong>The video segment base</strong>: <code class="language-plaintext highlighter-rouge">0xB8000</code> → <code class="language-plaintext highlighter-rouge">0xC00B8000</code>. Base bits 31..24 are the top byte of the descriptor’s high dword, so a single <code class="language-plaintext highlighter-rouge">or</code> does it. <code class="language-plaintext highlighter-rouge">gs</code> must then be reloaded, or it keeps the base it cached earlier (post 2).</li>
  <li><strong>The stack</strong>: <code class="language-plaintext highlighter-rouge">esp += 0xC0000000</code>. This happens <em>before</em> paging is on, when <code class="language-plaintext highlighter-rouge">0xC009F000</code> points at nothing. It works because nothing touches the stack between the <code class="language-plaintext highlighter-rouge">add</code> and <code class="language-plaintext highlighter-rouge">mov cr0, eax</code>: no <code class="language-plaintext highlighter-rouge">push</code>, no <code class="language-plaintext highlighter-rouge">call</code>.</li>
</ul>

<p>Why does execution survive <code class="language-plaintext highlighter-rouge">mov cr0, eax</code> at all? The next instruction’s address is still around <code class="language-plaintext highlighter-rouge">0x990</code>, and the identity map (PDE 0) translates it to the same physical address. Without PDE 0, the very next fetch would page-fault, with no IDT to handle it.</p>

<p>At the magic breakpoint (<code class="language-plaintext highlighter-rouge">xchg bx, bx</code>) right after this:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gs:0x0018, dh=0xc0c0930b, dl=0x80000007, valid=7
	Data segment, base=0xc00b8000, limit=0x00007fff, Read/Write, Accessed
gdtr:base=0x00000000c0000803, limit=0x1f
CR0=0xe0000011: PG CD NW ac wp ne ET ts em mp PE
CR3=0x000000100000
</code></pre></div></div>

<p><img src="/assets/images/posts/toy-os-3-paging/paging.png" alt="Bochs: Entering MBR, Successfully loaded loader, Successfully entered protection mode, Successfully enabled paging" /></p>

<p><em>The last line is written through <code class="language-plaintext highlighter-rouge">gs</code> with base <code class="language-plaintext highlighter-rouge">0xC00B8000</code>, so it went through the page tables.</em></p>

<h2 id="the-virtual-address-space-now">The virtual address space now</h2>

<p><code class="language-plaintext highlighter-rouge">info tab</code> in Bochs lists every mapping in the current page tables:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cr3: 0x000000100000
0x0000000000000000-0x00000000000fffff -&gt; 0x000000000000-0x0000000fffff
0x00000000c0000000-0x00000000c00fffff -&gt; 0x000000000000-0x0000000fffff
0x00000000ffc00000-0x00000000ffc00fff -&gt; 0x000000101000-0x000000101fff
0x00000000fff00000-0x00000000fff00fff -&gt; 0x000000101000-0x000000101fff
0x00000000fff01000-0x00000000ffffefff -&gt; 0x000000402000-0x0000004fffff
0x00000000fffff000-0x00000000ffffffff -&gt; 0x000000100000-0x000000100fff
</code></pre></div></div>

<p><img src="/assets/images/posts/toy-os-3-paging/loader-va.svg" alt="Virtual address space after paging: identity map of 1 MiB, the kernel alias at 0xC0000000, empty kernel PDEs, and the page tables at 0xFFC00000; physical: low 1 MiB, PD+PTs, rest of RAM unmapped" /></p>

<p><em>The loader’s virtual address space (left) and where it lands in physical memory (right). Hatched = no mapping; touching it is a page fault.</em></p>

<p>The first two lines are the double mapping of the first MiB. The other four all come from one entry, PDE 1023.</p>

<h2 id="the-recursive-entry">The recursive entry</h2>

<p>PDE 1023 points at the page directory itself. A PDE and a PTE have the same format, so the CPU happily uses the directory as if it were a page table. For any VA in the top 4 MiB, <code class="language-plaintext highlighter-rouge">0xFFC00000</code>–<code class="language-plaintext highlighter-rouge">0xFFFFFFFF</code>:</p>

<ol>
  <li>The PD index is 1023, so the “page table” is the page directory.</li>
  <li>The PT index <em>i</em> picks PDE <em>i</em>, so the “page frame” is page table <em>i</em>.</li>
</ol>

<p>So the top 4 MiB is a window onto all the page tables, laid out in PDE order, and the last page of it, PT index 1023, is the directory itself. The comment in <code class="language-plaintext highlighter-rouge">loader.S</code> works through VA <code class="language-plaintext highlighter-rouge">0xFFC00001</code>: PD index 1023 → the PD; PT index 0 → PDE 0 → PT0 at <code class="language-plaintext highlighter-rouge">0x101000</code>; offset 1 → byte 1 of PT0.</p>

<p><img src="/assets/images/posts/toy-os-3-paging/recursive.svg" alt="The recursive window from 0xFFC00000 to 4 GiB: PT0 twice, PTs 769-1022, the page directory at 0xFFFFF000, and holes for empty PDEs" /></p>

<p><em>The top 4 MiB through PDE 1023. It matches the last four lines of <code class="language-plaintext highlighter-rouge">info tab</code>.</em></p>

<p>As my commit note says, this trick only works because PDEs and PTEs share the same format. It lets code edit a page table <em>by virtual address</em> without knowing where the table sits physically. Two formulas give the entry for any VA:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">pte_t</span> <span class="o">*</span><span class="n">pte</span> <span class="o">=</span> <span class="p">(</span><span class="n">pte_t</span> <span class="o">*</span><span class="p">)(</span><span class="mh">0xFFC00000</span> <span class="o">+</span> <span class="p">(</span><span class="n">va</span> <span class="o">&gt;&gt;</span> <span class="mi">12</span><span class="p">)</span> <span class="o">*</span> <span class="mi">4</span><span class="p">);</span>
<span class="n">pde_t</span> <span class="o">*</span><span class="n">pde</span> <span class="o">=</span> <span class="p">(</span><span class="n">pde_t</span> <span class="o">*</span><span class="p">)(</span><span class="mh">0xFFFFF000</span> <span class="o">+</span> <span class="p">(</span><span class="n">va</span> <span class="o">&gt;&gt;</span> <span class="mi">22</span><span class="p">)</span> <span class="o">*</span> <span class="mi">4</span><span class="p">);</span>
</code></pre></div></div>

<p>The kernel uses the first one exactly once, in post 6, to map the frames of its new page tables before it switches to them.</p>

<p><a href="/blogs/os/toy-os-4-kernel.html">Post 4</a> is the last thing the loader does: copy the kernel’s ELF segments to <code class="language-plaintext highlighter-rouge">0xC0001000</code> and jump to <code class="language-plaintext highlighter-rouge">main</code>.</p>

<h2 id="references">References</h2>

<ol>
  <li>郑钢, 《操作系统真相还原》, ch. 4 (进入保护模式: A20, CR0.PE, flushing the pipeline) and ch. 5 (启用内存分页机制: page directory at 1 MiB, the kernel at 3 GiB, PDE 1023 pointing at the directory).</li>
  <li>navi, <a href="https://segmentfault.com/a/1190000040153113">从零开始写 OS 内核 - GDT 与保护模式</a> and <a href="https://segmentfault.com/a/1190000040164669">虚拟内存初探</a>, segmentfault.</li>
  <li>D. P. Bovet and M. Cesati, <em>Understanding the Linux Kernel</em>, 3rd ed., ch. 2 “Memory Addressing”: “Paging in Hardware” (regular paging) and “Paging in Linux” (the kernel’s 3 GiB / 1 GiB split).</li>
  <li>Intel 64 and IA-32 Architectures Software Developer’s Manual, Vol. 3A, “Paging”: 32-bit paging, PDE and PTE formats.</li>
  <li>OSDev Wiki, <a href="https://wiki.osdev.org/A20_Line">A20 Line</a>.</li>
</ol>]]></content><author><name>Yuchen Tang</name></author><summary type="html"><![CDATA[The third toy-os worklog follows the loader from real mode into 32-bit protected mode and then turns on paging. It enables A20, loads the GDT, sets CR0.PE and far-jumps to reload CS; Bochs shows the CPU in protected mode while CS still caches its real-mode descriptor. The loader then builds a page directory at 1 MiB that maps the first MiB twice (identity and at 0xC0000000) plus a recursive entry, moves the GDT, the video segment and the stack into the higher half, and sets CR0.PG. Maps show where the page tables sit in physical memory and what the virtual address space looks like afterwards.]]></summary></entry><entry><title type="html">Toy OS 2: Segmentation and the GDT</title><link href="https://twicy.github.io/blogs/os/toy-os-2-segmentation.html" rel="alternate" type="text/html" title="Toy OS 2: Segmentation and the GDT" /><published>2026-10-09T00:00:00-07:00</published><updated>2026-10-09T00:00:00-07:00</updated><id>https://twicy.github.io/blogs/os/toy-os-2-segmentation</id><content type="html" xml:base="https://twicy.github.io/blogs/os/toy-os-2-segmentation.html"><![CDATA[<h1 id="toy-os-2-segmentation-and-the-gdt">Toy OS 2: Segmentation and the GDT</h1>

<p><em>Toy OS worklog: <a href="/blogs/os/toy-os-1-real-mode.html">1. Real mode</a> · <strong>2. Segmentation</strong> · <a href="/blogs/os/toy-os-3-paging.html">3. Paging</a> · <a href="/blogs/os/toy-os-4-kernel.html">4. Kernel</a> · <a href="/blogs/os/toy-os-5-interrupts.html">5. Interrupts</a> · <a href="/blogs/os/toy-os-6-buddy.html">6. Buddy</a> · <a href="/blogs/os/toy-os-7-slab.html">7. Slab</a> · <a href="https://github.com/twicy/toy-os/tree/fc88a1de36e8cd2cf495c46f3e6b36c01fc94f8b">Source</a></em></p>

<p>The loader at <code class="language-plaintext highlighter-rouge">0x800</code> (post 1) wants to leave real mode. In protected mode a segment register no longer holds <code class="language-plaintext highlighter-rouge">address / 16</code>. It holds a <em>selector</em>, an index into a table of <em>descriptors</em> that the OS writes in memory. So before setting <code class="language-plaintext highlighter-rouge">CR0.PE</code>, the loader needs that table: the Global Descriptor Table (GDT). This post covers the GDT. <a href="/blogs/os/toy-os-3-paging.html">Post 3</a> does the actual switch.</p>

<h2 id="table-of-contents">Table of Contents</h2>

<ul>
  <li><a href="#visible-and-hidden-parts-of-a-segment-register">Visible and hidden parts of a segment register</a></li>
  <li><a href="#selectors">Selectors</a></li>
  <li><a href="#descriptors">Descriptors</a></li>
  <li><a href="#the-loaders-gdt">The loader’s GDT</a></li>
  <li><a href="#flat-segments-like-linux">Flat segments, like Linux</a></li>
  <li><a href="#the-kernels-own-gdt">The kernel’s own GDT</a></li>
  <li><a href="#references">References</a></li>
</ul>

<h2 id="visible-and-hidden-parts-of-a-segment-register">Visible and hidden parts of a segment register</h2>

<p>Each segment register (<code class="language-plaintext highlighter-rouge">cs</code>, <code class="language-plaintext highlighter-rouge">ds</code>, <code class="language-plaintext highlighter-rouge">es</code>, <code class="language-plaintext highlighter-rouge">fs</code>, <code class="language-plaintext highlighter-rouge">gs</code>, <code class="language-plaintext highlighter-rouge">ss</code>) has a 16-bit visible part, the selector, and a hidden part, the descriptor cache: base, limit and access rights. When a selector is loaded (<code class="language-plaintext highlighter-rouge">mov ds, ax</code>, or a far jump for <code class="language-plaintext highlighter-rouge">cs</code>), the CPU reads the descriptor from the table, checks it, and copies it into the cache. Every later memory access uses the cache, not the table.</p>

<p>Bochs’s <code class="language-plaintext highlighter-rouge">sreg</code> prints both. In the line below, <code class="language-plaintext highlighter-rouge">dh</code> and <code class="language-plaintext highlighter-rouge">dl</code> are the two dwords of the cached descriptor:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cs:0x0008, dh=0x00cf9b00, dl=0x0000ffff, valid=1
	Code segment, base=0x00000000, limit=0xffffffff, Execute/Read, Non-Conforming, Accessed, 32-bit
</code></pre></div></div>

<p>Post 1 already showed the hidden part at work: at reset, CS has selector <code class="language-plaintext highlighter-rouge">0xF000</code> but base <code class="language-plaintext highlighter-rouge">0xFFFF0000</code>. In real mode a selector load sets base = selector × 16. In protected mode the base comes from the descriptor.</p>

<p>The CPU turns a <em>logical</em> address (selector:offset) into a <em>linear</em> address in three steps:</p>

<ol>
  <li>Find the descriptor: index × 8 into the table that GDTR points at.</li>
  <li>Check the offset against the limit, and the access against the type and privilege. A failed check raises #GP.</li>
  <li>Linear address = base + offset. Paging is still off, so linear = physical.</li>
</ol>

<p>Here is one real access. The loader’s <code class="language-plaintext highlighter-rouge">print_prot_mode</code> writes <code class="language-plaintext highlighter-rouge">mov [gs:ebx * 2], ax</code>. For the first character of row 2 of the screen, <code class="language-plaintext highlighter-rouge">ebx * 2 = 0x140</code>:</p>

<p><img src="/assets/images/posts/toy-os-2-segmentation/translation.svg" alt="gs = 0x18 selects GDT entry 3, the video segment with base 0xB8000, so offset 0x140 becomes linear address 0xB8140" /></p>

<p><em>A logical address through the loader’s GDT. The CPU actually reads the descriptor once, when <code class="language-plaintext highlighter-rouge">gs</code> is loaded, not on every access.</em></p>

<h2 id="selectors">Selectors</h2>

<p><img src="/assets/images/posts/toy-os-2-segmentation/selector.svg" alt="A selector: bits 15-3 are the descriptor index, bit 2 the table indicator, bits 1-0 the RPL" /></p>

<p><em>A selector. With TI = 0 and RPL = 0 it is simply the byte offset of the descriptor in the GDT.</em></p>

<p><code class="language-plaintext highlighter-rouge">boot.inc</code> builds the three selectors the loader uses:</p>

<div class="language-nasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">SELECTOR_CODE</span><span class="kd"> equ</span> <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="nv">SELECTOR_DESC_IDX_OFFSET</span><span class="p">)</span> <span class="o">+</span> <span class="err">\</span>
                  <span class="err">(0</span><span class="nf">b</span> <span class="o">&lt;&lt;</span> <span class="nv">SELECTOR_TI_OFFSET</span><span class="p">)</span> <span class="o">+</span> <span class="err">\</span>
                  <span class="err">(00</span><span class="nf">b</span> <span class="o">&lt;&lt;</span> <span class="nv">SELECTOR_RPL_OFFSET</span><span class="p">)</span>
<span class="no">SELECTOR_DATA</span><span class="kd"> equ</span> <span class="p">(</span><span class="mi">2</span> <span class="o">&lt;&lt;</span> <span class="nv">SELECTOR_DESC_IDX_OFFSET</span><span class="p">)</span> <span class="o">+</span> <span class="nv">...</span>
<span class="no">SELECTOR_VIDEO</span><span class="kd"> equ</span> <span class="p">(</span><span class="mi">3</span> <span class="o">&lt;&lt;</span> <span class="nv">SELECTOR_DESC_IDX_OFFSET</span><span class="p">)</span> <span class="o">+</span> <span class="nv">...</span>
</code></pre></div></div>

<p>Index 0 must be the null descriptor. Loading selector 0 into <code class="language-plaintext highlighter-rouge">ds</code> is allowed (it marks the register unusable), but any access through it faults.</p>

<h2 id="descriptors">Descriptors</h2>

<p>A descriptor is 8 bytes. The layout is odd because of history: the 80286 had 24-bit bases and 16-bit limits, and the 386 squeezed the missing high bits into the word the 286 had left reserved.</p>

<p><img src="/assets/images/posts/toy-os-2-segmentation/descriptor.svg" alt="The loader's code descriptor bit by bit: base 0, limit 0xFFFFF, G=1, D/B=1, P=1, DPL=0, S=1, type 1010" /></p>

<p><em>The loader’s code descriptor, <code class="language-plaintext highlighter-rouge">0x00CF9A00_0000FFFF</code>.</em></p>

<ul>
  <li><strong>Base</strong> (32 bits, in three pieces) and <strong>limit</strong> (20 bits, in two). With <strong>G = 1</strong> the limit counts 4 KiB units, so <code class="language-plaintext highlighter-rouge">0xFFFFF</code> means the whole 4 GiB.</li>
  <li><strong>D/B = 1</strong>: a 32-bit segment (default operand size and <code class="language-plaintext highlighter-rouge">esp</code> for stacks).</li>
  <li><strong>P = 1</strong>: present. <strong>DPL</strong>: the privilege level the descriptor belongs to (0 = kernel, 3 = user).</li>
  <li><strong>S = 1</strong>: a code or data segment. With S = 0 it would be a system descriptor, such as a TSS or a gate.</li>
  <li><strong>Type</strong> for code is X C R A: executable, not conforming, readable, accessed = <code class="language-plaintext highlighter-rouge">1010</code>. For data it is X E W A: not executable, expands up, writable = <code class="language-plaintext highlighter-rouge">0010</code>. The CPU sets A when it loads the descriptor, which is why Bochs shows <code class="language-plaintext highlighter-rouge">dh=0x00cf9b00</code> (type <code class="language-plaintext highlighter-rouge">1011</code>).</li>
</ul>

<p>Instead of a magic <code class="language-plaintext highlighter-rouge">0x00CF9A00</code>, <code class="language-plaintext highlighter-rouge">boot.inc</code> composes each field with a shift:</p>

<div class="language-nasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">; -------- Code segment --------</span>
<span class="c1">; Base: 0, Limit: 0xFFFFF * 4K = 4G</span>
<span class="c1">; Type: x=1,c=0,r=1,a=0</span>
<span class="no">DESC_CODE_HIGH_32</span><span class="kd"> equ</span> <span class="p">(</span><span class="mi">0</span> <span class="o">&lt;&lt;</span> <span class="nv">DESC_BASE_31_24_OFFSET</span><span class="p">)</span> <span class="o">+</span> <span class="err">\</span>
                      <span class="err">(1</span> <span class="err">&lt;&lt;</span> <span class="nf">DESC_G_OFFSET</span><span class="p">)</span> <span class="o">+</span> <span class="err">\</span>
                      <span class="err">(1</span> <span class="err">&lt;&lt;</span> <span class="nf">DESC_DB_OFFSET</span><span class="p">)</span> <span class="o">+</span> <span class="err">\</span>
                      <span class="err">(0</span> <span class="err">&lt;&lt;</span> <span class="nf">DESC_L_OFFSET</span><span class="p">)</span> <span class="o">+</span> <span class="err">\</span>
                      <span class="err">(0</span> <span class="err">&lt;&lt;</span> <span class="nf">DESC_AVL_OFFSET</span><span class="p">)</span> <span class="o">+</span> <span class="err">\</span>
                      <span class="err">(1111</span><span class="nf">b</span> <span class="o">&lt;&lt;</span> <span class="nv">DESC_LIMIT_19_16_OFFSET</span><span class="p">)</span> <span class="o">+</span> <span class="err">\</span>
                      <span class="err">(1</span> <span class="err">&lt;&lt;</span> <span class="nf">DESC_P_OFFSET</span><span class="p">)</span> <span class="o">+</span> <span class="err">\</span>
                      <span class="err">(00</span><span class="nf">b</span> <span class="o">&lt;&lt;</span> <span class="nv">DESC_DPL_OFFSET</span><span class="p">)</span> <span class="o">+</span> <span class="err">\</span>
                      <span class="err">(1</span> <span class="err">&lt;&lt;</span> <span class="nf">DESC_S_OFFSET</span><span class="p">)</span> <span class="o">+</span> <span class="err">\</span>
                      <span class="err">(1010</span><span class="nf">b</span> <span class="o">&lt;&lt;</span> <span class="nv">DESC_TYPE_OFFSET</span><span class="p">)</span> <span class="o">+</span> <span class="err">\</span>
                      <span class="err">(0</span> <span class="err">&lt;&lt;</span> <span class="nf">DESC_BASE_23_16_OFFSET</span><span class="p">)</span>

<span class="no">DESC_CODE_LOW_32</span><span class="kd"> equ</span> <span class="p">(</span><span class="mi">0</span> <span class="o">&lt;&lt;</span> <span class="nv">DESC_BASE_15_0_OFFSET</span><span class="p">)</span> <span class="o">+</span> <span class="err">\</span>
                     <span class="err">(0</span><span class="nf">xFFFF</span> <span class="o">&lt;&lt;</span> <span class="nv">DESC_LIMIT_15_0_OFFSET</span><span class="p">)</span>
</code></pre></div></div>

<h2 id="the-loaders-gdt">The loader’s GDT</h2>

<p>The table lives inside <code class="language-plaintext highlighter-rouge">loader.bin</code>, right after its first instruction:</p>

<div class="language-nasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">section</span> <span class="nv">loader</span> <span class="nv">vstart</span><span class="err">=</span><span class="nv">LOADER_BASE_ADDR</span>
<span class="nf">jmp</span> <span class="nv">loader_entry</span>

<span class="c1">; temporary gdt to enter protection mode</span>
<span class="nl">GDT_BASE:</span>
<span class="nl">RESV_DESC:</span>
    <span class="kd">dd</span> <span class="mh">0x0</span>
    <span class="kd">dd</span> <span class="mh">0x0</span>
<span class="nl">CODE_DESC:</span>
    <span class="kd">dd</span> <span class="nv">DESC_CODE_LOW_32</span>
    <span class="kd">dd</span> <span class="nv">DESC_CODE_HIGH_32</span>
<span class="nl">DATA_DESC:</span>
    <span class="kd">dd</span> <span class="nv">DESC_DATA_LOW_32</span>
    <span class="kd">dd</span> <span class="nv">DESC_DATA_HIGH_32</span>
<span class="nl">VIDEO_DESC:</span>
    <span class="kd">dd</span> <span class="nv">DESC_VIDEO_LOW_32</span>
    <span class="kd">dd</span> <span class="nv">DESC_VIDEO_HIGH_32</span>
<span class="no">GDT_SIZE</span><span class="kd"> equ</span> <span class="kc">$</span> <span class="o">-</span> <span class="nv">GDT_BASE</span>
<span class="no">GDT_LIMIT</span><span class="kd"> equ</span> <span class="nv">GDT_SIZE</span> <span class="o">-</span> <span class="mi">1</span>

<span class="c1">; lgdt 48-bit</span>
<span class="nl">GDT_PTR:</span>
    <span class="kd">dw</span> <span class="nv">GDT_LIMIT</span>
    <span class="kd">dd</span> <span class="nv">GDT_BASE</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">jmp loader_entry</code> assembles to 3 bytes (<code class="language-plaintext highlighter-rouge">E9 87 00</code>), so in memory:</p>

<table>
  <thead>
    <tr>
      <th>Physical</th>
      <th>Bytes</th>
      <th>What</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">0x800</code>–<code class="language-plaintext highlighter-rouge">0x802</code></td>
      <td>3</td>
      <td><code class="language-plaintext highlighter-rouge">jmp loader_entry</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">0x803</code>–<code class="language-plaintext highlighter-rouge">0x822</code></td>
      <td>32</td>
      <td>GDT: null, code, data, video</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">0x823</code>–<code class="language-plaintext highlighter-rouge">0x828</code></td>
      <td>6</td>
      <td>GDTR operand for <code class="language-plaintext highlighter-rouge">lgdt</code>: limit <code class="language-plaintext highlighter-rouge">0x001F</code>, base <code class="language-plaintext highlighter-rouge">0x00000803</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">0x829</code>–</td>
      <td> </td>
      <td>messages, then code</td>
    </tr>
  </tbody>
</table>

<p><code class="language-plaintext highlighter-rouge">lgdt [GDT_PTR]</code> copies those 6 bytes into GDTR. The table itself stays where it is: the CPU reads it from memory every time a selector is loaded. At the first protected-mode instruction (<code class="language-plaintext highlighter-rouge">0x922</code>, post 3), Bochs shows:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gdtr:base=0x0000000000000803, limit=0x1f
gdt (base=0x0000000000000803, limit=31):
gdt[0x0000]=&lt;null entry&gt;
gdt[0x0008]=Code segment, base=0x00000000, limit=0xffffffff, Execute/Read, Non-Conforming, Accessed, 32-bit
gdt[0x0010]=Data segment, base=0x00000000, limit=0xffffffff, Read/Write
gdt[0x0018]=Data segment, base=0x000b8000, limit=0x00007fff, Read/Write
</code></pre></div></div>

<p>The video descriptor is the only interesting one. Its base is <code class="language-plaintext highlighter-rouge">0xB8000</code> and its limit is <code class="language-plaintext highlighter-rouge">0x0007</code> with G = 1, so the limit is (7 + 1) × 4 KiB − 1 = <code class="language-plaintext highlighter-rouge">0x7FFF</code>: 32 KiB, exactly the color text buffer <code class="language-plaintext highlighter-rouge">0xB8000</code>–<code class="language-plaintext highlighter-rouge">0xBFFFF</code>. A write through <code class="language-plaintext highlighter-rouge">gs</code> past the buffer raises #GP instead of scribbling over the VGA BIOS ROM. That limit check is the only real protection segmentation gives toy-os.</p>

<p><img src="/assets/images/posts/toy-os-2-segmentation/segments.svg" alt="The linear address space: the code and data segments cover all 4 GiB, the video segment only 0xB8000 to 0xBFFFF" /></p>

<p><em>What each of the loader’s segments can reach.</em></p>

<h2 id="flat-segments-like-linux">Flat segments, like Linux</h2>

<p>Code and data are <em>flat</em>: base 0, limit 4 GiB. Translation does nothing, offset = linear address, and a C pointer is just a linear address. ULK (ch. 2, “Segmentation in Linux”) describes Linux doing the same. Its four main segments, <code class="language-plaintext highlighter-rouge">__KERNEL_CS</code>, <code class="language-plaintext highlighter-rouge">__KERNEL_DS</code>, <code class="language-plaintext highlighter-rouge">__USER_CS</code> and <code class="language-plaintext highlighter-rouge">__USER_DS</code>, all have base 0 and limit 2<sup>32</sup> − 1. They differ only in type and DPL. ULK’s reasons are that memory management is simpler when every process uses the same linear addresses, and that RISC architectures barely support segmentation. Protection moves to paging (post 3). Segments are left with one job: their DPL decides whether the CPU runs at ring 0 or ring 3.</p>

<h2 id="the-kernels-own-gdt">The kernel’s own GDT</h2>

<p>The loader’s GDT has two problems. It lives inside <code class="language-plaintext highlighter-rouge">loader.bin</code>, which is dead once the kernel runs, and it has no user segments. In the revision commit (<a href="https://github.com/twicy/toy-os/commit/cbd7402"><code class="language-plaintext highlighter-rouge">cbd7402</code></a>), <code class="language-plaintext highlighter-rouge">main()</code> starts with <code class="language-plaintext highlighter-rouge">init_gdt()</code>, which builds a new table in the kernel’s <code class="language-plaintext highlighter-rouge">.bss</code>:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* GDT indices, a selector is index &lt;&lt; 3 | RPL
 * kernel code must stay at 1: the loader's cs and every IDT gate use selector 0x08 */</span>
<span class="cp">#define KERNEL_CODE_IDX 0x1
#define KERNEL_DATA_IDX 0x2
#define USER_CODE_IDX 0x3
#define USER_DATA_IDX 0x4
</span>
<span class="k">struct</span> <span class="n">gdt_entry</span> <span class="p">{</span>
    <span class="kt">uint16_t</span> <span class="n">limit_low</span><span class="p">;</span>
    <span class="kt">uint16_t</span> <span class="n">base_low</span><span class="p">;</span>
    <span class="kt">uint8_t</span> <span class="n">base_middle</span><span class="p">;</span>
    <span class="kt">uint8_t</span> <span class="n">access</span><span class="p">;</span>
    <span class="kt">uint8_t</span> <span class="n">granularity</span><span class="p">;</span>
    <span class="kt">uint8_t</span> <span class="n">base_high</span><span class="p">;</span>
<span class="p">}</span> <span class="n">__attribute__</span><span class="p">((</span><span class="n">packed</span><span class="p">));</span>
</code></pre></div></div>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="k">struct</span> <span class="n">gdt_entry</span> <span class="n">gdt</span><span class="p">[</span><span class="n">MAX_GDT_ENTRIES</span><span class="p">];</span>
<span class="k">static</span> <span class="k">struct</span> <span class="n">gdtr</span> <span class="n">gdtr</span><span class="p">;</span>

<span class="kt">void</span> <span class="nf">init_gdt</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">gdt</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="k">struct</span> <span class="n">gdt_entry</span><span class="p">){</span><span class="mi">0</span><span class="p">};</span>

    <span class="n">gdt</span><span class="p">[</span><span class="n">KERNEL_CODE_IDX</span><span class="p">].</span><span class="n">limit_low</span> <span class="o">=</span> <span class="mh">0xFFFF</span><span class="p">;</span>
    <span class="n">gdt</span><span class="p">[</span><span class="n">KERNEL_CODE_IDX</span><span class="p">].</span><span class="n">access</span> <span class="o">=</span> <span class="mh">0x9A</span><span class="p">;</span> <span class="c1">// Present, Ring 0, Code Segment</span>
    <span class="n">gdt</span><span class="p">[</span><span class="n">KERNEL_CODE_IDX</span><span class="p">].</span><span class="n">granularity</span> <span class="o">=</span> <span class="mh">0xCF</span><span class="p">;</span> <span class="c1">// 4K granularity, 32-bit</span>
    <span class="p">...</span>
    <span class="n">gdtr</span><span class="p">.</span><span class="n">limit</span> <span class="o">=</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">gdt</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span><span class="p">;</span>
    <span class="n">gdtr</span><span class="p">.</span><span class="n">base</span> <span class="o">=</span> <span class="p">(</span><span class="kt">uint32_t</span><span class="p">)</span><span class="o">&amp;</span><span class="n">gdt</span><span class="p">;</span>

    <span class="c1">// lgdt only changes where descriptors are read from, each segment register keeps</span>
    <span class="c1">// the descriptor it cached when loaded, so reload them all from the new table</span>
    <span class="c1">// cs can only be reloaded by a far jump</span>
    <span class="n">asm</span> <span class="k">volatile</span><span class="p">(</span><span class="s">"lgdt %0</span><span class="se">\n\t</span><span class="s">"</span>
                 <span class="s">"ljmp %1, $1f</span><span class="se">\n</span><span class="s">"</span>
                 <span class="s">"1:</span><span class="se">\n\t</span><span class="s">"</span>
                 <span class="s">"mov %2, %%ax</span><span class="se">\n\t</span><span class="s">"</span>
                 <span class="s">"mov %%ax, %%ds</span><span class="se">\n\t</span><span class="s">"</span>
                 <span class="s">"mov %%ax, %%es</span><span class="se">\n\t</span><span class="s">"</span>
                 <span class="s">"mov %%ax, %%fs</span><span class="se">\n\t</span><span class="s">"</span>
                 <span class="s">"mov %%ax, %%gs</span><span class="se">\n\t</span><span class="s">"</span>
                 <span class="s">"mov %%ax, %%ss"</span>
                 <span class="o">:</span> <span class="o">:</span> <span class="s">"m"</span><span class="p">(</span><span class="n">gdtr</span><span class="p">),</span> <span class="s">"i"</span><span class="p">(</span><span class="n">KERNEL_CODE_SELECTOR</span><span class="p">),</span> <span class="s">"i"</span><span class="p">(</span><span class="n">KERNEL_DATA_SELECTOR</span><span class="p">)</span>
                 <span class="o">:</span> <span class="s">"eax"</span><span class="p">,</span> <span class="s">"memory"</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>Index</th>
      <th>Selector</th>
      <th><code class="language-plaintext highlighter-rouge">access</code></th>
      <th>Bits P · DPL · S · type</th>
      <th>Segment</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td><code class="language-plaintext highlighter-rouge">0x08</code></td>
      <td><code class="language-plaintext highlighter-rouge">0x9A</code></td>
      <td>1 · 00 · 1 · 1010</td>
      <td>kernel code</td>
    </tr>
    <tr>
      <td>2</td>
      <td><code class="language-plaintext highlighter-rouge">0x10</code></td>
      <td><code class="language-plaintext highlighter-rouge">0x92</code></td>
      <td>1 · 00 · 1 · 0010</td>
      <td>kernel data</td>
    </tr>
    <tr>
      <td>3</td>
      <td><code class="language-plaintext highlighter-rouge">0x18</code> (<code class="language-plaintext highlighter-rouge">0x1B</code> with RPL 3)</td>
      <td><code class="language-plaintext highlighter-rouge">0xFA</code></td>
      <td>1 · 11 · 1 · 1010</td>
      <td>user code</td>
    </tr>
    <tr>
      <td>4</td>
      <td><code class="language-plaintext highlighter-rouge">0x20</code> (<code class="language-plaintext highlighter-rouge">0x23</code> with RPL 3)</td>
      <td><code class="language-plaintext highlighter-rouge">0xF2</code></td>
      <td>1 · 11 · 1 · 0010</td>
      <td>user data</td>
    </tr>
  </tbody>
</table>

<p><code class="language-plaintext highlighter-rouge">granularity = 0xCF</code> is the high nibble G = 1, D/B = 1 and the low nibble limit bits 19:16 = <code class="language-plaintext highlighter-rouge">0xF</code>. All four segments are flat, so they differ only in DPL and type, as in Linux. Index 3 used to be the video segment. The kernel’s VGA code now writes to <code class="language-plaintext highlighter-rouge">0xC00B8000</code> through the flat data segment, so <code class="language-plaintext highlighter-rouge">gs</code> becomes plain <code class="language-plaintext highlighter-rouge">0x10</code>.</p>

<p>The <code class="language-plaintext highlighter-rouge">lgdt</code> comment is the important part. GDTR only says where descriptors are <em>read from</em>, and every segment register keeps the descriptor it cached when it was loaded. If <code class="language-plaintext highlighter-rouge">cs</code> and <code class="language-plaintext highlighter-rouge">ss</code> were not reloaded, they would keep pointing into the loader’s table, and that would only show up much later, as a fault the first time something reloads them. <code class="language-plaintext highlighter-rouge">cs</code> cannot be the target of a <code class="language-plaintext highlighter-rouge">mov</code>, hence the <code class="language-plaintext highlighter-rouge">ljmp</code> to the next line. Bochs right after <code class="language-plaintext highlighter-rouge">init_gdt()</code>:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gdt (base=0x00000000c0007d80, limit=127):
gdt[0x0000]=&lt;null entry&gt;
gdt[0x0008]=Code segment, base=0x00000000, limit=0xffffffff, Execute/Read, Non-Conforming, Accessed, 32-bit
gdt[0x0010]=Data segment, base=0x00000000, limit=0xffffffff, Read/Write, Accessed
gdt[0x0018]=Code segment, base=0x00000000, limit=0xffffffff, Execute/Read, Non-Conforming, 32-bit
gdt[0x0020]=Data segment, base=0x00000000, limit=0xffffffff, Read/Write
gdt[0x0028]=&lt;null entry&gt;
...
gs:0x0010, dh=0x00cf9300, dl=0x0000ffff, valid=1
	Data segment, base=0x00000000, limit=0xffffffff, Read/Write, Accessed
</code></pre></div></div>

<p>The table has 16 entries (<code class="language-plaintext highlighter-rouge">limit = 16 × 8 − 1 = 127</code>). The free ones are for later, such as a TSS. It sits at VA <code class="language-plaintext highlighter-rouge">0xC0007D80</code>, which is physical <code class="language-plaintext highlighter-rouge">0x7D80</code>–<code class="language-plaintext highlighter-rouge">0x7DFF</code>, the last 128 bytes of where the MBR was loaded in post 1. The kernel image has long since overwritten the MBR (post 4).</p>

<p>A word on privilege, which post 5 builds on. The low two bits of <code class="language-plaintext highlighter-rouge">cs</code> are the <em>current privilege level</em> (CPL). A selector carries a <em>requested</em> privilege level (RPL), and a descriptor its DPL. To load a data segment, the CPU requires max(CPL, RPL) ≤ DPL. Everything in toy-os runs at CPL 0 so far, so the checks always pass.</p>

<h2 id="references">References</h2>

<ol>
  <li>郑钢, 《操作系统真相还原》, ch. 4 (保护模式入门): segment descriptors, the GDT, selectors.</li>
  <li>navi, <a href="https://segmentfault.com/a/1190000040153113">从零开始写 OS 内核 - GDT 与保护模式</a> and <a href="https://segmentfault.com/a/1190000040187304">全局描述符表 GDT</a>, segmentfault.</li>
  <li>D. P. Bovet and M. Cesati, <em>Understanding the Linux Kernel</em>, 3rd ed., ch. 2 “Memory Addressing”: “Segmentation in Hardware” and “Segmentation in Linux”.</li>
  <li>Intel 64 and IA-32 Architectures Software Developer’s Manual, Vol. 3A, “Protected-Mode Memory Management”: segment selectors, segment descriptors, descriptor tables.</li>
</ol>]]></content><author><name>Yuchen Tang</name></author><summary type="html"><![CDATA[The second toy-os worklog covers x86 segmentation, which has to be set up before the switch to protected mode. In protected mode a segment register holds a selector, and the CPU caches the base, limit and access rights of the descriptor it names in the Global Descriptor Table. The post decodes a descriptor bit by bit, traces one address through the loader's four-entry GDT, and explains why toy-os, like Linux, uses flat 4 GiB segments. Once in C, the kernel replaces that GDT with its own kernel and user segments, which end up in the last 128 bytes of where the MBR used to be.]]></summary></entry><entry><title type="html">Toy OS 1: From Power-On to the MBR</title><link href="https://twicy.github.io/blogs/os/toy-os-1-real-mode.html" rel="alternate" type="text/html" title="Toy OS 1: From Power-On to the MBR" /><published>2026-10-08T00:00:00-07:00</published><updated>2026-10-08T00:00:00-07:00</updated><id>https://twicy.github.io/blogs/os/toy-os-1-real-mode</id><content type="html" xml:base="https://twicy.github.io/blogs/os/toy-os-1-real-mode.html"><![CDATA[<h1 id="toy-os-1-from-power-on-to-the-mbr">Toy OS 1: From Power-On to the MBR</h1>

<p><em>Toy OS worklog: <strong>1. Real mode</strong> · <a href="/blogs/os/toy-os-2-segmentation.html">2. Segmentation</a> · <a href="/blogs/os/toy-os-3-paging.html">3. Paging</a> · <a href="/blogs/os/toy-os-4-kernel.html">4. Kernel</a> · <a href="/blogs/os/toy-os-5-interrupts.html">5. Interrupts</a> · <a href="/blogs/os/toy-os-6-buddy.html">6. Buddy</a> · <a href="/blogs/os/toy-os-7-slab.html">7. Slab</a> · <a href="https://github.com/twicy/toy-os/tree/fc88a1de36e8cd2cf495c46f3e6b36c01fc94f8b">Source</a></em></p>

<p>toy-os is a small 32-bit x86 kernel I am writing from scratch. I want to <em>see</em> what happens between power-on and a kernel that hands out memory, not just read about it. It runs in the Bochs emulator. The boot path follows 《操作系统真相还原》 and navi’s <a href="https://segmentfault.com/a/1190000040124650">从零开始写 OS 内核</a> series. For memory management I copy Linux’s design, as described in <em>Understanding the Linux Kernel</em> (ULK).</p>

<p>The source code is on GitHub: <a href="https://github.com/twicy/toy-os">twicy/toy-os</a>. These worklogs cover the code up to commit <a href="https://github.com/twicy/toy-os/commit/fc88a1d"><code class="language-plaintext highlighter-rouge">fc88a1d</code></a> (2026-10-20), one milestone per post:</p>

<ol>
  <li>Real mode: from power-on through the BIOS to the MBR (this post)</li>
  <li><a href="/blogs/os/toy-os-2-segmentation.html">Segmentation and the GDT</a></li>
  <li><a href="/blogs/os/toy-os-3-paging.html">Protected mode and paging</a></li>
  <li><a href="/blogs/os/toy-os-4-kernel.html">Loading the kernel</a></li>
  <li><a href="/blogs/os/toy-os-5-interrupts.html">Interrupts, the PIC and the timer</a></li>
  <li><a href="/blogs/os/toy-os-6-buddy.html">Physical memory: E820, memblock and the buddy system</a></li>
  <li><a href="/blogs/os/toy-os-7-slab.html">Slab and kmalloc</a></li>
</ol>

<p>Every address in these posts comes from the real thing: Bochs’s debugger, <code class="language-plaintext highlighter-rouge">readelf</code>, or the toy-os screen.</p>

<h2 id="table-of-contents">Table of Contents</h2>

<ul>
  <li><a href="#setup">Setup</a></li>
  <li><a href="#power-on-the-first-instruction">Power-on: the first instruction</a></li>
  <li><a href="#the-bios-loads-sector-0-to-0x7c00">The BIOS loads sector 0 to 0x7C00</a></li>
  <li><a href="#real-mode-addressing-and-the-first-mib">Real-mode addressing and the first MiB</a></li>
  <li><a href="#the-mbr">The MBR</a></li>
  <li><a href="#references">References</a></li>
</ul>

<h2 id="setup">Setup</h2>

<p>The tools:</p>

<ul>
  <li><strong>Bochs 2.8</strong>, with the <code class="language-plaintext highlighter-rouge">bochs</code> and <code class="language-plaintext highlighter-rouge">bochs-x</code> packages, 512 MiB of RAM and the <em>legacy</em> BIOS image (<code class="language-plaintext highlighter-rouge">BIOS-bochs-legacy</code>). The sound card needs <code class="language-plaintext highlighter-rouge">sound: driver=dummy</code>, and the keyboard is set with <code class="language-plaintext highlighter-rouge">keyboard:</code>, because the old <code class="language-plaintext highlighter-rouge">keyboard_mapping:</code> option is gone.</li>
  <li><strong>nasm</strong> for the boot code, and later <code class="language-plaintext highlighter-rouge">gcc -m32</code> and <code class="language-plaintext highlighter-rouge">ld</code> for the kernel (post 4).</li>
  <li>A flat disk image, <code class="language-plaintext highlighter-rouge">toy-os.img</code>, with 20 cylinders × 16 heads × 63 sectors. The Makefile writes each binary into it with <code class="language-plaintext highlighter-rouge">dd</code>.</li>
</ul>

<div class="language-make highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">dd</span> <span class="nv">if</span><span class="o">=</span>./mbr.bin    <span class="nv">of</span><span class="o">=</span>./toy-os.img <span class="nv">bs</span><span class="o">=</span>512 <span class="nv">count</span><span class="o">=</span>1   <span class="nv">conv</span><span class="o">=</span>notrunc
<span class="err">dd</span> <span class="nv">if</span><span class="o">=</span>./loader.bin <span class="nv">of</span><span class="o">=</span>./toy-os.img <span class="nv">bs</span><span class="o">=</span>512 <span class="nv">count</span><span class="o">=</span>4   <span class="nv">seek</span><span class="o">=</span>2 <span class="nv">conv</span><span class="o">=</span>notrunc
<span class="err">dd</span> <span class="nv">if</span><span class="o">=</span>./kernel.bin <span class="nv">of</span><span class="o">=</span>./toy-os.img <span class="nv">bs</span><span class="o">=</span>512 <span class="nv">count</span><span class="o">=</span>200 <span class="nv">seek</span><span class="o">=</span>9 <span class="nv">conv</span><span class="o">=</span>notrunc
</code></pre></div></div>

<p><img src="/assets/images/posts/toy-os-1-real-mode/disk-layout.svg" alt="The toy-os disk image: the MBR in sector 0, loader.bin in sectors 2 to 5, kernel.bin in sectors 9 to 208" /></p>

<p><em>Where each binary sits on the disk, and who reads it into memory.</em></p>

<p>On a HiDPI screen, Bochs’s own X11 window is tiny and cannot be scaled. So <code class="language-plaintext highlighter-rouge">make run</code> starts Bochs with its VNC server (<code class="language-plaintext highlighter-rouge">display_library: rfb</code>) and opens it in Remmina, scaled up. Two catches: the Bochs VNC server only speaks 8-bit color, and it panics on the X11 keymap files, so it runs without one. The old window is still there as <code class="language-plaintext highlighter-rouge">make run-x11</code>. The screenshots in these posts come straight from that VNC framebuffer.</p>

<h2 id="power-on-the-first-instruction">Power-on: the first instruction</h2>

<p>A reset puts the CPU in <em>real mode</em>, the 16-bit mode of the 8086. Real-mode addresses are <code class="language-plaintext highlighter-rouge">segment × 16 + offset</code>, yet the first instruction does not come from <code class="language-plaintext highlighter-rouge">CS × 16 + IP</code>. Here is the Bochs debugger at instruction 0, before anything has run (trimmed):</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Next at t=0
(0) [0x0000fffffff0] f000:fff0 (unk. ctxt): jmpf 0xf000:e05b          ; ea5be000f0
rip: 00000000_0000fff0
cs:0xf000, dh=0xff0093ff, dl=0x0000ffff, valid=7
	Data segment, base=0xffff0000, limit=0x0000ffff, Read/Write, Accessed
CR0=0x60000010: pg CD NW ac wp ne ET ts em mp pe
</code></pre></div></div>

<p>CS holds <code class="language-plaintext highlighter-rouge">0xF000</code>, but its <strong>base</strong> is <code class="language-plaintext highlighter-rouge">0xFFFF0000</code>, not <code class="language-plaintext highlighter-rouge">0xF000 × 16 = 0xF0000</code>. Every segment register has a hidden part, the descriptor cache, holding the base and limit the CPU actually uses. At reset the CPU loads a special value into it, so the first fetch is at <code class="language-plaintext highlighter-rouge">0xFFFF0000 + 0xFFF0 = 0xFFFFFFF0</code>, 16 bytes below 4 GiB. The chipset maps the BIOS ROM there. That ROM has room for one instruction, a far jump: <code class="language-plaintext highlighter-rouge">jmpf 0xf000:e05b</code> reloads CS the normal real-mode way (base <code class="language-plaintext highlighter-rouge">0xF0000</code>), and the BIOS carries on from its copy at <code class="language-plaintext highlighter-rouge">0xFE05B</code> in the first MiB. (Post 2 is about this hidden part.)</p>

<p><code class="language-plaintext highlighter-rouge">CR0.PE = 0</code> (lower-case <code class="language-plaintext highlighter-rouge">pe</code> above) means real mode. The cache is also off (<code class="language-plaintext highlighter-rouge">CD</code>, <code class="language-plaintext highlighter-rouge">NW</code>) until the BIOS turns it on.</p>

<h2 id="the-bios-loads-sector-0-to-0x7c00">The BIOS loads sector 0 to 0x7C00</h2>

<p>The BIOS runs its power-on self test (POST), sets up the interrupt vector table and its data area, finds the disks and prints a banner. Then it walks the boot order. For a hard disk, it reads LBA 0 (512 bytes) to <code class="language-plaintext highlighter-rouge">0000:7C00</code>. If the last two bytes are <code class="language-plaintext highlighter-rouge">0x55 0xAA</code>, it jumps there. I set a breakpoint at <code class="language-plaintext highlighter-rouge">0x7C00</code>:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(0) Breakpoint 1, 0x0000000000007c00 in ?? ()
Next at t=7109628
(0) [0x000000007c00] 0000:7c00 (unk. ctxt): jmpf 0x0000:7c48          ; ea487c0000
rax: 00000000_0000aa55
rdx: 00000000_00000080
rsp: 00000000_0000ffd6
</code></pre></div></div>

<p>The BIOS ran 7.1 million instructions before the first byte of toy-os. <code class="language-plaintext highlighter-rouge">DL = 0x80</code> is the boot drive (the first hard disk). <code class="language-plaintext highlighter-rouge">AX = 0xAA55</code> is probably left over from the signature check.</p>

<p><img src="/assets/images/posts/toy-os-1-real-mode/bios.png" alt="Bochs stopped at 0x7C00: the VGA BIOS and Bochs BIOS banners, then &quot;Booting from Hard Disk...&quot;" /></p>

<p><em>The screen at that breakpoint. The BIOS has printed “Booting from Hard Disk…” and toy-os has not run yet.</em></p>

<p>Why 0x7C00? The usual explanation goes back to the IBM PC 5150. Its smallest model had 32 KiB of RAM, and its BIOS put the boot sector in the last 1 KiB of it: 0x8000 − 0x400 = 0x7C00, leaving 512 bytes above the sector for its data and stack.</p>

<h2 id="real-mode-addressing-and-the-first-mib">Real-mode addressing and the first MiB</h2>

<p>In real mode a segment register is just a number: the physical address is <code class="language-plaintext highlighter-rouge">segment × 16 + offset</code>. That gives 20 bits, 1 MiB. <code class="language-plaintext highlighter-rouge">FFFF:FFFF</code> would be <code class="language-plaintext highlighter-rouge">0x10FFEF</code>, but the 8086 had only 20 address lines, so the address wrapped around to <code class="language-plaintext highlighter-rouge">0x0FFEF</code>. Later CPUs keep that wrap-around until the A20 line is enabled (post 3).</p>

<p>This is the first MiB at the moment the MBR runs, with the two pieces toy-os has placed so far:</p>

<p><img src="/assets/images/posts/toy-os-1-real-mode/low-1mib-mbr.svg" alt="Physical memory map of the first MiB when the MBR runs" /></p>

<p><em>Physical memory below 1 MiB when the MBR runs (not to scale). “free” means unused RAM.</em></p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">0x00000</code>–<code class="language-plaintext highlighter-rouge">0x004FF</code>: the real-mode <strong>interrupt vector table</strong> (256 × 4-byte <code class="language-plaintext highlighter-rouge">segment:offset</code> pairs) and the <strong>BIOS Data Area</strong>. The BIOS needs both, and they must not be overwritten while the code still makes BIOS calls.</li>
  <li><code class="language-plaintext highlighter-rouge">0x07C00</code>–<code class="language-plaintext highlighter-rouge">0x07DFF</code>: <strong>the MBR</strong>. It sets <code class="language-plaintext highlighter-rouge">sp = 0x7C00</code>, so its stack grows down from just below itself.</li>
  <li><code class="language-plaintext highlighter-rouge">0x00800</code>–<code class="language-plaintext highlighter-rouge">0x00FFF</code>: where the MBR puts <strong>loader.bin</strong>, 4 sectors. It used to be at <code class="language-plaintext highlighter-rouge">0x900</code>, as in the book, and moved to <code class="language-plaintext highlighter-rouge">0x800</code> in a later revision. The E820 memory map (post 6) sits just below it, at <code class="language-plaintext highlighter-rouge">0x500</code>–<code class="language-plaintext highlighter-rouge">0x784</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">0x9F000</code>–<code class="language-plaintext highlighter-rouge">0x9FFFF</code>: the <strong>Extended BIOS Data Area</strong>. From <code class="language-plaintext highlighter-rouge">0xA0000</code> up it is not RAM at all: video memory (the color text buffer is at <code class="language-plaintext highlighter-rouge">0xB8000</code>), the VGA BIOS ROM at <code class="language-plaintext highlighter-rouge">0xC0000</code>, and the system BIOS at the top.</li>
</ul>

<h2 id="the-mbr">The MBR</h2>

<p>The MBR has 510 bytes of code and data, plus the signature. It does four things: it sets up the segment registers, clears the screen and prints, reads the loader, and jumps to it.</p>

<h3 id="segments-and-stack">Segments and stack</h3>

<div class="language-nasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">%include "boot.inc"
</span><span class="k">SECTION</span> <span class="nv">MBR</span> <span class="nv">vstart</span><span class="err">=</span><span class="mh">0x7c00</span>
    <span class="nf">jmp</span> <span class="mh">0x0</span><span class="p">:</span><span class="nv">mbr_entry</span>
    <span class="nf">...</span>
<span class="nl">mbr_entry:</span>
    <span class="nf">xor</span> <span class="nb">ax</span><span class="p">,</span><span class="nb">ax</span>
    <span class="nf">mov</span> <span class="nb">ds</span><span class="p">,</span><span class="nb">ax</span>
    <span class="nf">mov</span> <span class="nb">es</span><span class="p">,</span><span class="nb">ax</span>
    <span class="nf">mov</span> <span class="nb">ss</span><span class="p">,</span><span class="nb">ax</span>
    <span class="nf">mov</span> <span class="nb">sp</span><span class="p">,</span><span class="mh">0x7c00</span>
    <span class="nf">mov</span> <span class="nb">fs</span><span class="p">,</span><span class="nb">ax</span>
    <span class="nf">mov</span> <span class="nb">ax</span><span class="p">,</span><span class="mh">0xb800</span>
    <span class="nf">mov</span> <span class="nb">gs</span><span class="p">,</span><span class="nb">ax</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">vstart=0x7c00</code> makes nasm assemble every label as an offset from 0, starting at <code class="language-plaintext highlighter-rouge">0x7C00</code>. That only works if <code class="language-plaintext highlighter-rouge">CS = 0</code>. Most BIOSes jump to <code class="language-plaintext highlighter-rouge">0000:7C00</code>, but some jump to <code class="language-plaintext highlighter-rouge">07C0:0000</code>, which is the same physical byte with a different CS. The far jump at the top makes <code class="language-plaintext highlighter-rouge">CS:IP = 0000:7C48</code> either way. (That is the <code class="language-plaintext highlighter-rouge">jmpf 0x0000:7c48</code> in the debugger output above.) <code class="language-plaintext highlighter-rouge">gs = 0xB800</code> points at the text buffer.</p>

<h3 id="printing-through-the-bios">Printing through the BIOS</h3>

<p>In real mode the BIOS does the work. <code class="language-plaintext highlighter-rouge">int 10h</code> with <code class="language-plaintext highlighter-rouge">AH = 06h</code> scrolls the window up, which clears the screen, and <code class="language-plaintext highlighter-rouge">AH = 02h</code> puts the cursor at the top left. The print routine lives in <code class="language-plaintext highlighter-rouge">util.inc</code>, shared by the MBR and the loader. It reads the cursor (<code class="language-plaintext highlighter-rouge">AH = 03h</code>), then writes the string (<code class="language-plaintext highlighter-rouge">AH = 13h</code>), which handles CR LF itself:</p>

<div class="language-nasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">; cx should hold character count</span>
<span class="c1">; es:bp should point to the string</span>
<span class="nl">print_real_mode:</span>
    <span class="c1">; get cursor position, dh = row, dl = col</span>
    <span class="c1">; it also returns the cursor shape in cx, so save the count</span>
    <span class="nf">push</span> <span class="nb">cx</span>
    <span class="nf">mov</span> <span class="nb">ah</span><span class="p">,</span><span class="mh">0x03</span>
    <span class="nf">mov</span> <span class="nb">bh</span><span class="p">,</span><span class="mh">0x0</span>
    <span class="nf">int</span> <span class="mh">10h</span>
    <span class="nf">pop</span> <span class="nb">cx</span>
    <span class="c1">; Write string</span>
    <span class="nf">mov</span> <span class="nb">ah</span><span class="p">,</span><span class="mh">0x13</span>
    <span class="nf">mov</span> <span class="nb">al</span><span class="p">,</span><span class="mh">0x01</span>
    <span class="nf">mov</span> <span class="nb">bh</span><span class="p">,</span><span class="mh">0x0</span>
    <span class="nf">mov</span> <span class="nb">bl</span><span class="p">,</span><span class="mh">0x07</span>
    <span class="nf">int</span> <span class="mh">10h</span>
    <span class="nf">ret</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">%include</code> pastes the file where the directive is, so <code class="language-plaintext highlighter-rouge">util.inc</code> must go somewhere execution cannot fall into. It also refuses to assemble outside 16-bit code (<code class="language-plaintext highlighter-rouge">%if __?BITS?__ != 16</code> / <code class="language-plaintext highlighter-rouge">%error</code>). Once the loader switches to 32-bit code, BIOS calls are gone for good.</p>

<h3 id="reading-the-loader-with-ata-pio">Reading the loader with ATA PIO</h3>

<p>There is no file system, just sectors. The MBR talks to the primary ATA controller through I/O ports, in 28-bit LBA mode:</p>

<table>
  <thead>
    <tr>
      <th>Port</th>
      <th>Write</th>
      <th>Read</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">0x1F0</code></td>
      <td> </td>
      <td>data, one 16-bit word at a time</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">0x1F2</code></td>
      <td>sector count</td>
      <td> </td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">0x1F3</code>–<code class="language-plaintext highlighter-rouge">0x1F5</code></td>
      <td>LBA bits 0–23</td>
      <td> </td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">0x1F6</code></td>
      <td><code class="language-plaintext highlighter-rouge">0xE0</code> | LBA bits 24–27 (LBA mode, master drive)</td>
      <td> </td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">0x1F7</code></td>
      <td>command: <code class="language-plaintext highlighter-rouge">0x20</code> = read sectors</td>
      <td>status: BSY (bit 7), DF (5), DRQ (3), ERR (0)</td>
    </tr>
  </tbody>
</table>

<p>After the command, the drive raises DRQ once per 512-byte sector, and each sector is 256 <code class="language-plaintext highlighter-rouge">in ax, dx</code> reads from the data port:</p>

<div class="language-nasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="c1">; the drive raises DRQ once per sector, wait before each one</span>
<span class="nl">.next_sector:</span>
    <span class="c1">; reading 0x1f7 returns status</span>
    <span class="c1">; status lags behind the command / last data word by up to 400ns</span>
    <span class="c1">; 4 reads give the drive that time, so stale bits are not trusted</span>
    <span class="nf">mov</span> <span class="nb">dx</span><span class="p">,</span><span class="mh">0x1f7</span>
    <span class="nf">in</span> <span class="nb">al</span><span class="p">,</span><span class="nb">dx</span>
    <span class="nf">in</span> <span class="nb">al</span><span class="p">,</span><span class="nb">dx</span>
    <span class="nf">in</span> <span class="nb">al</span><span class="p">,</span><span class="nb">dx</span>
    <span class="nf">in</span> <span class="nb">al</span><span class="p">,</span><span class="nb">dx</span>
<span class="nl">.not_ready:</span>
    <span class="nf">in</span> <span class="nb">al</span><span class="p">,</span><span class="nb">dx</span>
    <span class="c1">; bit 7 BSY = 1: the other bits are not valid yet</span>
    <span class="nf">test</span> <span class="nb">al</span><span class="p">,</span><span class="mh">0x80</span>
    <span class="nf">jnz</span> <span class="nv">.not_ready</span>
    <span class="c1">; bit 5 DF device fault, bit 0 ERR</span>
    <span class="nf">test</span> <span class="nb">al</span><span class="p">,</span><span class="mh">0x21</span>
    <span class="nf">jnz</span> <span class="nb">di</span><span class="nv">sk_error</span>
    <span class="c1">; bit 3 DRQ = 0: sector data not ready yet</span>
    <span class="nf">test</span> <span class="nb">al</span><span class="p">,</span><span class="mh">0x08</span>
    <span class="nf">jz</span> <span class="nv">.not_ready</span>

    <span class="c1">; data port, 256 words = one 512 byte sector</span>
    <span class="nf">mov</span> <span class="nb">dx</span><span class="p">,</span><span class="mh">0x1f0</span>
    <span class="nf">mov</span> <span class="nb">cx</span><span class="p">,</span><span class="mi">256</span>
<span class="nl">.go_on_read:</span>
    <span class="nf">in</span> <span class="nb">ax</span><span class="p">,</span><span class="nb">dx</span>
    <span class="nf">mov</span> <span class="p">[</span><span class="nb">bx</span><span class="p">],</span><span class="nb">ax</span>
    <span class="nf">add</span> <span class="nb">bx</span><span class="p">,</span><span class="mi">2</span>
    <span class="nf">loop</span> <span class="nv">.go_on_read</span>

    <span class="nf">dec</span> <span class="nb">di</span>
    <span class="nf">jnz</span> <span class="nv">.next_sector</span>
    <span class="nf">ret</span>
</code></pre></div></div>

<p>The first version (<a href="https://github.com/twicy/toy-os/commit/b5e42cb"><code class="language-plaintext highlighter-rouge">b5e42cb</code></a>) did what the book does: it waited for DRQ once, then read <code class="language-plaintext highlighter-rouge">count × 256</code> words in one go. That works in Bochs, but the protocol says to wait before <em>each</em> sector, and the status is only valid about 400 ns after the last access. In the revision (<a href="https://github.com/twicy/toy-os/commit/cbd7402"><code class="language-plaintext highlighter-rouge">cbd7402</code></a>) the MBR waits per sector, checks DF and ERR, and prints “Disk read error” and halts on failure. The MBR has no caller to return an error to, so halting is all it can do.</p>

<p>Then come the jump and the signature:</p>

<div class="language-nasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="nf">mov</span> <span class="nb">eax</span><span class="p">,</span><span class="nv">LOADER_START_SECTOR</span>     <span class="c1">; LBA 2</span>
    <span class="nf">mov</span> <span class="nb">bx</span><span class="p">,</span><span class="nv">LOADER_BASE_ADDR</span>         <span class="c1">; 0x800</span>
    <span class="nf">mov</span> <span class="nb">cx</span><span class="p">,</span><span class="mi">4</span>
    <span class="nf">call</span> <span class="nv">read_disk</span>
    <span class="nf">...</span>
    <span class="nf">jmp</span> <span class="nv">LOADER_BASE_ADDR</span>

<span class="kd">times</span> <span class="mi">510</span><span class="o">-</span><span class="p">(</span><span class="kc">$</span><span class="o">-</span><span class="kc">$$</span><span class="p">)</span> <span class="nv">db</span> <span class="mi">0</span>
<span class="kd">db</span> <span class="mh">0x55</span><span class="p">,</span><span class="mh">0xaa</span>
</code></pre></div></div>

<p><img src="/assets/images/posts/toy-os-1-real-mode/mbr.png" alt="Bochs at the loader's first instruction: &quot;Entering MBR ...&quot; and &quot;Successfully loaded loader ...&quot;" /></p>

<p><em>Stopped at 0x800, the loader’s first instruction. Both lines were printed by the MBR through <code class="language-plaintext highlighter-rouge">int 10h</code>.</em></p>

<p>Two things have to stay in sync between the Makefile and the assembly (both are in my commit notes):</p>

<ol>
  <li>The <code class="language-plaintext highlighter-rouge">seek=2</code> in the Makefile must match <code class="language-plaintext highlighter-rouge">LOADER_START_SECTOR</code> in <code class="language-plaintext highlighter-rouge">boot.inc</code>.</li>
  <li>The <code class="language-plaintext highlighter-rouge">dd</code> <code class="language-plaintext highlighter-rouge">count</code> must match the number of sectors the MBR reads (<code class="language-plaintext highlighter-rouge">mov cx, ...</code>), and both must cover the whole <code class="language-plaintext highlighter-rouge">loader.bin</code>. At first the loader was smaller than one sector, so <code class="language-plaintext highlighter-rouge">count=1</code> and <code class="language-plaintext highlighter-rouge">mov cx, 1</code> were enough. Once it grew protected-mode code (post 3), it no longer fit, so both are now 4 (2 KiB; <code class="language-plaintext highlighter-rouge">loader.bin</code> is 881 bytes).</li>
</ol>

<p>Next, the loader leaves real mode. First it needs a GDT, the subject of <a href="/blogs/os/toy-os-2-segmentation.html">post 2</a>.</p>

<h2 id="references">References</h2>

<ol>
  <li>郑钢, 《操作系统真相还原》, ch. 2 (编写 MBR 主引导记录) and ch. 3 (完善 MBR): the MBR, the real-mode memory layout, and reading the disk through ports.</li>
  <li>navi, <a href="https://segmentfault.com/a/1190000040131294">从零开始写 OS 内核 - BIOS 启动到实模式</a>, segmentfault.</li>
  <li>D. P. Bovet and M. Cesati, <em>Understanding the Linux Kernel</em>, 3rd ed., Appendix A “System Startup”: the BIOS and the boot loader.</li>
  <li>OSDev Wiki, <a href="https://wiki.osdev.org/ATA_PIO_Mode">ATA PIO Mode</a> and <a href="https://wiki.osdev.org/Memory_Map_(x86)">Memory Map (x86)</a>.</li>
  <li>Intel 64 and IA-32 Architectures Software Developer’s Manual, Vol. 3A, “Processor Management and Initialization”: the reset state and the first instruction executed.</li>
</ol>]]></content><author><name>Yuchen Tang</name></author><summary type="html"><![CDATA[The first worklog of toy-os, a 32-bit x86 kernel written from scratch and run in Bochs. It follows the machine from reset, where the CPU fetches its first instruction at 0xFFFFFFF0, through the BIOS, which loads sector 0 of the disk to 0x7C00, into a 512-byte MBR. The MBR clears the screen and prints through BIOS calls, then reads the loader from the disk with ATA PIO port I/O. A map shows what occupies the first MiB of physical memory at that point.]]></summary></entry><entry><title type="html">The Second Mathematical Crisis</title><link href="https://twicy.github.io/blogs/other/second_mathematical_crisis.html" rel="alternate" type="text/html" title="The Second Mathematical Crisis" /><published>2026-06-27T00:00:00-07:00</published><updated>2026-06-27T00:00:00-07:00</updated><id>https://twicy.github.io/blogs/other/second-mathematical-crisis</id><content type="html" xml:base="https://twicy.github.io/blogs/other/second_mathematical_crisis.html"><![CDATA[<h1 id="the-second-mathematical-crisis">The Second Mathematical Crisis</h1>

<h2 id="special-thank-you">Special Thank You</h2>

<p>This blog is an almost identical replay of this educational video series:</p>

<ul>
  <li><a href="https://www.bilibili.com/video/BV1ZnqDBhEvm">【数学史】数学危机（上）——无穷小量的幽灵与分析学的严谨化</a></li>
</ul>

<p>I personally find them extremely interesting, and wish I had watched them earlier when I was in college.</p>

<h2 id="before-introduction">Before Introduction</h2>

<p>Long before the debut of the second mathematical crisis, ancient Greek philosopher Zeno of Elea presented the famous “Zeno’s paradoxes”</p>

<blockquote>
  <p>In a race, the quickest runner can never over­take the slowest, since the pursuer must first reach the point whence the pursued started, so that the slower must always hold a lead.</p>
</blockquote>

<p>To put numbers on the problem, say Achilles (the quickest runner) moves at <strong>10 m/s</strong> and the tortoise (the slowest) moves at <strong>1 m/s</strong>, and the tortoise was <strong>9 m ahead</strong> of Achilles. Zeno is saying, the accumulated time that Achilles spent reaching the point the tortoise was, which can be calculated below</p>

\[t = 0.9 + 0.09 + 0.009 + ...\]

<p>is going to extend forever and never ends.</p>

<blockquote>
  <p>Is $0.999\ldots$ ending somewhere?</p>
</blockquote>

<p>That seemingly easy question has bothered the top minds of the entire globe for more than 150 years.</p>

<h2 id="introduction">Introduction</h2>

<p>The invention of calculus by Newton and Leibniz revolutionized mathematics. It just works!</p>

\[(x^2)^{\prime} = 2x, (x^3)^{\prime} = 3x^2\]

<p>Yet the foundations of calculus were surprisingly shaky. Mathematicians routinely manipulated <strong>infinitesimals</strong>—quantities smaller than any measurable number, yet somehow not zero. This worked extraordinarily well in practice, but philosophers questioned whether the arguments were logically sound.</p>

<p>This debate eventually led to the rigorous foundations of modern analysis during the nineteenth century.</p>

<h2 id="1-george-berkeley-16851753">1. George Berkeley (1685–1753)</h2>

<p><img src="/assets/images/posts/second-mathematical-crisis/the-analyst-book-cover.jpg" alt="The Analyst" /></p>

<blockquote>
  <p><em>“The Analyst; or, A Discourse Addressed to an Infidel Mathematician”</em> (1734)</p>
</blockquote>

<p>George Berkeley is better known as the philosopher of <strong>Idealism</strong>, summarized by the famous Latin phrase</p>

<blockquote>
  <p><strong>Esse est percipi</strong></p>

  <p>(“To be is to be perceived.”)</p>
</blockquote>

<p>Ironically, although remembered primarily as a philosopher and bishop, Berkeley understood contemporary mathematics remarkably well. His famous criticism was aimed not at the results of calculus, but at its logical foundations.</p>

<p>A famous quotation:</p>

<blockquote>
  <p><strong>“And what are these Fluxions? The Velocities of evanescent Increments. And what are these same evanescent Increments? They are neither finite Quantities, nor Quantities infinitely small, nor yet nothing. May we not call them the ghosts of departed quantities?”</strong></p>
</blockquote>

<p>This “ghosts of departed quantities” became one of the most famous criticisms in the history of mathematics.</p>

<h3 id="example-1">Example 1</h3>

<p>Suppose</p>

\[y=x^2.\]

<p>Take two nearby points</p>

\[x,\qquad x+\Delta x.\]

<p>Then</p>

\[\frac{(x+\Delta x)^2-x^2}{(x + \Delta x) - x} = \frac{2x\Delta x + \Delta x^2}{\Delta x} = 2x+\Delta x = 2x.\]

<p>At first,</p>

<ul>
  <li>$\Delta x$ is treated as an ordinary nonzero number so division is allowed.</li>
</ul>

<p>Later,</p>

<ul>
  <li>we let $\Delta x=0$.</li>
</ul>

<p>The simple question Berkeley asked was:</p>

<blockquote>
  <p>Is $\Delta x$ zero or is it not?</p>
</blockquote>

<ul>
  <li>If it is zero, division by it is impossible.</li>
  <li>If it is nonzero, why may we simply discard it?</li>
</ul>

<p>He added that:</p>

<p><img src="/assets/images/posts/second-mathematical-crisis/the-analyst-p93.jpg" alt="The Analyst P93" /></p>

<blockquote>
  <p>Qu. 63. Whether such Mathematicians as cry out against Mysteries, have ever examined their own Principles?</p>
</blockquote>

<blockquote>
  <p>Qu. 64. Whether Mathematicians, who are so delicate in religious Points, are strictly scrupulous in their own Science? Whether they do not submit to Authority, take things upon Trust, and believe Points inconceivable? Whether they have not their Mysteries, and what is more, their Repugnancies and Contradictions?</p>
</blockquote>

<h2 id="2-thomas-bayes-17011761">2. Thomas Bayes (1701–1761)</h2>

<p>Today Bayes is famous for Bayes’ theorem
\(P(A|B) = \frac{P(B|A)P(A)}{P(B)}\)
but during his lifetime he also wrote in defense of Newtonian calculus. He was also elected a Fellow of the Royal Society.</p>

<p><img src="/assets/images/posts/second-mathematical-crisis/coat-of-arms-of-the-royal-society.webp" alt="Fellow of the Royal Society" /></p>

<p>His two principal mathematical AND theological works include</p>

<ul>
  <li><em>An Introduction to the Doctrine of Fluxions</em> (1736)</li>
  <li><em>Divine Benevolence</em> (1731)</li>
</ul>

<p>Bayes argued that infinitesimals should be viewed as <strong>arbitrarily small quantities</strong>, not mysterious objects.</p>

<p>However, Berkeley’s criticism remained:</p>

<blockquote>
  <p>“Arbitrarily small” is still <strong>not zero</strong>.</p>
</blockquote>

<p>Merely making something smaller does not explain why it may suddenly be treated as zero. The following example would be an even better illustration of the issue.</p>

<h3 id="example-2">Example 2</h3>

\[0.99999\ldots =1?\]

<blockquote>
  <p>If “Arbitrarily small” is still <strong>not zero</strong>, they are not equal. No room for ambiguity</p>
</blockquote>

<p>We now know that these two terms are equal in the scope of real analysis. But one of the $\cancel{\text{misleading}}$ interesting proofs I myself was taught in middle school was:</p>

<p>Let
\(S=0.9+0.09+0.009+\cdots.\)</p>

<p>Then
\(10S=9+0.9+0.09+\cdots,\)</p>

<p>so
\(9S=9,\)</p>

<p>thus
\(S=1.\)</p>

<p>But from another perspective, this could also lead us to a ridiculous conclusion: 
\(S=9+90+900+\cdots.\)</p>

<p>Again,</p>

\[10S=90+900+9000\cdots,\]

<p>leading to</p>

\[9S=-9,\]

<p>so</p>

\[S=-1.\]

<p>The key piece here is that many operations valid for finite series do not work for infinite series.</p>

<h2 id="3-jean-le-rond-dalembert">3. Jean le Rond d’Alembert</h2>

<p>Converging series:</p>

\[1 + \frac{1}{2} + \frac{1}{4} + \cdots\]

<p>There are non-converging series:</p>

\[1 + 2 + 3 + 4 + \cdots\]

<p>The first formal way to tell if an infinite series converges is the Ratio Test (d’Alembert’s test), named after Jean le Rond d’Alembert</p>

<p>If
\(L=\lim_{n\to\infty}\left|\frac{a_{n+1}}{a_n}\right|,\)</p>

<p>then</p>

<ul>
  <li>$L&lt;1$: converges</li>
  <li>$L&gt;1$: diverges</li>
  <li>$L=1$: inconclusive</li>
</ul>

<p>Example for $L=1$, the Harmonic series:</p>

\[a_n=\frac1n\]

<p>Since</p>

\[L=\lim_{n\to\infty}\left|\frac{a_{n+1}}{a_n}\right| = 1\]

<p>the Ratio Test cannot determine convergence for</p>

\[\sum_{n=1}^{\infty}\frac{1}{n} = 1 + \frac{1}{2} + \frac{1}{3} + \cdots\]

<p>At this time, there was still no formal definition of infinitesimals and limits.</p>

<p>Another fun fact about d’Alembert is, he worked with Denis Diderot in editing <em>Encyclopédie</em>. It was the first encyclopedia to include contributions from many named contributors and the first to describe the mechanical arts.</p>

<p><img src="/assets/images/posts/second-mathematical-crisis/encyclopedie-de-d-alembert-et-diderot-premiere-page-enc-1-na5.jpg" alt="Encyclopédie" /></p>

<p>Instead of trying to define infinitesimals, d’Alembert proposed defining calculus using <strong>limits</strong>.</p>

<p>His article <strong>“Limite”</strong> in the <em>Encyclopédie</em> (edited by Diderot and d’Alembert, Le chevalier de Jaucourt, and numerous other contributors) famously states that infinitesimals are unnecessary and should be replaced by limiting processes.</p>

<p>A representative quotation:</p>

<blockquote>
  <p><strong>“One does not pass to the limit; one approaches it as closely as one wishes.”</strong></p>
</blockquote>

<h2 id="4-euler-and-lagrange">4. Euler and Lagrange</h2>

<p>On the other side, there were people who did not care about the definition of infinitesimals at all.</p>

<p>Euler was unconcerned by philosophical objections.</p>

<blockquote>
  <p><strong>“The objection that the analysis of the infinitely small neglects mathematical rigor disappears entirely, since nothing is rejected except what is absolutely nothing.”</strong></p>
</blockquote>

<p>Unlike Berkeley, Euler was primarily interested in solving mathematical and physical problems rather than debating philosophical foundations. His work spanned mechanics, astronomy, optics, fluid dynamics, number theory, and engineering.</p>

<p><img src="/assets/images/posts/second-mathematical-crisis/bridges-of-konigsberg.png" alt="Bridges of Königsberg" /></p>

<p>To Euler, the remarkable success of infinitesimal methods in producing correct results was itself compelling evidence of their validity. This pragmatic attitude explains why he regarded Berkeley’s criticism as a philosophical concern rather than a mathematical obstacle.</p>

<p><img src="/assets/images/posts/second-mathematical-crisis/leonhard-euler-jakob-emanuel-handmann-kunstmuseum-basel.jpg" alt="Leonhard Euler" /></p>

<blockquote>
  <p>By the way, Euler was a true virtuoso. He made groundbreaking contributions across mathematics, physics, astronomy, and engineering. Even after losing the sight in one eye and later becoming almost completely blind, he continued to produce an extraordinary volume of influential work, <strong>demonstrating remarkable perseverance alongside his unparalleled mathematical genius</strong>.</p>
</blockquote>

<p>Joseph-Louis Lagrange attempted to eliminate infinitesimals entirely by rebuilding calculus upon <strong>Taylor’s theorem</strong>. He believed that differentiation should not rely on mysterious infinitesimal quantities, but instead on ordinary algebra and infinite polynomial expansions. In his <em>Théorie des fonctions analytiques</em> (1797), Lagrange argued that every sufficiently smooth function should be representable by its Taylor series, making infinite series—not infinitesimals—the true foundation of calculus.</p>

<h3 id="taylor-series">Taylor Series</h3>

<p>Brook Taylor was a younger contemporary of Isaac Newton and, like Newton, a Fellow of the Royal Society. In 1712, Taylor served on the Royal Society committee that examined the famous priority dispute between Isaac Newton and Gottfried Wilhelm Leibniz over the invention of calculus. Although Taylor is remembered today for Taylor’s theorem, his work was originally motivated by problems in finite differences and interpolation.</p>

<p><img src="/assets/images/posts/second-mathematical-crisis/isaac-newton-westminster.jpg" alt="Isaac Newton" /></p>

<p>One of Newton’s many remarkable contributions was his generalization of the classical binomial theorem. Instead of restricting the exponent to positive integers, Newton showed that the expansion also works for fractional and negative exponents (later mathematicians extended it to arbitrary real and even complex exponents), provided the resulting infinite series converges.</p>

<p>\((1+x)^k = \sum_{n=0}^{\infty} \binom{k}{n}x^n = 1 + kx + \frac{k(k-1)}{2!}x^2 + \frac{k(k-1)(k-2)}{3!}x^3 + \cdots\)
for any $k$ and $|x| &lt; 1$, for convergence</p>

<p>At some point he was sure that all functions (not just binomial) should be able to expand as a polynomial series</p>

\[f(x) = a_{0} + a_{1}x + a_{2}x^2 + \cdots\]

<p>Taylor’s key contribution was determining the coefficients of this infinite polynomial.</p>

<p>Suppose we wish to approximate a function around the point $x=x_0$.</p>

<p>For a zeroth-order approximation,</p>

\[f(x)\approx f(x_0).\]

<p>Including the first-order term gives</p>

\[f(x)\approx
f(x_0)
+
f'(x_0)(x-x_0).\]

<p>Including the second-order term,</p>

\[f(x)\approx
f(x_0)
+
f'(x_0)(x-x_0)
+
\frac{f''(x_0)}{2!}(x-x_0)^2.\]

<p>The factor $\frac{1}{2!}$ appears because</p>

\[\frac{d^2}{dx^2}(x-x_0)^2=2!,\]

<p>so dividing by $2!$ ensures that the second derivative of the polynomial equals $f’‘(x_0)$.</p>

<p>Continuing this process, we arrive at Taylor’s theorem</p>

\[f(x)=
\sum_{n=0}^{\infty}
\frac{f^{(n)}(x_0)}{n!}(x-x_0)^n.\]

<p>Colin Maclaurin specialized Taylor’s theorem at $x = 0$:</p>

\[f(x) = \sum_{n=0}^{\infty}
\frac{f^{(n)}(0)}{n!}x^n.\]

<h3 id="example-3">Example 3</h3>

\[e^x
=
1+x+\frac{x^2}{2!}+\frac{x^3}{3!}+\cdots\]

<p>Let $x=2$</p>

\[7.389056\cdots
=
e^2
\approx
1
+
2
+
\frac{2^2}{2!}
+
\frac{2^3}{3!}
+
\frac{2^4}{4!}
=
7.000000\cdots\]

<p>Adding more terms rapidly improves the approximation.</p>

<h3 id="example-4">Example 4</h3>

\[\sin x
=
x
-
\frac{x^3}{3!}
+
\frac{x^5}{5!}
-
\frac{x^7}{7!}
+\cdots\]

<p>Let $x=2$</p>

\[0.909297\cdots
=
\sin2
\approx
2
-
\frac{2^3}{3!}
+
\frac{2^5}{5!}
=
0.933333\cdots\]

<p>Again, including more terms produces increasingly accurate approximations.</p>

<h3 id="the-problem-of-taylor-series">The problem of Taylor Series</h3>

<p>Using Taylor Expansion, instead of using $\approx$ or putting a $=$ here with a guilty conscience, we can confidently place an $=$ between $0.99999\ldots$ and $1$, because</p>

\[f(x) = \frac{1}{1 - x}, f^{(1)}(x) = \frac{1}{(1 - x)^2}, f^{(2)}(x) = \frac{2}{(1 - x)^3}, \cdots, f^{(n)}(x) = \frac{n!}{(1 - x)^{n+1}}\]

<p>Therefore $f^{(n)}(0) = n!$</p>

\[\frac{1}{1 - x} = f(x) = \sum_{n=0}^{\infty}
\frac{f^{(n)}(0)}{n!}x^n = \sum_{n=0}^{\infty}x^n.\]

<p>Substitute $x = 0.1$,</p>

\[\frac{1}{1-0.1} = \sum_{n=0}^{\infty}0.1^n\]

\[10 = \sum_{n=0}^{\infty}9\times(0.1)^{n}\]

\[1 = \sum_{n=1}^{\infty}9\times(0.1)^{n}\]

<p>This method totally bypassed the definition of infinitesimals, but comes with a deadly imperfection. Taylor series do <strong>not</strong> always work well with some functions. If instead of $0.1$, but $x = 10$ was substituted in the above equation:</p>

\[-\frac{1}{9}=\frac{1}{1 - 10} = \sum_{n=0}^{\infty}
\frac{f^{(n)}(0)}{n!}10^n = \sum_{n=0}^{\infty}10^n = \ldots11111.\]

<p>In another example:</p>

\[\sqrt{1 + x^2} = 1 + \frac{1}{2}x^2 - \frac{1}{8}x^4 + \frac{1}{16}x^6 -\frac{5}{128}x^8+ \frac{7}{256}x^{10} \cdots\]

<p>Substitute with $x = 2$</p>

\[\sqrt{1 + 2^2} ?\approx 23\]

<p>However, if we substitute with $x = \frac{1}{2}$, it works again</p>

\[1.118033989\cdots =\sqrt{1 + (\frac{1}{2})^2} \approx 1.118038177\cdots\]

<p>These two failures only mean that the series diverges when $x$ is too far from $x_0$. The real blow to Lagrange’s program came from Cauchy in 1823: the function</p>

\[f(x) =
\begin{cases}
e^{-1/x^2}, &amp; x \neq 0 \\
0, &amp; x = 0
\end{cases}\]

<p>is smooth everywhere and $f^{(n)}(0) = 0$ for every $n$, so its Taylor series at $0$ is identically $0$, yet $f(x) &gt; 0$ for every $x \neq 0$. The series converges everywhere, just not to $f$.</p>

<p>So Lagrange’s hope that every sufficiently smooth function could be represented by its Taylor series died. What survives is Taylor’s theorem with a remainder term, which Lagrange himself gave in the same 1797 book: for some $\xi$ between $x_0$ and $x$,</p>

\[f(x) = \sum_{k=0}^{n}
\frac{f^{(k)}(x_0)}{k!}(x - x_0)^k + R_{n}(x),
\qquad
R_n(x) = \frac{f^{(n+1)}(\xi)}{(n+1)!}(x - x_0)^{n+1}.\]

<h2 id="5-augustin-louis-cauchy">5. Augustin-Louis Cauchy</h2>

<p>If Berkeley exposed the logical weaknesses of infinitesimals, and Lagrange attempted to replace them with Taylor series, it was Augustin-Louis Cauchy who began rebuilding calculus on rigorous foundations.</p>

<p>Cauchy entered the École Polytechnique only a few years after Joseph-Louis Lagrange had taught there. His landmark textbook, Cours d’analyse (1821), marks the beginning of modern rigorous analysis.</p>

<blockquote>
  <p>“When the successive numerical values of the same variable decrease indefinitely, so as to fall below any given number, this variable becomes what is called an infinitely small quantity, or an infinitesimal.”</p>
</blockquote>

<p>This is very close to the $\varepsilon-\delta$ language we see today.</p>

<p>Also, he was the first to prove Taylor’s theorem rigorously, establishing his well-known form of the remainder.</p>

<h2 id="6-karl-weierstrass">6. Karl Weierstrass</h2>

<p>Karl Weierstrass has a famous function named after him, Weierstrass Function.</p>

<p>\(f(x) = \sum_{n = 0}^{\infty}a^n\cos(b^n \pi x)\)
where $0&lt;a&lt;1$, $b$ is a positive odd integer, and $ab &gt; 1 +\frac{3}{2}\pi$.</p>

<p><img src="/assets/images/posts/second-mathematical-crisis/weierstrass-function.jpg" alt="Weierstrass Function" /></p>

<p>It is famous for being continuous everywhere but differentiable nowhere. People tend to think that continuous functions should be smooth after zooming in, but Weierstrass function proves them wrong.</p>

<p>And finally at this moment, as we take off from Leibniz defining continuity as “natura non facit saltus” or “nature makes no jump”, we finally arrive at the modern <strong>$\varepsilon-\delta$</strong> definition of a limit, formalized by Weierstrass (building on Cauchy’s work), is as follows.</p>

<p>Let $f$ be a function defined on an open interval around $a$ (possibly excluding $a$ itself). We say</p>

\[\lim_{x\to a}f(x)=L\]

<p>if and only if</p>

\[\boxed{
\forall\,\varepsilon&gt;0,\;
\exists\,\delta&gt;0,\;
\text{such that whenever }
0&lt;|x-a|&lt;\delta,
\text{ we have }
|f(x)-L|&lt;\varepsilon.
}\]

<h2 id="references">References</h2>

<ol>
  <li>Berkeley, G. (1734). <em>The Analyst; or, A Discourse Addressed to an Infidel Mathematician.</em></li>
  <li>Bayes, T. (1736). <em>An Introduction to the Doctrine of Fluxions.</em></li>
  <li>Bayes, T. (1731). <em>Divine Benevolence; or, an Attempt to Prove that the Principal End of the Divine Providence and Government is the Happiness of His Creatures.</em></li>
  <li>d’Alembert, J. le Rond. (1751–1772). “Limite,” in <em>Encyclopédie</em>, edited by Denis Diderot and Jean le Rond d’Alembert.</li>
  <li>Euler, L. (1755). <em>Institutiones Calculi Differentialis.</em></li>
  <li>Lagrange, J.-L. (1797). <em>Théorie des fonctions analytiques.</em></li>
  <li>Cauchy, A.-L. (1821). <em>Cours d’analyse de l’École Royale Polytechnique.</em></li>
  <li>Weierstrass, K. (1872). “Über continuierliche Funktionen eines reellen Arguments, die für keinen Werth des letzteren einen bestimmten Differentialquotienten besitzen.”</li>
  <li>Grabiner, J. V. (1981). <em>The Origins of Cauchy’s Rigorous Calculus.</em></li>
  <li>Boyer, C. B., &amp; Merzbach, U. C. (2011). <em>A History of Mathematics</em> (3rd ed.).</li>
  <li>Kline, M. (1972). <em>Mathematical Thought from Ancient to Modern Times.</em></li>
  <li>Stillwell, J. (2010). <em>Mathematics and Its History</em> (3rd ed.).</li>
</ol>]]></content><author><name>Yuchen Tang</name></author><summary type="html"><![CDATA[How calculus got rigorous foundations. Starting from Zeno's paradox and whether 0.999… equals 1, the post follows Berkeley's attack on infinitesimals as "ghosts of departed quantities", the responses of Bayes, d'Alembert and Euler, and Lagrange's attempt to rebuild calculus on Taylor series, which failed because a smooth function need not equal its Taylor series. It ends with Cauchy, Weierstrass and the ε–δ definition of a limit. Retold from a Bilibili video series.]]></summary></entry><entry><title type="html">SGEMM CUDA kernel Part 2</title><link href="https://twicy.github.io/blogs/gpu/sgemm_p2.html" rel="alternate" type="text/html" title="SGEMM CUDA kernel Part 2" /><published>2026-03-15T00:00:00-07:00</published><updated>2026-03-15T00:00:00-07:00</updated><id>https://twicy.github.io/blogs/gpu/sgemm-p2</id><content type="html" xml:base="https://twicy.github.io/blogs/gpu/sgemm_p2.html"><![CDATA[<h1 id="sgemm-cuda-kernel-second-half">SGEMM CUDA kernel (Second Half)</h1>

<p>Following the previous blog, this is somewhat of a separate thread, which records my attempt to optimize the SGEMM kernel to the extreme.</p>

<p>I know there are many excellent engineers who have already achieved this, but to be honest, their code is often very hard to understand.</p>

<p>I will update this blog occasionally, as GPU is really not the focus of my work, but it still fascinates me a lot.</p>

<p>Again, source code can be found <a href="https://github.com/twicy/CUDA">here</a>.</p>

<h2 id="table-of-contents">Table of Contents</h2>

<ul>
  <li><a href="#test-environment-setup">Test Environment Setup</a></li>
  <li><a href="#part-8-warp-tile-v5">Part 8: Warp Tile (v5)</a>
    <ul>
      <li><a href="#nsight-analysis-of-the-previous-kernel-sgemm_v4">Nsight Analysis of the previous kernel <code class="language-plaintext highlighter-rouge">sgemm_v4</code></a></li>
      <li><a href="#better-memory-access-management-with-warp-tile">Better memory access management with warp tile</a></li>
      <li><a href="#lds128-shared-memory-transactions"><code class="language-plaintext highlighter-rouge">LDS.128</code> Shared Memory Transactions</a></li>
    </ul>
  </li>
  <li><a href="#part-9-coalesced-writeback-using-cs-v6">Part 9: Coalesced Writeback using Cs (v6)</a></li>
</ul>

<h2 id="test-environment-setup">Test Environment Setup</h2>

<p>After finishing grad school, I have to switch my test platform from the high end <code class="language-plaintext highlighter-rouge">AD102GL [RTX 6000 Ada Generation]</code> workstation to the cheapest Google VM I can get with <code class="language-plaintext highlighter-rouge">Nvidia L4</code> GPU</p>

<p>Google has a <a href="https://docs.cloud.google.com/compute/docs/gpus/install-drivers-gpu">really nice script</a> that automates the installation of GPU drivers and CUDA tools.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-L</span> https://storage.googleapis.com/compute-gpu-installation-us/installer/latest/cuda_installer.pyz <span class="nt">--output</span> cuda_installer.pyz
<span class="nb">sudo </span>python3 cuda_installer.pyz install_driver
reboot
<span class="nb">sudo </span>python3 cuda_installer.pyz install_cuda
reboot
</code></pre></div></div>

<h2 id="part-8-warp-tile-v5">Part 8: Warp Tile (v5)</h2>

<h3 id="nsight-analysis-of-the-previous-kernel-sgemm_v4">Nsight Analysis of the previous kernel <code class="language-plaintext highlighter-rouge">sgemm_v4</code></h3>

<p>Using Nsight Compute, we can profile and analyze memory behavior.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo</span> <span class="si">$(</span>which ncu<span class="si">)</span> <span class="nt">--set</span> full <span class="nt">-o</span> profile_output <span class="nt">-f</span> ./sgemm_v4 4096 4096 4096
ncu <span class="nt">-i</span> profile_output.ncu-rep <span class="o">&gt;</span> sgemm_v4.txt
</code></pre></div></div>

<p>Below is part of the profile result of <code class="language-plaintext highlighter-rouge">sgemm_v4</code>.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    Section: Memory Workload Analysis Tables
    OPT   Est. Speedup: 58.32%                                                                                          
          The memory access pattern for global stores to L1TEX might not be optimal. On average, only 4.0 of the 32     
          bytes transmitted per sector are utilized by each thread. This could possibly be caused by a stride between   
          threads. Check the Source Counters section for uncoalesced global stores.                                     
    ----- --------------------------------------------------------------------------------------------------------------
    OPT   Est. Speedup: 27.27%                                                                                          
          The memory access pattern for shared loads might not be optimal and causes on average a 5.0 - way bank        
          conflict across all 134217728 shared load requests.This results in 268435456 bank conflicts,  which           
          represent 40.00% of the overall 671091703 wavefronts for shared loads. Check the Source Counters section for  
          uncoalesced shared loads.                                                                                     
    ----- --------------------------------------------------------------------------------------------------------------
    OPT   Est. Speedup: 22.72%                                                                                          
          The memory access pattern for shared stores might not be optimal and causes on average a 2.4 - way bank       
          conflict across all 20971520 shared store requests.This results in 16777216 bank conflicts,  which represent  
          33.33% of the overall 50331648 wavefronts for shared stores. Check the Source Counters section for            
          uncoalesced shared stores.              
</code></pre></div></div>

<p>There are two major problems:</p>

<ul>
  <li><strong>Memory access</strong> to global memory is <strong>not coalesced</strong>, wasting bandwidth. Therefore, threads within a warp should write to contiguous global memory addresses.</li>
  <li><strong>Shared memory load/store conflicts</strong> =&gt; mostly when loading and storing back to global memory</li>
</ul>

<h3 id="better-memory-access-management-with-warp-tile">Better memory access management with warp tile</h3>

<blockquote>
  <p>Credit: The following illustrations comes mostly from <a href="https://salykova.github.io/sgemm-gpu">Advanced Matrix Multiplication Optimization on NVIDIA GPUs</a>, the same hierarchy was mentioned in this paper <a href="https://dl.acm.org/doi/10.1145/3372419">Strassen’s Algorithm Reloaded on GPUs</a>.</p>
</blockquote>

<p><img src="/assets/images/posts/sgemm-p2/sgemm-v5-ast.jpg" alt="Transposed As" /></p>

<p>In <code class="language-plaintext highlighter-rouge">sgemm_v4</code>, each thread reads off a $1 \times 4$ floats (<code class="language-plaintext highlighter-rouge">float4</code>) from global memory and then stores a $4 \times 1$ to <code class="language-plaintext highlighter-rouge">AsT</code> shared memory:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">float4</span> <span class="n">a4</span> <span class="o">=</span> <span class="n">FLOAT4</span><span class="p">(</span><span class="n">A</span><span class="p">[</span><span class="n">OFFSET</span><span class="p">(</span><span class="n">a_row</span><span class="p">,</span> <span class="n">a_col</span><span class="p">,</span> <span class="n">M</span><span class="p">,</span> <span class="n">K</span><span class="p">)]);</span>
<span class="n">AsT</span><span class="p">[</span><span class="n">shmem_col</span><span class="p">][</span><span class="n">shmem_row</span><span class="p">]</span> <span class="o">=</span> <span class="n">a4</span><span class="p">.</span><span class="n">x</span><span class="p">;</span>
<span class="n">AsT</span><span class="p">[</span><span class="n">shmem_col</span> <span class="o">+</span> <span class="mi">1</span><span class="p">][</span><span class="n">shmem_row</span><span class="p">]</span> <span class="o">=</span> <span class="n">a4</span><span class="p">.</span><span class="n">y</span><span class="p">;</span>
<span class="n">AsT</span><span class="p">[</span><span class="n">shmem_col</span> <span class="o">+</span> <span class="mi">2</span><span class="p">][</span><span class="n">shmem_row</span><span class="p">]</span> <span class="o">=</span> <span class="n">a4</span><span class="p">.</span><span class="n">z</span><span class="p">;</span>
<span class="n">AsT</span><span class="p">[</span><span class="n">shmem_col</span> <span class="o">+</span> <span class="mi">3</span><span class="p">][</span><span class="n">shmem_row</span><span class="p">]</span> <span class="o">=</span> <span class="n">a4</span><span class="p">.</span><span class="n">w</span><span class="p">;</span>
</code></pre></div></div>

<p>However in <code class="language-plaintext highlighter-rouge">sgemm_v5</code>, each thread reads off a $4 \times 1$ but stores $1 \times 4$ floats (<code class="language-plaintext highlighter-rouge">float4</code>) to shared memory:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">auto</span> <span class="n">ld_g2r_a</span> <span class="o">=</span> <span class="p">[</span><span class="o">&amp;</span><span class="p">]</span> <span class="p">(</span><span class="kt">int</span> <span class="n">ph</span><span class="p">)</span> <span class="p">{</span>
      <span class="kt">int</span> <span class="n">col</span> <span class="o">=</span> <span class="n">ph</span> <span class="o">*</span> <span class="mi">8</span> <span class="o">+</span> <span class="n">A_thread_col</span><span class="p">;</span>
      <span class="n">A_ldg_reg</span><span class="p">.</span><span class="n">x</span> <span class="o">=</span> <span class="p">(</span><span class="n">A_row</span> <span class="o">&lt;</span> <span class="n">M</span> <span class="o">&amp;&amp;</span> <span class="n">col</span> <span class="o">&lt;</span> <span class="n">K</span><span class="p">)</span> <span class="o">?</span> <span class="n">A</span><span class="p">[</span><span class="n">OFFSET</span><span class="p">(</span><span class="n">A_row</span><span class="p">,</span> <span class="n">col</span><span class="p">,</span> <span class="n">K</span><span class="p">)]</span> <span class="o">:</span> <span class="mf">0.0</span><span class="n">f</span><span class="p">;</span>
      <span class="n">A_ldg_reg</span><span class="p">.</span><span class="n">y</span> <span class="o">=</span> <span class="p">(</span><span class="n">A_row</span> <span class="o">+</span> <span class="mi">1</span> <span class="o">&lt;</span> <span class="n">M</span> <span class="o">&amp;&amp;</span> <span class="n">col</span> <span class="o">&lt;</span> <span class="n">K</span><span class="p">)</span> <span class="o">?</span> <span class="n">A</span><span class="p">[</span><span class="n">OFFSET</span><span class="p">(</span><span class="n">A_row</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span> <span class="n">col</span><span class="p">,</span> <span class="n">K</span><span class="p">)]</span> <span class="o">:</span> <span class="mf">0.0</span><span class="n">f</span><span class="p">;</span>
      <span class="n">A_ldg_reg</span><span class="p">.</span><span class="n">z</span> <span class="o">=</span> <span class="p">(</span><span class="n">A_row</span> <span class="o">+</span> <span class="mi">2</span> <span class="o">&lt;</span> <span class="n">M</span> <span class="o">&amp;&amp;</span> <span class="n">col</span> <span class="o">&lt;</span> <span class="n">K</span><span class="p">)</span> <span class="o">?</span> <span class="n">A</span><span class="p">[</span><span class="n">OFFSET</span><span class="p">(</span><span class="n">A_row</span> <span class="o">+</span> <span class="mi">2</span><span class="p">,</span> <span class="n">col</span><span class="p">,</span> <span class="n">K</span><span class="p">)]</span> <span class="o">:</span> <span class="mf">0.0</span><span class="n">f</span><span class="p">;</span>
      <span class="n">A_ldg_reg</span><span class="p">.</span><span class="n">w</span> <span class="o">=</span> <span class="p">(</span><span class="n">A_row</span> <span class="o">+</span> <span class="mi">3</span> <span class="o">&lt;</span> <span class="n">M</span> <span class="o">&amp;&amp;</span> <span class="n">col</span> <span class="o">&lt;</span> <span class="n">K</span><span class="p">)</span> <span class="o">?</span> <span class="n">A</span><span class="p">[</span><span class="n">OFFSET</span><span class="p">(</span><span class="n">A_row</span> <span class="o">+</span> <span class="mi">3</span><span class="p">,</span> <span class="n">col</span><span class="p">,</span> <span class="n">K</span><span class="p">)]</span> <span class="o">:</span> <span class="mf">0.0</span><span class="n">f</span><span class="p">;</span>
<span class="p">};</span>

<span class="k">auto</span> <span class="n">st_r2s_a</span> <span class="o">=</span> <span class="p">[</span><span class="o">&amp;</span><span class="p">]</span> <span class="p">()</span> <span class="p">{</span>
      <span class="n">FLOAT4</span><span class="p">(</span><span class="n">AsT</span><span class="p">[</span><span class="n">A_thread_col</span><span class="p">][</span><span class="n">A_thread_row</span><span class="p">])</span> <span class="o">=</span> <span class="n">A_ldg_reg</span><span class="p">;</span>
<span class="p">};</span>
</code></pre></div></div>

<p>I think both approaches need 128 memory transactions (32-byte sectors) to load $128 \times 8$ floats from the global memory, because each row of the tile is only 8 floats (32 bytes) wide, a quarter of a 128-byte cache line.</p>

<p>However, when it comes to storing into the shared memory, <code class="language-plaintext highlighter-rouge">sgemm_v5</code> uses vectorized store to better utilize the shared memory bandwidth, while <code class="language-plaintext highlighter-rouge">sgemm_v4</code> does not.</p>

<p>Bank conflict-wise (at this stage, before computation), both schemes are clean. Specifically, <code class="language-plaintext highlighter-rouge">sgemm_v5</code> intentionally inserts 4 columns to “shift” away the conflicts.</p>

<p><img src="/assets/images/posts/sgemm-p2/sgemm-v5-ast-bank-conflict.jpg" alt="Reduce Bank Conflict of As" /></p>

<p><code class="language-plaintext highlighter-rouge">sgemm_v5</code> also has a very clever memory access arrangement for B. For the most part, all memory load from global memory are obviously coalesced. When storing to shared memory, there are also no bank conflicts.</p>

<p><img src="/assets/images/posts/sgemm-p2/sgemm-v5-bs.jpg" alt="Stay as-is Bs" /></p>

<h3 id="lds128-shared-memory-transactions"><code class="language-plaintext highlighter-rouge">LDS.128</code> Shared Memory Transactions</h3>

<blockquote>
  <p>Credit: The following section relies heavily on this Zhihu Blog <a href="https://zhuanlan.zhihu.com/p/690052715">搞懂 CUDA Shared Memory 上的 bank conflicts 和向量化指令（LDS.128 / float4）的访存特点</a> and <a href="https://github.com/Yinghan-Li/YHs_Sample/blob/master/cuda/gemm/sgemm.cu">YHs_Sample sgemm</a>.</p>
</blockquote>

<p>The big picture of warp tile looks like this:</p>

<p><img src="/assets/images/posts/sgemm-p2/warp-tile.jpg" alt="Warp Tile Big Picture" /></p>

<p>But unlike what one would expect, how threads within a warp are mapped to shared memory is not simple and trivial. <code class="language-plaintext highlighter-rouge">sgemm_v5</code> uses the following mapping.</p>

<p><img src="/assets/images/posts/sgemm-p2/zigzag-warp.jpg" alt="Zig Zag mapping" /></p>

<p>This is because of the presence of a broadcast limitation if we simply put neighboring threads next to each other.</p>

<p><code class="language-plaintext highlighter-rouge">LDS.32</code>, <code class="language-plaintext highlighter-rouge">LDS.64</code> and <code class="language-plaintext highlighter-rouge">LDS.128</code> are the SASS (machine code) instructions used when threads load 32 bits (one float), 64 bits and 128 bits (four floats or <code class="language-plaintext highlighter-rouge">float4</code>) from shared memory. Their PTX counterparts are <code class="language-plaintext highlighter-rouge">ld.shared.f32</code>, <code class="language-plaintext highlighter-rouge">ld.shared.v2.f32</code> and <code class="language-plaintext highlighter-rouge">ld.shared.v4.f32</code>.</p>

<h4 id="lds32"><code class="language-plaintext highlighter-rouge">LDS.32</code></h4>

<ul>
  <li>When multiple threads access <strong>the same word</strong> (32-bit) (thus of course within the same bank), the broadcast mechanism is triggered. That word is simultaneously sent to the corresponding threads.</li>
</ul>

<blockquote>
  <p>If all threads within a warp reads the same word, only 1 memory transaction</p>
</blockquote>

<ul>
  <li>When multiple threads access <strong>different words within the same bank</strong>, a bank conflict occurs. The request is then split into multiple memory transactions, which are issued serially for execution.</li>
</ul>

<blockquote>
  <p>If all threads within a warp reads totally different words in the same bank, 32 memory transactions needed.</p>
</blockquote>

<h4 id="lds64"><code class="language-plaintext highlighter-rouge">LDS.64</code></h4>

<p>Each thread requesting 2 words, thus 16 threads together saturate the 128-byte transaction size. The hardware then splits one warp (32 threads) into 2 half-warps (16 threads), leading to two memory transactions.</p>

<p>Only <strong>under certain scenarios</strong> will these <strong>two memory transactions be merged into one</strong>. For all active threads in a warp (where active means the thread has a memory access request):</p>
<ul>
  <li>For thread i, thread i xor 1 is either inactive or accesses the same memory address.</li>
</ul>

<blockquote>
  <p>(i.e., T0 == T1, T2 == T3, T4 == T5, T6 == T7, etc.)</p>
</blockquote>

<ul>
  <li>For thread i, thread i xor 2 is either inactive or accesses the same memory address.</li>
</ul>

<blockquote>
  <p>(i.e., T0 == T2, T1 == T3, T4 == T6, T5 == T7, etc.)</p>
</blockquote>

<p>Example 1: 32 threads accessing 32 consecutive <code class="language-plaintext highlighter-rouge">uint2</code></p>

<ul>
  <li>Does not meet the above requirements</li>
  <li>First half-warp will access the first 128 bytes, causing one memory transaction</li>
  <li>Second half-warp will trigger another</li>
</ul>

<p>Example 2:</p>

<p><img src="/assets/images/posts/sgemm-p2/lds64-mem-transaction.jpg" alt="LDS.64 Memory Transaction Merge Example" /></p>

<ul>
  <li>T0 == T1, T2 == T3, …</li>
  <li>One merged memory transaction</li>
</ul>

<h4 id="lds128"><code class="language-plaintext highlighter-rouge">LDS.128</code></h4>

<p>Each thread requesting 4 words, thus 8 threads alone will saturate the 128 bytes of data. The hardware then splits one warp (32 threads) into 2 half-warps (16 threads), and each half-warp into 2 <strong>quarter-warps</strong> (8 threads), <strong>leading to up to 4 memory transactions</strong>.</p>

<p>Only when the same rule applies (as the above mentioned <code class="language-plaintext highlighter-rouge">LDS.64</code>) will the group of quarter-warp memory transactions be merged into one “half-warp” memory transactions, however these “half-warp” memory transactions will not further be merged into 1, meaning there will <strong>at least be 2 memory transactions</strong>.</p>

<p>Example 1: 32 threads accessing 32 consecutive <code class="language-plaintext highlighter-rouge">uint4</code></p>

<ul>
  <li>Does not meet the requirements</li>
  <li>Total of 4 quarter-warp memory transactions</li>
</ul>

<p>Example 2: Thread 15 and Thread 16 access the same <code class="language-plaintext highlighter-rouge">uint4</code></p>

<p><img src="/assets/images/posts/sgemm-p2/lds128-mem-transaction-ex1.jpg" alt="Thread 15 and Thread 16 access the same `uint4`" /></p>

<ul>
  <li>Does not meet either requirement</li>
  <li>2 quarter-warp memory transactions</li>
</ul>

<p>Example 3: Thread 0 and Thread 15 access the same <code class="language-plaintext highlighter-rouge">uint4</code></p>

<p><img src="/assets/images/posts/sgemm-p2/lds128-mem-transaction-ex2.jpg" alt="Thread 0 and Thread 15 access the same `uint4`" /></p>

<ul>
  <li>Meet both requirements</li>
  <li>1 merged half-warp memory transaction</li>
</ul>

<p>Example 4: Irregular scenario</p>

<p><img src="/assets/images/posts/sgemm-p2/lds128-mem-transaction-ex3.jpg" alt="Irregular scenario" /></p>

<ul>
  <li>First half-warp meets the first requirement; Second meets the second</li>
  <li>The requirement needs to be <strong>coherent</strong> across the entire warp, therefore the warp meets neither the first requirement nor the second</li>
  <li>4 memory quarter-warp transactions</li>
</ul>

<h4 id="how-it-associates-with-sgemm-kernel-here">How it associates with SGEMM kernel here</h4>

<p>When threads are reading segments from shared memory, they are progressively reading <code class="language-plaintext highlighter-rouge">float4</code>s, and that is the “32 threads accessing 32 consecutive <code class="language-plaintext highlighter-rouge">uint4</code>” mentioned in the <a href="#lds128">above section</a></p>

<p>From the <a href="#lds128-shared-memory-transactions">previous sections</a>, we get to see that we should either put</p>

<ul>
  <li>(t0, t1), (t2, t3), … reading the same word</li>
  <li>(t0, t2), (t1, t3), … reading the same word</li>
</ul>

<p>And that is exactly what we observed from <code class="language-plaintext highlighter-rouge">sgemm_v5</code>:</p>

<ul>
  <li>When reading col frag from As (row frag from AsT), T0 = T2, T1 = T3, …</li>
  <li>When reading row frag from Bs, T0 = T1, T2 = T3, …</li>
</ul>

<p><strong>This Z shaped mapping will make quarter-warp memory transactions merged into half-warp transactions</strong></p>

<h2 id="part-9-coalesced-writeback-using-cs-v6">Part 9: Coalesced Writeback using Cs (v6)</h2>

<p>In <code class="language-plaintext highlighter-rouge">sgemm_v5</code>, I “shuffled” threads within the warp in a zig-zag fashion, as mentioned above, to reduce the <code class="language-plaintext highlighter-rouge">LDS.128</code> memory transactions.</p>

<p><code class="language-plaintext highlighter-rouge">sgemm_v5</code> directly stores those results from per-thread registers to global memory, and since we adopt the zig-zag layout, they are no longer coalesced. Optionally, we can use shared memory as an intermediate buffer to rectify global memory access pattern.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">__shared__</span> <span class="n">__align__</span><span class="p">(</span><span class="mi">16</span><span class="p">)</span> <span class="kt">float</span> <span class="n">Cs</span><span class="p">[</span><span class="mi">8</span><span class="p">][</span><span class="mi">16</span><span class="p">][</span><span class="mi">36</span><span class="p">];</span>
<span class="k">auto</span> <span class="n">st_r2g_c</span> <span class="o">=</span> <span class="p">[</span><span class="o">&amp;</span><span class="p">]</span> <span class="p">()</span> <span class="p">{</span>
      <span class="cp">#pragma unroll
</span>      <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="mi">2</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
            <span class="cp">#pragma unroll
</span>            <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">j</span> <span class="o">&lt;</span> <span class="mi">2</span><span class="p">;</span> <span class="n">j</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
                  <span class="cp">#pragma unroll
</span>                  <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">row</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">row</span> <span class="o">&lt;</span> <span class="mi">4</span><span class="p">;</span> <span class="n">row</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
                        <span class="n">FLOAT4</span><span class="p">(</span><span class="n">Cs</span><span class="p">[</span><span class="n">warpId</span><span class="p">][</span><span class="n">frag_row</span> <span class="o">*</span> <span class="mi">4</span> <span class="o">+</span> <span class="n">row</span><span class="p">][</span><span class="n">frag_col</span> <span class="o">*</span> <span class="mi">4</span><span class="p">])</span> <span class="o">=</span> <span class="n">FLOAT4</span><span class="p">(</span><span class="n">acc</span><span class="p">[</span><span class="mi">4</span> <span class="o">*</span> <span class="n">i</span> <span class="o">+</span> <span class="n">row</span><span class="p">][</span><span class="mi">4</span> <span class="o">*</span> <span class="n">j</span><span class="p">]);</span>
                  <span class="p">}</span>
                  <span class="n">__syncwarp</span><span class="p">();</span>

                  <span class="k">const</span> <span class="kt">int</span> <span class="n">c_col</span> <span class="o">=</span> <span class="n">block_col</span> <span class="o">+</span> <span class="n">warp_col</span> <span class="o">+</span> <span class="n">j</span> <span class="o">*</span> <span class="mi">32</span> <span class="o">+</span> <span class="n">laneId</span><span class="p">;</span>
                  
                  <span class="cp">#pragma unroll
</span>                  <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">p</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">p</span> <span class="o">&lt;</span> <span class="mi">16</span><span class="p">;</span> <span class="n">p</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
                        <span class="k">const</span> <span class="kt">int</span> <span class="n">c_row</span> <span class="o">=</span> <span class="n">block_row</span> <span class="o">+</span> <span class="n">warp_row</span> <span class="o">+</span> <span class="n">i</span> <span class="o">*</span> <span class="mi">16</span> <span class="o">+</span> <span class="n">p</span><span class="p">;</span>
                        <span class="k">if</span> <span class="p">(</span><span class="n">c_row</span> <span class="o">&lt;</span> <span class="n">M</span> <span class="o">&amp;&amp;</span> <span class="n">c_col</span> <span class="o">&lt;</span> <span class="n">N</span><span class="p">)</span> <span class="p">{</span>
                              <span class="n">C</span><span class="p">[</span><span class="n">OFFSET</span><span class="p">(</span><span class="n">c_row</span><span class="p">,</span> <span class="n">c_col</span><span class="p">,</span> <span class="n">N</span><span class="p">)]</span> <span class="o">=</span> <span class="n">Cs</span><span class="p">[</span><span class="n">warpId</span><span class="p">][</span><span class="n">p</span><span class="p">][</span><span class="n">laneId</span><span class="p">];</span>
                        <span class="p">}</span>
                  <span class="p">}</span>
                  
                  <span class="n">__syncwarp</span><span class="p">();</span>
            <span class="p">}</span>
      <span class="p">}</span>
<span class="p">};</span>
</code></pre></div></div>

<p>Each warp writes into its <code class="language-plaintext highlighter-rouge">[16][36]</code> intermediate buffer, and then we flush the 16 rows one by one. Note that <code class="language-plaintext highlighter-rouge">__syncwarp()</code> instead of <code class="language-plaintext highlighter-rouge">__syncthreads()</code> is used, because each warp independently works on its own <code class="language-plaintext highlighter-rouge">Cs[warpId]</code> array, we only need threads within a warp to synchronize.</p>

<p>There is an obvious tradeoff to make here, as in <code class="language-plaintext highlighter-rouge">sgemm_v6</code> we have more synchronizations but our store to the global memory is coalesced.</p>

<p>On the <code class="language-plaintext highlighter-rouge">Nvidia L4</code> GPU I am using, this actually hurts the performance a little bit.</p>

<h2 id="future-works">Future Works</h2>

<ol>
  <li>Have more illustrations</li>
  <li>Consider bank conflict of registers</li>
  <li>Double buffering</li>
  <li>PTX analysis and so on</li>
</ol>

<h2 id="references">References</h2>

<ul>
  <li><a href="https://dl.acm.org/doi/10.1145/3372419">Strassen’s Algorithm Reloaded on GPUs</a></li>
  <li><a href="https://salykova.github.io/sgemm-gpu">Advanced Matrix Multiplication Optimization on NVIDIA GPUs</a></li>
  <li><a href="https://docs.nvidia.com/cuda/inline-ptx-assembly/index.html">Inline PTX Assembly in CUDA</a></li>
  <li><a href="https://zhuanlan.zhihu.com/p/690052715">搞懂 CUDA Shared Memory 上的 bank conflicts 和向量化指令（LDS.128 / float4）的访存特点</a></li>
  <li><a href="https://github.com/Yinghan-Li/YHs_Sample/blob/master/cuda/gemm/sgemm.cu">YHs_Sample sgemm</a></li>
</ul>]]></content><author><name>Yuchen Tang</name></author><summary type="html"><![CDATA[Continues the SGEMM kernel on a cloud NVIDIA L4. Nsight Compute shows two problems in version 4: uncoalesced global stores and shared-memory bank conflicts. Version 5 adds a warp tile with a zig-zag thread layout, so that LDS.128 shared-memory loads merge from quarter-warp into half-warp transactions. Version 6 stages results in shared memory to coalesce the writeback to global memory, which turned out slightly slower on the L4.]]></summary></entry></feed>