Setting up the GDT in c using gcc.

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.
User avatar
bloodline
Member
Member
Posts: 264
Joined: Tue Sep 15, 2020 8:07 am
Location: London, UK

Setting up the GDT in c using gcc.

Post by bloodline »

Firstly I just want to say x86 assembler and associated architecture is an abomination... The world should have let it die.

Ok, So I've just wasted a day trawling through examples of setting up a Global Descriptor Table, now the structure of the table is a mess, but easy enough to build if you follow the docs, the trouble begins when you want to give this information to the CPU.

The sample code seem to vary between AT&T and intel syntax (and various different assemblers) and, honestly I find it a bit confusing... my only real clue which it being used is when I see a operand I think should be a destination being used as a source. I'm a 68k guy, so AT&T syntax wins for me.

Anyway, enough ranting. Here is the inline x86 for gcc so others can use it:

Code: Select all


    uint32_t ptr = (uint32_t)&gdt_ptr; // get the pointer to your GDT table as a 32bit int
    
    asm volatile("movl %0,%%eax    \n\t"  //load the pointer int eax
                 "lgdt (%%eax)     \n\t"            //Set the CPU GDT pointer to your table
                 
                 
                 "movl %%cr0,%%eax \n\t"   // Get the value of the cr0 register
                 "or    $0x1, %%al \n\t"       // set PE (Protection Enable) bit in CR0 (Control Register 0)
                 "movl %%eax, %%cr0\n\t"   // reload the cr0 register with the PE bit set.
                 
                 //Segments confuse the hell out of me so hopefully this code will make them go away
                 //16 being the offset to the supervisor Data segment in my GDT
                 "movw $0x10, %%ax \n\t"  // load 16 into the ax register
                 "movw  %%ax, %%ds \n\t"   // copy 16  into all of the segment registers
                 "movw  %%ax, %%es \n\t"
                 "movw  %%ax, %%fs \n\t"
                 "movw  %%ax, %%gs \n\t"
                 "movw  %%ax, %%ss \n\t"
                 "ljmp $0x08,$boing\n\t"   // apparently this loads the CS segment register with 8 
                 // 8 being the offset to the Supervisor code segment in my GDT
                 "boing:" : : "r" (ptr) : "eax"); // tells gcc that %0 = ptr and that I use eax as a temp
}
Now to do the same with the interrupt descriptor table... probably tomorrow, as I've wasted too much time today.
Last edited by bloodline on Sat Sep 19, 2020 2:40 pm, edited 1 time in total.
CuriOS: A single address space GUI based operating system built upon a fairly pure Microkernel/Nanokernel. Download latest bootable x86 Disk Image: https://github.com/h5n1xp/CuriOS/blob/main/disk.img.zip
Discord:https://discord.gg/zn2vV2Su
nexos
Member
Member
Posts: 1081
Joined: Tue Feb 18, 2020 3:29 pm
Libera.chat IRC: nexos

Re: Setting up the GDT in c using gcc.

Post by nexos »

A couple things:
1. Use a separate assembler. Inline ASM is a pain to get right. Use an external file.
2. You don't need to set the PE bit. If you already in Pmode, you just load the GDT pointer with lgdt, put the segments in the segment registers, and far jump to set CS.
3. Segmentation isn't that bad. Basically, the segment registers contains an offset into the GDT. The entry at this offset contains the base of the segment. All memory accesses are relevant to this base, and they can't surpass the limit. IMO segmentation is more secure then paging, and I think it is too bad it didn't catch.
"How did you do this?"
"It's very simple — you read the protocol and write the code." - Bill Joy
Projects: NexNix | libnex | nnpkg
User avatar
bloodline
Member
Member
Posts: 264
Joined: Tue Sep 15, 2020 8:07 am
Location: London, UK

Re: Setting up the GDT in c using gcc.

Post by bloodline »

nexos wrote:A couple things:
1. Use a separate assembler. Inline ASM is a pain to get right. Use an external file.
Indeed it is! But I find having the ASM inline with my C code much easier to fit with my workflow (I feel I have a bit more control over the c/asm interaction, I freely admit this might be illusionary :D ), and others might too... If they do then hopefully they will find my example useful!
2. You don't need to set the PE bit. If you already in Pmode, you just load the GDT pointer with lgdt, put the segments in the segment registers, and far jump to set CS.
I wasn’t 100% sure about this so I appreciate the advice here... I’m using Qemu’s -kernel option to test my builds... Since I’m not sure what state the emulated hardware is in I figured that it wouldn’t hurt to ensure the PE bit is set...
3. Segmentation isn't that bad. Basically, the segment registers contains an offset into the GDT. The entry at this offset contains the base of the segment. All memory accesses are relevant to this base, and they can't surpass the limit. IMO segmentation is more secure then paging, and I think it is too bad it didn't catch.
Yeah, I can see how they are supposed to work conceptually, but it does seem less flexible than paging... so you lose a lot of features.
CuriOS: A single address space GUI based operating system built upon a fairly pure Microkernel/Nanokernel. Download latest bootable x86 Disk Image: https://github.com/h5n1xp/CuriOS/blob/main/disk.img.zip
Discord:https://discord.gg/zn2vV2Su
nexos
Member
Member
Posts: 1081
Joined: Tue Feb 18, 2020 3:29 pm
Libera.chat IRC: nexos

