Octocontrabass wrote:The typical OS design looks like this example on the wiki. Switching tasks is done by switching kernel stacks. If the task is ring 0, the new stack will contain a return value that will pick up the ring 0 thread where it left off. If the new task is ring 3, the new stack will contain a return value that will pick up in ring 0 right before the appropriate code to return to ring 3.
You may also want to look at
this example on the wiki.
That's certainly possible. That's typically how my design works too when the SaveThread function is used, which is the normal case when a thread is blocked.
However, the kernel debugging interface will save the register state rather than just block. So, if you single step some code, the trap gate will save the register state in the thread block, and if the thread switches to the application, it will save application state rather than kernel state. That's when the load of application context is used. The kernel debugger uses this so it can trace threads in kernel or in the application by simply using the thread block. The user can also directly modify register context before doing a trace. This interface can also trace V86 code when accessing the BIOS or running DOS applications.
The application debugger works in a similar way, although it must use the application interrupts & exception structure the ABI defines. Still, the application debugger will read register state for a debugged thread from the thread control block and not from some other memory area or the stack. This design is also why the application debugger can seemlessly trace code into kernel space and show the correct register state & source.
Another advantage of this design is that application threads can be created directly using application register state and then start their execution in user space not in kernel space.
When a thread gets an exception it cannot handle, the register state is saved in the exception handler and the thread is moved to a special "debug" list. When a debugged thread is restarted it is moved to the ready queue.
This is how the code to save the register state looks like when called from an execption: (ebp will point to some registers stored by the exception handler, including eflags, ss:esp and cs:eip, ds, ebp, eax & ebx)
Code: Select all
push fs
mov al,[ebp].trap_exc_nr
mov ds:p_fault_vector,al
mov eax,[ebp].trap_err
mov dword ptr ds:p_fault_code,eax
mov dword ptr ds:p_fault_code+4,0
;
mov eax,[ebp].trap_eax
mov dword ptr ds:p_rax,eax
mov eax,[ebp].trap_ebx
mov dword ptr ds:p_rbx,eax
mov dword ptr ds:p_rcx,ecx
mov dword ptr ds:p_rdx,edx
mov dword ptr ds:p_rsi,esi
mov dword ptr ds:p_rdi,edi
mov eax,[ebp].trap_ebp
mov dword ptr ds:p_rbp,eax
;
mov eax,[ebp].trap_eflags
mov dword ptr ds:p_rflags,eax
mov ax,[ebp].trap_cs
mov ds:p_cs,ax
mov eax,[ebp].trap_eip
mov dword ptr ds:p_rip,eax
;
pop si
test dword ptr [ebp].trap_eflags,20000h
jnz debug_vm
debug_pm:
mov al,[ebp].trap_cs
test al,3
jz debug_kernel
;
mov ax,[ebp].trap_ss
mov ds:p_ss,ax
mov eax,[ebp].trap_esp
mov dword ptr ds:p_rsp,eax
jmp debug_pm_common
debug_kernel:
mov ax,ss
mov ds:p_ss,ax
mov eax,ebp
add eax,trap_esp
mov dword ptr ds:p_rsp,eax
debug_pm_common:
mov ax,[ebp].trap_pds
mov ds:p_ds,ax
mov ax,es
mov ds:p_es,ax
mov ds:p_fs,si
mov ax,gs
mov ds:p_gs,ax
jmp debug_save_ok
debug_vm:
mov ax,[ebp].trap_gs
mov ds:p_gs,ax
mov ax,[ebp].trap_fs
mov ds:p_fs,ax
mov ax,[ebp].trap_ds
mov ds:p_ds,ax
mov ax,[ebp].trap_es
mov ds:p_es,ax
mov ax,[ebp].trap_ss
mov ds:p_ss,ax
mov eax,[ebp].trap_esp
mov dword ptr ds:p_rsp,eax
debug_save_ok: