The segment limit in real mode is 64K, even on a 386 or later CPU where you can use 32-bit address-size via prefixes. e.g. mov ax, [edx + ecx*4]
is still limited to offsets of 64 KiB in real mode.
If you exceed this limit, it raises a #GP exception on 286+. (Or #SS
if the segment was SS).
8086 didn't have #SS or #GP exceptions, it had no protection general or otherwise, just using Sreg << 4
added to the offset to form a linear address.
16-bit address-size can exceed the 64K segment limit via a word or wider access at seg:FFFF
. On 8086, the higher byte comes from seg:0000
(wrapping of the offset in the logical address before computing a new linear address for the 2nd memory bus transaction, not accessing outside the 64K linear range of the segment).
On 286 and later, #GP
or #SS
for both data and instructions in this case as well. https://www.os2museum.com/wp/does-eip-wrap-around-in-16-bit-segments/
In general, addressing modes like [bx + si + 1]
wrap at 16 bits. (And push word
with SP=0 wraps to SP=FFFEh, no problem as long as the stack is aligned). So only code using the 0x67
address-size prefix (added in 386) for addressing modes like [eax]
can exceed segment limits in real mode, except for word or wider accesses at the very end of a segment.
Segments that start within 64K of the highest possible address wrap around at 1MiB on 8086, and on later CPUs if A20 is disabled. Otherwise they extend past 1MiB for an address like FFFF:FFFF
seg:off = 0x10ffef
linear. See What are Segments and how can they be addressed in 8086 mode?
Unreal mode: flat memory model for 386 real mode
If you switch to protected mode and set a segment register, the CPU keeps the segment description (base + limit) cached internally, even across switching back to 16-bit real mode. This situation is called unreal mode.
Writing to a segment register in 16-bit mode only sets the segment base to value << 4
without changing the limit, so unreal mode is somewhat durable for segments other than CS. CS:EIP is special, especially if you need to avoid truncating EIP to 16 bits when returning from interrupts or whatever. See the osdev wiki linked earlier.
push
/pop
/call
/ret
use SS:ESP
or SS:SP
according to the B
flag in the current stack-segment descriptor; the address-size prefix only affects stuff like push word [eax]
vs. push word [si]
.
The GDT / LDT are ignored when you write a value to a segment register in real mode. The value is used directly to set the cached segment base, not as a selector at all.
(Each segment is separate; unreal mode isn't an actual mode like protected vs. real; the CPU is in real mode. Writing the FS register, for example, puts that segment back into normal real-mode behaviour except for its limit, but doesn't change the others. It's just a name for being in real mode with cached segment descriptors with larger limits, so you can use 32-bit address-size for a larger flat address space. Often with base=0 and limit=4G)
AFAIK, there's no way to query the internal limit value of a segment in real mode. lsl
loads the segment-limit value directly from a descriptor in the GDT / LDT in memory, not from the internal value (so it's not what you want), and it's not available in real mode anyway.
See comments on this answer for more details about taking segments out of unreal mode intentionally or unintentionally.
286 and 386 CPUs supported a LOADALL
instruction which could set segment limits from real mode, but later CPUs don't have it. Commenters say that SMM (system management mode) may be able to do something similar on modern x86.