如何在编译/链接时使用地址进行计算?

我写了一些用于初始化IDT的代码,它将32位地址存储在两个不相邻的16位半中。 IDT可以存储在任何地方,并通过运行LIDT指令告诉CPU在哪里。

这是初始化表的代码:

 void idt_init(void) { /* Unfortunately, we can't write this as loops. The first option, * initializing the IDT with the addresses, here looping over it, and * reinitializing the descriptors didn't work because assigning a * a uintptr_t (from (uintptr_t) handler_func) to a descr (aka * uint64_t), according to the compiler, "isn't computable at load * time." * The second option, storing the addresses as a local array, simply is * inefficient (took 0.020ms more when profiling with the "time" command * line program!). * The third option, storing the addresses as a static local array, * consumes too much space (the array will probably never be used again * during the whole kernel runtime). * But IF my argument against the third option will be invalidated in * the future, THEN it's the best option I think. */ /* Initialize descriptors of exception handlers. */ idt[EX_DE_VEC] = idt_trap(ex_de); idt[EX_DB_VEC] = idt_trap(ex_db); idt[EX_NMI_VEC] = idt_trap(ex_nmi); idt[EX_BP_VEC] = idt_trap(ex_bp); idt[EX_OF_VEC] = idt_trap(ex_of); idt[EX_BR_VEC] = idt_trap(ex_br); idt[EX_UD_VEC] = idt_trap(ex_ud); idt[EX_NM_VEC] = idt_trap(ex_nm); idt[EX_DF_VEC] = idt_trap(ex_df); idt[9] = idt_trap(ex_res); /* unused Coprocessor Segment Overrun */ idt[EX_TS_VEC] = idt_trap(ex_ts); idt[EX_NP_VEC] = idt_trap(ex_np); idt[EX_SS_VEC] = idt_trap(ex_ss); idt[EX_GP_VEC] = idt_trap(ex_gp); idt[EX_PF_VEC] = idt_trap(ex_pf); idt[15] = idt_trap(ex_res); idt[EX_MF_VEC] = idt_trap(ex_mf); idt[EX_AC_VEC] = idt_trap(ex_ac); idt[EX_MC_VEC] = idt_trap(ex_mc); idt[EX_XM_VEC] = idt_trap(ex_xm); idt[EX_VE_VEC] = idt_trap(ex_ve); /* Initialize descriptors of reserved exceptions. * Thankfully we compile with -std=c11, so declarations within * for-loops are possible! */ for (size_t i = 21; i < 32; ++i) idt[i] = idt_trap(ex_res); /* Initialize descriptors of hardware interrupt handlers (ISRs). */ idt[INT_8253_VEC] = idt_int(int_8253); idt[INT_8042_VEC] = idt_int(int_8042); idt[INT_CASC_VEC] = idt_int(int_casc); idt[INT_SERIAL2_VEC] = idt_int(int_serial2); idt[INT_SERIAL1_VEC] = idt_int(int_serial1); idt[INT_PARALL2_VEC] = idt_int(int_parall2); idt[INT_FLOPPY_VEC] = idt_int(int_floppy); idt[INT_PARALL1_VEC] = idt_int(int_parall1); idt[INT_RTC_VEC] = idt_int(int_rtc); idt[INT_ACPI_VEC] = idt_int(int_acpi); idt[INT_OPEN2_VEC] = idt_int(int_open2); idt[INT_OPEN1_VEC] = idt_int(int_open1); idt[INT_MOUSE_VEC] = idt_int(int_mouse); idt[INT_FPU_VEC] = idt_int(int_fpu); idt[INT_PRIM_ATA_VEC] = idt_int(int_prim_ata); idt[INT_SEC_ATA_VEC] = idt_int(int_sec_ata); for (size_t i = 0x30; i < IDT_SIZE; ++i) idt[i] = idt_trap(ex_res); } 

idt_trapidt_int ,定义如下:

 #define idt_entry(off, type, priv) \ ((descr) (uintptr_t) (off) & 0xffff) | ((descr) (KERN_CODE & 0xff) << \ 0x10) | ((descr) ((type) & 0x0f) << 0x28) | ((descr) ((priv) & \ 0x03) << 0x2d) | (descr) 0x800000000000 | \ ((descr) ((uintptr_t) (off) & 0xffff0000) << 0x30) #define idt_int(off) idt_entry(off, 0x0e, 0x00) #define idt_trap(off) idt_entry(off, 0x0f, 0x00) 

idtuint64_t的数组,因此这些宏被隐式转换为该类型。 uintptr_t是保证能够将指针值保持为整数的类型,以及通常为32位宽的32位系统。 (64位IDT具有16字节条目;此代码用于32位)。

