Как сделать вычисления с адресами во время компиляции / компоновки?
Я написал некоторый код для инициализации IDT, который хранит 32-битные адреса в двух несмежных 16-битных половинах. IDT может храниться где угодно, и вы сообщаете процессору, где, запустив 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'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), но меня просто раздражает, что что-то технически выполнимое при связывании времени действительно возможно.
2 ответа
Основная проблема заключается в том, что адреса функций являются константами времени соединения, а не компиляцией времени. Компилятор не может просто получить 32-битные двоичные целые и вставить их в сегмент данных двумя отдельными частями. Вместо этого он должен использовать формат объектного файла, чтобы указать компоновщику, где он должен заполнить окончательное значение (+ смещение) того символа, когда соединение выполнено. Распространенными случаями являются непосредственный операнд инструкции, смещение действующего адреса или значение в разделе данных.
Было бы возможно, чтобы 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.
}
}
Это компилируется. Смотрите различия -m32
а также -m64
вывод asm в проводнике компилятора 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 делает это для восстановления памяти из кода и данных, которые необходимы только один раз, при запуске.) Это даст вам оптимальный компромисс для небольшого двоичного размера (так как 32-битные адреса меньше, чем 64-битные записи IDT), и память времени выполнения не тратится впустую на код настроить 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
, Это много байтов машинного кода.
Одним из способов будет использование промежуточной таблицы переходов, которая находится по фиксированному адресу. Вы могли бы инициализировать 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)
}
}
Поместите это в скрипт компоновщика сразу после .text
раздел. Это обеспечит попадание исполняемого кода в таблицу в область исполняемой памяти.
Вы можете указать компоновщику использовать ваш скрипт следующим образом, используя --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));
}