Re: Setting up the GDT in c using gcc.

Post by nexos »

bloodline wrote:(I feel I have a bit more control over the c/asm interaction, I freely admit this might be illusionary :D )
Kind of, but not really. You can use global and external symbols (in NASM) and then that gives you good interaction.
"How did you do this?"
"It's very simple — you read the protocol and write the code." - Bill Joy
Projects: NexNix | libnex | nnpkg
nexos
Member
Member
Posts: 1081
Joined: Tue Feb 18, 2020 3:29 pm
Libera.chat IRC: nexos

Re: Setting up the GDT in c using gcc.

Post by nexos »

bloodline wrote:Since I’m not sure what state the emulated hardware is in I figured that it wouldn’t hurt to ensure the PE bit is set...
I thinks it is the same as what is specified by the Multiboot standard. It can be found here.
"How did you do this?"
"It's very simple — you read the protocol and write the code." - Bill Joy
Projects: NexNix | libnex | nnpkg
User avatar
bloodline
Member
Member
Posts: 264
Joined: Tue Sep 15, 2020 8:07 am
Location: London, UK

Re: Setting up the GDT in c using gcc.

Post by bloodline »

nexos wrote:
bloodline wrote:Since I’m not sure what state the emulated hardware is in I figured that it wouldn’t hurt to ensure the PE bit is set...
I thinks it is the same as what is specified by the Multiboot standard. It can be found here.
It would make sense for the -kernel option to do that.

Great link, Section 3.2 give the machine state! I’m going to need to examine this when I want to get VBE working... that’s a way off yet :D
CuriOS: A single address space GUI based operating system built upon a fairly pure Microkernel/Nanokernel. Download latest bootable x86 Disk Image: https://github.com/h5n1xp/CuriOS/blob/main/disk.img.zip
Discord:https://discord.gg/zn2vV2Su
User avatar
iansjack
Member
Member
Posts: 4811
Joined: Sat Mar 31, 2012 3:07 am
Location: Chichester, UK

Re: Setting up the GDT in c using gcc.

Post by iansjack »

You shouldn't use the same segment selector for code, data, and stack. (Sorry - my mistake. You are using different selectors for code and data.)

Large amounts of inline assembler are incredibly confusing and to be avoided.
Last edited by iansjack on Sat Sep 19, 2020 11:26 am, edited 2 times in total.
nexos
Member
Member
Posts: 1081
Joined: Tue Feb 18, 2020 3:29 pm
Libera.chat IRC: nexos

Re: Setting up the GDT in c using gcc.

Post by nexos »

bloodline wrote:Great link, Section 3.2 give the machine state! I’m going to need to examine this when I want to get VBE working... that’s a way off yet
qemu -kernel doesn't support VBE. You have to use native GRUB for that (unfortunately).
"How did you do this?"
"It's very simple — you read the protocol and write the code." - Bill Joy
Projects: NexNix | libnex | nnpkg
User avatar
bloodline
Member
Member
Posts: 264
Joined: Tue Sep 15, 2020 8:07 am
Location: London, UK

Re: Setting up the GDT in c using gcc.

Post by bloodline »

iansjack wrote:You shouldn't use the same segment selector for code, data, and stack.
Yeah, I really have no idea what I’m doing with this memory stuff, all I need for now is a nice flat 32bit flat address space (as I’m used to with the 68k). Is there a better way to do this?
Large amounts of inline assembler are incredibly confusing and to be avoided.
I don’t intend having too much asm, just using it to set up the bizarre x86 registers and manipulate the stack.
CuriOS: A single address space GUI based operating system built upon a fairly pure Microkernel/Nanokernel. Download latest bootable x86 Disk Image: https://github.com/h5n1xp/CuriOS/blob/main/disk.img.zip
Discord:https://discord.gg/zn2vV2Su
nexos
Member
Member
Posts: 1081
Joined: Tue Feb 18, 2020 3:29 pm
Libera.chat IRC: nexos

Re: Setting up the GDT in c using gcc.

Post by nexos »

