The Well-Factored 386

Rick A. Hohensee
2003-07-28T08:28:08Z

The four actual main modes of the 386

Operation of the INTeL 80386 is clearly much more flexible than currently
used, as far as the basic states the machine can run in. "Real Mode" and
"Protected Mode" are two possibilities out of several that are concievably
useful. It can be seen from things that are possible in "Real Mode", such
as "Unreal Mode", use of 386-era instructions and so on, that the
canonical modes are not monolithic entities, but are cases of various
combinations of discrete state switches. There is no 8086 in a 386.
Canonical Wake-up Real Mode, which I will refer to as CWR Mode, seems to
be how these several state switches are set at machine reset, but are
semantically just settings of independant things, and clearly CWR Mode can
be set up from individual settings available to the user. This is also
much more evident from a careful traversal of the detailed instruction
specs in the INTeL Programmer's Reference (386INTEL.TXT) for the 80386.

I'd say the two most global switches in the machine are the PE bit,
Protection Enabled, quite sensibly placed at bit 0 in control register 0,
and the D bit of the active code segment descriptor affiliated with CS,
the Default Bit. Two bits gives four possible states. All four are capable
of being the default mode of a system. Two are used as such; CWR Mode and
Protected Mode. Here's a breakdown and some neologizing...

PE=0 Dbit=0 (un)Real Mode
PE=0 Dbit=1 Forreal Mode
PE=1 Dbit=0 Veal Mode (BKA "286 task")
PE=1 Dbit=1 Protected Mode

The documented modes are the outside cases in a binary-count listing,
where PE=Dbit. There's a very good reason for that. IRET is sensitive to
Dbit, whereas interrupts push a stack frame of operands sized in
accordance with PE. This is some of INTeL's* description of the action of
INT, which represents all interrupts in this regard, when PE=0...

IF PE = 0
THEN GOTO REAL-ADDRESS-MODE;

REAL-ADDRESS-MODE:
Push (FLAGS);
IF <! 0; (* Clear interrupt flag *)
TF <! 0; (* Clear trap flag *)
Push(CS);
Push(IP);
(* No error codes are pushed *)
CS <! IDT[Interrupt number * 4].selector;
IP <! IDT[Interrupt number * 4].offset;

That is, IP is pushed if PE=0, not EIP. FLAGS is also a dual (a "word" in
INTeLese, 16 bits). Thus if, for example, you are in Forreal Mode, you get
a 6-byte stack push of FLAGS, CS and then IP on an interrupt, and the
corresponding IRET will later pop 12 bytes for basically the same 3
registers, and the system streaks off to the big bitbucket in the sky.

HOW ever, IRET is only PARTIALLY sensitive to Dbit. What I didn't say a
moment ago is that IRET is really sensitive to the current effective code
size, of which the D bit is just one component. If we prefix the IRET in a
Forreal Mode interrupt handler with 0x66, the other-operand-size prefix,
it will pop duals in a use32 CS. The interrupt occuring with PE=0 pushed
duals. It works. The inside cases of Veal Mode and Forreal Mode simply
need thier IRETs un-default-sized. In Veal Mode you coerce IRET into using
quads in a use16 code segment, and in Forreal Mode the same prefix in a
use32 code segment will coerce the IRET pops to duals. In Veal Mode, where
PE=1 when an interrupt occurs, you also have the option of handling the
interrupt with a Dbit=1 segment for the pertinent interrupt handler, in
which case it can IRET back to Veal Mode with a naked IRET. I suppose you
can also segregate Veal Mode segments with thier descriptor bases and
limits. In all the quirkiness of the 386, the broad orthogonality of the
other-sizing prefixes is quite refreshing, and even works on stuff like
POPA.

This is from the blow-by-blow description of IRET...

IF OperandSize = 32 (* Instruction = IRETD *)
THEN EIP <! Pop();
ELSE (* Instruction = IRET *)
IP <! Pop();

Again, OperandSize is Dbit as effected by a possible prefix.

