Ответ 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
. Это много байтов машинного кода.