bloodline wrote:I don’t intend having too much asm, just using it to set up the bizarre x86 registers and manipulate the stack.
You're going to need inline ASM for ISR handler stubs and context switching. Those must be put in there own files.
"How did you do this?"
"It's very simple — you read the protocol and write the code." - Bill Joy
Projects: NexNix | libnex | nnpkg
User avatar
bloodline
Member
Member
Posts: 264
Joined: Tue Sep 15, 2020 8:07 am
Location: London, UK

Re: Setting up the GDT in c using gcc.

Post by bloodline »

nexos wrote:
bloodline wrote:Great link, Section 3.2 give the machine state! I’m going to need to examine this when I want to get VBE working... that’s a way off yet
qemu -kernel doesn't support VBE. You have to use native GRUB for that (unfortunately).
I’m a VERY long way from there :) , qemu -kenel loader is fantastic for rapid testing at this early stage!
CuriOS: A single address space GUI based operating system built upon a fairly pure Microkernel/Nanokernel. Download latest bootable x86 Disk Image: https://github.com/h5n1xp/CuriOS/blob/main/disk.img.zip
Discord:https://discord.gg/zn2vV2Su
User avatar
bloodline
Member
Member
Posts: 264
Joined: Tue Sep 15, 2020 8:07 am
Location: London, UK

Re: Setting up the GDT in c using gcc.

Post by bloodline »

nexos wrote:
bloodline wrote:I don’t intend having too much asm, just using it to set up the bizarre x86 registers and manipulate the stack.
You're going to need inline ASM for ISR handler stubs and context switching. Those must be put in there own files.
Interrupts are next (now I apparently have something resembling a valid memory layout), so expect some stupid questions as I confuse myself ;)
CuriOS: A single address space GUI based operating system built upon a fairly pure Microkernel/Nanokernel. Download latest bootable x86 Disk Image: https://github.com/h5n1xp/CuriOS/blob/main/disk.img.zip
Discord:https://discord.gg/zn2vV2Su
User avatar
bloodline
Member
Member
Posts: 264
Joined: Tue Sep 15, 2020 8:07 am
Location: London, UK

Re: Setting up the GDT in c using gcc.

Post by bloodline »

iansjack wrote:You shouldn't use the same segment selector for code, data, and stack. (Sorry - my mistake. You are using different selectors for code and data.)
I'm glad you pointed this out (even though you were mistaken), as it make me go back over and check I did actually understand what is going on.

So I what have set up is a 5 entry GDT:

0: Null;
1: Supervisor Code;
2: Supervisor Data;
3: User Code;
4: User Data;

The code above loads the third table entry (Supervisor Data - offset 16) into the Segment Registers ds, es, fs, gs, and ss, then the jump instruction pushes the second entry (Supervisor Code - offset 8 ) into the cs register.

I'm assuming that at some point, I'm going to load the segment registers ds, es, fs, gs, and ss with the value 32, and jump into my user space code giving it an offset of 24... and I assume enter ring 3.

