Struggling with TSS

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

Re: Struggling with TSS

Post by bloodline »

Octocontrabass wrote:
bloodline wrote:entry: 5, base: &tss, limit: sizeof(tss) , access: 0x82, granularity: 0x40
Your access byte indicates a LDT descriptor. For a 32-bit TSS descriptor, use 0x89.

Your granularity byte has an undefined bit set. Just use 0.
Ok... I've been playing with the access and granularity values all of yeasterday... nothing seems to work. The values I ended up with were based on what seemed logical from the documentation.
bloodline wrote:tss.cs = 0x1B //my user code segment + 3 (since I want to execute in ring 3)
tss.ss = 0x23 //my user data segment + 3 (since I want to execute in ring 3)
tss.ds = 0x23 //my user data segment + 3 (since I want to execute in ring 3)
tss.es = 0x23 //my user data segment + 3 (since I want to execute in ring 3)
tss.fs = 0x23 //my user data segment + 3 (since I want to execute in ring 3)
tss.gs = 0x23 //my user data segment + 3 (since I want to execute in ring 3)
You don't need to set any of these fields unless you're using hardware task switching. Hardware task switching is strongly discouraged outside of the odd cases like #DF.

You do need to set the IOPB field.
Ok, so I have reserved 4k at the end of the TSS structure, zeroed it, and then set the IOPB field to 104 (the size of the TSS structure) to point to the clear area after it... If I've understood the documentation correctly, that should give full access to the port map?
bloodline wrote: movw $0x2B, %ax
ltr %ax
Any particular reason you're setting the RPL to 3?
Because I have no idea what I'm doing and I have tried 0x28 (which seemed logical as only the supervisor mode would need to access this segment), and that didn't work.

So I've tried your suggestions, I'm still getting the GPF... but at least now I'm getting an error so I know it's a GFP!!
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: 5572
Joined: Mon Mar 25, 2013 7:01 pm

Re: Struggling with TSS

Post by Octocontrabass »

bloodline wrote:Ok, so I have reserved 4k at the end of the TSS structure, zeroed it, and then set the IOPB field to 104 (the size of the TSS structure) to point to the clear area after it... If I've understood the documentation correctly, that should give full access to the port map?
I'm not sure why you'd want to give ring 3 full access to all I/O ports, but if you do want to do that, you need 8193 bytes for the permission bitmap (an extra 0xFF byte at the end is required).
bloodline wrote:So I've tried your suggestions, I'm still getting the GPF... but at least now I'm getting an error so I know it's a GFP!!
Intel documents all of the exceptions each instruction can cause in volume 2. There are only a handful of reasons LTR could cause #GP, and of those, two of them are possible here:
  • If the source selector points to a segment that is not a TSS or to one for a task that is already busy.
  • If the selector points to LDT or is beyond the GDT limit.
User avatar
bloodline
Member
Member
Posts: 264
Joined: Tue Sep 15, 2020 8:07 am
Location: London, UK

Re: Struggling with TSS

Post by bloodline »

Octocontrabass wrote:
bloodline wrote:Ok, so I have reserved 4k at the end of the TSS structure, zeroed it, and then set the IOPB field to 104 (the size of the TSS structure) to point to the clear area after it... If I've understood the documentation correctly, that should give full access to the port map?
I'm not sure why you'd want to give ring 3 full access to all I/O ports, but if you do want to do that, you need 8193 bytes for the permission bitmap (an extra 0xFF byte at the end is required).
bloodline wrote:So I've tried your suggestions, I'm still getting the GPF... but at least now I'm getting an error so I know it's a GFP!!
Intel documents all of the exceptions each instruction can cause in volume 2. There are only a handful of reasons LTR could cause #GP, and of those, two of them are possible here:
  • If the source selector points to a segment that is not a TSS or to one for a task that is already busy.
  • If the selector points to LDT or is beyond the GDT limit.
You’ve cracked it! I actually can’t thank you enough, this was driving me mad :D

