7

I'm working on a project that has tight boot time requirements. The targeted architecture is an IA-32 based processor running in 32 bit protected mode. One of the areas identified that can be improved is that the current system dynamically initializes the processor's IDT (interrupt descriptor table). Since we don't have any plug-and-play devices and the system is relatively static, I want to be able to use a statically built IDT.

However, this proving to be troublesome for the IA-32 arch since the 8 byte interrupt gate descriptors splits the ISR address. The low 16 bits of the ISR appear in the first 2 bytes of the descriptor, some other bits fill in the next 4 bytes, and then finally the last 16 bits of the ISR appear in the last 2 bytes.

I wanted to use a const array to define the IDT and then simply point the IDT register at it like so:

typedef struct s_myIdt {
    unsigned short isrLobits;
    unsigned short segSelector;
    unsigned short otherBits;
    unsigned short isrHibits;
} myIdtStruct;

myIdtStruct myIdt[256] = {
    { (unsigned short)myIsr0, 1, 2, (unsigned short)(myIsr0 >> 16)},
    { (unsigned short)myIsr1, 1, 2, (unsigned short)(myIsr1 >> 16)},

etc.

Obviously this won't work as it is illegal to do this in C because myIsr is not constant. Its value is resolved by the linker (which can do only a limited amount of math) and not by the compiler.

Any recommendations or other ideas on how to do this?

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
jkayca
  • 241
  • 3
  • 5
  • My suggestion would be to make sure that your IDT and ISRs are in the same module (and obviously that the ISR is loaded in a fixed position) and then use labels. I tried doing this with GCC but it didn't like using the `&&myIsr0` syntax outside of a function and I don't have the inline assembly skills to declare the IDT using `__asm__` syntax. I'd probably just compile this one module using NASM (personal preference) with the ISRs being stub calls to C functions. That would be my suggestion, although I definitely can't claim to be an expert :) – Justin Oct 12 '12 at 16:34
  • Here's a kind of annoying suggestion that I'd give: use assembly. When using very specific processor features or extremely low level constructs, call into some assembly. It makes it easier. – Linuxios Oct 13 '12 at 00:06
  • This is not my forte; can someone briefly explain or point me to to why this is illegal? – taz Oct 13 '12 at 00:55
  • 1
    The problem is not C vs. assembly. The format of x86 IDT entries is scrambled and the linker can't make heads or tails of it. – srking Oct 13 '12 at 00:57
  • See also http://stackoverflow.com/questions/16351889/nasm-emit-msw-of-non-scalar-link-time-value , for the same problem in assembly language instead of C. – Wayne Conrad May 03 '13 at 12:52
  • 1
    There is a related question and answer that offers up a solution using a linker script (and the C pre-processor) to build the IDT and GDT at link time rather than compile/assembly time: https://stackoverflow.com/questions/58192042/solution-needed-for-building-a-static-idt-and-gdt-at-assemble-compile-link-time – Michael Petch Oct 01 '19 at 23:26

1 Answers1

3

You ran into a well known x86 wart. I don't believe the linker can stuff the address of your isr routines in the swizzled form expected by the IDT entry.

If you are feeling ambitious, you could create an IDT builder script that does something like this (Linux based) approach. I haven't tested this scheme and it probably qualifies as a nasty hack anyway, so tread carefully.

Step 1: Write a script to run 'nm' and capture the stdout.

Step 2: In your script, parse the nm output to get the memory address of all your interrupt service routines.

Step 3: Output a binary file, 'idt.bin' that has the IDT bytes all setup and ready for the LIDT instruction. Your script obviously outputs the isr addresses in the correct swizzled form.

Step 4: Convert his raw binary into an elf section with objcopy:

objcopy -I binary -O elf32-i386 idt.bin idt.elf

Step 5: Now idt.elf file has your IDT binary with the symbol something like this:

> nm idt.elf
000000000000000a D _binary_idt_bin_end
000000000000000a A _binary_idt_bin_size
0000000000000000 D _binary_idt_bin_start

Step 6: relink your binary including idt.elf. In your assembly stubs and linker scripts, you can refer to symbol _binary_idt_bin_start as the base of the IDT. For example, your linker script can place the symbol _binary_idt_bin_start at any address you like.

Be careful that relinking with the IDT section doesn't move anyting else in your binary, e.g. your isr routines. Manage this in your linker script (.ld file) by puting the IDT into it's own dedicated section.

---EDIT--- From comments, there seems to be confusion about the problem. The 32-bit x86 IDT expects the address of the interrupt service routine to be split into two different 16-bit words, like so:

 31           16 15            0
+---------------+---------------+
| Address 31-16 |               |
+---------------+---------------+
|               | Address 15-0  |
+---------------+---------------+

A linker is thus unable to plug-in the ISR address as a normal relocation. So, at boot time, software must construct this split format, which slows boot time.

srking
  • 4,512
  • 1
  • 30
  • 46
  • 1
    This is the conclusion I have come to. A script or some other program is going to have to parse the symbol table of the ELF image and construct the swizzled IDT. The binary output file is a good idea. I was thinking of generating an actual C file but with the binary method I can avoid the compilation step which would be somewhat of a nightmare considering how our build system works. – jkayca Oct 15 '12 at 15:49
  • The binary method also avoids the need to inject code into your already linked firmware image. – srking Oct 15 '12 at 18:35
  • There is a related question and answer that offers up a solution using a linker script (and the C pre-processor) to build the IDT and GDT at link time rather than compile/assembly time. – Michael Petch Oct 01 '19 at 23:27