這個是副產品, 在寫 process switch 這系列時, 順到想的, 因為不是很好表達, 我只在 mosut 講這個, 並沒有想要 blog 一篇, 不過還是寫出來好了, 最主要是給我回憶用的。
我一直在思考, 如何才能用程式碼來表達出 race condition, 而不是老師上課教的, 只能用冥想的方式來理解。
以前老師的例子就是
a=0;
proc_a:
a=a+1;
proc_b:
a=a+1;
若是 a 的初始值為 0, 由於 a=a+1 會被 c compiler 編譯成 3 個指令:
mov 0x804a018,%eax
add $0x1,%eax
mov %eax,0x804a018
所以若不一次做完這 3 個組合語言指令, 在 proc_a, proc_b 執行過後, a 的值可能只被加了一次, 最終結果不是 2, 而是 1。
以下的程式碼便是用來產生這樣的結果, 由於我可以決定 int $0x30 的位置, 所以可以很容易複製這樣的行為, 若是靠 timer 來中斷, 要遇到這樣的巧合就很難控制了。
程式很短, 但不代表他很簡單, 需要了解 x86 process switch implementation(0), x86 process switch implementation(1), 否則應該看不懂這篇。
L66 ~ 68 是一個完整的 a=a+1; 而 L54 ~ 57 被我安插了 int $0x30, 導致 a=a+1 不能完整執行就會 switch 要另外一個 process, 這個程式的表現一樣要使用 bochs single step, 在 blog 上我只能儘量表達清楚, 無法帶你跑一次 (其實我可以錄下 bochs 畫面, 不過不用這麼折磨我吧)。
那麼要如何解決這問題, 其中之一是使用 spin lock, x86 可以參考 test and set 指令實作出 spin lock, 將這 3 個指令保護下來, 一次做完這 3 個指令:
http://en.wikipedia.org/wiki/X86_instruction_listings
http://en.wikipedia.org/wiki/Test-and-set
2013年5月23日 星期四
2013年4月30日 星期二
x86 process switch implementation (1.5) - save/restore in real mode
遲來的 1.5 篇, 我都已經辛苦的把 x86 protected mode 寫完了說。
我終於想出如何 save/restore in x86/dos real mode, 實在令我開心, 距離第一篇將近有 45 天的時間。這個程式是原創等級的程式, 在我苦思如何在 stm32f4discovery (arm cortex m4) 實作 context switch 時來的靈感。在 100 行左右的組合語言程式碼就可完成, 實在是小而美。
當從 proc_a 到 (int $0x30) switch_proc 的 stack 變化, 這次不再是手工圖了, 有了漂亮的表格可看, 數字部份就是 stack 的位址變化。
所以再次回到 proc_a 時, 只要讓 esp 回到 0x495 即可。說的果然比做的容易多了, 那麼要怎麼實作呢?
這次捨棄固定的 stack_frame_a, stack_frame_b, 在每次進入 switch_proc 把 esp 存起來即可, 再根據 cur_proc 決定要存在 a_sp 或 b_sp, 並把 esp 指向 a_sp 或 b_sp 完成下次的 process switch。
有了這個, 這程式可以說是 context switch, 不只是 process switch 了, 已經可以把 context save/restore。
這是 proc_b 的 stack 變化過程:
要體驗她, 一樣需要在 bochs single step 過才能有所體會。
source code:
https://github.com/descent/process/
real_mode_stack branch
我終於想出如何 save/restore in x86/dos real mode, 實在令我開心, 距離第一篇將近有 45 天的時間。這個程式是原創等級的程式, 在我苦思如何在 stm32f4discovery (arm cortex m4) 實作 context switch 時來的靈感。在 100 行左右的組合語言程式碼就可完成, 實在是小而美。
當從 proc_a 到 (int $0x30) switch_proc 的 stack 變化, 這次不再是手工圖了, 有了漂亮的表格可看, 數字部份就是 stack 的位址變化。
497 | |
495 | %cx |
493 | flag |
491 | %cs |
48f | %eip |
48d | %ax |
所以再次回到 proc_a 時, 只要讓 esp 回到 0x495 即可。說的果然比做的容易多了, 那麼要怎麼實作呢?
這次捨棄固定的 stack_frame_a, stack_frame_b, 在每次進入 switch_proc 把 esp 存起來即可, 再根據 cur_proc 決定要存在 a_sp 或 b_sp, 並把 esp 指向 a_sp 或 b_sp 完成下次的 process switch。
有了這個, 這程式可以說是 context switch, 不只是 process switch 了, 已經可以把 context save/restore。
這是 proc_b 的 stack 變化過程:
59f | |
59d | %bx |
flag | |
%cs | |
597 | %eip |
595 | %ax |
要體驗她, 一樣需要在 bochs single step 過才能有所體會。
source code:
https://github.com/descent/process/
real_mode_stack branch
2013年3月16日 星期六
x86 process switch implementation (0) - in dos/x86
我曾經在「os process」提到 os porcess 的切換, 那時候令我興奮不已。但當時的概念還是很模糊, 我決定要用我自己的話來說明這個概念, 也證明我真的搞懂。
化繁為簡是我的學習原則, 對於 process 切換的掌握度還不夠, 打算用自己的方法來實作一個 process 切換的程式。希望這程式符合幾點:
- 程式碼小
- 在 dos 下執行
- 使用 x86 real mode
那這程式有多小呢? 大概像下面這樣:
加了註解還沒超過 100 行的 x86 組合語言。是的, 這是組合語言, 因為 c 語言還沒低階到可以設定暫存器, 設定暫存器是組合語言的工作。
66 .global proc_a 67 proc_a: 68 1: 69 mov $0x1, %ax 70 int $0x30 71 jmp 1b 72 73 .global proc_b 74 proc_b: 75 1: 76 mov $0x2, %bl 77 int $0x30 78 jmp 1b
proc_a 和 proc_b 便是我們的兩個 process, 你可能會抗議那明明就只是兩個 function。是的, 你沒說錯, 但你寫的 c main 程式是不是也只是個 function, 而你卻認為他是一個 process 呢? 若是使用 call proc_a, call proc_b 的方式來執行, 那是 function 的用法, 不是 process, 所以接下來的程式碼要用很其特的方法 (iret) 來執行 proc_a, proc_b。
這兩個 process 只有 3 行, 夠簡單, 只要略懂組合語言的程式員, 幾乎不用說明就可看懂。使用的 process 切換方式是類似 windows 3.1 的 non-preemptive 方式, 需要由 process 自己釋放 cpu 來讓其他 process 執行。int $0x30 就是用來做這件事情。當然我可以把 int $0x30 包裝成類似 os_yield(), 不過這樣複雜度就提高了。
jmp 1b 就是回到 lable 1, L78 的 jmp 1b 回到 L75, L71 的 jmp 1b 回到 L68, 很直覺, 所以這個 process 只做一件事情, 把 0x1 放到 ax 暫存器, 另外一個 process 把 0x2 放到 bl 暫存器, 就是這麼簡單。
上圖可以說明一切, 也許你會嫌字很醜, 應該用電腦畫才是, 不過手工可是很難得的。重點在 stack_frame, 裡頭有 3 個欄位: 分別代表 eip, cs, flag, 用來儲存 proc_a, proc_b 目前的這 3 個值。int 0x30 isr 便是切換 stack_frame, stack_frame+6 來執行 proc_a, proc_b。
節錄 ref 1 的 int 指令做的事情:
- 把旗標暫存器 push 堆疊
- 禁止其他中斷發生
- 清除陷阱旗標
- 把 CS 暫存器 push 堆疊
- 把 INT n 的下一指令位址 push 堆疊
- 由 0000:(4n) 位址取出中斷服務程式所在位址,並執行長程跳躍指令,至該處繼續執行
- 由堆疊中 pop 4 bytes (cs:ip),並把控制權交到該 4 bytes 所指位址
- 由堆疊 pop 旗標暫存器 (2 bytes)
int 指令則會把下一個指令的 %cs:%eip 存到 %ss:%esp 指到的地方, 所以 int 發動的時候, 會把 proc_a 下個指令存到 stack_frame, stack_frame+2 裡頭, 等著我們下次發動 iret 再讓 proc_a 執行起來; 執行 proc_b 也是同樣的道理, 很容易理解吧!
這程式的執行結果不重要, 重要的是執行過程, 怎麼感受這個執行過程? 這就稍微難一點, 我是用 bochs 內建 debugger single step, 觀察所有暫存器, stack 來檢查程式是否有真的執行切換。
6 xchg %bx, %bx #bochs magic break point
是 bochs magic break point, 程式執行到這行, 會讓 bochs 中斷停下來, 就可使用 single step指令來觀察整個程式行為。
而程式的過程便是在 proc_a, proc_b 之前相互執行, 為了簡單, 我沒有印出任何字元, 所以從螢幕上看不出任何事情, 為了有趣, 我自己倒是寫了一個印出 a, b 的版本, 有興趣的朋友可以自己改看看, 在 proc_a 印出 a, prob_b 印出 b。
這個程式該怎麼執行呢? makefile 規則會把這隻程式編譯成 .com, 直接 copy 到 dos 執行即可, 再使用 bochs 的內建除錯器就可以追蹤整個流程。dos 環境最好不要載入任何記憶體管理程式, ex: himem.sys, emm386.exe, 在我測試改寫 0x30 中斷時, 會造成一些問題, 我花了不少時間排除這問題。
程式很簡單, 說明也很簡單, 希望不要造成誤會, 如果你已經理解這篇的解釋, process switch 並沒有這麼簡單, 我簡化很多東西, 這沒有考慮很週嚴 (ex: 沒有保存所有的暫存器, switch_proc 沒有保存自己的 stack), 真正的 process switch 還要加上不少 code, 而且我還沒搞定 x86 real mode 如何保存 %esp 的問題。x86 保護模式在權限切換時, iret/int 指令會保存 %ss:%esp。這程式若用上保護模式, 那得加上不少 code, 模糊了我要表達的事情, 就先這樣。
儘管有如此缺失, 但用來作為 process switch 的實作理解, 不到 100 行的組合語言程式能發揮如此功用, 已經足夠。
下篇文章 x86 process switch implementation (1) 就會複雜一點了。
soure code:
https://github.com/descent/process
git commit : d25cb21e036b953f19ec69610c411c550dcfa8d6
這個程式的除錯相當麻煩, 沒有 ice 等級的除錯器, 要寫好她相當困難。好在有 bochs。
x86 中斷改寫參考資料:
- 第 36 章 中斷
- 中断矢量表的结构
- 中断服务程序
- http://books.google.com.tw/
books?id=LPZDMQvMvwMC&pg= PA208&lpg=PA208&dq=%E4%BF%AE% E6%94%B9%E4%B8%AD%E6%96%B7%E5% 90%91%E9%87%8F+dos&source=bl& ots=d0LzeMfr-F&sig=CgacTkwyR_ bi6pQQN3kC7onPa0g&hl=zh-TW&sa= X&ei=8n4rUcemIoqIkwXjsYGIDQ& ved=0CDcQ6AEwAQ#v=onepage&q=% E4%BF%AE%E6%94%B9%E4%B8%AD%E6% 96%B7%E5%90%91%E9%87%8F%20dos& f=false - https://www.google.com.tw/
search?q=%E4%BF%AE%E6%94%B9% E4%B8%AD%E6%96%B7%E5%90%91%E9% 87%8F+dos&ie=utf-8&oe=utf-8& aq=t&rls=org.mozilla:en-US: official&client=firefox-a& channel=fflb
2013年3月15日 星期五
x86 process switch implementation (1) - save %ax
和前面那一個版本只差了一些小地方:
proc_a, proc_b 會同時使用 %ax, 所以在切換這兩個 process 時, %ax 需要 save/restore, 總不能切回 proc_a 時, %ax 變成 2, 這就不對了。要怎麼做呢?process switch 的重點也在於此。
在 stack frame 上用一個欄位來保存 %ax, 在 int 0x30 切換 process 時, 保存和回復這個 %ax 即可。聽起來很簡單, 但一開始我可是花了一星期才搞懂 (希望大家不要像我一樣, 花這麼多的時間)。L65, L75 就是用來做這件事情。
這一樣要使用 bochs 內建 debugger 來跑過一次才能有所體會, 並且將相關暫存器, 記憶體內容 dump 出來, 程式體驗有時候甚至比理解觀念還重要。我只能解釋到這裡, 其他就得靠讀者們自己跑程式碼。不過還是希望不要讓大家誤會 process switch 是如此簡單, 一樣是省略很多不太容易說明的細節 (ex: stack 的保存與回復) 才想到的解釋方式。會 save/restore %ax 之後, 當然其他的 register 應該也沒問題, 依樣畫葫蘆即可。
這系列的目的不是說明 process switch 的實作, 只是希望能幫助有興趣的同好在看相關的程式碼時, 能容易看些。
該來的還是跑不掉, 我本來不想提及保護模式, 這得加上不少程式碼, 違反了我提到的簡單原則, 我只想將重點擺在 process switch, 不過最後的說明還是得用到, x86 process switch implementation (2) 要談談恐怖的保護模式。
49 .global proc_a 50 proc_a: 51 1: 52 mov $0x1, %ax 53 int $0x30 54 jmp 1b 55 56 .global proc_b 57 proc_b: 58 1: 59 mov $0x2, %al 60 int $0x30 61 jmp 1b
proc_a, proc_b 會同時使用 %ax, 所以在切換這兩個 process 時, %ax 需要 save/restore, 總不能切回 proc_a 時, %ax 變成 2, 這就不對了。要怎麼做呢?process switch 的重點也在於此。
87 stack_frame_a: 88 .word 0x9# ax 89 .word 0x0# eip 90 .word 0x1# cs 91 .word 0x2# flag 92 93 stack_frame_b: 94 .word 0x9# ax 95 .word 0x0# eip 96 .word 0x1# cs 97 .word 0x2# flag
在 stack frame 上用一個欄位來保存 %ax, 在 int 0x30 切換 process 時, 保存和回復這個 %ax 即可。聽起來很簡單, 但一開始我可是花了一星期才搞懂 (希望大家不要像我一樣, 花這麼多的時間)。L65, L75 就是用來做這件事情。
這一樣要使用 bochs 內建 debugger 來跑過一次才能有所體會, 並且將相關暫存器, 記憶體內容 dump 出來, 程式體驗有時候甚至比理解觀念還重要。我只能解釋到這裡, 其他就得靠讀者們自己跑程式碼。不過還是希望不要讓大家誤會 process switch 是如此簡單, 一樣是省略很多不太容易說明的細節 (ex: stack 的保存與回復) 才想到的解釋方式。會 save/restore %ax 之後, 當然其他的 register 應該也沒問題, 依樣畫葫蘆即可。
這系列的目的不是說明 process switch 的實作, 只是希望能幫助有興趣的同好在看相關的程式碼時, 能容易看些。
該來的還是跑不掉, 我本來不想提及保護模式, 這得加上不少程式碼, 違反了我提到的簡單原則, 我只想將重點擺在 process switch, 不過最後的說明還是得用到, x86 process switch implementation (2) 要談談恐怖的保護模式。
2013年3月14日 星期四
x86 process switch implementation (2) - x86 protected mode verison
我知道, 長很多是吧!除了進入保護模式的差異, 這程式和 x86 process switch implementation (1) - save %ax 做的是一樣的事情。而 int 0x30 就不僅僅是改改位址就好, 需要設定 idt。而且 stack 長度也由 2byte 變為 4byte。
要完全看懂這程式可能很不容易, 但是如果把焦點一樣放在 process switch, 不要管那些保護模式相關的程式碼, 應該還是可以看懂。
L114 ~ 214 都很熟悉吧!和 x86 real mode 版本完全一樣。那我是不是很無聊做了個保護模式的版本, 只是要炫燿, 當然不是, 我需要保護模式的另外一個功能來完成最後的步驟 - stack save/restore。
請看下一篇, A ... 沒下一篇阿!糟糕 xD!!
要完全看懂這程式可能很不容易, 但是如果把焦點一樣放在 process switch, 不要管那些保護模式相關的程式碼, 應該還是可以看懂。
L114 ~ 214 都很熟悉吧!和 x86 real mode 版本完全一樣。那我是不是很無聊做了個保護模式的版本, 只是要炫燿, 當然不是, 我需要保護模式的另外一個功能來完成最後的步驟 - stack save/restore。
請看下一篇, A ... 沒下一篇阿!糟糕 xD!!
2013年3月13日 星期三
x86 process switch implementation (3) - stack save/restore
這個版本程式碼大上不少, 難度也增加不少, 為減少一點複雜度, 這篇省略了 int 0x13 isr 的 stack 部份。這次要把 proc_a, proc_b 的 stack 保存/恢復。
為了要達成在真實模式下無法達到的 stack 保存/恢復 (其實是我想不到什麼方法, 所以只好出動保護模式), 需要使用保護模式的權限切換, 所以程式才變成如此複雜, 若不使用權限切換, 則和真實模式一樣, iret/int 不會保存/恢復 stack。
在這樣的環境下, iret/int 指令除了之前提到的行為還會將 %ss:%esp 的值也一起保存/恢復。
一開始, 在權限方面的 gdt, idt 設定有些問題 (要在 ring0/ring3 切換), bochs 幫了很大的忙, 讓我除錯的速度加快不少, 若沒這個 bochs, 我可能還在冥想程式哪裡出錯了。真的很佩服之前寫 os 的人, 頭腦掉很清楚才能寫出正確的 os 程式。
我沒打算解釋保護模式下的權限切換, 從 L209 開始看起即可。這邊的 code 就很單純, 把 stack_frame_a, stack_fram_b 填上需要的值, 和之前的真實模式比起來, 多了 %ss:%esp。而在保護模式下, %ss, %cs 也變成了 selector, 所以如果不熟保護模式, 也不要管這個, 單純看 %eip, % esp 即可, 重點是把 process switch 搞懂, 而不是注重在別的地方。
如果你真想完全搞懂, 需要保護模式相關知識:
請參考: 杨文博的自己動手寫作業系統
先來檢視這些事情:
手工圖又來了, 老問題, 很醜是吧!別為難我了, 看到那複雜的曲線, 我可畫不了精美的版本, 就將就點。 stack_fame_a, stack_frame_b 多了兩個欄位用來保存 %ss, %esp, iret 發動前, 將 %ss:%esp 指到 ① 或 ② , iret 發動後, eip, es, flag, esp, ss 就會依序被填入
stack_fame_a + 4
stack_fame_a + 8
stack_fame_a +12
stack_fame_a +16
stack_fame_a + 20
所指到的值。
而 int $0x30 發動時, stack 會怎麼變化呢?
這時是從 ring 3 -> ring0, stack 會使用 tss 的 ss0:esp0 來填入 %ss:%esp。
所以 tss 的 ss0:esp0 該怎麼設定呢?
填入 ③ 或 ④ 的位址。
假設現在要切換到 proc_a, 程式要做的事情有:
tss 的 esp0 要填入 ③ 的位址 (ss 簡單起見, 使用相同的 selector), %esp 填入 ① 的位址, 最後發動 iret。
從 proc_a 執行 int $0x13 時, stack 變化方式為:
esp 的值變成 ③ 而
ss, esp, flag, es, eip 依序 push 到 stack,當最終切換到 int 0x13 isr 時, eps 在 ① 的位置, 在 push %eax 之後, 剛好把 proc_a 所有的東西 (%eax, ss, esp, flag, es, eip) 都保存下來。
這時 int 0x13 isr 繼續準備 proc_b 的切換動作:
tss 的 esp0 要填入 ④ 的位址 (ss 簡單起見, 使用相同的 selector), %esp 填入 ② 的位址, 最後發動 iret。
疑, 沒有我想像中的難表達, 不過不知道讀者們有沒搞懂, 希望沒讓你們更糊塗了。
最後還剩下 isr 本身的 stack, 這部份相對簡單, 應該可以不用出 x86 process switch implementation (4) 了。
source code:
https://github.com/descent/process protected branch
① ② ③ ④ symbol ref:
http://blog.yam.com/picryam/article/10903476
http://blog.aican.info/2008/04/blog-post_10.html
為了要達成在真實模式下無法達到的 stack 保存/恢復 (其實是我想不到什麼方法, 所以只好出動保護模式), 需要使用保護模式的權限切換, 所以程式才變成如此複雜, 若不使用權限切換, 則和真實模式一樣, iret/int 不會保存/恢復 stack。
在這樣的環境下, iret/int 指令除了之前提到的行為還會將 %ss:%esp 的值也一起保存/恢復。
一開始, 在權限方面的 gdt, idt 設定有些問題 (要在 ring0/ring3 切換), bochs 幫了很大的忙, 讓我除錯的速度加快不少, 若沒這個 bochs, 我可能還在冥想程式哪裡出錯了。真的很佩服之前寫 os 的人, 頭腦掉很清楚才能寫出正確的 os 程式。
我沒打算解釋保護模式下的權限切換, 從 L209 開始看起即可。這邊的 code 就很單純, 把 stack_frame_a, stack_fram_b 填上需要的值, 和之前的真實模式比起來, 多了 %ss:%esp。而在保護模式下, %ss, %cs 也變成了 selector, 所以如果不熟保護模式, 也不要管這個, 單純看 %eip, % esp 即可, 重點是把 process switch 搞懂, 而不是注重在別的地方。
如果你真想完全搞懂, 需要保護模式相關知識:
- 如何進入保護模式?
- 如何設定 idt?
- 如何從 ring0 -> ring3 -> ring0?
請參考: 杨文博的自己動手寫作業系統
先來檢視這些事情:
- stack frame a, stack frame b 的內容
- tss
- 執行 iret 切換到 proc_a 檢查 %ss:%esp
- int $0x13 檢查 %ss:%esp
寄件者 write_os |
手工圖又來了, 老問題, 很醜是吧!別為難我了, 看到那複雜的曲線, 我可畫不了精美的版本, 就將就點。 stack_fame_a, stack_frame_b 多了兩個欄位用來保存 %ss, %esp, iret 發動前, 將 %ss:%esp 指到 ① 或 ② , iret 發動後, eip, es, flag, esp, ss 就會依序被填入
stack_fame_a + 4
stack_fame_a + 8
stack_fame_a +12
stack_fame_a +16
stack_fame_a + 20
所指到的值。
而 int $0x30 發動時, stack 會怎麼變化呢?
這時是從 ring 3 -> ring0, stack 會使用 tss 的 ss0:esp0 來填入 %ss:%esp。
所以 tss 的 ss0:esp0 該怎麼設定呢?
填入 ③ 或 ④ 的位址。
假設現在要切換到 proc_a, 程式要做的事情有:
tss 的 esp0 要填入 ③ 的位址 (ss 簡單起見, 使用相同的 selector), %esp 填入 ① 的位址, 最後發動 iret。
從 proc_a 執行 int $0x13 時, stack 變化方式為:
esp 的值變成 ③ 而
ss, esp, flag, es, eip 依序 push 到 stack,當最終切換到 int 0x13 isr 時, eps 在 ① 的位置, 在 push %eax 之後, 剛好把 proc_a 所有的東西 (%eax, ss, esp, flag, es, eip) 都保存下來。
這時 int 0x13 isr 繼續準備 proc_b 的切換動作:
tss 的 esp0 要填入 ④ 的位址 (ss 簡單起見, 使用相同的 selector), %esp 填入 ② 的位址, 最後發動 iret。
疑, 沒有我想像中的難表達, 不過不知道讀者們有沒搞懂, 希望沒讓你們更糊塗了。
最後還剩下 isr 本身的 stack, 這部份相對簡單, 應該可以不用出 x86 process switch implementation (4) 了。
source code:
https://github.com/descent/process protected branch
① ② ③ ④ symbol ref:
http://blog.yam.com/picryam/article/10903476
http://blog.aican.info/2008/04/blog-post_10.html
訂閱:
文章 (Atom)