The value I loaded into lgdt was for a 5 entry GDT, it’s now 6 entries... boy do I feel stupid #-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
User avatar
bloodline
Member
Member
Posts: 264
Joined: Tue Sep 15, 2020 8:07 am
Location: London, UK

Re: Struggling with TSS

Post by bloodline »

Ok, now I have a working TSS... I'm struggling to see the point of it.

An interrupt occurs, I want:

1 Save the CPU state the the stack.
2. Save the User stack pointer somewhere.
3. The CPU to enter ring0,
4. Load the Supervisor stack pointer.
5 Restore the Supervisor state from the stack.
6. Execute some supervisor function.
7. Save the Supervisor state on the stack.*
8. Save the Supervisor stack pointer somewhere.*
9. Return to ring3.
10. Load the User stack pointer (here I might chose to load a different stack pointer, i.e. a context switch).
11. Restore the User state from the stack.
12. Continue execution, as before the interrupt.

As you can see from my earlier posts, I already have a working mechanism for this with USP and SSP variables where I can save and swap the mode stacks... but all this happens in ring0.

With a valid TSS, when an interrupt occurs, the esp0 field is used for the supervisor stack pointer... What happens to the user mode stack pointer? I see that the interrupt stack frame pushes the user esp onto the stack... ok, so I can save that value somewhere.
Now where do I put the new stack pointer, for the user mode context I wish to return to?

All this would be so much easier with two stack pointers, which is what I thought the TSS would give me.

* -edit- saving the surpervisor state isn't actually important, unless the interrupts nest...

Ok... So now, I realise the mess that is the x86, has three different types of interrupt :roll:
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: 5572
Joined: Mon Mar 25, 2013 7:01 pm

Re: Struggling with TSS

Post by Octocontrabass »

bloodline wrote:1 Save the CPU state the the stack.
2. Save the User stack pointer somewhere.
3. The CPU to enter ring0,
4. Load the Supervisor stack pointer.
When an interrupt occurs in user mode, the CPU switches to supervisor mode, loads the supervisor stack from the TSS, and pushes the user mode return address and stack pointer on the supervisor stack.
bloodline wrote:5 Restore the Supervisor state from the stack.
6. Execute some supervisor function.
7. Save the Supervisor state on the stack.*
There should be no supervisor state to restore before calling your supervisor function, and there should be no supervisor state to save afterwards. If there is, you're doing something very strange! There is user state that must be saved before you can call a supervisor function, and it must be restored afterwards (although system calls may wish to modify the saved user state).
bloodline wrote:8. Save the Supervisor stack pointer somewhere.*
If you save the user state on the supervisor stack, switching to a different supervisor stack is all that's necessary to switch to a different task. In this model, you have one supervisor stack per user thread. (This is a very common arrangement.)

Even if you choose to store the user state elsewhere, this is the point where you should perform a context switch if one is necessary.
bloodline wrote:9. Return to ring3.
10. Load the User stack pointer (here I might chose to load a different stack pointer, i.e. a context switch).
11. Restore the User state from the stack.
12. Continue execution, as before the interrupt.
The IRET instruction loads the return address from the stack and (if the return address indicates ring 3) restores the user stack pointer and switches to ring 3. That's all one instruction, so anything else you want to do with the user context has to happen before that point.
bloodline wrote:As you can see from my earlier posts, I already have a working mechanism for this with USP and SSP variables where I can save and swap the mode stacks... but all this happens in ring0.
If you're in ring 0, you don't have user stacks, they're all supervisor stacks.
bloodline wrote:With a valid TSS, when an interrupt occurs, the esp0 field is used for the supervisor stack pointer... What happens to the user mode stack pointer? I see that the interrupt stack frame pushes the user esp onto the stack... ok, so I can save that value somewhere.
Now where do I put the new stack pointer, for the user mode context I wish to return to?
You switch to another supervisor stack, which has the new user stack pointer already on it ready for you to IRET into. Unless you're going for an alien design with only one supervisor stack per CPU core.
User avatar
bloodline
Member
Member
Posts: 264
Joined: Tue Sep 15, 2020 8:07 am
Location: London, UK

