When x86 boots, it's not strictly in classic real mode, it's in "unreal" mode, with CS.base
= 0xFFFF0000 and CS.limit
= 64K.
On 286 and later, memory addressing doesn't use the segment register values directly. Every segment register is associated with some internal state, a base and limit. For example, mov eax, [edi]
calculates the linear address as DS.base + edi
from the DS:EDI
logical address encoded by the [edi]
addressing mode.
Many people (including Intel's documentation) call this a "descriptor cache", but it's not just a cache. It's guaranteed to keep its state, never reloading from a GDT entry that might have changed. Nothing ever invalidates it, only writes a new value. And its value doesn't always come from a descriptor.
There's no way to write those internal segment base/limit values directly for most segments, only FS and GS bases in 64-bit mode via MSRs or the new wrfsbase
instructions. Or on 286 with LOADALL
.
Writing a value to a segment register updates the internal base in a way that depends on the mode you were in when you executed mov ds, ax
or pop ds
, or retf
or jmp far
or whatever for CS. (Or handling an interrupt and loading a new CS:EIP from the IDT (protected mode) or IVT (real mode), or iret
or TSS stuff loading a new SS.)
- In real mode, the
base = value << 4
. The limit is unchanged.
- In protected mode, the segment register value is used as an index ("selector") into the GDT or LDT to load a new base and limit. (The low 3 bits of a Segment selector are 2-bit privilege level, and 1 bit LDT vs. GDT. The rest is a byte offset into the selected table.)
- On power-on / reset,
CS.base
has an initial value of 0xFFFF0000. It didn't get that way by caching something else, it's just separate architectural state that has its own initial value.
Changing modes (e.g. from real to protected or vice versa) does not reload or change segment register values, or their internal base and limit. It doesn't reinterpret the segment register values and use them to set new bases and limits or anything like that. For example, when a modern UEFI firmware wants to load a legacy 16-bit MBR bootloader (after having switched to long mode after power-on), it has to switch back to real mode. To do that, it needs GDT entries with limit = 64K to load each segment from, if it set new limits on its way from unreal reset state to long mode via protected mode.
Anyway, the architectural model supports segment bases that don't match what you'd get from writing the segment register in the current mode. This is how we can have CS = 0xF000
with CS.base = 0xFFFF0000
as the reset state. Those are two independent values.
The bootup linear address is the top 16 bytes of physical address space
With EIP = 0x0000FFF0, that makes a linear address of 0xFFFFFFF0, the last 16 bytes of linear = physical address space. Same as how 8086 used the last 16 bytes of its 20-bit linear = physical address space.
8086 only supported real mode and didn't have separate internal state for base and limit. It's hard-wired to use CS<<4
as the base for linear address calculations. (In hardware, a constant shift is just a matter of wiring the inputs to an adder.) So its CS on reset of 0xF000
gives it a linear base of 0xF0000
. (That's the same CS:IP value 386 still boots with, but 386 uses a different CS.base to get a different linear address.) Related: a Q&A about the fact that it's only 386 and later that uses a 32-bit CS base.
The idea is you wire up a ROM so it responds when all the address lines from A31 to A20 are asserted. This might be an "alias" for (part of) a larger ROM that's also mapped at another address, perhaps the end of the 1st MiB where typical 8086 systems put it. That makes it accessible via segment bases that you can set while in the initial real-ish mode, so for example the BIOS can enable interrupts. (Handling interrupts involves pushing a CS value and restoring it via iret
, which applies real-mode rules for setting CS.base). And so a BIOS can access static data in the ROM via a DS or ES value it can set. (Although you can do that with mov eax, [cs:si]
or use cs rep movsd
to copy a block from ROM at CS:SI to somewhere in RAM at ES:DI, since the CS prefix lets you use it for data addressing.) As ninjali commented, it was normal for PCs to have the ROM aliased to both places:
Those top 16 bytes could contain a near jmp
to jump to earlier in the top 64KiB without touching CS, if the BIOS wants to run a bit more code before far-jumping to a new CS:IP and/or enabling interrupts, or even switch to protected mode to avoid ever needing any ROM taking up space in the low 1MiB of physical address space. (Stephen Kitt comments that this was the design rationale for 286 also resetting with code fetch coming from the top 16 bytes of its 24-bit physical address space).
386's CS.base = 0xFFFF0000 is 64KiB away from the highest possible address, just like 8086's initial base = 0xF0000, so that's how much room you have for code with just near jumps and no interrupts. Plenty of room for mov-immediate or cs rep movsd
to set up a GDT if you want to switch out of real mode.
As Nate commented, 386 manuals even describe this as "After RESET, address lines A{31-20} are automatically asserted for instruction fetches," until after the first jump or call to a new segment. Which, unless you switch to protected mode before doing so, will be in the low 1MiB because of how segment reg writes are interpreted in (un)real mode, which will de-assert those address lines.
(With paging disabled, physical address = linear address. Paging is disabled in real/unreal mode, and is optional in protected mode. Mandatory for long mode. With paging enabled, 32-bit or 64-bit linear addresses are virtual, translated to physical by the page tables and the TLB which is an actual cache of page-table entries (PTEs).)
Related:
Fun fact: Unreal mode (setting base=0 / limit=-1 so you can use 32-bit address size to access a flat 4GiB of memory in 16-bit code) is somewhat durable since writing a segment reg in real mode doesn't update the limit, only base. So for a flat memory model you can use CS=DS=ES=SS=0, with limit=-1. You can still make BIOS calls like int 0x10
; BIOS code that changes seg regs will restore your seg base when it restores your segment register value, without changing the limit.
(But interrupts in real mode don't save/restore the upper half of EIP, so that's a limitation on using huge code; only convenient for 32-bit data addressing.)
There are some other x86 bootup questions on SO, since modern x86's reset state is unchanged from 386. Modern PCs have UEFI firmware that can load software from disk in 32 or 64-bit mode, but most are able to switch to real mode (CSM = compatibility support modules in your boot options) to run legacy BIOS MBR 512-bit boot sectors, providing the legacy int 0x10
and int 0x13
and other BIOS interfaces.
In a few years, the proposed x86S will finally drop 16-bit mode
Interesting timing for asking this on retrocomputing instead of SO: this week, Intel proposed x86-S (simplified), which will drop support for legacy mode, keeping only long mode (and its 32-bit compat sub-mode, but without 16-bit address-size being possible, simplifying the decoders for machine code). See https://www.phoronix.com/news/Intel-X86-S-64-bit-Only and Intel's whitepaper: https://www.intel.com/content/www/us/en/developer/articles/technical/envisioning-future-simplified-architecture.html
(It'll be at least some years before real CPUs that work this way are sold, if ever!)
Until some version of that becomes a reality, this isn't only a retrocomputing question; current x86-64 CPUs still boot the same as 386.