简体   繁体   中英

Implementing GDT with basic kernel

I've recently become fascinated with kernel development, and started with the bare bones tutorial on the OSDev Wiki. After implementing the Hello World example, I moved on and began attempting to create the Global Descriptor Table. From various sources online I pieced together some code for the GDT, which ultimately fails. Is there something wrong in my implementation of this, and if that is not immediately clear, is there any source that could provide more info?

In short, the following implementation of a kernel with a GDT fails to load using GRUB. I am compiling with gcc and as , can provide any other info needed.

boot.s

.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);
}

There seem to be several problems. I didn't check specific bits of your GDT entries (that's the work one must do on his own with Intel manuals in hands).

First thing (that doesn't cause problems now, but may do it in future) is that you specify and work with 4-byte wide limits, although GDT works only with 20 bits. You should change your gdt_install function to pass only 20-bit limit and document it in comments for future use. Another solution is of course to shift parameter twelve bits right, but it will make less sense and may be explained differently next time you get back to GDT management.

Second thing that doesn't seem to be correct is the way you get parameter for gdt_flush . Stack grows downwards, so the last pushed item is located on (%esp) (that's return address pushed by call instruction) and the parameter you want is located on 4(%esp) .

I assume you already are in protected mode and actual GDT is already set up by boot loader, so I can't see any obvious reason for another far jump (that consumes at least three clocks), although it isn't always true that code segment is placed directly after null segment. What I don't like on that jump is the label used as jump destination. I would recommend checking it, as it is a far jump that needs absolute value.

I know this response is quite late, but here's my answer in case anyone is still wondering.

First of all, the OSDEV GDT tutorial notes that 0x10 and 0x08 are placeholder values - they're meant to be replaced with the actual addresses of your segments. These two may work if you've written your own bootloader and are using QEMU, but if you're using GRUB then $0x10 would be for code and $0x18 for data. See here (ignore the discussion on interrupts). You could try your luck with using these, but there's no guarantee it will work 100% of the time.

What the values $0x08/$0x10 are actually meant to be referring to are the linear addresses of the segments defined in your GDT. In order to calculate these and to fix your problem (assuming the rest of the C code is correct), you will need to calculate the addresses of your segments with respect to the address of the start of your GDT, so (pseudocode):

code segment addr => &gdt[1] - &gdt
data segment addr => &gdt[2] - &gdt

If you want to implement this in C, you'll have to pass these as parameters to your gdt_flush() , and then retrieve them in assembly via the stack or registers (depending on how your compiler passes parameters). Then, you modify your gdt_flush assembly function to assign the address of 'data segment addr' to your desired segment registers and the address of 'code segment addr' as the segment for your far-jump, like so:

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

For this to work, you must also be sure that your compiler/linker loads your C and assembly code in the same segment. Honestly, the best way to implement this would be in assembly, as in the example here and here . If you still insist on using C, then you'll have to figure out how to calculate these addresses some other way - which shouldn't be too hard considering your entries are contiguous and are each 8 bytes long.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM