This question is inspired by a problem many have encountered over the years, especially in x86 operating system development. Recently a related NASM question was bumped up by an edit. In that case the person was using NASM and was getting the assemble time error:
shift operator may only be applied to scalar values
Another related question asks about a problem with GCC code when generating a static IDT at compile time that resulted in the error:
initializer element is not constant
In both cases the issue is related to the fact that an IDT entry requires an address to an exception handler and a GDT may need a base address to another structure like a Task Segment Structure (TSS). Normally this isn't an issue because the linking process can resolve these addresses through relocation fixups. In the case of an IDT entry or GDT Entry, the fields split up the base/function addresses. There are no relocation types that can tell a linker to shift bits around and then place them in memory the way they are laid out in a GDT/IDT entry. Peter Cordes has written a good explanation of that in this answer.
My question is not asking what the issue is, but a request for functional, and practical solutions to the problem. Although I am self-answering this, it is only one of many possible solutions. I only ask that solutions proposed meet these requirements:
- The GDT and IDT should not have their addresses fixed to a specific physical or linear address.
- At a minimum the solution should be able to work with ELF objects and ELF executables. If it work for other formats, even better!
- It doesn't matter if a solution is part of the process of building a final executable/binary or not. If a solution requires build time processing after the executable/binary is generated that is also acceptable.
- The GDT (or IDT) needs to appear as fully resolved when loaded in memory. Solutions must not require run-time fixups.
Sample Code that Doesn't Work
I'm providing some sample code in the form of a legacy bootloader1 that tries to create a static IDT and GDT at assembly time but fails with these errors when assembled with nasm -f elf32 -o boot.o boot.asm
:
boot.asm:78: error: `&' operator may only be applied to scalar values boot.asm:78: error: `&' operator may only be applied to scalar values boot.asm:79: error: `&' operator may only be applied to scalar values boot.asm:79: error: `&' operator may only be applied to scalar values boot.asm:80: error: `&' operator may only be applied to scalar values boot.asm:80: error: `&' operator may only be applied to scalar values boot.asm:81: error: `&' operator may only be applied to scalar values boot.asm:81: error: `&' operator may only be applied to scalar values
The code is:
macros.inc
; Macro to build a GDT descriptor entry
%define MAKE_GDT_DESC(base, limit, access, flags) \
(((base & 0x00FFFFFF) << 16) | \
((base & 0xFF000000) << 32) | \
(limit & 0x0000FFFF) | \
((limit & 0x000F0000) << 32) | \
((access & 0xFF) << 40) | \
((flags & 0x0F) << 52))
; Macro to build a IDT descriptor entry
%define MAKE_IDT_DESC(offset, selector, access) \
((offset & 0x0000FFFF) | \
((offset & 0xFFFF0000) << 32) | \
((selector & 0x0000FFFF) << 16) | \
((access & 0xFF) << 40))
boot.asm:
%include "macros.inc"
PM_MODE_STACK EQU 0x10000
global _start
bits 16
_start:
xor ax, ax
mov ds, ax
mov es, ax
mov ss, ax
mov sp, ax ; Stack grows down from physical address 0x00010000
; SS:SP = 0x0000:0x0000 wraps to top of 64KiB segment
cli
cld
lgdt [gdtr] ; Load our GDT
mov eax, cr0
or eax, 1
mov cr0, eax ; Set protected mode flag
jmp CODE32_SEL:start32 ; FAR JMP to set CS
bits 32
start32:
mov ax, DATA32_SEL ; Setup the segment registers with data selector
mov ds, ax
mov es, ax
mov ss, ax
mov esp, PM_MODE_STACK ; Set protected mode stack pointer
mov fs, ax ; Not currently using FS and GS
mov gs, ax
lidt [idtr] ; Load our IDT
; Test the first 4 exception handlers
int 0
int 1
int 2
int 3
.loop:
hlt
jmp .loop
exc0:
iret
exc1:
iret
exc2:
iret
exc3:
iret
align 4
gdt:
dq MAKE_GDT_DESC(0, 0, 0, 0) ; null descriptor
.code32:
dq MAKE_GDT_DESC(0, 0x000fffff, 10011010b, 1100b)
; 32-bit code, 4kb gran, limit 0xffffffff bytes, base=0
.data32:
dq MAKE_GDT_DESC(0, 0x000fffff, 10010010b, 1100b)
; 32-bit data, 4kb gran, limit 0xffffffff bytes, base=0
.end:
CODE32_SEL equ gdt.code32 - gdt
DATA32_SEL equ gdt.data32 - gdt
align 4
gdtr:
dw gdt.end - gdt - 1 ; limit (Size of GDT - 1)
dd gdt ; base of GDT
align 4
; Create an IDT which handles the first 4 exceptions
idt:
dq MAKE_IDT_DESC(exc0, CODE32_SEL, 10001110b)
dq MAKE_IDT_DESC(exc1, CODE32_SEL, 10001110b)
dq MAKE_IDT_DESC(exc2, CODE32_SEL, 10001110b)
dq MAKE_IDT_DESC(exc3, CODE32_SEL, 10001110b)
.end:
align 4
idtr:
dw idt.end - idt - 1 ; limit (Size of IDT - 1)
dd idt ; base of IDT
Footnotes
1I chose a bootloader as an example since a Minimal Complete Verifiable Example was easier to produce. Although the code is in a bootloader, similar code is usually written as part of a kernel or other non-bootloader code. The code may often be written in languages other than assembly, like C/C++ etc.
Because a legacy bootloader is always loaded by the BIOS at physical address 0x7c00, there are other specific solutions for this case that can be done at assembly time. Such specific solutions break the more general use cases in OS development where a developer usually doesn't want to hard code the IDT or GDT addresses to specific linear/physical addresses, as it is preferable to let the linker do that for them.