[x86] #GP when setting GDT Base != 0 and Limit != 0xFFFFF

Question about which tools to use, bugs, the best way to implement a function, etc should go here. Don't forget to see if your question is answered in the wiki first! When in doubt post here.
Post Reply
krjdev
Posts: 7
Joined: Tue Jan 28, 2020 12:11 am
Location: Graz - Austria

[x86] #GP when setting GDT Base != 0 and Limit != 0xFFFFF

Post by krjdev »

Hi,

I'm a newbie in x86 architecture. Have read several tutorials for setting the GDT, also on this site.
Get always a General Protection Fault (Error code 0), when I try to set the Kernel code segment at the start of the .text section
and Kernel data segment to .data, .bss., etc. No issues when I use 0 for base and 0xFFFFF for limit

Here is my function for setting the entries:

Code: Select all

static void gdt_set_entry(struct gdt_entry *entry, 
                          uint32_t base, 
                          uint32_t limit, 
                          uint8_t access, 
                          uint8_t flags)
{
    if (!entry)
        return;
    
    entry->lo = (limit & 0xFFFF);
    entry->lo |= ((base & 0xFFFF) << 16);
    entry->hi = ((base & 0xFF0000) >> 16);
    entry->hi |= ((uint32_t) access << 8);
    entry->hi |= (limit & 0xF0000);
    entry->hi |= (((uint32_t) flags & 0xF) << 20);
    entry->hi |= (base & 0xFF000000);
}
Defines:

Code: Select all

#define GDT_ACCESS_RW           0x02
#define GDT_ACCESS_DIRECTION    0x04
#define GDT_ACCESS_EXEC         0x08
#define GDT_ACCESS_SEGMENT      0x10
#define GDT_ACCESS_RING0        0x00
#define GDT_ACCESS_RING1        0x20
#define GDT_ACCESS_RING2        0x40
#define GDT_ACCESS_RING3        0x60
#define GDT_ACCESS_PRESENT      0x80
#define GDT_FLAG_SIZE           0x04
#define GDT_FLAG_GRAN           0x08

#define GDT_ENTRIES             5
The GDT structs

Code: Select all

struct gdt_entry {
    uint32_t lo;
    uint32_t hi;
};

struct gdt_ptr {
    uint16_t limit;
    uint32_t ptr;
} __attribute__((packed));
/* The working entries */

Code: Select all

static void gdt_init(void)
{
    /* NULL entry */
    gdt_set_entry(&gdt[0], 0, 0, 0, 0);
    
    /* Kernel Code */
    gdt_set_entry(&gdt[1], 0, 0xFFFFF, GDT_ACCESS_EXEC | 
                                       GDT_ACCESS_SEGMENT | 
                                       GDT_ACCESS_RING0 | 
                                       GDT_ACCESS_PRESENT, 
                                       GDT_FLAG_SIZE | 
                                       GDT_FLAG_GRAN);
    
    /* Kernel Data */
    gdt_set_entry(&gdt[2], 0, 0xFFFFF, GDT_ACCESS_RW | 
                                       GDT_ACCESS_SEGMENT | 
                                       GDT_ACCESS_RING0 | 
                                       GDT_ACCESS_PRESENT, 
                                       GDT_FLAG_SIZE | 
                                       GDT_FLAG_GRAN);
    
    /* User Code */
    gdt_set_entry(&gdt[3], 0, 0xFFFFF, GDT_ACCESS_EXEC | 
                                        GDT_ACCESS_SEGMENT | 
                                        GDT_ACCESS_RING3 | 
                                        GDT_ACCESS_PRESENT, 
                                        GDT_FLAG_SIZE | 
                                        GDT_FLAG_GRAN);
    
    /* User Data */
    gdt_set_entry(&gdt[4], 0, 0xFFFFF, GDT_ACCESS_RW | 
                                       GDT_ACCESS_SEGMENT | 
                                       GDT_ACCESS_RING3 | 
                                       GDT_ACCESS_PRESENT, 
                                       GDT_FLAG_SIZE | 
                                       GDT_FLAG_GRAN);
    
    gdt_p.ptr = (uint32_t) &gdt[0];
    gdt_p.limit = ((sizeof(struct gdt_entry) * GDT_ENTRIES) - 1);
    _cpu_gdt_flush((uint32_t) &gdt_p, ((uint32_t )&gdt[2].lo - (uint32_t)&gdt[0].lo));
}
The _cpu_gdt_flush Function:

Code: Select all

FUNCTION(_cpu_gdt_flush)
    pushl %ebp
    movl %esp, %ebp
    movl 8(%esp), %eax
    lgdt (%eax)
    movl 12(%esp), %eax
    movw %ax, %es
    movw %ax, %fs
    movw %ax, %gs
    movw %ax, %ss
    ljmp $0x8, $.1 
.1:
    leave
    ret
The #GP occurs in the ljmp instruction.

Kernel Sections:
.text 0x100000 Size 0x5274
.rodata 0x106000 Size 0x42e
.bss 0x107000 Size 0xcbec
.stack 0x115000 Size 0x4000

Tried it with the following values:
Base 0x100000 Limit: 5 for the code segment
Base 0x106000 Limit: 18 for the data segment

Can somebody explain me please, what I'm doing wrong? Maybe I have misunderstood something.

Edit:

Corrected some typos.
Last edited by krjdev on Tue Jan 28, 2020 11:12 pm, edited 1 time in total.

Code: Select all

/* Beware of bugs in the above code;
I have only proved it correct, not tried it. */
Original quote by Donald E. Knuth
Octocontrabass
Member
Member
Posts: 5578
Joined: Mon Mar 25, 2013 7:01 pm

Re: [x86] #GP when setting GDT Base != 0 and Limit != 0xFFFF

Post by Octocontrabass »

Setting the segment base to a non-zero address causes address translation. For example, when the base is 0x100000, then virtual address 0 (in your program) is linear address 0x100000 (in your page tables, or in physical memory if you're not using paging).

Your ljmp instruction has a destination somewhere around 0x100000. That destination is much higher than the segment limit, so you get a general protection fault. But even if you extend the limit, it still won't work: your code segment translates the destination address from somewhere near 0x100000 to somewhere near 0x200000.

It's possible to fix your ljmp so that it reaches the correct destination, but things still will not work because your compiler assumes CS, DS, ES, and SS all have the same base address. It can't handle your different code and data segment base addresses, and it can't handle you changing the segment base partway through.

What are you trying to do with segmentation? You probably want to use paging instead.
krjdev
Posts: 7
Joined: Tue Jan 28, 2020 12:11 am
Location: Graz - Austria

Re: [x86] #GP when setting GDT Base != 0 and Limit != 0xFFFF

Post by krjdev »

Octocontrabass wrote:[...]
Your ljmp instruction has a destination somewhere around 0x100000. That destination is much higher than the segment limit, so you get a general protection fault. But even if you extend the limit, it still won't work: your code segment translates the destination address from somewhere near 0x100000 to somewhere near 0x200000.

It's possible to fix your ljmp so that it reaches the correct destination, but things still will not work because your compiler assumes CS, DS, ES, and SS all have the same base address. It can't handle your different code and data segment base addresses, and it can't handle you changing the segment base partway through.
Okay, thank you.

I have disassembled the code:

Code: Select all

0010002e <_cpu_gdt_flush>:
  10002e:       55                      push   %ebp
  10002f:       89 e5                   mov    %esp,%ebp
  100031:       8b 44 24 08             mov    0x8(%esp),%eax
  100035:       0f 01 10                lgdtl  (%eax)
  100038:       8b 44 24 0c             mov    0xc(%esp),%eax
  10003c:       8e c0                   mov    %eax,%es
  10003e:       8e e0                   mov    %eax,%fs
  100040:       8e e8                   mov    %eax,%gs
  100042:       8e d0                   mov    %eax,%ss
  100044:       ea 4b 00 10 00 08 00    ljmp   $0x8,$0x10004b

0010004b <.1>:
  10004b:       c9                      leave  
  10004c:       c3                      ret
Although I understand your explanation, the destination of the ljmp instruction should be 0x4b and not 0x10004b.
Correct me please, if I'm wrong.
What are you trying to do with segmentation? You probably want to use paging instead.
Was actually an experiment for the purpose of learning. I thought, it's a additional protection.

Yes, I will use paging instead.
Last edited by krjdev on Wed Jan 29, 2020 5:24 am, edited 1 time in total.

Code: Select all

/* Beware of bugs in the above code;
I have only proved it correct, not tried it. */
Original quote by Donald E. Knuth
Octocontrabass
Member
Member
Posts: 5578
Joined: Mon Mar 25, 2013 7:01 pm

Re: [x86] #GP when setting GDT Base != 0 and Limit != 0xFFFF

Post by Octocontrabass »

krjdev wrote:Although I understand your explanation, the destination of the ljmp instruction should be 0x4b and not 0x10004b.
That's correct.
krjdev wrote:
What are you trying to do with segmentation? You probably want to use paging instead.
Was actually a experiment for the purpose of learning. I thought, it's a additional protection.
There is at least one case where it can provide additional protection. OpenBSD uses the CS segment limit to prevent executing writable data on CPUs that lack the no-execute flag.
alexfru
Member
Member
Posts: 1112
Joined: Tue Mar 04, 2014 5:27 am

Re: [x86] #GP when setting GDT Base != 0 and Limit != 0xFFFF

Post by alexfru »

Post Reply