This is why INTeL only mentions Veal Mode as "286 tasks". Tasks don't
IRET. Most of what a 386 provides big facilities for, like tasks, can also
be done piecemeal, as would be the case running a system in straight
Forreal Mode.

Canonical Wake-up Real Mode and Unreal Mode are two flavors of the same
basic setting of PE and Dbit. The difference is in segment limits. This
points up a basic concept here. 8086-style segments are a lightweight
layer that may or may not be active, but 386 segments are always active.
PE=0 does not turn them off. PE=0 means you can't change them until you
turn PE back to 1. PE should be called "PCE, Protections Changes Enabled".

This means the four basic modes have a state diagram about like so...

_______________
| WAKE-UP |
|canonical rmode|
| PE=0 use16 |
|_______________|
A |set PE to 1
| |
set PE to 0| |
___|________V__
| Veal |
| Mode |
| PE=1 use16 |
|_______________|
A |far xfer
| | to use32 CS
far xfer to | |
use16 CS| |
___|_______V___
| Protected |
| Mode |
| PE=1 use32 |
|_______________|
A |set PE to 1
| |
set PE to 0| |
___|________V__
| Forreal Mode |
| Mode |
| PE=0 use32 |
|_______________|

Interesting. Forreal Mode is the furthest from Wake-up, and you can only
get to Forreal Mode from pmode. Forreal Mode is like a back room off of
pmode. You can interrupt between pmode and Veal Mode. Conversely, events
in rmode or Forreal Mode (PE=0) have to be handled in the mode they occur
in.

The above diagram does not account for different segment limits and bases
than those in effect in WakeUp Rmode, so e.g. unreal mode is not shown.
This chart also does not account for V86 Mode, which basically requires
386 task-switch constructs. The above modes and transitions do NOT need a
TSS, or paging. I suspect V86 could run off Veal Mode OR pmode.

Veal Mode is a protected mode. That means it's a quick switch to good old
use32 Protected Mode. It may afford some better code density, and thus
speed. Forreal Mode is unprotected, which means interrupts are handled
somewhat faster, which benefits realtime services. Either can be
incorporated into a multi-mode system. Forreal Mode also doesn't seem to
have any analogy at all in current use, other than the INTeL "flat model",
which is a simplified pmode, but that similarity is rather superficial.

For the 386 to do what it does, and particularly to have pulled it off in
the mid-eighties, things have to be fairly simple. You can figure that
whatever the 386 does, it's implemented as simply as possible. The modes
of the machine are composites of simple switches, and various effects,
such as loaded descriptors, tend to persist until changed. This has always
been the case with e.g. 8086 segment registers. In the 386, however,
8086-style "segments" are not alternates of 386 segments, they are a
superficiality on top of them. With that in mind, the states one goes
through simply getting from real mode to pmode start to make more sense.

The following (osimplay) code demonstrates the superficiality of 8086
"schmegments" vis-a-vis 386 segments, even when PE=0...

<from flat pmode>
. global/flipPE # Welcome to Forreal Mode
# cell still 4

= 4 to A # 32-bit schmegmented addressing demo
= A to ES # proving we got PE = to 0.
= 9348539 to A
= A to @ $((0xb8a00))
ES
= A to @ $((0xb8a00))

That code causes two identical attribute-glyph pairs to appear on the VGA
text screen 32 charcells displaced from each other. That is the 4 in ES,
times 16 as per 8086, divided by two since each charcell is 2 bytes. Plus
a flat 32-bit address to put it on the screen. Forreal Mode thus does
32-bit 8086 schmegmented address computation. What is this type of
addressing useful for? Precious little. In Forreal mode you've got 32 bits
to start with. The point is you have the big flat segments left over from
pmode. The 8086 address-math does prove we're not in pmode any more,
however. Otherwise the ES prefix of 4 would have GPFed.

Rick Hohensee
Precision Mojo Engineer

