24

As far as I know, x86 CPUs start up in 16-bit 'real' mode (maybe some don't).

  • The reset vector is 0xFFFFFFF0 (in most CPUs and in this context)
  • In this 16-bit real mode, we can only access 16 bit registers.
  • The CPU's CS register stores the segment and IP stores the offset.
  • For example, I expect the CS register can store maximum 0xFFFF and the IP register can store 0xFFFF, and which from my research - I found that (0xFFFF * 16) + 0xFFFF = 0x10FFEF, i.e., allows 20-bit addresses to be accessed.

(Wikipedia says that the reset vector following the Intel 80386 processor is always 0xFFFFFFF0) Now, my question is - How can the CPU start at the reset vector which is a 32 bit address but be in 16 bit real mode where even the CS and the IP register together cannot represent a 32 bit address using segments and offsets? Is there any other hardware-wiring way that this works?

11
  • 1
    I am new to these CPU inside-things, any suggestion of how I can learn these things will be helpful. May 25, 2023 at 15:41
  • 10
    Modern x86 does not truly start in real mode, it's "unreal" mode with a CS base that isn't 16x the segment-register value. Duplicate on Stack Overflow: Software initialization code at 0xFFFFFFF0H (it makes sense as an SO question: modern x86 still boots this way. At least until Intel's proposed x86-S (simplified) becomes reality and CPUs will drop support for legacy mode, keeping only long mode (and its 32-bit compat sub-mode, but without 16-bit address-size being an option: phoronix.com/news/Intel-X86-S-64-bit-Only) May 26, 2023 at 3:03
  • Nitpick: 0x10FFEF requires 21 bits to express. May 26, 2023 at 15:33
  • 1
    @user3840170, ah yes. I understood. I understand that 0x10FFEF is more than 20 bits. But that doesn't change the meaning of my sentence. As far as I know, I meant to say that if we are able to access 0x10FFEF maximum, then we are surely able to access 20 bit addresses. As far as I know, CPUs, when they get such an address (in this mode), they discard the highest bit, right? May 26, 2023 at 15:57
  • 3
    The 8088 had only 20 address lines, so it truncated physical addresses to that width. The 286 did not, so IBM rigged the memory controller to truncate it for the CPU, which had to be additionally disabled when transitioning to protected mode. This is what all the references to the A20 gate refer to. Later x86 models followed the 286, and eventually built the functionality into the CPU in the form of the (recently-obsoleted) A20M# pin (because having it external to the CPU interferes with caching). May 26, 2023 at 16:10

4 Answers 4

28

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 in 486 and later, by 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.

1
16

To understand this, you need to understand the basics of segment:offset addressing in protected mode: the segment value points to a descriptor, which contains a base address, segment limit and various other pieces of information. The actual address is determined by adding the offset to the segment base address. To avoid having to look up descriptors all the time, the CPU maintains a descriptor cache, containing the descriptor information for the currently-loaded segments (determined by the values in CS, DS etc.).

Even in real mode, 286 and later x86 CPUs calculate addresses using this descriptor cache. The expected real mode behaviour is implemented by handling segment register loads specially: in real mode, loading a segment register transparently updates the underlying descriptor cache to correspond to the appropriate linear address (the value loaded in the segment register, multiplied by 0x10, with a 64KiB limit).

On 386 CPUs and later, on reset, the CS register is set to 0xF000, and descriptor cache points to 0xFFFF0000; the IP register is set to 0xFFF0. This situation only lasts until the next far jump (or far call etc.), which sets the values in the descriptor cache as would be expected in real mode.

The LOADALL instruction can be used to manipulate the descriptor cache to achieve a similar result. The underlying principle is used in “unreal mode” too — instead of changing the base address however, the technique usually relies on changing the segment limits in the descriptor cache, to allow access to the full 4GiB address space using 32-bit offsets.

1
9

In short, yes the 386 will boot from 0xFFFFFFF0 address mentioned in real mode, because CS selector base is set to 0xFFFF0000 and IP will be 0x0000FFF0. The value of the CS register itself is irrelevant at that point, as the CS register value is not directly used as memory address, it's rather used as a selector, as if it was index to a larger structure called a segment descriptor that among many other things defines a 32-bit base address. In 386 real mode loading the segment register actually updates the underlying segment descriptor base address to address memory like a 8086/8088, but in protected mode loading a segment register with a value actually means loading the underlying segment descriptor data structure from a table indexed by the segment register value. The default values for segment base/limit/etc are set on hardware reset, thus the system memory address decoding must have ROM at end of 4G memory space and alias it anywhere required.

There are many wrong assumptions which I will try to address.

All x86 CPUs so far start in real mode where it is in a mode that is compatible with the code and memory model of 8086/8088.

  • The reset vector is not 0xFFFFFFF0, it can't be because that's not a memory address a 8086/8088 or any CPU in real mode can access from the segmented real mode memory model point of view. On reset, in a 8086/8088, the CS register will be 0xFFFF and IP register will be 0x0000. It will be slightly different values and map to different memory addresses based on CPU. On a real 8086/8088 CPU with only segmented real mode available, that is right below maximum addressable 1 MB, address 0xFFFF0. On a 286, the CS is 0xF000 and IP is 0xFFF0, but as the CS is just a selector, the actual CS Base is 0xFF0000, so the address is right below the maximum addressable 16 MB, address 0xFFFFF0. On a 386, the CS:IP are identical to 286, but CS Base is 0xFFFF0000, so the address is right below the maximum addressable 4GB, address 0xFFFFFFF0.

  • In 16-bit real mode you are not limited to 16-bit registers if you have longer registers like on a 386.

  • The CS only stores the segment directly if you have a 8086/8088, in reality the later processors call CS just a selector and the CS Base may differ. In practice, later processors in real mode also load the CS Base in tandem with CS register when you update it. So from programmer point of view, the CS register seems to work like in real mode, but actually loading the CS register will just update the CS selector base address with a value that matches the CS as a segment register.

  • Yes, in real mode the CS and IP are 16-bit registers and can be used to access only 1MB on a 8086/8088, and slightly more than 1 MB on a 286 and above, the CS Base can be set to point somewhere higher during reset. On a 386 the IP is already a 32-bit register, but in real mode segments are limited to 64kB so even if IP can increase from 0x0000FFFF to 0x00010000, it would be a segment violation to access beyond limit of 0x0000FFFF and no useful piece of real mode code would also try to allow that.

Also many things apply when the system in question is a PC alike computer. If e.g. the 386 CPU is in another kind of computer or embedded system, then it does not have to be downwards compatible with any PC system that runs BIOS or DOS or games or anything.

4
  • What is meant by the "CS Base" in "..the CS is just a selector, the actual CS Base is 0xFF0000..."? Do you mean the same thing as the other answer said, i.e., the segment descriptor? But you also say "The value of the CS register itself is irrelevant at that point." May 25, 2023 at 18:55
  • I updated my answer based on your previous comment. I hope it is now more clear.
    – Justme
    May 25, 2023 at 19:04
  • Thanks. It is more clear. So, this implies that loading a value on to the CS register (using far jmps, etc.) will permanently make us unable to switch back to the 0xFFFFFFF0 address without switching modes? (ignoring the LOADALL instructions, etc.). Also, final question: Who put that descriptor there? The BIOS ROM is mapped to that address that CS (as a selector) initially points to? Or some hardware magic? May 25, 2023 at 19:22
  • 1
    @Exampleperson: I think the missing piece is that (part of) the ROM is also aliased at the end of the first megabyte of memory, see e.g: pcjs.org/blog/2015/04/16 and martin.uy/blog/bios-execution-in-qemu-where-it-all-starts
    – ninjalj
    May 26, 2023 at 2:32
2

I'm putting this in as a separate answer since some people might disagree with my logic and it will give them a chance to downvote me.

If you're interested and have the time, I think you'll find https://bitsavers.org/components/intel/80286/121960-001_iAPX_286_Operating_Systems_Writers_Guide_1983.pdf valuable.

I'm inclined to argue that the architecture of the '286 was a derivative of the earlier failed iAPX432, but since it could not be manufactured cost-effectively in ~1978 it was further cut down to the 8086 as an entry-level device which Intel felt could compete against Zilog, Motorola and the rest.

The '432 was intended to compete with the mainframe systems of the day (Burroughs, Multics and so on). This was before the flat memory model of the IBM S/360 and 68000 etc. had swept the board, and segmentation in various forms was not seen as being an inherently bad idea.

The '432 would have needed a total of three chips, each of 50-100k transistors. The '286 needed around 120k. It's well known that the design of the 8086 lost a couple of segment registers, but that allowed Intel to fit it into a chip of <30k transistors.

This progression really provides the only robust explanation of why Intel adopted the somewhat weird segment:offset addressing scheme in the 8086. The '286 wasn't an enhanced 8086, but instead the 8086 was a derivative of the '286 architecture, and that was a derivative of a long-running project which had gorged voraciously (and ultimately choked) on the architectures of established systems.

In short, the 8086 was a cut-down version (losing two segment registers) of a cut-down version (not implementing descriptors) of a cut-down version (the iAPX-286) of a failed engineering project (the iAPX-432). And the 8088 was, of course, cut down even further to reduce the data bus width.

https://thechipletter.substack.com/p/iapx432-gordon-moore-risk-and-intels ("Operation Crush").

The key thing is that the Wp article specifically refers to PHYSICAL ADDRESS. This is a specific term that is explained deep within the documentation of the various x86 CPUs, but the bottom line is that various internal states of the chip are hard-wired to make this accessible at least until the first JMP opcode.

During boot, I'm not sure whether you can safely talk about cs:ip being translated to that address. As others have said, cs reads back as ffff and ip (initially) reads back as 0000 (I think, I don't have my stuff with me), on an 8086 or V20 etc. that would translate to ffff0 which is 16 bytes below the top of the BIOS ROM. But on a '286 or later the physical address is just what's been stuffed into various lookaside registers by the reset signal: think of it as deriving from a code segment descriptor hardwired deep inside the chip. Divine revelation rather than translation.

Once the physical reset vector has been generated from the lookaside registers, some bits might be ignored because the overall system design quite simply has no intention of supporting that much memory: hence a '386 with fffffff0 as a boot vector which only expects to support a few hundred Mb of SIMMs treats it as xffffff0. However this is external to the CPU chip, in the same way that gating the A20 signal and forcing a hard reset are outside the CPU.

4
  • That's a neat, possibly controversial piece of history. It does not answer the asked question in any way though.
    – Justme
    May 29, 2023 at 7:39
  • @Justme Quite frankly, I don't care. What I do care about is that OP has subsequently said that he's interested in learning about the topic, and I feel that that Intel document is the best way of getting a good understanding of it (for the reasons I have given). May 29, 2023 at 8:02
  • 2
    Would be a nice story, if not contradicted by history. At the time the 8086 was developed, the 432 project was still in full swing. It was further developed by a very small team with the sole target to build a stop gap measure for 8080/85 customers demanding larger memory sizes and threatening to look for other choices. The use of segmented memory access is simply a result of trying to keep within 16 bit address range to get comparable code size as well as easy semi automatic conversion and low effort to accommodate expanded code size.
    – Raffzahn
    May 29, 2023 at 23:56
  • @MarkMorganLloyd You don't need to add too much to this answer to make it answer the question – perhaps quoting relevant sections from the Intel document. Could you do so, please?
    – wizzwizz4
    May 30, 2023 at 1:04

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .