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

时间:2015-07-11 19:20:14

标签: c linker x86 operating-system

我写了一些用于初始化http://psyadam.neoclaw.net/tables8.html的代码,它将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_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所做的那样),但它让我感到恼火的是,在连接时间技术上可行的东西实际上 可行。

2 个答案:

答案 0 :(得分:4)

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

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

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

e.g。你可以用这种方式编写它,在最终二进制文件中生成一个带有完整填充的表,这样数据就可以就地洗牌:

#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表格式不同。)< / p>

我认为我的大小计算正确(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,你得到一个紧凑的循环。-Os的阈值较低。)

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

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

答案 1 :(得分:3)

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

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

在<{strong> .text部分后面的链接脚本中添加它。这将确保表中的可执行代码进入可执行内存区域。

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

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

以下宏为您提供了包含相应jump的{​​{1}}(或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), ... }; 的非固定地址。

isr