用基本内核实现GDT



我最近迷上了内核开发,并从OSDev Wiki上的基本教程开始。在实现HelloWorld示例之后,我继续前进,并开始尝试创建全局描述符表。我从网上的各种来源拼凑了一些GDT的代码,但最终失败了。我在实现这一点时是否有问题,如果还不清楚,是否有任何来源可以提供更多信息?

简而言之,以下使用GDT的内核实现无法使用GRUB加载。我正在用gccas编译,可以提供任何其他需要的信息。

引导的

.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个字节长,这应该不会太难。

相关内容

  • 没有找到相关文章

最新更新