如何在 compile/linking 时间对地址进行计算?

How to do computations with addresses at compile/linking time?

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

这是初始化table的代码:

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_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 0.01 也是如此)但是让我恼火的是,在链接时技术上可行的东西实际上是 可行。

主要问题是函数地址是link时间常数,不是严格的编译时间常数。编译器不能只获取 32b 二进制整数并将其分成两个单独的部分插入数据段。相反,它必须使用目标文件格式来指示 linker 在 linking 完成后应该在哪里填充哪个符号的最终值(+偏移量)。常见的情况是作为指令的立即操作数、有效地址中的位移或数据部分中的值。 (但在所有这些情况下,它仍然只是填充 32 位绝对地址,因此所有 3 个都使用相同的 ELF 重定位类型。relative 跳转/调用偏移的位移有不同的重定位。)

ELF 有可能被设计为存储符号引用,以便在 link 时间用地址的复杂函数(或至少像 MIPS 上的高/低半部分)替换lui $t0, %hi(symbol) / ori $t0, $t0, %lo(symbol) 从两个 16 位立即数构建地址常量)。但实际上唯一允许的功能是 addition/subtraction,用于 mov eax, [ext_symbol + 16].

之类的东西

您的 OS 内核二进制文件当然有可能在构建时拥有一个具有完全解析地址的静态 IDT,因此您在 运行 时需要做的就是执行一个 lidt指令。 不过,标准 构建工具链是一个障碍。如果没有 post-处理您的 executable.

,您可能无法实现此目的

例如你可以这样写,在最终的二进制文件中生成一个带有完整填充的 table,这样数据就可以就地洗牌:

#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位的IDTtable格式不一样模式。)

我认为我的大小计算正确 (bytes - 1),如 http://wiki.osdev.org/Interrupt_Descriptor_Table. Minimum value 100h bytes long (encoded as 0x99). See also https://en.wikibooks.org/wiki/X86_Assembly/Global_Descriptor_Table 中所述。 (lgdt size/pointer 的工作方式相同,尽管 table 本身具有不同的格式。)


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

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

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

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

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

发送到 isr 将是间接的,如下所示:

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

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

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

// 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 部分之后的链接描述文件中。这将确保 table 中的 executable 代码将进入 executable 内存区域。

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

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

下面的宏给出了包含jump(或call)指令到相应isr.

的地址
// 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),
    ...
};

下面是一个演示工作示例,它显示了从固定地址的指令分派到具有 non-fixed 地址的 isr

#include <stdio.h>

// 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));
}