Re: Struggling with TSS

Post by bloodline »

Octocontrabass wrote:
bloodline wrote:1 Save the CPU state the the stack.
2. Save the User stack pointer somewhere.
3. The CPU to enter ring0,
4. Load the Supervisor stack pointer.
When an interrupt occurs in user mode, the CPU switches to supervisor mode, loads the supervisor stack from the TSS, and pushes the user mode return address and stack pointer on the supervisor stack.
I understand now why there is a preference for per task kernel stacks now on the x86. The task state is stored on the kernel stack not the task stack. I'm slowly getting this. Appreciate your patience.
bloodline wrote:5 Restore the Supervisor state from the stack.
6. Execute some supervisor function.
7. Save the Supervisor state on the stack.*
There should be no supervisor state to restore before calling your supervisor function, and there should be no supervisor state to save afterwards. If there is, you're doing something very strange! There is user state that must be saved before you can call a supervisor function, and it must be restored afterwards (although system calls may wish to modify the saved user state).
By "save the supervisor state", I really meant clean up the stack... I was also trying to think how I could allow the interrupts to nest... but I completely agree, no supervisor state needs to be saved in normal circumstances. If you look at my earlier context switching mechanism, it should allow interrupts to preempt each other...
bloodline wrote:8. Save the Supervisor stack pointer somewhere.*
If you save the user state on the supervisor stack, switching to a different supervisor stack is all that's necessary to switch to a different task. In this model, you have one supervisor stack per user thread. (This is a very common arrangement.)

Even if you choose to store the user state elsewhere, this is the point where you should perform a context switch if one is necessary.
Ok, so working this through (generally, I know there are error codes and int numbers to be stored, etc...), using a per task kernel stack model...

Let's assume we've just entered an ordinary interrupt;
1. The CPU has loaded %esp with the value found in TSS.esp0;
2. The user mode state (eip, cs, eflags, useresp, ss) has been pushed on to the stack
3. pusha to save the cpu state.
4. Do the supervisor function.
5. popa the cpu state.
6. iret


Now assume we've just entered an interrupt which can task switch (usually timer);
1. The CPU has loaded %esp with the value found in TSS.esp0.
2. The user mode state (eip, cs, eflags, useresp, ss) has been pushed on to the stack.
3. pusha to save the cpu state.
4. Determine if a task switch needed. If NO task switch needed goto 7.
5. Load TSS.esp0 with new task kernel stack pointer.
6. Load %esp with new task kernel stack pointer.
7. popa the cpu state.
8. iret.

Is my mental model correct?
Having two stacks per task seems a little wasteful, but inconsequential on x86 systems which come with hundreds/thousands of megabytes of ram as standard.
bloodline wrote:9. Return to ring3.
10. Load the User stack pointer (here I might chose to load a different stack pointer, i.e. a context switch).
11. Restore the User state from the stack.
12. Continue execution, as before the interrupt.
The IRET instruction loads the return address from the stack and (if the return address indicates ring 3) restores the user stack pointer and switches to ring 3. That's all one instruction, so anything else you want to do with the user context has to happen before that point.
bloodline wrote:As you can see from my earlier posts, I already have a working mechanism for this with USP and SSP variables where I can save and swap the mode stacks... but all this happens in ring0.
If you're in ring 0, you don't have user stacks, they're all supervisor stacks.
Apologies for being unclear, I have been using task stack and user stack interchangeably... My terminology is incorrect when talking about x86, I will try to be clearer.
bloodline wrote:With a valid TSS, when an interrupt occurs, the esp0 field is used for the supervisor stack pointer... What happens to the user mode stack pointer? I see that the interrupt stack frame pushes the user esp onto the stack... ok, so I can save that value somewhere.
Now where do I put the new stack pointer, for the user mode context I wish to return to?
You switch to another supervisor stack, which has the new user stack pointer already on it ready for you to IRET into. Unless you're going for an alien design with only one supervisor stack per CPU core.
That is a good link. My original design was as described at the bottom of that page (writing microkernels for embedded systems is the experience I have), but I can't really think of any advantage to that design on the x86.
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: 5572
Joined: Mon Mar 25, 2013 7:01 pm

