Решение, необходимое для создания статической IDT и GDT во время сборки / компиляции / компоновки - PullRequest
6 голосов
/ 01 октября 2019

Этот вопрос вдохновлен проблемой, с которой многие сталкивались на протяжении многих лет, особенно при разработке операционной системы x86. Недавно связанный с NASM вопрос был отредактирован редактированием. В этом случае человек использовал NASM и получал ошибку времени сборки:

оператор сдвига может применяться только к скалярным значениям

Другой связанный вопрос спрашивает о проблеме с кодом GCC при генерации статической IDT во время компиляции, которая привела к ошибке:

элемент инициализатора не является постоянным

В обоих случаяхЭта проблема связана с тем фактом, что запись IDT требует адреса для обработчика исключений, а GDT может потребоваться базовый адрес для другой структуры, такой как структура сегмента задачи (TSS). Обычно это не проблема, потому что процесс связывания может разрешить эти адреса с помощью исправлений перемещения. В случае записи IDT или GDT Entry поля разделяют адреса базы / функции. Не существует типов перемещения, которые могут указывать компоновщику сдвигать биты и затем помещать их в память так, как они размещены в записи GDT / IDT. Питер Кордес написал хорошее объяснение того, что в этот ответ .

Мой вопрос не спрашивает, в чем проблема, но запрос на функционал и практические решения проблемы. Хотя я сам отвечаю на это, это только одно из многих возможных решений. Я только прошу, чтобы предлагаемые решения отвечали этим требованиям:

  • У GDT и IDT не должно быть адресов, привязанных к конкретному физическому или линейному адресу.
  • Как минимум, решение должно бытьумеет работать с объектами ELF и исполняемыми файлами ELF. Если это работает для других форматов, даже лучше!
  • Не имеет значения, является ли решение частью процесса создания окончательного исполняемого / двоичного файла или нет. Если решение требует обработки времени сборки после создания исполняемого / двоичного файла, что также является приемлемым.
  • GDT (или IDT) должен отображаться как полностью разрешенный при загрузке в память. Решения не должны требовать исправлений во время выполнения.

Пример кода, который не работает

Я предоставляю пример кода в виде устаревшего загрузчика 1 , который пытается создать статические IDT и GDT во время сборки, но не может выполнить эти ошибки при сборке с nasm -f elf32 -o boot.o boot.asm:

boot.asm:78: error: `&' operator may only be applied to scalar values
boot.asm:78: error: `&' operator may only be applied to scalar values
boot.asm:79: error: `&' operator may only be applied to scalar values
boot.asm:79: error: `&' operator may only be applied to scalar values
boot.asm:80: error: `&' operator may only be applied to scalar values
boot.asm:80: error: `&' operator may only be applied to scalar values
boot.asm:81: error: `&' operator may only be applied to scalar values
boot.asm:81: error: `&' operator may only be applied to scalar values

Код:

macros.inc

; Macro to build a GDT descriptor entry
%define MAKE_GDT_DESC(base, limit, access, flags) \
    (((base & 0x00FFFFFF) << 16) | \
    ((base & 0xFF000000) << 32) | \
    (limit & 0x0000FFFF) | \
    ((limit & 0x000F0000) << 32) | \
    ((access & 0xFF) << 40) | \
    ((flags & 0x0F) << 52))

; Macro to build a IDT descriptor entry
%define MAKE_IDT_DESC(offset, selector, access) \
    ((offset & 0x0000FFFF) | \
    ((offset & 0xFFFF0000) << 32) | \
    ((selector & 0x0000FFFF) << 16) | \
    ((access & 0xFF) << 40))

boot.asm :

%include "macros.inc"

PM_MODE_STACK EQU 0x10000

global _start

bits 16

_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

    ; Test the first 4 exception handlers
    int 0
    int 1
    int 2
    int 3

.loop:
    hlt
    jmp .loop

exc0:
    iret
exc1:
    iret
exc2:
    iret
exc3:
    iret

align 4
gdt:
    dq MAKE_GDT_DESC(0, 0, 0, 0)   ; null descriptor
.code32:
    dq MAKE_GDT_DESC(0, 0x000fffff, 10011010b, 1100b)
                                ; 32-bit code, 4kb gran, limit 0xffffffff bytes, base=0
.data32:
    dq MAKE_GDT_DESC(0, 0x000fffff, 10010010b, 1100b)
                                ; 32-bit data, 4kb gran, limit 0xffffffff bytes, base=0
.end:

CODE32_SEL equ gdt.code32 - gdt
DATA32_SEL equ gdt.data32 - gdt

align 4
gdtr:
    dw gdt.end - gdt - 1        ; limit (Size of GDT - 1)
    dd gdt                      ; base of GDT

align 4
; Create an IDT which handles the first 4 exceptions
idt:
    dq MAKE_IDT_DESC(exc0, CODE32_SEL, 10001110b)
    dq MAKE_IDT_DESC(exc1, CODE32_SEL, 10001110b)
    dq MAKE_IDT_DESC(exc2, CODE32_SEL, 10001110b)
    dq MAKE_IDT_DESC(exc3, CODE32_SEL, 10001110b)
.end:

align 4
idtr:
    dw idt.end - idt - 1        ; limit (Size of IDT - 1)
    dd idt                      ; base of IDT

Сноски

  • 1 Я выбрал загрузчик в качестве примера, поскольку Minimal Complete Verifiable Example было проще в изготовлении. Хотя код находится в загрузчике, подобный код обычно пишется как часть ядра или другого кода, не являющегося загрузчиком. Код часто может быть написан на языках, отличных от ассемблера, таких как C / C ++ и т. Д.

  • Поскольку устаревший загрузчик всегда загружается BIOS по физическому адресу 0x7c00, существуют другие конкретные решениядля этого случая это можно сделать во время сборки. Такие конкретные решения нарушают более общие случаи использования в разработке ОС, когда разработчик обычно не хочет жестко кодировать адреса IDT или GDT по конкретным линейным / физическим адресам, поскольку предпочтительно, чтобы компоновщик сделал это для них.

1 Ответ

6 голосов
/ 01 октября 2019

Одним из решений, которое я чаще всего использую, является использование компоновщика 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, вывод должен выглядеть следующим образом:

enter image description here

...