George Claghorn

Discovering the D/B flag

When a Multiboot 2-compliant boot loader like GRUB invokes a 32-bit x86 operating system, it provides a magic number in the EAX register (0x36d76289) and the physical address of a boot information table in EBX.

These values will eventually be useful to Georgix, so the first thing its bootstrap code does is push them onto the stack (in reverse order, so they can be popped off in order):

_start:
    # Set up the stack.
    movl $boot.stack.high, %esp

    # The bootloader provides:
    #
    # * A magic number in EAX
    # * The physical address of the boot information record in EBX
    #
    # Save these values on the stack to pass to the Rust entrypoint later.
    pushl %ebx
    pushl %eax

After a bit of setup, the bootstrap code jumps to the Rust entrypoint, passing the magic number and boot information table pointer as arguments. Per the C calling convention, the first two arguments are passed in the EDI and ESI registers:

popl %edi
popl %esi
ljmp $boot.global_descriptor_table.code, $main

The bootstrap code didn’t always work this way. Until recently, it saved the magic number and boot information table pointer in EDI and ESI rather than on the stack. I wanted to use the stack from the start—to clarify that these values aren’t needed for setup, and to free up EDI and ESI for other purposes—but ran into some trouble.

Part of the setup that the bootstrap code performs before jumping into Rust is loading a Global Descriptor Table and populating the segment selector registers (including SS, the stack segment selector). The GDT contains a code segment with its L bit set. Georgix enters 64-bit long mode by long-jumping into this code segment, as shown above.

The trouble I encountered was that I could save and restore values on the stack entirely before or entirely after populating SS, but I couldn’t save a value before populating SS and restore it after:

# This works (EAX contains 0xfeedface at the hlt):
pushl $0xfeedface
popl %eax
hlt
movw $boot.global_descriptor_table.data, %ss

# This also works (EAX contains 0xfeedface at the hlt):
movw $boot.global_descriptor_table.data, %ss
push $0xfeedface
popl %eax
hlt

# This doesn't work (EAX contains 0 at the hlt):
push $0xfeedface
movw $boot.global_descriptor_table.data, %ss
popl %eax
hlt

I initially put off learning too much about segmentation because it’s more or less unused in 64-bit mode, where Georgix does most of its work. But it started to bother me that I didn’t understand what was wrong here.

The AMD64 system programming manual has my back. A data segment descriptor contains a flag called D/B, and it affects the behavior of stack instructions (emphasis mine):

Bit 22 of the upper doubleword. For expand-down data segments (E=1), setting D=1 sets the upper bound of the segment at 0_FFFF_FFFFh. Clearing D=0 sets the upper bound of the segment at 0_FFFFh.

In the case where a data segment is referenced by the stack selector (SS), the D bit is referred to as the B bit. For stack segments, the B bit sets the default stack size. Setting B=1 establishes a 32-bit stack referenced by the 32-bit ESP register. Clearing B=0 establishes a 16-bit stack referenced by the 16-bit SP register.

Descriptions of the PUSH and POP instructions reiterate that they’re affected by the stack segment’s D/B flag. For example, for POP:

The address-size attribute of the stack segment determines the stack pointer size (16 bits or 32 bits-the source address size)… (The B flag in the stack segment’s segment descriptor determines the stack’s address-size attribute…)

The data segment descriptor in Georgix’s boot GDT had the D/B flag cleared. Selecting it with SS caused PUSH and POP to operate on SP rather than ESP. To fix, I needed only to set the data segment descriptor’s D/B flag (number 54):

  .equ boot.global_descriptor_table.data, . - boot.global_descriptor_table
-     .quad (1 << 55) | (0xF << 48) | (1 << 47) | (1 << 44) | (1 << 41) | 0xFFFF
+     .quad (1 << 55) | (1 << 54) | (0xF << 48) | (1 << 47) | (1 << 44) | (1 << 41) | 0xFFFF

This causes PUSH and POP to operate on ESP after this segment is selected in SS—at least until 64-bit mode is activated. Thereafter, they operate on RSP unconditionally.