Search code examples
exceptionassemblyx86cpu-architectureosdev

When #GP is raised from v8086 mode does the processor push an error code on the ring0 stack?


More broadly the question really is - when an exception is generated in v8086 mode that is propagated to a protected-mode interrupt/trap gate, does an error code get pushed onto the stack after the return address is pushed for those exceptions with an error code?

Say for instance I am running in V8086 mode (CPL=3, VM=1, PE=1) with an IOPL of 0. I would expect that the privileged instruction HLT should raise a #GP exception. NASM code could look something like:

bits 32

    xor ebx, ebx                ; EBX=0
    push ebx                    ; Real mode GS=0
    push ebx                    ; Real mode FS=0
    push ebx                    ; Real mode DS=0
    push ebx                    ; Real mode ES=0
    push V86_STACK_SEG
    push V86_STACK_OFS          ; v8086 stack SS:SP (grows down from SS:SP)
    push dword 1<<EFLAGS_VM_BIT | 1<<EFLAGS_BIT1
                                ; Set VM Bit, IF bit is off, DF=0(forward direction),
                                ; IOPL=0, Reserved bit (bit 1) always 1. Everything
                                ; else 0. These flags will be loaded in the v8086 mode
                                ; during the IRET. We don't want interrupts enabled
                                ; because we don't have a proper v86 monitor
                                ; GPF handler to process them.
    push V86_CS_SEG             ; Real Mode CS (segment)
    push v86_mode_entry         ; Entry point (offset)
    iret                        ; Transfer control to v8086 mode and our real mode code

bits 16    
v86_mode_entry:
    hlt                         ; This should raise a #GP exception

When the protected-mode #GP exception handler starts running I want to know if an error code is pushed on the stack after CS:EIP.

One may say RTFM but the Intel documentation is the source of confusion.


Reason for the Question

Intel documents the exceptions and error codes in the Intel® 64 and IA-32 Architectures Software Developer’s Manual Vol 3A Table 6-2:

enter image description here

From the table #DF, #TS, #NP, #SS, #GP, #PF, and #AC have error codes. Intel documents that in real-address mode error codes are not pushed on the stack, but it seems to be suggested that in all other legacy modes (16/32-bit protected mode and v8086 mode) and long mode (64-bit and 16/32-bit compatibility modes) that an error code is pushed.

In Volume 2A in the instruction set reference for INT n/INTO/INT3/INT1—Call to Interrupt Procedure it says in the pseudo-code of those instructions the state REAL_ADDRESS_MODE has these items pushed:

Push(CS);
Push(IP);
(* No error codes are pushed in real-address mode*)
CS ← IDT(Descriptor (vector_number « 2), selector));
EIP ← IDT(Descriptor (vector_number « 2), offset)); (* 16 bit offset AND 0000FFFFH *)

Intel has gone out of their way to make it quite clear in real-address mode - error codes don't apply.

The Instruction Set Reference for the INT n/INTO/INT3/INT1—Call to Interrupt Procedure the pseudo-code defines the mechanics of INTER-PRIVILEGE-LEVEL-INTERRUPT or INTRA-PRIVILEGE-LEVEL-INTERRUPT states. Although the gate size (16/32/64-bit) determines the width of the data (including the width of the error code) the error code is pushed (if applicable) and is documented specifically with:

