|
廟堂之上, 朽木為官; 殿陛之間, 禽獸食祿。 |
基礎知識:
- x86 function call 時的 stack 變化以及 function 如何 return。
如果不懂「基礎知識 1」提到的東西, 這篇文章可能會難倒你, 可參閱組合語言書籍中呼叫 c 語言的那部份。
setjmp/longjmp 這兩組函式我很陌生, 沒怎麼用過這對 function, 不知道什麼場合會出動他們, 只知道在 c 上可以用他們模擬類似 c++ 的 exception handling(請參閱: ref 2)。c++ 上有 exception handling 可用, 這兩個傢伙一點都派不上用場, 在純 c 裡頭才會需要。而 coroutine 似乎也可用這組 function 來實作。
除了 exception handling, 還可以用來寫 coroutine, 以及
user mode thread, 算是比較進階的程式技巧。
我有興趣的是: 「這是」怎麼做的, 由於已經理解 context switch, 沒道理這兩個 function 的實作我做不出來。我想了一下, 應該很簡單, 而實際上也真的很簡單, 讓 souce code 來說話。
不過在這之前先來看看怎麼用 setjmp/longjmp。
執行結果:
ret: 0
init setjmp
xxx
ret: 5
exit
第一次執行 setjmp (L9), setjmp 會記住 L9 的位置 (記住哪個位址呢? 這一行 c 語言會被轉成好幾個組合語言), 而 ret 會設為 0 (setjmp 傳回 0), 在執行 longjmp (L21) 之後, 會跳到 L9, 並且將 ret 設為 5 (longjmp 傳入的 5)。有點神奇, 是嗎?
setjmp 會把目前狀態存下來, 什麼是目前的狀態, 根據以下文章,
Exactly what “program state” does setjmp save?
似乎是很模糊的定義, 一般來說是暫存器, stack, 那浮點數的暫存器需要存起來嗎? 目前看到的實作, 幾乎都沒存浮點數暫存器。stack 當然也沒有保存。
那應該存哪些暫存器呢? 理論來說應該全部都要存吧, 要不然 longjmp 怎麼回復所有的暫存器。
參考 newlib 的實作:
http://sourceware.org/newlib/
x64 (沒研究, 看不懂, 跳過, 似乎沒有保存所有 X64 暫存器)
msp430 (不知道在寫什麼, 跳過)
x86, 跳過 ... A, 再跳這篇就結束了!
終於來到我熟悉的 32bit x86, 來看看 jmp_buf 到底是什麼?真是令人意外, 就是一塊記憶體區域, 用來存這些暫存器的值, 不是什麼複雜的資料結構。jmp_buf 可以看成一個指標指向一塊記憶體, setjmp 會接收這個指標當參數, 然後把暫存器存在這裡 (L24~L34 那些暫存器)。
所以 setjmp 就是把這些暫存器存起來, 那 eip 存什麼呢?就存 return address (src/newlib/libc/sys/sysvi386/_setjmp.S L20, 21), 然後 longjmp
會想辦法跳到這裡; L11 ~ L19 則是把其他暫存器存到其他的 offset 上, 不過 offset 24 應該是 %ebp, 28 應該是 %esp, 把上面的這兩個欄位對就即可, 不過不影響程式的正確性, 因為 longjmp 也寫反了。
比較重要的是 eip, 和 esp, 我們關注這 2 個暫存器的值是怎麼保存的。
table 1 是 jmp_buf 的內容, jmp_buf[32] 存 eip 的值, jmp_buf[28] 存 esp 的值, 其他就是另外的那些暫存器。
table 1. jmp_buf
32 | eip |
28 | esp |
24 | ebp |
20 | edi |
16 | esi |
12 | edx |
8 | ecx |
4 | ebx |
0 | eax |
c 的 setjmp(jmp_buf_var); 會被翻譯成 list 1. 的組合語言指令。table 2 是 list 1 在執行時, stack 的變化。在執行 list 1 L1 之前, 對應到 table 2 L2, eps 是 0xffffcff4, 當執行 push $0x804a060, eps 先減 4, 變成 0xffffcff0, 再把 0x804a060 複製到 0xffffcff0 這個位址。所以這時候的 stack 變化為 table 2 L3, 而被 push 的 0x804a060 就是 setjmp 的參數 jmp_buf。
當執行 call 0x8048515, esp 先減 4, 變成 0xffffcfec, 再把 return address 0x80484e2 (list 1 L3) 複製到 0xffffcfec 這個位址。
所以當執行到 list 2 L11 (setjmp 的第一個指令) 的時候, 這時候的 eps 是 0xffffcfec, return address 在 0xffffcfec, setjmp 就是要把這個位址存起來。 src/newlib/libc/sys/sysvi386/_setjmp.S L20, 21 就是在把這個 return address 存到 jmp_buf[32]。
而 jmp_buf 在 0xffffcff0, 所以要操作這個位址來存暫存器的值, 所以應該是 esp + 4 的位址, 那位什麼 src/newlib/libc/sys/sysvi386/_setjmp.S L10 是取 esp + 8 呢? 因為 setjmp 一進來就 push ebx, 導致 esp 又減 4, 所以 esp + 8 才是 jmp_buf 的位址。
user mode thread 實作就是透過改變 eip, esp 欄位, 來讓 thread function 有自己的 stack, 也讓這個 thread 從該 function 第一行開始執行。
table 2. call setjmp 的 stack frame
1 esp | | |
2 0xffffcff4 | | |
3 0xffffcff0 | 0x804a060 (傳給 setjmp 的 jmb_buf) | push $0x804a060 |
4 0xffffcfec | return address: 0x80484e2 | call 0x8048515 |
總之, setjmp 就是把的當時的 stack frame 的狀態存起來,但是只有暫存器的部份, 一旦用 longjmp 回到那時候的 stack frame, 但是 stack frame 裡頭的資料和當時儲存的時候不同, 那就 ... 嘿嘿, 因為 setjmp 並沒有保存 stack, 這是和 context switch 的不同之處。
longjmp 自然做的是相反的事情, 把 jmp_buf 裡頭的值設定給所有暫存器, 還原當時 setjmp 的執行環境。
newlib/newlib/libc/machine/i386/setjmp.S
這是另外一個版本, 有點不同。
L14 則是 longjmp 傳入的數字, 在 longjmp 回到 setjmp 的時候, 拿來改變 setjmp 的 return value。
怎麼改變的, 以下是呼叫 setjmp 時的反組譯程式碼, setjmp 存的就是 L184 (address: 80484d2), longjmp 發動時就是回到 L184; 而改變了 %eax, 就會改變 setjmp 的回傳值 (ref L184)。
L19 設定這次的 function return address, 這樣離開時, 就會回到 setjmp 存的位址。
知一可以求五, 舉一可以反三, 相信你可以做出 arm cortex m3 的版本。
這裡 /arm-eabi-toolchain/newlib-2013.05/newlib/libc/machine/arm/setjmp.S 可以偷看哦!你要自己試試看, 還是等我下篇文章呢?
我打算在某聚會介紹這個, 順便提提為什麼 sjlj_1.c 是有問題的, 但在我的平台可以執行這程式而沒有錯誤。
20140227 補充:
這個版本有些錯誤, 在遇到 register 變數時有可能會出錯。
這是修改後的版本:
https://github.com/descent/progs/blob/master/coroutine/my_setjmp.S
https://github.com/descent/progs/blob/master/coroutine/sample.c 就是讓這個實作出錯的例子, 我已經修改完成, 目前沒問題了, 但不保證沒有其他問題。
ref:
- 全面了解setjmp与longjmp(C语言异常处理机制)
- 第15集 C语言中的异常处理机制, 15~19 在講怎麼用這個作類似 c++ exception 的功能。
- SETJMP & LONGJMP Implementation Analysis
- Understanding Setjmp/Longjmp (pdf)
很棒的文章,谢谢
回覆刪除很高興你喜歡這篇文章。
回覆刪除