我写了一些初始化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 (a.k.a.
* 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_trap
和idt_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)
idt
是uint64_t
的数组,因此这些宏隐式转换为该类型。 uintptr_t
是保证能够将指针值保存为整数的类型,并且在通常为32位宽的32位系统上。 (一个64位IDT有16个字节的条目;此代码用于32位)。由于正在修改地址,我得到了
initializer element is not constant
的警告。绝对可以确保链接时知道该地址。
我可以做些什么来做这项工作? 使
idt
数组自动运行是可行的,但是这将要求整个内核在一个函数的上下文中运行,这会带来一些麻烦。我可以在运行时通过其他一些工作来完成这项工作(就像Linux 0.01一样),但这让我很烦恼,在链接时在技术上可行的实际上在中是可行的。
最佳答案
主要问题是函数地址是链接时间常数,而不是严格编译时间常数。编译器不能只获取32b个二进制整数并将其分成两个单独的部分粘贴到数据段中。取而代之的是,它必须使用目标文件格式来向链接器指示链接完成后应在何处填写哪个符号的最终值(+偏移)。常见情况是作为指令的立即操作数,有效地址中的位移或数据部分中的值。 (但是在所有这些情况下,它仍然只是填写32位绝对地址,因此所有3个都使用相同的ELF重定位类型。对于跳转/调用偏移的相对位移,存在不同的重定位。)
ELF可能已被设计为存储符号引用,以便在链接时用地址的复杂函数代替(或至少像MIPS上的lui $t0, %hi(symbol)
/ ori $t0, $t0, %lo(symbol)
的高/低半部分那样从两个地址构建地址常量) 16位立即数)。但实际上,唯一允许的功能是addition/subtraction,用于mov eax, [ext_symbol + 16]
之类的东西。
您的OS内核二进制文件当然可以在构建时使用具有完全解析地址的静态IDT,因此在运行时所需要做的就是执行一条lidt
指令。 但是,标准
建立工具链是一个障碍。如果不对可执行文件进行后期处理,您可能无法实现这一目标。
例如您可以这样写,以在最终二进制文件中生成带有完整填充的表,以便可以就地对数据进行随机排序:
#include <stdint.h>
#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<count ; i++) {
uint16_t tmp1 = idt[i].rt.selector;
uint16_t tmp2 = idt[i].rt.offset_2;
idt[i].rt.offset_2 = tmp1;
idt[i].rt.selector = tmp2;
// or do this swap in fewer insns with SSE or MMX pshufw, but using vector instructions before setting up the IDT may be insane.
}
}
这确实可以编译。请参阅Godbolt编译器资源管理器上的a diff of the
-m32
and -m64
asm output。查看数据部分的布局(请注意.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
大小/指针的工作方式相同,尽管表本身具有不同的格式。)另一个选项不是在数据部分中使IDT静态,而是在bss部分中将其存储,并将数据作为立即常量存储在将对其初始化的函数中(或在该函数读取的数组中)。 。
无论哪种方式,该函数(及其数据)都可以位于
.init
节中,您可以在完成后重新使用其内存。 (Linux这样做是为了在启动时从仅需要一次的代码和数据中回收内存。)这将为您提供小二进制大小的最佳折衷方案(因为32b地址小于64b IDT条目),并且不会在代码上浪费运行时内存设置IDT。在启动时运行一次的小循环对CPU时间的影响可以忽略不计。 (Godbolt上的版本已完全展开,因为我只有2个条目,并且即使使用-Os
,它也将地址作为32位立即数嵌入到每个指令中。有了足够大的表(只需复制/粘贴即可复制一行),您就可以得到甚至在-O3
时也会出现一个紧凑的循环。-Os
的阈值较低。)如果没有内存重用haxx,那么可能存在一个紧密的循环来重写64b条目。在构建时这样做会更好,但随后您需要一个自定义工具来对内核二进制文件运行转换。
从理论上讲,将数据存储在立即数中听起来不错,但是每个条目的代码总计可能超过64b,因为它无法循环。将地址分为两部分的代码必须完全展开(或放置在函数中并调用)。即使您有一个循环来存储所有相同的多次输入内容,每个指针也需要一个
mov r32, imm32
才能在寄存器中获取地址,然后是mov word [idt+i + 0], ax
/ shr eax, 16
/ mov word [idt+i + 6], ax
。那是很多机器代码字节。关于c - 在编译/链接时如何使用地址进行计算?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/31360888/