Intuition What this page is
The parent x86 overview showed you how things work. Here we hit every case the two big x86 skills can throw at you: address arithmetic (segment × 16 + offset, in every regime) and register windows (RAX → EAX → AX → AL, including the traps). We enumerate the scenarios first, then work one example per scenario, forcing you to forecast before each answer.
Two prerequisite ideas we lean on, both built from zero below so you never meet a symbol cold:
A hex digit is one of 0 , 1 , … , 9 , A , … , F — sixteen values, i.e. exactly 4 bits (because 2 4 = 16 ). This is the reason x86 loves hex: one hex digit = one nibble = 4 wires.
A shift-left-by-n written x ≪ n means "glue n zero-bits onto the right end of x 's binary form," which multiplies the value by 2 n . So ≪ 4 is × 16 , i.e. "append one hex zero."
Two pieces of plain math shorthand appear below — here's what they mean so no symbol is a surprise:
[ a , b ] (an interval ) is shorthand for "every value from a up to b , endpoints included." So x ∈ [ 0 , 3 ] just means 0 ≤ x ≤ 3 .
x mod m (modulo ) means "the remainder left after removing whole copies of m from x " — i.e. what's left once x has wrapped around a wheel of size m . For example 0x100000 mod 0x100000 = 0 because it wraps exactly once back to the start.
Everything on this topic is one of these cells . Each worked example below is tagged with the cell it covers, so by the end no cell is left unseen.
#
Case class
What's special about it
Example
A
Address: ordinary case
segment & offset both nonzero, no wrap
Ex 1
B
Address: zero degenerate
offset = 0 , or segment = 0
Ex 2
C
Address: overlap / aliasing
two different (seg, off) pairs → same byte
Ex 3
D
Address: wrap-around / limit
sum exceeds 20 bits (≥ 1 MB)
Ex 4
E
Register: ordinary window read
pull EAX/AX/AL/AH out of RAX
Ex 5
F
Register: partial-write trap
writing EAX zeroes upper 32; AL/AH/AX/RAX writes behave differently
Ex 6
G
Word problem (real world)
"will this .COM file fit?"
Ex 7
H
Exam twist
instruction-length reasoning + a "gotcha" address
Ex 8
Below, all numbers with a 0x prefix are hexadecimal (base 16); everything else is decimal.
Worked example Ex 1 (Cell A) — plain physical address
Real mode. Segment = 0x2F00 , Offset = 0x0140 . Find the physical byte address.
Forecast: guess the leading hex digits before reading on — is it near 0x2F1... something?
Shift the segment left by 4 bits (× 16 ): 0x2F00 ≪ 4 = 0x2F000 .
Why this step? The segment names a 16-byte-aligned "paragraph." Multiplying by 16 turns the 16-bit segment into the top 16 bits of a 20-bit address; the trailing hex 0 is the appended nibble (see figure below).
Add the offset : 0x2F000 + 0x0140 = 0x2F140 .
Why this step? The offset selects the exact byte inside the segment's window, so it fills the low bits .
Answer: 0x2F140.
Verify: the result must lie in [ seg × 16 , seg × 16 + 0xFFFF ] = [ 0x2F000 , 0x3EFFF ] . Indeed 0x2F140 sits just inside. ✓
The figure below shows the assembly literally: watch the yellow box gain a trailing 0 (that's the ≪ 4 ), then the green offset drop into the low nibbles, giving the red result 0x2F140.
Worked example Ex 2 (Cell B) — offset zero, then segment zero
(i) Segment = 0x1000 , Offset = 0x0000 . (ii) Segment = 0x0000 , Offset = 0x0400 .
Forecast: when one input is 0, does the formula break? Predict both answers.
(i) 0x1000 ≪ 4 + 0 = 0x10000 + 0 = 0x10000 .
Why this step? Offset 0 means "the very first byte of the segment." Nothing degenerates — the formula is total, it handles zero like any other value.
(ii) 0x0000 ≪ 4 + 0x0400 = 0 + 0x0400 = 0x00400 .
Why this step? Segment 0 means "the window starts at physical address 0," so the offset is the physical address. This is exactly how the CPU reaches the low interrupt-vector table.
Answers: (i) 0x10000, (ii) 0x00400.
Verify: both are < 1 MB = 0x100000 , so no wrap. And (ii) equals its offset — the sanity check for "segment 0." ✓
Common mistake "Segment 0 is illegal / meaningless."
Why it feels right: zero often means "empty" or "invalid."
The fix: segment 0 is perfectly legal; it just anchors the window at physical 0. The interrupt vector table literally lives there.
Intuition Two names, one byte
Because a segment slides in 16-byte steps but an offset can roam over 65536 bytes , many different (segment, offset) pairs point to the same physical byte. This is address aliasing , a genuine 8086 hazard.
Worked example Ex 3 (Cell C) — prove two pairs alias
Show that 0x1234:0x0005 and 0x1230:0x0045 hit the same byte. (Notation seg:off.)
Forecast: guess whether they collide before computing.
First pair: 0x1234 ≪ 4 + 0x0005 = 0x12340 + 0x0005 = 0x12345 .
Why this step? Standard formula — establishes the target byte.
Second pair: 0x1230 ≪ 4 + 0x0045 = 0x12300 + 0x0045 = 0x12345 .
Why this step? We lowered the segment by 4 (four paragraphs) and raised the offset by 0x40 (64 bytes) — the two changes cancel because 4 paragraphs down = 4 × 16 = 64 bytes = 0x40 up. They balance exactly.
Both = 0x12345 — they alias.
Verify: difference of physical addresses must be 0 : 0x12345 − 0x12345 = 0 . ✓ The offset only spans 16 bits (0x0000–0xFFFF), so any physical byte has up to ⌊ 65536/16 ⌋ = 4096 different names — a huge overlap web.
The figure below plots the single red target byte 0x12345 on a physical number line, with the blue pair (short +0x05 hop from base 0x12340) and the green pair (longer +0x45 hop from base 0x12300) both arriving at it — two arrows, one landing spot.
Intuition What happens past the top?
The real-mode adder produces a value that can reach 0xFFFF × 16 + 0xFFFF = 0x10FFEF — that's past 1 MB . On the original 8086 there were only 20 address wires, so bit 20 fell off and the address wrapped to 0 (the famous "A20" story). We treat that as "take the result modulo 2 20 " (see the modulo reminder at the top).
Worked example Ex 4 (Cell D) — force a wrap
Segment = 0xFFFF , Offset = 0x0010 . Give the raw sum and the wrapped 20-bit address the 8086 actually used.
Forecast: the raw sum overflows 20 bits — where does it land after wrap?
Raw sum: 0xFFFF ≪ 4 + 0x0010 = 0xFFFF0 + 0x0010 = 0x100000 .
Why this step? 0xFFFF0 is the highest paragraph; adding 0x10 tips us to exactly 2 20 .
Wrap to 20 bits: 0x100000 mod 2 20 = 0x100000 − 0x100000 = 0x00000 .
Why this step? Only 20 wires exist (0x00000 –0xFFFFF ), so the 21st bit is discarded — the address rolls over to the bottom.
Answers: raw 0x100000, wrapped 0x00000.
Verify: raw sum lies in the theoretical range [ 0 , 0x10FFEF ] ✓, and 0x100000 mod 0x100000 = 0 ✓ — the classic "high memory area" wrap that DOS relied on.
Definition Register window
The names RAX / EAX / AX / AL are the same 64 physical bits , each name exposing a smaller low slice. Reading a smaller name = masking off (throwing away) the high bits. No data moves.
Worked example Ex 5 (Cell E) — slice a register
RAX = 0xDEADBEEFCAFEBABE. Read EAX, AX, AL, and AH.
Forecast: which bytes survive for each name?
EAX = low 32 bits = low 8 hex digits = 0xCAFEBABE.
Why this step? 32 bits = 8 nibbles = 8 hex digits from the right, so we keep the rightmost 8 digits and drop the rest.
AX = low 16 bits = low 4 hex digits = 0xBABE.
Why this step? 16 bits = 4 nibbles = 4 hex digits, so AX is a strictly smaller window than EAX — we keep only the rightmost 4 digits.
AL = low 8 bits = low 2 hex digits = 0xBE.
Why this step? 8 bits = 2 nibbles = 2 hex digits; AL is the lowest byte , so only the last two digits survive.
AH = bits 8–15 (the second byte, not the lowest) = 0xBA.
Why this step? AH is the odd one: it's the high byte of AX , i.e. digits 3–4 from the right, a leftover from 8086's split AX = AH:AL.
Answers: EAX 0xCAFEBABE, AX 0xBABE, AL 0xBE, AH 0xBA.
Verify: rebuild AX from its halves: AH ≪ 8 + AL = 0xBA00 + 0xBE = 0xBABE ✓, matching step 2.
The figure below stacks the 16 hex digits of RAX and brackets each name's window from the right: green EAX (8 digits), yellow AX (4), red AL (2), and — offset from the low end — the orange AH bracket, making vivid that AH is not at the bottom.
Intuition The single most surprising x86 rule
Writing to EAX zero-extends: it clears the top 32 bits of RAX. But writing to AX, AL, or AH leaves the upper bits untouched (they merge ). And writing to RAX replaces all 64 bits. This asymmetry burns everyone once. Reason: x86-64's designers made 32-bit writes clear the top to help the pipeline, but had to keep 8/16-bit writes merging for 8086 compatibility — a fossil colliding with a modern rule.
Worked example Ex 6 (Cell F) — track the upper bits through every write width
Start RAX = 0x1122334455667788. Apply in order:
mov eax, 0x0000000A; then mov al, 0xFF; then mov ah, 0x77; then mov ax, 0xBEEF; then mov rax, 0xCAFEF00DDEADBEEF. Give RAX after each.
Forecast: predict whether the 0x11223344 prefix survives each write, and which byte mov ah touches.
mov eax, 0x0000000A (32-bit) → zeroes upper 32 : RAX = 0x000000000000000A.
Why this step? The zero-extend rule. The old 0x11223344 is gone forever.
mov al, 0xFF (8-bit low) → merges lowest byte, upper untouched: RAX = 0x00000000000000FF.
Why this step? AL only replaces the lowest byte (0A → FF ); no zero-extend for 8-bit.
mov ah, 0x77 (8-bit high of AX ) → merges the second byte, everything else untouched: RAX = 0x000000000000_77FF = 0x00000000000077FF.
Why this step? AH writes bits 8–15, i.e. digits 3–4 from the right, leaving AL (0xFF) and all higher bytes alone. This is the missing 8-bit-offset case: an 8-bit write that does not touch the bottom byte.
mov ax, 0xBEEF (16-bit) → merges low 2 bytes: RAX = 0x000000000000BEEF.
Why this step? AX replaces the low 2 bytes together (overwriting both the AH=0x77 and AL=0xFF from before); bits above 16 stay as they were (all zero here).
mov rax, 0xCAFEF00DDEADBEEF (64-bit) → replaces all 64 bits : RAX = 0x0CAFEF00DDEADBEEF? No — the literal is 15 hex digits, so RAX = 0x0CAFEF00DDEADBEEF padded to 16 digits: RAX = 0x0CAFEF00DDEADBEEF.
Why this step? A full-width write has nothing to merge or zero-extend — it simply is the new register. This anchors the top of the width spectrum (8 → 16 → 32 → 64).
Answers: 0x000000000000000A → 0x00000000000000FF → 0x00000000000077FF → 0x000000000000BEEF → 0x0CAFEF00DDEADBEEF.
Verify: the AH write must set bits 8–15 to 0x77 while leaving AL=0xFF: ( 0x77 ≪ 8 ) + 0xFF = 0x7700 + 0xFF = 0x77FF ✓. Re-run the merge trap from a nonzero prefix RAX = 0xFFFFFFFF000000FF doing mov al, 0x11: merge gives 0xFFFFFFFF00000011, keeping the 0xFFFFFFFF ✓. And the 64-bit write keeps exactly the literal's value ✓.
mov al, x clears the rest of the register like mov eax, x does."
Why it feels right: the 32-bit form clears the top, so you assume all partial forms do.
The fix: only 32-bit destination writes zero the upper 32 bits. 8-bit (AL/AH) and 16-bit (AX) writes merge into the existing register; 64-bit (RAX) writes replace everything.
Definition CS and PSP (two DOS terms we're about to use)
CS = the Code Segment register , one of x86's segment registers (mentioned in the parent note). In real mode it holds the segment number of where the current program's code lives; the physical base of that window is CS × 16 .
PSP = Program Segment Prefix , a fixed 256-byte (0x100) header block DOS builds at the very front of every loaded program (command line, environment pointer, etc.). Your code starts right after it, at offset 0x100.
Worked example Ex 7 (Cell G) — will the
.COM program fit?
A DOS .COM program is loaded at CS = 0x0800, and all code+data+stack must live in one 64 KB segment . The program is 0xFE00 bytes of code, needs 0x0100 bytes of PSP header at the front, and reserves 0x0200 bytes of stack at the top. Does it fit in the segment, and what physical address is its last usable byte?
Forecast: guess yes/no before adding up.
Total demand: PSP header + code + stack = 0x0100 + 0xFE00 + 0x0200 = 0x10100 bytes.
Why this step? A single real-mode segment can address offsets 0x0000–0xFFFF, i.e. 0x10000 = 65536 bytes total. Compare demand to that budget.
Compare: 0x10100 > 0x10000 , so it overflows by 0x100 = 256 bytes . It does not fit.
Why this step? The offset field is only 16 bits; anything past 0xFFFF cannot be reached from this CS without changing the segment register.
Segment base (for context): 0x0800 ≪ 4 = 0x08000 , so the segment spans physical 0x08000–0x17FFF; its last addressable byte is 0x17FFF.
Why this step? To report the answer as a real physical address we must anchor the segment's window in memory: base = CS× 16 , and the highest reachable byte is base + largest offset (0xFFFF). This turns "it overflows" into a concrete address the loader would care about.
Answer: does NOT fit — over by 256 bytes; segment covers 0x08000–0x17FFF.
Verify: last byte = base + max offset = 0x08000 + 0xFFFF = 0x17FFF ✓, and 0x10100 − 0x10000 = 0x100 = 256 ✓. This overflow is exactly why big programs used the .EXE format with multiple segments — see Memory Addressing Modes .
Definition ModR/M (a one-byte encoding field)
Most x86 instructions that touch operands carry a ModR/M byte — a single byte that names which register and/or memory operand the opcode acts on, and picks the addressing mode. It's part of the instruction's own bytes. Some short forms (like the accumulator-with-displacement MOV used below) skip it. For the full field layout see Memory Addressing Modes ; for what these bytes become after decode see Micro-operations (µops) .
Worked example Ex 8 (Cell H) — the two-part gotcha
(a) An instruction mov eax, [0x11223344] (32-bit displacement, no ModR/M byte, real 5-byte form) sits at CS:IP = 0x0000:0x7FFE. Here IP is the instruction pointer — the offset of the next instruction to run inside the code segment. After this mov executes, IP advances past it. Give the physical address of the first byte of the next instruction . (b) Could that same mov legally be mov rax, 0x1122334455667788? Reason about the 15-byte length cap from the parent note.
Forecast: watch for the offset nearly hitting 0xFFFF.
(a) length of this MOV: opcode 0xA1 (1) + 32-bit displacement (4) = 5 bytes.
Why this step? The constant 0x11223344 is baked inside the instruction as a 4-byte displacement — bigger constant ⇒ longer instruction (the root cause of variable length).
Next IP (offset): 0x7FFE + 5 = 0x8003 .
Why this step? IP moves forward by the instruction's byte length. No wrap: 0x8003 < 0xFFFF.
Next physical address: 0x0000 ≪ 4 + 0x8003 = 0x08003 .
Why this step? Standard CS×16+offset with CS = 0.
(b) length of a 64-bit-immediate MOV: REX.W prefix (1) + opcode (1) + 8-byte immediate (8) = 10 bytes.
Why this step? 10 ≤ 15 , the hard cap, so it's legal — but note it's five times longer than the 8086-era 2-byte register move, showing how immediates inflate length.
Answers: (a) next byte at physical 0x08003; (b) yes — 10 bytes ≤ 15-byte cap.
Verify: (a) 0x7FFE + 5 = 0x8003 and 0x08003 with CS 0 ✓; (b) 1 + 1 + 8 = 10 and 10 ≤ 15 ✓ (contrast RISC vs CISC , where every instruction is a fixed 4 bytes).
Recall Cover-the-matrix self-test
(Cell C) Give a second seg:off pair aliasing 0x30A0:0x0008.
(Cell D) What does 0xFFFF:0x0011 wrap to on a 20-bit bus?
(Cell F) RAX = 0xAAAAAAAAAAAAAAAA; after mov eax, 1, then mov ah, 2, then mov al, 3, what is RAX?
(Cell G) How many bytes total can one real-mode segment address?
Recall Answers
e.g. segment 0x30A0 - 1 = 0x309F, offset 0x0008 + 0x10 = 0x0018 → 0x309F:0x0018 (both give 0x309A8).
0xFFFF0 + 0x11 = 0x100001 → wraps to 0x00001.
mov eax, 1 zero-extends → 0x0000000000000001; mov ah, 2 merges byte 1 → 0x0000000000000201; mov al, 3 merges byte 0 → 0x0000000000000203.
0x10000 = 65536 bytes.
Cross-links: Instruction Set Architecture (ISA) · Registers and the Register File · Fetch-Decode-Execute Cycle · ARM Architecture · parent x86 overview .