About Me
Welcome to my blog! This is where I collect my observations and notes on programming and technology. The main subjects range from implementation details to broader ideas about programming.
Main Topics
- Engineering Projects: Exploring implementation details and how technical systems work.
- C/C++: Notes on language features and programming techniques.
- The Programmer’s Perspective: Ideas about developing a career and a way of thinking as a programmer.
For more, visit the categories page.
Contact
If you have questions or would like to discuss something, please get in touch through the About page.
Thank you for reading and for your support. I hope these notes help you on your own technical journey!
Overview
These notes follow task-context initialization, switching, and stack management in the educational runtime. Each task needs independently preserved stack state. Introducing user programs adds another distinction between the stack used by a program and the stack available to kernel code.
The first switch occurs during startup; later switches are triggered through calls or interrupts. The exact stack arrangement described below is an intermediate implementation, rather than a complete design for isolating untrusted user processes.
rt_hw_stack_init and kcontext
The supplied stack address is treated as the top of the available stack region. Initialization aligns it to an eight-byte boundary in this implementation, then stores tentry, parameter, and texit.
The address of those three stored values becomes the argument to a wrapper function. kcontext reserves space for the saved register frame below the current top, initializes it, and sets the wrapper as the entry point.
When execution reaches the wrapper, it reads the three values through the argument pointer and invokes the thread entry appropriately. The layout must ensure that the saved values are not overwritten while the context is being created.
First and Subsequent Context Switches
rt_hw_context_switch_to handles the first transfer, when there is no old task context to save. It obtains thread information, establishes the destination, and invokes yield.
rt_hw_context_switch follows a similar path but also records the source. The deeper trap handler uses the thread information and the recorded from and to values to save and select the appropriate contexts.
Passing the Initial Argument
The argument expected by the entry function belongs in the saved a0 register slot. The restore code in trap.S loads it, and the function receives it through the ordinary calling convention. The same rule applies when the entry wrapper is written in assembly.
The wrapper’s three logical inputs do not require three separate register arguments: they are placed together on the prepared stack, and their address is passed as one argument.
The PCB Union
The members of a union share the same starting address. They do not share their highest addresses. This matters when reasoning about a PCB whose storage is also viewed as a stack array.
Introducing a User Stack
Initially every task was kernel code using a stack region inside its PCB. Adding a user program introduced another stack, initially chosen near heap.end in the experiment.
The trap assembly saves registers relative to the stack pointer active at the trap. In this intermediate setup, a trap from a user program therefore created the saved frame on the user’s stack. I handled the saved sp slot separately from the general register-mapping macro to avoid restoring it twice at incompatible points.
The scheduler records the returned context pointer in the current PCB. Selecting a task returns its saved frame pointer; the assembly must first use that frame to restore registers and then resume with the task’s intended stack pointer.
This raised a design question: should restoration begin from a copied frame in the PCB, or directly from the frame on the task’s stack? Mixing those approaches changes the required stack adjustments. I tried to minimize assembly changes while keeping the existing kernel-task path working.
At registration, the implementation prepared initial context information for the user stack and the PCB, and initially set the PCB’s cp to the PCB-resident frame.
Keeping Context Pointers Consistent
An initial frame is only a starting point. Once a task traps, the trap handler receives the address of the newly saved frame, which may differ from that initial address.
With kernel tasks, both the running stack and the saved context already live in the PCB’s stack region. The earlier code therefore did not expose the distinction clearly. With user tasks, the running stack is elsewhere, so the distinction becomes visible.
One possible solution is to copy the saved frame into a PCB-resident context. My implementation instead updates pcb->cp to point to the frame supplied by the trap handler. No memory copy is performed.
Consequently, a user task initially has a PCB-resident context pointer, but after it traps the pointer identifies a saved frame on its user stack. A kernel task continues to have its saved frames inside its kernel-stack region.
When switching to a kernel task, the assembly selects the returned kernel-stack frame, restores its registers, and recovers its stack pointer. When switching to a user task, it performs the corresponding restoration from the saved user-stack frame.
Repeated traps keep the active cp pointers updated. The user’s original PCB-resident initialization frame need not change, because it is no longer the frame used to resume that task.
This explains the experiment’s behavior, but a protected OS normally switches to a trusted kernel stack on entry from user mode. Saving trusted kernel state on an untrusted user stack requires further security and fault-handling design.
Initializing a User Program
context_uload wraps loading and ucontext creation. The loader-side code establishes the user stack, while ucontext constructs the initial context associated with the PCB’s kernel stack.
I did not initialize sp again in the Navy program’s _start. The OS, Nanos in this setup, allocates and selects the user program’s stack before entering it. _start is the executable entry point, but it should respect the startup convention established by that loader.
The current launch policy traps through yield and lets schedule choose the program. Later mechanisms can make program selection more explicit.
To verify which stack a task actually uses, print both its saved context pointer and saved stack-pointer value in the scheduler and compare them with the allocated regions.
If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. All the images used in the blog are my original works or AI works, if you want to take it,don't hesitate. Thank you !