<script data-pm-proxy="intercept"></script><?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[gdbplus's Substack]]></title><description><![CDATA[My personal Substack]]></description><link>https://gdbplus.substack.com</link><image><url>https://substackcdn.com/image/fetch/$s_!42IY!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3bb02da3-b5e5-483b-8b5d-2a62b7943554_1280x1280.png</url><title>gdbplus&apos;s Substack</title><link>https://gdbplus.substack.com</link></image><generator>Substack</generator><lastBuildDate>Fri, 04 Sep 2026 16:36:10 GMT</lastBuildDate><atom:link href="/__u/gdbplus.substack.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[DavidZhu]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[gdbplus@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[gdbplus@substack.com]]></itunes:email><itunes:name><![CDATA[gdbplus]]></itunes:name></itunes:owner><itunes:author><![CDATA[gdbplus]]></itunes:author><googleplay:owner><![CDATA[gdbplus@substack.com]]></googleplay:owner><googleplay:email><![CDATA[gdbplus@substack.com]]></googleplay:email><googleplay:author><![CDATA[gdbplus]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[A Complete Guide to x86 Jump Machine Code: NOP & All Common JMP Variants]]></title><description><![CDATA[Machine code is the raw byte instruction set that the x86 CPU directly executes, while assembly language is merely a human-readable abstraction of these binary bytes.]]></description><link>https://gdbplus.substack.com/p/a-complete-guide-to-x86-jump-machine</link><guid isPermaLink="false">https://gdbplus.substack.com/p/a-complete-guide-to-x86-jump-machine</guid><dc:creator><![CDATA[gdbplus]]></dc:creator><pubDate>Wed, 26 Aug 2026 23:59:04 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!tzlD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0762baa-3ad7-4eac-8495-a2e9c7cc1af4_1536x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Machine code is the raw byte instruction set that the x86 CPU directly executes, while assembly language is merely a human-readable abstraction of these binary bytes. To master low-level programming, reverse engineering, and shellcode development, it is essential to understand the encoding rules, memory layout, and address calculation logic of core instructions &#8212; especially <strong>NOP</strong> and various <strong>JMP (jump)</strong> instructions. This article systematically explains the full set of common x86 jump instructions, combining byte composition, assembly correspondence, and step-by-step address computation principles</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!tzlD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0762baa-3ad7-4eac-8495-a2e9c7cc1af4_1536x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!tzlD!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0762baa-3ad7-4eac-8495-a2e9c7cc1af4_1536x1024.png 424w, /__u/substackcdn.com/image/fetch/$s_!tzlD!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0762baa-3ad7-4eac-8495-a2e9c7cc1af4_1536x1024.png 848w, /__u/substackcdn.com/image/fetch/$s_!tzlD!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0762baa-3ad7-4eac-8495-a2e9c7cc1af4_1536x1024.png 1272w, /__u/substackcdn.com/image/fetch/$s_!tzlD!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0762baa-3ad7-4eac-8495-a2e9c7cc1af4_1536x1024.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!tzlD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0762baa-3ad7-4eac-8495-a2e9c7cc1af4_1536x1024.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b0762baa-3ad7-4eac-8495-a2e9c7cc1af4_1536x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1469748,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://gdbplus.substack.com/i/212927247?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0762baa-3ad7-4eac-8495-a2e9c7cc1af4_1536x1024.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!tzlD!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0762baa-3ad7-4eac-8495-a2e9c7cc1af4_1536x1024.png 424w, /__u/substackcdn.com/image/fetch/$s_!tzlD!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0762baa-3ad7-4eac-8495-a2e9c7cc1af4_1536x1024.png 848w, /__u/substackcdn.com/image/fetch/$s_!tzlD!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0762baa-3ad7-4eac-8495-a2e9c7cc1af4_1536x1024.png 1272w, /__u/substackcdn.com/image/fetch/$s_!tzlD!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0762baa-3ad7-4eac-8495-a2e9c7cc1af4_1536x1024.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>.</p><h2>1. Basic Foundation: The NOP Instruction (0x90)</h2><p>The NOP (No Operation) instruction is the most basic single-byte instruction in x86 architecture, with a fixed machine code value of <strong>0x90</strong>.</p><p>Key characteristics of NOP:</p><ul><li><p>Occupies <strong>1 byte</strong> of memory space and consumes one CPU clock cycle;</p></li><li><p>Performs no data operations, no register modifications, and no program logic changes;</p></li><li><p>Main application scenarios: program padding, memory address alignment, debugging breakpoint reservation, and shellcode space filling.</p></li></ul><p>In memory layout, consecutive NOPs form blank instruction intervals, which are often used as buffer zones for jump instruction execution in low-level development.</p><h2>2. Core Rule of All Relative Jump Instructions</h2><p>Most common jump instructions in x86 are<strong>relative jumps</strong>. Unlike absolute jumps that directly specify a target memory address, relative jumps only store an offset value. The CPU follows a fixed formula to calculate the final jump target, which is the most error-prone core principle for beginners:</p><p><strong>Target Address = Address of the instruction immediately after the JMP instruction + signed offset</strong></p><p>It is critical to note that the offset is <strong>not</strong> based on the starting address of the JMP instruction itself, but on the address of the next instruction after the entire JMP instruction ends. In GNU assembly syntax, the symbol <code>$</code> represents the starting address of the current instruction.</p><h2>3. Full Analysis of Common x86 JMP Instruction Variants</h2><h3>3.1 Short Relative Jump (Opcode: 0xEB)</h3><p>The short jump is the smallest and most flexible relative jump instruction, applicable only to short-distance program jumps.</p><ul><li><p><strong>Instruction length</strong>: 2 bytes total (1-byte opcode <code>0xEB</code> + 1-byte signed offset)</p></li><li><p><strong>Jump range</strong>: 8-bit signed offset, covering <strong>-128 bytes ~ +127 bytes</strong></p></li><li><p><strong>Application scenarios</strong>: Local small loops, short conditional branches, and tiny code block jumps</p></li></ul><p>Practical memory example and calculation:</p><p>Memory layout: <br>0x0000: 90        nop<br>0x0001: EB 03     jmp $+3</p><p>Calculation steps:</p><ol><li><p>The JMP instruction starts at 0x0001 and occupies 2 bytes;</p></li><li><p>Address of the next instruction: 0x0001 + 2 = 0x0003;</p></li><li><p>Jump offset = 0x03;</p></li><li><p>Final target address: 0x0003 + 0x03 = 0x0006.</p></li></ol><p>This instruction implements a short forward jump. By using a negative offset (e.g., <code>EB FD</code>, offset = -3), it can implement a backward short jump.</p><h3>3.2 Near Relative Jump (Opcode: 0xE9)</h3><p>The 0xE9 near relative jump is the most widely used jump instruction in x86 32-bit programs and shellcode, with a far larger jump range than short jumps.</p><ul><li><p><strong>Instruction length</strong>: 5 bytes total (1-byte opcode <code>0xE9</code> + 4-byte little-endian signed offset)</p></li><li><p><strong>Jump range</strong>: 32-bit signed offset, covering <strong>-2GB ~ +2GB</strong>, covering the entire program memory space</p></li><li><p><strong>Application scenarios</strong>: cross-code-block jumps, position-independent shellcode, and large program logic jumps</p></li></ul><p>Basic example (zero-offset jump):<br>0x0002: E9 00 00 00 00     jmp $+5</p><p>Calculation logic:</p><ol><li><p>The JMP instruction starts at 0x0002 and occupies 5 bytes;</p></li><li><p>Address of the next instruction: 0x0002 + 5 = 0x0007;</p></li><li><p>Offset = 0, so the target address is exactly 0x0007;</p></li></ol><p>This zero-offset jump is a &#8220;null jump&#8221; that directly executes the next instruction, which is often used for instruction padding and verification.</p><p>Extended practical examples:</p><ul><li><p>Forward jump 0x10 bytes: Machine code <code>E9 10 00 00 00</code>, assembly <code>jmp $+0x15</code></p></li><li><p>Backward jump 0x20 bytes: Machine code <code>E9 E0 FF FF FF</code> (negative numbers stored in two&#8217;s complement)</p></li></ul><h3>3.3 Indirect Absolute Jump (Opcode: 0xFF)</h3><p>Different from relative jumps, the<strong>0xFF indirect jump</strong> is an absolute jump. It does not calculate the target address through offsets, but directly reads the complete absolute address from registers or memory, with no offset calculation involved.</p><h4>3.3.1 Register indirect jump</h4><p>Typical instruction: <code>jmp eax</code>, machine code <code>FF E0</code> (2 bytes)</p><p>Execution logic: The CPU directly assigns the 32-bit address value stored in the EAX register to the instruction pointer (EIP) to complete the jump.</p><p>Common variants:<code>FF E1</code> (jmp ecx), <code>FF E2</code> (jmp edx), applicable to function pointer calls and dynamic jump scenarios.</p><h4>3.3.2 Memory indirect jump</h4><p>Typical instruction: <code>jmp [0x1000]</code>, machine code <code>FF 25 00 10 00 00</code> (6 bytes)</p><p>Execution logic: The CPU reads the 32-bit absolute address data stored at the memory address 0x1000, then assigns it to EIP, realizing indirect jump based on memory data.</p><p>Application scenarios: Jump table calls, dynamic program address scheduling, and pointer-based program branching.</p><h2>4. Comprehensive Comparison of All Jump Instructions</h2><p>The following table summarizes the core parameters and characteristics of all mainstream x86 jump instructions, facilitating quick distinction and application:</p><p><strong>1. Short Relative Jump (0xEB)</strong><br>Size: 2 Bytes | Operand: 8-bit signed offset | Calculation: Target = Next Instr Address + Offset | Usage: Short local loop, tiny conditional branch<br><br><strong>2. Near Relative Jump (0xE9)</strong><br>Size: 5 Bytes | Operand: 32-bit signed offset | Calculation: Target = Next Instr Address + Offset | Usage: Long-distance code jump, position-independent shellcode<br><br><strong>3. Register Indirect Jump (0xFF E0~E7)</strong><br>Size: 2 Bytes | Operand: Register stored absolute address | Calculation: EIP = Register value | Usage: Function pointer call, program dynamic jump<br><br><strong>4. Memory Indirect Jump (0xFF 25)</strong><br>Size: 6 Bytes | Operand: Memory pointer data | Calculation: EIP = *Memory address | Usage: Jump table invocation, dynamic memory address scheduling</p><h2>5. Integrated Assembly &amp; Machine Code Example</h2><p>The following complete memory code snippet integrates NOP and all jump types, intuitively showing the actual memory layout of different instructions:</p><p>0x0000  90                  nop<br>0x0001  EB 05               jmp $+7     ; Short relative forward jump<br>0x0003  90                  nop<br>0x0004  90                  nop<br>0x0005  E9 08 00 00 00      jmp $+13    ; Near relative long jump<br>0x0009  FF E1               jmp ecx     ; Register indirect absolute jump</p><h2>6. Key Takeaways &amp; Common Pitfalls</h2><ul><li><p><strong>NOP (0x90)</strong> is a single-byte empty instruction, mainly used for memory padding and alignment;</p></li><li><p>All <strong>relative jumps (0xEB, 0xE9)</strong> calculate targets based on the <strong>next instruction address</strong>, not the current JMP address &#8212; this is the most common error in manual shellcode writing and reverse analysis;</p></li><li><p>Relative jumps are position-independent, making them the core choice for shellcode; indirect absolute jumps rely on fixed register/memory addresses, suitable for program internal dynamic calls;</p></li><li><p>Short jumps have strict distance limitations, while near jumps cover the full memory range, which should be selected according to actual code distance in development.</p></li></ul>]]></content:encoded></item><item><title><![CDATA[Demystifying x86-64 Firmware Boot: Modes, Registers, and I/O]]></title><description><![CDATA[Introduction]]></description><link>https://gdbplus.substack.com/p/demystifying-x86-64-firmware-boot</link><guid isPermaLink="false">https://gdbplus.substack.com/p/demystifying-x86-64-firmware-boot</guid><dc:creator><![CDATA[gdbplus]]></dc:creator><pubDate>Tue, 25 Aug 2026 12:24:47 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!opPc!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F153acb2b-f25f-4a5e-9bda-175ed428c432_1536x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<ol><li></li></ol><h1>Introduction</h1><p>The x86-64 boot sequence is one of the most misunderstood areas in low-level systems programming. Firmware engineers, kernel developers, and students alike frequently conflate concepts that live at entirely different layers of the system: assembler directives with CPU modes, register names with separate registers, memory models with execution modes, and I/O instructions with privilege levels.</p><p>This article builds a correct mental model from the ground up. We trace the boot flow from the reset vector to C code, dissect each CPU mode, clarify what BITS directives actually control, explain register aliasing, and demystify port-mapped I/O. By the end, you will understand not just what happens during boot, but why each layer exists and how they interact.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!opPc!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F153acb2b-f25f-4a5e-9bda-175ed428c432_1536x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!opPc!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F153acb2b-f25f-4a5e-9bda-175ed428c432_1536x1024.png 424w, /__u/substackcdn.com/image/fetch/$s_!opPc!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F153acb2b-f25f-4a5e-9bda-175ed428c432_1536x1024.png 848w, /__u/substackcdn.com/image/fetch/$s_!opPc!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F153acb2b-f25f-4a5e-9bda-175ed428c432_1536x1024.png 1272w, /__u/substackcdn.com/image/fetch/$s_!opPc!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F153acb2b-f25f-4a5e-9bda-175ed428c432_1536x1024.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!opPc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F153acb2b-f25f-4a5e-9bda-175ed428c432_1536x1024.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/153acb2b-f25f-4a5e-9bda-175ed428c432_1536x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1854694,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://gdbplus.substack.com/i/212690734?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F153acb2b-f25f-4a5e-9bda-175ed428c432_1536x1024.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!opPc!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F153acb2b-f25f-4a5e-9bda-175ed428c432_1536x1024.png 424w, /__u/substackcdn.com/image/fetch/$s_!opPc!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F153acb2b-f25f-4a5e-9bda-175ed428c432_1536x1024.png 848w, /__u/substackcdn.com/image/fetch/$s_!opPc!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F153acb2b-f25f-4a5e-9bda-175ed428c432_1536x1024.png 1272w, /__u/substackcdn.com/image/fetch/$s_!opPc!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F153acb2b-f25f-4a5e-9bda-175ed428c432_1536x1024.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div class="callout-block" data-callout="true"><p><strong>Target audience:</strong> Firmware engineers, OS kernel developers, embedded systems programmers, and advanced students who have seen x86 assembly but want a precise, layered understanding of boot-time architecture</p><p>.</p></div><ol start="2"><li></li></ol><h1>The Boot Sequence: From Reset Vector to C Code</h1><p>When an x86-64 CPU receives power or a reset signal, it does not start executing your operating system. It begins in a minimal 16-bit environment called Real Mode, at a fixed memory address. The firmware (BIOS or UEFI SEC phase) must then shepherd the CPU through several mode transitions before it can execute ordinary C code.</p><ol><li></li></ol><h2>Reset Vector and Real Mode Entry</h2><p>The reset vector is the physical address from which the CPU fetches its first instruction after reset. For all x86 and x86-64 CPUs &#8212; Intel and AMD alike &#8212; this address is <strong>0xFFFFFFF0</strong> (the top of the 4 GB address space, minus 16 bytes). In real-mode segmentation terms, this corresponds to CS:IP = F000:FFF0 with the upper address bits hardwired high.</p><p>At this point, the CPU is in <strong>16-bit Real Mode</strong>: segment:offset addressing, a 1 MB addressable memory window (20-bit address bus), no memory protection, no privilege levels, and no paging. The first firmware instructions run here.</p><pre><code><code>; Reset vector: physical 0xFFFFFFF0 (F000:FFF0 real mode)
BITS 16
cli
xor ax, ax
mov ds, ax
mov ss, ax
mov sp, 0x7C00        ; temporary stack (placeholder)
</code></code></pre><ol start="2"><li></li></ol><h2>Early Initialization: A20, GDT, and PAE</h2><p>Before the CPU can leave real mode, several prerequisites must be satisfied:</p><ol><li><p><strong>A20 Line Enable:</strong> The 20th address line (A20) must be enabled to allow access to memory above 1 MB. On modern systems this is often already enabled by silicon reset, but legacy firmware explicitly enables it (historically via the keyboard controller on port 0x64, or via port 0x92).</p></li><li><p><strong>Load GDT:</strong> The Global Descriptor Table (GDT) must be loaded with at least a null descriptor, a 64-bit code segment descriptor, and a 64-bit data segment descriptor. The GDT defines the segment attributes that protected mode and long mode rely on.</p></li><li><p><strong>Enable PAE:</strong> Physical Address Extension (PAE), controlled by CR4 bit 5, must be enabled. PAE is a hard prerequisite for long mode &#8212; it changes the page table hierarchy to the 4-level structure that 64-bit mode uses.</p></li></ol><pre><code><code>call enable_a20
lgdt [gdt_descriptor]

; Enable PAE (CR4 bit 5)
mov eax, cr4
or  eax, 1 &lt;&lt; 5
mov cr4, eax
</code></code></pre><ol start="3"><li></li></ol><h2>Enabling Long Mode: The EFER.LME Step</h2><p>With PAE enabled and the GDT loaded, the next step is to set the Long Mode Enable (LME) bit in the Extended Feature Enable Register (EFER). EFER is a Model-Specific Register (MSR) at address <strong>0xC0000080</strong>, and LME is bit 8.</p><p>Setting LME does not by itself activate long mode. It merely arms the mechanism. Long mode becomes active only after paging is enabled and a far jump reloads the code segment (CS) with a 64-bit descriptor.</p><pre><code><code>; Load PML4 base into CR3
mov eax, pml4_table
mov cr3, eax

; Enable Long Mode (EFER.LME, bit 8)
mov ecx, 0xC0000080
rdmsr
or  eax, 1 &lt;&lt; 8
wrmsr
</code></code></pre><p>Note that CR3 is loaded before enabling long mode. CR3 holds the physical base address of the PML4 (Page Map Level 4) table &#8212; the root of the 4-level page table walk. Without valid page tables, enabling paging will cause an immediate page fault.</p><ol start="4"><li></li></ol><h2>The Transient Protected Mode</h2><p>This is the step that causes the most confusion. To activate long mode, you must set both CR0.PE (Protected Mode Enable, bit 0) and CR0.PG (Paging, bit 31). When these are set with EFER.LME already armed, the CPU enters <strong>32-bit Protected Mode</strong> &#8212; but only momentarily.</p><pre><code><code>; Enable Protected Mode + Paging (CR0.PE bit 0 + CR0.PG bit 31)
mov eax, cr0
or  eax, (1 &lt;&lt; 31) | (1 &lt;&lt; 0)
mov cr0, eax
</code></code></pre><div class="callout-block" data-callout="true"><p><strong>Common bug:</strong> Many simplified examples set only CR0.PG and omit CR0.PE. If PE is not set, the processor never enters protected mode, and setting PG with LME=1 causes a General Protection fault (#GP). Both bits must be set together.</p></div><p>After the <code>mov cr0, eax</code> instruction, the CPU is in a transitional state. Long mode is enabled (LME=1, PE=1, PG=1) but not yet active, because the CS register still holds its old real-mode cached attributes. The CPU uses those old attributes to fetch and decode the very next instruction. In effect, the processor is in 32-bit protected mode for the duration of exactly one instruction.</p><ol start="5"><li></li></ol><h2>Far Jump: The Actual Mode Switch</h2><p>The far jump is what actually activates 64-bit long mode. It performs two critical actions simultaneously:</p><ol><li><p>It flushes the instruction pipeline, discarding any pre-fetched instructions that were decoded under the old mode.</p></li><li><p>It reloads the CS register from the GDT entry at selector 0x08. If that descriptor has the L-bit (Long Mode bit) set to 1, the CPU switches to 64-bit mode.</p></li></ol><pre><code><code>; Far jump flushes pipeline, loads 64-bit CS (L-bit=1)
jmp 0x08:long_mode_start

BITS 64
long_mode_start:
mov ax, 0x10
mov ds, ax
mov es, ax
mov ss, ax
lea rsp, [stack_top]
</code></code></pre><p>After the far jump, the CPU is fully in 64-bit long mode. The data segments (DS, ES, SS) are reloaded with selector 0x10 (the 64-bit data segment), and the stack pointer is set to a valid 64-bit stack.</p><ol start="6"><li></li></ol><h2>Hand-off to PEI Core</h2><p>With the CPU in 64-bit mode and a valid stack, the firmware can now call C code. In the UEFI Platform Initialization (PI) architecture, this is the <strong>PEI Core</strong> (Pre-EFI Initialization), entered via a function typically named <code>pei_main</code>.</p><pre><code><code>call pei_main        ; hand off to PEI Core (C code)
hlt
</code></code></pre><p>From this point forward, the firmware executes primarily in C, with occasional assembly for low-level operations. The SEC (Security) phase &#8212; the assembly code we have been tracing &#8212; is complete.</p><ol start="3"><li></li></ol><h1>CPU Modes Explained</h1><p>The x86-64 architecture defines several execution modes, each with distinct addressing capabilities, register widths, and protection mechanisms. Understanding the differences is essential for firmware and kernel development.</p><ol><li></li></ol><h2>Real Mode (16-bit)</h2><p>Real Mode is the CPU&#8217;s native state at reset. It emulates the behavior of the original 8086 processor:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!pSvz!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa943b7c2-87e6-47fd-bbf5-69ecc9198337_1116x592.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!pSvz!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa943b7c2-87e6-47fd-bbf5-69ecc9198337_1116x592.png 424w, /__u/substackcdn.com/image/fetch/$s_!pSvz!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa943b7c2-87e6-47fd-bbf5-69ecc9198337_1116x592.png 848w, /__u/substackcdn.com/image/fetch/$s_!pSvz!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa943b7c2-87e6-47fd-bbf5-69ecc9198337_1116x592.png 1272w, /__u/substackcdn.com/image/fetch/$s_!pSvz!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa943b7c2-87e6-47fd-bbf5-69ecc9198337_1116x592.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!pSvz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa943b7c2-87e6-47fd-bbf5-69ecc9198337_1116x592.png" width="1116" height="592" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a943b7c2-87e6-47fd-bbf5-69ecc9198337_1116x592.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:592,&quot;width&quot;:1116,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:65552,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://gdbplus.substack.com/i/212690734?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa943b7c2-87e6-47fd-bbf5-69ecc9198337_1116x592.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!pSvz!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa943b7c2-87e6-47fd-bbf5-69ecc9198337_1116x592.png 424w, /__u/substackcdn.com/image/fetch/$s_!pSvz!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa943b7c2-87e6-47fd-bbf5-69ecc9198337_1116x592.png 848w, /__u/substackcdn.com/image/fetch/$s_!pSvz!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa943b7c2-87e6-47fd-bbf5-69ecc9198337_1116x592.png 1272w, /__u/substackcdn.com/image/fetch/$s_!pSvz!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa943b7c2-87e6-47fd-bbf5-69ecc9198337_1116x592.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Real mode&#8217;s 1 MB limit is a direct inheritance from the 8086&#8217;s 20-bit address bus. The A20 gate (mentioned earlier) exists because the 8086 would wrap addresses above 1 MB back to zero, and some legacy software relied on this behavior.</p><ol start="2"><li></li></ol><h2>Protected Mode (32-bit)</h2><p>Protected Mode, introduced with the 80286 and extended to 32-bit with the 80386, is the mode that enables modern operating system features:</p><p>Protected mode is enabled by setting CR0.PE (bit 0). In the boot flow we traced, protected mode is entered but immediately exited via the far jump to long mode. It is a corridor, not a destination.</p><ol start="3"><li></li></ol><h2>Long Mode (64-bit)</h2><p>Long Mode is the 64-bit mode of the x86-64 architecture, originally designed by AMD as AMD64 and later adopted by Intel as Intel 64. It is enabled by the combination of EFER.LME=1, CR0.PE=1, and CR0.PG=1, and activated by a far jump to a 64-bit code segment.</p><p>Long Mode is not a single mode &#8212; it contains two sub-modes, selected by the attributes of the current code segment (CS) descriptor:</p><ol><li></li></ol><h3>64-bit Sub-mode</h3><p>When CS.L=1 and CS.D=0, the CPU is in full 64-bit mode:</p><ul><li><p>64-bit general-purpose registers (RAX, RBX, RCX, RDX, RSI, RDI, RBP, RSP, and R8&#8211;R15)</p></li><li><p>64-bit linear addressing (canonical addresses, 48-bit or 57-bit with LA57)</p></li><li><p>4-level (or 5-level) page table hierarchy</p></li><li><p>Flat memory model &#8212; segmentation is largely disabled (CS, DS, ES, SS bases are forced to 0; FS and GS remain configurable)</p></li><li><p>RIP-relative addressing</p></li></ul><ol start="2"><li></li></ol><h3>Compatibility Sub-mode</h3><p>When CS.L=0, the CPU remains in long mode but executes legacy 32-bit (CS.D=1) or 16-bit (CS.D=0) code. This allows 32-bit applications to run under a 64-bit operating system without emulation. The system firmware (UEFI) and modern OS kernels run in 64-bit sub-mode, while compatibility mode is used primarily for running legacy user-space applications.</p><div class="callout-block" data-callout="true"><p><strong>Key insight:</strong> Compatibility mode is NOT the same as legacy 32-bit protected mode. The CPU remains in long mode (paging, 64-bit interrupt handling, 64-bit system calls), but the current code segment executes with 32-bit semantics. A 64-bit OS can switch a thread between 64-bit mode and compatibility mode simply by changing the CS selector.</p></div><ol start="4"><li></li></ol><h2>Flat Mode: A Memory Model, Not a CPU Mode</h2><p>&#8220;Flat Mode&#8221; is frequently mistaken for a CPU execution mode. It is not. Flat mode is a <strong>memory model configuration</strong> in which all segment registers point to the same linear address space &#8212; base address 0, limit set to the maximum. When segmentation is effectively disabled, the logical address (the address the program works with) equals the linear address (the address after segmentation, before paging).</p><p>Flat mode can coexist with different CPU modes:</p><p><strong>Configuration</strong></p><p><strong>Description</strong></p><p>Flat Protected Mode</p><p>CS, DS, ES, SS all have base=0, limit=0xFFFFFFFF. This is the standard memory model used by Linux, Windows, and virtually all 32-bit operating systems. Segmentation provides no isolation; protection is enforced entirely by paging.</p><p>Unreal Mode (Flat Real Mode)</p><p>A clever hack: temporarily enter protected mode, load DS and/or ES with a 4 GB limit descriptor, then return to real mode. The segment registers retain their expanded limits even after switching back to real mode, allowing access to memory above 1 MB while still executing 16-bit real-mode code. Used by some BIOS firmware and legacy software.</p><p>Flat Long Mode</p><p>In 64-bit long mode, flat mode is effectively mandatory. The CPU forces CS, DS, ES, and SS bases to 0 and ignores their limits. Only FS and GS retain configurable bases (used for thread-local storage and per-CPU data).</p><p>The critical distinction: <strong>CPU modes</strong> (Real, Protected, Long) are hardware execution states controlled by CR0, CR4, EFER, and CS attributes. <strong>Flat mode</strong> is a software configuration of segment registers that can be applied within certain CPU modes. You cannot &#8220;switch to flat mode&#8221; &#8212; you configure a flat memory model while running in protected mode or long mode.</p><ol start="4"><li></li></ol><h1>BITS Directives: Assembler Layer, Not CPU Layer</h1><p>One of the most persistent sources of confusion in x86 assembly is the relationship between <code>BITS</code> directives and CPU modes. They are entirely different things at different layers of the system.</p><ol><li></li></ol><h2>What BITS Actually Does</h2><p><code>BITS 16</code>, <code>BITS 32</code>, and <code>BITS 64</code> are <strong>assembler directives</strong> &#8212; instructions to the assembler (NASM, GAS, etc.), not to the CPU. They tell the assembler which instruction encoding mode to use for the instructions that follow. The CPU never sees these directives; they are consumed at assembly time and affect the machine code bytes that are emitted.</p><p>Specifically, BITS controls:</p><ul><li><p><strong>Default operand size:</strong> Whether <code>mov ax, 0</code> or <code>mov eax, 0</code> is the &#8220;natural&#8221; encoding that does not require an operand-size prefix (0x66).</p></li><li><p><strong>Default address size:</strong> Whether 16-bit or 32-bit addressing forms are used by default (controlled by the 0x67 prefix in 16/32-bit modes).</p></li><li><p><strong>Available register names:</strong> In BITS 64, the assembler accepts RAX, RBX, R8&#8211;R15, and other 64-bit register names. In BITS 16 or BITS 32, these names are not recognized.</p></li><li><p><strong>REX prefixes:</strong> In BITS 64, the assembler automatically emits REX prefixes when 64-bit registers or extended registers are used.</p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!2TQU!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6a8c6ded-f3a8-4b73-83aa-1bd686680ee7_1487x314.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!2TQU!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6a8c6ded-f3a8-4b73-83aa-1bd686680ee7_1487x314.png 424w, /__u/substackcdn.com/image/fetch/$s_!2TQU!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6a8c6ded-f3a8-4b73-83aa-1bd686680ee7_1487x314.png 848w, /__u/substackcdn.com/image/fetch/$s_!2TQU!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6a8c6ded-f3a8-4b73-83aa-1bd686680ee7_1487x314.png 1272w, /__u/substackcdn.com/image/fetch/$s_!2TQU!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6a8c6ded-f3a8-4b73-83aa-1bd686680ee7_1487x314.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!2TQU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6a8c6ded-f3a8-4b73-83aa-1bd686680ee7_1487x314.png" width="1456" height="307" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6a8c6ded-f3a8-4b73-83aa-1bd686680ee7_1487x314.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:307,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="/__u/substackcdn.com/image/fetch/$s_!2TQU!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6a8c6ded-f3a8-4b73-83aa-1bd686680ee7_1487x314.png 424w, /__u/substackcdn.com/image/fetch/$s_!2TQU!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6a8c6ded-f3a8-4b73-83aa-1bd686680ee7_1487x314.png 848w, /__u/substackcdn.com/image/fetch/$s_!2TQU!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6a8c6ded-f3a8-4b73-83aa-1bd686680ee7_1487x314.png 1272w, /__u/substackcdn.com/image/fetch/$s_!2TQU!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6a8c6ded-f3a8-4b73-83aa-1bd686680ee7_1487x314.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><ol start="2"><li><p></p></li></ol><h2>BITS vs Mode: The Orthogonality Principle</h2><p>The most important concept to internalize is that <code>BITS</code> directives and CPU modes are <strong>orthogonal</strong> &#8212; they operate independently and can be combined in ways that may seem counterintuitive:</p><ul><li><p><strong>BITS 32 code in 16-bit protected mode:</strong> You can assemble code with BITS 32 and run it in a 16-bit code segment. The assembler emits 32-bit encodings (with implicit 0x66 prefixes relative to the 16-bit default), and the CPU executes them correctly because the 0x66 prefix overrides the CS default operand size.</p></li><li><p><strong>BITS 16 code in compatibility mode:</strong> Under long mode&#8217;s compatibility sub-mode with a 16-bit CS (CS.L=0, CS.D=0), you can execute BITS 16-encoded code. The CPU remains in long mode but decodes instructions as 16-bit.</p></li><li><p><strong>BITS 64 code requires 64-bit mode:</strong> This is the one combination that is not freely mixable. BITS 64 uses REX prefixes and 64-bit registers that only exist when the CPU is in 64-bit sub-mode (CS.L=1). Executing BITS 64 code in any other mode will produce undefined behavior or invalid opcodes.</p></li></ul><div class="callout-block" data-callout="true"><p><strong>The rule to remember:</strong> BITS tells the assembler what to emit. The far jump and control register writes tell the CPU what to become. They are independent levers. The boot code uses BITS 16 for the real-mode portion (even though some instructions like <code>mov eax, cr4</code> use 32-bit registers with implicit 0x66 prefixes), then switches to BITS 64 after the far jump.</p></div><ol start="5"><li><p></p></li></ol><h1>Register Aliasing: AL, AX, EAX, RAX</h1><p>Another common misconception is treating AL, AX, EAX, and RAX as four separate registers. They are not &#8212; they are four different-sized windows into a single physical register in the CPU&#8217;s register file.</p><ol><li><p></p></li></ol><h2>The Register Hierarchy</h2><p>The x86 architecture has accumulated register widths over decades of evolution, and each new, wider register name overlays the previous one:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!Y4Lr!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e2dedf2-a82b-4e10-b0ba-8424ecea9dcc_1495x462.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!Y4Lr!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e2dedf2-a82b-4e10-b0ba-8424ecea9dcc_1495x462.png 424w, /__u/substackcdn.com/image/fetch/$s_!Y4Lr!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e2dedf2-a82b-4e10-b0ba-8424ecea9dcc_1495x462.png 848w, /__u/substackcdn.com/image/fetch/$s_!Y4Lr!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e2dedf2-a82b-4e10-b0ba-8424ecea9dcc_1495x462.png 1272w, /__u/substackcdn.com/image/fetch/$s_!Y4Lr!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e2dedf2-a82b-4e10-b0ba-8424ecea9dcc_1495x462.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!Y4Lr!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e2dedf2-a82b-4e10-b0ba-8424ecea9dcc_1495x462.png" width="1456" height="450" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9e2dedf2-a82b-4e10-b0ba-8424ecea9dcc_1495x462.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:450,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:58113,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://gdbplus.substack.com/i/212690734?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e2dedf2-a82b-4e10-b0ba-8424ecea9dcc_1495x462.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!Y4Lr!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e2dedf2-a82b-4e10-b0ba-8424ecea9dcc_1495x462.png 424w, /__u/substackcdn.com/image/fetch/$s_!Y4Lr!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e2dedf2-a82b-4e10-b0ba-8424ecea9dcc_1495x462.png 848w, /__u/substackcdn.com/image/fetch/$s_!Y4Lr!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e2dedf2-a82b-4e10-b0ba-8424ecea9dcc_1495x462.png 1272w, /__u/substackcdn.com/image/fetch/$s_!Y4Lr!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e2dedf2-a82b-4e10-b0ba-8424ecea9dcc_1495x462.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Visually, the register looks like this:</p><pre><code><code>63                          31              15       7        0
+---------------------------+---------------+--------+--------+
|                           |               |   AH   |   AL   |  &lt;- 8-bit
|                           |               +---- AX ----+    |  &lt;- 16-bit
|                           +---------- EAX ---------------+  |  &lt;- 32-bit
+---------------------------- RAX ----------------------------+  &lt;- 64-bit
</code></code></pre><p>When you write to <code>AL</code>, you modify only the lowest 8 bits. The upper bits (bits 8&#8211;63) remain unchanged. When you write to <code>AX</code>, you modify bits 0&#8211;15. When you write to <code>EAX</code>, you modify bits 0&#8211;31 &#8212; with one important exception in 64-bit mode, described below.</p><ol start="2"><li><p></p></li></ol><h2>Behavior Across Modes</h2><p>The register file is the same physical silicon in every CPU mode. What changes is which windows are addressable and how writes to partial registers behave:</p><ol><li><p></p></li></ol><h3>16-bit Real Mode</h3><p>AL and AX are natively accessible. EAX physically exists in the silicon but requires a 0x66 operand-size prefix to access. The assembler emits this prefix automatically when you write <code>mov eax, 0</code> under BITS 16. The upper 32 bits (bits 32&#8211;63) are not architecturally visible in real mode &#8212; they exist but cannot be read or written.</p><ol start="2"><li><p></p></li></ol><h3>32-bit Protected Mode</h3><p>AL, AX, and EAX are all accessible. EAX is the native size (no prefix needed); AX requires a 0x66 prefix. The upper 32 bits (bits 32&#8211;63) still exist in the register file but are not architecturally visible. They are typically zero from reset and preserved across mode switches, but software cannot observe or modify them in 32-bit mode.</p><ol start="3"><li><p></p></li></ol><h3>64-bit Long Mode</h3><p>All four names (AL, AX, EAX, RAX) are architecturally visible and directly addressable. This is where the most interesting behavior occurs.</p><ol start="3"><li><p></p></li></ol><h2>The 64-bit Zero-Extension Quirk</h2><p>In 64-bit mode, writing to a 32-bit register (such as EAX) has an unusual side effect: it <strong>zero-extends</strong> the upper 32 bits of the corresponding 64-bit register. This is different from writing to 8-bit or 16-bit registers, which preserve the upper bits.</p><pre><code><code>mov rax, 0xAAAAAAAAAAAAAAAA   ; RAX = 0xAAAAAAAAAAAAAAAA

mov eax, 0x12345678           ; RAX = 0x0000000012345678
;                               ^^^^^^^^^^ upper 32 bits ZEROED!

mov ax, 0x1234                ; RAX = 0x00000000AAAA1234
;                               ^^^^^^^^^^^^^^^^ upper bits preserved

mov al, 0x56                  ; RAX = 0x00000000AAAA1256
;                               ^^^^^^^^^^^^^^^^^^^^^ only low byte changed
</code></code></pre><p>This zero-extension is not a bug &#8212; it is a deliberate architectural design decision. In 32-bit x86, partial-register writes (writing to AL or AX while EAX holds other data) created false dependencies that forced the CPU to merge the partial write with the existing upper bits, causing pipeline stalls and performance penalties. By zero-extending 32-bit writes in 64-bit mode, the architecture eliminates these dependencies: after <code>mov eax, X</code>, the full 64-bit value of RAX is known to be <code>0:X</code>, with no dependency on the previous value of RAX.</p><div class="callout-block" data-callout="true"><p><strong>Practical implication:</strong> In 64-bit code, if you want to preserve the upper 32 bits of RAX while modifying the lower 32 bits, you cannot use <code>mov eax, X</code> &#8212; it will zero the upper bits. You must use a read-modify-write sequence such as <code>and rax, 0xFFFFFFFF00000000</code> followed by <code>or rax, X</code>, or use a different register. In practice, this is rarely needed because 64-bit code treats 32-bit operations as producing clean 64-bit values.</p></div><ol start="6"><li><p></p></li></ol><h1>Port-Mapped I/O: IN and OUT Instructions</h1><p>The x86 architecture provides two mechanisms for communicating with hardware devices: memory-mapped I/O (MMIO), where device registers appear in the physical memory address space, and port-mapped I/O (PMIO), where devices are addressed through a separate I/O address space. The <code>IN</code> and <code>OUT</code> instructions are the gateway to port-mapped I/O.</p><ol><li><p></p></li></ol><h2>I/O Address Space vs Memory Address Space</h2><p>The x86 I/O address space is completely separate from the memory address space. It contains 65,536 I/O ports, numbered from 0x0000 to 0xFFFF. Each port can transfer 8, 16, or 32 bits at a time. This is distinct from the physical memory address space, which is 4 GB (32-bit) or 256 TB (48-bit 64-bit) and is accessed via normal load/store instructions (<code>mov</code>).</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!eSFl!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2eeb341-40f8-4f1c-b661-c4fc1fd24322_1460x442.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!eSFl!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2eeb341-40f8-4f1c-b661-c4fc1fd24322_1460x442.png 424w, /__u/substackcdn.com/image/fetch/$s_!eSFl!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2eeb341-40f8-4f1c-b661-c4fc1fd24322_1460x442.png 848w, /__u/substackcdn.com/image/fetch/$s_!eSFl!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2eeb341-40f8-4f1c-b661-c4fc1fd24322_1460x442.png 1272w, /__u/substackcdn.com/image/fetch/$s_!eSFl!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2eeb341-40f8-4f1c-b661-c4fc1fd24322_1460x442.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!eSFl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2eeb341-40f8-4f1c-b661-c4fc1fd24322_1460x442.png" width="1456" height="441" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e2eeb341-40f8-4f1c-b661-c4fc1fd24322_1460x442.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:441,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:69302,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://gdbplus.substack.com/i/212690734?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2eeb341-40f8-4f1c-b661-c4fc1fd24322_1460x442.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!eSFl!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2eeb341-40f8-4f1c-b661-c4fc1fd24322_1460x442.png 424w, /__u/substackcdn.com/image/fetch/$s_!eSFl!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2eeb341-40f8-4f1c-b661-c4fc1fd24322_1460x442.png 848w, /__u/substackcdn.com/image/fetch/$s_!eSFl!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2eeb341-40f8-4f1c-b661-c4fc1fd24322_1460x442.png 1272w, /__u/substackcdn.com/image/fetch/$s_!eSFl!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2eeb341-40f8-4f1c-b661-c4fc1fd24322_1460x442.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><ol start="2"><li><p></p></li></ol><h2>Instruction Syntax and Encoding</h2><p>The <code>IN</code> instruction reads data from an I/O port into the accumulator register (AL, AX, or EAX). The <code>OUT</code> instruction writes data from the accumulator to an I/O port. The port number can be specified as an immediate value or via the DX register.</p><pre><code><code>; Immediate port (only 0x00-0xFF)
in  al, 0x60        ; read 8-bit from port 0x60 (keyboard data)
in  ax, 0x1F0       ; read 16-bit from port 0x1F0 (IDE data)
in  eax, 0xCFC      ; read 32-bit from port 0xCFC (PCI config data)
out 0x60, al        ; write 8-bit to port 0x60
out 0xCF8, eax      ; write 32-bit to port 0xCF8 (PCI config address)

; Port in DX (any port 0x0000-0xFFFF)
mov dx, 0x3F8
in  al, dx          ; read 8-bit from COM1 data port
out dx, ax          ; write 16-bit to port in DX
</code></code></pre><p><strong>Rule</strong></p><p><strong>Detail</strong></p><p>Data register is fixed</p><p>IN always reads into AL/AX/EAX. OUT always writes from AL/AX/EAX. You cannot use other registers for the data.</p><p>Immediate port range</p><p>Only ports 0x00&#8211;0xFF can be specified as an immediate. Ports 0x0100&#8211;0xFFFF must use DX.</p><p>No 64-bit I/O</p><p>Even in 64-bit long mode, IN/OUT only support 8/16/32-bit transfers via AL/AX/EAX. RAX is never used with IN/OUT.</p><p>Encoding is mode-independent</p><p>The IN/OUT instruction encodings are identical in real, protected, and long mode. No mode-specific prefixes are needed.</p><ol start="3"><li><p></p></li></ol><h2>Privilege and Access Control</h2><p>The behavior of IN/OUT instructions changes dramatically depending on the CPU mode and privilege level:</p><ol><li><p></p></li></ol><h3>Real Mode</h3><p>In real mode, there is no privilege protection. Any code can execute IN/OUT instructions to any I/O port. This is why firmware running in real mode has unrestricted hardware access.</p><ol start="2"><li><p></p></li></ol><h3>Protected Mode and Long Mode</h3><p>In protected mode and long mode, I/O access is controlled by two mechanisms:</p><ol><li><p><strong>IOPL (I/O Privilege Level):</strong> A 2-bit field in the EFLAGS register (bits 12&#8211;13) that specifies the minimum privilege level required to execute I/O instructions. If the Current Privilege Level (CPL) is numerically greater than IOPL, IN/OUT instructions cause a General Protection fault (#GP). Firmware and kernel code run at CPL=0, so IOPL does not restrict them. User applications run at CPL=3 and are typically blocked unless IOPL is set to 3.</p></li><li><p><strong>I/O Permission Bitmap:</strong> A bitmask stored in the Task State Segment (TSS) that grants per-port access even when CPL &gt; IOPL. Each bit corresponds to one I/O port (0 = access allowed, 1 = access denied). This allows an OS to grant a user-space driver access to specific ports (e.g., a parallel port) without opening all I/O access.</p></li></ol><div class="callout-block" data-callout="true"><p><strong>Security implication:</strong> On a modern OS, user applications cannot directly execute IN/OUT instructions. Any attempt will fault into the kernel, which either emulates the I/O (if the I/O permission bitmap allows it) or terminates the application. This is why hardware access on modern systems requires kernel drivers or special frameworks (e.g., Linux ioperm/iopl, Windows IN/OUT privilege APIs).</p></div><ol start="4"><li><p></p></li></ol><h2>Common I/O Ports</h2><p>While the I/O address space has 65,536 ports, only a small fraction are commonly used. Here are some classic examples:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!jFRn!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0f175c0-f679-40fe-aa3d-92763f13010c_1504x767.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!jFRn!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0f175c0-f679-40fe-aa3d-92763f13010c_1504x767.png 424w, /__u/substackcdn.com/image/fetch/$s_!jFRn!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0f175c0-f679-40fe-aa3d-92763f13010c_1504x767.png 848w, /__u/substackcdn.com/image/fetch/$s_!jFRn!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0f175c0-f679-40fe-aa3d-92763f13010c_1504x767.png 1272w, /__u/substackcdn.com/image/fetch/$s_!jFRn!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0f175c0-f679-40fe-aa3d-92763f13010c_1504x767.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!jFRn!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0f175c0-f679-40fe-aa3d-92763f13010c_1504x767.png" width="1456" height="743" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d0f175c0-f679-40fe-aa3d-92763f13010c_1504x767.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:743,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:144118,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://gdbplus.substack.com/i/212690734?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0f175c0-f679-40fe-aa3d-92763f13010c_1504x767.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!jFRn!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0f175c0-f679-40fe-aa3d-92763f13010c_1504x767.png 424w, /__u/substackcdn.com/image/fetch/$s_!jFRn!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0f175c0-f679-40fe-aa3d-92763f13010c_1504x767.png 848w, /__u/substackcdn.com/image/fetch/$s_!jFRn!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0f175c0-f679-40fe-aa3d-92763f13010c_1504x767.png 1272w, /__u/substackcdn.com/image/fetch/$s_!jFRn!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0f175c0-f679-40fe-aa3d-92763f13010c_1504x767.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Many of these legacy ports are emulated or deprecated on modern systems (keyboard controllers, IDE, COM ports are often absent), but the PCI config ports (0xCF8/0xCFC) remain universally supported for PCIe configuration access, and the RTC ports remain for basic timekeeping.</p><ol start="7"><li><p></p></li></ol><h1>Putting It All Together: The Full Picture</h1><p>We have examined five distinct layers of the x86-64 boot environment. Let us synthesize them into a unified understanding:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!3qaT!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0861da87-e67f-46d8-9c20-e11643242f23_1504x687.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!3qaT!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0861da87-e67f-46d8-9c20-e11643242f23_1504x687.png 424w, /__u/substackcdn.com/image/fetch/$s_!3qaT!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0861da87-e67f-46d8-9c20-e11643242f23_1504x687.png 848w, /__u/substackcdn.com/image/fetch/$s_!3qaT!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0861da87-e67f-46d8-9c20-e11643242f23_1504x687.png 1272w, /__u/substackcdn.com/image/fetch/$s_!3qaT!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0861da87-e67f-46d8-9c20-e11643242f23_1504x687.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!3qaT!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0861da87-e67f-46d8-9c20-e11643242f23_1504x687.png" width="1456" height="665" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0861da87-e67f-46d8-9c20-e11643242f23_1504x687.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:665,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:143769,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://gdbplus.substack.com/i/212690734?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0861da87-e67f-46d8-9c20-e11643242f23_1504x687.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!3qaT!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0861da87-e67f-46d8-9c20-e11643242f23_1504x687.png 424w, /__u/substackcdn.com/image/fetch/$s_!3qaT!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0861da87-e67f-46d8-9c20-e11643242f23_1504x687.png 848w, /__u/substackcdn.com/image/fetch/$s_!3qaT!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0861da87-e67f-46d8-9c20-e11643242f23_1504x687.png 1272w, /__u/substackcdn.com/image/fetch/$s_!3qaT!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0861da87-e67f-46d8-9c20-e11643242f23_1504x687.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Now, trace the boot sequence one more time with all five layers in mind:</p><ol><li><p><strong>Reset:</strong> CPU starts in Real Mode (hardware layer). The assembler used BITS 16 (assembler layer) for the code that runs here. Instructions use AL/AX (register layer), with EAX accessible via 0x66 prefix. The memory model is segmented (not flat). IN/OUT is unrestricted.</p></li><li><p><strong>Early init:</strong> Still in Real Mode. A20 is enabled via IN/OUT to port 0x64 or 0x92. GDT is loaded. PAE is enabled via CR4. The code uses <code>mov eax, cr4</code> &#8212; BITS 16 encoding with implicit 0x66 prefix for the 32-bit register.</p></li><li><p><strong>Long mode arming:</strong> EFER.LME is set via <code>rdmsr</code>/<code>wrmsr</code> (MSR access, not IN/OUT). CR3 is loaded with the PML4 base. Still in Real Mode at the hardware layer.</p></li><li><p><strong>Mode switch #1:</strong> CR0.PE and CR0.PG are set. CPU enters 32-bit Protected Mode (hardware layer) &#8212; transiently. The next instruction (far jump) is fetched using old CS attributes. The assembler still used BITS 16 for this instruction because it appears before the BITS 64 directive.</p></li><li><p><strong>Mode switch #2:</strong> Far jump to selector 0x08. CS is reloaded with a 64-bit code segment (L-bit=1). CPU enters 64-bit Long Mode (hardware layer). The assembler switches to BITS 64 at the <code>long_mode_start:</code> label. Now RAX is accessible. The memory model is flat (mandatory in long mode). IN/OUT remains available but now privilege-gated (though firmware runs at ring 0).</p></li><li><p><strong>C hand-off:</strong><code>call pei_main</code> transfers control to C code running in 64-bit long mode with a flat memory model. The SEC phase assembly is complete.</p></li></ol><ol start="8"><li><p></p></li></ol><h1>Summary and Key Takeaways</h1><ol><li><p><strong>The boot flow involves exactly 2 mode switches:</strong> Real Mode &#8594; Protected Mode (transient, one instruction) &#8594; Long Mode (64-bit). The reset state is Real Mode, not a switch.</p></li><li><p><strong>BITS directives are not CPU modes.</strong> BITS 16/32/64 tell the assembler how to encode instructions. The CPU mode is controlled by CR0, CR4, EFER, and CS. They are orthogonal.</p></li><li><p><strong>AL, AX, EAX, RAX are one register.</strong> They are different-sized windows into the same physical register file. In 64-bit mode, writing EAX zero-extends the upper 32 bits; writing AX or AL preserves them.</p></li><li><p><strong>Flat mode is a memory model, not a CPU mode.</strong> It means all segments point to base 0 with maximum limit. It can be configured in protected mode (flat protected model) or is mandatory in long mode. Unreal mode achieves flat addressing while staying in real mode.</p></li><li><p><strong>IN/OUT work identically in all modes.</strong> The instruction encoding is the same in real, protected, and long mode. What changes is privilege: unrestricted in real mode, gated by IOPL and I/O Permission Bitmap in protected/long mode. Data sizes are 8/16/32-bit only &#8212; never 64-bit.</p></li><li><p><strong>CR0.PE must be set alongside CR0.PG.</strong> A common bug in simplified boot examples is setting only PG without PE. Without PE, the CPU never enters protected mode, and long mode cannot activate.</p></li><li><p><strong>The far jump is the actual mode switch.</strong> Setting control registers arms the mode; the far jump that reloads CS with a 64-bit descriptor (L-bit=1) is what activates 64-bit long mode.</p></li></ol><ol start="9"><li><p></p></li></ol><h1>Glossary</h1><p><strong>Term</strong></p><p><strong>Definition</strong></p><p>SEC</p><p>Security Phase &#8212; the first phase of UEFI PI boot, written in assembly, responsible for initial CPU state setup and hand-off to PEI.</p><p>PEI</p><p>Pre-EFI Initialization &#8212; the second boot phase, primarily C code, responsible for memory initialization and early platform setup.</p><p>GDT</p><p>Global Descriptor Table &#8212; a table in memory that defines segment descriptors used by protected mode and long mode.</p><p>PAE</p><p>Physical Address Extension &#8212; a feature (CR4 bit 5) that enables 36-bit+ physical addressing and is a prerequisite for long mode.</p><p>EFER</p><p>Extended Feature Enable Register &#8212; MSR 0xC0000080, containing the LME (Long Mode Enable) bit and other feature controls.</p><p>PML4</p><p>Page Map Level 4 &#8212; the root page table in the 4-level paging hierarchy used by long mode. Its physical address is stored in CR3.</p><p>CS.L</p><p>The Long Mode bit in a code segment descriptor. When set (CS.L=1) with CS.D=0, the CPU executes in 64-bit mode.</p><p>IOPL</p><p>I/O Privilege Level &#8212; a 2-bit field in EFLAGS (bits 12&#8211;13) that controls which privilege levels may execute IN/OUT instructions.</p><p>TSS</p><p>Task State Segment &#8212; a system segment that contains, among other things, the I/O Permission Bitmap used for per-port I/O access control.</p><p>MMIO</p><p>Memory-Mapped I/O &#8212; a mechanism where device registers appear in the physical memory address space, accessed via normal load/store instructions.</p><p>PMIO</p><p>Port-Mapped I/O &#8212; a mechanism where devices are addressed through a separate 64 KB I/O address space, accessed via IN/OUT instructions.</p><p>Unreal Mode</p><p>A configuration where the CPU is in real mode but segment registers have 4 GB limits (loaded via a temporary protected-mode switch), allowing access to memory above 1 MB.</p>]]></content:encoded></item><item><title><![CDATA[ACPI Does NOT Enumerate PCIe Devices — Here’s How Firmware & Linux Actually Work Together]]></title><description><![CDATA[Clearing up the most common firmware misconception: PCIe discovery = hardware scan, ACPI = platform configuration]]></description><link>https://gdbplus.substack.com/p/acpi-does-not-enumerate-pcie-devices</link><guid isPermaLink="false">https://gdbplus.substack.com/p/acpi-does-not-enumerate-pcie-devices</guid><dc:creator><![CDATA[gdbplus]]></dc:creator><pubDate>Sun, 16 Aug 2026 02:49:39 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!WHD0!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6e21688-83b4-4766-9770-8a3370b2dad0_1402x1057.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!WHD0!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6e21688-83b4-4766-9770-8a3370b2dad0_1402x1057.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!WHD0!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6e21688-83b4-4766-9770-8a3370b2dad0_1402x1057.png 424w, /__u/substackcdn.com/image/fetch/$s_!WHD0!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6e21688-83b4-4766-9770-8a3370b2dad0_1402x1057.png 848w, /__u/substackcdn.com/image/fetch/$s_!WHD0!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6e21688-83b4-4766-9770-8a3370b2dad0_1402x1057.png 1272w, /__u/substackcdn.com/image/fetch/$s_!WHD0!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6e21688-83b4-4766-9770-8a3370b2dad0_1402x1057.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!WHD0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6e21688-83b4-4766-9770-8a3370b2dad0_1402x1057.png" width="1402" height="1057" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b6e21688-83b4-4766-9770-8a3370b2dad0_1402x1057.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1057,&quot;width&quot;:1402,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1833263,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://gdbplus.substack.com/i/211373980?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6e21688-83b4-4766-9770-8a3370b2dad0_1402x1057.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!WHD0!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6e21688-83b4-4766-9770-8a3370b2dad0_1402x1057.png 424w, /__u/substackcdn.com/image/fetch/$s_!WHD0!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6e21688-83b4-4766-9770-8a3370b2dad0_1402x1057.png 848w, /__u/substackcdn.com/image/fetch/$s_!WHD0!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6e21688-83b4-4766-9770-8a3370b2dad0_1402x1057.png 1272w, /__u/substackcdn.com/image/fetch/$s_!WHD0!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6e21688-83b4-4766-9770-8a3370b2dad0_1402x1057.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>ACPI usually <strong>does not directly enumerate ordinary PCIe devices</strong>. The key distinction is:</p><blockquote><p><strong>PCI/PCIe enumeration discovers the devices; ACPI describes the PCIe topology and platform-specific resources/operations to Linux.</strong></p></blockquote><p>For an onboard PCIe device, the typical boot flow is:</p><pre><code><code>             BIOS / UEFI
                 &#9474;
        PCIe Root Complex
                 &#9474;
        &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9524;&#9472;&#9472;&#9472;&#9472;&#9488;
        &#9474;                  &#9474;
   PCIe Root Port      PCIe Root Port
        &#9474;                  &#9474;
     NVMe / NIC          Wi-Fi
        &#9474;
        &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
                       &#9474;
                 PCI configuration
                 space / BARs
                       &#9474;
                    Linux</code></code></pre><h3>1. What ACPI tells Linux</h3><p>ACPI tables such as <strong>DSDT/SSDT</strong> describe the PCI host bridge and its relationship to the PCI hierarchy.</p><p>A simplified ACPI namespace might look like:</p><pre><code><code>\_SB
 &#9492;&#9472;&#9472; PCI0                 &#8592; PCI Host Bridge
      &#9474;
      &#9500;&#9472;&#9472; _HID = PNP0A08
      &#9474;
      &#9500;&#9472;&#9472; _SEG = 0
      &#9500;&#9472;&#9472; _BBN = 0
      &#9474;
      &#9500;&#9472;&#9472; _CRS            &#8592; PCIe MMIO / I/O / bus resources
      &#9474;
      &#9500;&#9472;&#9472; RP01            &#8592; Root Port
      &#9474;    &#9492;&#9472;&#9472; ...
      &#9474;
      &#9492;&#9472;&#9472; RP02
           &#9492;&#9472;&#9472; ...</code></code></pre><p>For example:</p><pre><code><code>Device (PCI0)
{
    Name (_HID, EisaId ("PNP0A08"))
    Name (_CID, EisaId ("PNP0A03"))

    Name (_SEG, Zero)
    Name (_BBN, Zero)

    Method (_CRS, 0, Serialized)
    {
        ...
        // PCIe MMIO windows
        // PCI bus number ranges
        // I/O ranges
    }
}</code></code></pre><p>Linux uses this information to create the PCI host bridge.</p><div><hr></div><h2>2. But where does the actual PCIe device come from?</h2><p>Suppose the motherboard has an onboard Ethernet controller:</p><pre><code><code>AMD/Intel CPU
     &#9474;
     &#9474; PCIe
     &#9660;
Root Port 00:01.0
     &#9474;
     &#9660;
Ethernet Controller
01:00.0</code></code></pre><p>The BIOS doesn&#8217;t normally need an ACPI device like:</p><pre><code><code>Device (ETH0)
{
    Name (_HID, "PCI....")
}</code></code></pre><p>Instead, Linux performs <strong>PCI configuration-space enumeration</strong>.</p><p>Linux essentially does:</p><pre><code><code>PCI bus 00
   &#9474;
   &#9500;&#9472;&#9472; device 00:00.0
   &#9474;
   &#9500;&#9472;&#9472; device 00:01.0
   &#9474;       &#9474;
   &#9474;       &#9492;&#9472;&#9472; PCIe secondary bus 01
   &#9474;               &#9474;
   &#9474;               &#9492;&#9472;&#9472; 01:00.0 Ethernet
   &#9474;
   &#9492;&#9472;&#9472; device 00:02.0</code></code></pre><p>Linux reads:</p><pre><code><code>Vendor ID
Device ID
Class Code
Header Type
BARs
Capabilities
PCIe Capability
MSI/MSI-X
Subsystem ID
...</code></code></pre><p>So the fundamental discovery mechanism is <strong>PCI configuration space</strong>, not ACPI.</p><div><hr></div><h1>3. What is ACPI&#8217;s role then?</h1><p>ACPI provides information that PCI configuration space cannot provide or that is platform-specific.</p><p>For example:</p><pre><code><code>                    ACPI
                     &#9474;
        &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
        &#9474;            &#9474;            &#9474;
       _CRS         _PRT         _OSC
        &#9474;            &#9474;            &#9474;
   resources       IRQ        PCIe ownership
        &#9474;
        &#9660;
   PCI Host Bridge
        &#9474;
        &#9660;
PCI configuration-space scan
        &#9474;
        &#9660;
   PCI devices</code></code></pre><p>Important ACPI mechanisms include:</p><h3><code>_CRS</code></h3><p>Describes resources assigned to the host bridge.</p><p>For example:</p><pre><code><code>PCI bus:
    Bus 0-255

PCI MMIO:
    0x80000000 - 0xFFFFFFFF

PCI I/O:
    0x0000 - 0xFFFF</code></code></pre><p>Linux uses this to understand the address space available for PCI devices.</p><div><hr></div><h3><code>_PRT</code></h3><p>Historically used to describe <strong>PCI interrupt routing</strong>.</p><p>For example:</p><pre><code><code>PCI device
    &#9474;
    &#9500;&#9472;&#9472; INTA
    &#9474;
    &#9492;&#9472;&#9472; _PRT
          &#9474;
          &#9660;
        GSI 16
          &#9474;
          &#9660;
        IOAPIC</code></code></pre><p>On modern PCIe systems, MSI/MSI-X often makes <code>_PRT</code> less important for the device itself, but it can still matter for legacy INTx routing.</p><div><hr></div><h3><code>_OSC</code></h3><p>This is particularly important for PCIe.</p><p>It allows the OS and firmware to negotiate control of PCI/PCIe features.</p><p>Conceptually:</p><pre><code><code>Linux
  &#9474;
  &#9474; _OSC
  &#9660;
ACPI firmware
  &#9474;
  &#9500;&#9472;&#9472; PCIe native hotplug
  &#9500;&#9472;&#9472; PME
  &#9500;&#9472;&#9472; AER
  &#9500;&#9472;&#9472; ASPM
  &#9492;&#9472;&#9472; other PCIe capabilities</code></code></pre><div><hr></div><h1>4. What about an onboard PCIe device?</h1><p>This is probably the most important point.</p><p>Imagine a motherboard has an onboard:</p><pre><code><code>Intel/AMD SoC
      &#9474;
      &#9660;
PCIe Root Port
      &#9474;
      &#9660;
Onboard NIC</code></code></pre><p>Firmware may configure:</p><pre><code><code>PCIe Root Port
Bus = 00
Device = 01
Function = 0</code></code></pre><p>and the NIC appears at:</p><pre><code><code>01:00.0</code></code></pre><p>Linux then scans the PCI bus.</p><p>You can see it with:</p><pre><code><code>lspci</code></code></pre><p>For example:</p><pre><code><code>00:01.0 PCI bridge: AMD PCIe Root Port
01:00.0 Ethernet controller: Realtek ...</code></code></pre><p>The NIC&#8217;s:</p><pre><code><code>Vendor ID
Device ID
Class
BAR
PCIe capabilities
MSI-X</code></code></pre><p>come from <strong>PCI configuration space</strong>.</p><p>Not from the DSDT.</p><div><hr></div><h1>5. So why do I sometimes see PCI devices in ACPI?</h1><p>Because ACPI may contain a corresponding <strong>ACPI namespace object for a PCI device or bridge</strong>, especially when the platform needs OS-visible device-specific functionality.</p><p>For example:</p><pre><code><code>ACPI namespace

\_SB.PCI0.RP01
             &#9474;
             &#9492;&#9472;&#9472; Device(...)</code></code></pre><p>Linux can associate ACPI objects with PCI devices using the PCI address:</p><pre><code><code>Segment
Bus
Device
Function</code></code></pre><p>Conceptually:</p><pre><code><code>ACPI Device
   &#9474;
   &#9474; PCI address
   &#9474;
   &#9660;
0000:01:00.0
   &#9474;
   &#9660;
struct pci_dev</code></code></pre><p>The ACPI object can provide additional information such as:</p><pre><code><code>power management
reset methods
GPIO
DSM
hotplug
wake
platform-specific controls</code></code></pre><div><hr></div><h1>6. Very important: <code>_ADR</code></h1><p>One common way ACPI identifies a PCI device is <code>_ADR</code>.</p><p>For example:</p><pre><code><code>Device (DEV0)
{
    Name (_ADR, 0x00010000)
}</code></code></pre><p>The encoding is:</p><pre><code><code>31             16 15              0
+----------------+----------------+
|    Device      |    Function    |
+----------------+----------------+</code></code></pre><p>For example:</p><pre><code><code>Device = 1
Function = 0

_ADR = (1 &lt;&lt; 16) | 0
     = 0x00010000</code></code></pre><p>So ACPI can associate:</p><pre><code><code>ACPI DEV0
    &#9474;
    &#9474; _ADR = 0x00010000
    &#9660;
PCI device
01:00.0</code></code></pre><p>But again, <code>_ADR</code> <strong>doesn&#8217;t cause PCI enumeration</strong>.</p><p>It identifies/augments an already-existing PCI device.</p><div><hr></div><h1>7. The complete Linux boot flow</h1><p>For an onboard PCIe device, think about the process like this:</p><pre><code><code>              BIOS / UEFI
                  &#9474;
                  &#9474; ACPI tables
                  &#9660;
        &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
        &#9474; DSDT / SSDT / MCFG &#9474;
        &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9516;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;
                  &#9474;
                  &#9660;
           Linux ACPI core
                  &#9474;
                  &#9660;
        PCI Host Bridge driver
                  &#9474;
                  &#9474; _CRS
                  &#9474; _OSC
                  &#9474; _PRT
                  &#9660;
             PCI core
                  &#9474;
                  &#9474; scan PCI bus
                  &#9660;
       PCI configuration space
                  &#9474;
       &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
       &#9474;          &#9474;           &#9474;
       &#9660;          &#9660;           &#9660;
    Root Port    NIC        NVMe
    00:01.0    01:00.0     02:00.0
       &#9474;          &#9474;           &#9474;
       &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;
                  &#9660;
          struct pci_dev
                  &#9474;
                  &#9660;
        PCI driver matching
                  &#9474;
                  &#9660;
          Network / NVMe /
          other subsystem</code></code></pre><div><hr></div><h1>8. Where does MCFG fit?</h1><p>For modern PCIe systems, ACPI commonly provides the <strong>MCFG table</strong>.</p><p>It tells Linux where <strong>PCI Express Enhanced Configuration Space (ECAM)</strong> is mapped.</p><p>For example:</p><pre><code><code>MCFG

Segment 0
Bus 0 - 255
ECAM base = 0xE0000000</code></code></pre><p>Then Linux can calculate the configuration-space address.</p><p>Conceptually:</p><pre><code><code>ECAM address =
    base
  + (bus      &lt;&lt; 20)
  + (device   &lt;&lt; 15)
  + (function &lt;&lt; 12)
  + register</code></code></pre><p>So for:</p><pre><code><code>Bus      = 1
Device   = 0
Function = 0
Register = 0x00</code></code></pre><p>Linux accesses:</p><pre><code><code>0xE0000000
+ (1 &lt;&lt; 20)
+ (0 &lt;&lt; 15)
+ (0 &lt;&lt; 12)
+ 0</code></code></pre><p>and reads:</p><pre><code><code>Vendor ID
Device ID</code></code></pre><p>This is how Linux discovers the PCI device.</p><div><hr></div><h1>9. The key distinction</h1><p>For firmware development, I would remember this table:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!4-sk!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e11a48f-050b-4a03-b201-ed7feae60e89_1024x1058.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!4-sk!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e11a48f-050b-4a03-b201-ed7feae60e89_1024x1058.png 424w, /__u/substackcdn.com/image/fetch/$s_!4-sk!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e11a48f-050b-4a03-b201-ed7feae60e89_1024x1058.png 848w, /__u/substackcdn.com/image/fetch/$s_!4-sk!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e11a48f-050b-4a03-b201-ed7feae60e89_1024x1058.png 1272w, /__u/substackcdn.com/image/fetch/$s_!4-sk!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e11a48f-050b-4a03-b201-ed7feae60e89_1024x1058.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!4-sk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e11a48f-050b-4a03-b201-ed7feae60e89_1024x1058.png" width="1024" height="1058" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2e11a48f-050b-4a03-b201-ed7feae60e89_1024x1058.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1058,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:60397,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://gdbplus.substack.com/i/211373980?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e11a48f-050b-4a03-b201-ed7feae60e89_1024x1058.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!4-sk!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e11a48f-050b-4a03-b201-ed7feae60e89_1024x1058.png 424w, /__u/substackcdn.com/image/fetch/$s_!4-sk!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e11a48f-050b-4a03-b201-ed7feae60e89_1024x1058.png 848w, /__u/substackcdn.com/image/fetch/$s_!4-sk!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e11a48f-050b-4a03-b201-ed7feae60e89_1024x1058.png 1272w, /__u/substackcdn.com/image/fetch/$s_!4-sk!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e11a48f-050b-4a03-b201-ed7feae60e89_1024x1058.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div><hr></div><h2>10. A very useful firmware/Linux debugging model</h2><p>When you have an <strong>onboard PCIe device that doesn&#8217;t appear in Linux</strong>, debug it in this order:</p><pre><code><code>BIOS
 &#9474;
 &#9500;&#9472; PCIe Root Port enabled?
 &#9474;
 &#9500;&#9472; PERST# released?
 &#9474;
 &#9500;&#9472; REFCLK present?
 &#9474;
 &#9500;&#9472; device link comes up?
 &#9474;
 &#9500;&#9472; PCIe enumeration in BIOS?
 &#9474;
 &#9500;&#9472; Bus numbers assigned?
 &#9474;
 &#9500;&#9472; BARs assigned?
 &#9474;
 &#9492;&#9472; ACPI tables correct?
          &#9474;
          &#9660;
        Linux
          &#9474;
          &#9500;&#9472; PCI host bridge created?
          &#9474;
          &#9500;&#9472; PCI bus scanned?
          &#9474;
          &#9500;&#9472; lspci sees device?
          &#9474;
          &#9500;&#9472; BAR resources correct?
          &#9474;
          &#9500;&#9472; MSI/MSI-X works?
          &#9474;
          &#9492;&#9472; driver binds?</code></code></pre><p>The most important diagnostic split is:</p><p><strong>If </strong><code>lspci</code><strong> cannot see the device &#8594; investigate PCIe hardware/link/firmware enumeration/host bridge configuration.</strong></p><p><strong>If </strong><code>lspci</code><strong> sees it but the driver doesn&#8217;t work &#8594; investigate PCI config space, BARs, IRQ/MSI, ACPI power/GPIO/DSM, and the Linux driver.</strong></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://gdbplus.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading gdbplus's Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div class="captioned-button-wrap" data-attrs="{&quot;url&quot;:&quot;https://gdbplus.substack.com/p/acpi-does-not-enumerate-pcie-devices?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="CaptionedButtonToDOM"><div class="preamble"><p class="cta-caption">Thanks for reading gdbplus's Substack! This post is public so feel free to share it.</p></div><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://gdbplus.substack.com/p/acpi-does-not-enumerate-pcie-devices?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="ButtonCreateButton"><a class="button primary" href="/__u/gdbplus.substack.com/p/acpi-does-not-enumerate-pcie-devices?utm_source=substack&amp;utm_medium=email&amp;utm_content=share&amp;action=share"><span>Share</span></a></p></div><p></p>]]></content:encoded></item><item><title><![CDATA[🔄 What Really Happens When Linux Reboots? Follow the ACPI FADT → EC RAM Path]]></title><description><![CDATA[When you type:]]></description><link>https://gdbplus.substack.com/p/what-really-happens-when-linux-reboots</link><guid isPermaLink="false">https://gdbplus.substack.com/p/what-really-happens-when-linux-reboots</guid><dc:creator><![CDATA[gdbplus]]></dc:creator><pubDate>Sat, 15 Aug 2026 09:33:16 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!CvJz!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c922c38-05f1-4f60-a14a-f823eaf23b4f_1672x941.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When you type:</p><pre><code><code>reboot</code></code></pre><p>it looks like a simple operation.</p><p>But on an x86 platform, the Linux kernel usually <strong>doesn&#8217;t directly know which hardware register or EC command will reset the system</strong>.</p><p>That hardware-specific knowledge is provided by <strong>BIOS/UEFI through ACPI</strong>.</p><p>The diagram shows an interesting implementation where the platform uses <strong>Embedded Controller (EC) RAM</strong> as part of the ACPI reset mechanism.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!CvJz!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c922c38-05f1-4f60-a14a-f823eaf23b4f_1672x941.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!CvJz!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c922c38-05f1-4f60-a14a-f823eaf23b4f_1672x941.png 424w, /__u/substackcdn.com/image/fetch/$s_!CvJz!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c922c38-05f1-4f60-a14a-f823eaf23b4f_1672x941.png 848w, /__u/substackcdn.com/image/fetch/$s_!CvJz!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c922c38-05f1-4f60-a14a-f823eaf23b4f_1672x941.png 1272w, /__u/substackcdn.com/image/fetch/$s_!CvJz!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c922c38-05f1-4f60-a14a-f823eaf23b4f_1672x941.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!CvJz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c922c38-05f1-4f60-a14a-f823eaf23b4f_1672x941.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0c922c38-05f1-4f60-a14a-f823eaf23b4f_1672x941.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2345334,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://gdbplus.substack.com/i/211285095?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c922c38-05f1-4f60-a14a-f823eaf23b4f_1672x941.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!CvJz!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c922c38-05f1-4f60-a14a-f823eaf23b4f_1672x941.png 424w, /__u/substackcdn.com/image/fetch/$s_!CvJz!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c922c38-05f1-4f60-a14a-f823eaf23b4f_1672x941.png 848w, /__u/substackcdn.com/image/fetch/$s_!CvJz!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c922c38-05f1-4f60-a14a-f823eaf23b4f_1672x941.png 1272w, /__u/substackcdn.com/image/fetch/$s_!CvJz!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c922c38-05f1-4f60-a14a-f823eaf23b4f_1672x941.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div><hr></div><h2>1&#65039;&#8419; BIOS/UEFI defines the reboot interface</h2><p>During BIOS/UEFI development, the platform firmware builds the ACPI tables and publishes them to the OS.</p><p>One important table is the:</p><p><strong>FADT &#8212; Fixed ACPI Description Table</strong></p><p>The FADT can provide reset information such as:</p><pre><code><code>ResetRegister
ResetValue
ResetCommand
ResetStatus</code></code></pre><p>Conceptually, firmware is telling Linux:</p><blockquote><p>&#8220;If you want to reset this platform, use this interface and this value.&#8221;</p></blockquote><p>The important point is that <strong>Linux doesn&#8217;t need to understand the underlying board implementation</strong>.</p><p>The implementation could involve:</p><ul><li><p>EC</p></li><li><p>PMIC</p></li><li><p>PCH/chipset</p></li><li><p>Super I/O</p></li><li><p>GPIO</p></li><li><p>PCI configuration space</p></li><li><p>platform-specific reset logic</p></li></ul><p>ACPI provides the abstraction layer.</p><div><hr></div><h2>2&#65039;&#8419; Where does the FADT come from?</h2><p>The boot flow is roughly:</p><pre><code><code>BIOS / UEFI
     &#9474;
     &#9500;&#9472;&#9472; Build DSDT / SSDT
     &#9500;&#9472;&#9472; Build FADT
     &#9500;&#9472;&#9472; Build MADT / MCFG / HPET ...
     &#9474;
     &#9660;
ACPI Tables in Memory
     &#9474;
     &#9660;
RSDP
     &#9474;
     &#9660;
RSDT / XSDT
     &#9474;
     &#9660;
Linux Kernel</code></code></pre><p>On a UEFI system, the firmware can provide the ACPI table information to the OS through the <strong>EFI configuration table</strong>.</p><p>Linux&#8217;s early ACPI initialization discovers the RSDP, follows the RSDT/XSDT, and eventually locates the FADT.</p><p>This is one of the most important concepts to understand when debugging <strong>UEFI &#8594; Linux platform handoff</strong>.</p><div><hr></div><h2>3&#65039;&#8419; Linux doesn&#8217;t just &#8220;know&#8221; the reset register</h2><p>Once Linux has parsed the FADT, the ACPI subsystem can obtain the reset information.</p><p>The conceptual flow is:</p><pre><code><code>Linux reboot
     &#9474;
     &#9660;
Kernel reboot framework
     &#9474;
     &#9660;
ACPI reset handling
     &#9474;
     &#9660;
Read FADT reset information
     &#9474;
     &#9500;&#9472;&#9472; Reset Register
     &#9500;&#9472;&#9472; Reset Value
     &#9492;&#9472;&#9472; Reset Command
     &#9474;
     &#9660;
Write to platform-defined interface
     &#9474;
     &#9660;
EC
     &#9474;
     &#9660;
Hardware Reset</code></code></pre><p>This is a powerful firmware/OS separation:</p><p><strong>BIOS describes the hardware.<br>Linux consumes the description.</strong></p><div><hr></div><h2>4&#65039;&#8419; Where EC RAM becomes interesting</h2><p>Imagine the platform has an Embedded Controller with a RAM/register area:</p><pre><code><code>EC RAM

0x66 &#9472;&#9472; Reset Register
0x67 &#9472;&#9472; Reset Status
0x68 &#9472;&#9472; Reset Command</code></code></pre><p>Firmware can expose the appropriate reset interface through ACPI.</p><p>Then the EC firmware can implement something like:</p><pre><code><code>Linux writes reset command
        &#9474;
        &#9660;
     EC RAM
        &#9474;
        &#9660;
EC firmware detects command
        &#9474;
        &#9660;
Execute reset sequence
        &#9474;
        &#9500;&#9472;&#9472; Disable / prepare EC functions
        &#9500;&#9472;&#9472; Notify platform
        &#9500;&#9472;&#9472; Assert reset
        &#9492;&#9472;&#9472; Reset system</code></code></pre><p>The actual EC implementation is platform-specific.</p><p>That&#8217;s the key idea:</p><p><strong>ACPI standardizes the interface, while EC firmware implements the hardware behavior.</strong></p><div><hr></div><h2>5&#65039;&#8419; What happens after <code>reboot</code>?</h2><p>A simplified sequence looks like this:</p><pre><code><code>User
 &#9474;
 &#9474;  reboot -r now
 &#9660;
Linux Kernel
 &#9474;
 &#9660;
Reboot framework
 &#9474;
 &#9660;
ACPI subsystem
 &#9474;
 &#9660;
FADT
 &#9474;
 &#9500;&#9472;&#9472; ResetRegister
 &#9500;&#9472;&#9472; ResetValue
 &#9492;&#9472;&#9472; ResetCommand
 &#9474;
 &#9660;
EC interface
 &#9474;
 &#9660;
EC RAM / EC register
 &#9474;
 &#9660;
EC firmware
 &#9474;
 &#9660;
Platform reset
 &#9474;
 &#9660;
CPU starts again
 &#9474;
 &#9660;
BIOS / UEFI</code></code></pre><p>Notice something important:</p><p><strong>Linux doesn&#8217;t need a special driver for every motherboard reset implementation.</strong></p><p>That is exactly the value of ACPI.</p><div><hr></div><h2>6&#65039;&#8419; What does the firmware engineer actually need to implement?</h2><p>From the BIOS/UEFI side, you need to make sure that:</p><h3>ACPI tables are correct</h3><p>The FADT must describe the reset mechanism correctly.</p><h3>The EC implementation matches the ACPI definition</h3><p>If ACPI says:</p><pre><code><code>ResetRegister = X
ResetValue    = Y</code></code></pre><p>the EC/platform hardware must actually respond as expected.</p><h3>The ACPI tables are published correctly</h3><p>The OS must be able to discover:</p><pre><code><code>RSDP
  &#8595;
XSDT
  &#8595;
FADT</code></code></pre><h3>The reset sequence is safe</h3><p>The platform must handle the transition from:</p><pre><code><code>Linux running
      &#8595;
shutdown/reboot preparation
      &#8595;
EC reset command
      &#8595;
platform reset</code></code></pre><div><hr></div><h2>7&#65039;&#8419; How do you debug this on Linux?</h2><p>Start by dumping the ACPI tables:</p><pre><code><code>sudo acpidump &gt; acpi.dat</code></code></pre><p>Extract the tables:</p><pre><code><code>acpixtract -a acpi.dat</code></code></pre><p>Disassemble AML:</p><pre><code><code>iasl -d DSDT.dat</code></code></pre><p>Check the kernel&#8217;s ACPI messages:</p><pre><code><code>dmesg | grep -i acpi</code></code></pre><p>Inspect tables exposed by Linux:</p><pre><code><code>ls /sys/firmware/acpi/tables/</code></code></pre><p>For example:</p><pre><code><code>DSDT
SSDT1
SSDT2
FACP
APIC
MCFG
HPET
...</code></code></pre><p>You can then compare:</p><pre><code><code>BIOS source
     &#8595;
Compiled ACPI table
     &#8595;
Binary table in firmware
     &#8595;
/sys/firmware/acpi/tables/
     &#8595;
Linux ACPI parser</code></code></pre><p>This is an extremely useful workflow when debugging a new platform.</p><div><hr></div><h2>8&#65039;&#8419; The most important firmware lesson</h2><p>When something like reboot, shutdown, thermal management, sleep, or device enumeration doesn&#8217;t work under Linux, don&#8217;t immediately assume:</p><blockquote><p>&#8220;Linux needs a new driver.&#8221;</p></blockquote><p>Sometimes the real problem is much earlier:</p><pre><code><code>Hardware
   &#8595;
EC firmware
   &#8595;
BIOS/UEFI
   &#8595;
ACPI tables
   &#8595;
Linux ACPI subsystem
   &#8595;
Linux driver / subsystem</code></code></pre><p>A mistake in <strong>any layer</strong> can produce an OS-level symptom.</p><p>For example:</p><pre><code><code>Linux reboot fails
       &#9474;
       &#9500;&#9472;&#9472; Kernel problem?
       &#9500;&#9472;&#9472; ACPI parser?
       &#9500;&#9472;&#9472; Wrong FADT?
       &#9500;&#9472;&#9472; Wrong reset register?
       &#9500;&#9472;&#9472; Wrong reset value?
       &#9500;&#9472;&#9472; EC RAM mapping?
       &#9500;&#9472;&#9472; EC firmware?
       &#9492;&#9472;&#9472; Platform reset hardware?</code></code></pre><p>This is why ACPI debugging is a valuable skill for both <strong>UEFI engineers and Linux kernel engineers</strong>.</p><div><hr></div><h2>&#128273; The big picture</h2><p>ACPI is more than a collection of tables.</p><p>It is a <strong>contract between firmware and the operating system</strong>.</p><p>For reboot:</p><pre><code><code>BIOS/UEFI
  "Here is the reset interface."
           &#9474;
           &#9660;
        ACPI FADT
           &#9474;
           &#9660;
     Linux ACPI Core
  "I understand the interface."
           &#9474;
           &#9660;
       EC / Hardware
  "I implement the reset."
           &#9474;
           &#9660;
        REBOOT &#128260;</code></code></pre><p>The same philosophy extends far beyond reboot&#8212;to <strong>CPU P-states/C-states, thermal zones, power buttons, sleep states, GPIO events, device enumeration, PCI resources, EC communication, and platform-specific control methods</strong>.</p><p>For firmware engineers moving into Linux, understanding this <strong>firmware &#8594; ACPI &#8594; kernel subsystem &#8594; hardware</strong> chain is one of the best ways to connect UEFI knowledge with Linux driver development.</p><p><strong>If you&#8217;re debugging a Linux platform issue, always ask:<br>&#8220;What did the firmware actually tell Linux?&#8221;</strong></p><p>#ACPI #UEFI #BIOS #Linux #LinuxKernel #Firmware #EDK2 #EmbeddedController #EC #PlatformFirmware #KernelDevelopment #EmbeddedSystems #SystemArchitecture #LinuxDriver #FirmwareEngineering #x86 #ACPI</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://gdbplus.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading gdbplus's Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div class="captioned-button-wrap" data-attrs="{&quot;url&quot;:&quot;https://gdbplus.substack.com/p/what-really-happens-when-linux-reboots?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="CaptionedButtonToDOM"><div class="preamble"><p class="cta-caption">Thanks for reading gdbplus's Substack! This post is public so feel free to share it.</p></div><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://gdbplus.substack.com/p/what-really-happens-when-linux-reboots?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="ButtonCreateButton"><a class="button primary" href="/__u/gdbplus.substack.com/p/what-really-happens-when-linux-reboots?utm_source=substack&amp;utm_medium=email&amp;utm_content=share&amp;action=share"><span>Share</span></a></p></div>]]></content:encoded></item><item><title><![CDATA[How BIOS Builds ACPI Tables — and How Linux Consumes Them]]></title><description><![CDATA[David Zhu]]></description><link>https://gdbplus.substack.com/p/how-bios-builds-acpi-tables-and-how</link><guid isPermaLink="false">https://gdbplus.substack.com/p/how-bios-builds-acpi-tables-and-how</guid><dc:creator><![CDATA[gdbplus]]></dc:creator><pubDate>Fri, 14 Aug 2026 12:16:49 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Rksa!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8be5bdef-a886-4867-b5a1-28bbc2b90f00_1536x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote><p><strong>David Zhu</strong></p></blockquote><div><hr></div><blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!Rksa!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8be5bdef-a886-4867-b5a1-28bbc2b90f00_1536x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!Rksa!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8be5bdef-a886-4867-b5a1-28bbc2b90f00_1536x1024.png 424w, /__u/substackcdn.com/image/fetch/$s_!Rksa!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8be5bdef-a886-4867-b5a1-28bbc2b90f00_1536x1024.png 848w, /__u/substackcdn.com/image/fetch/$s_!Rksa!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8be5bdef-a886-4867-b5a1-28bbc2b90f00_1536x1024.png 1272w, /__u/substackcdn.com/image/fetch/$s_!Rksa!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8be5bdef-a886-4867-b5a1-28bbc2b90f00_1536x1024.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!Rksa!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8be5bdef-a886-4867-b5a1-28bbc2b90f00_1536x1024.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8be5bdef-a886-4867-b5a1-28bbc2b90f00_1536x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1772389,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://gdbplus.substack.com/i/211170535?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8be5bdef-a886-4867-b5a1-28bbc2b90f00_1536x1024.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!Rksa!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8be5bdef-a886-4867-b5a1-28bbc2b90f00_1536x1024.png 424w, /__u/substackcdn.com/image/fetch/$s_!Rksa!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8be5bdef-a886-4867-b5a1-28bbc2b90f00_1536x1024.png 848w, /__u/substackcdn.com/image/fetch/$s_!Rksa!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8be5bdef-a886-4867-b5a1-28bbc2b90f00_1536x1024.png 1272w, /__u/substackcdn.com/image/fetch/$s_!Rksa!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8be5bdef-a886-4867-b5a1-28bbc2b90f00_1536x1024.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Every modern x86 system runs on ACPI. It is the interface that lets the OS stop guessing about the hardware and start reading a machine-readable description of it: what CPUs exist, where the APIC is, how to put the machine to sleep, and where every PCIe device lives. The BIOS builds these tables at boot. Linux parses them to bring up the platform.</p><p>But the handoff is rarely explained end to end. What happens between firmware composing a DSDT and the kernel&#8217;s acpi_boot_table_init() finding the RSDP?</p><p>Let me walk the full pipeline &#8212; from ASL source to /sys/firmware/acpi/tables/.</p></blockquote><div><hr></div><h2><strong>Part 1: The ACPI Table Family</strong></h2><blockquote><p>Before the flow, a quick map of the tables that matter. They are a directed graph rooted at a single pointer.</p><p>&#9656; <strong>RSDP &#8212; Root System Description Pointer.</strong> The entry point. A small structure with the signature <code>"RSD PTR "</code> that contains the physical address of the RSDT (32-bit) and/or XSDT (64-bit). Everything else is reachable from here.</p><p>&#9656; <strong>RSDT / XSDT &#8212; Root/Extended System Description Table.</strong> Arrays of pointers to every other ACPI table. RSDT holds 32-bit addresses, XSDT holds 64-bit. Modern systems use XSDT.</p><p>&#9656; <strong>FADT (FACP) &#8212; Fixed ACPI Description Table.</strong> Power management: fixed hardware register blocks, PM timer, sleep state (S3/S5) control, reset register.</p><p>&#9656; <strong>MADT (APIC) &#8212; Multiple APIC Description Table.</strong> CPU and interrupt controller topology: LAPIC entries, IOAPIC entries, interrupt source overrides.</p><p>&#9656; <strong>DSDT &#8212; Differentiated System Description Table.</strong> The primary device tree, written in AML bytecode. The namespace root &#8212; <code>\_SB</code>, <code>\_PR</code>, <code>\_GPE</code> &#8212; lives here.</p><p>&#9656; <strong>SSDT &#8212; Secondary System Description Table.</strong> Additional AML. CPU hotplug, device descriptions, and OEM-specific objects that extend the DSDT namespace.</p><p>&#9656; <strong>MCFG &#8212; PCIe ECAM table.</strong> Memory-mapped configuration space base addresses and segment groups for PCIe.</p><p>The critical detail: <strong>DSDT and SSDT are bytecode, not data structures.</strong> They are written in ASL (ACPI Source Language), compiled to AML (ACPI Machine Language) with iasl, and interpreted by the OS at runtime. The fixed tables (FADT, MADT, MCFG) are plain C structures.</p></blockquote><div><hr></div><h2><strong>Part 2: BIOS Side &#8212; Building the Tables</strong></h2><h3><strong>The AcpiTableDxe Driver</strong></h3><blockquote><p>On EDK2, the core driver lives at MdeModulePkg/Universal/Acpi/AcpiTableDxe/. It produces the EFI_ACPI_TABLE_PROTOCOL &#8212; the interface platform code uses to register ACPI tables.</p><p>From AcpiTableDxe.c, the driver maintains an internal linked list of all tables (EFI_ACPI_TABLE_LIST). The key entry points are InstallAcpiTable() and UninstallAcpiTable(). The protocol also exposes GetAcpiTable(), which the OS loader can use to query the raw table data.</p></blockquote><h3><strong>How Platform Code Registers Tables</strong></h3><blockquote><p>Platform code &#8212; a dedicated DXE driver, or in many AMD/Intel reference designs the AcpiPlatform driver &#8212; composes each table and calls InstallAcpiTable():</p><p>&#9656; <strong>FADT</strong> &#8212; built as a C struct (EFI_ACPI_2_0_FIXED_ACPI_DESCRIPTION_TABLE), fields filled with the platform&#8217;s power management configuration.</p><p>&#9656; <strong>MADT</strong> &#8212; built dynamically based on the number of CPUs discovered during PEI/DXE, one LAPIC entry per core.</p><p>&#9656; <strong>DSDT/SSDT</strong> &#8212; the AML blobs produced at build time by iasl, embedded into the firmware image as binary arrays and installed verbatim.</p><p>Each InstallAcpiTable() call:</p></blockquote><ol><li><p>Validates the table&#8217;s 4-byte signature and checksum</p></li><li><p>Allocates a copy in EfiACPIReclaimMemory (a memory type the OS can safely reclaim after boot)</p></li><li><p>Appends the table to the driver&#8217;s internal list</p></li><li><p>Optionally triggers a rebuild of the RSDT/XSDT</p></li></ol><h3><strong>Finalization: Publishing the RSDP</strong></h3><blockquote><p>When boot reaches the point of OS handoff, the firmware finalizes the root tables and publishes the RSDP:</p><p>// From AcpiTableDxe &#8212; install the root pointer into the EFI config table Status = gBS-&gt;InstallConfigurationTable ( &amp;gEfiAcpi20TableGuid, // ACPI 2.0+ (XSDT) AcpiTableInstance-&gt;Rsdp1 );</p><p>For ACPI 1.0 compatibility, the driver also installs gEfiAcpiTableGuid pointing to the RSDT-based RSDP.</p><p>The RSDP itself is a small structure:</p><p>struct RSDP { char Signature[8]; // &#8220;RSD PTR &#8220; uint8_t Checksum; char OemId[6]; uint8_t Revision; // 0 = ACPI 1.0, 2 = ACPI 2.0+ uint32_t RsdtAddress; // 32-bit RSDT (always present) uint32_t Length; // RSDP length (ACPI 2.0+) uint64_t XsdtAddress; // 64-bit XSDT (ACPI 2.0+) uint8_t ExtendedChecksum; uint8_t Reserved[3]; };</p><p>The XSDT then points to every other table by 64-bit physical address.</p><p><strong>The handoff mechanism differs by boot path:</strong></p><p>&#9656; <strong>UEFI systems:</strong> The RSDP lives in the EFI Configuration Table. The OS loader (or the kernel&#8217;s EFI stub) reads it directly &#8212; no scanning.</p><p>&#9656; <strong>Legacy BIOS systems:</strong> The firmware writes the RSDP into the Extended BIOS Data Area (EBDA) or the top 128 KB of system memory (0x000E0000&#8211;0x000FFFFF). The OS finds it by scanning for the <code>"RSD PTR "</code> signature on a 16-byte boundary.</p><p>Both mechanisms target the same result: the OS ends up holding the RSDP&#8217;s XsdtAddress.</p></blockquote><div><hr></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!Pf_J!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f0ef85e-9d29-4135-b734-b64b156d1b09_2560x1440.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!Pf_J!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f0ef85e-9d29-4135-b734-b64b156d1b09_2560x1440.png 424w, /__u/substackcdn.com/image/fetch/$s_!Pf_J!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f0ef85e-9d29-4135-b734-b64b156d1b09_2560x1440.png 848w, /__u/substackcdn.com/image/fetch/$s_!Pf_J!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f0ef85e-9d29-4135-b734-b64b156d1b09_2560x1440.png 1272w, /__u/substackcdn.com/image/fetch/$s_!Pf_J!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f0ef85e-9d29-4135-b734-b64b156d1b09_2560x1440.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!Pf_J!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f0ef85e-9d29-4135-b734-b64b156d1b09_2560x1440.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8f0ef85e-9d29-4135-b734-b64b156d1b09_2560x1440.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:447855,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://gdbplus.substack.com/i/211170535?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f0ef85e-9d29-4135-b734-b64b156d1b09_2560x1440.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!Pf_J!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f0ef85e-9d29-4135-b734-b64b156d1b09_2560x1440.png 424w, /__u/substackcdn.com/image/fetch/$s_!Pf_J!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f0ef85e-9d29-4135-b734-b64b156d1b09_2560x1440.png 848w, /__u/substackcdn.com/image/fetch/$s_!Pf_J!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f0ef85e-9d29-4135-b734-b64b156d1b09_2560x1440.png 1272w, /__u/substackcdn.com/image/fetch/$s_!Pf_J!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f0ef85e-9d29-4135-b734-b64b156d1b09_2560x1440.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2><strong>Part 3: Linux Side &#8212; Reading the Tables</strong></h2><h3><strong>Early Boot: Finding the RSDP</strong></h3><blockquote><p>On x86, acpi_boot_table_init() runs very early &#8212; before PCI, before SMP. From drivers/acpi/tables.c:</p><p>acpi_status __init acpi_boot_table_init(void) { acpi_status status; status = acpi_initialize_tables(...); ... }</p><p>The RSDP discovery follows this order:</p><p>&#9656; <strong>UEFI systems:</strong> The kernel reads efi.acpi20 (or efi.acpi) from the EFI System Table, which contains the RSDP physical address directly.</p><p>&#9656; <strong>Legacy systems:</strong> acpi_find_root_pointer() scans low memory &#8212; first the EBDA, then the 0xE0000&#8211;0xFFFFF range &#8212; looking for the <code>"RSD PTR "</code> signature. On a hit it validates the checksum and revision.</p><p>Once the RSDP is found, the kernel extracts XsdtAddress, maps the XSDT into kernel virtual address space via early_ioremap(), and validates it.</p></blockquote><h3><strong>Walking the XSDT</strong></h3><blockquote><p>acpi_tb_parse_root_table() iterates the XSDT entries. Each entry is a 64-bit physical address of a table header. Every table found is stored in acpi_gbl_root_table_list &#8212; the kernel&#8217;s master index of ACPI tables, keyed by signature (DSDT, FADT, MADT, SSDT, MCFG, and dozens more).</p><p>At this point nothing is interpreted yet. The kernel has only catalogued the tables.</p></blockquote><h3><strong>The AML Interpreter</strong></h3><blockquote><p>The interesting part is what happens to DSDT/SSDT. The kernel&#8217;s AML interpreter (drivers/acpi/acpica/) loads the DSDT bytecode, builds the ACPI namespace, then loads each SSDT and merges it in.</p><p>The namespace is a tree of objects:</p><p>&#9656; <strong>_SB</strong> &#8212; system bus, contains PCI root bridges and devices</p><p>&#9656; <strong>_PR</strong> &#8212; processor objects</p><p>&#9656; <strong>_GPE</strong> &#8212; general-purpose events (sci/notify sources)</p><p>&#9656; <strong>_S0&#8211;_S5</strong> &#8212; sleep state objects</p><p>Devices appear with ACPI Hardware IDs like PNP0C0D (lid button), PNP0C09 (embedded controller), or PNP0A03 (PCI host bridge). The ACPI driver enumerates these and creates the corresponding platform devices. Standard Linux drivers then bind to them exactly as they would to a PCI or platform device.</p></blockquote><h3><strong>Sysfs Export</strong></h3><blockquote><p>The kernel exposes every raw table as a binary file under /sys/firmware/acpi/tables/:</p><p>&#9656; <strong>DSDT</strong> &#8212; the primary AML table</p><p>&#9656; <strong>SSDT</strong>* &#8212; secondary AML tables (one file each)</p><p>&#9656; <strong>FACP</strong> &#8212; the FADT</p><p>&#9656; <strong>APIC</strong> &#8212; the MADT</p><p>&#9656; <strong>MCFG</strong> &#8212; PCIe ECAM</p><p>&#9656; <strong>XSDT</strong> &#8212; the root table itself</p></blockquote><h3><strong>Userspace Tools</strong></h3><blockquote><p>&#9656; <strong>acpidump</strong> &#8212; dumps all raw ACPI tables into one binary</p><p>&#9656; <strong>acpixtract</strong> &#8212; extracts individual tables from an acpidump</p><p>&#9656; <strong>iasl -d</strong> &#8212; disassembles an AML table back to readable ASL (the standard way to debug a misbehaving DSDT)</p><p>&#9656; <strong>dmesg | grep ACPI</strong> &#8212; the kernel logs every table it initializes, which is the first thing to check when ACPI misbehaves</p><p>Try this on any Linux box right now:</p><p>ls /sys/firmware/acpi/tables/ acpidump | acpixtract # or: cat /sys/firmware/acpi/tables/DSDT &gt; /tmp/dsdt.aml iasl -d /tmp/dsdt.aml # read the actual device tree of your machine</p></blockquote><div><hr></div><h2><strong>The Full Flow in Summary</strong></h2><ol><li><p>Platform ASL source is compiled to AML (DSDT/SSDT) with iasl at build time</p></li><li><p>Fixed tables (FADT, MADT, MCFG) are built as C structs at runtime</p></li><li><p>AcpiTableDxe installs EFI_ACPI_TABLE_PROTOCOL; platform code calls InstallAcpiTable() per table</p></li><li><p>Firmware builds the RSDP and publishes it via InstallConfigurationTable() (UEFI) or the EBDA/0xE0000 region (legacy)</p></li><li><p>Linux&#8217;s acpi_boot_table_init() reads the RSDP from the EFI config table or scans for &#8220;RSD PTR &#8220;</p></li><li><p>The kernel walks the XSDT, catalogues every table in acpi_gbl_root_table_list</p></li><li><p>The AML interpreter loads DSDT/SSDT and builds the ACPI namespace</p></li><li><p>Drivers bind to devices found in the namespace; raw tables are exposed at /sys/firmware/acpi/tables/</p></li></ol><blockquote><p>The elegant part: a single 8-byte signature string is the root of a graph that describes the entire machine. The BIOS writes a description; Linux reads it and brings the platform to life. No probing, no guessing, no hardcoded addresses &#8212; just a well-defined handoff that has scaled from 1996 laptops to today&#8217;s multi-socket servers.</p></blockquote><div><hr></div><blockquote><p>#firmware #uefi #edk2 #acpi #linuxkernel #bios #embedded #x86 #gdbplus</p></blockquote>]]></content:encoded></item><item><title><![CDATA[How the UEFI Timer Architectural Protocol Works in EDK2 — From Protocol Definition to Hardware Interrupt]]></title><description><![CDATA[Thanks for reading gdbplus's Substack!]]></description><link>https://gdbplus.substack.com/p/how-the-uefi-timer-architectural</link><guid isPermaLink="false">https://gdbplus.substack.com/p/how-the-uefi-timer-architectural</guid><dc:creator><![CDATA[gdbplus]]></dc:creator><pubDate>Thu, 23 Jul 2026 12:27:05 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!tqIW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89f185f2-1871-409d-8e3c-579183824593_2560x1440.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p></p><p></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://gdbplus.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading gdbplus's Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!tqIW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89f185f2-1871-409d-8e3c-579183824593_2560x1440.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!tqIW!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89f185f2-1871-409d-8e3c-579183824593_2560x1440.png 424w, /__u/substackcdn.com/image/fetch/$s_!tqIW!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89f185f2-1871-409d-8e3c-579183824593_2560x1440.png 848w, /__u/substackcdn.com/image/fetch/$s_!tqIW!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89f185f2-1871-409d-8e3c-579183824593_2560x1440.png 1272w, /__u/substackcdn.com/image/fetch/$s_!tqIW!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89f185f2-1871-409d-8e3c-579183824593_2560x1440.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!tqIW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89f185f2-1871-409d-8e3c-579183824593_2560x1440.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/89f185f2-1871-409d-8e3c-579183824593_2560x1440.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:354343,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://gdbplus.substack.com/i/208190465?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89f185f2-1871-409d-8e3c-579183824593_2560x1440.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!tqIW!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89f185f2-1871-409d-8e3c-579183824593_2560x1440.png 424w, /__u/substackcdn.com/image/fetch/$s_!tqIW!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89f185f2-1871-409d-8e3c-579183824593_2560x1440.png 848w, /__u/substackcdn.com/image/fetch/$s_!tqIW!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89f185f2-1871-409d-8e3c-579183824593_2560x1440.png 1272w, /__u/substackcdn.com/image/fetch/$s_!tqIW!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89f185f2-1871-409d-8e3c-579183824593_2560x1440.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>When you call <code>gBS-&gt;Stall(1000)</code> in a DXE driver, a lot happens before that microsecond delay returns. But the timer that powers event-based timing &#8212; <code>SetTimer()</code>, <code>EVT_TIMER</code> events, the firmware&#8217;s heartbeat &#8212; is an entirely different mechanism. That mechanism is the Timer Architectural Protocol.</p><p>It&#8217;s one of only six Architectural Protocols the DXE Core requires to boot. Without it, the dispatcher halts with a fatal assertion. And yet it&#8217;s one of the simplest protocols in the PI specification: four function pointers, one handler slot, and a whole lot of trust placed in the platform to get the hardware programming right.</p><p>Let&#8217;s walk through the entire stack, from the protocol definition in <code>MdePkg</code> down to the CPU interrupt vector.</p><h2><strong>The Protocol: EFI_TIMER_ARCH_PROTOCOL</strong></h2><p>Defined in <code>MdePkg/Include/Protocol/Timer.h</code>, the protocol is astonishingly minimal:</p><pre><code><code>struct _EFI_TIMER_ARCH_PROTOCOL {
  EFI_TIMER_REGISTER_HANDLER        RegisterHandler;
  EFI_TIMER_SET_TIMER_PERIOD        SetTimerPeriod;
  EFI_TIMER_GET_TIMER_PERIOD        GetTimerPeriod;
  EFI_TIMER_GENERATE_SOFT_INTERRUPT GenerateSoftInterrupt;
};
</code></code></pre><p>That&#8217;s it. Four function pointers. The GUID is <code>{ 0x26baccb3, 0x6f42, 0x11d4, {0xbc, 0xe7, 0x0, 0x80, 0xc7, 0x3c, 0x88, 0x81} }</code>.</p><p>The protocol operates in units of <strong>100 nanoseconds</strong>. A <code>TimerPeriod</code> of 100000 means &#8220;fire every 10 milliseconds.&#8221; The tick callback &#8212; <code>EFI_TIMER_NOTIFY</code> &#8212; receives a <code>UINT64 Time</code> parameter telling it how much time elapsed since the last tick (in 100ns units). If the hardware can detect missed interrupts, <code>Time</code> may be larger than <code>TimerPeriod</code>.</p><h3><strong>The Four Services</strong></h3><p>&#9656; <strong>RegisterHandler(NotifyFunction)</strong>: Register or unregister the interrupt callback. Only ONE handler can be registered at a time &#8212; if one is already registered, you get <code>EFI_ALREADY_STARTED</code>. Pass <code>NULL</code> to unregister. The callback runs at <code>TPL_HIGH_LEVEL</code>, the highest task priority level in UEFI.</p><p>&#9656; <strong>SetTimerPeriod(TimerPeriod)</strong>: Program the hardware timer frequency in 100ns units. The implementation rounds up to the nearest supported period. Pass <code>0</code> to <strong>disable</strong> timer interrupts entirely &#8212; this is not the same as <code>CLI</code>. The hardware stops generating interrupts, but the CPU&#8217;s interrupt flag is unaffected.</p><p>&#9656; <strong>GetTimerPeriod(*TimerPeriod)</strong>: Return the current period. Returns <code>0</code> if the timer is disabled.</p><p>&#9656; <strong>GenerateSoftInterrupt()</strong>: Fire a software-generated timer interrupt. The registered handler executes exactly as if hardware triggered it &#8212; the handler cannot distinguish between a real interrupt and a soft one. Returns <code>EFI_UNSUPPORTED</code> if the platform doesn&#8217;t support software timer interrupts.</p><h2><strong>The Hardware Layer: Three Backends, One Protocol</strong></h2><p>EDK2 ships three implementations for different platforms. The DXE Core never knows which one it&#8217;s talking to &#8212; it just locates the protocol GUID and starts calling.</p><h3><strong>1. HPET Timer Dxe (PcAtChipsetPkg/HpetTimerDxe)</strong></h3><p>The High Precision Event Timer is the modern x86 standard. Key facts:</p><p>&#9656; <strong>Counter</strong>: 64-bit main counter, minimum 10 MHz clock, monotonically increasing<br>&#9656; <strong>Comparators</strong>: Up to 32 independent timer comparators; the driver uses one<br>&#9656; <strong>Periodic mode</strong>: The comparator is set to fire at a fixed interval<br>&#9656; <strong>MSI support</strong>: Can deliver interrupts via MSI (Message Signaled Interrupt) if <code>PcdHpetMsiEnable</code> is TRUE &#8212; bypasses the I/O APIC entirely<br>&#9656; <strong>PCDs consumed</strong>: <code>PcdHpetBaseAddress</code>, <code>PcdHpetLocalApicVector</code>, <code>PcdHpetDefaultTimerPeriod</code></p><p>The HPET registers are memory-mapped at the address in the ACPI HPET table. The driver uses <code>IoLib</code> to program the General Configuration register, the Main Counter Value register, and the Timer N Comparator Value register.</p><h3><strong>2. Local APIC Timer Dxe (OvmfPkg/LocalApicTimerDxe)</strong></h3><p>Used in virtualized environments (OVMF, QEMU). The local APIC timer is per-CPU, simpler than HPET, and doesn&#8217;t require ACPI table parsing.</p><p>&#9656; <strong>Clock source</strong>: FSB (Front-Side Bus) clock, divided by a configurable divider (1, 2, 4, 8, 16, 32, 64, or 128)<br>&#9656; <strong>Frequency calculation</strong>: <code>TimerCount = (TimerPeriod &#215; PcdFSBClock) / 10,000,000</code><br>&#9656; <strong>Overflow guard</strong>: If <code>TimerCount &gt; MAX_UINT32</code>, clamped to <code>MAX_UINT32</code> with <code>TimerPeriod = 429,496,730</code><br>&#9656; <strong>One-shot mode</strong>: The APIC timer is programmed in one-shot mode &#8212; the interrupt handler re-arms it on every tick<br>&#9656; <strong>Vector</strong>: <code>LOCAL_APIC_TIMER_VECTOR</code> &#8212; a fixed interrupt vector assigned by the platform</p><p>The initialization sequence in <code>TimerDriverInitialize()</code>:</p><pre><code><code>// 1. Assert protocol not already installed
ASSERT_PROTOCOL_ALREADY_INSTALLED(NULL, &amp;gEfiTimerArchProtocolGuid);

// 2. Locate CPU Arch Protocol (needed for interrupt registration)
Status = gBS-&gt;LocateProtocol(&amp;gEfiCpuArchProtocolGuid, NULL, (VOID **)&amp;mCpu);

// 3. Force timer disabled initially
Status = TimerDriverSetTimerPeriod(&amp;mTimer, 0);

// 4. Register interrupt handler
Status = mCpu-&gt;RegisterInterruptHandler(mCpu, LOCAL_APIC_TIMER_VECTOR,
                                         TimerInterruptHandler);

// 5. Enable timer at default period
Status = TimerDriverSetTimerPeriod(&amp;mTimer, DEFAULT_TIMER_TICK_DURATION);

// 6. Install Timer Arch Protocol
Status = gBS-&gt;InstallMultipleProtocolInterfaces(&amp;mTimerHandle,
            &amp;gEfiTimerArchProtocolGuid, &amp;mTimer, NULL);
</code></code></pre><p>The interrupt handler itself is straightforward. On each tick:</p><p>&#9656; Send EOI (End of Interrupt) to the local APIC<br>&#9656; Check if a notify function is registered<br>&#9656; Call it, passing <code>mTimerPeriod</code> as the time delta<br>&#9656; The handler uses <code>NestedInterruptRestoreTPL</code> to handle nested interrupts correctly</p><h3><strong>3. ARM Generic Timer Dxe (ArmPkg/Drivers/TimerDxe)</strong></h3><p>ARM platforms use the architectural generic timer, present in all ARMv7-A and ARMv8-A cores.</p><p>&#9656; <strong>System counter</strong>: 56-64 bit counter, fixed frequency (typically 1-50 MHz), accessible via <code>CNTPCT_EL0</code> (physical) or <code>CNTVCT_EL0</code> (virtual)<br>&#9656; <strong>Timer registers</strong>: <code>CNTP_TVAL_EL0</code> (physical timer value), <code>CNTP_CTL_EL0</code> (physical timer control)<br>&#9656; <strong>Interrupt</strong>: Private Peripheral Interrupt (PPI), routed through the GIC (Generic Interrupt Controller)<br>&#9656; <strong>Virtual timer offset</strong>: Hypervisors can offset <code>CNTVCT_EL0</code> for guest migration &#8212; the virtual timer is preferred in virtualized ARM environments</p><h2><strong>The Consumer: How DXE Core Uses the Timer</strong></h2><p>Once the Timer Architectural Protocol is installed, the DXE Core discovers it during core initialization and registers its internal <code>CoreTimerTick</code> function.</p><h3><strong>CoreTimerTick &#8212; The Heartbeat</strong></h3><p>Every tick, the DXE Core&#8217;s timer callback performs this work:</p><p>&#9656; <strong>Advances </strong><code>mEfiSystemTime</code>: The firmware&#8217;s wall clock advances by the timer period. This feeds <code>GetTime()</code> and <code>RT-&gt;GetTime()</code>.</p><p>&#9656; <strong>Walks the timer event queue</strong>: All <code>EVT_TIMER</code> events are checked. If an event&#8217;s trigger time has passed, the event is signaled.</p><p>&#9656; <strong>Dispatches notify-signal callbacks</strong>: Events created with <code>EVT_TIMER | EVT_NOTIFY_SIGNAL</code> have their notification function called at <code>TPL_HIGH_LEVEL</code>.</p><p>&#9656; <strong>Decrements the watchdog</strong>: The boot watchdog timer (if enabled) counts down. If it hits zero, the system resets.</p><p>&#9656; <strong>Checks for periodic events</strong>: <code>EVT_TIMER</code> events created with <code>TimerPeriod &gt; 0</code> are re-armed for the next interval.</p><h3><strong>CoreStall() &#8212; A Different Mechanism</strong></h3><p>This is a common point of confusion. <code>gBS-&gt;Stall()</code> does <strong>not</strong> use the Timer Architectural Protocol. Instead:</p><p>&#9656; For short delays: spins on the CPU timestamp counter (<code>AsmReadTsc()</code> on x86) &#8212; pure busy-wait<br>&#9656; For longer delays: calls <code>gMetronome-&gt;WaitForTick()</code> &#8212; the Metronome Architectural Protocol, which is a separate abstraction<br>&#9656; The DXE Core&#8217;s <code>CoreStall()</code> falls back to a calibrated spin loop if the metronome is unavailable</p><p>The Timer Arch Protocol is for <strong>event-driven</strong> timing. Stall is for <strong>blocking</strong> delays. They serve different purposes and use different mechanisms.</p><h3><strong>SetTimer() and EVT_TIMER Events</strong></h3><p>When a driver calls <code>gBS-&gt;SetTimer(Event, TimerRelative, 50000000)</code>, the DXE Core:</p><p>&#9656; Converts the trigger time (50,000,000 &#215; 100ns = 5 seconds) to an absolute tick count<br>&#9656; Inserts the event into the timer event queue, sorted by trigger time<br>&#9656; On every <code>CoreTimerTick</code> call, checks if any event&#8217;s trigger time &#8804; current tick count<br>&#9656; When the event fires: if <code>EVT_NOTIFY_SIGNAL</code>, calls the notify function; if <code>EVT_NOTIFY_WAIT</code>, signals the event so <code>WaitForEvent()</code> unblocks</p><h2><strong>Why an Architectural Protocol?</strong></h2><p>The PI specification defines six &#8220;Architectural Protocols&#8221; that are mandatory for the DXE Core to function:</p><p>&#9656; <strong>CPU Architectural Protocol</strong> &#8212; interrupt management, flush caches, read timers<br>&#9656; <strong>RTC Architectural Protocol</strong> &#8212; real-time clock access<br>&#9656; <strong>Timer Architectural Protocol</strong> &#8212; periodic timer interrupt (this one)<br>&#9656; <strong>Metronome Architectural Protocol</strong> &#8212; fixed-rate tick for Stall()<br>&#9656; <strong>Watchdog Timer Architectural Protocol</strong> &#8212; boot watchdog<br>&#9656; <strong>Variable Write Architectural Protocol</strong> &#8212; non-volatile variable storage</p><p>Each Architectural Protocol represents a hardware capability that the DXE Core cannot function without. If any of them is missing when the DXE Core initializes, the dispatcher asserts and the boot halts.</p><p>The &#8220;Architectural&#8221; designation means the protocol is defined by the PI specification itself &#8212; not by a vendor or an interest group. All UEFI-compliant platforms must provide an implementation.</p><h2><strong>Debugging Tips</strong></h2><p>If you&#8217;re bringing up a new platform and the DXE Core hangs during initialization:</p><p>&#9656; <strong>Check the serial log for &#8220;Arch Protocol&#8221;</strong>: The DXE Core prints which Architectural Protocols it&#8217;s waiting for. If you see it stuck waiting for the timer, your timer driver isn&#8217;t being dispatched.</p><p>&#9656; <strong>Verify DEPEX</strong>: The timer driver&#8217;s <code>[Depex]</code> section may require <code>gEfiCpuArchProtocolGuid</code>. If the CPU Arch Protocol isn&#8217;t installed yet, the timer driver won&#8217;t load.</p><p>&#9656; <strong>Assert in </strong><code>InstallMultipleProtocolInterfaces</code>: Each timer driver does <code>ASSERT_PROTOCOL_ALREADY_INSTALLED</code> before installing. If two timer drivers try to install, the second one asserts. Check your platform DSC &#8212; you should only include ONE timer driver.</p><p>&#9656; <strong>Interrupt not firing</strong>: If the protocol installs but <code>CoreTimerTick</code> is never called, check the interrupt routing. On x86, verify the I/O APIC redirection table entry for the timer vector. On ARM, check the GIC distributor configuration for the timer PPI.</p><p>&#9656; <strong>SetTimerPeriod(0) confusion</strong>: If you call <code>SetTimerPeriod(0)</code> and events stop firing, that&#8217;s expected behavior &#8212; you disabled the timer. The DXE Core does this intentionally during critical sections.</p><h2><strong>Bottom Line</strong></h2><p>The Timer Architectural Protocol is a masterclass in firmware abstraction. Four functions. One handler slot. Three completely different hardware implementations. And the consumer &#8212; the DXE Core &#8212; never knows which one is running underneath.</p><p>That&#8217;s the power of UEFI&#8217;s protocol model. The interface is the contract. The implementation is the platform&#8217;s problem.</p><div><hr></div><p><em>David Zhu writes about UEFI firmware internals, EDK2 implementation details, and embedded systems at <a href="/__u/gdbplus.substack.com/">gdbplus.substack.com</a>. If you&#8217;re debugging a DXE driver or bringing up a new platform, there&#8217;s more where this came from.</em></p><div class="captioned-button-wrap" data-attrs="{&quot;url&quot;:&quot;https://gdbplus.substack.com/p/how-the-uefi-timer-architectural?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="CaptionedButtonToDOM"><div class="preamble"><p class="cta-caption">Thanks for reading gdbplus's Substack! This post is public so feel free to share it.</p></div><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://gdbplus.substack.com/p/how-the-uefi-timer-architectural?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="ButtonCreateButton"><a class="button primary" href="/__u/gdbplus.substack.com/p/how-the-uefi-timer-architectural?utm_source=substack&amp;utm_medium=email&amp;utm_content=share&amp;action=share"><span>Share</span></a></p></div><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://gdbplus.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading gdbplus's Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[The Firmware Gotcha Nobody Warns You About: Why Your Code Gets Slower When You Plug In a Debug Card]]></title><description><![CDATA[How a one-line I/O port read can turn your firmware from responsive to glacial, and what the Time Stamp Counter has to do with it.]]></description><link>https://gdbplus.substack.com/p/the-firmware-gotcha-nobody-warns</link><guid isPermaLink="false">https://gdbplus.substack.com/p/the-firmware-gotcha-nobody-warns</guid><dc:creator><![CDATA[gdbplus]]></dc:creator><pubDate>Wed, 01 Jul 2026 12:28:40 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!ZsNB!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F640bf2d2-93ac-4169-b995-fb0e1d2954c0_2560x1440.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Picture this. You&#8217;ve been developing a UEFI firmware module for weeks. It boots fast. Timing is consistent. Everything checks out on your reference board. Then you plug in a POST debug card &#8212; one of those little PCIe or LPC doodads that displays hex codes during boot &#8212; and suddenly your init sequence takes three times longer. Peripherals time out. Watchdog fires. The code that was rock-solid in the lab now can&#8217;t even finish POST.</p><p>You didn&#8217;t change a thing. You just plugged in a diagnostic tool.</p><p>Welcome to one of firmware&#8217;s most insidious timing traps: the I/O port 80 delay gotcha. And to understand it properly, we need to start with the CPU feature that makes precise timing possible in the first place &#8212; the Time Stamp Counter.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!ZsNB!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F640bf2d2-93ac-4169-b995-fb0e1d2954c0_2560x1440.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!ZsNB!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F640bf2d2-93ac-4169-b995-fb0e1d2954c0_2560x1440.png 424w, /__u/substackcdn.com/image/fetch/$s_!ZsNB!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F640bf2d2-93ac-4169-b995-fb0e1d2954c0_2560x1440.png 848w, /__u/substackcdn.com/image/fetch/$s_!ZsNB!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F640bf2d2-93ac-4169-b995-fb0e1d2954c0_2560x1440.png 1272w, /__u/substackcdn.com/image/fetch/$s_!ZsNB!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F640bf2d2-93ac-4169-b995-fb0e1d2954c0_2560x1440.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!ZsNB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F640bf2d2-93ac-4169-b995-fb0e1d2954c0_2560x1440.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/640bf2d2-93ac-4169-b995-fb0e1d2954c0_2560x1440.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:454582,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://gdbplus.substack.com/i/204429692?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F640bf2d2-93ac-4169-b995-fb0e1d2954c0_2560x1440.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!ZsNB!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F640bf2d2-93ac-4169-b995-fb0e1d2954c0_2560x1440.png 424w, /__u/substackcdn.com/image/fetch/$s_!ZsNB!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F640bf2d2-93ac-4169-b995-fb0e1d2954c0_2560x1440.png 848w, /__u/substackcdn.com/image/fetch/$s_!ZsNB!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F640bf2d2-93ac-4169-b995-fb0e1d2954c0_2560x1440.png 1272w, /__u/substackcdn.com/image/fetch/$s_!ZsNB!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F640bf2d2-93ac-4169-b995-fb0e1d2954c0_2560x1440.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><div><hr></div><h2><strong>Part 1: The Time Stamp Counter &#8212; Your CPU&#8217;s Built-in Stopwatch</strong></h2><p>Every modern x86 CPU has a hidden clock ticking away inside it. Actually, it&#8217;s not hidden at all &#8212; it&#8217;s the <strong>Time Stamp Counter (TSC)</strong>, and it&#8217;s been there since the Intel Pentium and AMD K5 days.</p><p>The TSC is a 64-bit register that increments with every core clock cycle. Reset it at power-on, and it counts up. And up. At 2 GHz, it won&#8217;t wrap around for roughly 292 years. It&#8217;s accessible through a single instruction:</p><pre><code><code>rdtsc             ; EDX:EAX &#8592; 64-bit TSC value
                  ; ~20&#8211;30 cycles on modern CPUs
</code></code></pre><p>That&#8217;s it. One instruction, ~20-30 cycles of overhead, and you have a high-precision timestamp. The upper 32 bits land in EDX, the lower 32 in EAX. Combine them into a 64-bit value and you&#8217;ve got a monotonically-increasing counter with sub-nanosecond granularity.</p><h3><strong>Invariant TSC: The Game-Changer</strong></h3><p>On older CPUs, the TSC had a dirty secret: it wasn&#8217;t reliable. If the CPU changed frequency (P-state transitions), the TSC rate changed too. Two timestamps taken seconds apart could be wildly inaccurate if a frequency shift happened in between.</p><p>This was fixed with the <strong>invariant TSC</strong>, guaranteed by CPUID leaf 0x80000007:EDX[8]. On any CPU that advertises this feature (which is every x86 chip from the last 15+ years), the TSC ticks at a constant rate regardless of:</p><ul><li><p>P-state (frequency scaling)</p></li><li><p>C-state (power saving)</p></li><li><p>Which core you&#8217;re reading from</p></li></ul><p>All cores share the same underlying tick source. The TSC became the gold standard for high-performance timing on x86. Linux even uses it as its preferred clock source &#8212; run <code>cat /sys/devices/system/clocksource/clocksource0/current_clocksource</code> and you&#8217;ll almost certainly see <code>tsc</code>.</p><p>There&#8217;s also <strong>RDTSCP</strong>, a serialized variant that guarantees all previous instructions have completed before the TSC is read &#8212; important for precise benchmarking where you don&#8217;t want out-of-order execution to skew your measurements.</p><h3><strong>What Firmware Actually Uses TSC For</strong></h3><p>In the UEFI/BIOS world, the TSC is a workhorse:</p><p>&#9656; <strong>Performance profiling</strong> &#8212; wrap a PEIM dispatch or DXE driver entry point with RDTSC, compute the delta, and you know exactly how long it took.</p><p>&#9656; <strong>Spin-wait delays</strong> &#8212; when you need a microsecond-level delay and the timer hardware isn&#8217;t ready yet (hello, early PEI phase before the ACPI timer is configured), you spin on TSC deltas.</p><p>&#9656; <strong>Microbenchmarks</strong> &#8212; DDR training, PCIe link initialization, USB controller reset sequences &#8212; all need precise timing measurements.</p><p>&#9656; <strong>Event timestamping</strong> &#8212; correlate firmware log entries with hardware events at cycle-level precision.</p><p>&#9656; <strong>APIC timer calibration</strong> &#8212; measure the bus clock to configure the Local APIC timer divisor.</p><p>The TSC is fast, precise, and universally available. It&#8217;s the closest thing firmware has to a Swiss Army knife for timing.</p><p>But what do you do when you&#8217;re in really early boot &#8212; before memory is initialized, before the chipset timers are configured, and you just need a cheap, predictable delay? Something that takes a handful of microseconds and doesn&#8217;t depend on any initialized hardware?</p><p>Enter the I/O port read trick.</p><div><hr></div><h2><strong>Part 2: The I/O Port 80 Trick &#8212; A Convenient Lie</strong></h2><p>In the early stages of firmware initialization &#8212; SEC and PEI phases in UEFI PI parlance, or the reset vector through early POST in traditional BIOS &#8212; your toolbox is extremely limited. DRAM isn&#8217;t up yet. The 8254 PIT might not be configured. The HPET isn&#8217;t enumerated. The TSC works, but TSC-based spin-wait loops need the invariant TSC guarantee, and on some platforms you haven&#8217;t confirmed CPUID leaf 0x80000007 yet.</p><p>So firmware developers reach for the oldest trick in the x86 book: an I/O port read as a delay.</p><p>An I/O bus cycle on x86 takes a predictable amount of time &#8212; roughly 1 microsecond on typical hardware when no device responds. The CPU issues the read, the bus controller waits through the ISA/LPC bus cycle timeout, nobody claims the transaction, and the bus controller returns ~0xFF to the CPU. End of story. Consistent, reliable, available from the very first instruction after reset.</p><p>And which I/O port gets abused for this purpose? Port <strong>80h</strong>.</p><p>Why? Because firmware is already writing POST codes to port 80h. It&#8217;s the standard diagnostic port &#8212; every BIOS since the IBM PC/AT has written checkpoint codes to port 80h during boot. The code is already there, the port address is hard-coded into every firmware codebase on the planet, and adding a read after the write seems harmless:</p><pre><code><code>// Classic firmware delay pattern (WRONG)
IoWrite8(0x80, PostCode);   // Write POST checkpoint
IoRead8(0x80);              // Read-back for ~1&#956;s delay
</code></code></pre><p>The write sends the POST code to any attached debug hardware. The read &#8212; the thinking goes &#8212; just burns ~1 microsecond while the bus controller waits for a device that isn&#8217;t there.</p><p>This works perfectly. Until it doesn&#8217;t.</p><div><hr></div><h2><strong>Part 3: The Trap &#8212; When Port 80h Has a Listener</strong></h2><p>The moment you plug in a POST debug card &#8212; whether it&#8217;s a PCIe POST card, an LPC debug module, or a BMC with port 80h snooping &#8212; everything changes.</p><p>That debug card isn&#8217;t passively watching. It&#8217;s actively <strong>decoding</strong> I/O cycles targeting port 80h. When the CPU issues <code>inb(0x80)</code>, the card sees the address on the bus, recognizes it as port 80h, and asserts its claim.</p><p>Here&#8217;s the critical mechanism: on the ISA bus (and its LPC successor), a slow device can stretch the bus cycle by de-asserting <strong>IOCHRDY</strong> (I/O Channel Ready). This inserts wait states. The CPU sits there, held, waiting for the device to finish whatever it&#8217;s doing and re-assert IOCHRDY.</p><p>A debug card is, by design, a <strong>slow device</strong>. It&#8217;s a microcontroller running at a few tens of MHz, connected through an LPC-to-SPI bridge or similar, reading the port value, formatting it for a 7-segment display or UART output. It needs time to process. So it holds IOCHRDY low.</p><p>The result: your &#8220;1 microsecond&#8221; delay becomes 5 microseconds. Or 10. Or 50. Or more.</p><blockquote><p><strong>A slow device on the bus can hold the I/O cycle arbitrarily long.</strong></p></blockquote><p>There&#8217;s no timeout in the ISA/LPC bus protocol for IOCHRDY. The device can theoretically hold it forever. In practice, most debug cards add 5-50&#956;s per access, but I&#8217;ve seen worse. And if your firmware code reads port 80h in a tight loop for timing &#8212; say, 100 iterations for a &#8220;100&#956;s delay&#8221; &#8212; you&#8217;re now waiting anywhere from 500&#956;s to 5ms. Per delay loop.</p><p>Imagine what happens to a boot sequence that has dozens of these scattered through early initialization. Suddenly your 3-second POST becomes 15 seconds. Peripherals with tight initialization timeouts start failing. The watchdog timer fires because a critical section overran its budget.</p><p>All because of a debug card. The tool that&#8217;s supposed to help you debug is actually creating bugs.</p><div><hr></div><h2><strong>Part 4: The Performance Numbers</strong></h2><p>Let&#8217;s quantify this. On a typical Intel platform with an LPC debug card:</p><p>&#9656; <strong>Without debug card:</strong> ~1.0&#8211;1.2&#956;s per <code>inb(0x80)</code> &#8212; the bus controller&#8217;s no-device timeout is predictable and fast.</p><p>&#9656; <strong>With debug card (quality LPC POST card):</strong> ~5&#8211;8&#956;s per <code>inb(0x80)</code> &#8212; the card&#8217;s microcontroller needs time to latch and display.</p><p>&#9656; <strong>With debug card (cheap USB-to-LPC analyzer):</strong> ~15&#8211;50&#956;s per <code>inb(0x80)</code> &#8212; slower bridge chips, more processing overhead.</p><p>&#9656; <strong>With BMC snooping port 80h:</strong> variable &#8212; anywhere from 3&#956;s to 100&#956;s depending on BMC load and the snooping implementation.</p><p>That&#8217;s a <strong>5&#8211;50&#215; slowdown</strong> from a single hardware addition. And it&#8217;s not proportional &#8212; it&#8217;s a step function. The moment that debug card is plugged in, every port 80h access in your entire firmware codebase gets slower. You can&#8217;t selectively exempt some accesses; the bus doesn&#8217;t work that way.</p><div><hr></div><h2><strong>Part 5: The Fix &#8212; Unclaimed Ports</strong></h2><p>The solution is simple once you understand the mechanism: <strong>use an I/O port that no device decodes</strong>.</p><p>On x86 platforms, the I/O address space is 64KB (0x0000&#8211;0xFFFF), but only a fraction of it is claimed by real hardware. The rest is empty &#8212; no device responds, so the bus controller times out immediately and returns ~0xFF.</p><p>Some safe unclaimed ports commonly used in firmware:</p><p>&#9656; <strong>0xED</strong> &#8212; widely used in EDK2 and coreboot for delay purposes<br>&#9656; <strong>0xEE</strong> &#8212; also common, same behavior<br>&#9656; <strong>0xEF</strong> &#8212; another safe choice<br>&#9656; <strong>0xEB</strong> &#8212; occasionally used, but verify your platform isn&#8217;t using it</p><p>The corrected code:</p><pre><code><code>// Correct approach: separate POST code write from delay
IoWrite8(0x80, PostCode);   // Write POST checkpoint (debug card sees this)
IoRead8(0xED);              // Read unclaimed port for delay (debug card ignores this)
</code></code></pre><p>The write to port 80h still goes to the debug card &#8212; good, you want that for diagnostics. But the delay read goes to port 0xED, which no device claims. The bus controller times out immediately (~1&#956;s), and your timing is consistent regardless of whether a debug card is plugged in.</p><h3><strong>But Wait &#8212; What About the Write?</strong></h3><p>An important nuance: <strong>writing</strong> to port 80h is typically not a performance problem, even with a debug card. Here&#8217;s why:</p><p>On the ISA/LPC bus, writes are <strong>posted</strong>. The CPU issues the write and continues executing; the bus controller acknowledges immediately and the data eventually reaches the device. The CPU doesn&#8217;t wait for the device to accept the data. Reads, however, are <strong>non-posted</strong> &#8212; the CPU must wait for the data to come back. That&#8217;s why <code>outb(0x80, val)</code> is fast but <code>inb(0x80)</code> can be slow.</p><p>So keep the POST code writes to port 80h &#8212; they&#8217;re valuable for debugging. Just separate the delay mechanism from the debug port.</p><div><hr></div><h2><strong>Part 6: How to Detect This in Your Own Codebase</strong></h2><p>If you maintain or work with firmware that does I/O port delays, here&#8217;s how to audit:</p><p>&#9656; <strong>Step 1:</strong> Grep for <code>0x80</code> or <code>0x0080</code> in your codebase. Look for <code>inb</code>, <code>IoRead8</code>, <code>__inbyte</code>, or whatever I/O abstraction your codebase uses.</p><p>&#9656; <strong>Step 2:</strong> For each match, ask: is this being used for timing (delay, spin-wait, microsecond wait) or purely for POST code display?</p><p>&#9656; <strong>Step 3:</strong> If it&#8217;s timing, replace <code>0x80</code> with <code>0xED</code> (or another verified-unclaimed port on your platform).</p><p>&#9656; <strong>Step 4:</strong> Add a comment. Future-you will thank present-you:</p><pre><code><code>//
// Port 0xED is unclaimed on this platform.
// DO NOT change to 0x80 &#8212; debug cards will stretch the bus cycle.
//
IoRead8(0xED);
</code></code></pre><p>&#9656; <strong>Step 5:</strong> Test with AND without a debug card. Your boot time should be identical in both configurations.</p><h3><strong>EDK2 Already Does This (Mostly)</strong></h3><p>If you&#8217;re working with EDK2, search for <code>IoRead8 (0xED)</code> &#8212; you&#8217;ll find it used extensively, especially in early PEI modules and chipset initialization code. The EDK2 community learned this lesson the hard way years ago. But legacy code, vendor BLOBs, and older codebases still have port 80h delay patterns hiding in them.</p><div><hr></div><h2><strong>Part 7: The Bigger Lesson</strong></h2><p>This isn&#8217;t really about port 80h. It&#8217;s about a fundamental principle of hardware-near programming:</p><blockquote><p><strong>Hardware that you think is passive may be actively participating in your bus transactions.</strong></p></blockquote><p>A debug card isn&#8217;t a passive monitor. An oscilloscope probe isn&#8217;t passive. A JTAG debugger isn&#8217;t passive. Any tool that connects to your platform&#8217;s buses or signals can change the timing characteristics of the system you&#8217;re trying to measure.</p><p>Other examples of the same class of problem:</p><p>&#9656; <strong>JTAG/SWD debuggers</strong> &#8212; setting breakpoints can change cache behavior and execution timing. A Heisenbug that disappears when you attach the debugger? That&#8217;s this principle in reverse.</p><p>&#9656; <strong>Logic analyzers</strong> &#8212; probe capacitance can alter signal rise times on high-speed buses. Your DDR training that passes with no probes attached may fail with probes.</p><p>&#9656; <strong>Serial console</strong> &#8212; UART output at 115200 baud can add milliseconds of delay per print statement. Your firmware that works fine with serial logging disabled may hit race conditions when logging is enabled.</p><p>&#9656; <strong>SPI flash programmers</strong> &#8212; in-system programming tools that hold the flash in reset can cause the chipset to time out on the SPI bus, triggering unexpected error paths.</p><p>The common thread: <strong>measurement changes the system</strong>. Always validate your firmware timing in the configuration that most closely matches production &#8212; and in the configuration that exposes the worst-case timing.</p><div><hr></div><h2><strong>Bottom Line</strong></h2><p>The TSC and port 80h delay trap represent two sides of the same coin: precise timing in firmware is both essential and fragile.</p><p>The TSC gives you cycle-accurate measurement for profiling, benchmarking, and calibration. Use it. It&#8217;s one of the most reliable timing primitives on x86, and the invariant TSC guarantee means it works the same way on every modern platform.</p><p>But when you need a quick delay in early boot &#8212; before the timers are up, before memory is initialized &#8212; reach for an I/O port read. Just don&#8217;t reach for port 80h. The POST code debug port has one job: displaying POST codes. Let it do that job, and use an unclaimed port (0xED, 0xEE) for your delays.</p><p>The debug card you plug in to find timing bugs shouldn&#8217;t be the cause of them.</p><div><hr></div><p><em>David Zhu is a firmware engineer working on UEFI BIOS, embedded systems, and hardware-near software. He writes about the dark corners of x86 platform initialization at <a href="/__u/gdbplus.substack.com/">gdbplus.substack.com</a>.</em></p>]]></content:encoded></item><item><title><![CDATA[Making Legacy UART (COM1 0x3F8 / COM2 0x2F8) Work Across the Entire AMD Boot Flow]]></title><description><![CDATA[From PSP &#8594; ABL &#8594; UEFI BIOS &#8594; Linux on AMD Zen Platforms]]></description><link>https://gdbplus.substack.com/p/making-legacy-uart-com1-0x3f8-com2</link><guid isPermaLink="false">https://gdbplus.substack.com/p/making-legacy-uart-com1-0x3f8-com2</guid><dc:creator><![CDATA[gdbplus]]></dc:creator><pubDate>Tue, 30 Jun 2026 11:33:59 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!f34L!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff632e608-1ebd-4f45-a75d-090805341fe7_1536x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://gdbplus.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading gdbplus's Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p><em>One of the first things every firmware engineer does on a new board is print &#8220;Hello World.&#8221; Surprisingly, making that message appear consistently across every boot stage is much more complicated than simply programming a 16550 UART.</em></p><div><hr></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!f34L!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff632e608-1ebd-4f45-a75d-090805341fe7_1536x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!f34L!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff632e608-1ebd-4f45-a75d-090805341fe7_1536x1024.png 424w, /__u/substackcdn.com/image/fetch/$s_!f34L!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff632e608-1ebd-4f45-a75d-090805341fe7_1536x1024.png 848w, /__u/substackcdn.com/image/fetch/$s_!f34L!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff632e608-1ebd-4f45-a75d-090805341fe7_1536x1024.png 1272w, /__u/substackcdn.com/image/fetch/$s_!f34L!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff632e608-1ebd-4f45-a75d-090805341fe7_1536x1024.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!f34L!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff632e608-1ebd-4f45-a75d-090805341fe7_1536x1024.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f632e608-1ebd-4f45-a75d-090805341fe7_1536x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2402348,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://gdbplus.substack.com/i/204260430?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff632e608-1ebd-4f45-a75d-090805341fe7_1536x1024.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!f34L!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff632e608-1ebd-4f45-a75d-090805341fe7_1536x1024.png 424w, /__u/substackcdn.com/image/fetch/$s_!f34L!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff632e608-1ebd-4f45-a75d-090805341fe7_1536x1024.png 848w, /__u/substackcdn.com/image/fetch/$s_!f34L!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff632e608-1ebd-4f45-a75d-090805341fe7_1536x1024.png 1272w, /__u/substackcdn.com/image/fetch/$s_!f34L!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff632e608-1ebd-4f45-a75d-090805341fe7_1536x1024.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>Introduction</h2><p>When bringing up an AMD platform, one of the first milestones is obtaining serial debug output.</p><p>Most engineers expect writing to <strong>I/O port 0x3F8</strong> (COM1) or <strong>0x2F8</strong> (COM2) to &#8220;just work.&#8221;</p><p>In reality, the debug path changes several times during boot:</p><pre><code></code></pre><pre><code><code>Power On
    &#9474;
    &#9660;
+--------------------+
| PSP Boot ROM       |
+--------------------+
    &#9474;
    &#9660;
+--------------------+
| PSP Firmware       |
+--------------------+
    &#9474;
    &#9660;
+--------------------+
| AMD ABL            |
| (AGESA Bootloader) |
+--------------------+
    &#9474;
    &#9660;
+--------------------+
| UEFI BIOS          |
| SEC&#8594;PEI&#8594;DXE&#8594;BDS    |
+--------------------+
    &#9474;
    &#9660;
+--------------------+
| Linux Kernel       |
+--------------------+</code></code></pre><p>Every stage uses different software.</p><p>Every stage has different ownership of the UART.</p><p>Understanding how the same COM port remains usable across the entire boot process is an important topic for BIOS and platform firmware engineers.</p><div><hr></div><h1>The Legacy COM Port</h1><p>The PC architecture has defined two classic UART addresses for decades.</p><p>PortIO BaseIRQCOM10x3F8IRQ4COM20x2F8IRQ3</p><p>These addresses are part of the IBM PC architecture and remain supported today&#8212;even on modern AMD Zen 5 systems.</p><p>Applications simply perform:</p><pre><code></code></pre><pre><code><code>outb(character, 0x3F8);</code></code></pre><p>without caring whether the underlying UART is inside:</p><ul><li><p>Super I/O<br></p></li><li><p>Embedded Controller<br></p></li><li><p>AMD SoC<br></p></li><li><p>FPGA<br></p></li><li><p>PCIe device<br></p></li></ul><p>The platform firmware hides those implementation details.</p><div><hr></div><h1>Modern AMD UART Architecture</h1><p>On modern AMD platforms there is no ISA bus anymore.</p><p>Instead, communication flows like this:</p><pre><code></code></pre><pre><code><code>             AMD SoC
      +----------------------+
      |                      |
      |  CPU                 |
      |                      |
      | PSP                  |
      | SMU                  |
      |                      |
      | UART Controller      |
      +----------+-----------+
                 |
              eSPI/LPC
                 |
      +----------+-----------+
      |     PCH / EC         |
      +----------+-----------+
                 |
          UART Connector</code></code></pre><p>The CPU does <strong>not</strong> directly expose ISA hardware.</p><p>Instead:</p><ul><li><p>eSPI<br></p></li><li><p>LPC bridge<br></p></li><li><p>UART controller<br></p></li><li><p>IO decoder<br></p></li></ul><p>combine to emulate traditional PC I/O space.</p><div><hr></div><h1>Step 1 &#8212; PSP Initializes the UART</h1><p>The first executable firmware is the PSP ROM.</p><p>At this point:</p><ul><li><p>DRAM is unavailable<br></p></li><li><p>x86 cores are still asleep<br></p></li><li><p>PCI enumeration hasn&#8217;t happened<br></p></li><li><p>BIOS has not executed<br></p></li></ul><p>The PSP performs minimal hardware initialization.</p><p>Typical operations include:</p><ul><li><p>clock enable<br></p></li><li><p>reset release<br></p></li><li><p>UART mux selection<br></p></li><li><p>GPIO pin configuration<br></p></li></ul><p>Example:</p><pre><code></code></pre><pre><code><code>PSP
 &#9474;
 &#9500;&#9472;&#9472; Enable UART Clock
 &#9500;&#9472;&#9472; Configure GPIO
 &#9500;&#9472;&#9472; Set Baud Rate
 &#9500;&#9472;&#9472; Enable FIFO
 &#9492;&#9472;&#9472; Output Debug Characters</code></code></pre><p>Many AMD internal debug messages originate here.</p><div><hr></div><h1>Step 2 &#8212; AMD ABL Uses the Same UART</h1><p>Next comes AMD Boot Loader (ABL).</p><p>ABL performs:</p><ul><li><p>DDR initialization<br></p></li><li><p>SoC initialization<br></p></li><li><p>fabric setup<br></p></li><li><p>SMU communication<br></p></li><li><p>handing off to BIOS<br></p></li></ul><p>Instead of creating another UART driver, ABL usually reuses the initialized UART.</p><pre><code></code></pre><pre><code><code>PSP
    &#9474;
UART already configured
    &#9474;
    &#9660;
ABL
    &#9474;
Print("Memory Init...")</code></code></pre><p>This allows uninterrupted serial logs from the earliest boot stages.</p><div><hr></div><h1>Step 3 &#8212; BIOS Takes Ownership</h1><p>Once x86 execution begins:</p><pre><code></code></pre><pre><code><code>SEC
PEI
DXE</code></code></pre><p>the BIOS becomes responsible for serial output.</p><p>The BIOS serial driver usually performs:</p><pre><code></code></pre><pre><code><code>16550 Initialization

DLL
DLM
FCR
LCR
MCR
IER</code></code></pre><p>and exposes a standard Serial Port Library.</p><p>Example:</p><pre><code></code></pre><pre><code><code>SerialPortWrite();

DebugPrint();

ASSERT();</code></code></pre><p>In EDK2:</p><pre><code></code></pre><pre><code><code>DebugLib
      &#9474;
SerialPortLib
      &#9474;
16550 Driver
      &#9474;
UART Hardware</code></code></pre><p>From this point onward every DEBUG() macro eventually reaches COM1.</p><div><hr></div><h1>Step 4 &#8212; ACPI Describes the UART</h1><p>Before booting Linux, BIOS must describe the UART.</p><p>There are two common methods.</p><h2>Legacy PC</h2><p>COM1 simply exists.</p><p>Linux probes:</p><pre><code></code></pre><pre><code><code>0x3F8
IRQ4</code></code></pre><p>No Device Tree required.</p><div><hr></div><h2>Modern ACPI</h2><p>BIOS publishes the UART in ACPI.</p><p>Example resources include:</p><pre><code></code></pre><pre><code><code>Memory Region

Interrupt

Clock

Power Resource</code></code></pre><p>Linux reads ACPI and loads the appropriate driver.</p><div><hr></div><h1>Step 5 &#8212; Linux Loads the Driver</h1><p>After ExitBootServices():</p><pre><code></code></pre><pre><code><code>Linux
   &#9474;
ACPI Enumeration
   &#9474;
8250 Driver
   &#9474;
ttyS0</code></code></pre><p>The driver initializes:</p><pre><code></code></pre><pre><code><code>FIFO

Interrupt

DMA (optional)

Console</code></code></pre><p>Applications now use:</p><pre><code></code></pre><pre><code><code>printf()

dmesg

systemd

login

shell</code></code></pre><p>All through the same UART.</p><div><hr></div><h1>Keeping the Same UART Alive</h1><p>One challenge is ensuring every stage uses the same hardware configuration.</p><pre><code></code></pre><pre><code><code>PSP
 &#9474;
 &#9660;
ABL
 &#9474;
 &#9660;
BIOS
 &#9474;
 &#9660;
Linux</code></code></pre><p>If one stage changes:</p><ul><li><p>baud rate<br></p></li><li><p>clock source<br></p></li><li><p>GPIO mux<br></p></li><li><p>FIFO mode<br></p></li></ul><p>the serial log becomes unreadable.</p><p>Typical bring-up practice is:</p><ul><li><p>115200 baud<br></p></li><li><p>8 data bits<br></p></li><li><p>no parity<br></p></li><li><p>1 stop bit<br></p></li><li><p>leave UART enabled<br></p></li></ul><p>allowing continuous logging throughout boot.</p><div><hr></div><h1>How Does 0x3F8 Actually Reach the UART?</h1><p>When x86 executes:</p><pre><code></code></pre><pre><code><code>OUT 0x3F8, AL</code></code></pre><p>the processor does not directly drive UART pins.</p><p>Instead, the transaction passes through several hardware blocks:</p><pre><code></code></pre><pre><code><code>CPU
 &#9474;
IO Instruction
 &#9474;
IO Decode
 &#9474;
eSPI/LPC Bridge
 &#9474;
UART Controller
 &#9474;
TX FIFO
 &#9474;
Serial Pin</code></code></pre><p>This compatibility layer allows software written decades ago to continue functioning on modern AMD systems.</p><div><hr></div><h1>Why Does Linux Still Call It ttyS0?</h1><p>Linux&#8217;s 8250/16550 subsystem preserves decades of compatibility.</p><pre><code></code></pre><pre><code><code>COM1
 &#8595;
0x3F8
 &#8595;
8250 Driver
 &#8595;
ttyS0</code></code></pre><p>Even on a cutting-edge Zen 5 platform, the familiar <code>/dev/ttyS0</code> often represents the same logical serial port used by the PSP, ABL, and BIOS during boot.</p><div><hr></div><h1>Debug Flow Across the Entire Boot Chain</h1><p>A complete serial log typically looks like this:</p><pre><code></code></pre><pre><code><code>Power On

&#8595;

PSP ROM
    "PSP Boot..."

&#8595;

PSP Firmware
    "Loading ABL..."

&#8595;

AMD ABL
    "DDR Training..."

&#8595;

UEFI SEC
    "SEC Entry"

&#8595;

PEI
    "Memory Installed"

&#8595;

DXE
    "PCI Enumeration"

&#8595;

BDS
    "Boot Option"

&#8595;

Linux
    "Starting kernel..."

&#8595;

systemd

&#8595;

Login Prompt</code></code></pre><p>Capturing this uninterrupted stream is invaluable when diagnosing early boot failures, memory training issues, or kernel startup problems.</p><div><hr></div><h1>Best Practices for Firmware Engineers</h1><p>When enabling UART debug on AMD platforms:</p><ul><li><p>Configure the UART as early as possible in the PSP or ABL stage.<br></p></li><li><p>Use a consistent baud rate (typically 115200 8N1) across all boot phases.<br></p></li><li><p>Avoid reinitializing the UART unless configuration changes are necessary.<br></p></li><li><p>Ensure the BIOS correctly advertises the serial port through ACPI (or Device Tree on embedded platforms).<br></p></li><li><p>Use a shared SerialPortLib implementation to keep debug output consistent.<br></p></li><li><p>Verify that Linux receives the correct <code>console=</code> kernel parameter (for example, <code>console=ttyS0,115200</code>).<br></p></li><li><p>Keep UART enabled until the operating system has fully taken ownership to preserve a continuous debug log.<br></p></li></ul><div><hr></div><h1>Conclusion</h1><p>Although <strong>COM1 (0x3F8)</strong> and <strong>COM2 (0x2F8)</strong> date back to the original IBM PC, they remain central to modern platform bring-up. On AMD Zen platforms, the same logical UART can be used seamlessly by the <strong>PSP</strong>, <strong>AMD ABL</strong>, <strong>UEFI BIOS</strong>, and <strong>Linux</strong>, providing an uninterrupted window into the system from the first instruction after reset to the operating system login prompt.</p><p>For firmware engineers, understanding this handoff is more than an academic exercise&#8212;it is one of the most effective tools for debugging platform initialization, memory training, silicon bring-up, and early kernel boot.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://gdbplus.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading gdbplus's Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div class="captioned-button-wrap" data-attrs="{&quot;url&quot;:&quot;https://gdbplus.substack.com/p/making-legacy-uart-com1-0x3f8-com2?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="CaptionedButtonToDOM"><div class="preamble"><p class="cta-caption">Thanks for reading gdbplus's Substack! This post is public so feel free to share it.</p></div><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://gdbplus.substack.com/p/making-legacy-uart-com1-0x3f8-com2?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="ButtonCreateButton"><a class="button primary" href="/__u/gdbplus.substack.com/p/making-legacy-uart-com1-0x3f8-com2?utm_source=substack&amp;utm_medium=email&amp;utm_content=share&amp;action=share"><span>Share</span></a></p></div>]]></content:encoded></item><item><title><![CDATA[How EDK2 Creates SMBIOS/DMI Tables — and How Linux Consumes Them]]></title><description><![CDATA[David Zhu]]></description><link>https://gdbplus.substack.com/p/how-edk2-creates-smbiosdmi-tables</link><guid isPermaLink="false">https://gdbplus.substack.com/p/how-edk2-creates-smbiosdmi-tables</guid><dc:creator><![CDATA[gdbplus]]></dc:creator><pubDate>Wed, 24 Jun 2026 12:33:41 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!LajI!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5477c63f-8877-4bbe-86cb-407b6fdeddd0_2560x1440.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>David Zhu</strong></p><div><hr></div><p>Every x86 system you touch &#8212; laptop, server, embedded board &#8212; carries a SMBIOS table. It is the single source of truth for &#8220;what hardware is in this box.&#8221; The BIOS builds it during boot. Linux reads it to populate /sys/class/dmi/id/. dmidecode decodes it for your shell scripts and inventory tools.</p><p>But how does it actually get there? What happens between the EDK2 firmware calling SmbiosAdd() and the Linux kernel&#8217;s dmi_scan_machine() finding the anchor string?</p><p>Let me walk through the full pipeline &#8212; from EDK2 driver to Linux sysfs</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!LajI!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5477c63f-8877-4bbe-86cb-407b6fdeddd0_2560x1440.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!LajI!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5477c63f-8877-4bbe-86cb-407b6fdeddd0_2560x1440.png 424w, /__u/substackcdn.com/image/fetch/$s_!LajI!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5477c63f-8877-4bbe-86cb-407b6fdeddd0_2560x1440.png 848w, /__u/substackcdn.com/image/fetch/$s_!LajI!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5477c63f-8877-4bbe-86cb-407b6fdeddd0_2560x1440.png 1272w, /__u/substackcdn.com/image/fetch/$s_!LajI!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5477c63f-8877-4bbe-86cb-407b6fdeddd0_2560x1440.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!LajI!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5477c63f-8877-4bbe-86cb-407b6fdeddd0_2560x1440.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5477c63f-8877-4bbe-86cb-407b6fdeddd0_2560x1440.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:510230,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://gdbplus.substack.com/i/203391147?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5477c63f-8877-4bbe-86cb-407b6fdeddd0_2560x1440.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!LajI!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5477c63f-8877-4bbe-86cb-407b6fdeddd0_2560x1440.png 424w, /__u/substackcdn.com/image/fetch/$s_!LajI!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5477c63f-8877-4bbe-86cb-407b6fdeddd0_2560x1440.png 848w, /__u/substackcdn.com/image/fetch/$s_!LajI!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5477c63f-8877-4bbe-86cb-407b6fdeddd0_2560x1440.png 1272w, /__u/substackcdn.com/image/fetch/$s_!LajI!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5477c63f-8877-4bbe-86cb-407b6fdeddd0_2560x1440.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>.</p><div><hr></div><h2><strong>Part 1: EDK2 Side &#8212; Building the Table</strong></h2><h3><strong>The SmbiosDxe Driver</strong></h3><p>The core driver lives at MdeModulePkg/Universal/SmbiosDxe/. It is a standard DXE driver that produces the EFI_SMBIOS_PROTOCOL &#8212; the interface platform code uses to register SMBIOS structures.</p><p>From SmbiosDxe.c, the driver entry point:</p><p>From MdeModulePkg/Universal/SmbiosDxe/SmbiosDxe.c, SmbiosDriverEntryPoint():</p><p>mPrivateData.Smbios.Add = SmbiosAdd;<br>mPrivateData.Smbios.UpdateString = SmbiosUpdateString;<br>mPrivateData.Smbios.Remove = SmbiosRemove;<br>mPrivateData.Smbios.GetNext = SmbiosGetNext;<br>mPrivateData.Smbios.MajorVersion = (UINT8)(PcdGet16 (PcdSmbiosVersion) &gt;&gt; 8);<br>mPrivateData.Smbios.MinorVersion = (UINT8)(PcdGet16 (PcdSmbiosVersion) &amp; 0x00ff);</p><p>Status = gBS-&gt;InstallProtocolInterface (<br>&amp;mPrivateData.Handle,<br>&amp;gEfiSmbiosProtocolGuid,<br>EFI_NATIVE_INTERFACE,<br>&amp;mPrivateData.Smbios<br>);</p><p>The driver maintains an internal linked list of all SMBIOS structures &#8212; each one an EFI_SMBIOS_ENTRY tracked by the private SMBIOS_INSTANCE.</p><h3><strong>How Platform Code Populates Tables</strong></h3><p>Platform code &#8212; typically a separate DXE driver like EmulatorPkg/PlatformSmbiosDxe/ &#8212; calls SmbiosProtocol-&gt;Add() for each SMBIOS type:</p><p>&#9656; <strong>Type 0:</strong> BIOS Information &#8212; vendor string, version, release date</p><p>&#9656; <strong>Type 1:</strong> System Information &#8212; manufacturer, product name, serial, UUID</p><p>&#9656; <strong>Type 2:</strong> Baseboard &#8212; motherboard vendor, product, version</p><p>&#9656; <strong>Type 3:</strong> Chassis &#8212; enclosure type, manufacturer, asset tag</p><p>&#9656; <strong>Type 4:</strong> Processor Information &#8212; socket, family, speed, cores</p><p>&#9656; <strong>Type 7:</strong> Cache Information &#8212; L1/L2/L3, size, associativity</p><p>&#9656; <strong>Type 17:</strong> Memory Device &#8212; DIMM size, speed, manufacturer, serial</p><p>Each call to Add() follows the same sequence inside SmbiosDxe:</p><ol><li><p>GetSmbiosStructureSize() calculates the total size including the string table area at the end of the structure</p></li><li><p>The record is allocated as an EFI_SMBIOS_ENTRY with an internal EFI_SMBIOS_RECORD_HEADER</p></li><li><p>A SMBIOS_HANDLE_ENTRY is created and inserted into the allocated handle list</p></li><li><p>The structure is inserted into the DataListHead linked list via InsertTailList()</p></li><li><p>SmbiosTableConstruction() is called to rebuild the flattened SMBIOS table</p></li></ol><h3><strong>The Anchor and Entry Point Structure</strong></h3><p>The SMBIOS 2.x Entry Point Structure (EPS) uses the anchor string <em>SM</em> and intermediate anchor <em>DMI</em>. From SmbiosDxe.c:</p><p>EntryPointStructureData = {<br>{ 0x5f, 0x53, 0x4d, 0x5f }, // &#8220;<em>SM</em>&#8220; AnchorString<br>0, // Checksum (filled later)<br>0x1f, // EntryPointLength<br>0, // MajorVersion<br>0, // MinorVersion<br>0, // MaxStructureSize<br>0, // EntryPointRevision<br>{ 0, 0, 0, 0, 0 }, // FormattedArea<br>{ 0x5f, 0x44, 0x4d, 0x49, 0x5f }, // &#8220;<em>DMI</em>&#8220; IntermediateAnchorString<br>0, // IntermediateChecksum<br>0, // TableLength<br>0, // TableAddress (physical)<br>0, // NumberOfSmbiosStructures<br>0 // SmbiosBcdRevision<br>};</p><p>For SMBIOS 3.0, the 64-bit EPS uses anchor <em>SM3</em> and a simpler layout: anchor, checksum, length, major/minor version, document revision, entry point revision, reserved, and a 64-bit structure table maximum size.</p><h3><strong>Finalization: SmbiosTableConstruction()</strong></h3><p>When structures are added (or on removal), SmbiosTableConstruction() assembles the final table and publishes it to the UEFI Configuration Table:</p><p>if (Smbios32BitTable) {<br>Status = SmbiosCreateTable ((VOID **)&amp;Eps);<br>if (!EFI_ERROR (Status)) {<br>gBS-&gt;InstallConfigurationTable (&amp;gEfiSmbiosTableGuid, Eps);<br>}<br>}</p><p>if (Smbios64BitTable) {<br>Status = SmbiosCreate64BitTable ((VOID **)&amp;Eps64Bit);<br>if (!EFI_ERROR (Status)) {<br>gBS-&gt;InstallConfigurationTable (&amp;gEfiSmbios3TableGuid, Eps64Bit);<br>}<br>}</p><p>This is the critical handoff: the SMBIOS table is registered with the EFI firmware via InstallConfigurationTable(). When the OS bootloader or kernel queries the EFI System Table, it reads the SMBIOS table pointer directly from the list of configuration tables &#8212; no memory scanning required on UEFI systems.</p><p>The 32-bit table is allocated below 4 GB (AllocateMaxAddress at 0xFFFFFFFF). The 64-bit table can live anywhere in the physical address space.</p><div><hr></div><h2><strong>Part 2: Linux Side &#8212; Reading the Tables</strong></h2><h3><strong>Early Boot: dmi_scan_machine()</strong></h3><p>On x86, the Linux kernel calls dmi_scan_machine() from arch/x86/kernel/dmi_scan.c extremely early in boot &#8212; before ACPI, before PCI enumeration. Two paths exist:</p><p>&#9656; <strong>UEFI systems:</strong> The kernel reads the SMBIOS entry point from the EFI Configuration Table (efi.smbios or efi.smbios3). No scanning needed.</p><p>&#9656; <strong>Legacy BIOS systems:</strong> The kernel scans physical memory from 0x000F0000 to 0x000FFFFF in 16-byte increments, looking for the <em>SM</em> or <em>SM3</em> anchor strings.</p><p>In either case, the kernel validates the EPS checksum, extracts the structure table physical address and length, and maps the table into kernel virtual address space via early_ioremap(). It then walks the type-length-handle headers to decode each SMBIOS structure.</p><h3><strong>The Sysfs Interface</strong></h3><p>Once the kernel finishes decoding, the dmi_id driver creates the /sys/class/dmi/id/ directory with individual files for each field:</p><p>&#9656; <strong>bios_vendor</strong> &#8212; SMBIOS Type 0, vendor string</p><p>&#9656; <strong>bios_version</strong> &#8212; SMBIOS Type 0, BIOS version</p><p>&#9656; <strong>bios_date</strong> &#8212; SMBIOS Type 0, release date</p><p>&#9656; <strong>sys_vendor</strong> &#8212; SMBIOS Type 1, system manufacturer</p><p>&#9656; <strong>product_name</strong> &#8212; SMBIOS Type 1, product name</p><p>&#9656; <strong>product_serial</strong> &#8212; SMBIOS Type 1, serial number</p><p>&#9656; <strong>product_uuid</strong> &#8212; SMBIOS Type 1, UUID</p><p>&#9656; <strong>board_vendor</strong> &#8212; SMBIOS Type 2, baseboard manufacturer</p><p>&#9656; <strong>board_name</strong> &#8212; SMBIOS Type 2, baseboard product name</p><p>&#9656; <strong>chassis_vendor</strong> &#8212; SMBIOS Type 3, chassis manufacturer</p><p>&#9656; <strong>chassis_type</strong> &#8212; SMBIOS Type 3, chassis type enumeration</p><p>&#9656; <strong>processor_</strong>* &#8212; SMBIOS Type 4, multiple CPU info files</p><p>These files are plain text, readable without root. Try: cat /sys/class/dmi/id/product_name &#8212; it works on your machine right now.</p><p>The raw binary tables are also exposed via /sys/firmware/dmi/tables/:</p><p>&#9656; <strong>DMI</strong> &#8212; the raw SMBIOS 2.x entry point structure</p><p>&#9656; <strong>smbios_entry_point</strong> &#8212; the SMBIOS 3.0 entry point (if present)</p><p>&#9656; <strong>DMI</strong> &#8212; the raw structure table</p><h3><strong>Userspace: dmidecode</strong></h3><p>The dmidecode tool reads these kernel interfaces. On modern kernels it prefers /sys/firmware/dmi/tables/ (the raw binary tables). On older systems it falls back to reading /dev/mem directly (requires root).</p><p>Common queries:</p><p>&#9656; <strong>dmidecode -t bios</strong> &#8212; BIOS vendor, version, release date, ROM size, characteristics</p><p>&#9656; <strong>dmidecode -t system</strong> &#8212; System manufacturer, product name, serial, UUID, wake-up type</p><p>&#9656; <strong>dmidecode -t baseboard</strong> &#8212; Motherboard vendor, product, version, asset tag</p><p>&#9656; <strong>dmidecode -t processor</strong> &#8212; Socket designation, type, family, speed, core count, thread count</p><p>&#9656; <strong>dmidecode -t memory</strong> &#8212; Each DIMM slot: size, type, speed, manufacturer, serial, part number</p><p>&#9656; <strong>dmidecode -t 17</strong> &#8212; Same as -t memory, but using the raw type number</p><p>These queries power inventory scripts in data centers, hardware detection in provisioning systems, and forensic analysis tools.</p><div><hr></div><h2><strong>The Full Flow in Summary</strong></h2><ol><li><p>Platform DXE driver populates SMBIOS type structures at boot</p></li><li><p>SmbiosProtocol-&gt;Add() stores each structure in an internal linked list</p></li><li><p>SmbiosTableConstruction() flattens the list, builds the EPS with correct checksums</p></li><li><p>gBS-&gt;InstallConfigurationTable() publishes the table to the EFI System Table</p></li><li><p>Linux kernel reads the table pointer from the EFI Configuration Table</p></li><li><p>dmi_decode() walks the structure table, fills dmi_dev entries</p></li><li><p>/sys/class/dmi/id/ is created with human-readable fields</p></li><li><p>dmidecode (or direct sysfs reads) gives userspace full access</p></li></ol><p>The same SMBIOS table that EDK2 assembled in firmware memory is what you read with dmidecode -t bios &#8212; with no intermediate format conversion, no parsing ambiguity, and no configuration. It just works.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://gdbplus.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading gdbplus's Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div><hr></div><p></p><div class="captioned-button-wrap" data-attrs="{&quot;url&quot;:&quot;https://gdbplus.substack.com/p/how-edk2-creates-smbiosdmi-tables?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="CaptionedButtonToDOM"><div class="preamble"><p class="cta-caption">Thanks for reading gdbplus's Substack! This post is public so feel free to share it.</p></div><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://gdbplus.substack.com/p/how-edk2-creates-smbiosdmi-tables?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="ButtonCreateButton"><a class="button primary" href="/__u/gdbplus.substack.com/p/how-edk2-creates-smbiosdmi-tables?utm_source=substack&amp;utm_medium=email&amp;utm_content=share&amp;action=share"><span>Share</span></a></p></div><p><strong>#firmware</strong> <strong>#uefi</strong> <strong>#edk2</strong> <strong>#linuxkernel</strong> <strong>#smbios</strong> <strong>#embedded</strong> <strong>#x86</strong> <strong>#gdbplus</strong></p>]]></content:encoded></item><item><title><![CDATA[STM32: When J-Link and Serial Both Go Silent ]]></title><description><![CDATA[&#8212; The BOOT0 Jumper Wire Recovery]]></description><link>https://gdbplus.substack.com/p/stm32-when-j-link-and-serial-both</link><guid isPermaLink="false">https://gdbplus.substack.com/p/stm32-when-j-link-and-serial-both</guid><dc:creator><![CDATA[gdbplus]]></dc:creator><pubDate>Sun, 21 Jun 2026 10:17:31 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!cxtO!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff96bfecf-272a-4ce2-98c6-69c1f7f4dc5f_2560x1440.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>David Zhu &#183; gdbplus &#183; June 2026</strong></p><div><hr></div><p>Your J-Link can&#8217;t connect. The serial download tool times out. The chip powers up but won&#8217;t talk.</p><p>You start questioning the hardware &#8212; is the MCU dead? Is the SWD trace broken? Did you fry something with static?</p><p>Then you remember: <strong>check the boot pins.</strong></p><div><hr></div><h2><strong>The Situation</strong></h2><p>I had an STM32 development board. The onboard pulldown resistor ties BOOT0 to GND &#8212; factory default, perfectly normal. The chip boots from Flash, as intended.</p><p>Two problems appeared simultaneously:</p><p>&#9656; <strong>Serial ISP (FlyMCU):</strong> No response on UART. The chip never entered the bootloader to listen.</p><p>&#9656; <strong>J-Link (SWD):</strong> Cannot connect. The debug probe handshake timed out, no matter how many times I power-cycled.</p><p>The power LED was on. The 3.3V rail measured fine. The chip wasn&#8217;t <em>dead</em> &#8212; it just couldn&#8217;t hear anything I was sending.</p><div><hr></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!cxtO!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff96bfecf-272a-4ce2-98c6-69c1f7f4dc5f_2560x1440.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!cxtO!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff96bfecf-272a-4ce2-98c6-69c1f7f4dc5f_2560x1440.png 424w, /__u/substackcdn.com/image/fetch/$s_!cxtO!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff96bfecf-272a-4ce2-98c6-69c1f7f4dc5f_2560x1440.png 848w, /__u/substackcdn.com/image/fetch/$s_!cxtO!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff96bfecf-272a-4ce2-98c6-69c1f7f4dc5f_2560x1440.png 1272w, /__u/substackcdn.com/image/fetch/$s_!cxtO!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff96bfecf-272a-4ce2-98c6-69c1f7f4dc5f_2560x1440.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!cxtO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff96bfecf-272a-4ce2-98c6-69c1f7f4dc5f_2560x1440.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f96bfecf-272a-4ce2-98c6-69c1f7f4dc5f_2560x1440.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:341285,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://gdbplus.substack.com/i/202937987?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff96bfecf-272a-4ce2-98c6-69c1f7f4dc5f_2560x1440.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!cxtO!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff96bfecf-272a-4ce2-98c6-69c1f7f4dc5f_2560x1440.png 424w, /__u/substackcdn.com/image/fetch/$s_!cxtO!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff96bfecf-272a-4ce2-98c6-69c1f7f4dc5f_2560x1440.png 848w, /__u/substackcdn.com/image/fetch/$s_!cxtO!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff96bfecf-272a-4ce2-98c6-69c1f7f4dc5f_2560x1440.png 1272w, /__u/substackcdn.com/image/fetch/$s_!cxtO!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff96bfecf-272a-4ce2-98c6-69c1f7f4dc5f_2560x1440.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2><strong>The Root Cause: BOOT0 = 0</strong></h2><p>STM32 determines its boot source by sampling BOOT0 and BOOT1 at the rising edge of nRST. The truth table is burned into every STM32 reference manual, but worth repeating:</p><p>&#9656; <strong>BOOT0=0, BOOT1=X:</strong> Main Flash memory (0x0800 0000) &#8212; the default</p><p>&#9656; <strong>BOOT0=1, BOOT1=0:</strong> System Memory bootloader (0x1FFF 0000) &#8212; the ISP interface</p><p>&#9656; <strong>BOOT0=1, BOOT1=1:</strong> Embedded SRAM (0x2000 0000)</p><p>When BOOT0=0, the ROM bootloader is completely bypassed. USART1 (PA9/PA10) &#8212; the pins your serial ISP tool expects &#8212; never gets initialized. The chip boots directly from whatever is in Flash.</p><p>If Flash contains corrupted data &#8212; bad stack pointer at 0x08000000, invalid reset vector, garbled option bytes that lock SWD &#8212; you&#8217;re in a Catch-22:</p><pre><code><code>// The boot sequence that traps you:
// 1. Power-on reset occurs
// 2. BOOT0 sampled: 0 &#8594; route to Flash
// 3. Core fetches SP from 0x08000000: garbage
// 4. Core fetches PC from 0x08000004: garbage
// 5. Core executes undefined instructions &#8594; undefined state
// 6. SWD/JTAG handshake fails (core is incoherent)
// 7. Serial ISP never activates (bootloader was bypassed in step 2)
//
// Result: two independent interfaces, both dead.
// The chip isn't bricked. It can't hear you because
// it never reached a state where anything is listening.
</code></code></pre><p>This is the STM32 equivalent of a catch-22: corrupted Flash prevents the debugger from attaching, but you need the debugger to fix the Flash.</p><div><hr></div><h2><strong>The Recovery: One Jumper Wire</strong></h2><p>The fix requires no soldering, no external programmer, no expensive tools. Just one Dupont wire:</p><h3><strong>Step 1: Force ISP Mode</strong></h3><p>Connect BOOT0 to 3.3V with a jumper wire. Ground BOOT1. Press RESET.</p><p>At the rising edge of nRST, the MCU samples BOOT0=1, BOOT1=0. This selects System Memory &#8212; the factory-programmed ROM bootloader. This bootloader is <strong>hardwired silicon</strong> &#8212; it doesn&#8217;t depend on Flash content. It lives in a dedicated ROM region that ST burned at the factory and cannot be corrupted.</p><pre><code><code>// The recovery sequence:
// 1. BOOT0 jumpered to 3.3V
// 2. BOOT1 jumpered to GND
// 3. Press RESET button (NRST low&#8594;high)
// 4. At reset edge: BOOT0=1, BOOT1=0 &#8594; System Memory
// 5. ROM bootloader activates
// 6. USART1 initialized, waiting for Flash Loader protocol
// 7. FlyMCU now sees the chip
</code></code></pre><h3><strong>Step 2: Flash via FlyMCU</strong></h3><p>Open FlyMCU. Select the correct COM port (check Device Manager if unsure). Baud rate 115200 &#8212; the reliable default. Load your HEX file. Click &#8220;Start Programming.&#8221;</p><p>The ROM bootloader receives the firmware over UART and writes it to Flash. This works because the bootloader has direct access to the Flash controller &#8212; it doesn&#8217;t need the Flash to be coherent first.</p><h3><strong>Step 3: Restore Normal Boot</strong></h3><p>Remove the BOOT0&#8594;3.3V jumper. Remove the BOOT1&#8594;GND jumper. Press RESET.</p><p>BOOT0 returns to GND via the onboard pulldown resistor. The chip boots from Flash &#8212; executing the freshly-written firmware. If your firmware blinks an LED, the LED blinks. Board is functional.</p><div><hr></div><h2><strong>The Bonus: J-Link Comes Back to Life</strong></h2><p>This is the non-obvious part. J-Link was failing <em>before</em> the ISP recovery &#8212; and it works <em>after</em>. Why?</p><p><strong>Before ISP:</strong> Flash contains garbage. The core fetches a nonsense stack pointer, a nonsense program counter, and executes undefined instructions. The SWD/JTAG debug logic needs a coherent core to negotiate the attach sequence. If the core is executing garbage or stuck in a fault loop, the handshake times out.</p><p><strong>After ISP:</strong> Flash contains a valid firmware image. Proper stack pointer at 0x08000000. Valid reset vector. Coherent code. The core enters a well-defined state after reset, and the debug probe can attach normally.</p><p>The ISP recovery didn&#8217;t <em>fix</em> J-Link &#8212; it fixed the <em>software state</em> that was preventing J-Link from connecting. The debug interface was always electrically fine. The core just wasn&#8217;t in a state where it could respond.</p><p>This is why ISP is the &#8220;master key&#8221; for STM32 recovery: it breaks the Catch-22 by sidestepping Flash entirely.</p><div><hr></div><h2><strong>Practical Takeaways</strong></h2><p>&#9656; <strong>Measure BOOT0 first.</strong> Before assuming a chip is bricked, put a multimeter on BOOT0 at reset. If it reads 0V, serial ISP will never work &#8212; the bootloader was never activated, regardless of what your flashing tool reports.</p><p>&#9656; <strong>ISP recovery is the lowest-dependency path.</strong> It only needs BOOT0=1, BOOT1=0, and a working UART. No external programmer. No special tools. No Segger license.</p><p>&#9656; <strong>J-Link failure &#8800; dead chip.</strong> The debug probe handshake can fail for software reasons. A corrupted Flash state is invisible to your multimeter but fatal to SWD. Clean firmware via ISP restores coherent state.</p><p>&#9656; <strong>Keep jumper wires in your kit.</strong> Two Dupont wires (BOOT0&#8594;3.3V, BOOT1&#8594;GND) are the cheapest STM32 recovery tool you can own. They take zero space and cost nothing. When you need them, they&#8217;re worth more than a Segger J-Link.</p><p>&#9656; <strong>The ROM bootloader is your insurance policy.</strong> ST burned it at the factory. It can&#8217;t be erased. It can&#8217;t be corrupted. It&#8217;s always there, waiting for BOOT0=1 and a valid Flash Loader command. Know how to reach it.</p><div><hr></div><h2><strong>Bottom Line</strong></h2><p>The STM32 boot pin configuration is the first thing that determines whether your chip can hear you. When BOOT0 sits at GND &#8212; as it does on most development boards &#8212; you&#8217;re locked into Flash boot. If Flash is corrupt, you lose both serial ISP and debug access simultaneously.</p><p>The fix is mechanically trivial (one jumper wire, one reset button press) but conceptually counterintuitive: you&#8217;re not fixing the debug interface. You&#8217;re bypassing Flash entirely and using a hardwired ROM path that predates your firmware.</p><p>Once you understand that the ROM bootloader is electrically independent of Flash health, the recovery sequence becomes obvious. The jumper wire isn&#8217;t a hack &#8212; it&#8217;s the documented escape hatch that ST designed into every chip.</p><div class="file-embed-wrapper" data-component-name="FileToDOM"><div class="file-embed-container-reader"><div class="file-embed-container-top"><image class="file-embed-thumbnail-default" src="/__u/substackcdn.com/image/fetch/$s_!0Cy0!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack.com%2Fimg%2Fattachment_icon.svg"></image><div class="file-embed-details"><div class="file-embed-details-h1">Stm32 Boot0 Isp Recovery A4</div><div class="file-embed-details-h2">1.27MB &#8729; PDF file</div></div><a class="file-embed-button wide" href="/__u/gdbplus.substack.com/api/v1/file/96987a82-69e0-4efd-9340-b3129e2c2ca8.pdf"><span class="file-embed-button-text">Download</span></a></div><a class="file-embed-button narrow" href="/__u/gdbplus.substack.com/api/v1/file/96987a82-69e0-4efd-9340-b3129e2c2ca8.pdf"><span class="file-embed-button-text">Download</span></a></div></div><div><hr></div><p><em>David Zhu writes about firmware, embedded systems, and the hardware-software boundary at <a href="/__u/gdbplus.substack.com/">gdbplus.substack.com</a>. If this saved you an hour of debugging, there&#8217;s more where it came from.</em></p><div class="captioned-button-wrap" data-attrs="{&quot;url&quot;:&quot;https://gdbplus.substack.com/p/stm32-when-j-link-and-serial-both?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="CaptionedButtonToDOM"><div class="preamble"><p class="cta-caption">Thanks for reading gdbplus's Substack! This post is public so feel free to share it.</p></div><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://gdbplus.substack.com/p/stm32-when-j-link-and-serial-both?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="ButtonCreateButton"><a class="button primary" href="/__u/gdbplus.substack.com/p/stm32-when-j-link-and-serial-both?utm_source=substack&amp;utm_medium=email&amp;utm_content=share&amp;action=share"><span>Share</span></a></p></div><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://gdbplus.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading gdbplus's Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p><strong>#STM32</strong> <strong>#Embedded</strong> <strong>#Firmware</strong> <strong>#Debugging</strong> <strong>#Microcontroller</strong></p>]]></content:encoded></item><item><title><![CDATA[SMBus: From Hardware to Firmware to Linux Kernel]]></title><description><![CDATA[How one 2-wire bus connects three engineering worlds &#8212; and why understanding all three makes you a better engineer in any one of them.]]></description><link>https://gdbplus.substack.com/p/smbus-from-hardware-to-firmware-to</link><guid isPermaLink="false">https://gdbplus.substack.com/p/smbus-from-hardware-to-firmware-to</guid><dc:creator><![CDATA[gdbplus]]></dc:creator><pubDate>Wed, 17 Jun 2026 11:25:09 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!XKHS!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F889d0646-2a15-4b76-9a4d-16ea8ae3f9bc_2560x1440.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>How one 2-wire bus connects three engineering worlds &#8212; and why understanding all three makes you a better engineer in any one of them.</strong></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!XKHS!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F889d0646-2a15-4b76-9a4d-16ea8ae3f9bc_2560x1440.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!XKHS!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F889d0646-2a15-4b76-9a4d-16ea8ae3f9bc_2560x1440.png 424w, /__u/substackcdn.com/image/fetch/$s_!XKHS!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F889d0646-2a15-4b76-9a4d-16ea8ae3f9bc_2560x1440.png 848w, /__u/substackcdn.com/image/fetch/$s_!XKHS!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F889d0646-2a15-4b76-9a4d-16ea8ae3f9bc_2560x1440.png 1272w, /__u/substackcdn.com/image/fetch/$s_!XKHS!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F889d0646-2a15-4b76-9a4d-16ea8ae3f9bc_2560x1440.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!XKHS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F889d0646-2a15-4b76-9a4d-16ea8ae3f9bc_2560x1440.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/889d0646-2a15-4b76-9a4d-16ea8ae3f9bc_2560x1440.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:478375,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://gdbplus.substack.com/i/202417270?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F889d0646-2a15-4b76-9a4d-16ea8ae3f9bc_2560x1440.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!XKHS!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F889d0646-2a15-4b76-9a4d-16ea8ae3f9bc_2560x1440.png 424w, /__u/substackcdn.com/image/fetch/$s_!XKHS!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F889d0646-2a15-4b76-9a4d-16ea8ae3f9bc_2560x1440.png 848w, /__u/substackcdn.com/image/fetch/$s_!XKHS!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F889d0646-2a15-4b76-9a4d-16ea8ae3f9bc_2560x1440.png 1272w, /__u/substackcdn.com/image/fetch/$s_!XKHS!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F889d0646-2a15-4b76-9a4d-16ea8ae3f9bc_2560x1440.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><div><hr></div><p>The System Management Bus is easy to overlook. Two wires. Seven-bit addresses. A handful of registers. How complicated can it be?</p><p>Very, as it turns out. Because SMBus is not one thing &#8212; it&#8217;s three things, depending on where you stand.</p><p>If you&#8217;re a hardware engineer, it&#8217;s a physical bus with voltage levels, pull-up resistors, and timing margins. If you&#8217;re a firmware engineer, it&#8217;s the thing you read SPD from before the memory controller initialises. If you&#8217;re a kernel engineer, it&#8217;s an i2c-i801 driver and a sysfs node.</p><p>Same two wires. Three completely different mental models.</p><div><hr></div><h2><strong>Hardware: The Physics of Two Wires</strong></h2><p>At the physical level, SMBus is deceptively simple. SMBCLK and SMBDAT, both open-drain, both pulled up to VDDIO &#8212; usually 1.8V or 3.3V depending on the platform generation. Standard speed is 100kHz. Fast mode pushes 400kHz. Devices are addressed with 7 bits.</p><p>But the simplicity hides teeth. SMBus is not I&#178;C. Here&#8217;s what trips people up:</p><p>&#9656; <strong>Timeout:</strong> SMBus mandates a 25-35ms clock-low timeout. I&#178;C devices can stretch the clock indefinitely. Drop an I&#178;C temperature sensor on an SMBus segment, and it may hold SCL low past the timeout &#8212; the SMBus controller declares a failure and the transaction dies mid-byte.</p><p>&#9656; <strong>Voltage domains:</strong> DDR4 DIMMs typically use 3.3V VDDSPD. But DDR5 moves to 1.8V I3C on the same pins for SPD &#8212; it&#8217;s not SMBus at all anymore. Plug a DDR5 DIMM into a board expecting to read SPD via SMBus and you get silence on the bus.</p><p>&#9656; <strong>Pull-up strength:</strong> SMBus high-power mode (SMBus 3.1) requires stronger pull-ups to achieve 400kHz on a loaded bus. The I&#178;C spec assumes a lightly loaded bus. A server with 16 DIMMs per channel has significant bus capacitance &#8212; weak pull-ups produce rounded edges that violate timing.</p><p>&#9656; <strong>Sideband signals:</strong> SMBALERT# lets slaves interrupt the host. The host must issue an Alert Response Address (ARA) read to discover which slave fired. If your firmware ignores SMBALERT# &#8212; which many platform bring-ups do &#8212; temperature sensors can assert it forever, and the bus stays in an interrupt storm.</p><p>The takeaway: if you&#8217;re a firmware or kernel engineer, you can&#8217;t treat SMBus as &#8220;just I&#178;C with a different name.&#8221; The physics matter when things break.</p><div><hr></div><h2><strong>Firmware: The Critical Path Through PEI</strong></h2><p>In UEFI firmware, SMBus matters most in a phase where almost nothing else works: PEI, before main memory is available.</p><p>Here&#8217;s the boot flow that catches every new platform team at least once:</p><ol><li><p>CPU comes out of reset. Cache-As-RAM (CAR) is set up. The PEI core starts.</p></li><li><p>The memory reference code (MRC) needs to know: what DIMMs are installed? How many ranks? What timings?</p></li><li><p>The answer is in the SPD EEPROM on each DIMM &#8212; accessible only via SMBus.</p></li><li><p>But the SMBus controller itself requires PCI enumeration to find its I/O base.</p></li><li><p>And PCI enumeration requires... at least some memory to be configured.</p></li></ol><p>This circular dependency is resolved by platform-specific PEI modules (PchSmbusPei on Intel, equivalent on AMD) that hardcode the SMBus controller&#8217;s PCI BDF (Bus 0, Device 1Fh, Function 4h) and read the I/O base directly from PCI config space offset 20h. No full enumeration &#8212; just one targeted config read, then I/O-space SMBus transactions.</p><p>&#9656; <strong>The data path:</strong> <code>SmbusPpi-&gt;Execute(0x50, ReadByte, offset, &amp;data)</code> &#8212; repeated 256 times for a full SPD dump. Each call hits the SMBus Host Controller registers: write slave address to HST_ADD, write command to HST_CMD, set START in HST_CNT, poll BUSY in HST_STS, read HST_DAT0.</p><p>&#9656; <strong>In DXE</strong>, the SmbusDxe driver publishes EFI_SMBUS_HC_PROTOCOL, wrapping the same I/O port access behind a protocol interface. DXE drivers and applications use SmbusLib for convenience.</p><p>&#9656; <strong>The protocol itself:</strong> Execute() for transactions, ArpDevice() for address discovery, GetArpMap() for the ARP table, Notify() for SMBALERT# callbacks. Most platforms only use Execute().</p><div><hr></div><h2><strong>Linux Kernel: The Bus Re-emerges</strong></h2><p>Once Linux boots, SMBus reappears through a completely different code path.</p><p>The i2c-i801 driver (for Intel) or i2c-piix4 driver (for AMD) probes the PCI device, maps the same I/O ports the firmware used, and registers an i2c_adapter. Then:</p><p>&#9656; <strong>i2c-core-smbus</strong> wraps the raw I&#178;C transactions in SMBus protocol semantics: <code>i2c_smbus_read_byte_data()</code>, <code>i2c_smbus_read_word_data()</code>, <code>i2c_smbus_read_block_data()</code>. These functions handle PEC (Packet Error Checking) and the 32-byte block limit.</p><p>&#9656; <strong>sysfs</strong> exposes devices at <code>/sys/bus/i2c/devices/i2c-0/</code>. Each probed slave appears as a subdirectory with its own attributes. The at24 EEPROM driver creates an <code>eeprom</code> file &#8212; <code>cat /sys/bus/i2c/devices/0-0050/eeprom</code> dumps raw SPD bytes.</p><p>&#9656; <strong>i2c-tools</strong> gives userspace command-line access: <code>i2cdetect -y 0</code> scans the bus, <code>i2cget -y 0 0x50 0x02 b</code> reads SPD byte 2 (memory type), <code>i2cdump</code> dumps the full device register space.</p><p>&#9656; <strong>decode-dimms</strong> (from i2c-tools) parses the raw SPD into human-readable timing tables &#8212; the CLI equivalent of what MRC does in silicon initialization.</p><div><hr></div><h2><strong>The Connection No One Talks About</strong></h2><p>Here&#8217;s what makes SMBus uniquely interesting as an engineering topic: firmware and the kernel touch the <strong>same physical hardware</strong> on the <strong>same bus</strong>, but they are completely disconnected from each other.</p><p>EDK2 reads SPD in PEI &#8594; trains the memory controller &#8594; boots the OS &#8594; hands off control. Linux probes i2c-i801 &#8594; finds the same SMBus controller &#8594; discovers the same SPD EEPROM at 0x50 &#8594; reads it fresh.</p><p>There is no handoff. No ACPI table that passes SPD data from firmware to kernel. The kernel probes the bus from scratch. This means:</p><p>&#9656; If firmware configured something incorrectly (wrong VDDIO, wrong bus speed), the kernel inherits that broken configuration.</p><p>&#9656; If a device was put into an unexpected state by firmware, the kernel may not be able to recover it.</p><p>&#9656; If the ACPI DSDT declares the SMBus controller&#8217;s resources incorrectly (overlapping I/O ranges, wrong interrupt routing), the kernel driver will fail to probe &#8212; even though the firmware just used the same controller successfully.</p><div><hr></div><h2><strong>Pitfalls I&#8217;ve Hit (So You Don&#8217;t Have To)</strong></h2><p>&#9656; <strong>SPD reads return all 0xFF on a new platform.</strong> The VDDIO rail for SMBus wasn&#8217;t enabled in the power sequencing. The EEPROM was unpowered. The bus looked idle (both lines high from pull-ups), so the controller read back 0xFF for every byte. Took two days to trace.</p><p>&#9656; <strong>i2c-i801 probe failure with &#8220;I/O resource conflict.&#8221;</strong> The ACPI DSDT declared the SMBus I/O range as a MotherboardResource, but the kernel&#8217;s resource management treated it as already-in-use. The fix: either fix the _CRS method in the DSDT, or use the <code>acpi_enforce_resources=lax</code> kernel parameter as a temporary workaround.</p><p>&#9656; <strong>SMBALERT# storm after a suspend/resume cycle.</strong> The firmware put the temperature sensor into a low-power mode during S3, but didn&#8217;t clear the SMBALERT# condition. On resume, the sensor immediately re-asserted the interrupt. The kernel&#8217;s i2c-i801 driver saw SMBALERT# stuck low and polled ARA in a tight loop &#8212; consuming 100% of one CPU core until the offending sensor was power-cycled.</p><p>&#9656; <strong>I&#178;C slave on an SMBus segment caused intermittent bus hangs.</strong> A third-party I&#178;C GPIO expander held SCL low for 40ms during a multi-byte write &#8212; 5ms past the SMBus timeout. The SMBus controller declared a timeout and reset the bus, corrupting the in-flight SPD read from the DIMM. The symptom: memory training would sometimes fail on cold boot, but always work on warm boot.</p><div><hr></div><h2><strong>Bottom Line</strong></h2><p>SMBus is a great topic for engineers who want to understand how the stack actually works &#8212; not just their layer, but the layers above and below.</p><p>Hardware engineers who understand what MRC does with SPD data design better power sequencing. Firmware engineers who understand SMBus timing write PEI modules that don&#8217;t hang. Kernel engineers who understand the firmware boot path debug i2c-i801 probe failures faster.</p><p>Same two wires. Three worlds. Knowing all three makes you better at any one.</p><div><hr></div><p><em>David Zhu writes about firmware, platform architecture, and the hardware-firmware boundary at <a href="/__u/gdbplus.substack.com/">gdbplus.substack.com</a>.</em></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://gdbplus.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading gdbplus's Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div class="captioned-button-wrap" data-attrs="{&quot;url&quot;:&quot;https://gdbplus.substack.com/p/smbus-from-hardware-to-firmware-to?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="CaptionedButtonToDOM"><div class="preamble"><p class="cta-caption">Thanks for reading gdbplus's Substack! This post is public so feel free to share it.</p></div><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://gdbplus.substack.com/p/smbus-from-hardware-to-firmware-to?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="ButtonCreateButton"><a class="button primary" href="/__u/gdbplus.substack.com/p/smbus-from-hardware-to-firmware-to?utm_source=substack&amp;utm_medium=email&amp;utm_content=share&amp;action=share"><span>Share</span></a></p></div>]]></content:encoded></item><item><title><![CDATA[/dev/mem + mmap: Accessing ACPI MMIO Regions from Linux Userspace]]></title><description><![CDATA[A deep-dive into physical memory access from userspace &#8212; no kernel driver needed.]]></description><link>https://gdbplus.substack.com/p/devmem-mmap-accessing-acpi-mmio-regions</link><guid isPermaLink="false">https://gdbplus.substack.com/p/devmem-mmap-accessing-acpi-mmio-regions</guid><dc:creator><![CDATA[gdbplus]]></dc:creator><pubDate>Wed, 03 Jun 2026 10:58:18 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!-Yvk!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21e86e21-f8e0-4041-88ef-b44dc1dc2eda_2560x1440.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>By David Zhu | GDBplus</p><p>Most developers interact with hardware through kernel drivers. The kernel abstracts away physical addresses behind files, sysfs nodes, and ioctl() interfaces. But there&#8217;s a direct path: <code>/dev/mem</code> &#8212; the physical memory device &#8212; combined with <code>mmap()</code>, lets userspace map arbitrary physical addresses and read or write them as if they were regular memory.</p><p>This article explores how that mechanism works, with a real-world example: accessing ACPI MMIO regions (specifically, the PCIe ECAM space) from a userspace tool.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!-Yvk!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21e86e21-f8e0-4041-88ef-b44dc1dc2eda_2560x1440.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!-Yvk!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21e86e21-f8e0-4041-88ef-b44dc1dc2eda_2560x1440.png 424w, /__u/substackcdn.com/image/fetch/$s_!-Yvk!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21e86e21-f8e0-4041-88ef-b44dc1dc2eda_2560x1440.png 848w, /__u/substackcdn.com/image/fetch/$s_!-Yvk!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21e86e21-f8e0-4041-88ef-b44dc1dc2eda_2560x1440.png 1272w, /__u/substackcdn.com/image/fetch/$s_!-Yvk!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21e86e21-f8e0-4041-88ef-b44dc1dc2eda_2560x1440.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!-Yvk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21e86e21-f8e0-4041-88ef-b44dc1dc2eda_2560x1440.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/21e86e21-f8e0-4041-88ef-b44dc1dc2eda_2560x1440.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:434228,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://gdbplus.substack.com/i/200434244?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21e86e21-f8e0-4041-88ef-b44dc1dc2eda_2560x1440.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!-Yvk!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21e86e21-f8e0-4041-88ef-b44dc1dc2eda_2560x1440.png 424w, /__u/substackcdn.com/image/fetch/$s_!-Yvk!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21e86e21-f8e0-4041-88ef-b44dc1dc2eda_2560x1440.png 848w, /__u/substackcdn.com/image/fetch/$s_!-Yvk!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21e86e21-f8e0-4041-88ef-b44dc1dc2eda_2560x1440.png 1272w, /__u/substackcdn.com/image/fetch/$s_!-Yvk!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21e86e21-f8e0-4041-88ef-b44dc1dc2eda_2560x1440.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><div><hr></div><h2><strong>1. What Is /dev/mem?</strong></h2><p><code>/dev/mem</code> is a character device (major 1, minor 1) implemented in <code>drivers/char/mem.c</code>. It represents the system&#8217;s entire physical address space as a seekable file. Every byte in physical memory &#8212; RAM, MMIO ranges, flash, PCI configuration space &#8212; is addressable via read()/write() at the corresponding file offset.</p><pre><code><code>// Reading byte at physical address 0xFED00000
fd = open("/dev/mem", O_RDONLY);
lseek(fd, 0xFED00000, SEEK_SET);
read(fd, &amp;value, 4);
</code></code></pre><p>But <code>read()</code> and <code>write()</code> are slow for repeated access &#8212; each call is a syscall, and the kernel copies data into a temporary buffer. That&#8217;s where <code>mmap()</code> changes everything.</p><div><hr></div><h2><strong>2. mmap: Mapping Physical Memory to Userspace</strong></h2><p><code>mmap()</code> creates a direct virtual-to-physical mapping in the process&#8217;s page tables. After the mmap() call succeeds, every load or store instruction at the mapped address hits the physical device directly &#8212; no kernel mediation, no syscall overhead.</p><pre><code><code>fd = open("/dev/mem", O_RDWR | O_SYNC);
void *ptr = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE,
                 MAP_SHARED, fd, 0xFED00000);

// Now userspace can read/write directly:
uint32_t reg = *(volatile uint32_t *)ptr;
</code></code></pre><h3><strong>How the Kernel Implements It</strong></h3><p>When <code>mmap()</code> is called on a <code>/dev/mem</code> fd, the kernel&#8217;s VFS layer dispatches to the <code>mmap_mem()</code> function in <code>drivers/char/mem.c</code>:</p><ol><li><p><strong>Validation</strong>: <code>valid_phys_addr_range(offset, size)</code> checks whether the requested physical address is in an allowed range. On systems with <code>CONFIG_STRICT_DEVMEM=y</code>, this restricts access to ranges explicitly listed in the kernel&#8217;s <code>iomem_resource</code> tree.</p></li><li><p><strong>Page table setup</strong>: <code>remap_pfn_range()</code> creates PTE entries mapping the userspace virtual address to the physical page frame number. The <code>pgprot_uncached</code> attribute ensures MMIO reads bypass the CPU cache &#8212; critical for hardware registers.</p></li><li><p><strong>Fault handling</strong>: On first access, the CPU&#8217;s MMU walks the page tables and finds a valid mapping. From then on, the page is present and no page fault occurs.</p></li></ol><p>The result: a userspace pointer that behaves like a direct window into physical memory.</p><div><hr></div><h2><strong>3. Kernel Guards: STRICT_DEVMEM and Lockdown</strong></h2><p>Not every physical address is fair game. Linux has several guardrails:</p><p>&#9656; <strong>CONFIG_STRICT_DEVMEM</strong> &#8212; When enabled (default on most distros), <code>/dev/mem</code> rejects reads to arbitrary RAM but allows access to I/O memory (MMIO) ranges listed in <code>/proc/iomem</code>. The function <code>devmem_is_allowed()</code> in <code>arch/x86/mm/init.c</code> governs this on x86.</p><p>&#9656; <strong>CONFIG_IO_STRICT_DEVMEM</strong> &#8212; A stricter variant that also blocks MMIO access. Rarely enabled by default.</p><p>&#9656; <strong>Kernel Lockdown</strong> &#8212; When Secure Boot is active or the kernel is in lockdown mode, <code>/dev/mem</code> is completely disabled. The kernel refuses to open the device, returning <code>-EPERM</code>.</p><p>&#9656; <strong>Page alignment</strong>: The offset passed to mmap() must be page-aligned. You can still access unaligned addresses by adding an intra-page offset to the returned pointer.</p><p>On modern Ubuntu and Fedora, you&#8217;ll typically need to either disable Secure Boot, boot with <code>iomem=relaxed</code>, or add <code>nokaslr</code> to the kernel command line to use <code>/dev/mem</code> freely.</p><div><hr></div><h2><strong>4. ACPI MMIO Access: A Concrete Example</strong></h2><p>Let&#8217;s walk through a real use case: reading PCIe device configuration space via the MCFG (Memory-mapped Configuration) table, without using the Linux PCI subsystem.</p><h3><strong>Step 1: Find the RSDP</strong></h3><p>The Root System Description Pointer (RSDP) is the entry point for all ACPI tables. It&#8217;s located at one of three places:</p><p>&#9656; <strong>EBDA</strong> (Extended BIOS Data Area): The segment stored at <code>0x40E</code> in low memory<br>&#9656; <strong>0xE0000&#8211;0xFFFFF</strong>: The BIOS ROM area<br>&#9656; <strong>UEFI system table</strong>: On UEFI systems, available via <code>/sys/firmware/efi/systab</code></p><pre><code><code>// Find RSDP on legacy BIOS
uint16_t ebda_seg = *(uint16_t *)(0x40E);
uint8_t *ebda = (uint8_t *)(ebda_seg &lt;&lt; 4);
// Search for "RSD PTR " signature in EBDA and 0xE0000&#8211;0xFFFFF
</code></code></pre><h3><strong>Step 2: Walk RSDP &#8594; XSDT &#8594; MCFG</strong></h3><p>The RSDP contains a pointer to the XSDT (Extended System Description Table). The XSDT is an array of 64-bit pointers to other ACPI tables. Iterate through the entries and find the one with signature &#8220;MCFG&#8221;:</p><pre><code><code>XSDT *xsdt = (XSDT *)(uintptr_t)rsdp-&gt;XsdtAddress;
int entries = (xsdt-&gt;Header.Length - sizeof(ACPI_TABLE_HEADER)) / 8;
for (int i = 0; i &lt; entries; i++) {
    ACPI_TABLE_HEADER *hdr = (ACPI_TABLE_HEADER *)(uintptr_t)xsdt-&gt;Entry[i];
    if (memcmp(hdr-&gt;Signature, "MCFG", 4) == 0) {
        // Found MCFG
    }
}
</code></code></pre><h3><strong>Step 3: Extract the ECAM Base Address</strong></h3><p>The MCFG table contains one or more allocation entries, each describing a PCI segment group:</p><pre><code><code>typedef struct {
    uint64_t BaseAddress;
    uint16_t SegmentGroup;
    uint8_t  StartBus;
    uint8_t  EndBus;
    uint32_t Reserved;
} MCFG_ENTRY;

uint64_t ecam_base = mcfg_entry-&gt;BaseAddress;  // e.g., 0xE0000000
</code></code></pre><p>The ECAM (Enhanced Configuration Access Mechanism) maps the 4KB PCI config space of each device function to a flat MMIO range. The mapping formula is:</p><pre><code><code>ECAM offset = (Bus &lt;&lt; 20) | (Device &lt;&lt; 15) | (Function &lt;&lt; 12) + Register
</code></code></pre><h3><strong>Step 4: Map the ECAM Region via /dev/mem</strong></h3><p>Now that we know the physical base address, we map it:</p><pre><code><code>int fd = open("/dev/mem", O_RDONLY | O_SYNC);
size_t ecam_size = (mcfg_entry-&gt;EndBus - mcfg_entry-&gt;StartBus + 1) * 32 * 8 * 4096;
void *ecam = mmap(NULL, ecam_size, PROT_READ, MAP_SHARED, fd, ecam_base);
</code></code></pre><h3><strong>Step 5: Read PCI Config Registers</strong></h3><p>With the mapping in place, we can enumerate every device on the bus:</p><pre><code><code>void read_pci_config(void *ecam, int bus, int dev, int func, int offset) {
    size_t ecam_off = ((size_t)bus &lt;&lt; 20) | ((size_t)dev &lt;&lt; 15) |
                      ((size_t)func &lt;&lt; 12) | offset;
    volatile uint32_t *reg = (uint32_t *)((uint8_t *)ecam + ecam_off);
    uint32_t value = *reg;
    // Check if device exists: Vendor ID != 0xFFFF
    if ((value &amp; 0xFFFF) != 0xFFFF) {
        printf("Bus %02x Dev %02x Func %x: VID=0x%04x DID=0x%04x\n",
               bus, dev, func, value &amp; 0xFFFF, value &gt;&gt; 16);
    }
}
</code></code></pre><p>This is precisely what tools like <code>pciutils</code> (lspci) and UEFI firmware do internally &#8212; the difference is that we&#8217;re doing it from userspace with zero kernel assistance per access.</p><div><hr></div><h2><strong>5. When to Use This (And When Not To)</strong></h2><p>&#9656; <strong>Use /dev/mem + mmap when:</strong></p><ul><li><p>You&#8217;re performing bare-metal debugging (memory dumps, register inspection)</p></li><li><p>You need to access hardware registers that have no kernel driver</p></li><li><p>You&#8217;re writing a diagnostic tool that reads firmware tables (ACPI, SMBIOS)</p></li><li><p>You need the speed: one mmap(), unlimited zero-copy reads</p></li></ul><p>&#9656; <strong>Don&#8217;t use /dev/mem when:</strong></p><ul><li><p>A kernel driver already provides the interface (use sysfs/iio instead)</p></li><li><p>The platform has Secure Boot / Lockdown enabled</p></li><li><p>You need portability (it&#8217;s x86/ARM-specific and requires root)</p></li><li><p>The target is production code &#8212; this is a debugging and development technique</p></li></ul><div><hr></div><h2><strong>6. Beyond ECAM: Other ACPI MMIO Targets</strong></h2><p>The same technique applies to any MMIO region described in ACPI tables:</p><p>&#9656; <strong>FADT &#8594; PM1a registers</strong>: Power management event/control registers at the physical address in <code>FADT.Pm1aEventBlock</code> and <code>FADT.Pm1aControlBlock</code></p><p>&#9656; <strong>FADT &#8594; GPE0 registers</strong>: General Purpose Event registers for wake and SCI routing</p><p>&#9656; <strong>MADT &#8594; LAPIC address</strong>: Local APIC base for reading APIC IDs and flags</p><p>&#9656; <strong>DMAR &#8594; IOMMU registers</strong>: DMA remapping hardware registers for VT-d inspection</p><p>&#9656; <strong>DBG2 &#8594; Debug port</strong>: Serial debug port MMIO for early boot console output</p><p>All of these follow the same pattern: find the table, extract the physical address, open <code>/dev/mem</code>, mmap, and access.</p><div><hr></div><h2><strong>7. Performance: Why mmap Wins</strong></h2><p><strong>MethodOverhead per AccessSuitable For</strong><code>read()</code> on <code>/dev/mem</code>1 syscall + kernel copyOccasional register reads<code>mmap()</code> on <code>/dev/mem</code>0 (direct VA&#8594;PA in TLB)Repeated register polling, scan<code>pread()</code> on sysfs file1 syscall + driver callback + copyOne-time attribute readsKernel driver + ioctl()1 syscall + context switchProduction device I/O</p><p>For any workload that needs more than a handful of register accesses, mmap is orders of magnitude faster. The cost is paid once at mapping time; every subsequent access is a single load/store instruction.</p><div><hr></div><h2><strong>Summary</strong></h2><p><code>/dev/mem</code> is the Linux kernel&#8217;s most direct interface to physical hardware. Combined with <code>mmap()</code>, it gives userspace unfiltered access to any physical address &#8212; including ACPI MMIO regions &#8212; with the performance of a direct virtual-to-physical mapping.</p><p>The ACPI MMIO example demonstrates the full workflow: locate the RSDP, walk the table chain, extract an ECAM base address from the MCFG, map it via <code>/dev/mem</code>, and read PCI configuration registers from userspace with zero per-access overhead.</p><p>It&#8217;s a powerful technique for firmware debugging, hardware diagnostics, and low-level system inspection &#8212; provided you navigate the kernel&#8217;s security guardrails.</p><div><hr></div><p><em>Deep-dive content like this, every week. Follow on LinkedIn and subscribe at <a href="/__u/gdbplus.substack.com/">gdbplus.substack.com</a>.</em></p>]]></content:encoded></item><item><title><![CDATA[16550 UART Hardware Flow Control: What Every BIOS Engineer Should Know]]></title><description><![CDATA[By David Zhu | GDBplus]]></description><link>https://gdbplus.substack.com/p/16550-uart-hardware-flow-control</link><guid isPermaLink="false">https://gdbplus.substack.com/p/16550-uart-hardware-flow-control</guid><dc:creator><![CDATA[gdbplus]]></dc:creator><pubDate>Sat, 30 May 2026 02:35:37 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!vkdl!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F958c2684-5ef5-4cf3-8948-b0d4cd86117e_2560x1440.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>By David Zhu | GDBplus</strong></p><div><hr></div><p>If you&#8217;ve ever debugged a firmware hang by watching serial output, you&#8217;ve trusted the 16550 UART to deliver every byte. But here&#8217;s the uncomfortable truth: at 115200 bps, that 16-byte RX FIFO fills in 1.4 milliseconds. If your ISR doesn&#8217;t drain it in time &#8212; and in a BIOS environment, SMI handlers, PCI enumeration storms, and DXE dispatcher overhead all compete for CPU &#8212; the bytes are <strong>silently gone</strong>.</p><p>RTS/CTS hardware flow control is the fix. Let&#8217;s walk through what the 16550 actually does when FCR[6] and FCR[7] are set</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!vkdl!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F958c2684-5ef5-4cf3-8948-b0d4cd86117e_2560x1440.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!vkdl!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F958c2684-5ef5-4cf3-8948-b0d4cd86117e_2560x1440.png 424w, /__u/substackcdn.com/image/fetch/$s_!vkdl!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F958c2684-5ef5-4cf3-8948-b0d4cd86117e_2560x1440.png 848w, /__u/substackcdn.com/image/fetch/$s_!vkdl!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F958c2684-5ef5-4cf3-8948-b0d4cd86117e_2560x1440.png 1272w, /__u/substackcdn.com/image/fetch/$s_!vkdl!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F958c2684-5ef5-4cf3-8948-b0d4cd86117e_2560x1440.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!vkdl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F958c2684-5ef5-4cf3-8948-b0d4cd86117e_2560x1440.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/958c2684-5ef5-4cf3-8948-b0d4cd86117e_2560x1440.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:304551,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://gdbplus.substack.com/i/199823533?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F958c2684-5ef5-4cf3-8948-b0d4cd86117e_2560x1440.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!vkdl!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F958c2684-5ef5-4cf3-8948-b0d4cd86117e_2560x1440.png 424w, /__u/substackcdn.com/image/fetch/$s_!vkdl!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F958c2684-5ef5-4cf3-8948-b0d4cd86117e_2560x1440.png 848w, /__u/substackcdn.com/image/fetch/$s_!vkdl!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F958c2684-5ef5-4cf3-8948-b0d4cd86117e_2560x1440.png 1272w, /__u/substackcdn.com/image/fetch/$s_!vkdl!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F958c2684-5ef5-4cf3-8948-b0d4cd86117e_2560x1440.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>.</p><div><hr></div><h2><strong>1. The Problem: Why BIOS Serial is Fragile</strong></h2><p>The 16550 is the workhorse UART in PC platforms. The Super I/O (or eSPI-attached EC) exposes it. EDK2&#8217;s <code>SerialPortLib</code> talks to it. And it has a 16-byte FIFO.</p><p>At 115200 bps, with 8N1 framing:</p><p>&#9656; <strong>1 byte on the wire:</strong> (1 start + 8 data + 1 stop) / 115200 = <strong>86.8 &#956;s</strong></p><p>&#9656; <strong>16-byte FIFO fills in:</strong> 16 &#215; 86.8 &#956;s = <strong>1.39 ms</strong></p><p>&#9656; <strong>Your ISR deadline:</strong> drain the FIFO in under 1.39 ms, every time, forever</p><p>In a real BIOS boot path, 1.39 ms is nothing. An SMI can hold the CPU for hundreds of microseconds. PEI memory initialization can block for milliseconds. A single PCI config space read that triggers an SERR# can stall long enough to overflow the FIFO.</p><p>The result? <strong>Silent data loss.</strong> The UART&#8217;s Line Status Register won&#8217;t flag an overrun until it&#8217;s already happening &#8212; and by then you&#8217;ve lost bytes you can&#8217;t recover. Your debug log has gaps. Your console redirection drops characters. And you spend hours chasing a bug that doesn&#8217;t exist in the code.</p><div><hr></div><h2><strong>2. Out-of-Band Signaling: RTS and CTS</strong></h2><p>Hardware flow control uses two dedicated signal lines that operate independently of the TXD/RXD data path:</p><p>&#9656; <strong>RTS (Request to Send) &#8212; UART output.</strong> When the 16550 asserts RTS (drives it low), it&#8217;s telling the remote peer: &#8220;I&#8217;m ready, send data.&#8221; When RTS goes high, it means: &#8220;Stop now.&#8221;</p><p>&#9656; <strong>CTS (Clear to Send) &#8212; UART input.</strong> The 16550 monitors this pin. When the remote peer de-asserts CTS, the 16550 stops transmitting &#8212; no firmware action needed.</p><p>This is <strong>out-of-band</strong> signaling. The RTS/CTS state changes consume zero bits of the data stream. Unlike XON/XOFF (which embeds 0x11/0x13 in the data), RTS/CTS works identically whether you&#8217;re sending ASCII debug text or raw binary firmware capsule updates.</p><div><hr></div><h2><strong>3. The Registers That Matter</strong></h2><p>In a BIOS codebase (EDK2 or coreboot), you touch these registers:</p><p>&#9656; <strong>FCR (FIFO Control Register, offset 2, DLAB=0 required)</strong></p><ul><li><p><strong>Bit 0:</strong> FIFO Enable (must be 1 for auto flow control)</p></li><li><p><strong>Bit 6:</strong> Auto-RTS Enable &#8212; hardware manages RTS autonomously</p></li><li><p><strong>Bit 7:</strong> Auto-CTS Enable &#8212; hardware responds to CTS autonomously</p></li><li><p><strong>Bits 7:6 (Write-only FCR):</strong> RX FIFO trigger level (00=1, 01=4, 10=8, 11=14 bytes)</p></li></ul><p>&#9656; <strong>MCR (Modem Control Register, offset 4)</strong></p><ul><li><p><strong>Bit 1:</strong> RTS manual control &#8212; only relevant when Auto-RTS is off</p></li></ul><p>&#9656; <strong>MSR (Modem Status Register, offset 6)</strong></p><ul><li><p><strong>Bit 4:</strong> CTS pin state (read-only)</p></li></ul><p>&#9656; <strong>IER (Interrupt Enable Register, offset 1, DLAB=0)</strong></p><ul><li><p><strong>Bit 3:</strong> EDSSI &#8212; Modem Status Interrupt, fires on CTS/DSR/DCD/RI changes</p></li></ul><p><strong>Real EDK2-style initialization:</strong></p><pre><code><code>//
// Configure 16550: enable FIFO + Auto-RTS + Auto-CTS
// Trigger level: 14 bytes (FCR[7:6] = 11)
//
UINT8 FcrValue = (UINT8)((0x3 &lt;&lt; 6) |   // Trigger at 14 bytes
                          (0x1 &lt;&lt; 3)  |   // DMA Mode Select
                          (0x1 &lt;&lt; 0));     // FIFO Enable
IoWrite8 (gSerialIoBase + FCR_OFFSET, FcrValue);

//
// Read back IIR (offset 2, same address as FCR but read-only)
// to verify FIFOs are enabled (IIR[7:6] = 11 for 16550)
//
UINT8 IirValue = IoRead8 (gSerialIoBase + IIR_OFFSET);
if ((IirValue &amp; 0xC0) == 0xC0) {
  DEBUG ((EFI_D_INFO, "16550 FIFO + Auto Flow Control active\n"));
}
</code></code></pre><div><hr></div><h2><strong>4. Auto-RTS: The Hardware Automaton</strong></h2><p>When FCR[6] = 1, the 16550 contains a comparator that tracks the RX FIFO fill level against the trigger threshold. The sequence is:</p><ol><li><p>RX FIFO crosses the trigger level &#8594; hardware <strong>de-asserts RTS</strong> (drives it high) on the next bit boundary</p></li><li><p>Remote peer detects RTS change &#8594; stops transmitting after the current byte completes</p></li><li><p>Your ISR fires, reads the FIFO, FIFO drops below trigger</p></li><li><p>Hardware <strong>re-asserts RTS</strong> (drives it low) &#8594; remote peer resumes</p></li></ol><p><strong>This entire loop runs in hardware.</strong> The critical path from &#8220;FIFO hits trigger&#8221; to &#8220;RTS changes&#8221; is measured in nanoseconds &#8212; it&#8217;s comparator &#8594; output pin, no firmware in the loop.</p><p><strong>Trigger level trade-off for BIOS use:</strong></p><p>&#9656; <strong>1 byte (FCR[7:6]=00):</strong> ISR fires for every byte. Lowest latency, highest CPU overhead. Almost never used in BIOS.</p><p>&#9656; <strong>4 bytes (FCR[7:6]=01):</strong> Conservative. Many BIOS defaults. Leaves 12 bytes of headroom.</p><p>&#9656; <strong>8 bytes (FCR[7:6]=10):</strong> Balanced. About 700 &#956;s between interrupts at 115200 bps.</p><p>&#9656; <strong>14 bytes (FCR[7:6]=11):</strong> Fewest interrupts. But only 2 bytes of slack (16 - 14 = 2). At 115200, that&#8217;s 174 &#956;s for the remote peer to react to RTS &#8212; tight but workable for modern UARTs.</p><p><strong>BIOS-specific note:</strong> During SMM entry, the CPU saves state and the SMI handler runs in SMRAM. If the SMI handler takes 300 &#956;s, a 14-byte trigger level means you have only ~174 &#956;s of slack after RTS de-asserts. If the remote peer&#8217;s RTS detection latency exceeds that window, bytes 15 and 16 can still be lost <em>even with Auto-RTS enabled</em>. For SMI-heavy firmware, consider a 8-byte trigger level for more margin.</p><div><hr></div><h2><strong>5. Auto-CTS: Transparent TX Gating</strong></h2><p>When FCR[7] = 1, the 16550&#8217;s TX state machine is gated by the CTS input:</p><p>&#9656; Remote peer de-asserts CTS &#8594; 16550 pauses TX <strong>mid-byte</strong> if necessary</p><p>&#9656; CTS re-asserted &#8594; TX resumes from the exact stopping point</p><p>&#9656; The THR (Transmit Holding Register) maintains its contents</p><p>The firmware writes to THR as usual. The hardware handles the rest. Your <code>SerialPortWrite()</code> function in <code>SerialPortLib</code> doesn&#8217;t need to check CTS &#8212; the chip does it.</p><p><strong>Common BIOS bug:</strong> I&#8217;ve seen platforms where Auto-CTS is enabled but the CTS pin is unconnected (floating) on the board. The 16550 sees a random logic level and either blocks TX permanently or lets it through. The symptom: serial output works on some boards and is completely dead on others. The fix: a pulldown resistor on CTS, or disabling Auto-CTS if RTS/CTS handshake isn&#8217;t physically wired.</p><div><hr></div><h2><strong>6. Manual Flow Control: When You Can&#8217;t Use Auto</strong></h2><p>If your platform&#8217;s 16550 variant doesn&#8217;t support Auto-RTS/CTS (some older clones don&#8217;t), you can implement flow control in firmware using MCR and MSR:</p><p><strong>Polling approach (PEI phase, before interrupts):</strong></p><pre><code><code>while (BytesToSend &gt; 0) {
  // Wait for THR empty
  while ((IoRead8 (Base + LSR_OFFSET) &amp; B_THR_EMPTY) == 0);
  // Check CTS before writing
  if ((IoRead8 (Base + MSR_OFFSET) &amp; B_CTS) != 0) {
    IoWrite8 (Base + THR_OFFSET, *Buffer++);
    BytesToSend--;
  }
  // If CTS is de-asserted, spin &#8212; peer isn't ready
}
</code></code></pre><p><strong>Interrupt-driven (DXE phase):</strong></p><pre><code><code>// In SerialPortLib initialization:
// Enable Modem Status Interrupt (EDSSI)
IoWrite8 (Base + IER_OFFSET, (B_RX_AVAILABLE | B_THR_EMPTY |
                                B_RX_STATUS   | B_MODEM_STATUS));

// The ISR checks IIR to identify the interrupt source:
UINT8 IirValue = IoRead8 (Base + IIR_OFFSET);
if ((IirValue &amp; IIR_MODEM_STATUS) != 0) {
  // CTS changed &#8212; re-evaluate TX readiness
  UINT8 MsrValue = IoRead8 (Base + MSR_OFFSET);
  if ((MsrValue &amp; B_CTS) != 0) {
    // CTS is asserted, resume transmitting
    ResumeTransmit();
  }
}
</code></code></pre><p>The problem with manual flow control: <strong>latency.</strong> From CTS change to ISR execution, you&#8217;ve got interrupt controller latency + CPU interrupt gate delay + potential SMI preemption. In BIOS, this can easily exceed 100 &#956;s. At 115200 bps, that&#8217;s a full byte missed.</p><p>Auto flow control (FCR[6]=1, FCR[7]=1) eliminates this entirely. The hardware reacts in nanoseconds.</p><div><hr></div><h2><strong>7. Hardware Flow Control vs. XON/XOFF</strong></h2><p>If you&#8217;re debugging a BIOS, you&#8217;re sending binary data: firmware capsule updates, memory dumps, PCI config space hex dumps, compressed debug payloads. That makes the choice clear.</p><p>&#9656; <strong>Signal path:</strong> RTS/CTS uses dedicated physical wires, completely outside the data stream. XON/XOFF injects 0x11 (XON) and 0x13 (XOFF) directly into the byte stream.</p><p>&#9656; <strong>Binary compatibility:</strong> RTS/CTS works with any data &#8212; hex dumps, raw flash images, compressed logs. XON/XOFF breaks catastrophically when 0x11 or 0x13 appears in the payload.</p><p>&#9656; <strong>Response latency:</strong> RTS/CTS is hardware-level (nanoseconds). XON/XOFF requires the receiving UART to recognize the byte, generate an interrupt, and have the ISR process it &#8212; easily 50&#8211;100 &#956;s in BIOS.</p><p>&#9656; <strong>Hardware requirement:</strong> RTS/CTS needs five wires (TXD, RXD, RTS, CTS, GND). XON/XOFF needs only three (TXD, RXD, GND).</p><p>&#9656; <strong>Real BIOS scenario:</strong> You&#8217;re capturing a full PCI config space dump over serial at 921600 bps. The hex dump contains bytes 0x00&#8211;0xFF uniformly. XON/XOFF will randomly pause and resume your stream based on data content. RTS/CTS won&#8217;t.</p><p><strong>Bottom line for BIOS engineers:</strong> If you&#8217;re doing any serial communication that involves binary data &#8212; and most BIOS debugging does &#8212; hardware flow control is not optional. It&#8217;s the only reliable option.</p><div><hr></div><h2><strong>8. Common Pitfalls in BIOS Environments</strong></h2><p>&#9656; <strong>DLAB gate:</strong> FCR lives at I/O offset 2, but only when LCR[7] (DLAB) = 0. If you write FCR while DLAB is set, you&#8217;re writing to the divisor latch instead. Your &#8220;flow control enable&#8221; silently becomes a garbage baud rate divisor.</p><p>&#9656; <strong>FIFO must be enabled first:</strong> Auto-RTS and Auto-CTS are features of the FIFO mode. If FCR[0] = 0 (16450 compatibility mode, no FIFO), FCR[6] and FCR[7] are ignored. This catches platforms that set FCR in two separate writes &#8212; a &#8220;FIFO enable&#8221; write followed by a &#8220;flow control&#8221; write &#8212; where an intermediate read flushes the write-only FCR state.</p><p>&#9656; <strong>SMI latency breaks Auto-RTS:</strong> 14-byte trigger at 115200 bps leaves only 174 &#956;s of slack. If SMI handlers consume 200+ &#956;s, the remote peer may not have time to react to RTS de-assertion before bytes overflow. Test with worst-case SMI duration, not average.</p><p>&#9656; <strong>Floating CTS pin:</strong> Auto-CTS (FCR[7]=1) with unconnected CTS pin &#8594; random TX behavior across boots and boards. Always verify CTS is either physically connected or pulled to an active-low (asserted) state with a resistor.</p><p>&#9656; <strong>Super I/O configuration dependency:</strong> The 16550 inside a Super I/O (like NCT6796D or IT8625E) may default to RTS/CTS pins disabled. You must configure the Super I/O&#8217;s Logical Device registers (usually via LPC/eSPI at 0x2E/0x4E) to enable the UART&#8217;s RTS/CTS pins before the 16550 registers have any effect.</p><p>&#9656; <strong>EDK2 SerialPortLib:</strong> Most <code>SerialPortLib</code> implementations don&#8217;t configure flow control &#8212; they just set baud rate and enable the FIFO. If your platform needs hardware flow control, you&#8217;ll need to customize <code>SerialPortInitialize()</code> to write FCR with the Auto-RTS/CTS bits set.</p><div><hr></div><h2><strong>Summary</strong></h2><p>Two bits. FCR[6] and FCR[7]. That&#8217;s all it takes to transform your serial port from a best-effort debug channel into a reliable transport. The hardware automaton inside the 16550 handles the rest &#8212; no ISR latency to worry about, no polling loops, no silent data loss.</p><p>For BIOS engineers shipping platform firmware, hardware flow control isn&#8217;t a nice-to-have. It&#8217;s the difference between debug logs you can trust and debug logs that lie to you.</p><div><hr></div><p><em>This article is part of the GDBplus firmware internals series. Full annotated source code walkthroughs: <a href="/__u/gdbplus.substack.com/">gdbplus.substack.com</a></em></p><p><strong>#UEFI #BIOS #Firmware #16550 #EDK2 #Embedded</strong></p>]]></content:encoded></item><item><title><![CDATA[How .vfr Files Form a UEFI Setup Page — The Complete Pipeline]]></title><description><![CDATA[By David Zhu &#183; gdbplus.substack.com]]></description><link>https://gdbplus.substack.com/p/how-vfr-files-form-a-uefi-setup-page</link><guid isPermaLink="false">https://gdbplus.substack.com/p/how-vfr-files-form-a-uefi-setup-page</guid><dc:creator><![CDATA[gdbplus]]></dc:creator><pubDate>Sun, 24 May 2026 12:08:13 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!fbCn!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1145a02-fd8f-4af6-a5f5-bac2cc4cbee9_2560x1440.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>By David Zhu &#183; <a href="/__u/gdbplus.substack.com/">gdbplus.substack.com</a></strong></p><div><hr></div><p>When you enter BIOS Setup and see those configuration menus &#8212; the gray-and-blue screens with dropdowns, checkboxes, and numeric inputs &#8212; you&#8217;re looking at the output of a remarkably elegant pipeline. That pipeline starts with a single <code>.vfr</code> file.</p><p>VFR stands for <strong>Visual Form Representation</strong>. It&#8217;s a declarative language in the UEFI specification that describes <em>what</em> a setup page looks like, not <em>how</em> to render it. Think of it as HTML for firmware configuration screens &#8212; you declare the form structure, and the UEFI HII (Human Interface Infrastructure) handles all the rendering.</p><p>But a <code>.vfr</code> file doesn&#8217;t work alone. It&#8217;s part of a carefully orchestrated build-time + runtime pipeline involving at least 5 other file types. Here&#8217;s how they all fit together</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!fbCn!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1145a02-fd8f-4af6-a5f5-bac2cc4cbee9_2560x1440.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!fbCn!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1145a02-fd8f-4af6-a5f5-bac2cc4cbee9_2560x1440.png 424w, /__u/substackcdn.com/image/fetch/$s_!fbCn!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1145a02-fd8f-4af6-a5f5-bac2cc4cbee9_2560x1440.png 848w, /__u/substackcdn.com/image/fetch/$s_!fbCn!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1145a02-fd8f-4af6-a5f5-bac2cc4cbee9_2560x1440.png 1272w, /__u/substackcdn.com/image/fetch/$s_!fbCn!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1145a02-fd8f-4af6-a5f5-bac2cc4cbee9_2560x1440.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!fbCn!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1145a02-fd8f-4af6-a5f5-bac2cc4cbee9_2560x1440.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b1145a02-fd8f-4af6-a5f5-bac2cc4cbee9_2560x1440.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:381654,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://gdbplus.substack.com/i/199060859?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1145a02-fd8f-4af6-a5f5-bac2cc4cbee9_2560x1440.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!fbCn!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1145a02-fd8f-4af6-a5f5-bac2cc4cbee9_2560x1440.png 424w, /__u/substackcdn.com/image/fetch/$s_!fbCn!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1145a02-fd8f-4af6-a5f5-bac2cc4cbee9_2560x1440.png 848w, /__u/substackcdn.com/image/fetch/$s_!fbCn!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1145a02-fd8f-4af6-a5f5-bac2cc4cbee9_2560x1440.png 1272w, /__u/substackcdn.com/image/fetch/$s_!fbCn!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1145a02-fd8f-4af6-a5f5-bac2cc4cbee9_2560x1440.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>.</p><div><hr></div><h2><strong>The File Cast &#8212; Who Does What</strong></h2><p>&#9656; <strong>Vfr.vfr</strong> &#8212; The form definition. Declares the visual layout: questions, oneof dropdowns, numeric inputs, checkboxes, ordered lists, and conditional display rules (SuppressIf, GrayOutIf). Written in VFR language &#8212; a C-like DSL specified in the UEFI spec.</p><p>&#9656; <strong>Strings.uni</strong> &#8212; Unicode string tokens. Every piece of text visible on the setup page (question prompts, option labels, help text) is stored here as a token. This enables multi-language support &#8212; swap the UNI file, and your BIOS speaks Chinese, Japanese, or German without touching the VFR.</p><p>&#9656; <strong>Vfr.h / Driver.h</strong> &#8212; Header with GUIDs and class definitions. VFR needs GUID references for protocols, variable GUIDs, and form set GUIDs. These are defined here so both the VFR and C code share the same constants.</p><p>&#9656; <strong>Driver.inf</strong> &#8212; EDK2 module description. Lists VFR and UNI files as sources so the build system knows to compile them. Defines which protocols the driver consumes and produces.</p><p>&#9656; <strong>Driver.c / HiiConfigAccess.c</strong> &#8212; The runtime driver. Registers forms with the HII Database, publishes EFI_HII_CONFIG_ACCESS_PROTOCOL, and implements the callback functions (ExtractConfig, RouteConfig, Callback) that process user input.</p><p>&#9656; <strong>Vfr.vfrbin</strong> &#8212; The compiled IFR (Internal Forms Representation) binary. This is what actually gets embedded in the firmware volume. It&#8217;s a compact, tokenized representation of your VFR + UNI. A full configuration page with 50 questions compiles to about 2KB.</p><div><hr></div><h2><strong>Stage-by-Stage Pipeline</strong></h2><h3><strong>Stage 1 &#8212; Author: Write VFR + UNI</strong></h3><p>You start by writing two files.</p><p>The <strong>VFR file</strong> looks like a structured C header:</p><pre><code><code>formset
  guid = FORMSET_GUID,
  title = STRING_TOKEN(STR_FORM_SET_TITLE),
  help  = STRING_TOKEN(STR_FORM_SET_HELP),

  form
    formid = FORM_ID_MAIN,
    title  = STRING_TOKEN(STR_MAIN_FORM_TITLE);

    oneof varid = MyDriver.EnableFeature,
      prompt = STRING_TOKEN(STR_ENABLE_PROMPT),
      help   = STRING_TOKEN(STR_ENABLE_HELP),
      option text = STRING_TOKEN(STR_DISABLE), value = 0, flags = DEFAULT;
      option text = STRING_TOKEN(STR_ENABLE),  value = 1, flags = 0;
    endoneof;

    numeric varid = MyDriver.BaudRate,
      prompt = STRING_TOKEN(STR_BAUD_PROMPT),
      help   = STRING_TOKEN(STR_BAUD_HELP),
      minimum = 9600, maximum = 115200, step = 0, default = 115200;
    endnumeric;
  endform;
endformset;
</code></code></pre><p>Every <code>STRING_TOKEN(...)</code> references the <strong>UNI file</strong>:</p><pre><code><code>#langdef en-US "English"
#string STR_FORM_SET_TITLE   #language en-US "Serial Port Configuration"
#string STR_ENABLE_PROMPT    #language en-US "Enable Serial Console"
#string STR_ENABLE           #language en-US "Enabled"
#string STR_DISABLE          #language en-US "Disabled"
</code></code></pre><p>This separation is critical. The VFR describes <em>structure</em>; the UNI provides <em>content</em>. When Intel needs to ship the same BIOS Setup in 12 languages, they swap the UNI file &#8212; the VFR never changes.</p><h3><strong>Stage 2 &#8212; Compile: VfrCompiler &#8594; IFR Binary</strong></h3><p>At build time, <code>VfrCompiler</code> (a tool in EDK2&#8217;s BaseTools) takes VFR + UNI and produces:</p><ul><li><p><strong>Vfr.vfrbin</strong> &#8212; IFR binary. Compact, token-optimized. Every form, question, option, and conditional is encoded in a byte stream that the HII parser reads at runtime.</p></li><li><p><strong>Vfr.h</strong> (auto-generated) &#8212; C definitions for form IDs, question IDs, and offset calculations, so your driver code can reference form elements by name.</p></li></ul><p><strong>Pitfall:</strong> VfrCompiler has notoriously cryptic error messages. A missing semicolon in a VFR file can produce &#8220;syntax error at line 1&#8221; with no further context. Always compile incrementally &#8212; add one form element at a time.</p><p><strong>Pitfall:</strong> String token IDs are auto-generated from UNI ordering. If you insert a new string at the top of the UNI file, ALL subsequent token IDs shift. Your VFR will silently reference the wrong strings. Solution: use explicit <code>#string TOKEN_NAME</code> and never rely on implicit ordering.</p><h3><strong>Stage 3 &#8212; Package: Embed in Firmware Volume</strong></h3><p>The build system (GenFfs + GenFv) takes the IFR binary and string package and embeds them as FFS (Firmware File System) sections inside the driver&#8217;s FFS file.</p><p>Your Driver.inf declares the inputs:</p><pre><code><code>[Sources]
  Driver.c
  Vfr.vfr
  Strings.uni

[Guids]
  gEfiIfrFrameworkGuid            # IFR binary section type
  gEfiStringPackageGuid           # String package section type
</code></code></pre><p>The result: a single FFS file containing your driver executable PLUS the IFR form definition PLUS the string package. The HII infrastructure can locate all three at runtime from one FFS file.</p><h3><strong>Stage 4 &#8212; Register: Driver Binds to HII Database</strong></h3><p>When the DXE dispatcher loads your driver, the driver&#8217;s entry point does three things:</p><ol><li><p><strong>Installs EFI_HII_DATABASE_PROTOCOL</strong> &#8212; registers form package (IFR binary) and string package with the database.</p></li><li><p><strong>Installs EFI_HII_CONFIG_ACCESS_PROTOCOL</strong> &#8212; exposes callback functions the Form Browser calls when the user interacts with your form.</p></li><li><p><strong>Returns EFI_HII_PACKAGE_LIST_PROTOCOL</strong> &#8212; so the HII infrastructure can discover your forms.</p></li></ol><p>The critical data structure is the <strong>HII handle</strong> &#8212; a handle that groups together your form package, string package, and config access callback.</p><pre><code><code>Status = gHiiDatabase-&gt;NewPackageList(
    gHiiDatabase, &amp;PackageListHeader,
    NULL, &amp;mHiiHandle);
</code></code></pre><p>At this point, your form exists in the HII database but is not yet visible. Like a web server that&#8217;s registered a route but hasn&#8217;t received a request yet.</p><h3><strong>Stage 5 &#8212; Parse: HII Infrastructure Reads IFR</strong></h3><p>When the user enters BIOS Setup (typically F2/Del during POST), the <strong>Form Browser</strong> (a DXE driver: <code>SetupBrowserDxe</code> or <code>UiApp</code>) activates.</p><p>The Form Browser calls <code>EFI_HII_DATABASE_PROTOCOL.GetPackageList()</code> to retrieve ALL registered form packages. It then parses each IFR binary bytecode &#8212; walking the opcode stream to build an internal representation:</p><ul><li><p><code>EFI_IFR_FORM_SET_OP</code> &#8594; new form set begins</p></li><li><p><code>EFI_IFR_ONE_OF_OP</code> &#8594; dropdown question</p></li><li><p><code>EFI_IFR_NUMERIC_OP</code> &#8594; numeric input question</p></li><li><p><code>EFI_IFR_SUPPRESS_IF_OP</code> &#8594; conditional visibility</p></li><li><p><code>EFI_IFR_GRAY_OUT_IF_OP</code> &#8594; conditional disable</p></li></ul><p>String tokens are resolved against the string package in real time.</p><h3><strong>Stage 6 &#8212; Render: Form Browser Paints the Page</strong></h3><p>This is where the magic happens &#8212; and where VFR&#8217;s declarative nature shines.</p><p>The Form Browser has a <strong>rendering engine</strong> that converts IFR opcodes into on-screen UI elements. You did not write a single line of pixel-drawing code. The Form Browser handles:</p><ul><li><p>Layout (vertical stacking of questions)</p></li><li><p>Widget rendering (dropdown vs. text field vs. checkbox)</p></li><li><p>Keyboard navigation (arrow keys, Enter, Escape)</p></li><li><p>Default value highlighting</p></li><li><p>SuppressIf / GrayOutIf evaluation</p></li><li><p>Multi-page form handling</p></li><li><p>Help text popup (F1)</p></li></ul><p>This is why every UEFI BIOS Setup screen looks similar &#8212; they all use the same Form Browser engine. The VFR only describes <em>content</em>; the rendering is standardized.</p><h3><strong>Stage 7 &#8212; User Interacts: Config Routing Protocol</strong></h3><p>When the user changes a setting (toggles Enable Serial Console from &#8220;Disabled&#8221; to &#8220;Enabled&#8221;) and presses F10 to save, here&#8217;s what happens:</p><ol><li><p>Form Browser collects all changed values into a <strong>configuration string</strong> (EFI_HII_CONFIG_ROUTING_PROTOCOL format).</p></li><li><p>Form Browser calls <code>RouteConfig()</code> on your driver&#8217;s <strong>EFI_HII_CONFIG_ACCESS_PROTOCOL</strong>.</p></li><li><p>The config string looks like: <code>GUID=...&amp;OFFSET=...&amp;WIDTH=...&amp;VALUE=...</code></p></li><li><p>Your <code>RouteConfig()</code> parses this string, validates the input, and applies it.</p></li></ol><pre><code><code>EFI_STATUS RouteConfig(
    IN CONST EFI_HII_CONFIG_ACCESS_PROTOCOL *This,
    IN CONST EFI_STRING Configuration,
    OUT EFI_STRING *Progress) {
  // Parse Configuration string
  // Validate values
  // Apply to hardware or EFI variable
}
</code></code></pre><h3><strong>Stage 8 &#8212; Callback: Dynamic Form Updates</strong></h3><p>Sometimes the form needs to respond to user input <em>before</em> F10 is pressed. Example: toggling &#8220;Enable Serial Console&#8221; from Disabled to Enabled should reveal additional questions (Baud Rate, Data Bits, Parity).</p><p>This is handled by the <code>Callback()</code> function:</p><pre><code><code>EFI_STATUS Callback(
    IN CONST EFI_HII_CONFIG_ACCESS_PROTOCOL *This,
    IN EFI_BROWSER_ACTION Action,
    IN EFI_QUESTION_ID QuestionId,
    IN UINT8 Type,
    IN OUT EFI_IFR_TYPE_VALUE *Value,
    OUT EFI_BROWSER_ACTION_REQUEST *ActionRequest) {

  if (Action == EFI_BROWSER_ACTION_CHANGED &amp;&amp;
      QuestionId == QUESTION_ID_ENABLE_SERIAL) {
    // User toggled the checkbox. Tell Form Browser to refresh.
    *ActionRequest = EFI_BROWSER_ACTION_REQUEST_FORM_APPLY;
  }
}
</code></code></pre><p>The key action values:</p><ul><li><p><strong>EFI_BROWSER_ACTION_CHANGED</strong> &#8212; user changed a value (triggers SuppressIf re-evaluation)</p></li><li><p><strong>EFI_BROWSER_ACTION_RETRIEVE</strong> &#8212; form is about to be displayed (load defaults)</p></li><li><p><strong>EFI_BROWSER_ACTION_FORM_OPEN</strong> &#8212; form is opening (initialize context)</p></li><li><p><strong>EFI_BROWSER_ACTION_SUBMITTED</strong> &#8212; user pressed F10 (final validation)</p></li></ul><div><hr></div><h2><strong>The Complete File Set</strong></h2><p>Every UEFI setup page requires these files, minimum:</p><p>&#9656; <strong>Vfr.vfr</strong> &#8212; Form layout declaration (VFR DSL)</p><p>&#9656; <strong>Strings.uni</strong> &#8212; Display text tokens (Unicode string tokens)</p><p>&#9656; <strong>Driver.c</strong> &#8212; Runtime driver logic (C)</p><p>&#9656; <strong>Driver.inf</strong> &#8212; Module build definition (EDK2 INF)</p><p>Plus these generated outputs:</p><p>&#9656; <strong>Vfr.vfrbin</strong> &#8212; VfrCompiler &#8594; FFS packaging (IFR binary)</p><p>&#9656; <strong>Vfr.h</strong> &#8212; VfrCompiler &#8594; Driver.c includes (C definitions)</p><p>&#9656; <strong>FFS file</strong> &#8212; GenFfs / GenFv &#8594; DXE Dispatcher (executable + IFR + strings)</p><p>&#9656; <strong>String package</strong> &#8212; Build system &#8594; HII String Protocol (runtime display text)</p><div><hr></div><h2><strong>Why This Architecture Matters</strong></h2><p>The VFR &#8594; IFR &#8594; HII Browser pipeline is one of UEFI&#8217;s most underappreciated design decisions. Here&#8217;s why it&#8217;s elegant:</p><p><strong>1. Separation of content and presentation.</strong> The VFR describes the form. The Form Browser owns the rendering. You never write UI code. This is cleaner than Linux kernel&#8217;s Kconfig (which mixes menu structure and build logic) or U-Boot&#8217;s cmdline approach (no structured forms at all).</p><p><strong>2. Localization is free.</strong> Swap the UNI file, and your BIOS Setup speaks any language. No code changes. Intel ships the same EDK2 firmware image worldwide &#8212; only the UNI packages differ by region.</p><p><strong>3. Declarative conditionals.</strong> SuppressIf and GrayOutIf are evaluated by the Form Browser at display time. You don&#8217;t need to track UI state. When the user toggles a checkbox, Callback() fires &#8594; ActionRequest &#8594; Form Browser re-evaluates all conditionals &#8594; layout updates automatically.</p><p><strong>4. Config Routing decouples storage from UI.</strong> The form doesn&#8217;t care where values are stored &#8212; EFI variables, driver buffer, hardware registers. RouteConfig() is your translation layer. Change storage backend without touching the VFR.</p><p><strong>5. IFR is compact.</strong> A full configuration page with 50 questions compiles to maybe 2KB of IFR binary. Compare this to a JSON or XML representation &#8212; the binary opcode format was designed for embedded systems where every byte counts.</p><div><hr></div><h2><strong>Common Pitfalls</strong></h2><p>&#9656; <strong>VfrCompiler error: &#8220;syntax error&#8221; with no line number.</strong> Add <code>--preprocess</code> flag to see the preprocessed VFR output. The error is usually in a <strong>#include</strong>&#8216;d header, not the VFR itself.</p><p>&#9656; <strong>String token mismatch.</strong> UNI file order changed, tokens shifted, wrong strings appear. Use <code>#string TOKEN_NAME</code> (explicit) and never rely on auto-generated token IDs.</p><p>&#9656; <strong>Callback not firing.</strong> Forgot to set <code>EFI_IFR_FLAG_CALLBACK</code> on the question in VFR. Without it, the Form Browser never calls your Callback() function.</p><p>&#9656; <strong>RouteConfig returning EFI_UNSUPPORTED.</strong> Your config string parser is rejecting the format. Check the <code>OFFSET</code> field &#8212; it must match exactly what ExtractConfig() returns.</p><p>&#9656; <strong>Form not appearing in Setup Browser.</strong> Driver didn&#8217;t register with HII Database, or the HII handle was lost. Ensure <code>NewPackageList()</code> succeeds and <code>mHiiHandle</code> is valid.</p><p>&#9656; <strong>SuppressIf never updates.</strong> You need to request <code>EFI_BROWSER_ACTION_REQUEST_FORM_APPLY</code> in your Callback() &#8212; otherwise the Form Browser doesn&#8217;t know to re-evaluate conditionals.</p><div><hr></div><h2><strong>Bottom Line</strong></h2><p>VFR is a declarative form description language. You write what the form looks like in VFR, provide display strings in UNI, compile to IFR binary, embed in firmware, and the HII Form Browser does everything else at runtime.</p><p>Your driver&#8217;s job is only three things:</p><ol><li><p>Register the form + string packages with HII Database</p></li><li><p>Implement ExtractConfig() / RouteConfig() for reading/writing values</p></li><li><p>Implement Callback() for dynamic form behavior</p></li></ol><p>Everything else &#8212; layout, rendering, keyboard navigation, help text &#8212; is handled by the standard UEFI HII infrastructure. That&#8217;s the power of the VFR pipeline.</p><div><hr></div><p><em>Full source code walkthrough of a real EDK2 VFR driver, with annotated VFR/UNI/C files: <a href="/__u/gdbplus.substack.com/">gdbplus.substack.com</a></em></p><p><em><strong>#UEFI</strong> <strong>#EDK2</strong> <strong>#Firmware</strong> <strong>#BIOS</strong> <strong>#EmbeddedSystems</strong> <strong>#HII</strong></em></p>]]></content:encoded></item><item><title><![CDATA[0x5A — The Command That Lets Flash Chips Describe Themselves]]></title><description><![CDATA[Linkedin: DavidZhu]]></description><link>https://gdbplus.substack.com/p/0x5a-the-command-that-lets-flash</link><guid isPermaLink="false">https://gdbplus.substack.com/p/0x5a-the-command-that-lets-flash</guid><dc:creator><![CDATA[gdbplus]]></dc:creator><pubDate>Wed, 20 May 2026 11:50:32 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!EuH6!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F769b4706-8ced-44dd-974e-2df33c05b7c1_2560x1440.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a href="https://www.linkedin.com/in/david-zhu-3a68a855/">Linkedin: DavidZhu</a></p><p>SPI NOR flash has been a boot firmware workhorse for decades. But for most of that history, every flash chip was a black box &#8212; you either had the datasheet hardcoded into your bootloader, or you were in trouble.</p><p>JEDEC fixed this with <strong>JESD216</strong>: the Serial Flash Discoverable Parameters (SFDP) standard. And the key that unlocks it all is a single command byte: <strong>0x5A</strong>.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!EuH6!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F769b4706-8ced-44dd-974e-2df33c05b7c1_2560x1440.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!EuH6!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F769b4706-8ced-44dd-974e-2df33c05b7c1_2560x1440.png 424w, /__u/substackcdn.com/image/fetch/$s_!EuH6!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F769b4706-8ced-44dd-974e-2df33c05b7c1_2560x1440.png 848w, /__u/substackcdn.com/image/fetch/$s_!EuH6!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F769b4706-8ced-44dd-974e-2df33c05b7c1_2560x1440.png 1272w, /__u/substackcdn.com/image/fetch/$s_!EuH6!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F769b4706-8ced-44dd-974e-2df33c05b7c1_2560x1440.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!EuH6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F769b4706-8ced-44dd-974e-2df33c05b7c1_2560x1440.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/769b4706-8ced-44dd-974e-2df33c05b7c1_2560x1440.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:511277,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://gdbplus.substack.com/i/198546558?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F769b4706-8ced-44dd-974e-2df33c05b7c1_2560x1440.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!EuH6!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F769b4706-8ced-44dd-974e-2df33c05b7c1_2560x1440.png 424w, /__u/substackcdn.com/image/fetch/$s_!EuH6!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F769b4706-8ced-44dd-974e-2df33c05b7c1_2560x1440.png 848w, /__u/substackcdn.com/image/fetch/$s_!EuH6!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F769b4706-8ced-44dd-974e-2df33c05b7c1_2560x1440.png 1272w, /__u/substackcdn.com/image/fetch/$s_!EuH6!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F769b4706-8ced-44dd-974e-2df33c05b7c1_2560x1440.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><div><hr></div><h2><strong>The Problem: Every Flash Chip Was a Snowflake</strong></h2><p>Before SFDP, firmware had to maintain a lookup table mapping JEDEC manufacturer IDs to flash capabilities. See ID 0xC2 (Macronix)? Great, now check the model byte. Model 0x20? That&#8217;s 3V, 256Mb, Quad I/O. Model 0x19? Different erase sizes, slower max frequency.</p><p>This broke in three ways:</p><ul><li><p><strong>New chips broke old firmware</strong> &#8212; a new density variant with the same ID bytes needed a code update.</p></li><li><p><strong>Dead chips killed boot</strong> &#8212; if your lookup table was in flash and your flash died, you couldn&#8217;t discover a replacement.</p></li><li><p><strong>Every vendor had their own quirks</strong> &#8212; same density, same I/O, different erase granularity. Your table grew endlessly.</p></li></ul><p>The industry needed the flash chip to <strong>declare its own capabilities</strong> &#8212; not rely on the host to already know them.</p><div><hr></div><h2><strong>Enter SFDP: Flash Self-Description</strong></h2><p>JEDEC JESD216 defines the SFDP standard. Every compliant flash chip stores a structured parameter table in a dedicated internal ROM area. The host reads this table at boot using command <strong>0x5A</strong> (Read SFDP, or RDSFDP).</p><p>The transaction is straightforward:</p><p>&#9656; <strong>CS# low</strong> &#8212; Assert chip select. The flash is now listening.</p><p>&#9656; <strong>0x5A</strong> &#8212; Send the RDSFDP command byte on MOSI. This is the magic number.</p><p>&#9656; <strong>24-bit Address</strong> &#8212; 3 bytes specifying the offset into the SFDP address space. The SFDP table lives in its own memory map, separate from the main flash array. Address 0x000000 reads the SFDP header.</p><p>&#9656; <strong>1 Dummy Byte</strong> &#8212; One turnaround cycle. The flash switches its internal data path from MOSI (receiving) to MISO (transmitting). On the MX25L25635F, 0x5A always needs exactly 1 dummy cycle &#8212; this is declared in the SFDP table itself.</p><p>&#9656; <strong>Continuous Clock</strong> &#8212; Keep toggling SCK. The flash streams SFDP data bytes on MISO until you stop. Pull CS# high to end the transaction.</p><div><hr></div><h2><strong>The SFDP Header: 8 Bytes That Unlock Everything</strong></h2><p>Reading offset 0x00 returns the 8-byte SFDP header:</p><p>From JESD216 Section 6.3:</p><p>&#9656; <strong>Signature</strong> (Bytes 0&#8211;3): 0x50444653. That&#8217;s &#8220;SFDP&#8221; in ASCII, stored little-endian (&#8221;S&#8221; = 0x53 is byte 0, &#8220;P&#8221; = 0x50 is byte 1...). If these 4 bytes don&#8217;t match, the flash doesn&#8217;t support SFDP &#8212; fall back to the old ID lookup.</p><p>&#9656; <strong>Minor Version</strong> (Byte 4): 0x06 on the MX25L25635F. This maps to JESD216B, which added support for 4-byte addressing and additional parameter tables.</p><p>&#9656; <strong>Major Version</strong> (Byte 5): 0x01. Combined with minor, this tells you which version of the JESD216 spec the table follows.</p><p>&#9656; <strong>NPH &#8212; Number of Parameter Headers</strong> (Byte 6): 0x01 on the MX25L25635F. This chip has one parameter table. High-end flash might have 2&#8211;3 (basic SPI + 4-byte addressing + sector map).</p><p>&#9656; <strong>Access Protocol</strong> (Byte 7): 0xFF. Reserved/unused for standard SPI flash.</p><div><hr></div><h2><strong>The Parameter Header: Where to Go Next</strong></h2><p>Immediately after the SFDP header (offset 0x08), the parameter header tells you where the actual capability data lives:</p><p>From JESD216, each parameter header is 2 DWORDs (8 bytes):</p><p>&#9656; <strong>Parameter ID</strong> (Byte 0): 0xFF. This is the JEDEC-assigned ID for the &#8220;Basic SPI Flash Parameters&#8221; table &#8212; the one every SPI NOR flash must provide.</p><p>&#9656; <strong>Minor Revision</strong> (Byte 1): 0x00. Version of this specific parameter table.</p><p>&#9656; <strong>Table Length</strong> (Byte 2): Number of DWORDs in the table. For basic SPI, typically 16 DWORDs (0x10).</p><p>&#9656; <strong>Table Pointer</strong> (Bytes 4&#8211;6): 3-byte address where this parameter table starts in the SFDP space. On the MX25L25635F, this is 0x000030.</p><p>With the pointer in hand, you issue a second 0x5A command &#8212; this time with address 0x000030 &#8212; and read the 16-DWORD parameter table.</p><div><hr></div><h2><strong>What the Parameter Table Reveals About MX25L25635F</strong></h2><p>The 16 DWORDs (64 bytes) of the basic SPI parameter table tell you everything:</p><p>&#9656; <strong>DWORD[1] &#8212; Flash Density:</strong> The first DWORD after the parameter header encodes the total memory size as a bit count. For 256 megabits, this field returns 0x1FFFFFFF or similar encoding, allowing the host to compute 2^N bytes. The MX25L25635F reports 256Mb = 32MB = 33,554,432 bytes.</p><p>&#9656; <strong>DWORD[2] &#8212; I/O Mode Support:</strong> Bits [3:0] declare the supported read commands. The MX25L25635F advertises four modes &#8212; 1-1-1 (standard SPI, 0x03), 1-1-2 (dual output, 0x3B), 1-1-4 (quad output, 0x6B), and 1-4-4 (quad I/O, 0xEB). This lets the bootloader auto-select the fastest read mode the flash supports.</p><p>&#9656; <strong>DWORD[2] &#8212; Maximum Frequency:</strong> Bit [30] indicates support for &gt;100MHz. The MX25L25635F supports up to 133MHz in quad I/O read mode.</p><p>&#9656; <strong>DWORD[4] &#8212; Erase Types:</strong> Three erase instruction sizes are declared. Type 1 = 4KB subsector erase (0x20), Type 2 = 32KB half-block erase (0x52), Type 3 = 64KB block erase (0xD8). The flash advertises typical erase times for each type, so the host can set appropriate timeouts without guessing.</p><p>&#9656; <strong>DWORD[7] &#8212; Status Register Polling:</strong> Bit [3] declares whether the flash supports software write-in-progress polling via the status register. The MX25L25635F sets this bit, meaning the host can poll SR[0] instead of using fixed delays after each write/erase. This is critical for fast flash update performance.</p><p>&#9656; <strong>DWORD[9&#8211;10] &#8212; 4-Byte Addressing:</strong> Since 256Mb fits in a 24-bit address space (2^24 &#215; 8 bits = 128Mb, but 256Mb = 2^28 bits, and with page/block organization, addressing is byte-based: 32MB = 2^25 bytes), the MX25L25635F uses 3-byte addressing natively. Bits in DWORD[9] confirm this &#8212; no 4-byte address mode is needed.</p><p>&#9656; <strong>DWORD[15] &#8212; Quad Enable Requirements:</strong> Declares whether QE (Quad Enable) sits in a non-volatile status register (requiring Write Status Register to toggle) or a volatile configuration register. The MX25L25635F uses SR[6] as the QE bit &#8212; non-volatile, so it survives power cycles but needs explicit clearing if you want to run in 1-1-1 mode.</p><div><hr></div><h2><strong>Why This Matters in UEFI / EDK2 Firmware</strong></h2><p>In the EDK2 firmware stack, SPI NOR flash initialization typically happens inside a platform-specific PEIM (Pre-EFI Initialization Module) or early DXE driver. The conventional approach uses the JEDEC ID (command 0x9F) to look up flash parameters from a hardcoded table.</p><p>SFDP changes the flow:</p><p>&#9656; <strong>Read 0x9F</strong> (JEDEC ID) &#8212; Get manufacturer and model bytes. This still runs first, because 0x9F works on every SPI flash ever made. But now it&#8217;s just for logging and sanity checks.</p><p>&#9656; <strong>Read 0x5A</strong> (SFDP Header at offset 0x00) &#8212; Verify the &#8220;SFDP&#8221; signature (0x50444653). If it&#8217;s there, proceed with SFDP. If not, fall back to the legacy lookup.</p><p>&#9656; <strong>Read 0x5A</strong> (Parameter Table at pointer offset) &#8212; Parse the 16-DWORD basic SPI parameter table. Extract density, supported I/O modes, erase types, timing parameters, and status register layout.</p><p>&#9656; <strong>Auto-configure the SPI protocol</strong> &#8212; Based on DWORD[2], select the fastest supported read command (1-4-4 if available, 1-1-4 as minimum quad). Based on DWORD[4], configure the flash block driver with the correct erase opcodes and timeout values.</p><p>&#9656; <strong>Validate with 0x9F</strong> &#8212; Confirm the manufacturer ID matches and that the model is consistent with the SFDP declarations. Log any discrepancies.</p><p>This eliminates the fragile ID&#8594;capability lookup table entirely. New Macronix, Winbond, or GigaDevice chips drop in with no firmware change. The flash tells you what it can do.</p><div><hr></div><h2><strong>SPI Bus Details: Why One Dummy Byte?</strong></h2><p>The dummy byte in the 0x5A transaction isn&#8217;t arbitrary &#8212; it&#8217;s the turnaround cycle where the SPI bus changes direction. On a standard SPI bus, the master controls MOSI (Master Out, Slave In) while the slave drives MISO (Master In, Slave Out).</p><p>When you send 0x5A + 3 address bytes, the flash is listening on MOSI. After the address, it needs time to:</p><ol><li><p>Decode the SFDP offset address</p></li><li><p>Switch its internal data path from receiving to transmitting</p></li><li><p>Start clocking out the first SFDP data byte on MISO</p></li></ol><p>The dummy byte buys this time. On slower SPI modes (single I/O), one cycle is enough. In quad I/O read mode (command 0xEB), some chips need 4&#8211;8 dummy cycles because the quad data path takes longer to set up.</p><p>The SFDP table itself tells you how many dummy cycles each command needs &#8212; DWORD[2] bits [7:5] specify the mode bits and dummy cycle count for the Read SFDP command specifically. This is self-referential: the parameter table declares how to read the parameter table.</p><div><hr></div><h2><strong>Beyond Basic SPI: SFDP&#8217;s Extensible Design</strong></h2><p>JESD216 isn&#8217;t limited to basic SPI parameters. Additional parameter tables cover:</p><p>&#9656; <strong>4-Byte Addressing (Parameter ID 0x84):</strong> For flash chips &gt;256Mb that need 32-bit addresses. The table declares which commands support 4-byte addressing mode and how to toggle it.</p><p>&#9656; <strong>Sector Map (Parameter ID 0x81):</strong> For chips with uniform or hybrid sector layouts. Declares the erase granularity for each address region. Essential for flash file systems that need to know block boundaries.</p><p>&#9656; <strong>OTP / Security Registers (Parameter ID 0x8C):</strong> One-time programmable regions, serial numbers, and lock bits. The SFDP table declares their location, size, and access protocol.</p><p>The MX25L25635F exposes only the basic SPI parameter table (ID 0xFF), but higher-end Macronix chips add 4-byte addressing and sector map tables. The SFDP header&#8217;s NPH field tells you how many to expect &#8212; just iterate through them.</p><div><hr></div><h2><strong>Real-World Adoption</strong></h2><p>SFDP is now mandatory for any flash chip targeting modern platforms:</p><p>&#9656; <strong>Intel PCH SPI programming guide</strong> recommends SFDP-based discovery for all attached SPI NOR flash since Skylake generation (2015+).</p><p>&#9656; <strong>Linux kernel&#8217;s </strong><code>spi-nor</code><strong> subsystem</strong> has been SFDP-first since kernel 4.x. The <code>spi_nor_scan()</code> function in <code>drivers/mtd/spi-nor/core.c</code> reads SFDP before falling back to the legacy ID table.</p><p>&#9656; <strong>U-Boot bootloader</strong> uses SFDP in its <code>spi-flash</code> framework. The <code>spi_flash_scan()</code> logic reads the SFDP header and parameter tables during the flash probe sequence.</p><p>&#9656; <strong>Coreboot</strong> calls SFDP &#8220;the right way&#8221; and strongly discourages new ID-table entries for chips that support SFDP correctly.</p><div><hr></div><h2><strong>The Bottom Line</strong></h2><p>0x5A is a simple command &#8212; one byte, three address bytes, one dummy byte, infinite data out. But it represents a fundamental shift in how firmware interacts with hardware: from &#8220;I know what you are&#8221; to &#8220;<strong>tell me what you are.</strong>&#8220;</p><p>For the MX25L25635F, two 0x5A reads replace a 100-entry manufacturer ID lookup table. For the firmware engineer, it means never having to add &#8220;just one more flash chip&#8221; to the bootloader again.</p><p>The flash chip that describes itself is the flash chip that survives firmware updates, silicon revisions, and supply-chain substitutions. 0x5A is how you ask.</p><div><hr></div><p><strong>#sfdp</strong> <strong>#spi</strong> <strong>#norflash</strong> <strong>#firmware</strong> <strong>#embedded</strong> <strong>#macronix</strong> <strong>#mx25l25635f</strong> <strong>#jedec</strong> <strong>#bootloader</strong> <strong>#uefi</strong> <strong>#gdbplus</strong></p>]]></content:encoded></item><item><title><![CDATA[eSPI EC/SIO UART Access and Enablement in UEFI BIOS]]></title><description><![CDATA[Linkedin: Click David Zhu]]></description><link>https://gdbplus.substack.com/p/espi-ecsio-uart-access-and-enablement</link><guid isPermaLink="false">https://gdbplus.substack.com/p/espi-ecsio-uart-access-and-enablement</guid><dc:creator><![CDATA[gdbplus]]></dc:creator><pubDate>Tue, 19 May 2026 11:10:59 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!GScm!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a21f1a4-588b-4353-838e-da774774ece3_2560x1440.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Linkedin: Click <a href="https://www.linkedin.com/in/david-zhu-3a68a855/">David Zhu</a></p><p>The serial port has been the firmware engineer&#8217;s eyes and ears for decades. But the physical path from CPU to UART has fundamentally changed. LPC gave way to eSPI, and the debug UART that used to live on a simple Super I/O chip now hides behind an Embedded Controller on a 4-wire serial bus. This article traces that path &#8212; from the eSPI bus to the UEFI driver stack &#8212; and shows exactly how to access and enable EC/SIO UARTs on modern platforms</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!GScm!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a21f1a4-588b-4353-838e-da774774ece3_2560x1440.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!GScm!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a21f1a4-588b-4353-838e-da774774ece3_2560x1440.png 424w, /__u/substackcdn.com/image/fetch/$s_!GScm!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a21f1a4-588b-4353-838e-da774774ece3_2560x1440.png 848w, /__u/substackcdn.com/image/fetch/$s_!GScm!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a21f1a4-588b-4353-838e-da774774ece3_2560x1440.png 1272w, /__u/substackcdn.com/image/fetch/$s_!GScm!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a21f1a4-588b-4353-838e-da774774ece3_2560x1440.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!GScm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a21f1a4-588b-4353-838e-da774774ece3_2560x1440.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4a21f1a4-588b-4353-838e-da774774ece3_2560x1440.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:443719,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://gdbplus.substack.com/i/198392160?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a21f1a4-588b-4353-838e-da774774ece3_2560x1440.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!GScm!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a21f1a4-588b-4353-838e-da774774ece3_2560x1440.png 424w, /__u/substackcdn.com/image/fetch/$s_!GScm!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a21f1a4-588b-4353-838e-da774774ece3_2560x1440.png 848w, /__u/substackcdn.com/image/fetch/$s_!GScm!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a21f1a4-588b-4353-838e-da774774ece3_2560x1440.png 1272w, /__u/substackcdn.com/image/fetch/$s_!GScm!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a21f1a4-588b-4353-838e-da774774ece3_2560x1440.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>.</p><div><hr></div><h2><strong>Why eSPI Replaced LPC</strong></h2><p>The Low Pin Count bus served firmware well for 20 years, but it had three fatal flaws for modern platforms.</p><p>&#9656; <strong>Pin count:</strong> LPC required 7&#8211;13 pins. In a world where every pin on a 12&#215;12 mm package is contested real estate, that is a luxury.</p><p>&#9656; <strong>Voltage:</strong> LPC ran at 3.3V signaling. Modern silicon is 1.8V and below. Level shifters add cost, latency, and board area.</p><p>&#9656; <strong>Speed:</strong> LPC topped out at 33 MHz. Firmware images grew from 2 MB to 32 MB. Every millisecond of boot time matters.</p><p>eSPI solves all three. Four wires (CS#, CLK, MOSI, MISO). 1.8V signaling. 66 MHz with double-data-rate transfers giving effective throughput of 132 Mbps per channel. And critically &#8212; it is a packetized serial protocol, not a parallel bus, which means it can carry multiple logical channels over the same physical wires.</p><div><hr></div><h2><strong>The Four eSPI Channels</strong></h2><p>The eSPI specification (Intel document 327432) defines four virtual channels multiplexed over a single physical bus. Understanding which channel carries what is the key to debugging UART access issues.</p><p>&#9656; <strong>Channel 0 &#8212; Peripheral Channel:</strong> This is the LPC replacement. It carries memory-mapped I/O cycles and I/O-mapped I/O cycles. When UEFI reads or writes a UART register (I/O port 0x3F8 for COM1), the transaction travels over Peripheral Channel as a standard I/O read/write cycle. The EC or SIO decodes it and performs the actual UART hardware access. From the PCH&#8217;s perspective, it is talking to an LPC device through a serial tunnel.</p><p>&#9656; <strong>Channel 1 &#8212; Virtual Wire Channel:</strong> Sideband signals that used to have dedicated pins on LPC &#8212; SMI#, SCI#, PME#, SERIRQ &#8212; are now serialized as virtual wire messages. The UART interrupt line (typically IRQ4 for COM1) rides on Virtual Wire as an SERIRQ message.</p><p>&#9656; <strong>Channel 2 &#8212; OOB Message Channel:</strong> Out-of-band messaging. This replaces SMBus for EC communication. MCTP (Management Component Transport Protocol) packets flow here. Not directly relevant to UART access, but critical for EC firmware updates and BMC interactions.</p><p>&#9656; <strong>Channel 3 &#8212; Flash Access Channel:</strong> Shared SPI flash access. The EC can access the system SPI flash through this channel for firmware updates. BIOS and EC firmware can share a single SPI flash chip.</p><p><strong>The critical insight for UART debugging:</strong> The UART appears on Channel 0 as standard I/O cycles. The entire pre-existing UEFI serial stack &#8212; PciSioSerialDxe, SerialPortLib, DebugLib &#8212; works completely unchanged. The EC/SIO translates eSPI I/O transactions into physical UART register reads and writes transparently. If you can talk to the eSPI controller, you can talk to the UART.</p><div><hr></div><h2><strong>Real Hardware: Two Common Configurations</strong></h2><p>The eSPI ecosystem has converged on two dominant patterns: the EC (Embedded Controller) in mobile platforms, and the SIO (Super I/O) in desktop and server platforms.</p><h3><strong>Configuration A: EC-Based (Laptop/Notebook)</strong></h3><p>&#9656; <strong>Chip:</strong> Microchip MEC15xx &#8212; a dedicated Embedded Controller with eSPI slave interface</p><p>&#9656; <strong>Topology:</strong> PCH (eSPI Master) &#8594; eSPI Bus &#8594; MEC15xx (Slave) &#8594; Internal UART Block &#8594; UART TX/RX pins routed to debug header</p><p>&#9656; <strong>UARTs:</strong> MEC15xx typically exposes 2&#8211;3 16550-compatible UARTs. One is dedicated to debug output, another may be used for serial peripheral communication (touchpad, battery gas gauge).</p><p>&#9656; <strong>EC firmware:</strong> The MEC15xx runs its own firmware (typically Microchip&#8217;s MEC15xx SDK). The EC firmware must configure the UART pin mux and enable the UART clock before UEFI can access it. If UEFI DEBUG() output is silent, the first thing to check is whether EC firmware actually enabled the UART routing.</p><p>&#9656; <strong>Register access:</strong> From UEFI&#8217;s perspective, the UART appears at a standard I/O base address (e.g., 0x3F8). The PCH&#8217;s eSPI controller translates I/O cycles in that range into Peripheral Channel transactions targeting the EC. The EC&#8217;s hardware auto-decoder routes them to the UART block.</p><h3><strong>Configuration B: SIO-Based (Desktop/Server)</strong></h3><p>&#9656; <strong>Chip:</strong> ITE IT8628E &#8212; a Super I/O controller with eSPI interface</p><p>&#9656; <strong>Topology:</strong> PCH (eSPI Master) &#8594; eSPI Bus &#8594; IT8628E (Slave) &#8594; Logical Device UART1/UART2 &#8594; Physical COM port headers</p><p>&#9656; <strong>UARTs:</strong> IT8628E typically provides 2&#8211;4 fully independent 16550-compatible UARTs, each with its own logical device number (LDN). LDN 0x01 is usually UART1, LDN 0x02 is UART2, and so on.</p><p>&#9656; <strong>SIO register model:</strong> Unlike the EC model where register access is transparent, the IT8628E uses the classic Super I/O configuration model: enter configuration mode by writing 0x87 to the config port (typically 0x2E or 0x4E), select a logical device, enable it, set the I/O base address, and exit configuration mode. This must be done in PEI or early DXE before PciSioSerialDxe enumerates the device. The EFI_SIO_PROTOCOL&#8217;s RegisterAccess() function handles this enter/exit sequence.</p><p>&#9656; <strong>eSPI implications:</strong> On the IT8628E, configuration register access also goes through eSPI Peripheral Channel. The SIO configuration ports (0x2E/0x2F or 0x4E/0x4F) are routed over eSPI as standard I/O transactions. No special handling is needed &#8212; but the eSPI controller must be initialized first.</p><div><hr></div><h2><strong>The UEFI Driver Stack</strong></h2><p>The EDK2 driver stack for eSPI-based UART access is identical to the legacy LPC stack because eSPI abstracts the transport. Here is the full chain.</p><p><strong>Layer 1 &#8212; PCI Root Bridge / eSPI Controller</strong></p><p>The PCH&#8217;s eSPI controller appears as a PCI device (typically Device 1F, Function 0 on Intel platforms, DID 0x1Fxx range). Platform silicon initialization in PEI configures the eSPI frequency, I/O decode ranges, and channel enables. If this layer is misconfigured, all downstream devices are invisible.</p><p>Key PEI responsibilities from <code>MdePkg/Include/Ppi/SuperIo.h</code>:</p><pre><code><code>EFI_SIO_REG(ldn, reg)          // Pack logical device + register into a single UINT16
EFI_SIO_LDN_GLOBAL             // Special LDN for global SIO registers (0xFF)
</code></code></pre><p><strong>Layer 2 &#8212; SioBusDxe</strong></p><p>The SioBusDxe driver (<code>OvmfPkg/SioBusDxe/</code>) is the reference implementation for Super I/O bus enumeration. It creates child handles for each logical device on the SIO and installs EFI_SIO_PROTOCOL on them. The SioService.h header defines the core protocol:</p><pre><code><code>EFI_SIO_PROTOCOL.Sio.RegisterAccess()  // Low-level SIO register read/write
EFI_SIO_PROTOCOL.Sio.GetResources()    // ACPI resource descriptors
EFI_SIO_PROTOCOL.Sio.Modify()          // Table-based RMW operations
</code></code></pre><p><strong>Layer 3 &#8212; PciSioSerialDxe</strong></p><p>This is the workhorse. <code>MdeModulePkg/Bus/Pci/PciSioSerialDxe/</code> produces EFI_SERIAL_IO_PROTOCOL on each SIO child handle that has a UART logical device. The Serial.h header defines the complete UART register map:</p><pre><code><code>#define SERIAL_REGISTER_THR  0   // Transmit Holding Register
#define SERIAL_REGISTER_RBR  0   // Receive Buffer Register
#define SERIAL_REGISTER_DLL  0   // Divisor Latch LSB
#define SERIAL_REGISTER_IER  1   // Interrupt Enable Register
#define SERIAL_REGISTER_FCR  2   // FIFO Control Register
#define SERIAL_REGISTER_LCR  3   // Line Control Register
#define SERIAL_REGISTER_MCR  4   // Modem Control Register
#define SERIAL_REGISTER_LSR  5   // Line Status Register
</code></code></pre><p>The driver handles both PCI-native UARTs and SIO-based UARTs through a union type:</p><pre><code><code>typedef union {
  EFI_PCI_IO_PROTOCOL    *PciIo;
  EFI_SIO_PROTOCOL       *Sio;
} PARENT_IO_PROTOCOL_PTR;
</code></code></pre><p>This means the same SerialIo protocol implementation works regardless of whether the UART is behind a PCI BAR or an SIO logical device. For eSPI EC/SIO UARTs, the Sio path is used.</p><p><strong>Layer 4 &#8212; SerialPortLib</strong></p><p>Platform-specific. In OVMF, it is <code>OvmfPkg/Library/BaseSerialPortLib16550/</code>. In physical platforms, it is typically a custom library that knows the platform&#8217;s UART base address (from PCD), clock rate, and register stride. It uses the fixed PCDs to configure the UART without depending on the DXE driver stack &#8212; this is critical because DEBUG() output begins in SEC phase, long before SioBusDxe or PciSioSerialDxe load.</p><p><strong>Layer 5 &#8212; DebugLib</strong></p><p><code>MdePkg/Library/BaseDebugLibSerialPort/</code> &#8212; every <code>DEBUG((DEBUG_INFO, "..."))</code> call eventually lands in <code>SerialPortWrite()</code> and bytes travel out through the eSPI UART.</p><div><hr></div><h2><strong>Platform Configuration: The PCD Settings</strong></h2><p>The entire UART access path is controlled by a small set of PCDs. Getting one wrong produces silent failure.</p><p>&#9656; <strong>PcdSerialUseMmio</strong> &#8212; Must be FALSE for eSPI UARTs. eSPI Peripheral Channel carries I/O cycles, not MMIO. Setting this to TRUE is the most common misconfiguration.</p><p>&#9656; <strong>PcdSerialRegisterStride</strong> &#8212; Standard 16550 UARTs use stride 1 (adjacent I/O ports). Some PCI UARTs use stride 4 (DWORD-aligned access). For EC/SIO UARTs, this is always 1.</p><p>&#9656; <strong>PcdSerialBaudRate</strong> &#8212; 115200 is the firmware standard. Do not use 921600 for debug output &#8212; the eSPI Channel 0 turnaround time can introduce jitter that the UART&#8217;s 16-byte FIFO cannot absorb at very high baud rates.</p><p>&#9656; <strong>PcdSerialClockRate</strong> &#8212; 1.8432 MHz is the 16550 standard. Some ECs use an internal 48 MHz oscillator with a fractional divider. If the divider is off by even 0.1%, baud rate mismatch will cause framing errors. Verify the EC datasheet for the actual UART clock source.</p><p>&#9656; <strong>PcdPciSerialParameters</strong> &#8212; A table of <code>PCI_SERIAL_PARAMETER</code> structures for PCI UARTs. For SIO-based UARTs behind eSPI, this may not be needed; the SIO enumeration path discovers UARTs dynamically.</p><p>&#9656; <strong>PcdSerialLineControl</strong> &#8212; Set to 0x03 for 8 data bits, no parity, 1 stop bit (8N1). The encoding follows the 16550 LCR register format: bits[1:0] for word length (11 = 8 bits), bit[2] for stop bits (0 = 1 stop bit), bits[5:3] for parity (000 = none).</p><div><hr></div><h2><strong>Boot-Time Initialization Sequence</strong></h2><p>Understanding the exact order of initialization is essential when debugging why the serial console is silent.</p><p><strong>Phase 1 &#8212; SEC/PEI: Early Debug Output</strong></p><p>The platform&#8217;s SerialPortLib constructor is called. It reads PcdSerialUseMmio, PcdSerialRegisterBase, PcdSerialBaudRate, and other PCDs, then directly programs the UART hardware via I/O instructions. This works because:</p><ol><li><p>The PCH eSPI controller was configured by the boot ROM (hardware straps or early PEI silicon init) to route I/O cycles in the COM1 range (0x3F8&#8211;0x3FF) to eSPI Channel 0 targeting the EC/SIO.</p></li><li><p>The EC/SIO was already powered on and its UART clock was enabled by EC firmware or hardware defaults.</p></li></ol><p>If DEBUG() output works in SEC/PEI but stops in DXE, the eSPI routing was reconfigured mid-boot &#8212; a common silicon init bug.</p><p><strong>Phase 2 &#8212; DXE: SioBusDxe Loads</strong></p><p><code>SioBusDxe</code> connects to the PCI-to-ISA/eSPI bridge. It enumerates the SIO chip&#8217;s logical devices using the standard Super I/O configuration sequence (enter config mode &#8594; read device ID &#8594; enumerate LDN &#8594; exit config mode). For IT8628E, this means accessing ports 0x2E/0x2F or 0x4E/0x4F over eSPI Peripheral Channel.</p><p>For the MEC15xx, the enumeration path is different &#8212; the EC is not a classic Super I/O and is typically discovered via ACPI or a platform-specific protocol rather than through config port probing.</p><p><strong>Phase 3 &#8212; DXE: PciSioSerialDxe Loads</strong></p><p><code>PciSioSerialDxe</code> binds to each SIO child handle that represents a UART logical device. It installs EFI_SERIAL_IO_PROTOCOL and calls SerialPortInitialize() to configure the UART hardware (baud rate, data bits, FIFO enable).</p><p><strong>Phase 4 &#8212; BDS: Console Connection</strong></p><p>During BDS, <code>ConSplitterDxe</code> connects the serial console. <code>TerminalDxe</code> produces the <code>SIMPLE_TEXT_OUTPUT_PROTOCOL</code> on top of the SerialIo protocol, enabling printf-style output to the serial port.</p><div><hr></div><h2><strong>Debugging the Silent Serial Port</strong></h2><p>When the serial port is silent, work through this checklist in order.</p><p>&#9656; <strong>Verify eSPI controller init:</strong> Check that the PCH eSPI controller is out of reset and the I/O decode ranges include the COM port base address. On Intel platforms, this is in the LPC/eSPI PCI configuration space at Bus 0, Device 0x1F, Function 0.</p><p>&#9656; <strong>Verify EC/SIO is powered:</strong> The MEC15xx requires VTR (trickle power) and VCC. If VCC is not up, the eSPI slave interface is dead. Check the platform power sequencing.</p><p>&#9656; <strong>Verify EC firmware UART init:</strong> On MEC15xx platforms, the EC runs its own firmware. If the EC firmware does not configure the UART pin mux and enable the UART clock, no amount of UEFI configuration will produce output. Dump the EC firmware console if available.</p><p>&#9656; <strong>Verify SIO configuration mode:</strong> For IT8628E, check that the SIO logical device for UART1 (LDN 0x01) is enabled and its I/O base address is set to 0x3F8. Use the ITE configuration sequence: write 0x87 to 0x2E, write 0x87 to 0x2E (double-write enter), then read LDN 0x01 register 0x30 (enable) and registers 0x60-0x61 (base address).</p><p>&#9656; <strong>Check PcdSerialUseMmio:</strong> If this is TRUE but the UART is I/O-mapped, all register accesses go to the wrong address space. This produces silent failure &#8212; no crash, no output, just nothing. This is the single most common misconfiguration for eSPI UART access.</p><p>&#9656; <strong>Scope the eSPI bus:</strong> If you have physical access, scope the eSPI CS#, CLK, and MOSI/MISO lines. You should see Channel 0 I/O cycles during DEBUG() output. If the bus is idle, the eSPI controller is not routing UART I/O cycles.</p><div><hr></div><h2><strong>Why This Matters</strong></h2><p>Firmware debugging without a serial console is like surgery in the dark. On modern platforms, the debug UART is no longer a simple PCI or LPC device &#8212; it is an endpoint on a packet-switched serial network. The debugging surface has expanded from one chip (the UART) to four layers (eSPI controller, eSPI bus, EC/SIO firmware, UART hardware).</p><p>Understanding the full path &#8212; and knowing which layer is responsible for what &#8212; turns hours of blind debugging into minutes of targeted investigation.</p><div><hr></div><p><strong>#firmware</strong> <strong>#uefi</strong> <strong>#espi</strong> <strong>#embedded</strong> <strong>#debug</strong> <strong>#bios</strong> <strong>#edk2</strong> <strong>#gdbplus</strong></p>]]></content:encoded></item><item><title><![CDATA[How EDK2 BDS Finds the UEFI Shell as a Boot Option]]></title><description><![CDATA[David Zhu]]></description><link>https://gdbplus.substack.com/p/how-edk2-bds-finds-the-uefi-shell</link><guid isPermaLink="false">https://gdbplus.substack.com/p/how-edk2-bds-finds-the-uefi-shell</guid><dc:creator><![CDATA[gdbplus]]></dc:creator><pubDate>Mon, 18 May 2026 10:41:35 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!clGN!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b63db40-799a-4090-86f3-5b5de1d841c1_2560x1440.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a href="https://www.linkedin.com/in/david-zhu-3a68a855/">David Zhu</a></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://gdbplus.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="/__u/gdbplus.substack.com/subscribe"><span>Subscribe now</span></a></p><p>You power on a QEMU VM with OVMF firmware. The UEFI Shell appears in the boot menu. You type <code>exit</code>, and your OS boots. But have you ever wondered &#8212; how did BDS know the Shell was even there?</p><p>Unlike a disk-based OS loader, Shell.efi lives <strong>inside the firmware volume (FV)</strong>. It has no BlockIo handle. No SimpleFileSystem. No LoadFile protocol. BDS&#8217;s standard auto-enumeration cannot see it. So how does it become a boot option?</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!clGN!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b63db40-799a-4090-86f3-5b5de1d841c1_2560x1440.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!clGN!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b63db40-799a-4090-86f3-5b5de1d841c1_2560x1440.png 424w, /__u/substackcdn.com/image/fetch/$s_!clGN!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b63db40-799a-4090-86f3-5b5de1d841c1_2560x1440.png 848w, /__u/substackcdn.com/image/fetch/$s_!clGN!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b63db40-799a-4090-86f3-5b5de1d841c1_2560x1440.png 1272w, /__u/substackcdn.com/image/fetch/$s_!clGN!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b63db40-799a-4090-86f3-5b5de1d841c1_2560x1440.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!clGN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b63db40-799a-4090-86f3-5b5de1d841c1_2560x1440.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7b63db40-799a-4090-86f3-5b5de1d841c1_2560x1440.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:592783,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://gdbplus.substack.com/i/198242042?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b63db40-799a-4090-86f3-5b5de1d841c1_2560x1440.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!clGN!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b63db40-799a-4090-86f3-5b5de1d841c1_2560x1440.png 424w, /__u/substackcdn.com/image/fetch/$s_!clGN!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b63db40-799a-4090-86f3-5b5de1d841c1_2560x1440.png 848w, /__u/substackcdn.com/image/fetch/$s_!clGN!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b63db40-799a-4090-86f3-5b5de1d841c1_2560x1440.png 1272w, /__u/substackcdn.com/image/fetch/$s_!clGN!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b63db40-799a-4090-86f3-5b5de1d841c1_2560x1440.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p>Let&#8217;s trace the entire chain from firmware build to boot menu.</p><div><hr></div><h2><strong>The Build-Time Foundation: A GUID is Born</strong></h2><p>It starts in <code>ShellPkg/Application/Shell/Shell.inf</code>:</p><pre><code><code>FILE_GUID = 7C04A583-9E3E-4f1c-AD65-E05268D0B4D1
</code></code></pre><p>This GUID is the unique identifier that will follow Shell.efi through its entire lifecycle. <code>ShellPkg/ShellPkg.dec</code> aliases it as <code>gUefiShellFileGuid</code> so platform code can reference it symbolically.</p><p>When the firmware image is built, Shell.efi is embedded as an FFS (Firmware File System) file inside the DXE firmware volume. Its NameGuid in the FV header is that same <code>7C04A583-...</code> GUID. The platform knows exactly where to look.</p><div><hr></div><h2><strong>BDS Phase: Auto-Enumeration Misses the Shell</strong></h2><p>When BDS enters <code>PlatformBootManagerAfterConsole()</code>, the first thing it does is call <code>EfiBootManagerRefreshAllBootOption()</code>. This function scans for:</p><p>&#9656; <strong>BlockIo devices</strong> &#8212; disks, USBs, NVMe drives</p><p>&#9656; <strong>LoadFile protocols</strong> &#8212; network boot, HTTP boot</p><p>&#9656; <strong>SimpleFileSystem</strong> &#8212; partitions with recognizable filesystems</p><p>For each, it constructs a device path and creates a <code>Boot####</code> NVRAM variable. This is how your OS loader, network boot, and USB boot options are auto-discovered.</p><p>But Shell.efi lives <strong>inside the firmware volume</strong> &#8212; it&#8217;s part of the firmware binary itself. It exposes none of the above protocols. Auto-enumeration completely misses it.</p><div><hr></div><h2><strong>The Explicit Registration: PlatformRegisterFvBootOption()</strong></h2><p>After auto-enumeration, the platform code takes over. In <code>OvmfPkg/Library/PlatformBootManagerLibLight/PlatformBm.c</code>:</p><pre><code><code>// Register UEFI Shell
PlatformRegisterFvBootOption(
    &amp;gUefiShellFileGuid,          // 7C04A583-9E3E-...
    L"EFI Internal Shell",
    LOAD_OPTION_ACTIVE | LOAD_OPTION_CATEGORY_APP,
    ShellEnabled                  // from QEMU fw_cfg
);
</code></code></pre><p>This function does four things, in order:</p><p>&#9656; <strong>1. Build a device path.</strong> It constructs <code>MemoryMapped(...)/FvFile(7C04A583-9E3E-4f1c-...)</code> by prepending the firmware volume&#8217;s memory-mapped device path to an FvFile node containing the GUID.</p><p>&#9656; <strong>2. Verify the file actually exists.</strong> It calls <code>FirmwareVolume2-&gt;ReadFile()</code> with <code>Buffer=NULL</code> (metadata-only query) and the Shell&#8217;s GUID. If the FV doesn&#8217;t contain an FFS file with that NameGuid, the function returns immediately &#8212; no boot option created.</p><p>&#9656; <strong>3. Deduplicate.</strong> It scans all existing <code>Boot####</code> NVRAM variables. If a boot option with the same device path already exists, it either updates it (if attributes changed) or skips it (if unchanged). You don&#8217;t get duplicate Shell entries.</p><p>&#9656; <strong>4. Write to NVRAM.</strong> If the file is found and the option is new, it calls <code>EfiBootManagerAddLoadOptionVariable()</code> which writes a new <code>Boot####</code> variable and appends the option number to <code>BootOrder</code>.</p><p>The <code>ShellEnabled</code> flag comes from QEMU&#8217;s fw_cfg interface &#8212; <code>opt/org.tianocore/EFIShellSupport</code>. This lets you disable the Shell boot option entirely by passing <code>-fw_cfg name=opt/org.tianocore/EFIShellSupport,string=no</code> to QEMU. Without this flag, it defaults to TRUE.</p><div><hr></div><h2><strong>The Cleanup Pass: RemoveStaleFvFileOptions()</strong></h2><p>Firmware gets rebuilt. The DXE FV might shift in memory. FILE_GUIDs might change. A Shell boot option from a previous firmware build could point to a file that no longer exists.</p><p><code>RemoveStaleFvFileOptions()</code> walks every <code>Boot####</code> variable and checks:</p><p>&#9656; Does the device path start with <code>MemoryMapped(...)</code> or <code>Fv(...)</code>?</p><p>&#9656; Does the next node begin with <code>FvFile(...)</code>?</p><p>If both conditions are met, it calls <code>FileIsInFv()</code> to verify the FFS file still exists. If not &#8212; delete the boot option. This prevents stale entries from accumulating across firmware updates.</p><div><hr></div><h2><strong>The Final Order: SetBootOrderFromQemu()</strong></h2><p>After Shell registration and stale cleanup, the platform reads QEMU&#8217;s <code>bootorder</code> fw_cfg file. This determines the final <code>BootOrder</code> variable &#8212; which boot option BDS will try first, second, and so on.</p><p>The Shell is typically placed <strong>after</strong> disk and network boot options, so the system boots to your OS by default. But if you want the Shell first, you can control it via fw_cfg.</p><div><hr></div><h2><strong>Why This Matters</strong></h2><p>Understanding this mechanism explains several practical behaviors:</p><p>&#9656; <strong>Why the Shell is always there</strong> &#8212; even on a &#8220;blank&#8221; VM with no disks attached, the Shell appears because it&#8217;s hard-coded into the platform BDS code.</p><p>&#9656; <strong>Why </strong><code>exit</code><strong> drops back to the boot menu</strong> &#8212; exiting the Shell returns control to BDS, which then tries the next option in BootOrder.</p><p>&#9656; <strong>Why you can disable it</strong> &#8212; the <code>EFIShellSupport</code> fw_cfg knob gives production firmware builds a clean escape hatch.</p><p>&#9656; <strong>Why stale firmware builds don&#8217;t cause problems</strong> &#8212; <code>RemoveStaleFvFileOptions()</code> automatically cleans up old GUID references.</p><div><hr></div><h2><strong>The Complete Chain</strong></h2><pre><code><code>Build time:
  Shell.inf FILE_GUID = 7C04A583-...
  &#8594; FFS file placed in DXE FV with that NameGuid

Boot time (PlatformBootManagerAfterConsole):
  1. EfiBootManagerRefreshAllBootOption()   &#8594; disk, net, USB options
  2. PlatformRegisterFvBootOption(gUefiShellFileGuid)
     &#8594; Build device path: FvFile(7C04A583-...)
     &#8594; FV2-&gt;ReadFile() &#8212; verify file exists
     &#8594; EfiBootManagerAddLoadOptionVariable() &#8594; Boot####, BootOrder
  3. RemoveStaleFvFileOptions()             &#8594; delete dead FvFile entries
  4. SetBootOrderFromQemu()                 &#8594; final boot sequence
</code></code></pre><p>The key insight: <strong>the Shell is not discovered &#8212; it&#8217;s hard-coded to be found.</strong> BDS explicitly hunts for it by GUID inside the firmware volume, because it knows exactly what it&#8217;s looking for.</p><div><hr></div><p><strong>#firmware</strong> <strong>#uefi</strong> <strong>#edk2</strong> <strong>#bds</strong> <strong>#ovmf</strong> <strong>#bootloader</strong> <strong>#embeddedsystems</strong> <strong>#gdbplus</strong></p>]]></content:encoded></item><item><title><![CDATA[Why You Never Need to Define gBS and gSmst — EDK2’s Library Constructor Magic]]></title><description><![CDATA[Linkedin: click DavidZhu]]></description><link>https://gdbplus.substack.com/p/why-you-never-need-to-define-gbs</link><guid isPermaLink="false">https://gdbplus.substack.com/p/why-you-never-need-to-define-gbs</guid><dc:creator><![CDATA[gdbplus]]></dc:creator><pubDate>Sun, 17 May 2026 08:25:06 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!YgKr!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4210707c-0f88-483c-ad78-7e378eea037f_2560x1440.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Linkedin: click <a href="https://www.linkedin.com/in/david-zhu-3a68a855/">DavidZhu</a></p><div><hr></div><p>If you&#8217;ve written any EDK2 driver, you&#8217;ve typed <code>gBS-&gt;AllocatePool(...)</code> or <code>gSmst-&gt;SmmAllocatePool(...)</code> without ever declaring those variables. They just work. But <em>why</em>?</p><p>The short answer: EDK2&#8217;s <strong>library constructor</strong> system auto-initializes them before your module&#8217;s entry point even runs. Once you link the right library class, AutoGen wires up the call chain and the globals are ready. Here&#8217;s exactly how.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!YgKr!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4210707c-0f88-483c-ad78-7e378eea037f_2560x1440.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!YgKr!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4210707c-0f88-483c-ad78-7e378eea037f_2560x1440.png 424w, /__u/substackcdn.com/image/fetch/$s_!YgKr!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4210707c-0f88-483c-ad78-7e378eea037f_2560x1440.png 848w, /__u/substackcdn.com/image/fetch/$s_!YgKr!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4210707c-0f88-483c-ad78-7e378eea037f_2560x1440.png 1272w, /__u/substackcdn.com/image/fetch/$s_!YgKr!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4210707c-0f88-483c-ad78-7e378eea037f_2560x1440.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!YgKr!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4210707c-0f88-483c-ad78-7e378eea037f_2560x1440.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4210707c-0f88-483c-ad78-7e378eea037f_2560x1440.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:483465,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://gdbplus.substack.com/i/198096719?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4210707c-0f88-483c-ad78-7e378eea037f_2560x1440.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!YgKr!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4210707c-0f88-483c-ad78-7e378eea037f_2560x1440.png 424w, /__u/substackcdn.com/image/fetch/$s_!YgKr!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4210707c-0f88-483c-ad78-7e378eea037f_2560x1440.png 848w, /__u/substackcdn.com/image/fetch/$s_!YgKr!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4210707c-0f88-483c-ad78-7e378eea037f_2560x1440.png 1272w, /__u/substackcdn.com/image/fetch/$s_!YgKr!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4210707c-0f88-483c-ad78-7e378eea037f_2560x1440.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><div><hr></div><h2><strong>gBS &#8212; The Boot Services Pointer</strong></h2><p>Every DXE and UEFI driver includes <code>UefiBootServicesTableLib.h</code> and gets <code>gBS</code> for free. The mechanism has three layers:</p><h3><strong>Layer 1: The Header Declares </strong><code>extern</code></h3><p><strong>MdePkg/Include/Library/UefiBootServicesTableLib.h:</strong></p><pre><code><code>extern EFI_HANDLE         gImageHandle;
extern EFI_SYSTEM_TABLE  *gST;
extern EFI_BOOT_SERVICES *gBS;
</code></code></pre><p>Three globals, all <code>extern</code>. Your code just references them &#8212; the actual storage lives in the library.</p><h3><strong>Layer 2: The Library Owns the Definition and Constructor</strong></h3><p><strong>MdePkg/Library/UefiBootServicesTableLib/UefiBootServicesTableLib.c:</strong></p><pre><code><code>EFI_HANDLE         gImageHandle = NULL;
EFI_SYSTEM_TABLE   *gST         = NULL;
EFI_BOOT_SERVICES  *gBS         = NULL;

EFI_STATUS EFIAPI
UefiBootServicesTableLibConstructor (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
  gImageHandle = ImageHandle;
  gST          = SystemTable;
  gBS          = SystemTable-&gt;BootServices;   // &#8592; the magic line
  ASSERT (gBS != NULL);
  return EFI_SUCCESS;
}
</code></code></pre><p>The constructor receives the same <code>(ImageHandle, SystemTable)</code> pair that your module entry point gets. It caches the Boot Services pointer into the global <code>gBS</code> &#8212; simple pointer assignment.</p><h3><strong>Layer 3: INF Declares CONSTRUCTOR</strong></h3><p><strong>UefiBootServicesTableLib.inf:</strong></p><pre><code><code>CONSTRUCTOR = UefiBootServicesTableLibConstructor
LIBRARY_CLASS = UefiBootServicesTableLib|DXE_DRIVER UEFI_DRIVER ...
</code></code></pre><p>AutoGen reads this, generates <code>ProcessLibraryConstructorList()</code>, and embeds a call to it <em>before</em> your module&#8217;s entry point. That&#8217;s the whole trick: by the time your <code>MyDriverEntryPoint()</code> executes, the constructor has already run and <code>gBS</code> is valid.</p><h3><strong>How DXE Core Bootstraps This</strong></h3><p>The DXE Core itself links <code>UefiBootServicesTableLib</code>. During <code>DxeMain()</code>, it builds the System Table, populates <code>gDxeCoreST</code>, then calls:</p><pre><code><code>ProcessLibraryConstructorList (gDxeCoreImageHandle, gDxeCoreST);
</code></code></pre><p>This fires the constructor, which sets <code>gBS = gDxeCoreST-&gt;BootServices</code>. For every subsequent DXE driver loaded by the dispatcher, <code>CoreStartImage()</code> calls the module&#8217;s entry point with the same SystemTable &#8212; and AutoGen&#8217;s generated wrapper calls constructors first, in dependency order.</p><div><hr></div><h2><strong>gSmst &#8212; The SMM System Table Pointer</strong></h2><p>Same three-layer pattern, with one extra step: SMM lives in a separate address space, so you can&#8217;t just dereference the DXE SystemTable to find the SMM table.</p><h3><strong>Layer 1: Header</strong></h3><p><strong>MdePkg/Include/Library/SmmServicesTableLib.h:</strong></p><pre><code><code>extern EFI_SMM_SYSTEM_TABLE2  *gSmst;
</code></code></pre><h3><strong>Layer 2: Constructor Uses a Protocol Gateway</strong></h3><p><strong>MdePkg/Library/SmmServicesTableLib/SmmServicesTableLib.c:</strong></p><pre><code><code>EFI_SMM_SYSTEM_TABLE2  *gSmst = NULL;

EFI_STATUS EFIAPI
SmmServicesTableLibConstructor (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
  EFI_SMM_BASE2_PROTOCOL  *SmmBase2;

  SystemTable-&gt;BootServices-&gt;LocateProtocol (
    &amp;gEfiSmmBase2ProtocolGuid, NULL, (VOID **)&amp;SmmBase2
    );
  SmmBase2-&gt;GetSmstLocation (SmmBase2, &amp;gSmst);
  ASSERT (gSmst != NULL);
  return EFI_SUCCESS;
}
</code></code></pre><p>Notice the comment in the source: <em>&#8220;Do not use gBS from UefiBootServicesTableLib on purpose to prevent inclusion of gBS, gST, and gImageHandle from SMM Drivers.&#8221;</em> The constructor uses <code>SystemTable-&gt;BootServices</code> directly (the parameter, not the global) to locate the SMM Base 2 protocol, then calls <code>GetSmstLocation()</code> to retrieve the SMM System Table pointer. This is the bridge from the DXE world into SMM.</p><h3><strong>Layer 3: INF with DEPEX</strong></h3><p><strong>SmmServicesTableLib.inf:</strong></p><pre><code><code>CONSTRUCTOR  = SmmServicesTableLibConstructor
MODULE_TYPE  = DXE_SMM_DRIVER
LIBRARY_CLASS = SmmServicesTableLib|DXE_SMM_DRIVER
[Depex]
  gEfiSmmBase2ProtocolGuid
</code></code></pre><p>The DEPEX guarantees <code>EFI_SMM_BASE2_PROTOCOL</code> is installed before the constructor runs. No SmmBase2 &#8594; dispatcher waits. SmmBase2 ready &#8594; constructor fires &#8594; <code>gSmst</code> is alive.</p><div><hr></div><h2><strong>The Full Call Chain, Side by Side</strong></h2><p>&#9656; <strong>gBS (DXE/UEFI world):</strong> AutoGen wrapper &#8594; ProcessLibraryConstructorList &#8594; UefiBootServicesTableLibConstructor &#8594; <code>gBS = SystemTable-&gt;BootServices</code> &#8594; your EntryPoint</p><p>&#9656; <strong>gSmst (SMM world):</strong> DEPEX waits for SmmBase2 &#8594; AutoGen wrapper &#8594; ProcessLibraryConstructorList &#8594; SmmServicesTableLibConstructor &#8594; LocateProtocol &#8594; GetSmstLocation &#8594; <code>gSmst</code> filled &#8594; your EntryPoint</p><p>Same architecture, different protocol for crossing the SMM boundary.</p><div><hr></div><h2><strong>Why This Design Matters</strong></h2><p>&#9656; <strong>Zero boilerplate:</strong> You never write <code>gBS = SystemTable-&gt;BootServices</code> in your own code. The library handles it.</p><p>&#9656; <strong>Consistent initialization:</strong> Every module gets gBS/gSmst the same way &#8212; no copy-paste drift across hundreds of drivers.</p><p>&#9656; <strong>Dependency ordering:</strong> AutoGen resolves library constructor order automatically. If Library A&#8217;s constructor needs <code>gBS</code> and Library B provides it, AutoGen calls B&#8217;s constructor first.</p><p>&#9656; <strong>SMM isolation:</strong> By using <code>SystemTable</code> (the parameter) instead of the global <code>gBS</code>, the SMM library constructor avoids pulling the entire DXE global set into SMM drivers. Clean separation.</p><div><hr></div><h2><strong>What You Actually Need to Do</strong></h2><p>Nothing. Just include the right library class in your INF:</p><pre><code><code>[LibraryClasses]
  UefiBootServicesTableLib    # for gBS, gST, gImageHandle
  SmmServicesTableLib         # for gSmst (SMM drivers only)
</code></code></pre><p>Include the header in your source, and call <code>gBS-&gt;Whatever()</code> or <code>gSmst-&gt;Whatever()</code> &#8212; they&#8217;re already initialized when your entry point runs.</p><div><hr></div><p><strong>#firmware</strong> <strong>#uefi</strong> <strong>#edk2</strong> <strong>#smm</strong> <strong>#embeddedsystems</strong> <strong>#gdbplus</strong></p>]]></content:encoded></item><item><title><![CDATA[Deep Dive into Winbond W25Q256JV: A Datasheet Guide for Firmware and Linux Developers]]></title><description><![CDATA[Essential opcodes, register bits, and platform&#8209;specific wisdom without the marketing fluff.]]></description><link>https://gdbplus.substack.com/p/deep-dive-into-winbond-w25q256jv</link><guid isPermaLink="false">https://gdbplus.substack.com/p/deep-dive-into-winbond-w25q256jv</guid><dc:creator><![CDATA[gdbplus]]></dc:creator><pubDate>Sat, 16 May 2026 23:46:55 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!42IY!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3bb02da3-b5e5-483b-8b5d-2a62b7943554_1280x1280.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Linkedin : click <a href="https://www.linkedin.com/in/david-zhu-3a68a855/">David Zhu</a></p><p>The Winbond W25Q256JV is a workhorse 256 Mbit (32 MB) Serial NOR flash. It shows up on motherboards, embedded Linux boards, and anywhere you need reliable, fast non&#8209;volatile storage for firmware. If you&#8217;re writing UEFI EDK2 drivers or integrating it into a Linux system, you don&#8217;t need the glossy feature list &#8212; you need the raw datasheet facts that actually matter when the silicon meets your code. This article distills exactly that, without a single markdown table in sight.</p><div><hr></div><h2><strong>Quick Identification</strong></h2><p>When your SPI controller probes the bus, here&#8217;s what you&#8217;ll see:</p><ul><li><p><strong>JEDEC Manufacturer ID</strong>: <code>EFh</code> (Winbond)</p></li><li><p><strong>Device ID</strong>: <code>4019h</code><br><em>Note: The older W25Q256FV shares the same ID, so you can&#8217;t tell them apart by JEDEC ID alone. See the SFDP and addressing mode sections for the crucial difference.</em></p></li><li><p><strong>Density</strong>: 256 Mbit (32 MiB)</p></li><li><p><strong>Supply voltage</strong>: 2.7 V &#8211; 3.6 V (JV = standard voltage, as opposed to the lower&#8209;voltage JM variant)</p></li><li><p><strong>SFDP table location</strong>: The mandatory JEDEC SFDP table starts at address <code>000000h</code>, first header at offset <code>80h</code>.</p></li><li><p><strong>Unique ID</strong>: A factory&#8209;programmed 64&#8209;bit serial number, readable with command <code>4Bh</code>.</p></li></ul><div><hr></div><h2><strong>Memory Architecture &amp; Erase Operations</strong></h2><p>The flash is organised as a flat array of 4 KB sectors. Understanding the erase granularity is essential for laying out firmware volumes and UEFI variable stores.</p><ul><li><p><strong>Sector</strong> &#8212; 4 KB<br><em>Command</em>: <code>20h</code> (3&#8209;byte address) or <code>21h</code> (4&#8209;byte address)<br><em>Typical use</em>: UEFI variable storage, small FFS files, anything that requires fine&#8209;grained updates.</p></li><li><p><strong>Block (32 KB)</strong> &#8212; 32 KB<br><em>Command</em>: <code>52h</code> / <code>5Ch</code> (4&#8209;byte)<br><em>Typical use</em>: Less common; useful when 64 KB blocks are too large but you want more than a sector.</p></li><li><p><strong>Block (64 KB)</strong> &#8212; 64 KB<br><em>Command</em>: <code>D8h</code> / <code>DCh</code> (4&#8209;byte)<br><em>Typical use</em>: Large firmware volumes, kernel/initramfs partitions, the bread&#8209;and&#8209;butter erase unit for most firmware layouts.</p></li><li><p><strong>Chip Erase</strong> &#8212; Full 32 MB<br><em>Command</em>: <code>C7h</code> or <code>60h</code><br><em>Note</em>: This takes around 200 seconds. Don&#8217;t use it in a boot path.</p></li></ul><p>The chip contains exactly <strong>8,192 sectors</strong> of 4 KB each.</p><p><strong>Write granularity</strong>: 1 to 256 bytes per page. A page is 256 bytes, and you <strong>must not</strong> cross a page boundary in a single program command. Partial page writes are allowed, but the address automatically wraps within the page.</p><div><hr></div><h2><strong>The Essential Command Set</strong></h2><p>Forget the 200&#8209;page datasheet; these are the opcodes you&#8217;ll actually use in firmware and kernel code. All numbers are in hexadecimal.</p><h3><strong>Read Operations</strong></h3><ul><li><p><strong>Standard Read</strong> (<code>03h</code>) &#8212; 1&#8209;line input, 1&#8209;line output. Slow (&#8804;50 MHz) but easy to bring up.</p></li><li><p><strong>Fast Read</strong> (<code>0Bh</code>, 4&#8209;byte addr variant <code>0Ch</code>) &#8212; Adds dummy cycles for higher clock rates (up to 133 MHz). 1&#8209;line data out.</p></li><li><p><strong>Dual Output Read</strong> (<code>3Bh</code>, 4&#8209;byte <code>3Ch</code>) &#8212; Two data lines.</p></li><li><p><strong>Quad Output Read</strong> (<code>6Bh</code>, 4&#8209;byte <code>6Ch</code>) &#8212; Four data lines. <strong>Requires Quad Enable bit (QE) = 1</strong> in Status Register 2.</p></li><li><p><strong>Quad I/O Read</strong> (<code>EBh</code>, 4&#8209;byte <code>ECh</code>) &#8212; The fastest option. Uses 4 lines for address, mode bits, and data. QE must be set.</p></li></ul><h3><strong>Program &amp; Erase</strong></h3><ul><li><p><strong>Write Enable</strong> (<code>06h</code>) &#8212; Must be issued before every program, erase, or status register write. Sets the Write Enable Latch (WEL). WEL auto&#8209;clears after the operation completes.</p></li><li><p><strong>Volatile SR Write Enable</strong> (<code>50h</code>) &#8212; Required before writing Status Register 1 if the SRP bits lock it.</p></li><li><p><strong>Page Program</strong> (<code>02h</code>, 4&#8209;byte <code>12h</code>) &#8212; Up to 256 bytes, single data line.</p></li><li><p><strong>Quad Page Program</strong> (<code>32h</code>, 4&#8209;byte <code>34h</code>) &#8212; Faster programming using 4 data lines. QE must be set.</p></li><li><p><strong>Sector Erase 4 KB</strong> &#8212; <code>20h</code> / <code>21h</code></p></li><li><p><strong>Block Erase 32 KB</strong> &#8212; <code>52h</code> / <code>5Ch</code></p></li><li><p><strong>Block Erase 64 KB</strong> &#8212; <code>D8h</code> / <code>DCh</code></p></li><li><p><strong>Chip Erase</strong> &#8212; <code>C7h</code> or <code>60h</code></p></li></ul><h3><strong>Status Register Management</strong></h3><ul><li><p><strong>Read Status Register 1</strong> &#8212; <code>05h</code></p></li><li><p><strong>Read Status Register 2</strong> &#8212; <code>35h</code></p></li><li><p><strong>Read Status Register 3</strong> &#8212; <code>15h</code></p></li><li><p><strong>Write Status Register 1</strong> &#8212; <code>01h</code></p></li><li><p><strong>Write Status Register 2</strong> &#8212; <code>31h</code></p></li><li><p><strong>Write Status Register 3</strong> &#8212; <code>11h</code></p></li></ul><h3><strong>Address Mode Control</strong></h3><ul><li><p><strong>Enter 4&#8209;Byte Address Mode</strong> &#8212; <code>B7h</code> (immediate, volatile)</p></li><li><p><strong>Exit 4&#8209;Byte Address Mode</strong> &#8212; <code>E9h</code></p></li></ul><h3><strong>Security &amp; Identification</strong></h3><ul><li><p><strong>Read Unique ID</strong> &#8212; <code>4Bh</code> (returns 8 bytes)</p></li><li><p><strong>Read SFDP</strong> &#8212; <code>5Ah</code> (3&#8209;byte address, reads JEDEC parameter table)</p></li><li><p><strong>Read Security Registers</strong> &#8212; <code>48h</code></p></li><li><p><strong>Program Security Registers</strong> &#8212; <code>42h</code></p></li><li><p><strong>Erase Security Registers</strong> &#8212; <code>44h</code></p></li></ul><div><hr></div><h2><strong>Status Register Bits Demystified</strong></h2><p>Three status registers govern everything from write protection to quad mode. Here&#8217;s the bit&#8209;by&#8209;bit breakdown.</p><h3><strong>Status Register 1 (read with </strong><code>05h</code><strong>, write with </strong><code>01h</code><strong>)</strong></h3><ul><li><p><strong>Bit 0 &#8211; WIP</strong> (Write In Progress): 1 = the device is busy with an internal program/erase/write cycle. Poll this to know when an operation finishes.</p></li><li><p><strong>Bit 1 &#8211; WEL</strong> (Write Enable Latch): 1 = the device is ready to accept a program or erase command. You set this with <code>06h</code>.</p></li><li><p><strong>Bits 2&#8211;4 &#8211; BP0, BP1, BP2</strong>: Block Protect bits. Together they define a protected area that cannot be programmed or erased. The exact region depends on TB, SEC, and CMP settings.</p></li><li><p><strong>Bit 5 &#8211; TB</strong> (Top/Bottom): Selects whether the protected area starts from the top or the bottom of the memory.</p></li><li><p><strong>Bit 6 &#8211; SEC</strong> (Sector/Block Protect): 0 = protection works in 64 KB blocks; 1 = protection works in 4 KB sectors.</p></li><li><p><strong>Bit 7 &#8211; SRP0</strong> (Status Register Protect 0): Together with SRP1 (in SR2), it controls whether SR1 can be written or is permanently locked.</p></li></ul><h3><strong>Status Register 2 (read </strong><code>35h</code><strong>, write </strong><code>31h</code><strong>)</strong></h3><ul><li><p><strong>Bit 4 &#8211; QE</strong> (Quad Enable): This is the gatekeeper for all quad&#8209;SPI operations. It must be <strong>1</strong> before you issue any quad read or quad page program command. If your quad reads silently return garbage, check this bit first.</p></li><li><p><strong>Bit 5 &#8211; CMP</strong> (Complement Protect): Inverts the block&#8209;protection logic for certain configurations.</p></li><li><p><strong>Bit 6 &#8211; WPS</strong> (Write Protect Selection): Used when external hardware write&#8209;protect pin is active.</p></li><li><p><strong>Bit 7 &#8211; SRP1</strong>: Works with SRP0 to lock the status registers.</p></li></ul><h3><strong>Status Register 3 (read </strong><code>15h</code><strong>, write </strong><code>11h</code><strong>)</strong></h3><ul><li><p><strong>Bit 1 &#8211; ADS</strong> (Address Mode): <strong>1 = 4&#8209;byte address mode is active.</strong> This bit is non&#8209;volatile, so it survives power cycles. In EDK2 you may prefer the volatile <code>B7h</code> command to avoid permanent changes.</p></li><li><p><strong>Bits 2 and 6 &#8211; DRV0, DRV1</strong>: Output driver strength selection. Useful for signal integrity tuning on high&#8209;speed boards.</p></li><li><p><strong>Bit 7 &#8211; HOLD/RST</strong>: Configures the HOLD#/RESET# pin function.</p></li></ul><p><strong>Important for EDK2</strong>: The W25Q256JV powers up in the address mode defined by the non&#8209;volatile ADS bit. If your board ships with 3&#8209;byte mode, you&#8217;ll need to enter 4&#8209;byte mode at boot (via <code>B7h</code>) to access the full 32 MB.</p><div><hr></div><h2><strong>Addressing Modes: 3&#8209;Byte vs. 4&#8209;Byte</strong></h2><p>This is where many bring&#8209;up sessions go off the rails. The chip can operate with either 3&#8209;byte (24&#8209;bit) or 4&#8209;byte (32&#8209;bit) addresses.</p><ul><li><p><strong>3&#8209;byte mode</strong>: Only the lower 16 MB (128 Mbit) are accessible. Any command with an address above <code>FFFFFFh</code> wraps around. This is the legacy mode.</p></li><li><p><strong>4&#8209;byte mode</strong>: Full 32 MB address space is available. You can enter this mode in two ways:</p><ul><li><p><strong>Volatile</strong> &#8212; Command <code>B7h</code>. The mode persists until the next power cycle or hardware reset.</p></li><li><p><strong>Non&#8209;volatile</strong> &#8212; Write the ADS bit in Status Register 3. This survives power cycles but is a write cycle that wears the flash slightly.</p></li></ul></li></ul><p><strong>How to pick the right commands:</strong> The datasheet defines separate opcodes for many operations when using 4&#8209;byte addresses. For example, Sector Erase is <code>20h</code> with 3&#8209;byte address but <code>21h</code> when you&#8217;re passing a 4&#8209;byte address directly, <em>without</em> having switched the device into 4&#8209;byte mode via <code>B7h</code>. If you <em>have</em> entered 4&#8209;byte mode with <code>B7h</code>, then you can use the legacy opcodes (<code>20h</code>, <code>02h</code>, etc.) and they will interpret the address as 4 bytes. The Linux kernel and many EDK2 drivers use the explicit 4&#8209;byte opcodes for clarity.</p><p><strong>Linux kernel note</strong>: Because the W25Q256FV and JV share the same JEDEC ID, the SPI&#8209;NOR subsystem checks the <strong>SFDP header version</strong> to decide whether 4&#8209;byte addressing is truly supported. The JV&#8217;s SFDP correctly advertises this capability; the FV&#8217;s does not. You don&#8217;t need to handle this manually &#8212; just make sure your kernel prints <code>successfully parsed SFDP</code> during boot.</p><p><strong>EDK2 rule of thumb</strong>: Most platforms simply send <code>B7h</code> in the PEI or early DXE phase and then use 4&#8209;byte addresses everywhere. If you ever fall back to 3&#8209;byte mode (via <code>E9h</code> or a power cycle), remember to re&#8209;enter 4&#8209;byte mode before accessing firmware volumes above 16 MB.</p><div><hr></div><h2><strong>Security Registers &amp; Unique ID</strong></h2><p>Beyond the main array, the chip provides three 256&#8209;byte security registers. These are ideal for storing board&#8209;specific data, serial numbers, or public keys.</p><ul><li><p><strong>Register 1</strong> &#8212; Volatile (lost on power&#8209;down). Good for temporary data.</p></li><li><p><strong>Registers 2 and 3</strong> &#8212; OTP (One&#8209;Time Programmable). You can program them and then lock them permanently by writing a special byte to a lock register. Once locked, they become read&#8209;only forever &#8212; ideal for factory&#8209;provisioned keys.</p></li><li><p><strong>Unique ID</strong>: An 8&#8209;byte, factory&#8209;programmed serial number. Access it with command <code>4Bh</code>. This can serve as a device identity, UUID seed, or part of a secure boot chain.</p></li></ul><div><hr></div><h2><strong>SFDP: The Self&#8209;Describing Flash</strong></h2><p>The Serial Flash Discoverable Parameter (SFDP) table is a standardised data structure inside the flash. It describes all the capabilities we&#8217;ve listed above in a machine&#8209;readable format: supported opcodes, erase sizes, voltage ranges, timings, and mode&#8209;switch sequences.</p><ul><li><p><strong>Read command</strong>: <code>5Ah</code> (needs a 3&#8209;byte address and dummy cycles)</p></li><li><p><strong>Linux</strong>: The SPI&#8209;NOR subsystem relies heavily on SFDP. If the kernel can parse it, you don&#8217;t need to hardcode any opcodes or erase sizes in the device tree. A log message like <code>spi-nor spi0.0: w25q256jv (32768 Kbytes)</code> confirms a happy SFDP parse.</p></li><li><p><strong>EDK2</strong>: SFDP usage is optional. Many EDK2 SPI flash drivers still use hardcoded tables for known JEDEC IDs. If you&#8217;re writing a new driver, consider using SFDP to auto&#8209;configure timings and capabilities &#8212; it reduces platform&#8209;specific hacks.</p></li></ul><div><hr></div><h2><strong>Critical Timing Parameters</strong></h2><p>You won&#8217;t need most of the nanoseconds&#8209;precision numbers until you debug signal integrity, but these typical durations should always be in your head when writing firmware.</p><ul><li><p><strong>Power&#8209;up to first command</strong>: ~5 ms. Don&#8217;t try to talk to the chip immediately after power&#8209;on.</p></li><li><p><strong>Page Program (256 bytes)</strong>: ~0.4 ms</p></li><li><p><strong>Sector Erase (4 KB)</strong>: ~45 ms</p></li><li><p><strong>Block Erase (64 KB)</strong>: ~150 ms</p></li><li><p><strong>Chip Erase</strong>: ~200 seconds (you read that right &#8212; a full three minutes)</p></li><li><p><strong>Write Status Register</strong>: ~10 ms</p></li></ul><p>After issuing an erase or program, poll the <strong>WIP bit</strong> (Status Register 1, bit 0) until it clears. Never rely on fixed delays unless you&#8217;re in a bare&#8209;metal context with no interrupt infrastructure and even then, it&#8217;s risky.</p><div><hr></div><h2><strong>Platform Deep Dives</strong></h2><h3><strong>For EDK2 Firmware Developers</strong></h3><ol><li><p><strong>Driver skeleton</strong>: Implement your <code>SpiNorFlashRead()</code>, <code>SpiNorFlashWrite()</code>, and <code>SpiNorFlashErase()</code> around the command set above. Use the appropriate opcode for the current address mode.</p></li><li><p><strong>Write enable ritual</strong>: Every program/erase must be preceded by <code>06h</code>. WEL self&#8209;clears on completion, but a failed command may leave it set &#8212; don&#8217;t assume state.</p></li><li><p><strong>Page boundaries</strong>: Never write across a 256&#8209;byte page boundary. Your write routine should split larger buffers into page&#8209;aligned chunks.</p></li><li><p><strong>Address mode housekeeping</strong>: Enter 4&#8209;byte mode (<code>B7h</code>) early in PEI. If your reset vector or SEC phase runs from this flash, ensure the hardware boot ROM or pre&#8209;initialisation stage has already switched to 4&#8209;byte mode, or keep the initial code within the lower 16 MB.</p></li><li><p><strong>Block protection cleanup</strong>: After a firmware update, you may need to clear the BP bits in SR1 to allow writes to the entire chip. Check them if writes fail silently.</p></li><li><p><strong>Variable storage</strong>: For UEFI variables, use 4 KB sectors and implement wear&#8209;levelling at the firmware level. The chip&#8217;s 100,000 cycle endurance per sector is plenty for a well&#8209;designed variable driver.</p></li><li><p><strong>Security registers</strong>: Use them for platform manufacturing data. Locking them is irreversible, so test carefully.</p></li></ol><h3><strong>For Linux Kernel Developers</strong></h3><ol><li><p><strong>Device tree binding</strong>: The compatible string <code>"winbond,w25q256jv"</code> must be paired with <code>"jedec,spi-nor"</code>. The kernel&#8217;s SPI&#8209;NOR framework will handle the rest once it reads SFDP.</p></li><li><p><strong>Partitioning</strong>: Define MTD partitions either as child nodes in the device tree or via the <code>mtdparts=</code> kernel command line. Align partitions to erase block boundaries (preferably 64 KB) to avoid unwanted erase of adjacent data.</p></li><li><p><strong>Quad mode activation</strong>: The QE bit (SR2 bit 4) is set automatically by the framework when your device tree specifies <code>spi-rx-bus-width = &lt;4&gt;;</code> or <code>spi-tx-bus-width = &lt;4&gt;;</code>. Make sure your SPI controller actually supports quad mode.</p></li><li><p><strong>4&#8209;byte addressing</strong>: The kernel handles this transparently if SFDP is parsed. If you&#8217;re on a buggy board where SFDP fails, you may need to force it via the <code>m25p,fast-read</code> and address&#8209;width quirks, but that&#8217;s rare.</p></li><li><p><strong>Verification tools</strong>: After boot, use <code>lsmtd</code>, <code>mtdinfo</code>, or peek at <code>/sys/class/mtd/</code> to confirm the flash is detected and sized correctly. <code>flashrom</code> is your friend for low&#8209;level read/write tests during bring&#8209;up.</p></li></ol><div><hr></div><h2><strong>Final Thoughts</strong></h2><p>The W25Q256JV is a well&#8209;behaved, modern SPI NOR flash that doesn&#8217;t hide many surprises once you understand its status register layout and address&#8209;mode quirk. The most common stumbling blocks are:</p><ul><li><p>Forgetting to set the QE bit before quad operations.</p></li><li><p>Assuming 4&#8209;byte addressing works identically on the FV and JV variants (the JV is the one you want for &gt;16 MB).</p></li><li><p>Crossing page boundaries during writes.</p></li><li><p>Over&#8209;relying on fixed delays instead of polling WIP.</p></li></ul><p>Keep this cheat sheet handy when you&#8217;re deep in <code>SpiFlashErase()</code> and the chip is stubbornly not erasing. Chances are, the answer is a single bit flip away.</p><p><em>If you found this useful, consider subscribing for more firmware&#8209;level deep dives. And if you&#8217;re wrestling with a different flash chip, drop a comment &#8212; I might cover it next.<br><a href="http://chrome-extension://efaidnbmnnnibpcajpcglclefindmkaj/https://cdn.sparkfun.com/assets/c/2/9/2/6/W25Q256JV.pdf">W25Q256JV.pdf</a></em></p>]]></content:encoded></item><item><title><![CDATA[EDK2 SW SMI: All Four Register Approaches and the Dispatch Architecture]]></title><description><![CDATA[Linkedin: click David Zhu]]></description><link>https://gdbplus.substack.com/p/edk2-sw-smi-all-four-register-approaches</link><guid isPermaLink="false">https://gdbplus.substack.com/p/edk2-sw-smi-all-four-register-approaches</guid><dc:creator><![CDATA[gdbplus]]></dc:creator><pubDate>Sat, 16 May 2026 12:20:26 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Cpk8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbac2cc1f-0507-444c-a257-0787b91204b3_2560x1440.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Linkedin: click <a href="https://www.linkedin.com/in/david-zhu-3a68a855/">David Zhu</a> </p><div><hr></div><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://gdbplus.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="/__u/gdbplus.substack.com/subscribe"><span>Subscribe now</span></a></p><p>The Software SMI (SW SMI) is the primary inter-process communication mechanism between the Normal World (DXE/Runtime) and the Secure World (SMM) in UEFI firmware. Understanding exactly how an SMI gets triggered &#8212; and how handlers get dispatched &#8212; is essential for anyone writing SMM drivers, debugging firmware issues, or designing platform firmware.</p><p>This article walks through all four hardware trigger mechanisms, the protocol architecture that abstracts them, and the handler dispatch chain inside SMM</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!Cpk8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbac2cc1f-0507-444c-a257-0787b91204b3_2560x1440.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!Cpk8!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbac2cc1f-0507-444c-a257-0787b91204b3_2560x1440.png 424w, /__u/substackcdn.com/image/fetch/$s_!Cpk8!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbac2cc1f-0507-444c-a257-0787b91204b3_2560x1440.png 848w, /__u/substackcdn.com/image/fetch/$s_!Cpk8!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbac2cc1f-0507-444c-a257-0787b91204b3_2560x1440.png 1272w, /__u/substackcdn.com/image/fetch/$s_!Cpk8!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_webp, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbac2cc1f-0507-444c-a257-0787b91204b3_2560x1440.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!Cpk8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbac2cc1f-0507-444c-a257-0787b91204b3_2560x1440.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bac2cc1f-0507-444c-a257-0787b91204b3_2560x1440.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:581055,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://gdbplus.substack.com/i/197988103?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbac2cc1f-0507-444c-a257-0787b91204b3_2560x1440.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!Cpk8!, /__u/gdbplus.substack.com/w_424, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbac2cc1f-0507-444c-a257-0787b91204b3_2560x1440.png 424w, /__u/substackcdn.com/image/fetch/$s_!Cpk8!, /__u/gdbplus.substack.com/w_848, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbac2cc1f-0507-444c-a257-0787b91204b3_2560x1440.png 848w, /__u/substackcdn.com/image/fetch/$s_!Cpk8!, /__u/gdbplus.substack.com/w_1272, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbac2cc1f-0507-444c-a257-0787b91204b3_2560x1440.png 1272w, /__u/substackcdn.com/image/fetch/$s_!Cpk8!, /__u/gdbplus.substack.com/w_1456, /__u/gdbplus.substack.com/c_limit, /__u/gdbplus.substack.com/f_auto, /__u/gdbplus.substack.com/q_auto:good, /__u/gdbplus.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbac2cc1f-0507-444c-a257-0787b91204b3_2560x1440.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div><hr></div><h2><strong>The Hardware: Four Ways to Assert SMI#</strong></h2><p>Before any software protocol enters the picture, there has to be a hardware mechanism that pulls the CPU&#8217;s SMI# pin. EDK2 supports four distinct approaches, varying by chipset generation and platform.</p><h3><strong>1. I/O Port &#8212; The APM Register (Intel ICH/PCH Classic)</strong></h3><p>The oldest and most widely recognized mechanism. The Intel ICH and its successors (PCH) expose two I/O ports in the ACPI PM I/O space:</p><p>&#9656; <strong>ICH9_APM_STS (0xB3):</strong> The &#8220;data&#8221; or &#8220;status&#8221; register &#8212; a scratchpad that passes a byte to the SMI handler without triggering the SMI on its own.</p><p>&#9656; <strong>ICH9_APM_CNT (0xB2):</strong> The &#8220;command&#8221; register &#8212; writing to this port causes the chipset to assert SMI# on the CPU.</p><p>The sequence matters: write the data byte to APM_STS first, then write the command byte to APM_CNT. The SMM handler reads both back to determine which SW SMI was requested and what data was passed. This is the classic two-register pattern used on virtually all Intel-based platforms for two decades.</p><p>From the OVMF implementation in <code>OvmfPkg/SmmControl2Dxe/SmmControl2Dxe.c</code>:</p><pre><code><code>IoWrite8 (ICH9_APM_STS, DataPort    == NULL ? 0 : *DataPort);
IoWrite8 (ICH9_APM_CNT, CommandPort == NULL ? 0 : *CommandPort);
</code></code></pre><p>The chipset must be configured to route APM port writes to SMI generation &#8212; this is done by setting the APMC_EN and GBL_SMI_EN bits in the SMI_EN register, then locking the SMI_LOCK bit in GEN_PMCON_1 to prevent tampering.</p><h3><strong>2. SMM IPI &#8212; MSR / LAPIC Inter-Processor Interrupt</strong></h3><p>For multi-processor SMM entry, the BSP needs to bring all APs into SMM synchronously. This is done via an SMM IPI &#8212; a special inter-processor interrupt with delivery mode set to SMI.</p><p>The BSP writes to either the SMM IPI MSR or the Local APIC ICR (Interrupt Command Register) with the delivery mode field set to SMI (010b). This causes the target CPU(s) to assert their internal SMI# signal without any chipset involvement.</p><p>This approach is critical for the PiSmmCpu driver in <code>UefiCpuPkg/PiSmmCpuDxeSmm/SmmMp.c</code>, which uses <code>SendSmiIpiAllExcludingSelf()</code> to synchronize all APs into SMM during initialization and for periodic SMI-based MP services. The PI spec explicitly notes that &#8220;the ability to generate this event from a platform chipset agent is an optional capability&#8221; &#8212; making SMM IPI the processor-level fallback when chipset SMI isn&#8217;t available.</p><h3><strong>3. PCI Configuration Space &#8212; Southbridge Registers</strong></h3><p>Some platforms &#8212; particularly AMD-based systems and certain server designs &#8212; expose SMI trigger capability through PCI configuration space registers on the southbridge or FCH (Fusion Controller Hub).</p><p>Even on Intel platforms, the SMI enable and lock bits are configured via PCI config space. The OVMF SmmControl2Dxe driver uses <code>PciRead32()</code> and <code>PciOr16()</code> on the Q35 MCH (Bus 0, Device 0, Function 0) to read the PMBASE register and configure the SMI_EN and GEN_PMCON_1 registers:</p><pre><code><code>PmBase = PciRead32 (POWER_MGMT_REGISTER_Q35 (ICH9_PMBASE)) &amp;
         ICH9_PMBASE_MASK;
mSmiEnable = PmBase + ICH9_PMBASE_OFS_SMI_EN;
</code></code></pre><p>On some AMD platforms, the entire SMI trigger path &#8212; not just configuration &#8212; goes through PCI config space reads and writes to the southbridge&#8217;s SMI generation registers.</p><h3><strong>4. MMIO &#8212; Memory-Mapped Chipset BAR</strong></h3><p>Modern Platform Controller Hubs (PCH) and Power Management Controllers (PMC) map their registers into MMIO space through a PCI Base Address Register (BAR). Instead of legacy I/O port 0xB2, you write to:</p><pre><code><code>PmcBase + SMI_CTRL_OFFSET
</code></code></pre><p>Where PmcBase comes from the PMC device&#8217;s PCI BAR. This is the direction all modern Intel platforms (starting with Skylake-era PCHs) have moved toward &#8212; MMIO-mapped SMI generation, eliminating the need for the legacy I/O port 0xB2 entirely.</p><p>The software interface remains identical &#8212; you&#8217;re still writing a command byte and optionally a data byte &#8212; but the transport is an MMIO write instead of an <code>out</code> instruction.</p><div><hr></div><h2><strong>The Protocol Stack: How EDK2 Abstracts All Four</strong></h2><p>The elegance of EDK2 is that none of the above hardware differences leak into SMM driver code. Two protocols provide the complete abstraction:</p><h3><strong>SmmControl2 Protocol &#8212; Trigger from DXE</strong></h3><p><code>EFI_SMM_CONTROL2_PROTOCOL</code> (MdePkg/Include/Protocol/SmmControl2.h) is a DXE Runtime protocol with two functions:</p><p>&#9656; <strong>Trigger(CommandPort, DataPort, Periodic, Interval):</strong> Engenders the SMI. The platform-specific implementation writes to whatever hardware register (I/O, PCI, MMIO, or MSR) generates SMI#.</p><p>&#9656; <strong>Clear(Periodic):</strong> Acknowledges and clears any software status from the previous Trigger(). The PI spec explicitly notes that Clear() is not responsible for deasserting SMI# &#8212; that happens automatically on SMM entry.</p><p>&#9656; <strong>MinimumTriggerPeriod:</strong> Read-only field specifying the minimum periodic interval the hardware supports. Set to MAX_UINTN when periodic SMI is unsupported.</p><p>Every SMM consumer &#8212; whether it&#8217;s SmmCommunication, a variable driver, or a TCG driver &#8212; calls the same Trigger() interface. The platform DSC includes the appropriate SmmControl2Dxe implementation for its chipset, and that&#8217;s where the hardware-specific write lives.</p><h3><strong>SmmSwDispatch2 Protocol &#8212; Register Handlers in SMM</strong></h3><p>On the SMM side, <code>EFI_SMM_SW_DISPATCH2_PROTOCOL</code> (MdePkg/Include/Protocol/SmmSwDispatch2.h) lets SMM drivers register callback functions for specific SW SMI values:</p><pre><code><code>typedef struct {
  UINTN    SwSmiInputValue;
} EFI_SMM_SW_REGISTER_CONTEXT;
</code></code></pre><p>A driver calls <code>Register(DispatchFunction, &amp;RegisterContext, &amp;DispatchHandle)</code>, specifying the SwSmiInputValue it wants to handle. The SMM core maintains a dispatch table, and when an SMI arrives with that command byte, it calls the registered function.</p><p>Each SwSmiInputValue can have exactly one handler &#8212; it&#8217;s a 1:1 mapping. The handler receives an <code>EFI_SMM_SW_CONTEXT</code> containing:</p><p>&#9656; <strong>SwSmiCpuIndex:</strong> Which CPU generated the SMI</p><p>&#9656; <strong>CommandPort:</strong> The value written to APM_CNT (0xB2)</p><p>&#9656; <strong>DataPort:</strong> The value written to APM_STS (0xB3)</p><div><hr></div><h2><strong>The SmmCommunication Layer: Multiplexing Over a Single SMI</strong></h2><p>Hardware SW SMI values are a scarce resource &#8212; Intel ICH supports only values 00h-0FFh, and many are reserved for platform firmware. A single SW SMI number can&#8217;t serve dozens of consumers... unless you add a second dispatch layer.</p><p>This is exactly what PiSmmCommunication does. It consists of a DXE driver and an SMM driver working together:</p><p><strong>On the DXE side</strong> (SmmCommunication protocol consumer):</p><p>&#9656; Populate an <code>EFI_SMM_COMMUNICATE_HEADER</code> with a HeaderGuid and MessageLength</p><p>&#9656; Store a pointer to this header in an ACPI NVS memory location</p><p>&#9656; Call <code>SmmControl2&#8594;Trigger()</code> with the PiSmmCommunication SW SMI number as the command byte</p><p><strong>On the SMM side</strong> (PiSmmCommunicationSmm):</p><p>&#9656; The PiSmmCommunicationHandler reads the CommBuffer pointer from ACPI NVS</p><p>&#9656; Validates the buffer is outside SMRAM (security check)</p><p>&#9656; Calls <code>gSmst&#8594;SmiManage(&amp;CommunicateHeader&#8594;HeaderGuid, NULL, &amp;Data, &amp;CommSize)</code></p><p>SmiManage does a GUID-based dispatch &#8212; it looks up the HeaderGuid in the SMM handler database and routes the buffer to whichever driver registered for that GUID. This is how a single hardware SW SMI (e.g., value 0xE3) can serve the variable driver, the TCG driver, the FPDT driver, and any number of other SMM consumers &#8212; each with its own GUID-based channel.</p><p>The PiSmmCommunicationSmm driver registers for <code>SwSmiInputValue = (UINTN)-1</code>, which tells the SmmSwDispatch2 to auto-assign an available SW SMI number. The assigned number is saved in the SmmCommunicationContext and retrieved by the PEI/DXE side through the SMM Configuration Table.</p><div><hr></div><h2><strong>The Dispatch Chain: End to End</strong></h2><p>Putting it all together, here&#8217;s the complete flow from a DXE driver wanting to communicate with SMM:</p><ol><li><p>DXE driver fills an EFI_SMM_COMMUNICATE_HEADER with its GUID and payload</p></li><li><p>DXE driver calls SmmCommunication&#8594;Communicate(), which stores the buffer pointer in ACPI NVS</p></li><li><p>SmmCommunication calls SmmControl2&#8594;Trigger(SW_SMI_NUMBER, 0)</p></li><li><p>SmmControl2Dxe writes to the platform-specific hardware (I/O port 0xB2, PCI, MMIO, or SMM IPI)</p></li><li><p>CPU asserts SMI#, saves state, jumps to SMBASE+8000h</p></li><li><p>PiSmmCpu entry point reads APM_STS (or equivalent) to get SwSmiInputValue</p></li><li><p>SmiManage dispatches by SwSmiInputValue to PiSmmCommunicationHandler</p></li><li><p>PiSmmCommunicationHandler reads CommBuffer from ACPI NVS, validates it</p></li><li><p>PiSmmCommunicationHandler calls SmiManage(&amp;HeaderGuid, ...) for second-level GUID dispatch</p></li><li><p>The target SMM driver&#8217;s registered callback receives the data</p></li></ol><div><hr></div><h2><strong>Platform Examples</strong></h2><p><strong>OVMF (QEMU Q35):</strong> Uses approach #1 (I/O port 0xB2/0xB3). SmmControl2Dxe in <code>OvmfPkg/SmmControl2Dxe/</code> configures ICH9 APM registers. Source: <code>SmmControl2Dxe.c</code> lines 115-116.</p><p><strong>Intel Reference Platforms:</strong> SmmControl2 is typically provided by the PchSmmControl driver in the platform&#8217;s silicon package, which writes to the PCH PMC MMIO space (approach #4 on modern platforms).</p><p><strong>PiSmmCommunication:</strong> The reference implementation in <code>UefiCpuPkg/PiSmmCommunication/</code> shows the complete DXE/SMM dance &#8212; SmmSwDispatch2 registration in <code>PiSmmCommunicationSmm.c</code> (line 184-193) and Trigger in <code>PiSmmCommunicationPei.c</code> (line 337-346).</p><div><hr></div><p>The SW SMI mechanism may be considered &#8220;legacy&#8221; &#8212; and indeed, newer platforms are moving toward MM Communication over shared memory without SMI &#8212; but understanding these four hardware approaches and the EDK2 dispatch architecture remains essential knowledge for anyone working at the firmware level.</p><p><strong>#firmware</strong> <strong>#uefi</strong> <strong>#edk2</strong> <strong>#smm</strong> <strong>#x86</strong> <strong>#gdbplus</strong> <strong>#systemmanagementmode</strong> <strong>#bios</strong></p>]]></content:encoded></item></channel></rss>