我得到警告,由于播放中的地址修改, initializer element is not constant
绝对确定地址在链接时已知。
有什么我可以做的工作吗? 使idt数组自动运行但这需要整个内核在一个函数的上下文中运行,这将是一些不好的麻烦,我想。

我可以在运行时通过一些额外的工作来完成这项工作(如同Linux 1.0一样),但它让我感到恼火的是,在连接时间技术上可行的东西实际上可行的。

主要问题是函数地址是链接时常量, 而不是严格的编译时常数。 编译器不仅可以获得32b二进制整数,而且可以将它分成两个独立的部分。 相反,它必须使用目标文件格式向链接器指示在完成链接时它应该填充哪个符号的最终值(+偏移量)。 常见情况是指令的直接操作数,有效地址中的位移或数据部分中的值。

ELF可能被设计为存储符号引用,以便在链接时用一个复杂的地址函数替换(或至少高/低一半,如MIPS上的lui $t0, %hi(symbol) / ori $t0, $t0, %lo(symbol)从两个16位immediates构建地址常量。 但实际上唯一允许的function是加法/减法 ,用于mov eax, [ext_symbol + 16]

当然,您的OS内核二进制文件可能在构建时具有完全解析的地址的静态IDT,因此您在运行时需要执行的只是执行单个lidt指令。 但是 ,标准构建工具链是一个障碍。 如果不对可执行文件进行后处理,您可能无法实现此目的。

例如,您可以这样写,以生成一个在最终二进制文件中具有完整填充的表,因此可以在适当的位置对数据进行洗牌:

 #include  #define PACKED __attribute__((packed)) // Note, this is the 32-bit format. 64-bit is larger typedef union idt_entry { // we will postprocess the linker output to have this format // (or convert at runtime) struct PACKED runtime { // from OSdev wiki uint16_t offset_1; // offset bits 0..15 uint16_t selector; // a code segment selector in GDT or LDT uint8_t zero; // unused, set to 0 uint8_t type_attr; // type and attributes, see below uint16_t offset_2; // offset bits 16..31 } rt; // linker output will be in this format struct PACKED compiletime { void *ptr; // offset bits 0..31 uint8_t zero; uint8_t type_attr; uint16_t selector; // to be swapped with the high16 of ptr } ct; } idt_entry; // #define idt_ct_entry(off, type, priv) { .ptr = off, .type_attr = type, .selector = priv } #define idt_ct_trap(off) { .ct = { .ptr = off, .type_attr = 0x0f, .selector = 0x00 } } // generate an entry in compile-time format extern void ex_de(); // these are the raw interrupt handlers, written in ASM extern void ex_db(); // they have to save/restore *all* registers, and end with iret, rather than the usual C ABI. // it might be easier to use asm macros to create this static data, // just so it can be in the same file and you don't need cross-file prototypes / declarations // (but all the same limitations about link-time constants apply) static idt_entry idt[] = { idt_ct_trap(ex_de), idt_ct_trap(ex_db), // ... }; // having this static probably takes less space than instructions to write it on the fly // but not much more. It would be easy to make a lidt function that took a struct pointer. static const struct PACKED idt_ptr { uint16_t len; // encoded as bytes - 1, so 0xffff means 65536 void *ptr; } idt_ptr = { sizeof(idt) - 1, idt }; /****** functions *********/ // inline void load_static_idt(void) { asm volatile ("lidt %0" : // no outputs : "m" (idt_ptr)); // memory operand, instead of writing the addressing mode ourself, allows a RIP-relative addressing mode in 64bit mode // also allows it to work with -masm=intel or not. } // Do this once at at run-time // **OR** run this to pre-process the binary, after link time, as part of your build void idt_convert_to_runtime(void) { #ifdef DEBUG static char already_done = 0; // make sure this only runs once if (already_done) error; already_done = 1; #endif const int count = sizeof idt / sizeof idt[0]; for (int i=0 ; i 

这确实编译。 在Godbolt编译器资源管理器中查看-m32-m64 asm输出的差异。 查看数据部分中的布局(请注意.value.short的同义词,并且是16位。)(但请注意,64位模式的IDT表格式不同。)

我认为我的大小计算正确( bytes - 1 ),如http://wiki.osdev.org/Interrupt_Descriptor_Table中所述 。 最小值100h字节长(编码为0x99 )。 另见https://en.wikibooks.org/wiki/X86_Assembly/Global_Descriptor_Table 。 ( lgdt size / pointer的工作方式相同,尽管表本身的格式不同。)


另一个选项 ,而不是在数据部分中具有IDT静态,是将它放在bss部分中,数据存储为将初始化它的函数中的立即常量(或者在该函数读取的数组中)。

无论哪种方式,该函数(及其数据)都可以位于.init部分,其内存在完成后重复使用。 (Linux这样做是为了从启动时只需要一次的代码和数据中回收内存。)这将为您提供小二进制大小的最佳权衡(因为32b地址小于64b IDT条目),并且代码上没有浪费运行时内存设置IDT。 在启动时运行一次的小循环可以忽略不计的CPU时间。 (Godbolt上的版本完全展开,因为我只有2个条目,并且它将地址嵌入到每个指令中作为32位立即,即使使用-Os 。使用足够大的表(只需复制/粘贴以复制一行)即使在-O3也能获得一个紧凑的循环。- -O3的阈值较低。)

没有内存重用haxx,可能需要一个紧凑的循环来重写64b条目。 在构建时执行它会更好,但是你需要一个自定义工具来运行内核二进制文件上的转换。

将数据存储在immediates中理论上听起来不错,但每个条目的代码可能总共超过64b,因为它无法循环。 将地址拆分为两个的代码必须完全展开(或放在一个函数中并调用)。 即使你有一个循环存储所有相同的多条目的东西,每个指针需要一个mov r32, imm32来获取寄存器中的地址,然后移动mov word [idt+i + 0], ax / shr eax, 16 / mov word [idt+i + 6], ax 。 这是很多机器码字节。

一种方法是使用位于固定地址的中间跳转表。 您可以使用此表中的位置地址初始化idt (这将是编译时常量)。 跳转表中的位置将包含实际isr例程的jump指令。

isr的调度将是间接的,如下:

 trap -> jump to intermediate address in the idt -> jump to isr 

在固定地址创建跳转表的一种方法如下。

第1步:将跳转表放在一个部分中

 // this is a jump table at a fixed address void jump(void) __attribute__((section(".si.idt"))); void jump(void) { asm("jmp isr0"); // can also be asm("call ...") depending on need asm("jmp isr1"); asm("jmp isr2"); } 

步骤2:指示链接器在固定地址处找到该部分

 SECTIONS { .so.idt 0x600000 : { *(.si.idt) } } 

将它放在.text部分后面的链接描述文件中。 这将确保表中的可执行代码将进入可执行内存区域。

您可以使用Makefile--script选项指示链接器使用您的脚本,如下所示。

 LDFLAGS += -Wl,--script=my_script.lds 

下面的宏给出了包含相应isrjump (或call )指令的位置的地址。

 // initialize the idt at compile time with const values // you can find a cleaner way to generate offsets #define JUMP_ADDR(off) ((char*)0x600000 + 4 + (off * 5)) 

然后,您将使用修改的宏按如下方式初始化idt

 // your real idt will be initialized as follows #define idt_entry(addr, type, priv) \ ( \ ((descr) (uintptr_t) (addr) & 0xffff) | \ ((descr) (KERN_CODE & 0xff) << 0x10) | \ ((descr) ((type) & 0x0f) << 0x28) | \ ((descr) ((priv) & 0x03) << 0x2d) | \ ((descr) 0x1 << 0x2F) | \ ((descr) ((uintptr_t) (addr) & 0xffff0000) << 0x30) \ ) #define idt_int(off) idt_entry(JUMP_ADDR(off), 0x0e, 0x00) #define idt_trap(off) idt_entry(JUMP_ADDR(off), 0x0f, 0x00) descr idt[] = { ... idt_trap(ex_de), ... idt_int(int_casc), ... }; 

下面是一个演示工作示例,它显示了从固定地址的指令向非固定地址发送的isr

 #include  // dummy isrs for demo void isr0(void) { printf("==== isr0\n"); } void isr1(void) { printf("==== isr1\n"); } void isr2(void) { printf("==== isr2\n"); } // this is a jump table at a fixed address void jump(void) __attribute__((section(".si.idt"))); void jump(void) { asm("jmp isr0"); // can be asm("call ...") asm("jmp isr1"); asm("jmp isr2"); } // initialize the idt at compile time with const values // you can find a cleaner way to generate offsets #define JUMP_ADDR(off) ((char*)0x600000 + 4 + (off * 5)) // dummy idt for demo // see below for the real idt char* idt[] = { JUMP_ADDR(0), JUMP_ADDR(1), JUMP_ADDR(2), }; int main(int argc, char* argv[]) { int trap; char* addr = idt[trap = argc - 1]; printf("==== idt[%d]=%p\n", trap, addr); asm("jmp *%0\n" : :"m"(addr)); }