Re: Struggling with TSS

Post by Octocontrabass »

bloodline wrote:I was also trying to think how I could allow the interrupts to nest... but I completely agree, no supervisor state needs to be saved in normal circumstances. If you look at my earlier context switching mechanism, it should allow interrupts to preempt each other...
As long as your kernel stack is big enough, you don't need to do much special to nest interrupts. The only difference is that in supervisor mode, they continue using the existing stack instead of loading a new stack pointer from the TSS, so you must provide a large enough stack to allow each interrupt to save the context of whatever it interrupted. You can do that without switching to another kernel stack. (Unfortunately, the GCC instrumentation for determining the necessary stack size is not very convenient to use.)

You can prevent hardware interrupts from nesting, if you so desire, by clearing the interrupt flag. You can't prevent exceptions from nesting.

You probably shouldn't switch context if an interrupt occurs in supervisor mode. If you do, you risk causing priority inversion.
bloodline wrote:Ok, so working this through (generally, I know there are error codes and int numbers to be stored, etc...), using a per task kernel stack model...

[...]

Is my mental model correct?
Mostly. Why wouldn't you want to handle both the same way? Your timer interrupt probably needs to call a supervisor function, and your other interrupts might unblock a higher-priority task in their supervisor functions. (And if a supervisor function has been interrupted, you may want to defer the task switch until after that supervisor function completes to prevent a deadlock, even if said supervisor function normally could not cause a task switch.)
bloodline wrote:Having two stacks per task seems a little wasteful, but inconsequential on x86 systems which come with hundreds/thousands of megabytes of ram as standard.
But you don't know that there's two stacks. The user could have placed gibberish in ESP, and trying to store the user context there might crash the program or the entire OS. It's especially important for exception handlers, which can be called as a result of a program accidentally messing up its stack.
User avatar
bloodline
Member
Member
Posts: 264
Joined: Tue Sep 15, 2020 8:07 am
Location: London, UK

Re: Struggling with TSS

Post by bloodline »

Octocontrabass wrote:
bloodline wrote:Ok, so working this through (generally, I know there are error codes and int numbers to be stored, etc...), using a per task kernel stack model...

[...]

Is my mental model correct?
Mostly. Why wouldn't you want to handle both the same way? Your timer interrupt probably needs to call a supervisor function, and your other interrupts might unblock a higher-priority task in their supervisor functions. (And if a supervisor function has been interrupted, you may want to defer the task switch until after that supervisor function completes to prevent a deadlock, even if said supervisor function normally could not cause a task switch.)
I separated out the two different cases to make it clearer what is happening, in reality I have a single generalised handler.

Ok, I decided to modify the existing code (as posted earlier) to work using the TSS. This is what I have come up with:

Code: Select all


.section .bss
.align 16

.global tss             //setup space for the TSS in the bss
tss:           
.skip 4
.global tss_esp0    //Allow me to access this field in assembler
 tss_esp0:          
.skip 8
.global tss_esp1    //I use esp1 as a temp esp store
 tss_esp1:  
.skip 92

.global irq0
irq0:
    cli                   //disable interrupts for now, to keep things simple
    push $0          // save error code on stack       
    push $32       // save the int vector number on stack
    jmp irq_jump


irq_jump:
    pusha               // Save the GP regs on the stack

    movw %ds,%ax  //save the data segment pointer on the stack, as we now use different data segments 
    push %eax

    movw $0x10,%ax   //set the segments to use the kernel mode
    movw %ax,%ds
    movw %ax,%es
    movw %ax,%fs
    movw %ax,%gs

    mov $tss_esp1,%eax // save the current esp into the tss.esp1 field 
    mov %esp,(%eax)
    cld                            // SysV requires this to be clear before calling a C function.
    call irq_handler         // implemented in C
    mov $tss_esp1,%eax // restore the esp from the tss.esp1 field, if the irq_handler didn't change the tss.esp1 field, then this does nothing.
    mov (%eax),%esp

     pop %ebx               // restore the data segment pointer
    movw %bx,%ds
    movw %bx,%es
    movw %bx,%fs
    movw %bx,%gs

    popa                         // restore the CPU registers
    add $8, %esp             // clean up the stack

    sti
    iret