Push(ErrorCode); (* If needed, #-bytes *)

Where # is 2 (16-bit gate), 4 (32-bit gate), or 8 (64-bit gate).

The exception: The one place where an error code isn't documented as being pushed is in the state INTERRUPT-FROM-VIRTUAL-8086-MODE. A snippet of the relevant pseudo-code:

IF IDT gate is 32-bit
    THEN
        IF new stack does not have room for 40 bytes (error code pushed)
        or 36 bytes (no error code pushed)
            THEN #SS(error_code(NewSS,0,EXT)); FI;
            (* idt operand to error_code is 0 because selector is used *)
    ELSE (* IDT gate is 16-bit)
        IF new stack does not have room for 20 bytes (error code pushed)
        or 18 bytes (no error code pushed)
            THEN #SS(error_code(NewSS,0,EXT)); FI;
            (* idt operand to error_code is 0 because selector is used *)
FI;
IF instruction pointer from IDT gate is not within new code-segment limits
    THEN #GP(EXT); FI; (* Error code contains NULL selector *)
tempEFLAGS ← EFLAGS;
VM ← 0;
TF ← 0;
RF ← 0;
NT ← 0;
IF service through interrupt gate
    THEN IF = 0; FI;
TempSS ← SS;
TempESP ← ESP;
SS ← NewSS;
ESP ← NewESP;
(* Following pushes are 16 bits for 16-bit IDT gates and 32 bits for 32-bit IDT gates;
Segment selector pushes in 32-bit mode are padded to two words *)
Push(GS);
Push(FS);
Push(DS);
Push(ES);
Push(TempSS);
Push(TempESP);
Push(TempEFlags);
Push(CS);
Push(EIP);
GS ← 0; (* Segment registers made NULL, invalid for use in protected mode *)
FS ← 0;
DS ← 0;
ES ← 0;
CS ← Gate(CS); (* Segment descriptor information also loaded *)
CS(RPL) ← 0;
CPL ← 0;
IF IDT gate is 32-bit
    THEN
        EIP ← Gate(instruction pointer);
    ELSE (* IDT gate is 16-bit *)
        EIP ← Gate(instruction pointer) AND 0000FFFFH;
FI;
(* Start execution of new routine in Protected Mode *)

What is notably absent is any mention of the error code after Push(EIP); and before starting execution in protected mode. Of interest is that a check is done for enough stack space for the case of an error code and no error code. With a 32-bit interrupt/trap gate the size is either 40 with an error code or 36 without. This is the reason for the question1.


Footnotes

  • 1I had never paid close attention to the newer Intel documentation over the years and was unaware what the documentation was saying with regards to v8086 mode. My v8086 monitors and protected mode interrupt handlers have always been written to take into account exceptions with error codes and those without. I didn't notice the problems in the documentation until this past week when someone approached me about a discussion where this was mentioned in passing (but not explained).

Solution

  • TL;DR: The pseudo-code in the Intel instruction set reference is incorrect. If an exception in v8086 mode causes a protected mode call/interrupt gate to execute an exception handler then an error code will be pushed if the exception is one of those with an error code. #GP has an error code and it will be pushed on the ring 0 stack before transferring control to your #GP handler. You must manually remove it prior to doing an IRET.


    The answer is that an exception in Virtual 8086 mode (v8086 or v86) that is processed by a protected mode handler (through an interrupt or trap gate) will have the error code pushed for those exceptions that use one (including #GP). The pseudo-code should have been:

    Push(CS);
    Push(EIP);
    Push(ErrorCode); (* If needed *)
    

    In Intel® 64 and IA-32 Architectures Software Developer’s Manual Vol 1 in section 6.4.1 Call and Return Operation for Interrupt or Exception Handling Procedures documents inter (a privilege level change) and intra (privilege level remains the same) transitions as having this rule applied:

    Pushes an error code on the new stack (if appropriate).

    IMHO it would have probably been worded better as:

    Pushes an error code on the new stack (if applicable to the exception).

    v8086 mode is a special mode of protected-mode running at Privilege Level 3. These rules still apply since exceptions transition the processor from ring 3 to ring 0 (inter privilege level change) to handle interrupts via an interrupt/trap gate.


    Related Real-Address Mode Documentation Inconsistencies

    On the original 8086 processors the only exceptions were 0 through 4 (inclusive). That included #DE, #DB, NMI interrupt, #BP, and #OF. The rest were documented as reserved1 by Intel up to and including exception 31. None of the exceptions on the 8086 had error codes so this was never an issue. This changed on the 286 and later processors where exceptions with error codes were introduced.

    In Intel® 64 and IA-32 Architectures Software Developer’s Manual Vol 1 section 6.4.3, Intel says this about Real-address mode on later processors (286+)

    6.4.3 Interrupt and Exception Handling in Real-Address Mode

    When operating in real-address mode, the processor responds to an interrupt or exception with an implicit far call to an interrupt or exception handler. The processor uses the interrupt or exception vector as an index into an interrupt table. The interrupt table contains instruction pointers to the interrupt and exception handler procedures.

    The processor saves the state of the EFLAGS register, the EIP register, the CS register, and an optional error code on the stack before switching to the handler procedure.

    A return from the interrupt or exception handler is carried out with the IRET instruction.

    See Chapter 20, “8086 Emulation,” in the Intel® 64 and IA-32 Architectures Software Developer’s Manual, Volume 3B, for more information on handling interrupts and exceptions in real-address mode.

    I've emphasized the important part where the documentation claims that "an optional error code" is pushed. This is in fact not true. An error code is not pushed in real-address mode for exceptions that would normally have one pushed in other operating modes. This section does say to see Chapter 20, “8086 Emulation” of Volume 3B. In Chapter 20 we find Section 20.1.4 Interrupt and Exception Handling says this:

    The processor performs the following actions to make an implicit call to the selected handler:

    1. Pushes the current values of the CS and EIP registers onto the stack. (Only the 16 least-significant bits of the EIP register are pushed.)
    2. Pushes the low-order 16 bits of the EFLAGS register onto the stack.
    3. Clears the IF flag in the EFLAGS register to disable interrupts.
    4. Clears the TF, RF, and AC flags, in the EFLAGS register. Vol. 3B 20-5 8086 EMULATION
    5. Transfers program control to the location specified in the interrupt vector table. An IRET instruction at the end of the handler procedure reverses these steps to return program control to the interrupted program. Exceptions do not return error codes in real-address mode.

    This part of the documentation is correct. The 5 steps do not include pushing an error code. This is consistent with the pseudo-code in the instruction set reference for INT n/INTO/INT3/INT1—Call to Interrupt Procedure which has this documented for the state REAL_ADDRESS_MODE:

    Push(CS);
    Push(IP);
    (* No error codes are pushed in real-address mode*)
    CS ← IDT(Descriptor (vector_number « 2), selector));
    EIP ← IDT(Descriptor (vector_number « 2), offset)); (* 16 bit offset AND 0000FFFFH *)
    

    Footnotes

    • 1Although Intel reserved the unused exceptions up to interrupt 32 on the original 8086, IBM made a poor design decision mapping the external interrupt handlers of its PIC (interrupt controller) to interrupt 8 through 15 (inclusive) and placed BIOS calls in the reserved space as well. This caused problems on the IBM systems with a 286+ processor where the master PICs external interrupts overlapped with the exceptions that Intel added. For instance #GP and IRQ5 share the same interrupt number 13 (0x0d) in real-address mode.

      16-bit and 32-bit protected mode OSes generally move the master PICs base address from interrupt 8 to a location greater than interrupt 31 outside the reserved interrupts to avoid this problem.