【问题标题】:Solution needed for building a static IDT and GDT at assemble/compile/link time在汇编/编译/链接时构建静态 IDT 和 GDT 所需的解决方案
【发布时间】:2020-01-31 04:10:22
【问题描述】:

这个问题的灵感来自许多人多年来遇到的一个问题,尤其是在 x86 操作系统开发中。最近一个related NASM question 被一个编辑撞到了。在这种情况下,该人正在使用 NASM 并收到组装时间错误:

移位运算符只能应用于标量值

另一位related question 询问在编译时生成静态 IDT 时 GCC 代码的问题导致错误:

初始化元素不是常量

在这两种情况下,问题都与以下事实有关:IDT 条目需要异常处理程序的地址,而 GDT 可能需要另一个结构(如任务段结构 (TSS))的基地址。通常这不是问题,因为链接过程可以通过重定位修复解决这些地址。在IDT entryGDT Entry 的情况下,字段将基地址/函数地址分开。没有重定位类型可以告诉链接器移动位,然后按照它们在 GDT/IDT 条目中的布局方式将它们放置在内存中。 Peter Cordes 在this answer 中对此进行了很好的解释。

我的问题不是问问题是什么,而是对问题的功能实用解决方案的要求。尽管我是在自我回答这个问题,但这只是许多可能的解决方案之一。我只要求提出的解决方案满足这些要求:

  • 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

代码是:

ma​​cros.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 处加载旧版引导加载程序,因此对于这种情况,还有其他特定的解决方案可以在组装时完成。此类特定解决方案打破了 OS 开发中更一般的用例,其中开发人员通常不希望将 IDT 或 GDT 地址硬编码为特定的线性/物理地址,因为最好让链接器为它们执行此操作。

【问题讨论】:

    标签: assembly x86 nasm ld osdev


    【解决方案1】:

    我最常用的一个解决方案是实际使用 GNU 链接器 (ld) 为我构建 IDT 和 GDT。此答案不是编写 GNU 链接器脚本的入门书,但它确实使用了 BYTESHORTLONG 链接器脚本指令来构建 IDT、GDT、IDT 记录和 GDT记录。链接器可以使用涉及&lt;&lt;&gt;&gt;&amp;| 等的表达式,并对其最终解析的符号的虚拟内存地址 (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);
    

    exc0exc1exc2exc3 是从目标文件定义和导出的异常函数。您可以看到 IDT 条目使用 CODE32_SEL 作为代码段。链接器被告知在构建 GDT 时计算选择器编号。显然,随着 GDT 尤其是 IDT 的增长,这非常混乱并且变得更加笨拙。

    您可以使用像m4 这样的宏处理器来简化事情,但我更喜欢使用C preprocessor (cpp),因为它为更多开发人员所熟悉。尽管 C 预处理器通常用于预处理 C/C++ 文件,但它并不限于这些文件。您可以在任何类型的文本文件中使用它,包括链接器脚本。

    您可以创建一个宏文件并定义几个宏,例如MAKE_IDT_DESCMAKE_GDT_DESC,以创建 GDT 和 IDT 描述符条目。我使用ldh 代表(Linker Header)的扩展命名约定,但您可以随意命名这些文件:

    ma​​cros.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
    

    现在您只需将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.pp 创建link.ld,您可以使用以下命令:

    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),该表在链接描述文件中用于构建与该 TSS 关联的 GDT 条目。


    预处理链接描述文件;集合;关联;并生成一个作为引导扇区的二进制文件,可以使用以下命令:

    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
    

    要在 QEMU 中运行boot.bin 软盘映像,您可以使用以下命令:

    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'
    

    代码做了这些事情:

    • 使用 lgdt 指令加载 GDT 记录。
    • 处理器被置于 32 位保护且 A20 禁用。演示中的所有代码都位于物理地址 0x100000 (1MiB) 下,因此不需要启用 A20。
    • 使用lidt 加载IDT 记录。
    • 使用ltr 将TSS 选择器加载到任务寄存器中。
    • 调用每个异常处理程序(exc0exc1exc2exc3)。
    • 每个异常处理程序都会在显示屏的左上角打印一个数字(0、1、2、3)。

    如果它在 BOCHS 中正确运行,输出应如下所示:

    【讨论】:

      猜你喜欢
      • 2015-10-05
      • 1970-01-01
      • 1970-01-01
      • 2013-03-28
      • 2016-08-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-03-29
      相关资源
      最近更新 更多