多年来,尤其是在x86操作系统开发中遇到的许多问题都激发了这个问题。最近,related NASM question被编辑破坏了。在那种情况下,该人正在使用NASM并遇到汇编时间错误:
移位运算符只能应用于标量值
另一个related question询问在编译时生成静态IDT时导致错误的GCC代码问题:
初始化器元素不是常量
在这两种情况下,此问题都与以下事实有关:IDT条目需要一个指向异常处理程序的地址,而GDT可能需要到另一个结构(如任务段结构(TSS))的基地址。通常这不是问题,因为链接过程可以通过重定位修复程序来解析这些地址。在IDT entry或GDT Entry的情况下,这些字段将基地址/功能地址分开。没有任何重定位类型可以告诉链接程序将这些位移位,然后按照它们在GDT / IDT条目中的布局方式放置到内存中。彼得·科德斯(Peter Cordes)在this answer中对此进行了很好的解释。
我的问题不是问问题是什么,而是对问题的实用,实用解决方案的要求。尽管我对此进行了自我解答,但这只是许多可能的解决方案之一。我只要求提出的解决方案满足以下要求:
GDT和IDT的地址不应固定为特定的物理或线性地址。
该解决方案至少应能够使用ELF对象和ELF可执行文件。如果它适用于其他格式,那就更好了!
解决方案是否是构建最终可执行文件/二进制文件的过程的一部分都没有关系。如果解决方案需要在生成可执行文件/二进制文件后进行构建时间处理,那也是可以接受的。
GDT(或IDT)在加载到内存后需要显示为完全解析。解决方案必须不需要运行时修复程序。
无效的示例代码
我以传统bootloader1的形式提供了一些示例代码,该代码试图在组装时创建静态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加载,因此在组装时可以针对这种情况提供其他特定解决方案。这种特定的解决方案打破了OS开发中更普遍的用例,在这种情况下,开发人员通常不希望将IDT或GDT地址硬编码为特定的线性/物理地址,因为最好让链接器为它们做。
最佳答案
我最常用的一种解决方案是实际使用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 preprocessor(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
现在,您只需要在链接脚本中包含
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
),该链接表脚本中将使用该表来构建与该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记录。在禁用A20的情况下,处理器被置于32位保护状态。演示中的所有代码都位于物理地址0x100000(1MiB)下,因此不需要启用A20。
用
lidt
加载IDT记录。使用
ltr
将TSS选择器加载到任务寄存器中。调用每个异常处理程序(
exc0
,exc1
,exc2
和exc3
)。每个异常处理程序在显示屏的左上角打印一个数字(0、1、2、3)。
如果它在BOCHS中正确运行,则输出应如下所示: