Page 1 of 2
GDT reset code seems to have no effect
Posted: Sun Jan 22, 2023 9:05 pm
by Schol-R-LEA
I have made a number of changes to Ordo, most notably to the
linker script - I now have the object files named explicitly in the script, rather than relying on the Makefile, and have added separate sections for several of the system elements, such as the GDT, IDT, and page tables. I also stubbed in a
stack_base section, for when I start remapping things in the new page tables.
Code: Select all
/* The bootloader will look at this image and start execution at the symbol
designated at the entry point. */
ENTRY(kstart)
INPUT(
obj/kernel.o
obj/terminal.o
obj/mem.o
obj/idt.o
obj/gdt.o
obj/paging.o
obj/acpi.o
)
OUTPUT(kernel.elf)
OUTPUT_FORMAT(elf32-i386)
STARTUP(obj/kstart.o)
/* Tell where the various sections of the object files will be put in the final
kernel image. */
SECTIONS
{
/* Begin putting sections at the higher half. */
. = 0xC0000000;
/* the .text section. */
.text : ALIGN(4K)
{
*(.text)
}
/* Read-only data. */
.rodata : ALIGN(4K)
{
*(.rodata)
}
/* Read-write data (initialized) */
.data : ALIGN(4K)
{
*(.data)
}
/* Read-write data (uninitialized) and stack */
.bss : ALIGN(4K)
{
*(COMMON)
*(.bss)
}
/* hardware tables */
. = 0xC0100000;
.gdt BLOCK(64K) :
{
gdt = .;
. = . + 64K;
}
.tss BLOCK(4K) : ALIGN(4K)
{
default_tss = .;
. = . + 4K;
}
.idt BLOCK(4K) : ALIGN(4K)
{
idt = .;
. = . + 4K;
}
.paging BLOCK(4K) : ALIGN(4K)
{
page_directory = .;
. = . + 4K;
page_tables = .;
. = . + (1K * 4K);
}
/* set up the stack */
.stack : ALIGN(4M)
{
stack_base = .;
*(.stack)
}
}
I expect that I've made at least a few mistakes in this, but as things stand, I have the IDT pointing to the linker-defined section and it works correctly.
When I went to
replace the stub GDT from my bootloader with one constructed by and accessible to the kernel, it worked exactly as before.
Code: Select all
void reset_gdt()
{
struct GDT_R gdt_r = { sizeof(union GDT_Entry) * MAX_GDT_ENTRIES, gdt };
union GDT_Entry *entry = gdt;
// set the null GDT entry
entry->raw_entry = 0;
// system code selector
set_gdt_entry(++entry, 0x0fffff, 0, true, true, RING_0);
// system data selector
set_gdt_entry(++entry, 0x0fffff, 0, false, true, RING_0);
// system TSS selector
set_gdt_entry(entry, (uint32_t) default_tss, sizeof(struct TSS), false, true, RING_0);
(entry++)->fields.access.non_sys = false;
// user code selector
set_gdt_entry(++entry, 0x08ffff, 0, true, true, RING_3);
// user data selector
set_gdt_entry(++entry, 0x08ffff, 0, false, true, RING_3);
// set the GDT register
__asm__ __volatile__ (
"lgdt %0"
:
: "m" (gdt_r));
}
The fact that it worked on the first go seemed suspicious to me, though, so I checked what happened when I commented out the null descriptor - and nothing changed. This leads me to think that
reset_gdt() is not, in fact, changing the GDTR.
I am not certain how to check this further. I know that there is a
SGDT instruction, and I suppose I could write a function to check the GDTR's values using that, but I am not certain just what that would accomplish.
Can anyone see anything I've missed?
Re: GDT reset code seems to have no effect
Posted: Sun Jan 22, 2023 9:22 pm
by Octocontrabass
The GDTR only tells the CPU where to look when it needs to load a segment descriptor. You won't see the effect of your new GDT until you load a segment.
Re: GDT reset code seems to have no effect
Posted: Sun Jan 22, 2023 9:41 pm
by Schol-R-LEA
Octocontrabass wrote:The GDTR only tells the CPU where to look when it needs to load a segment descriptor. You won't see the effect of your new GDT until you load a segment.
Thank you, I didn't realize that. Is this something I am likely to need to concern myself with at this point, then? I don't expect that a segment descriptor would be accessed at this stage of the kernel, at least not until I try to launch a user-mode process. Is there any way to test the state of the segment descriptors, and is it even worth being concerned about?
Except for the specific location of the GDT - and the location of the default TSS descriptor's base - the new table should be almost exactly the same as the previous one; any faults in it are probably shared by the bootloader version.
Re: GDT reset code seems to have no effect
Posted: Sun Jan 22, 2023 9:53 pm
by Octocontrabass
Aside from fast system calls, every event or instruction that loads a segment selector will also load a descriptor, even if the selector doesn't change. That includes interrupts and the IRET instruction.
Re: GDT reset code seems to have no effect
Posted: Wed Jan 25, 2023 2:06 am
by Schol-R-LEA
Returning to this topic, after identifying fixing some issues with my GDT entry structure and the code for populating them, and I have re-written my GDTR loading code as a stand-alone assembly language function, and add a function for reloading the segment selectors (based on the function given in the GDT Tutorial).
Code: Select all
void reset_gdt()
{
struct GDT_R gdt_r = { GDT_SIZE, gdt };
union GDT_Entry *entry = gdt;
// first, clear the whole table
memset(entry, 0, GDT_SIZE);
// set the null GDT entry = redundant, but still worth doing
entry->raw_entry = 0;
// system code descriptor
set_gdt_entry(++entry, 0x000fffff, 0, true, true, RING_0);
// system data descriptor
set_gdt_entry(++entry, 0x000fffff, 0, false, true, RING_0);
// system TSS descriptor
kprintf("\nDefault TSS location %p\n", &default_tss);
set_gdt_entry(++entry, sizeof(struct TSS), (uint32_t) &default_tss, true, false, RING_0);
entry->fields.access.accessed = true;
entry->fields.access.non_sys = false;
entry->fields.limit_and_flags.bits_32 = false;
entry->fields.limit_and_flags.granularity = false;
// user code descriptor
set_gdt_entry(++entry, 0x0008ffff, 0, true, true, RING_3);
// user data descriptor
set_gdt_entry(++entry, 0x0008ffff, 0, false, true, RING_3);
// set the GDT register
set_gdt(&gdt_r);
// reload_segments();
}
and
Code: Select all
global set_gdt
global reload_segments
; set_gdt(gdt_r) takes a pointer to the GDTR structure
set_gdt:
lgdt [esp + 4]
ret
%define system_code_selector (1 << 3)
%define system_data_selector (2 << 3)
; code for reload_segments() taken from
; https://wiki.osdev.org/GDT_Tutorial
reload_segments:
; Reload CS register containing code selector
; both CS and DS should be zero at this point
jmp system_code_selector:.reload_cs
.reload_cs:
; Reload data segment registers
mov ax, system_data_selector
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
ret
However, calling
reload_segments() causes the system to triple fault. I'm not sure whether the problem is with
set_gdt() or
reload_segments().
Re: GDT reset code seems to have no effect
Posted: Wed Jan 25, 2023 2:17 am
by Octocontrabass
You're loading the function parameter into the GDTR, but the function parameter is the address of the struct instead of the contents of the struct.
Is there any particular reason you decided not to use inline assembly for this instruction?
Re: GDT reset code seems to have no effect
Posted: Wed Jan 25, 2023 9:25 am
by Schol-R-LEA
I originally had been using inline assembly for it, and seemed to be having a problem with it in that regards. The previous version for this specific part was
Code: Select all
// set the GDT register
__asm__ __volatile__ (
"lgdt %0"
:
: "m" (gdt_r));
Re: GDT reset code seems to have no effect
Posted: Wed Jan 25, 2023 9:37 am
by Schol-R-LEA
OK, so that was in fact the problem. I switched back to the inline version for the LGDT instruction, and now it seems to work.
Re: GDT reset code seems to have no effect
Posted: Wed Jan 25, 2023 10:02 am
by nullplan
For the record, the out-of-line solution would have been
Code: Select all
set_gdt:
mov eax, [esp+4]
lgdt [eax]
ret
A possibly more elegant solution would have been:
Code: Select all
;void set_gdt(void *gdt, size_t gdt_size)
set_gdt:
mov eax, [esp+4]
mov cx, [esp+8]
sub esp, 8
dec cx
mov eax, [esp+4]
mov cx, [esp+2]
lgdt [esp+2]
add esp, 8
ret
Then you don't even need to define a GDTR structure in C but can just call it as
Re: GDT reset code seems to have no effect
Posted: Wed Jan 25, 2023 10:11 am
by Schol-R-LEA
OK, so a new development has arisen. I have a section of data passed from my bootloader which contains, among other things, the memory map. At the beginning of the kernel, I move that data to a new location using my version of memcpy() to a fixed location set in the linker script. The GDT and IDT are also in spaces assigned using the linker script.
I have a debugging printout function, print_mmap() which I routinely call to show that it is what I expect it to be. Now, when I call this function before I call reset_gdt() with the new data location, it works fine. It also works correctly if I pass it the old data location regardless of where I call it. However, if I call it with the new data location after calling reset_gdt(), the data is missing - it shows an entry count of zero.
I've confirmed that there is no overlap between the boot data table and the GDT, so I don't think it is a case of the boot data table being overwritten.
Furthermore, if I set up the IDT and enable interrupts following the call to reset_gdt(), the OS immediately triple faults.
While I am not certain, this seems to indicate that something isn't quite right with either the system data descriptor, or with how I am resetting the DS and/or the ES selectors. I have a function for printing the GDT entries, and the descriptors have the values I expected them to have, leading me to think that the problem is in reload_segments().
Re: GDT reset code seems to have no effect
Posted: Wed Jan 25, 2023 11:08 am
by Schol-R-LEA
Quick update: If I move the boot data after the GDT is reset, it prints out correctly.
Re: GDT reset code seems to have no effect
Posted: Wed Jan 25, 2023 11:49 am
by Octocontrabass
Sounds like memory corruption. I thought it might be a problem with memset() or memcpy(), and they are wrong (the return value should be the first argument), but I don't think that explains what's happening.
You can dump the boot data memory using either the QEMU console or GDB to see if the contents after the apparent memory corruption resembles data that belongs somewhere else.
Page tables are also in memory, and can also be responsible for data that belongs in one location appearing elsewhere. You can check them with "info tlb" and "info mem" in the QEMU console.
This is again unrelated, but is there any particular reason you're setting the ring 3 segment limits to 0x8FFFFFFF? That's an unusual value, and paging already enforces privilege separation.
Re: GDT reset code seems to have no effect
Posted: Wed Jan 25, 2023 12:21 pm
by Schol-R-LEA
Octocontrabass wrote:Sounds like memory corruption. I thought it might be a problem with memset() or memcpy(), and they are wrong (the return value should be the first argument),
OK, I fixed that, that was careless of me.
Octocontrabass wrote:You can dump the boot data memory using either the QEMU console or GDB to see if the contents after the apparent memory corruption resembles data that belongs somewhere else.
Page tables are also in memory, and can also be responsible for data that belongs in one location appearing elsewhere. You can check them with "info tlb" and "info mem" in the QEMU console.
I haven't really use the QEMU console, but I will take a look into it.
Octocontrabass wrote:This is again unrelated, but is there any particular reason you're setting the ring 3 segment limits to 0x8FFFFFFF? That's an unusual value, and paging already enforces privilege separation.
I am not certain where I got that value, and never knew the reason for it myself - I suspect it was supposed to be 0xBFFFFFFF, though as you say it shouldn't make a difference once paging is enabled. It probably was an error which I copied from some now-forgotten source.
Re: GDT reset code seems to have no effect
Posted: Wed Jan 25, 2023 4:37 pm
by Schol-R-LEA
I was having some trouble getting the memory dump from the QEMU monitor, so figuring I would need some sort of dump utility in the OS anyway, I wrote something which gives a conventional hex dump. It is a bit ugly, more due to the limitations of my
kprintf() (I'll need to address that later), but it works.
I was able to confirm that the GDT is overwriting the boot data, but now I am at a bit of a loose end in determining how and where - It may require me to simply walk through the entire thing and see where it is happening, and even then I can't be certain if I will find it.
I may need to take a step back for a bit to think this through.
Code: Select all
#define LINE_SIZE 16
void dump_line(void* src, uint8_t size)
{
uint8_t* p = (uint8_t *) src;
for (uint8_t i= 0, *b = p; i < size; i++, b++)
{
if (i == (LINE_SIZE / 2))
{
kprintf("\t");
}
if (*b < 0x10)
{
kprintf("0");
}
kprintf("%x ", *b);
}
if (size < LINE_SIZE)
{
for (uint8_t i = 0; i < (LINE_SIZE - size); i++)
{
kprintf(" ");
}
if (size < (LINE_SIZE / 2))
{
kprintf("\t");
}
}
kprintf("\t");
for (uint8_t i = 0, *b = p; i < size; i++, b++)
{
if (i == LINE_SIZE / 2)
{
kprintc('-', GREEN, BLACK);
}
kprintf("%c", (*b < 'a' ? '.' : *b));
}
kprintf("\n");
}
void memdump(void* src, uint32_t size)
{
uint8_t* p = (uint8_t *) src;
uint32_t remainder = size % LINE_SIZE;
uint32_t lines = size / LINE_SIZE;
for (uint32_t i = 0; i < lines; i++, p += LINE_SIZE)
{
dump_line(p, LINE_SIZE);
}
p += LINE_SIZE;
if (remainder != 0)
{
dump_line(p, remainder);
}
}
Re: GDT reset code seems to have no effect
Posted: Wed Jan 25, 2023 4:46 pm
by Octocontrabass
Schol-R-LEA wrote:I was able to confirm that the GDT is overwriting the boot data, but now I am at a bit of a loose end in determining how and where
Setting a watchpoint in GDB should make this very easy, assuming it isn't page table corruption.
If it is page table corruption, you should see it using "info tlb" and "info mem" in the QEMU monitor. (Oh, did I mention "-monitor stdio"? That option might make it easier to control the QEMU monitor.)