/* ============================================ */
// Puedo C code

void irq_handler(){

//if a task switch needs to happen, then save the tss.esp1 into the ssp field of the task control block
//
// select the next task, load the task_control_block->ssp_top into tss.esp0, load tss.esp1 with the ssp from the next task control block saved during the last task switch.
// now when the handler returns we are running a different context.

}

This code works... I hope I'm not violating some some arcane spec by repurposing esp1, but pleasingly my tasks do now run in ring 3!

-edit- I now have a low priority idle task running In ring zero (surprisingly easy to do by ensuring the appropriate offset are set in the initial stack frame), which just runs an infinite loop with a privileged asm(“hlt”) instruction to halt the CPU when no tasks need to be run!! It’s been a struggle to get here, but I’m happy with the operation of this scheduler.
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
nullplan
Member
Member
Posts: 1790
Joined: Wed Aug 30, 2017 8:24 am

Re: Struggling with TSS

Post by nullplan »

That is a bad idea. If you wait long enough, you will have a stack overflow. The reason is that by the time you save your ESP, it was already decreased by the interrupt frame, the error code, the interrupt number, and the PUSHA. So the way you have it now, your ESP0 in each task will keep decreasing, until finally there is not enough space left on the stack to do whatever it is that needs doing.

My model is this: I have a member of the task struct, called "kstack", which is a uintptr_t. In most cases it just contains the same address as the task struct, since I allocate my task structs at the top of kernel stack. It never needs to change for as long as the task is alive. Indeed, it never changes after allocation at all. If the structure is deallocated (garbage collection on memory request), that member becomes invalid anyway. On task switch, I set ESP0 to whatever that kstack is pointing to. That is, unless the new task is a kernel task. In that case, it cannot switch to user mode, so the setting of ESP0 (or RSP0 in my case) doesn't matter. But that's a micro-optimization.

Other than that, your code could be improved in minor ways. You can push DS directly, and you should probably push ES, FS, and GS as well, not just assume they are all the same. You can also pop directly into DS, ES, FS, and GS. Even if they currently always are all the same, it doesn't cost you a lot (12 bytes) to provide for a future case where they may not be equal. And STI before IRET is useless, since the instruction following STI can never be interrupted, and IRET will pop flags as part of its operation, thereby overwriting the interrupt flag again.
Carpe diem!
User avatar
bloodline
Member
Member
Posts: 264
Joined: Tue Sep 15, 2020 8:07 am
Location: London, UK

Re: Struggling with TSS

Post by bloodline »

nullplan wrote:That is a bad idea. If you wait long enough, you will have a stack overflow. The reason is that by the time you save your ESP, it was already decreased by the interrupt frame, the error code, the interrupt number, and the PUSHA. So the way you have it now, your ESP0 in each task will keep decreasing, until finally there is not enough space left on the stack to do whatever it is that needs doing.
I appreciate it’s not very clear (mostly because of my use of the redundant esp1 in the tss), but I'm not saving anything in tss.esp0. Every time an interrupt occurs from ring 3 to ring 0, it will always push the stack frame from the top of the allocated kernel stack, since I never change that (tss.esp0 never ever changes per task, tss.esp1 does, this is a pointer to the bottom of the interrupt stack frame, A6 in 68000 speak), I have the esp1 saved simply to restore the state of the previously retired task (when leaving an interrupt), after that the esp1 variable is discarded.

This approach probably makes more sense on the 68k, as that is where I originally wrote it.

-edit-

