在 assemble/compile/link 时间构建静态 IDT 和 GDT 所需的解决方案
Solution needed for building a static IDT and GDT at assemble/compile/link time
这个问题的灵感来自多年来遇到的一个问题,尤其是在 x86 操作系统开发中。最近 related NASM question 被编辑提升了。在那种情况下,此人使用 NASM 并收到 assemble 时间错误:
shift operator may only be applied to scalar values
另一个 询问在编译时生成静态 IDT 时 GCC 代码的问题导致错误:
initializer element is not constant
在这两种情况下,问题都与以下事实有关:IDT 条目需要异常处理程序的地址,而 GDT 可能需要另一个结构(如任务段结构 (TSS))的基地址。通常这不是问题,因为链接过程可以通过重定位修复来解析这些地址。在 IDT entry or GDT Entry, the fields split up the base/function addresses. There are no relocation types that can tell a linker to shift bits around and then place them in memory the way they are laid out in a GDT/IDT entry. Peter Cordes has written a good explanation of that in .
的情况下
我的问题不是问问题是什么,而是请求功能和实用解决方案的问题。尽管我是在自我回答这个问题,但这只是众多可能解决方案中的一种。我只要求提出的解决方案满足这些要求:
- GDT 和 IDT 的地址不应固定为特定的物理地址或线性地址。
- 该解决方案至少应该能够使用 ELF 对象和 ELF 可执行文件。如果它适用于其他格式,那就更好了!
- 解决方案是否是构建最终 executable/binary 过程的一部分并不重要。如果解决方案需要在生成 executable/binary 之后进行构建时间处理,那也是可以接受的。
- GDT(或 IDT)在加载到内存中时需要显示为已完全解析。解决方案不得需要 运行 次修正。
不起作用的示例代码
我以遗留引导加载程序1 的形式提供了一些示例代码,它试图在汇编时创建静态 IDT 和 GDT,但在 assembled 与 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 地址硬编码到特定的 linear/physical 地址,因为最好让链接器做对他们来说。
我最常用的一个解决方案是实际使用 GNU linker (ld
) 为我构建 IDT 和 GDT。这个答案不是编写 GNU linker 脚本的入门书,但它确实使用了 BYTE
、SHORT
和 LONG
linker 脚本用于构建 IDT、GDT、IDT 记录和 GDT 记录的指令。 linker 可以使用涉及 <<
、>>
、&
、|
等的表达式,并在符号的虚拟内存地址 (VMA) 上执行这些操作最终解决。
问题是 linker 脚本相当愚蠢。他们没有宏语言,因此您最终不得不像这样编写 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
作为代码段。 linker 被告知在构建 GDT 时计算选择器编号。显然,随着 GDT 尤其是 IDT 的增长,这非常混乱并且变得更加笨拙。
您可以使用像 m4
to simplify things, but I prefer to use the C preprocessor (cpp
) 这样的宏处理器,因为很多开发人员都熟悉它。尽管 C 预处理器通常用于预处理 C/C++ 文件,但并不限于这些文件。您可以在任何类型的文本文件上使用它,包括 linker 脚本。
您可以创建一个宏文件并定义一对宏,如 MAKE_IDT_DESC
和 MAKE_GDT_DESC
来创建 GDT 和 IDT 描述符条目。我使用扩展命名约定,其中 ldh
代表(链接头),但您可以随意命名这些文件:
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
为了减少主要 linker 脚本中的混乱,您可以创建另一个构建 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
包含在 linker 脚本中您想要放置结构的点(在一个部分内):
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);
}
}
这个 linker 脚本是我用于引导扇区的典型脚本,但我所做的只是包含 gdtidt.ldh
文件以允许 linker 生成结构。唯一剩下要做的就是预处理 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 table (tss_entry
),它在 linker 脚本中用于构建与该 TSS 关联的 GDT 条目。
预处理 linker 脚本; assemble; link;并生成一个作为引导扇区的二进制文件,可以使用以下命令:
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。
- 加载 IDT 记录
lidt
。
- 使用
ltr
将 TSS 选择器加载到任务寄存器中。
- 调用每个异常处理程序(
exc0
、exc1
、exc2
和 exc3
)。
- 每个异常处理程序都会在显示屏的左上角打印一个数字 (0, 1, 2, 3)。
如果它 运行 在 BOCHS 中正确,输出应该如下所示:
这个问题的灵感来自多年来遇到的一个问题,尤其是在 x86 操作系统开发中。最近 related NASM question 被编辑提升了。在那种情况下,此人使用 NASM 并收到 assemble 时间错误:
shift operator may only be applied to scalar values
另一个
initializer element is not constant
在这两种情况下,问题都与以下事实有关:IDT 条目需要异常处理程序的地址,而 GDT 可能需要另一个结构(如任务段结构 (TSS))的基地址。通常这不是问题,因为链接过程可以通过重定位修复来解析这些地址。在 IDT entry or GDT Entry, the fields split up the base/function addresses. There are no relocation types that can tell a linker to shift bits around and then place them in memory the way they are laid out in a GDT/IDT entry. Peter Cordes has written a good explanation of that in
我的问题不是问问题是什么,而是请求功能和实用解决方案的问题。尽管我是在自我回答这个问题,但这只是众多可能解决方案中的一种。我只要求提出的解决方案满足这些要求:
- GDT 和 IDT 的地址不应固定为特定的物理地址或线性地址。
- 该解决方案至少应该能够使用 ELF 对象和 ELF 可执行文件。如果它适用于其他格式,那就更好了!
- 解决方案是否是构建最终 executable/binary 过程的一部分并不重要。如果解决方案需要在生成 executable/binary 之后进行构建时间处理,那也是可以接受的。
- GDT(或 IDT)在加载到内存中时需要显示为已完全解析。解决方案不得需要 运行 次修正。
不起作用的示例代码
我以遗留引导加载程序1 的形式提供了一些示例代码,它试图在汇编时创建静态 IDT 和 GDT,但在 assembled 与 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 地址硬编码到特定的 linear/physical 地址,因为最好让链接器做对他们来说。
我最常用的一个解决方案是实际使用 GNU linker (ld
) 为我构建 IDT 和 GDT。这个答案不是编写 GNU linker 脚本的入门书,但它确实使用了 BYTE
、SHORT
和 LONG
linker 脚本用于构建 IDT、GDT、IDT 记录和 GDT 记录的指令。 linker 可以使用涉及 <<
、>>
、&
、|
等的表达式,并在符号的虚拟内存地址 (VMA) 上执行这些操作最终解决。
问题是 linker 脚本相当愚蠢。他们没有宏语言,因此您最终不得不像这样编写 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
作为代码段。 linker 被告知在构建 GDT 时计算选择器编号。显然,随着 GDT 尤其是 IDT 的增长,这非常混乱并且变得更加笨拙。
您可以使用像 m4
to simplify things, but I prefer to use the C preprocessor (cpp
) 这样的宏处理器,因为很多开发人员都熟悉它。尽管 C 预处理器通常用于预处理 C/C++ 文件,但并不限于这些文件。您可以在任何类型的文本文件上使用它,包括 linker 脚本。
您可以创建一个宏文件并定义一对宏,如 MAKE_IDT_DESC
和 MAKE_GDT_DESC
来创建 GDT 和 IDT 描述符条目。我使用扩展命名约定,其中 ldh
代表(链接头),但您可以随意命名这些文件:
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
为了减少主要 linker 脚本中的混乱,您可以创建另一个构建 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
包含在 linker 脚本中您想要放置结构的点(在一个部分内):
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);
}
}
这个 linker 脚本是我用于引导扇区的典型脚本,但我所做的只是包含 gdtidt.ldh
文件以允许 linker 生成结构。唯一剩下要做的就是预处理 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 table (tss_entry
),它在 linker 脚本中用于构建与该 TSS 关联的 GDT 条目。
预处理 linker 脚本; assemble; link;并生成一个作为引导扇区的二进制文件,可以使用以下命令:
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。
- 加载 IDT 记录
lidt
。 - 使用
ltr
将 TSS 选择器加载到任务寄存器中。 - 调用每个异常处理程序(
exc0
、exc1
、exc2
和exc3
)。 - 每个异常处理程序都会在显示屏的左上角打印一个数字 (0, 1, 2, 3)。
如果它 运行 在 BOCHS 中正确,输出应该如下所示: