2013年3月16日 星期六

x86 process switch implementation (0) - in dos/x86


我曾經在「os process」提到 os porcess 的切換, 那時候令我興奮不已。但當時的概念還是很模糊, 我決定要用我自己的話來說明這個概念, 也證明我真的搞懂。

化繁為簡是我的學習原則, 對於 process 切換的掌握度還不夠, 打算用自己的方法來實作一個 process 切換的程式。希望這程式符合幾點:
  1. 程式碼小
  2. 在 dos 下執行
  3. 使用 x86 real mode
上述這些條件都是為了簡單, 若能用小小的程式以及簡單的執行環境就可以完成 process 切換, 相信理解起來會容易些。在 dos 使用 .com 執行檔, 可以讓我的程式最大有 64 K 那麼大 (那麼小), 對於所有程式碼都是自己打造的來說, 已經非常夠用, boot 磁區那 512 byte 才真的不夠人用。

那這程式有多小呢? 大概像下面這樣:

simple_proc.S
1 #define STACK_FRAME_OFFSET 6
 2 .code16
 3 .text
 4 .global begin
 5 begin:
 6   xchg %bx, %bx #bochs magic break point
 7   cli
 8 
 9   xor     %eax, %eax
10   mov     %cs,%ax
11   mov     %ax,%ds
12 
13 ## reset 0x30 interrupt
14   movw    $0x0, %bx
15   movw    %bx, %es
16   movw $switch_proc, %es:0xc0 # isr offset
17   movw %ax, %es:0xc2 #isr seg
18 
19 
20   movw    $0xb800, %ax
21   movw    %ax, %gs
22 
23 ## set stack frame eip
24   movw $proc_a, stack_frame
25   movw $proc_b, stack_frame+STACK_FRAME_OFFSET
26 
27 ## set stack frame cs
28   movw %cs, %ax
29   movw %ax, stack_frame+2
30   movw %ax, stack_frame+STACK_FRAME_OFFSET+2
31 
32 ## set stack frame flag
33   # get flag
34   pushf
35   movw (%esp), %ax
36   popf
37   movw %ax, stack_frame+4
38   movw %ax, stack_frame+STACK_FRAME_OFFSET+4
39 
40   int $0x30
41 
42   mov $0x4c00, %ax
43   int $0x21           
44 
45 cur_proc:
46   .word 0x0

51 
52   .space  256, 0
53 proc_stack_top_a:
54   .space  256, 0
55 proc_stack_top_b:
56 
57 stack_frame:
58   .word 0x0# eip
59   .word 0x1# cs
60   .word 0x2# flag
61 
62   .word 0x0# eip
63   .word 0x1# cs
64   .word 0x2# flag
65 
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
79 
80 .global switch_proc
81 switch_proc:
82   movw cur_proc, %dx
83   cmp $stack_frame, %dx
84   je 1f
85   movw $stack_frame, cur_proc
86   jmp 2f
87 1:
88   movw $stack_frame+STACK_FRAME_OFFSET, cur_proc
89 2:
90   movw cur_proc, %sp
91   iret

加了註解還沒超過 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 指令做的事情:
  1. 把旗標暫存器 push 堆疊
  2. 禁止其他中斷發生
  3. 清除陷阱旗標
  4. 把 CS 暫存器 push 堆疊
  5. 把 INT n 的下一指令位址 push 堆疊
  6. 由 0000:(4n) 位址取出中斷服務程式所在位址,並執行長程跳躍指令,至該處繼續執行
節錄 ref 1 的 iret 指令做的事情:
  1. 由堆疊中 pop 4 bytes (cs:ip),並把控制權交到該 4 bytes 所指位址
  2. 由堆疊 pop 旗標暫存器 (2 bytes)
所以就是這樣來讓 proc_a, proc_b 可以輪流執行。先把 proc_a, %cs 的值填到 stack_frame eip, cs 的地方 (stack_frame, stack_frame+2), 將 %sp 指到 stack_frame 或是 stack_frame+6 的地方, 然後發動 iret 即可跳到 proc_a 或是 proc_b。因為 iret 會把 stack_frame, stack_frame+2 載入到 %eip, %cs, 而 cpu 會執行 %cs:%eip 指向的程式碼, 就會去執行 proc_a, 這就是和直接 call proc_a 不同的執行方式。

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 中斷改寫參考資料:
  1. 第 36 章 中斷
  2. 中断矢量表的结构
  3. 中断服务程序
  4. 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
  5. 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

沒有留言:

張貼留言

使用 google 的 reCAPTCHA 驗證碼, 總算可以輕鬆留言了。

我實在受不了 spam 了, 又不想讓大家的眼睛花掉, 只好放棄匿名留言。這是沒辦法中的辦法了。留言的朋友需要有 google 帳號。