It might be easier to understand if you know that a task switch in my model can only happen in an interrupt, and you realise that in my task control structure I have two fields:
1. ssp_top, this is the top of the kernel stack and never changes and it is what is loaded into tss.esp0 upon a task switch.
2. ssp, this is the stack frame of the kernel stack in the interrupt when the task lost the CPU, this is what the interrupt stack frame will need to be restored to when the task regains the CPU and attempts to return from the interrupt.
My model is this: I have a member of the task struct, called "kstack", which is a uintptr_t. In most cases it just contains the same address as the task struct, since I allocate my task structs at the top of kernel stack. It never needs to change for as long as the task is alive. Indeed, it never changes after allocation at all. If the structure is deallocated (garbage collection on memory request), that member becomes invalid anyway. On task switch, I set ESP0 to whatever that kstack is pointing to. That is, unless the new task is a kernel task. In that case, it cannot switch to user mode, so the setting of ESP0 (or RSP0 in my case) doesn't matter. But that's a micro-optimization.

Other than that, your code could be improved in minor ways. You can push DS directly, and you should probably push ES, FS, and GS as well, not just assume they are all the same. You can also pop directly into DS, ES, FS, and GS. Even if they currently always are all the same, it doesn't cost you a lot (12 bytes) to provide for a future case where they may not be equal. And STI before IRET is useless, since the instruction following STI can never be interrupted, and IRET will pop flags as part of its operation, thereby overwriting the interrupt flag again.
Thanks for the advice, I'm still very unsure of the x86 addressing modes... I'm kind of just treating it like a Load-Store architecture at the moment, which helps reading the asm while I'm getting use to it.

I don't have need of any other segments (I'm trying to make this as close to a two level flat address space as possible, though I freely admit this might be suboptimal for the x86), so saving/restoring the segment registers independently doesn't offer me anything.

I'm hoping to remove the cli/sti instructions, I was just very keen to make sure no interrupts happened during an interrupt while I was getting my code working, but good all advice! I'm still learning the x86, I do really appreciate it!
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: 5572
Joined: Mon Mar 25, 2013 7:01 pm

Re: Struggling with TSS

Post by Octocontrabass »

bloodline wrote:I hope I'm not violating some some arcane spec by repurposing esp1, but pleasingly my tasks do now run in ring 3!
It doesn't violate any spec I'm aware of, but it does prevent nesting interrupts, and you must allow nesting for exception handlers.
bloodline wrote:I don't have need of any other segments (I'm trying to make this as close to a two level flat address space as possible, though I freely admit this might be suboptimal for the x86), so saving/restoring the segment registers independently doesn't offer me anything.
FS and GS are typically used as pointers to per-thread or per-CPU data structures. The other segments are usually left to fixed values since popular C compilers require the default segments to all share the same base address, but there's no harm in giving userspace the flexibility to change segment registers (as long as the changes are allowed by the CPU).
bloodline wrote:I'm hoping to remove the cli/sti instructions, I was just very keen to make sure no interrupts happened during an interrupt while I was getting my code working, but good all advice!
An interrupt can still occur between the start of your interrupt handler and the CLI instruction if you don't use an interrupt gate in your IDT.
User avatar
bloodline
Member
Member
Posts: 264
Joined: Tue Sep 15, 2020 8:07 am
Location: London, UK

Re: Struggling with TSS

Post by bloodline »

Octocontrabass wrote:
bloodline wrote:I hope I'm not violating some some arcane spec by repurposing esp1, but pleasingly my tasks do now run in ring 3!
It doesn't violate any spec I'm aware of, but it does prevent nesting interrupts, and you must allow nesting for exception handlers.
The obvious solution is to push %esp, just before the handler call, instead of storing it in tss.esp1, then just pop %esp immediately after the call... Actually now you've made me think about it, that is a much better idea. #-o

Then if the task switch needs to happen it can just alter the value at the top of the stack frame.

I'm glad you made me think about this a bit more... I'll test it out later!

