x86 Real-Mode Segmentation & the 8086 Memory Interface
Real-mode x86 forms a twenty-bit address from a sixteen-bit segment and offset, so many logical addresses alias the same byte. The original 8086 then maps bytes onto two eight-bit memory banks selected by A0 and BHE#, making alignment visible as bus cycles.
The 8086 has 16-bit offsets but a 20-bit external address bus. Segmentation bridges that mismatch: a logical address names a segment and an offset; the bus interface converts them into the physical byte address used by memory. The same arithmetic survives in current x86 real-address mode, while the original pin-level bank selection belongs specifically to the 8086 bus.
Segment plus offset #
For the original 8086, the address calculation is
where is the unsigned 16-bit segment value and is the unsigned 16-bit offset. Shifting by four makes segment bases start on 16-byte paragraph boundaries. Each segment can address 64 KiB of offsets, and adjacent segment windows overlap heavily.
The overlap lets software relocate a 64 KiB view cheaply, but it also creates aliases: different segment:offset pairs can name identical storage. Aliasing complicates pointer comparison, serialization, debugging, and reasoning about ownership.
Which segment is selected #
| Reference | Offset source | Default segment |
|---|---|---|
| instruction fetch | IP |
CS |
| stack push/pop/call/return | SP |
SS |
effective address using BP |
BP plus permitted index/displacement |
SS |
| most other data references | BX, SI, DI, or displacement combinations |
DS |
| string source | SI |
DS (override permitted) |
| string destination | DI |
ES on the 8086 |
A segment-override prefix changes the default for an eligible memory operand in the following instruction. It does not rewrite a segment register and it does not turn IP, SP, or arbitrary general registers into legal 8086 addressing components.
The effective address calculation and segment calculation are distinct:
- the EU calculates a 16-bit effective offset from allowed registers and displacement;
- the BIU selects a segment register, shifts it by four, adds the offset, and initiates the bus cycle.
That distinction is why LEA returns an effective offset without reading memory or adding a segment base.
The one-megabyte boundary and A20 #
The original device presents only twenty address bits. A sum that produces bit 20 cannot appear on its pins, so the bus address wraps modulo . This made addresses near FFFF:0010 alias low memory.
Later processors had more address lines and would naturally expose bit 20. The IBM PC/AT added control of the A20 line to preserve software that depended on the old wraparound. Modern systems normally enable A20 early; emulators and firmware still need a defined policy when reproducing legacy real-mode behavior.
Two byte banks on a sixteen-bit bus #
Memory is byte-addressed, but the 8086 transfers up to sixteen data bits per bus cycle. Board designs therefore arrange two eight-bit banks:
- the low bank carries the byte at an even address and is selected with
A0=0; - the high bank carries the byte at an odd address and is selected with active-low
BHE#=0; - an aligned word at an even address selects both banks in one transfer;
- an unaligned word beginning at an odd address spans two aligned words and requires two bus cycles.
| Transfer | A0 |
BHE# |
Active byte lanes |
|---|---|---|---|
| byte at even address | 0 | 1 | low bank |
| byte at odd address | 1 | 0 | high bank |
| word at even address | 0 | 0 | both banks |
| word at odd address | two cycles | two cycles | high byte of one word, low byte of the next |
This is not a general law that every unaligned x86 access takes exactly two external cycles. It is the direct consequence of the original 8086's 16-bit bus and bank-select signals. Current cores use caches, wider internal transfers, and split-access rules at several boundaries.
Memory and I/O are separate spaces #
The 8086 exposes a one-megabyte memory address space and a separate 16-bit port address space. Memory operands use ordinary instruction addressing and segmentation. IN and OUT address ports either with an immediate eight-bit port number or the value in DX; the accumulator is the data operand.
The separation affects board decoding and instruction choice. A numeric port address is not a memory pointer, even if the same numeric value exists in both spaces.
Worked address trace #
Assume:
DS = 2000h
BX = 0101h
SI = 0002h
instruction operand = word [BX + SI + 3]
The effective offset is 0101h + 0002h + 0003h = 0106h. Because no BP participates, the default segment is DS. The physical address is 2000h × 10h + 0106h = 20106h. It is even, so the word can select both byte banks in one original-8086 bus transfer.
Change only BX to 0102h; the physical address becomes 20107h, and the same word is now unaligned and crosses two bus cycles. Address arithmetic and transfer cost meet at the final physical byte address.
Diagnose the layer that failed #
| Observation | Likely layer |
|---|---|
| correct offset, wrong 64 KiB region | default/override segment selection |
| two logical pointers disagree but memory changes together | segment aliasing |
| byte accesses pass, odd-address words fail | bank selection or split-cycle handling |
| code works below 1 MiB but fails near the boundary | A20 policy or wrap assumption |
IN/OUT reaches nothing while memory reads work |
port-space decode, not segmentation |
Always write the logical address, effective offset, physical address, transfer width, and active lanes separately. Collapsing them into “the address” hides the fault.