Как выполнять вычисления с адресами во время компиляции/компоновки?

Я написал код для инициализации IDT, в котором хранятся 32-разрядные адреса в двух несмежных 16-битных половинах. IDT можно хранить в любом месте, и вы указываете CPU, где, выполняя инструкцию LIDT.

Это код для инициализации таблицы:

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 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), но меня просто раздражает то, что что-то технически возможно при связывании времени на самом деле в выполнимо.

Ответ 1

Основная проблема заключается в том, что адреса функций являются константами времени соединения, а не строго компиляцией времени. Компилятор не может просто получить 32-битные двоичные целые числа и вставить их в сегмент данных двумя отдельными частями. Вместо этого он должен использовать формат объектного файла, чтобы указать компоновщику, где он должен заполнить окончательное значение (+ смещение) того символа, когда соединение выполнено. Распространенными случаями являются непосредственный операнд инструкции, смещение действующего адреса или значение в разделе данных. (Но во всех этих случаях он все еще просто заполняет 32-битный абсолютный адрес, поэтому все 3 используют один и тот же тип перемещения ELF. Существует другое перемещение для относительных смещений для смещений перехода/вызова.)

Было бы возможно, чтобы ELF был предназначен для хранения ссылки на символ, которая будет заменена во время соединения сложной функцией адреса (или, по крайней мере, половин высоких/низких частот, как в MIPS для построения lui $t0, %hi(symbol)/ori $t0, $t0, %lo(symbol), адресные константы от двух 16-битных непосредственных). Но на самом деле единственная разрешенная функция - это сложение/вычитание для использования в таких вещах, как mov eax, [ext_symbol + 16].

Конечно, двоичный файл ядра вашей ОС может иметь статическую 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.
  }
}

Это компилируется. Смотрите различия в выводе asm -m32 и -m64 в проводнике компилятора Godbolt. Посмотрите на макет в разделе данных (обратите внимание, что .value является синонимом для .short и составляет 16 бит). (Но обратите внимание, что формат таблицы IDT отличается для 64-битного режима.)

Я думаю, что у меня есть правильный расчет размера (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 меньше, чем записи IDT 64b), и память времени выполнения не терялась на код настроить IDT. Небольшой цикл, который запускается один раз при запуске, занимает незначительное время процессора. (Версия на Godbolt полностью развертывается, потому что у меня есть только 2 записи, и он встраивает адрес в каждую инструкцию как 32-битную немедленную, даже с -Os. С достаточно большой таблицей (просто скопируйте/вставьте, чтобы дублировать строку) Вы получаете компактную петлю даже в -O3. Порог ниже для -Os.)

Без повторного использования памяти haxx, вероятно, стоит пойти по пути тесного цикла для перезаписи 64-битных записей. Делать это во время сборки было бы еще лучше, но тогда вам понадобится специальный инструмент для запуска преобразования в двоичном файле ядра.

Хранение данных в непосредственных элементах звучит хорошо в теории, но код для каждой записи, вероятно, будет составлять более 64b, потому что он не может зацикливаться. Код для разделения адреса на два должен быть полностью развернут (или помещен в функцию и вызван). Даже если бы у вас был цикл для хранения всего одного и того же для нескольких записей, каждому указателю понадобился бы mov r32, imm32, чтобы получить адрес в регистре, а затем mov word [idt+i + 0], ax/shr eax, 16/mov word [idt+i + 6], ax. Это много байтов машинного кода.

Ответ 2

Один из способов - использовать таблицу промежуточного перехода, расположенную по фиксированному адресу. Вы можете инициализировать 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)
  }
}

Поместите это в компоновщик script сразу после раздела .text. Это позволит убедиться, что исполняемый код в таблице перейдет в область исполняемого файла.

Вы можете указать компоновщику использовать script следующим образом, используя опцию --script в Makefile.

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),
    ...
};

Ниже приведен демонстрационный рабочий пример, который показывает отправку на 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));
}