-edit- I had some time at lunch and was able to implement this stack based approach (it works and should be able to support nested interrupts)... I've attached my kernel image if anyone wants to try it out... Not that it does much other than prove the concept. I will leave it running in an emulator to see if it crashes.

bloodline wrote:I don't have need of any other segments (I'm trying to make this as close to a two level flat address space as possible, though I freely admit this might be suboptimal for the x86), so saving/restoring the segment registers independently doesn't offer me anything.
FS and GS are typically used as pointers to per-thread or per-CPU data structures. The other segments are usually left to fixed values since popular C compilers require the default segments to all share the same base address, but there's no harm in giving userspace the flexibility to change segment registers (as long as the changes are allowed by the CPU).
Ok, now you've intrigued me!

In my OS model, the entire userspace is what in unix land would be more similar to a single process, with each task in my os being like a thread in a unix process, all sharing the same address space. I don't think the FS and GS are going to help much... But, perhaps if I decide to implement virtual memory they will become useful? I would like to add multiple CPU support too, so again I might have to think about how they can be used, I will google this tonight. if you have any links that would be most helpful, thanks.
Attachments
kernel.elf.zip
(10.56 KiB) Downloaded 31 times
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: 5572
Joined: Mon Mar 25, 2013 7:01 pm

Re: Struggling with TSS

Post by Octocontrabass »

bloodline wrote:Then if the task switch needs to happen it can just alter the value at the top of the stack frame.
If you allow hardware interrupts to nest, this can cause priority inversion if you're not careful. (For example, if a timer interrupt occurs in the middle of another hardware interrupt handler, you probably don't want to switch tasks until after that other hardware interrupt handler is finished.)
bloodline wrote:In my OS model, the entire userspace is what in unix land would be more similar to a single process, with each task in my os being like a thread in a unix process, all sharing the same address space. I don't think the FS and GS are going to help much... But, perhaps if I decide to implement virtual memory they will become useful?
You can still use virtual memory in a single address space to prevent free space fragmentation. It's especially useful in 64-bit mode, where you have an enormous 256TiB address space, but the relatively tiny amount of physical memory isn't a single contiguous block of physical addresses. (Paging is required to enable 64-bit mode anyway, so it might be a good idea to take advantage of it.)

But this use of FS and GS has nothing to do with virtual memory; no one uses segmentation for virtual memory anymore. In 32-bit mode, the FS or GS segment is set up much like the other segments, just with a nonzero base address. The segment then becomes a glorified pointer register to use in address calculations to reach the per-thread or per-CPU data. In 64-bit mode, there is no segmentation anymore; FS and GS are used exclusively as pointer registers, and it's possible to set their base addresses using MSRs instead of messing with descriptor tables. (But do note that loading FS or GS with a selector will still update the base according to the selected descriptor, and behavior differs between Intel and AMD when loading a null selector.)
bloodline wrote:I would like to add multiple CPU support too, so again I might have to think about how they can be used, I will google this tonight. if you have any links that would be most helpful, thanks.
For user mode, we have a wiki page on how Unix-like OSes use FS/GS (including a link to the thread-local storage specification), and Wikipedia has a pretty good overview on how Windows uses FS/GS.

I'm not aware of any good references to how OSes use FS/GS in supervisor (kernel) mode. All I can find are CPU speculative execution vulnerabilities...
User avatar
bloodline
Member
Member
Posts: 264
Joined: Tue Sep 15, 2020 8:07 am
Location: London, UK

Re: Struggling with TSS

Post by bloodline »

Octocontrabass wrote:
bloodline wrote:Then if the task switch needs to happen it can just alter the value at the top of the stack frame.
If you allow hardware interrupts to nest, this can cause priority inversion if you're not careful. (For example, if a timer interrupt occurs in the middle of another hardware interrupt handler, you probably don't want to switch tasks until after that other hardware interrupt handler is finished.)
I’m not sure a priority inversion would be an issue here, a running task should always be “preemptable”, and an interrupt should never depend upon a particular task being running. I my design, interrupts do little more than fill buffers/set signals/send acknowledge to hardware, all the servicing is handled by an appropriate task (the next time it is scheduled)... Microkernels FTW.

