# NachOS Trace code 筆記
- [概略](#概略)
- [Debug](#Debug)
- [吃 CLI 參數](#吃-CLI-參數)
- [Trace](#Trace)
- [Trace1](#Trace1)
- [Trace2](#Trace2)
- [Trace3](#Trace3)
- [Trace4](#Trace4)
- [Trace4_Init_ThreadedKernel](#Trace4_Init_ThreadedKernel)
- [Trace4_Timer](#Trace4_Timer)
- [Trace4_OneTick()](#Trace4_OneTick())
- [Trace4_Init_Machine](#Trace4_Init_Machine)
- [Trace4_Init_FileSystem](#Trace4_Init_FileSystem)
- [Trace5](#Trace5)
- [Trace6](#Trace6)
- [Trace6_StackAllocate](#Trace6_StackAllocate)
- [Trace7](#Trace7)
- [Trace7_SWITCH](#Trace7_SWITCH)
- [Trace8](#Trace8)
- [Trace8_Machine::Run](#Trace8_MachineRun)
- [Trace8_OneInstruction](#Trace8_OneInstruction)
- [Trace8_OneTick](#Trace8_OneTick)
- [Trace9](#Trace9)
- [Trace9_yieldOnReturn](#Trace9_yieldOnReturn)
- [Trace10_Halt](#Trace10_Halt)
## 概略
- [github repos 在此](https://github.com/LJP-TW/NachOS)
repos 已經更新過了,這份筆記提到的 code 是在 commit `Add TraceCode Note`
code 全放在 code 資料夾
Makefile 也推薦高速掃過看一下
[最後的章節](#Trace)會以執行流程的方式紀錄整個執行過程,追蹤 `userprog/nachos`
並會統整一些架構出來,放在中間的章節
## Debug
**初始化在 [threads/main.cc](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/threads/main.cc)**
用參數 `-d` 設定的字串創造一個 Debug Obj
Debug Class 定義於 [lib/debug.h](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/lib/debug.h)
假設 `-d` 設定為 `ti`(e.g. `./userprog/nachos -e test/test2 -d ti`)
那 Debug Class 的 enableFlags 會是字串 `ti`
接著在整個專案的任意處寫類似以下的語句:
- ```DEBUG(dbgThread, "My debug message")```
DEBUG 是一個 macro,也定義在 [lib/debug.h](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/lib/debug.h),會去看 dbgThread 代表的字元 `t` (同樣也寫在 lib/debug.h) 是否有在剛剛 `-d` 設定的字串中,有就輸出 `My debug message`
## 吃 CLI 參數
看 [Trace3](#Trace3)
## Trace
以下 Trace 在 code 底下執行 ```./userprog/nachos -e test/test1``` 的過程
### Trace1
進入點為 [threads/main.cc](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/threads/main.cc)
這裡也設定了 Debug 物件,詳細請看[Debug](#Debug)
接著 call ```kernel = new KernelType(argc, argv);```,KernelType 是一個 Macro,定義在 [threads/main.h](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/threads/main.h),會根據 define 了以下 symbol 做不一樣的行為
- NETWORK
- USER_PROGRAM
- 兩者都沒設
現在來追看看到底什麼 symbol 有被 define
### Trace2
因為我們追蹤的是 `userprog/nachos`,看一下 [userprog/Makefile](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/userprog/Makefile),以下 focus 在 跟 define 有關的部分
userprog/Makefile 中有一行
```
DEFINES = -DTHREADS -DUSER_PROGRAM -DFILESYS_NEEDED -DFILESYS_STUB
```
再往下一點有兩行
```
include ../Makefile.common
include ../Makefile.dep
```
[Makefile.common](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/Makefile.common) 裡有一行
```
CFLAGS = -g -Wall $(INCPATH) $(DEFINES) $(HOST) -DCHANGED
```
[Makefile.dep](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/Makefile.dep) 只有這樣
```
# CPU = x86, OS = linux
HOST = -Dx86 -DLINUX -DBSD
DISASM = disasm
```
對於 Makefile 的 `=` 跟 `:=` 兩個指派運算子的觀念可以看看[這篇文](http://dannysun-unknown.blogspot.com/2015/03/makefile.html),簡單來說,`=` 是會到要用到的時候才會展開
而 CFLAGS 是 compile 時,會餵給 compiler 吃的參數
最後整個 CFLAGS 是 `-g -Wall $(INCPATH這個我們先不考慮) -DTHREADS -DUSER_PROGRAM -DFILESYS_NEEDED -DFILESYS_STUB -Dx86 -DLINUX -DBSD -DCHANGED`
這些參數會使 compiler 在 compile 時預先 define
- THREADS
- USER_PROGRAM
- FILESYS_NEEDED
- FILESYS_STUB
- x86
- LINUX
- BSD
- CHANGED
因為 define 了 USER_PROGRAM,所以前面 [Trace1](#Trace1) 的 main.h 會 define KernelType 為 UserProgKernel
### Trace3
在 [threads/main.cc](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/threads/main.cc) 中執行 ```kernel = new KernelType(argc, argv);```,實際上是創造 UserProgKernel 物件
UserProgKernel 實作於 [userprog/userkernel.cc](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/userprog/userkernel.cc),會先執行父類別 ThreadedKernel 的 Constructor
ThreadedKernel 實作於 [threads/kernel.cc](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/threads/kernel.cc),設定 ```randomSlice = FALSE;```會在後續創造 Alarm obj 時用到
回到 UserProgKernel,吃到參數 `-e`,將 `execfile[0]` 設為 `test/test1`,`execfileNum` 為 1
可以看到 UserProgKernel 的 Constructor 只是吃吃參數而已,真正有做些初始化是在後面
### Trace4
在 [threads/main.cc](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/threads/main.cc) 中執行 ``` kernel->Initialize();```
執行 [userprog/userkernel.cc](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/userprog/userkernel.cc) 中的`UserProgKernel::Initialize()`
`UserProgKernel::Initialize()` 會執行
- `ThreadedKernel::Initialize()`
- 請看 [Trace4_Init_ThreadedKernel](#Trace4_Init_ThreadedKernel)
- `machine = new Machine(debugUserProg);`
- 請看 [Trace4_Init_Machine](#Trace4_Init_Machine)
- `fileSystem = new FileSystem();`
- 請看 [Trace4_Init_FileSystem](#Trace4_Init_FileSystem)
### Trace4_Init_ThreadedKernel
執行 [threads/kernel.cc](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/threads/kernel.cc) 中的`ThreadedKernel::Initialize()`
`ThreadedKernel::Initialize()` 會執行
- `stats = new Statistics();`
- `Statistics::Statistics()` 實作於 [machine/stats.cc](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/machine/stats.cc) 中
- 此元件掌管 Tick Counters 跟其他數據統計
- `interrupt = new Interrupt;`
- `Interrupt::Interrupt()` 實作於 [machine/interrupt.cc](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/machine/interrupt.cc) 中
- 主要模擬硬體中斷的元件
- 初始化一些 member
- `level = IntOff;`
- disable 中斷
- `pending = new SortedList<PendingInterrupt *>(PendingCompare);`
- 初始化 Pending list
- 裡面會存放著中斷,後續會再追到
- `inHandler = FALSE;`
- 還未在處裡中斷
- `yieldOnReturn = FALSE;`
- 還沒要進行 context switch
- `status = SystemMode;`
- 正在 kernel mode
- `scheduler = new Scheduler(type);`
- `Scheduler::Scheduler(SchedulerType type)` 實作於 [threads/scheduler.cc](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/threads/scheduler.cc) 中
- CPU Scheduler
- 初始化 blockedList、readyList
- `alarm = new Alarm(randomSlice);`
- `Alarm::Alarm(bool doRandom)` 實作於 [threads/alarm.cc](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/threads/alarm.cc) 中
- 設定 Timer 物件,randomSlice 為 False
- 請看 [Trace4_Timer](#Trace4_Timer)
- `currentThread = new Thread("main");`
- `Thread::Thread(char* threadName)` 實作於 `threads/threads.cc` 中
- 初始化大部分參數為 NULL,還需要 call `Thread::Fork()`
- `currentThread->setStatus(RUNNING);`
- 直接設 main thread 的 status 為 RUNNING
- `interrupt->Enable();`
- 實作於 [machine/interrupt.h](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/machine/interrupt.h),直接 call `Interrupt::SetLevel(IntOn);`
- 中斷元件從不接受中斷改變狀態為接受中斷,會 call 一次 `Interrupt::OneTick()`
- 請看 [Trace4_OneTick()](#Trace4_OneTick())
#### Trace4_Timer
`Timer::Timer(bool doRandom, CallBackObj *toCall)` 實作於 [machine/timer.cc](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/machine/timer.cc) 中
將 `callPeriodically` 設定為 `toCall`
並呼叫 `Timer::SetInterrupt()`
`Timer::SetInterrupt()` 中,若 Timer 為 Enable,則呼叫 `Interrupt::Schedule`
創造一個 PendingInterrupt 物件,紀錄著
- 什麼要被 call: this Timer
- 何時要被 call: delay,也就是 TimerTicks
- 中斷類型: TimerInt
並插入到 Pending list
#### Trace4_OneTick()
`Interrupt::OneTick()` 實作於 [machine/interrupt.cc](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/machine/interrupt.cc)
Trace 到此時,還在 kernel mode,totalTicks 會加上 SystemTick
(各種 Ticks 定義在 [machine/stats.h](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/machine/stats.h) 中)
先禁止接受中斷後執行
- `CheckIfDue(FALSE);`
`Interrupt::CheckIfDue(bool advanceClock)` 簡單來說是檢查 Pending list 有無已經到達預定執行時間的中斷要做,有就呼叫此中斷的 Callback,再呼叫中斷的 Callback 的期間,將 inHandler 設為 True,表示正在處理中斷,回傳值的狀況分為以下狀況
- Pending List 為空,沒有任何中斷要做
- Return false
- 下一個中斷要處理的時間還沒到,且 advanceClock 為 False
- Return false
- 此 function 原本行為是下一個中斷要處理的時間還沒到的話,會快轉到下一個要中斷要處理的時間,但若 advanceClock 為 false,表示告訴這個 function 先不要快轉
- 其餘狀況皆 return true
由於 Pending List 中已經有一個 TimerInt 的中斷,時間還沒到,應該要快轉,但 advanceClock 設 false,所以回傳 false
處理完中斷後,重新開啟接受中斷
看看有無要進行 content switch,有則執行 ```kernel->currentThread->Yield();```不過 Trace 到目前為止是還不會需要 content switch 的。
### Trace4_Init_Machine
`Machine::Machine(bool debug)` 實作於 [machine/machine.cc](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/machine/machine.cc),debug 參數為 false,簡單來說,就是初始化機器的暫存器為 0,清空 mainMemory
### Trace4_Init_FileSystem
`FileSystem::FileSystem(bool format)` 實作於 `filesys/filesys.cc`,format 預設為 true,這個就先不深追
### Trace5
在 [threads/main.cc](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/threads/main.cc) 中執行
```
CallOnUserAbort(Cleanup); //設定按下 ctrl+c 就 call `Cleanup`
kernel->SelfTest(); // 不是很重要,For debugging
kernel->Run(); // 繼續追蹤這個 function call
```
main 到此告一段落,Control flow 移轉到 kernel->Run()
### Trace6
`UserProgKernel::Run()` 首先一個 for 迴圈,為每支 userprogram 建立 Thread、分配 AddrSpace,並且執行 Fork
本例子是執行
```
t[n] = new Thread("test/test1");
t[n]->space = new AddrSpace();
t[n]->Fork((VoidFunctionPtr) &ForkExecute, (void *)t[n]);
```
最後執行 `ThreadedKernel::Run();`
- `t[n] = new Thread("test/test1");`
- 初始化大部分參數為 NULL,thread status 為 JUST_CREATED
- `t[n]->space = new AddrSpace();`
- `AddrSpace::AddrSpace()` 實作於 [userprog/addrspace.cc](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/userprog/addrspace.cc)
- 目前 pageTable 直接將 virtual address 映射為 physical address
- `t[n]->Fork((VoidFunctionPtr) &ForkExecute, (void *)t[n]);`
- 首先呼叫 `StackAllocate(func, arg);` 分配一塊 stack
- 請看 [Trace6_StackAllocate](#Trace6_StackAllocate)
- 將 thread 放到 ready list
- thread status 變成 READY
### Trace6_StackAllocate
`Thread::StackAllocate (VoidFunctionPtr func, void *arg)`首先執行
- `stack = (int *) AllocBoundedArray(StackSize * sizeof(int));`
`AllocBoundedArray(StackSize * sizeof(int))` 實作於 [lib/sysdep.cc](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/lib/sysdep.cc)
- 首先 call `getpagesize()` 取得一個 page 多大
- 創造 `pgSize * 2 + size` 的空間
- 將頭尾 2 頁設為不可 rwx
- 這 2 個 page 是為了偵測有無發生 stack overflow
- 最後 return 實際可用的第 2 頁位址
回到 `Thread::StackAllocate`,接下來會根據不同架構做不一樣的事情,在 [Trace2](#Trace2) 我們知道有 define x86,所以會執行
```
stackTop = stack + StackSize - 4;
*(--stackTop) = (int) ThreadRoot;
*stack = STACK_FENCEPOST;
machineState[PCState] =(void *)ThreadRoot;
machineState[StartupPCState] = (void *)ThreadBegin;
machineState[InitialPCState] = (void *)func;
machineState[InitialArgState] = (void *)arg;
machineState[WhenDonePCState] = (void *)ThreadFinish;
```
:::info
- PCState、StartupPCState 那些 define 在 [threads/switch.h](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/threads/switch.h)
```
#define PCState (_PC/4-1)
#define FPState (_EBP/4-1)
#define InitialPCState (_ESI/4-1)
#define InitialArgState (_EDX/4-1)
#define WhenDonePCState (_EDI/4-1)
#define StartupPCState (_ECX/4-1)
```
詳細機制後面會再提
:::
- stack 位址是 from high to low,所以將 stackTop 移動到這塊記憶體的最高記憶體位址
- 將 ThreadRoot push 上去
- 將 stack 最底設為 STACK_FENCEPOST,用來偵測有無 overflow
- STACK_FENCEPOST 只是一個 const int 0xdedbeef
- 定義於 threads/thread.cc
- ThreadRoot 先定義於 [threads/thread.h](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/threads/thread.h)
- 實作於 [threads/switch.s](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/threads/switch.s)
- 設定一些初始的 Register
- 例如將 `machineState[PC]` 設定為 ThreadRoot
- 後續會詳談,這邊留個印象就好
目前為止 thread `test/test1` 的 stack 上的資料大概像
![](https://i.imgur.com/5yAlqVs.png)
### Trace7
回到 `UserProgKernel::Run()`,for 迴圈跑完後執行 `ThreadedKernel::Run();`
`ThreadedKernel::Run()` 裡頭執行 `currentThread->Finish();`
`currentThread` 為在 [Trace4_Init_ThreadedKernel](#Trace4_Init_ThreadedKernel) 設定的 `main`
Thread `main` 的 `Thread::Finish` 執行了
- 首先關閉接收中斷
- 呼叫 `Sleep(TRUE)`
Thread `main` 的 `Thread::Sleep (bool finishing)` 執行了
- 將 thread `main` status 設為 BLOCKED
- 尋找下一個在 ready list 的 thread
- 找到 `test/test1`
- 執行 nextthread
- 執行 `kernel->scheduler->Run(Thread test/test1, TRUE);`
`Scheduler::Run (Thread test/test1, TRUE)` 執行了
- 將 toBeDestroyed 設為 `main`
- 呼叫 thread `main` 的 `CheckOverflow`
- 檢查 stack 上是否為 STACK_FENCEPOST
- 將 kernel->currentThread 設為 thread `test/test1`
- 將 thread `test/test1` status 設為 RUNNING
- 執行 SWITCH(`main`, `test/test1`)
- 請看 [Trace7_SWITCH](#Trace7_SWITCH)
- 執行 `CheckToBeDestroyed()`
- Delete thread `main`
- thread main 的 stack 應該於 SWITCH 中被清除
### Trace7_SWITCH
`void SWITCH(Thread *oldThread, Thread *newThread);`
- 定義於 [threads/thread.h](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/threads/thread.h)
- 實作於 [threads/switch.s](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/threads/switch.s)
在 [Trace2](#Trace2) 我們知道 define 了 x86,所以 switch.s 中的 SWITCH code 為
```
.comm _eax_save,4
.globl SWITCH
SWITCH:
movl %eax,_eax_save # save the value of eax
movl 4(%esp),%eax # move pointer to t1 into eax
movl %ebx,_EBX(%eax) # save registers
movl %ecx,_ECX(%eax)
movl %edx,_EDX(%eax)
movl %esi,_ESI(%eax)
movl %edi,_EDI(%eax)
movl %ebp,_EBP(%eax)
movl %esp,_ESP(%eax) # save stack pointer
movl _eax_save,%ebx # get the saved value of eax
movl %ebx,_EAX(%eax) # store it
movl 0(%esp),%ebx # get return address from stack into ebx
movl %ebx,_PC(%eax) # save it into the pc storage
movl 8(%esp),%eax # move pointer to t2 into eax
movl _EAX(%eax),%ebx # get new value for eax into ebx
movl %ebx,_eax_save # save it
movl _EBX(%eax),%ebx # retore old registers
movl _ECX(%eax),%ecx
movl _EDX(%eax),%edx
movl _ESI(%eax),%esi
movl _EDI(%eax),%edi
movl _EBP(%eax),%ebp
movl _ESP(%eax),%esp # restore stack pointer
movl _PC(%eax),%eax # restore return address into eax
movl %eax,4(%esp) # copy over the ret address on the stack
movl _eax_save,%eax
ret
```
(AT&T 語法的組語, src在左, dst在右)
對應 `SWITCH(main, test/test1)` 的組合語言是
```
push Thread * test/test1
push Thread * main
call SWITCH
```
所以執行 nachos 的主機的 stack 上會長這樣
![](https://i.imgur.com/7F2fpKg.png)
解釋 SWITCH 做的事情
```
movl %eax,_eax_save
```
- 先將 `eax` 的值存到 `_eax_save`
```
movl 4(%esp),%eax # move pointer to t1 into eax
movl %ebx,_EBX(%eax) # save registers
movl %ecx,_ECX(%eax)
movl %edx,_EDX(%eax)
movl %esi,_ESI(%eax)
movl %edi,_EDI(%eax)
movl %ebp,_EBP(%eax)
movl %esp,_ESP(%eax) # save stack pointer
```
:::info
- 這邊要配合 [switch.h](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/threads/switch.h) 看,switch.h 有一段 code 如下
```
#define _ESP 0
#define _EAX 4
#define _EBX 8
#define _ECX 12
#define _EDX 16
#define _EBP 20
#define _ESI 24
#define _EDI 28
#define _PC 32
```
- 還要配合 Thread Class 前半段的定義
```
class Thread {
private:
// NOTE: DO NOT CHANGE the order of these first two members.
// THEY MUST be in this position for SWITCH to work.
int *stackTop; // the current stack pointer
void *machineState[MachineStateSize]; // all registers except for stackTop
```
- Thread * `test/test1` + 0 就會存取到 stackTop
- Thread * `test/test1` + 4 就會存取到 machineState[0]
- Thread * `test/test1` + 8 就會存取到 machineState[1]
- 以此類推
:::
- 將 `eax` 的值改為 `esp + 4`,也就是 Thread * `main`
- 將 `ebx` 存到 `[eax + _EBX]`,也就是 `[eax + 8]`,也就是 Thread * `main` 的 `machineState[1]`,之後以 `machineState[EBX]` 形式表示
- 以此類推
- **其中**,`_ESP` 為 0,所以改 `machineState[ESP]` 實際上是改 Thread class 裡的 stackTop
```
movl _eax_save,%ebx
movl %ebx,_EAX(%eax)
```
- 將剛剛存的 `eax` 放到 `ebx`
- 存放到 Thread * `main` 的 `machineState[EAX]`
```
movl 0(%esp),%ebx
movl %ebx,_PC(%eax)
```
- 將 call 完 SWITCH 後要回去的 return address 放到 Thread * `main` 的 `machineState[PC]`
:::success
到目前為止存好了 Thread * `main` (oldThread) 各個暫存器的值
- esp 為:
![](https://i.imgur.com/7F2fpKg.png)
- PC 為 Return address
- 為 call 完 SWITCH 後的下一個指令位址
:::
```
movl 8(%esp),%eax
```
- 存取 Thread * `test/test1`
```
movl _EAX(%eax),%ebx # get new value for eax into ebx
movl %ebx,_eax_save # save it
movl _EBX(%eax),%ebx # retore old registers
movl _ECX(%eax),%ecx
movl _EDX(%eax),%edx
movl _ESI(%eax),%esi
movl _EDI(%eax),%edi
movl _EBP(%eax),%ebp
movl _ESP(%eax),%esp # restore stack pointer
```
- 將 Thread * `test/test1` 紀錄的各個暫存器的值恢復
- 將 `eax` 先存到 `_eax_save`
- 以此類推
- **其中**,取出 `_ESP(%eax)` 實際上是取出 Thread * `test/test1` 的 stackTop
- 複習這張圖
![](https://i.imgur.com/5yAlqVs.png)
- 所以現在機器上的 esp 會指向這一塊記憶體
![](https://i.imgur.com/3whnDRI.png)
```
movl _PC(%eax),%eax # restore return address into eax
movl %eax,4(%esp) # copy over the ret address on the stack
movl _eax_save,%eax
ret
```
- 將 Thread * `test/test1` 的 Program Counter 放到 return address
- 恢復 `eax`
- return,從 esp pop return address 到 PC,也就是跳轉到 ThreadRoot 上,所以現在 stack 長這樣
![](https://i.imgur.com/c3dpgws.png)
(pop 只會取值出來放到暫存器,並將 `esp` + 4,並不會改動值)
### Trace8
現在要跳轉到 Thread `test/test1` 的 Program counter 上
可以回顧 [Trace6_StackAllocate](#Trace6_StackAllocate),Thread `test/test1` `machineState[PC]` 是 `ThreadRoot` 的位址
所以 Control flow 現在會跑到 `ThreadRoot` 上執行
`void ThreadRoot()`
- 定義於 [threads/thread.h](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/threads/thread.h)
- 實作於 [threads/switch.s](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/threads/switch.s)
code 如下
```
.globl ThreadRoot
/* void ThreadRoot( void )
**
** expects the following registers to be initialized:
** eax points to startup function (interrupt enable)
** edx contains inital argument to thread function
** esi points to thread function
** edi point to Thread::Finish()
*/
ThreadRoot:
pushl %ebp
movl %esp,%ebp
pushl InitialArg
call StartupPC
call InitialPC
call WhenDonePC
# NOT REACHED
movl %ebp,%esp
popl %ebp
ret
```
- `ebp` 值初始為 0,push 上去後將 `esp` 的值放到 `ebp`
![](https://i.imgur.com/3ZoPJ8b.png)
- `pushl InitialArg`
![](https://i.imgur.com/I0ldntD.png)
- `call StartupPC`,會 push 下一個指令位址
![](https://i.imgur.com/MTycImd.png)
`StartupPC` 定義在 [threads/switch.h](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/threads/switch.h),就是 `%ecx`
在剛剛的 SWITCH 中,`%ecx` 的值設定為 Thread * `test/test1` 的 `machineState[ECX]`
而 Thread * `test/test1` 的 `machineState[ECX]` 的值在 [Trace6_StackAllocate](#Trace6_StackAllocate) 中有設定
```
machineState[StartupPCState] = (void *)ThreadBegin;
```
所以現在會呼叫 `ThreadBegin`
`static void ThreadBegin()` 實作於 [threads/thread.cc](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/threads/thread.cc)
```
static void ThreadBegin() { kernel->currentThread->Begin(); }
```
`kernel->currentThread` 現在為 Thread `test/test1`
所以會呼叫到 Thread `test/test1` 的 `Begin()`
`Thread::Begin()` 執行
- `kernel->scheduler->CheckToBeDestroyed();`
- Thread * `main` 在此被砍掉
- `kernel->interrupt->Enable();`
- `void Enable() { (void) SetLevel(IntOn); }`
- `Interrupt::SetLevel(IntStatus now)` 中斷從關閉到打開,會 call 一次 `OneTick()`
`Interrupt::OneTick()` 執行
- 還在 SystemMode,計數 sysmtemTicks
- 關閉中斷
- 執行 `CheckIfDue(FALSE);`
- pending list 有元件,但時間未到,又 advanceClock 為 False,故回傳 false
- 開啟中斷
- yieldOnReturn 為 false,不會做事
執行完了 return,從 stack pop 出 return address 到 PC,執行`call InitialPC`,會 push 下一個要執行的指令
![](https://i.imgur.com/IJKMYhd.png)
`InitialPC` 定義在 [threads/switch.h](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/threads/switch.h),就是 `%esi`
在剛剛的 SWITCH 中,`%esi` 的值設定為 Thread * `test/test1` 的 `machineState[ESI]`
而 Thread * `test/test1` 的 `machineState[ESI]` 的值在 [Trace6_StackAllocate](#Trace6_StackAllocate) 中有設定
```
machineState[InitialPCState] = (void *)func;
```
`func` 為 `ForkExecute`,所以現在會呼叫 `ForkExecute(Thread *t)`
- 實作於 [userprog/userkernel.cc](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/userprog/userkernel.cc)
- Thread * `t` 這第一個參數為 `esp` + 4 的 InitialArg,此時回頭追一下 InitialArg 是什麼
- 在 [Trace6](#Trace6) 可以看到 Fork 的 arg 是 `t[n]`
- 進一步在 [Trace6_StackAllocate](#Trace6_StackAllocate) 可以看到
```
machineState[InitialArgState] = (void *)arg;
```
- 所以 InitialArg ,也就是`t`,是 Thread `test/test1`
![](https://i.imgur.com/1FnmfeA.png)
- `ForkExecute(Thread *t)` 執行 `t->space->Execute(t->getName());`
執行 Thread `test/test1` 的 `space->Execute("test/test1")`
`AddrSpace::Execute(char *fileName)` 實作於 [userprog/addrspace.cc](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/userprog/addrspace.cc),執行了
- `Load(fileName)`
- 將主機(Linux)上的檔案讀進 `kernel->machine->mainMemory`
- `this->InitRegisters()`
- 裡頭用到的 `NumTotalRegs` 是定義於 [machine/machine.h](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/machine/machine.h),為 user program 能用的 CPU registers
- 將 `kernel->machine` 的所有 registers 先填為 0
- 包含將 `kernel->machine` 的 PC register 寫為 0 (user program 的 start,也就是 entry point,要放在 memory address 0)
- 將 `kernel->machine` 的 NextPC register 寫為 4
- 將 `kernel->machine` 的 StackReg 寫為最高位址
- `this->RestoreState()`
- 將 `kernel->machine` 的 pageTable 改為此 Thread 的 AddrSpace 的 pageTable
- `kernel->machine->Run()`
- 定義於 [machine/machine.h](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/machine/machine.h)
- 實作於 [machine/mipssim.cc](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/machine/mipssim.cc)
- **實際上 NachOS 以 MIPS 架構模擬執行 user program 的 function**
- 請看下一章節
### Trace8_Machine::Run
執行以下事情
```c=
nstruction *instr = new Instruction;
kernel->interrupt->setStatus(UserMode);
for (;;) {
OneInstruction(instr);
kernel->interrupt->OneTick();
if (singleStep && (runUntilTime <= kernel->stats->totalTicks))
Debugger();
}
```
- 首先,class `Instruction` 只是一個用來存放指令的結構
- 將 `kernel->interrupt` 設為 `UserMode`
- 接下來是無限迴圈
- 執行 OneInstruction
- 請看 [Trace8_OneInstruction](#Trace8_OneInstruction)
- 執行 `kernel->interrupt->OneTick()`
- 請看 [Trace8_OneTick](#Trace8_OneTick)
- 下面的 if 是如果有開啟 debug 選項,要求逐行動態 debug
- 本例沒有啟用
### Trace8_OneInstruction
- 定義於 [machine/machine.h](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/machine/machine.h)
- 實作於 [machine/mipssim.cc](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/machine/mipssim.cc)
這邊沒有認真追完,但看個大概還是能看得出來執行了什麼
```
ReadMem(registers[PCReg], 4, &raw)
```
- 實作於 [machine/translate.cc](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/machine/translate.cc)
- 從 PCReg 讀一條指令(MIPS 指令皆為 4 bytes)
```
instr->value = raw;
instr->Decode();
```
- 翻譯指令
```
int pcAfter = registers[NextPCReg] + 4;
```
- 先記著 NextPC
```
switch (instr->opCode)
{
case OP_xxx:
...
...
}
```
- 依照是什麼指令,選擇要做什麼事情
- 就是一個典型 Virtual Machine
- **其中 OP_SYSCALL 會執行 `RaiseException(SyscallException, 0);`**
- 請看 [Trace8_RaiseException](#Trace8_RaiseException)
- 有一些指令會更改 `nextLoadReg`、`nextLoadValue`,在待會的`DelayedLoad(nextLoadReg, nextLoadValue);` 會用到
```
DelayedLoad(nextLoadReg, nextLoadValue);
```
- 執行 `registers[registers[LoadReg]] = registers[LoadValueReg];`
- 設定 `registers[LoadReg] = nextReg`
- 設定 `registers[LoadValueReg] = nextValue;`
統整來說,call 了 `DelayedLoad(MyReg1, MyValue1);`,並不會馬上將 `MyValue1` 存到 Register `MyReg1` 上,而是要等下次再 call 到這個 function 才會存過去 (所以叫做 `DelayedLoad`)
- 大概猜想創造這個 function 是為了完成 MIPS 的指令,這裡我的猜想極可能有誤
```
registers[PrevPCReg] = registers[PCReg];
registers[PCReg] = registers[NextPCReg];
registers[NextPCReg] = pcAfter;
```
- 更新跟 PC 有關的 Register
#### Trace8_RaiseException
`Machine::RaiseException(ExceptionType which, int badVAddr)`
- 定義於 [machine/machine.h](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/machine/machine.h)
- 實作於 [machine/machine.cc](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/machine/machine.cc)
當執行 user 的 MIPS 程式時,若有 call syscall 或有其他狀況,都會觸發這個 function (寫在 [Trace8_OneInstruction](#Trace8_OneInstruction) 中)
執行了以下事情
- `registers[BadVAddrReg] = badVAddr;`
- 透過 virtual machine 暫存器傳遞參數
- `DelayedLoad(0, 0);`
- 將還沒 Load 的東西 Load 好
- `kernel->interrupt->setStatus(SystemMode);`
- 陷入 `SystemMode` (`KernelMode`)
- `ExceptionHandler(which);`
- 執行對應要做的事情
- `kernel->interrupt->setStatus(UserMode);`
- 回到 `UserMode`
`ExceptionHandler(ExceptionType which)`
- 實作於 [userprog/exception.cc](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/userprog/exception.cc)
- 這邊就是可以自訂 exception、syscall 的地方了
- 因為之前完成過自訂 syscall 的作業,對這邊算熟悉,就不細講這邊,這邊不懂的人請另找跟寫 syscall 相關的文章,應該有所幫助
### Trace8_OneTick
`Interrupt::OneTick()`
- 實作於 [machine/interrupt.cc](https://github.com/LJP-TW/NachOS/blob/540e13362406a95923271c6e653de03dfc3bb9a9/code/machine/interrupt.cc)
此時的 OneTick 為 `UserMode`
:::info
在此整理一下 OneTick 被 call 的狀況
第一次 `UserMode` OneTick 之前會歷經三次 `SystemMode` OneTick
- 第一次 `SystemMode` OneTick
- 在 `ThreadedKernel->Initialize()` 開啟接受中斷,進而呼叫到
- 第二次 `SystemMode` OneTick
- 在 `UserProgKernel->Run()` 初始化 user program,呼叫到 Fork 時,將 thread 放進 readylist 時有關開一次中斷,進而呼叫到
- 第三次 `SystemMode` OneTick
- `SWITCH` 過後,執行每個 user thread 都會先執行的 `Thread::Begin()`,裡頭有將中斷開啟,進而呼叫到
在此例中,若後面加上 debug flag `-d i`,可以看到前三個 Ticks 是一次跳 10,後面則一次跳 1,是因為設定在 machine/stats.h 中的 SystemTick 為 10,UserTick 為 1,而前三次 Ticks 是在 SystemMode 運行,後面的 Ticks 是在 UserMode
:::
### Trace9
這邊高速 Trace 一下,以 debug 資訊為輔助
在執行 nachos 的指令後面加上 `-d i`,可以看到跟 interrupt 有關的 debug 資訊
這邊小小複習一下 [Trace8_Machine::Run](#Trace8_MachineRun)
裡頭的無限迴圈做的事情就是
- 執行一行 user program (MIPS) 的指令
- 其中指令有可能會 `RaiseException` 進而陷入 SystemMode 執行 syscall
- call 一次 `kernel->interrupt->OneTick()`
- 每次 OneTick 就會
- 增加 Ticks 數
- 檢查 pending list 有無到期的中斷要執行
- 若 yieldOnReturn 為 True,則要多做一些事
- 請看 [Trace9_yieldOnReturn](#Trace9_yieldOnReturn)
還有整個程式執行完後的部分也還未追到
- 請看 [Trace10_Halt](#Trace10_Halt)
### Trace9_yieldOnReturn
在初始化時,有創造 `Timer`,並且有註冊在未來 TimerTicks 過後要執行 `Timer` 的 callback
- 可以回顧 [Trace4_Timer](#Trace4_Timer)
- TimerTicks 定義在 machine/stats.h,為 100
- 初始化時 totalTicks 為 0,所以這次註冊是註冊在 totalTicks 為 100 時會執行 `Timer` 的 callback
在剛剛的 [Trace9](#Trace9),在無限迴圈時會一直 call `OneTick`
因為是 UserMode,所以 totalTicks 一次加 UserTick
- UserTick 定義在 machine/stats.h,為 1
當 totalTicks 來到 100 時,`Interrupt::OneTick()` 的執行流程是
- 執行 `CheckIfDue(FALSE);`,裡頭執行了
- `next = pending->Front();`
- 抓取到 Timer 中斷,且現在為要處理這個中斷的時間點
- `kernel->machine->DelayedLoad(0, 0);`
- 將先前有設定要 DelayedLoad 的值正式 load 進去
- `inHandler = TRUE;` 設定正在處理中斷
- 進入 while 迴圈,處理每個到期的中斷
- `next = pending->RemoveFront();`
- 從 pending list 抓取第一個出來處理
- `next->callOnInterrupt->CallBack();`
- 呼叫 `Timer->CallBack()`
- 進一步呼叫 `Alarm->CallBack()`
- 若在 IdleMode、Pending list 中沒有其他未來要處理的中段,且符合一些其他條件,則關閉 `Timer`
- 若符合某些條件,則呼叫 `interrupt->YieldOnReturn();`
- 呼叫 `SetInterrupt();`,若 Timer 沒被 disable,則設定在未來 100 個 Tick 後要再一次執行 Timer 中斷
- `inHandler = FALSE;` 設定沒在處理中斷
- 若 yieldOnReturn 為 True
- 先將 yieldOnReturn 設為 False
- status 進入 SystemMode
- 執行 `kernel->currentThread->Yield();` 準備讓出 CPU
- 請看 [Trace9_yield](#Trace9_yield)
- status 回到原本的 Mode
:::info
所以整個流程可以理解為
- 下一個 Timer 時間點到了之後,就要執行 Content switch
:::
#### Trace9_yield
`Thread::Yield()` 執行的事情是
- 先關閉中斷
- 從 readylist 中找下一個要跑的 thread
- 如果 readylist 沒有 thread,就不做 content switch
- 如果有
- 則執行 `ReadyToRun()` 把 oldThread 丟進 ready list
- 再執行 `kernel->scheduler->Run(nextThread, FALSE);`
- `kernel->scheduler->Run` 主要就是做 Content switch
- 將中斷設為原本的狀態
此例子只有一個 user program,所以這裡不會做 Content switch
### Trace10_Halt
這邊在執行 nachos 的指令後面多加一個 flag `-d ita`,可以多看到跟 Interrupt、Thread、AddrSpace 有關的 debug 資訊
User program 最後會 call syscall `SC_Exit`
Function call stack:
- `Machine::Run` -> `OneInstruction` -> `RaiseException` -> `ExceptionHandler`
- switch case `SC_Exit` 執行了
- `kernel->currentThread->Finish();`
`kernel->currentThread->Finish();` 執行了
- 關閉中斷
- `Sleep(TRUE)` 永久睡去(其實就是死了)
- 設定此 thread status 為 BLOCKED
- While Ready list 沒有 thread
- 呼叫 `kernel->interrupt->Idle();`
- 設定系統進入 IdleMode
- 檢查未來是否還有中斷沒處理
- 有的,有一個 Timer 預計在 totalTicks 為 200 時執行
- 將 Ticks 快轉
- 呼叫 Timer 的 callback
- 呼叫 Alarm 的 callback
- 系統在 IdleMode,未來也沒有中斷了,將 Timer disable
- 呼叫 `Timer::SetInterrupt()`
- Timer 已被 disable,什麼事都不會做
- 設定系統進入 SystemMode
- Ready list 還是沒有 thread
- 呼叫 `kernel->interrupt->Idle();`
- 設定系統進入 IdleMode
- 檢查未來是否還有中斷沒處理
- 沒有
- 呼叫 `Halt()` 進行關機
###### tags: `OS`, `NachOS`