*
I've converted my 386INTEL.TXT to 7-bit ASCII-art, not to mention
op-source-dest and other unINTeLifications, and "<!" is the right-to-left
assignment operator in the instruction description code, helpfully
pronounced "becomes".

The appended hex/octal/listing appears to be handling INT 35 in Forreal
Mode, ala
otheroperandsize
dismiss

It also demos the 32-bit schmegmented addressing mentioned earlier, and it
keeps it's IDT in the VGA text buffer. The hex/octal input editor in
osimplay needed to type it in is Left As An Excercize To The Reader.

00000000 00... ALLOT 0x7C00
00007c00 270 28 02 = 552 to A
00007c03 271 01 00 = 1 to C
00007c06 31 322 XOR D with D
00007c08 8e 302 = D to ES
00007c0a 273 00 7c = 0x7c00 to B
00007c0d cd 13 submit 0x13
00007c0f fa nosurprises
00007c10 270 00 b8 = 0xb800 to A
00007c13 8e 330 = A to DS
00007c15 270 93 0d = 3475 to A
00007c18 89 006 2c 01 = A to @ 300
00007c1c 31 300 XOR A with A
00007c1e 8e 330 = A to DS
00007c20 0f 01 16 37 7d setGDT IGDTRpointer
00007c25 270 00 b8 = 0xb800 to A
00007c28 8e 330 = A to DS
00007c2a 270 94 0d = 3476 to A
00007c2d 89 006 30 01 = A to @ 304
00007c31 66 otheroperandsize
00007c32 31 300 XOR A with A
00007c34 100 1+ A
00007c35 0f 22 300 = A to CR0
00007c38 e9 00 00 jump hvfnvkfv
00007c3b (O) hvfnvkfv
00007c3b 89 006 34 01 = A to @ 308
00007c3f 89 006 35 01 = A to @ 309
00007c43 0f 20 300 = CR0 to A
00007c46 89 006 36 01 = A to @ 310
00007c4a 89 006 37 01 = A to @ 311
00007c4e ea 53 7c 10 00
00007c53 (O) u32
00007c53 270 18 00 00 00 = 0x18 to A
00007c58 8e 330 = A to DS
00007c5a 270 57 65 67 13 = 325543255 to A
00007c5f 89 005 d4 00 00 00 = A to @ 212
00007c65 89 005 d8 80 0b 00 = A to @ 753880
00007c6b 89 005 78 81 0b 00 = A to @ 754040
00007c71 89 005 18 82 0b 00 = A to @ 754200
00007c77 270 18 00 00 00 = 0x18 to A
00007c7c 8e 330 = A to DS
00007c7e 8e 300 = A to ES
00007c80 8e 320 = A to SS
00007c82 31 300 XOR A with A
00007c84 8e 340 = A to FS
00007c86 8e 350 = A to GS
00007c88 270 d1 00 00 00 = 0xd1 to A
00007c8d 272 64 00 00 00 = 0x64 to D
00007c92 ee send byte
00007c93 90 nop
00007c94 90 nop
00007c95 90 nop
00007c96 90 nop
00007c97 90 nop
00007c98 90 nop
00007c99 90 nop
00007c9a 90 nop
00007c9b 90 nop
00007c9c 90 nop
00007c9d 270 df 00 00 00 = 0xdf to A
00007ca2 272 60 00 00 00 = 0x60 to D
00007ca7 ee send byte
00007ca8 272 92 00 00 00 = 0x92 to D
00007cad ec recieve byte
00007cae 0d 02 00 00 00 OR 2 to A
00007cb3 ee send byte
00007cb4 (O) a20loop
00007cb4 270 55 55 aa aa = 0xaaaa5555 to A
00007cb9 89 005 d8 ff 1f 00 = A to @ 2097112
00007cbf 8b 035 d8 ff 1f 00 = @ 2097112 to B
00007cc5 39 303 -test A to B
00007cc7 0f 85 e7 ff ff ff when not zero a20loop
00007ccd 274 f0 ef 00 00 = 61424 to SP
00007cd2 31 377 XOR DI with DI
00007cd4 (O) perFMIDTvector
00007cd4 60 pushcore
00007cd5 270 aa 7d 00 00 = twitch32unP to A
00007cda e8 ae 00 00 00 call install32unP
00007cdf 61 pullcore
00007ce0 107 1+ DI
00007ce1 81 377 ff 00 00 00 -test 255 with DI
00007ce7 75 eb when not zero short perFMIDTvector
00007ce9 0f 01 1d bb 7d 00 00 setIDT IDT32unPpointer
00007cf0 31 333 XOR B with B
00007cf2 103 1+ B
00007cf3 0f 20 300 = CR0 to A
00007cf6 31 330 XOR B to A
00007cf8 0f 22 300 = A to CR0
00007cfb 270 04 00 00 00 = 4 to A
00007d00 8e 300 = A to ES
00007d02 270 bb a5 8e 00 = 9348539 to A
00007d07 89 005 00 8a 0b 00 = A to @ 756224
00007d0d 26 ES
00007d0e 89 005 00 8a 0b 00 = A to @ 756224
00007d14 66 otheroperandsize
00007d15 9c pushflags
00007d16 66 otheroperandsize
00007d17 68 00 00 push 0
00007d1a 66 otheroperandsize
00007d1b e8 8c 00 call twitch32unP
00007d1e (O) theloop
00007d1e 8b 005 88 88 0b 00 = @ 755848 to A
00007d24 100 1+ A
00007d25 89 005 88 88 0b 00 = A to @ 755848
00007d2b cd 23 submit 35
00007d2d 90 nop
00007d2e 90 nop
00007d2f 90 nop
00007d30 90 nop
00007d31 e9 e8 ff ff ff jump theloop
00007d36 f4 halt
00007d37 (O) IGDTRpointer
00007d37 00 04 3d 7d 00 00
00007d3d (O) GDT
00007d3d 00 00 00 00 00 00 00 00 0 the required NULL
00007d45 ff ff 00 00 00 89 cf 00 1 0x8 TSS
stackstack
00007d4d ff ff 00 00 00 9a cf 00 2 0x10 ring0 USE32
CS
00007d55 ff ff 00 00 00 92 cf 00 3 0x18 ring0 data
00007d5d ff ff 00 00 00 9a 00 00 4 0x20 USE16 CS
00007d65 ff ff 00 00 00 92 00 00 5 0x28 USE16 data
?S
00007d6d 00 00 00 00 00 00 00 00 6 0x30
00007d75 00 00 00 00 00 00 00 00 7 0x38
00007d7d 00 00 00 00 00 00 00 00 8 0x40
00007d85 00 00 00 00 00 00 00 00 9 0x48
00007d8d
00007d8d (O) install32unP
00007d8d c1 347 02 upshift 2 to DI
00007d90 81 307 00 80 0b 00 + IDT32unP to DI
00007d96 89 007 = A to @ DI
00007d98 c3 return
00007d99 (O) install16
00007d99 c1 347 02 upshift 2 to DI
00007d9c 81 307 00 84 0b 00 + IDT16 to DI
00007da2 e8 1a 00 00 00 call schmegment
00007da7 89 007 = A to @ DI
00007da9 c3 return
00007daa (O) twitch32unP
00007daa 120 push A
00007dab 8b 005 50 8a 0b 00 = @ 756304 to A
00007db1 100 1+ A
00007db2 89 005 50 8a 0b 00 = A to @ 756304
00007db8 130 pull A
00007db9 66 otheroperandsize
00007dba cf dismiss
00007dbb (O) IDT32unPpointer
00007dbb 00 04 00 80 0b 00
00007dc1 (O) schmegment
00007dc1 123 push B
00007dc2 89 303 = A to B
00007dc4 c1 353 10 downshift 16 to B
00007dc7 81 340 ff ff 00 00 AND 65535 to A
00007dcd c1 343 1c upshift 28 to B
00007dd0 09 330 OR B to A
00007dd2 133 pull B
00007dd3 c3 return