The question then will be how the x86 keeps its Supervisor stack and User stack separate (no A7 register here #-o ).
CuriOS: A single address space GUI based operating system built upon a fairly pure Microkernel/Nanokernel. Download latest bootable x86 Disk Image: https://github.com/h5n1xp/CuriOS/blob/main/disk.img.zip
Discord:https://discord.gg/zn2vV2Su
Octocontrabass
Member
Member
Posts: 5873
Joined: Mon Mar 25, 2013 7:01 pm

Re: Setting up the GDT in c using gcc.

Post by Octocontrabass »

bloodline wrote:Firstly I just want to say x86 assembler and associated architecture is an abomination... The world should have let it die.
Agreed. Unfortunately, the 68000 wasn't ready in time for IBM to use it in the PC.
bloodline wrote:Anyway, enough ranting. Here is the inline x86 for gcc so others can use it:
I like optimizing inline assembly, so here's my take:

Code: Select all

    asm volatile("lgdtl %0         \n\t"
                 "ljmp $0x08,$.L%= \n\t"
                 ".L%=:            \n\t"
                 "mov %1, %%ds     \n\t"
                 "mov %1, %%es     \n\t"
                 "mov %1, %%fs     \n\t"
                 "mov %1, %%gs     \n\t"
                 "mov %1, %%ss     \n\t" :: "m"(gdtr), "r"(0x10) );
The "r" constraint on the second operand could actually be "rm", but I can't think of any situation where the compiler might generate better code that way.
bloodline wrote:I'm assuming that at some point, I'm going to load the segment registers ds, es, fs, gs, and ss with the value 32, and jump into my user space code giving it an offset of 24... and I assume enter ring 3.
Supervisor code often runs due to user space code calling it, so jumping to user space for the first time is usually handled as a "return" to user space, even though you weren't called by user space.

Also, you can't load a user segment selector into SS while you're in supervisor mode. That's handled by whichever instruction you use to "return" to user mode. (Most examples use IRET, since it's available on all x86 CPUs, but it's also possible to use SYSEXIT or SYSRET if your CPU supports it.)
bloodline wrote:The question then will be how the x86 keeps its Supervisor stack and User stack separate (no A7 register here #-o ).
That's handled by the TSS, which is another very distinctly x86 mechanism. Fortunately, you don't have to populate the entire TSS or use it for task-switching; you only need a handful of fields so the CPU can find the supervisor stack when it's running in user mode.
User avatar
bloodline
Member
Member
Posts: 264
Joined: Tue Sep 15, 2020 8:07 am
Location: London, UK

Re: Setting up the GDT in c using gcc.

Post by bloodline »

Octocontrabass wrote:
bloodline wrote:Firstly I just want to say x86 assembler and associated architecture is an abomination... The world should have let it die.
Agreed. Unfortunately, the 68000 wasn't ready in time for IBM to use it in the PC.
Oops, I was in a particularly bad mood when I wrote that (I stand by what I said though)! I don't have much free time as I have two young children, so I was a little grumpy that I had spend the day just trying to get the GDT working. Though in reality, I actually learned quite a bit, its only taken me an hour or so this morning to get interrupts working (just need to read about remapping the IRQs so I can receive external ints).
bloodline wrote:Anyway, enough ranting. Here is the inline x86 for gcc so others can use it:
I like optimizing inline assembly, so here's my take:

Code: Select all

    asm volatile("lgdtl %0                 \n\t"
                                 "ljmp $0x08,$.L%= \n\t"
                                 ".L%=:                     \n\t"
                                 "mov %1, %%ds        \n\t"
                                 "mov %1, %%es        \n\t"
                                 "mov %1, %%fs        \n\t"
                                 "mov %1, %%gs       \n\t"
                                 "mov %1, %%ss       \n\t" :: "m"(gdtr), "r"(0x10) );
The "r" constraint on the second operand could actually be "rm", but I can't think of any situation where the compiler might generate better code that way.
I really like this, as it's more generalised. I think nicely optimised inline asm looks much nicer than raw x86 asm. Shouldn't the first opcode be lgdtl (%0), as we are loading a pointer? I initially had lgdtl (%0), but it caused the machine get stuck in a reset loop, but worked when I used eax as a temp reg.

Sorry if this is a rudimentary question, x86 asm is very new to me.

I attempted to build my interrupt stack frame using inline asm, but I must have made a mistake somewhere as it caused the interrupts to not return properly, so currently my int handler code is in a separate asm file... I will attempt to do it inline again when I get better at x86 asm.
bloodline wrote:I'm assuming that at some point, I'm going to load the segment registers ds, es, fs, gs, and ss with the value 32, and jump into my user space code giving it an offset of 24... and I assume enter ring 3.
Supervisor code often runs due to user space code calling it, so jumping to user space for the first time is usually handled as a "return" to user space, even though you weren't called by user space.

Also, you can't load a user segment selector into SS while you're in supervisor mode. That's handled by whichever instruction you use to "return" to user mode. (Most examples use IRET, since it's available on all x86 CPUs, but it's also possible to use SYSEXIT or SYSRET if your CPU supports it.)
Ok, I think I have a reasonable conceptual model... I'm still a little way off user mode yet, so plenty of time to understand it.
bloodline wrote:The question then will be how the x86 keeps its Supervisor stack and User stack separate (no A7 register here #-o ).
That's handled by the TSS, which is another very distinctly x86 mechanism. Fortunately, you don't have to populate the entire TSS or use it for task-switching; you only need a handful of fields so the CPU can find the supervisor stack when it's running in user mode.
Ahhhh... yeah, I've read about TSS, again task switching is a task for another day...

My roadmap:
1. Get external interrupts working so I can have keyboard and mouse;
2. Build a basic command interpreter so I can query the system as it's running (a ROMWack if you will ;) );
3. Get a memory manager working (I built a malloc implementation for use on my ARM uC boards, so I'll probably adapt that);
4. Set up a linear frame buffer, and a basic text console for output;
5. Task switching :wink:
6. File system (again I have already written one which I used on ARM uC when accessing SD Cards).
7. Move the whole lot to Long Mode (I really wanted to start in long mode, but I felt that might be trying to run before I could walk :? )
8. Do it all again for AArch64 (hopefully with a lot of code reuse), and make it work on my RaspberryPi 4 :lol:
CuriOS: A single address space GUI based operating system built upon a fairly pure Microkernel/Nanokernel. Download latest bootable x86 Disk Image: https://github.com/h5n1xp/CuriOS/blob/main/disk.img.zip
Discord:https://discord.gg/zn2vV2Su
Post Reply