0

During the development of a small kernel, I stumbled across a strange problem when booting up application processors using the APIC.

As stated on OSDev and the Intel-Manual, the processor first enters Real-Mode, and my goal is to get it to operate in Protected-Mode. After setting up a small stack and enabling the "A20"-Line and far-jumping to my 32-bit code, i tried to clear eax using xor eax, eax for sanity purposes.

To my suprise, only the lower word of eax got cleared, but the high word remained unchanged.

Running the kernel in QEMU

Funnily, if I simply do an xor ax, ax, instead of xor eax, eax, the register is cleared entirely.

Below is my code for bootstrapping an application processor using the APIC:


; Extern reference to the GDTR
section .data
extern g_gdtr

; Serves as a temporary stack used for flushing the cpu-pipeline
SMP_BOOT_STACK_SIZE equ 64
smp_boot_stack:
    dq 0
    dq 0
    dq 0
    dq 0

section .text
global __smp_ap_rm_entry
align 0x1000
[bits 16]
; Real-Mode Entry point for other processor cores (AP's)
; after a INIT-SIPI-SIPI was issued
__smp_ap_rm_entry:
    cli
    xor ax, ax
    mov ds, ax
    mov ss, ax
    lea bp, [ds:smp_boot_stack + SMP_BOOT_STACK_SIZE - 1]
    mov sp, bp
    
    ; Enable A20 line
    sti
    in al, 0x92
    or al, 2
    out 0x92, al
    cli

    lgdt [ds:g_gdtr]

    ; Enable Protected Mode
    mov eax, cr0
    or eax, 0x1
    mov cr0, eax

    ; Far jump
    push 0x8
    push __smp_ap_pm_entry
    retf

align 4
[bits 32]
__smp_ap_pm_entry:
    mov ax, word 0x10
    ; Doing this two times is somehow necessary (wtf?)
    mov es, ax
    mov es, ax 

    mov ss, ax
    mov fs, ax
    mov gs, ax
    mov ds, ax

    xor eax, eax
    int 3 ; To check for changed values in qemu

    jmp $

Moreover, I have also tried to assign a 32-bit value into a register, e.g. mov eax, 0xDEADBEEF, but only the BEEF part remains.

Does anyone have any idea why this is not working?

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
Dalex
  • 61
  • 1
  • 5
  • 2
    Double check that your `bits 32` is effective and the machine code is correct. `xor ax, ax` working instead of `xor eax, eax` usually means your assembler emitted code for 16 bit. – Jester Aug 09 '21 at 00:03
  • If you're in 32-bit mode, why does the CS descriptor show up as ```CS64```? – sj95126 Aug 09 '21 at 00:20
  • @sj95126 I don't think I understand your answer correctly. Where did you encounter this? – Dalex Aug 09 '21 at 00:33
  • 2
    In the QEMU output image you posted, the line starting with ```CS =0008```. A 32-bit code segment should be shown as ```CS32``` not ```CS64```. Are you sure your GDT is correct? – sj95126 Aug 09 '21 at 00:35
  • @sj95126 Thank very much you for the hint, this was indeed the problem all along! – Dalex Aug 09 '21 at 01:13

2 Answers2

2

As @sj95126 hinted, it seemed like I had loaded the wrong GDT. After creating and loading a GDT with 32-bit Segment-Descriptors, the problem was resolved.

If anyone is interested, below is the code for my new GDT used for switching from Real-Mode to Protected-Mode:

struct SegmentDescriptor32_s
{
    u16 SegmentLimitLow;
    u16 BaseAddressLow;
    union
    {
        struct
        {
            u32 BaseAddressMiddle : 8;
            u32 Type : 4;
            u32 DescriptorType : 1;
            u32 DescriptorPrivilegeLevel : 2;
            u32 Present : 1;
            u32 SegmentLimitHigh : 4;
            u32 System : 1;
            u32 LongMode : 1;
            u32 DefaultBig : 1;
            u32 Granularity : 1;
            u32 BaseAddressHigh : 8;
        };
        u32 Flags;
    };
} __attribute__((packed));

__attribute__((aligned(0x1000)))
static struct SegmentDescriptor32_s smp_ap_gdt[] =
{
    { /* Null-Selector */
        .SegmentLimitLow = 0x0,
        .BaseAddressLow = 0x0,
        .Flags = 0x0
    },
    { /* Flat Code */
        .SegmentLimitLow = 0xFFFF,
        .BaseAddressLow = 0x0000,
        .Flags = 0x00CF9A00
    },
    { /* Flat Data */
        .SegmentLimitLow = 0xFFFF,
        .BaseAddressLow = 0x0000,
        .Flags = 0x008F9200,
    },
    { /* TSS */
        .SegmentLimitLow = 0x68,
        .BaseAddressLow = 0x0000,
        .Flags = 0x00CF8900
    }
};
Dalex
  • 61
  • 1
  • 5
1
; Enable Protected Mode
mov eax, cr0
or eax, 0x1
mov cr0, eax

; Far jump
push 0x8
push __smp_ap_pm_entry
retf

When the architecture manual says a MOV CR0 that alters the PE bit must be immediately followed by a FAR JMP, they mean the specific instruction FAR JMP (opcode EA or FF, depending on how you want to express the operand), and they mean it must be the very next instruction. If you don't do this the effects are unpredictable. I suspect your emulator doesn't actually flip the switch until it executes a FAR JMP, so you're still in 16-bit real mode and the machine instruction 31 c0 still means xor ax,ax rather than xor eax, eax.

FAR JMP takes an explicit, absolute segment:offset expression; you can't just write jmp far __smp_ap_pm_entry. I don't know what exactly you will need to write.

(See section 9.9, "Mode Switching", of the Intel® 64 and IA-32 Architectures Software Developer's Manual Combined Volumes 3A, 3B, 3C, and 3D: System Programming Guide. If you haven't already read this manual, now would be an excellent time.)

zwol
  • 135,547
  • 38
  • 252
  • 361
  • 1
    The PE bit gets flipped immediately on the mov instruction. After that is complete you are in 16-bit protected mode. After the JMP (or RETF) you are in 32-bit protected mode(if the GDT has a 32-bit CS for that selector). The results of the instructions after flipping the bit with mov are defined and predictable. You are in 16-bit protected mode after the mov but without an explicit change in CS. It is not required to follow changing the PE bit by a FAR JMP or equivalent right away if the instructions after the `mov` are ones that are decoded the same way in real mode and 16-bit protected mode. – Michael Petch Aug 09 '21 at 10:46
  • The issue with decoding is of course the instruction prefetch queue isn't aware that the PE bit has changed and the `mov` itself doesn't flush it. So the architecture manual suggests the following instruction be something (like a JMP) that flushes the instruction prefetch queue. It doesn't have to be a FAR JMP either. Any instruction that clears the IPFQ will suffice including but not limited to near and far JMPs and CALLs. – Michael Petch Aug 09 '21 at 10:57
  • @MichaelPetch I'm literally quoting the architecture manual here! "_Immediately_ after [the MOV CR0], execute a FAR JMP or FAR CALL...Random failures may occur if _other instructions exist_ between the two" (emphasis mine) It does license lcall as well as ljmp but that's it. – zwol Aug 09 '21 at 12:38
  • 1
    You may note in the same architecture manual a code listing that they use something else than a far jmp.as I don't have access to the architecture manual on my phone but there is a code listing for boot up ROM example with a protected mode switch. The primary problem resolves down to the IPFQ – Michael Petch Aug 09 '21 at 12:56