Одним из решений, которое я чаще всего использую, является использование компоновщика GNU (ld
) для создания IDT и GDT для меня. Этот ответ не является учебником по написанию сценариев компоновщика GNU, но он использует директивы сценариев компоновщика BYTE
, SHORT
и LONG
для построения IDT, GDT, записи IDT и GDTзапись. Компоновщик может использовать выражения, включающие <<
, >>
, &
, |
и т. Д., И делать это на адресах виртуальной памяти (VMA) символов, которые он в конечном итоге разрешает.
Проблема заключается в том, чтоскрипты компоновщика довольно тупые. У них нет макроязыка, поэтому вам придется писать записи IDT и GDT, например:
. = ALIGN(4);
gdt = .;
NULL_SEL = ABSOLUTE(. - gdt);
SHORT(0);
SHORT(0);
BYTE(0 >> 16);
BYTE(0);
BYTE((0 >> 16 & 0x0f) | (0 << 4)); BYTE(0 >> 24);
CODE32_SEL = ABSOLUTE(. - gdt);
SHORT(0x000fffff);
SHORT(0);
BYTE(0 >> 16);
BYTE(10011010b);
BYTE((0x000fffff >> 16 & 0x0f) | (1100b << 4));
BYTE(0 >> 24);
DATA32_SEL = ABSOLUTE(. - gdt);
SHORT(0x000fffff);
SHORT(0);
BYTE(0 >> 16);
BYTE(10010010b);
BYTE((0x000fffff >> 16 & 0x0f) | (1100b << 4));
BYTE(0 >> 24);
gdt_size = ABSOLUTE(. - gdt);
. = ALIGN(4);
idt = .;
SHORT(exc0 & 0x0000ffff);
SHORT(CODE32_SEL);
BYTE(0x00);
BYTE(10001110b);
SHORT(exc0 >> 16);
SHORT(exc1 & 0x0000ffff);
SHORT(CODE32_SEL);
BYTE(0x00);
BYTE(10001110b);
SHORT(exc1 >> 16);
SHORT(exc2 & 0x0000ffff);
SHORT(CODE32_SEL);
BYTE(0x00);
BYTE(10001110b);
SHORT(exc2 >> 16);
SHORT(exc3 & 0x0000ffff);
SHORT(CODE32_SEL);
BYTE(0x00);
BYTE(10001110b);
SHORT(exc3 >> 16);
idt_size = ABSOLUTE(. - idt);
exc0
, exc1
, exc2
и exc3
являются функциями исключения, определенными и экспортированными из объектного файла. Вы можете видеть, что записи IDT используют CODE32_SEL
для сегмента кода. Линкеру сказано вычислить числа селекторов при построении GDT. Очевидно, что это очень грязно и становится более громоздким по мере роста GDT и особенно IDT.
Вы можете использовать макропроцессор, такой как m4
, чтобы упростить вещи, но я предпочитаю использоватьпрепроцессор C (cpp
), поскольку он знаком гораздо большему числу разработчиков. Хотя препроцессор C обычно используется для предварительной обработки файлов C / C ++, он не ограничивается этими файлами. Вы можете использовать его в любом текстовом файле, включая скрипты компоновщика.
Вы можете создать файл макроса и определить пару макросов, таких как MAKE_IDT_DESC
и MAKE_GDT_DESC
, для создания записей дескрипторов GDT и IDT. Я использую соглашение об именах расширений, где ldh
обозначает (Linker Header), но вы можете называть эти файлы как угодно:
macros.ldh :
#ifndef MACROS_LDH
#define MACROS_LDH
/* Linker script C pre-processor macros */
/* Macro to build a IDT descriptor entry */
#define MAKE_IDT_DESC(offset, selector, access) \
SHORT(offset & 0x0000ffff); \
SHORT(selector); \
BYTE(0x00); \
BYTE(access); \
SHORT(offset >> 16);
/* Macro to build a GDT descriptor entry */
#define MAKE_GDT_DESC(base, limit, access, flags) \
SHORT(limit); \
SHORT(base); \
BYTE(base >> 16); \
BYTE(access); \
BYTE((limit >> 16 & 0x0f) | (flags << 4));\
BYTE(base >> 24);
#endif
Чтобы сократить беспорядок в сценарии основного компоновщика, вы можете создать еще один заголовочный файл, который создает GDT и IDT (и связанные с ними записи):
gdtidt.ldh
#ifndef GDTIDT_LDH
#define GDTIDT_LDH
#include "macros.ldh"
/* GDT table */
. = ALIGN(4);
gdt = .;
NULL_SEL = ABSOLUTE(. - gdt); MAKE_GDT_DESC(0, 0, 0, 0);
CODE32_SEL = ABSOLUTE(. - gdt); MAKE_GDT_DESC(0, 0x000fffff, 10011010b, 1100b);
DATA32_SEL = ABSOLUTE(. - gdt); MAKE_GDT_DESC(0, 0x000fffff, 10010010b, 1100b);
/* TSS structure tss_entry and TSS_SIZE are exported from an object file */
TSS32_SEL = ABSOLUTE(. - gdt); MAKE_GDT_DESC(tss_entry, TSS_SIZE - 1, \
10001001b, 0000b);
gdt_size = ABSOLUTE(. - gdt);
/* GDT record */
. = ALIGN(4);
SHORT(0); /* These 2 bytes align LONG(gdt) on 4 byte boundary */
gdtr = .;
SHORT(gdt_size - 1);
LONG(gdt);
/* IDT table */
. = ALIGN(4);
idt = .;
MAKE_IDT_DESC(exc0, CODE32_SEL, 10001110b);
MAKE_IDT_DESC(exc1, CODE32_SEL, 10001110b);
MAKE_IDT_DESC(exc2, CODE32_SEL, 10001110b);
MAKE_IDT_DESC(exc3, CODE32_SEL, 10001110b);
idt_size = ABSOLUTE(. - idt);
/* IDT record */
. = ALIGN(4);
SHORT(0); /* These 2 bytes align LONG(idt) on 4 byte boundary */
idtr = .;
SHORT(idt_size - 1);
LONG(idt);
#endif
Теперь вам просто нужно включить macros.ldh
в верхней части скрипта компоновщика и включить gdtidt.ldh
в той точке скрипта компоновщика (внутри раздела), в которую вы хотите поместить структуры:
link.ld.pp :
OUTPUT_FORMAT("elf32-i386");
ENTRY(_start);
REAL_BASE = 0x00007c00;
SECTIONS
{
. = REAL_BASE;
.text : SUBALIGN(4) {
*(.text*);
}
.rodata : SUBALIGN(4) {
*(.rodata*);
}
.data : SUBALIGN(4) {
*(.data);
/* Place the IDT and GDT structures here */
#include "gdtidt.ldh"
}
/* Disk boot signature */
.bootsig : AT(0x7dfe) {
SHORT (0xaa55);
}
.bss : SUBALIGN(4) {
*(COMMON);
*(.bss)
}
/DISCARD/ : {
*(.note.gnu.property)
*(.comment);
}
}
Этот скрипт компоновщика является типичным, который я использую для загрузочных секторов, но все, что я сделал, это включил gdtidt.ldh
файл, позволяющий компоновщику генерировать структуры. Осталось только предварительно обработать файл link.ld.pp
. Я использую расширение .pp
для файлов препроцессора, но вы можете использовать любое расширение. Чтобы создать link.ld
из link.ld.pp
, вы можете использовать команду:
cpp -P link.ld.pp >link.ld
Полученный сгенерированный файл link.ld
будет выглядеть так:
OUTPUT_FORMAT("elf32-i386");
ENTRY(_start);
REAL_BASE = 0x00007c00;
SECTIONS
{
. = REAL_BASE;
.text : SUBALIGN(4) {
*(.text*);
}
.rodata : SUBALIGN(4) {
*(.rodata*);
}
.data : SUBALIGN(4) {
*(.data);
. = ALIGN(4);
gdt = .;
NULL_SEL = ABSOLUTE(. - gdt); SHORT(0); SHORT(0); BYTE(0 >> 16); BYTE(0); BYTE((0 >> 16 & 0x0f) | (0 << 4)); BYTE(0 >> 24);;
CODE32_SEL = ABSOLUTE(. - gdt); SHORT(0x000fffff); SHORT(0); BYTE(0 >> 16); BYTE(10011010b); BYTE((0x000fffff >> 16 & 0x0f) | (1100b << 4)); BYTE(0 >> 24);;
DATA32_SEL = ABSOLUTE(. - gdt); SHORT(0x000fffff); SHORT(0); BYTE(0 >> 16); BYTE(10010010b); BYTE((0x000fffff >> 16 & 0x0f) | (1100b << 4)); BYTE(0 >> 24);;
TSS32_SEL = ABSOLUTE(. - gdt); SHORT(TSS_SIZE - 1); SHORT(tss_entry); BYTE(tss_entry >> 16); BYTE(10001001b); BYTE((TSS_SIZE - 1 >> 16 & 0x0f) | (0000b << 4)); BYTE(tss_entry >> 24);;
gdt_size = ABSOLUTE(. - gdt);
. = ALIGN(4);
SHORT(0);
gdtr = .;
SHORT(gdt_size - 1);
LONG(gdt);
. = ALIGN(4);
idt = .;
SHORT(exc0 & 0x0000ffff); SHORT(CODE32_SEL); BYTE(0x00); BYTE(10001110b); SHORT(exc0 >> 16);;
SHORT(exc1 & 0x0000ffff); SHORT(CODE32_SEL); BYTE(0x00); BYTE(10001110b); SHORT(exc1 >> 16);;
SHORT(exc2 & 0x0000ffff); SHORT(CODE32_SEL); BYTE(0x00); BYTE(10001110b); SHORT(exc2 >> 16);;
SHORT(exc3 & 0x0000ffff); SHORT(CODE32_SEL); BYTE(0x00); BYTE(10001110b); SHORT(exc3 >> 16);;
idt_size = ABSOLUTE(. - idt);
. = ALIGN(4);
SHORT(0);
idtr = .;
SHORT(idt_size - 1);
LONG(idt);
}
.bootsig : AT(0x7dfe) {
SHORT (0xaa55);
}
.bss : SUBALIGN(4) {
*(COMMON);
*(.bss)
}
/DISCARD/ : {
*(.note.gnu.property)
*(.comment);
}
}
С небольшими изменениями вПример файла boot.asm
в вопросе, в результате которого мы получим:
boot.asm :
PM_MODE_STACK EQU 0x10000 ; Protected mode stack address
RING0_STACK EQU 0x11000 ; Stack address for transitions to ring0
TSS_IO_BITMAP_SIZE EQU 0 ; Size 0 disables IO port bitmap (no permission)
global _start
; Export the exception handler addresses so the linker can access them
global exc0
global exc1
global exc2
global exc3
; Export the TSS size and address of the TSS so the linker can access them
global TSS_SIZE
global tss_entry
; Import the IDT/GDT and selector values generated by the linker
extern idtr
extern gdtr
extern CODE32_SEL
extern DATA32_SEL
extern TSS32_SEL
bits 16
section .text
_start:
xor ax, ax
mov ds, ax
mov es, ax
mov ss, ax
mov sp, ax ; Stack grows down from physical address 0x00010000
; SS:SP = 0x0000:0x0000 wraps to top of 64KiB segment
cli
cld
lgdt [gdtr] ; Load our GDT
mov eax, cr0
or eax, 1
mov cr0, eax ; Set protected mode flag
jmp CODE32_SEL:start32 ; FAR JMP to set CS
bits 32
start32:
mov ax, DATA32_SEL ; Setup the segment registers with data selector
mov ds, ax
mov es, ax
mov ss, ax
mov esp, PM_MODE_STACK ; Set protected mode stack pointer
mov fs, ax ; Not currently using FS and GS
mov gs, ax
lidt [idtr] ; Load our IDT
; This TSS isn't used in this code since everything is running at ring 0.
; Loading a TSS is for demonstration purposes in this case.
mov eax, TSS32_SEL
ltr ax ; Load default TSS (used for exceptions, interrupts, etc)
xchg bx, bx ; Bochs magic breakpoint
; Test the first 4 exception handlers
int 0
int 1
int 2
int 3
.loop:
hlt
jmp .loop
exc0:
mov word [0xb8000], 0x5f << 8 | '0' ; Print '0'
iretd
exc1:
mov word [0xb8002], 0x5f << 8 | '1' ; Print '1'
iretd
exc2:
mov word [0xb8004], 0x5f << 8 | '2' ; Print '2'
iretd
exc3:
mov word [0xb8006], 0x5f << 8 | '3' ; Print '3'
iretd
section .data
; Generate a functional TSS structure
ALIGN 4
tss_entry:
.back_link: dd 0
.esp0: dd RING0_STACK ; Kernel stack pointer used on ring0 transitions
.ss0: dd DATA32_SEL ; Kernel stack selector used on ring0 transitions
.esp1: dd 0
.ss1: dd 0
.esp2: dd 0
.ss2: dd 0
.cr3: dd 0
.eip: dd 0
.eflags: dd 0
.eax: dd 0
.ecx: dd 0
.edx: dd 0
.ebx: dd 0
.esp: dd 0
.ebp: dd 0
.esi: dd 0
.edi: dd 0
.es: dd 0
.cs: dd 0
.ss: dd 0
.ds: dd 0
.fs: dd 0
.gs: dd 0
.ldt: dd 0
.trap: dw 0
.iomap_base:dw .iomap ; IOPB offset
.iomap: TIMES TSS_IO_BITMAP_SIZE db 0x00
; IO bitmap (IOPB) size 8192 (8*8192=65536) representing
; all ports. An IO bitmap size of 0 would fault all IO
; port access if IOPL < CPL (CPL=3 with v8086)
%if TSS_IO_BITMAP_SIZE > 0
.iomap_pad: db 0xff ; Padding byte that has to be filled with 0xff
; To deal with issues on some CPUs when using an IOPB
%endif
TSS_SIZE EQU $-tss_entry
Новый boot.asm
также создает таблицу TSS (tss_entry
), который используется в сценарии компоновщика для создания записи GDT, связанной с этим TSS.
Для предварительной обработки сценария компоновщика;собрать;ссылка;и сгенерировать двоичный файл, который работает как загрузочный сектор, можно использовать следующие команды:
cpp -P link.ld.pp >link.ld
nasm -f elf32 -gdwarf -o boot.o boot.asm
ld -melf_i386 -Tlink.ld -o boot.elf boot.o
objcopy -O binary boot.elf boot.bin
Чтобы запустить образ дискеты boot.bin
в QEMU, вы можете использовать команду:
qemu-system-i386 -drive format=raw,index=0,if=floppy,file=boot.bin
Чтобы запустить его с BOCHS, вы можете использовать команду:
bochs -qf /dev/null \
'floppya: type=1_44, 1_44="boot.bin", status=inserted, write_protected=0' \
'boot: floppy' \
'magic_break: enabled=0'
Код выполняет следующие действия:
- Загрузка записи GDT с помощью инструкции
lgdt
. - Процессор помещен в 32-битную защиту с отключенным A20. Весь код в демонстрации находится под физическим адресом 0x100000 (1MiB), поэтому включение A20 не требуется.
- Загружает запись IDT с помощью
lidt
. - Загружает селектор TSS в задачузарегистрироваться с помощью
ltr
. - Вызывает каждый из обработчиков исключений (
exc0
, exc1
, exc2
и exc3
). - Каждый обработчик исключений печатает число (0, 1, 2, 3) в верхнем левом углу дисплея.
Если он работает правильно в BOCHS, вывод должен выглядеть следующим образом: