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.
One solution that I most commonly use is to actually use the GNU linker (
ld
) to build the IDT and GDT for me. This answer isn't a primer on writing GNU linker scripts, but it does make use of theBYTE
,SHORT
, andLONG
linker script directives to build the IDT, the GDT, the IDT record, and the GDT record. The linker can use expressions involving<<
,>>
,&
,|
etc, and do these on the virtual memory addresses (VMA) of symbols it ultimately resolves.The problem is that the linker scripts are rather dumb. They don't have a macro language so you'd end up having to write the IDT and GDT entries like this:
exc0
,exc1
,exc2
, andexc3
are the exception functions defined and exported from an object file. You can see the IDT entries are usingCODE32_SEL
for the code segment. The linker is told to compute the selector numbers when building the GDT. Obviously this is very messy and becomes more unwieldy as the GDT and most especially the IDT grow.You could use a macro processor like
m4
to simplify things, but I prefer to use the C preprocessor (cpp
) as it is familiar to a lot more developers. Although the C pre-processor is usually used to pre-process C/C++ files, it isn't limited to those files. You can use it on any kind of text file including linker scripts.You can create a macro file and define a couple macros like
MAKE_IDT_DESC
andMAKE_GDT_DESC
to create GDT and IDT descriptor entries. I use an extension naming convention whereldh
stands for (Linker Header), but you can name these files whatever you wish:macros.ldh:
To cut down on the clutter in the main linker script you can create another header file that builds the GDT and IDT (and associated records):
gdtidt.ldh
Now you just have to include
gdtidt.ldh
in the linker script at a point (inside a section) that you'd like to place the structures:link.ld.pp:
This linker script is a typical one I use for boot sectors, but all I've done is include the
gdtidt.ldh
file to allow the linker to generate the structures. The only thing left to do is pre-process thelink.ld.pp
file. I use the.pp
extension for pre-processor files but you could use any extension. To createlink.ld
fromlink.ld.pp
you can use the command:The resulting
link.ld
file that gets generated will look like:With slight modifications to the sample
boot.asm
file in the question we end up with:boot.asm:
The new
boot.asm
also creates a TSS table (tss_entry
) which is used in the linker script to build the GDT entry associated with that TSS.To pre-process the linker script; assemble; link; and generate a binary file that works as a boot sector, the following commands can be used:
To run the
boot.bin
floppy disk image in QEMU you can use the command:To run it with BOCHS you can use the command:
The code does these things:
lgdt
instruction.lidt
.ltr
.exc0
,exc1
,exc2
, andexc3
).If it runs correctly in BOCHS the output should look like: