我最近迷上了内核开发,并从OSDev Wiki上的基本教程开始。在实现HelloWorld示例之后,我继续前进,并开始尝试创建全局描述符表。我从网上的各种来源拼凑了一些GDT的代码,但最终失败了。我在实现这一点时是否有问题,如果还不清楚,是否有任何来源可以提供更多信息?
简而言之,以下使用GDT的内核实现无法使用GRUB加载。我正在用gcc
和as
编译,可以提供任何其他需要的信息。
引导的
.section .text
.global _start
.type _start, @function
_start:
movl $stack_top, %esp
call kernel_main
cli
hlt
.Lhang:
jmp .Lhang
.size _start, . - _start
.global gdt_flush
gdt_flush:
cli
movl -4(%esp), %eax
lgdt (%eax)
movw $0x10, %ax
movw %ax, %ds
movw %ax, %es
movw %ax, %fs
movw %ax, %gs
movw %ax, %ss //the inclusion of this line or the following
jmp $0x08, $.flush //prevents the kernel from loading
.flush:
ret
.section .bootstrap_stack
stack_bottom:
.skip 16384
stack_top:
kernel.c
void kernel_main() {
gdt_install();
...
}
gdt.c
struct gdt_entry {
uint16_t limit_low;
uint16_t base_low;
uint8_t base_middle;
uint8_t access;
uint8_t granularity;
uint8_t base_high;
}__attribute__((packed));
struct gdt_ptr {
uint16_t limit;
uint32_t base;
}__attribute__((packed));
struct gdt_entry gdt[3];
struct gdt_ptr gp;
extern void gdt_flush(struct gdt_ptr *);
void gdt_set_gate(uint32_t num, uint32_t base, uint32_t limit, uint8_t access, uint8_t gran) {
gdt[num].base_low = (base & 0xFFFF);
gdt[num].base_middle = (base >> 16) & 0xFF;
gdt[num].base_high = (base >> 24) & 0xFF;
gdt[num].limit_low = (limit & 0xFFFF);
gdt[num].granularity = (limit >> 16) & 0x0F;
gdt[num].granularity |= (gran & 0x0F);
gdt[num].access = access;
}
void gdt_install() {
gp.limit = (sizeof(struct gdt_entry) * 3) - 1;
gp.base = (uint32_t) &gdt;
gdt_set_gate(0, 0, 0, 0, 0);
gdt_set_gate(1, 0, 0xFFFFFFFF, 0x9A, 0xCF);
gdt_set_gate(2, 0, 0xFFFFFFFF, 0x92, 0xCF);
gdt_flush(&gp);
}
似乎有几个问题。我没有检查你的GDT条目的特定部分(这是一个人必须自己用手中的英特尔手册完成的工作)。
第一件事(现在不会引起问题,但将来可能会发生)是指定并使用4字节宽的限制,尽管GDT只能使用20位。您应该更改gdt_install
函数以仅通过20位限制,并将其记录在注释中以备将来使用。当然,另一种解决方案是将参数右移12位,但这将没有什么意义,下次回到GDT管理时可能会有不同的解释。
第二件似乎不正确的事情是获取gdt_flush
参数的方式。堆栈向下增长,所以最后一个推送的项位于(%esp)
上(这是call
指令推送的返回地址),而您想要的参数位于4(%esp)
上。
我假设您已经处于保护模式,并且实际的GDT已经由引导加载程序设置,所以我看不出有任何明显的原因导致另一个远跳(至少需要三个时钟),尽管代码段并不总是直接放在空段之后。我不喜欢那次跳跃的是作为跳跃目的地的标签。我建议检查一下,因为这是一个需要绝对值的跳跃。
我知道这个响应已经很晚了,但这是我的答案,以防有人仍然想知道。
首先,OSDEV GDT教程注意到0x10和0x08是占位符值,它们应该替换为段的实际地址。如果您已经编写了自己的引导加载程序并使用QEMU,这两种方法可能会起作用,但如果您使用GRUB,则$0x10用于代码,$0x18用于数据。请参阅此处(忽略有关中断的讨论)。你可以用这些试试运气,但不能保证它100%有效。
值$0x08/$0x10实际上指的是GDT中定义的段的线性地址。为了计算这些并解决您的问题(假设C代码的其余部分是正确的),您需要根据GDT的起始地址计算段的地址,因此(伪代码):
code segment addr => &gdt[1] - &gdt
data segment addr => &gdt[2] - &gdt
如果您想在C中实现这一点,您必须将这些作为参数传递给gdt_flush()
,然后通过堆栈或寄存器在程序集中检索它们(取决于编译器传递参数的方式)。然后,您修改gdt_flush
汇编函数,将"数据段addr"的地址分配给所需的段寄存器,并将"代码段addr"的地址分配为您的远跳段,如下所示:
gdt_flush:
cli
movl -4(%esp), %eax
lgdt (%eax)
movw 'data segment addr', %ax
movw %ax, %ds
movw %ax, %es
movw %ax, %fs
movw %ax, %gs
movw %ax, %ss
jmp 'code segment addr', $.flush
.flush:
ret
为了实现这一点,您还必须确保编译器/链接器在同一段中加载C和汇编代码。老实说,实现这一点的最佳方式是组装,就像这里和这里的例子一样。如果你仍然坚持使用C,那么你必须想办法用其他方式计算这些地址——考虑到你的条目是连续的,每个条目都有8个字节长,这应该不会太难。