But my current scheduler is a simple priority based round robin, it is possible for a high priority task to depend upon a low priority one, but might be suffering from “time starvation”... But that is a user space problem for another day! :D

Either way, my kernel ran 9 tasks without any stack corruption, for over 16 hours... So I'm happy that we have successful resolved the this thread issue :D Thanks to all who assisted me.
bloodline wrote:In my OS model, the entire userspace is what in unix land would be more similar to a single process, with each task in my os being like a thread in a unix process, all sharing the same address space. I don't think the FS and GS are going to help much... But, perhaps if I decide to implement virtual memory they will become useful?
You can still use virtual memory in a single address space to prevent free space fragmentation. It's especially useful in 64-bit mode, where you have an enormous 256TiB address space, but the relatively tiny amount of physical memory isn't a single contiguous block of physical addresses. (Paging is required to enable 64-bit mode anyway, so it might be a good idea to take advantage of it.)
I've never done any system level coding for the 64 CPUs yet, part of my reason for wanting to write an OS for the x86 is so I can learn about x86-64. But 64bit can come later. I'll get paging working in 32bit first (no point trying to run before I can walk).

My memory allocator uses three tiers of free block pools, different size allocations come from their respective pool (large pool, medium pool, and small pool), this combined with free block coalescing within the pools effectively manages fragmentation. I've used it quite extensively on embedded systems with no MMU. If 64bit needs paging, then I'll implement a different system... and start a new thread when I get stuck :lol:
But this use of FS and GS has nothing to do with virtual memory; no one uses segmentation for virtual memory anymore. In 32-bit mode, the FS or GS segment is set up much like the other segments, just with a nonzero base address. The segment then becomes a glorified pointer register to use in address calculations to reach the per-thread or per-CPU data. In 64-bit mode, there is no segmentation anymore; FS and GS are used exclusively as pointer registers, and it's possible to set their base addresses using MSRs instead of messing with descriptor tables. (But do note that loading FS or GS with a selector will still update the base according to the selected descriptor, and behavior differs between Intel and AMD when loading a null selector.)
Ok, so I spent the evening reading about this, so it seems most operating systems use the GS register to point to the thread task structure... This would be useful if I had a process that needed to keep track of several threads. But I'm not making a Linux/unix clone here, there are no threads, just tasks.

Perhaps I've missed something subtle, I'll start new thread (in the OS Design forum topic) when I try and think more about how I could use these Registers.

-edit- thinking about it, I suppose the GS could point to the task’s own task control block... that might be useful for something? None of my previous designs have required that the task be able to access its own control structures... if it needs to make a change, then it makes a request to the kernel. Perhaps GS could point to the kernel’s interface? That might be useful?
bloodline wrote:I would like to add multiple CPU support too, so again I might have to think about how they can be used, I will google this tonight. if you have any links that would be most helpful, thanks.
For user mode, we have a wiki page on how Unix-like OSes use FS/GS (including a link to the thread-local storage specification), and Wikipedia has a pretty good overview on how Windows uses FS/GS.

I'm not aware of any good references to how OSes use FS/GS in supervisor (kernel) mode. All I can find are CPU speculative execution vulnerabilities...
Excellent links, thanks for the steer they got me started!!

I think I’m going to focus on the tedious task of getting a simple PIO mode ATA driver working next, since I can’t seem to figure out how to convince GRUB to setup a proper Graphic mode for me... I’ll probably have to start a new thread about that soon :cry:
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: 5572
Joined: Mon Mar 25, 2013 7:01 pm

Re: Struggling with TSS

Post by Octocontrabass »

bloodline wrote:I think I’m going to focus on the tedious task of getting a simple PIO mode ATA driver working next,
That sounds like fun.
bloodline wrote:since I can’t seem to figure out how to convince GRUB to setup a proper Graphic mode for me...
Last I checked, gfxpayload and an appropriate multiboot header.
Post Reply