Warm tip: This article is reproduced from serverfault.com, please click

assembly-在组装/编译/链接时构建静态IDT和GDT所需的解决方案

(assembly - Solution needed for building a static IDT and GDT at assemble/compile/link time)

发布于 2019-10-01 20:46:04

多年来,尤其是在x86操作系统开发中遇到的许多问题都激发了这个问题。最近,有关NASM的一个问题因修改而引起争议在那种情况下,该人正在使用NASM并遇到汇编时间错误:

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

另一个相关问题询问在编译时生成静态IDT时导致错误的GCC代码问题:

初始值设定项元素不是恒定的

在这两种情况下,此问题都与以下事实有关:IDT条目需要一个指向异常处理程序的地址,而GDT可能需要到另一个结构(如任务段结构(TSS))的基地址。通常这不是问题,因为链接过程可以通过重定位修复程序来解析这些地址。对于IDT条目GDT条目,这些字段将基址/功能地址拆分开。没有任何重定位类型可以告诉链接器将它们移位,然后按照它们在GDT / IDT条目中的布局方式放置到内存中。彼得·科德斯(Peter Cordes)在这个答案对此作了很好的解释

我的问题是不是问这个问题是什么,但对于一个请求的功能,以及实用的解决方案的问题。尽管我对此做出了自我回答,但这只是许多可能的解决方案之一。我只要求提出的解决方案满足以下要求:

  • 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我选择了一个引导加载程序作为示例,因为“最小完整可验证示例”更易于生成。尽管代码位于引导加载程序中,但是类似的代码通常是作为内核或其他非引导加载程序代码的一部分编写的。该代码通常可以用汇编语言以外的其他语言编写,例如C / C ++等。

  • 由于旧版引导加载程序始终由BIOS物理地址0x7c00加载,因此可以在组装时针对这种情况采用其他特定解决方案。这种特定的解决方案打破了OS开发中更普遍的用例,在这种情况下,开发人员通常不希望将IDT或GDT地址硬编码为特定的线性/物理地址,因为最好让链接器为它们做。

Questioner
Michael Petch
Viewed
11
Michael Petch 2020-10-16 01:44:27

我最常用的一种解决方案是实际使用GNU链接器(ld)为我构建IDT和GDT。这个答案不编写GNU链接脚本底漆,但它确实让使用的BYTESHORTLONG链接器脚本指令建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);

exc0exc1exc2,和exc3定义了与从对象文件中导出的异常的功能。你可以看到IDT条目正在CODE32_SEL用于代码段。建立GDT时,告诉链接器计算选择器编号。显然,这非常混乱,并且随着GDT(尤其是IDT)的增长而变得更加笨拙。

你可以使用宏处理器m4来简化操作,但是我更喜欢使用C预处理器cpp),因为它对许多开发人员来说都很熟悉。尽管C预处理器通常用于预处理C / C ++文件,但它不仅限于这些文件。你可以在任何类型的文本文件(包括链接器脚本)上使用它。

你可以创建一个宏文件并定义几个宏,例如MAKE_IDT_DESCMAKE_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

现在,你只需要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.ldlink.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),该链接表脚本中将使用该来构建与该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

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'

该代码执行以下操作:

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

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

在此处输入图片说明