2014年1月16日 星期四

setjmp/longjmp 實作 (x86 32 bit)

廟堂之上, 朽木為官; 殿陛之間, 禽獸食祿。

基礎知識:
  1. 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。

t0.c
 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <setjmp.h>
 4 
 5 jmp_buf mark;
 6 
 7 int main(int argc, char *argv[])
 8 {
 9   int ret = setjmp(mark);
10   printf("ret: %d\n", ret);
11   if (ret == 0)
12   {
13     printf("init setjmp\n");
14   }
15   else
16   {
17     printf("exit\n");
18     exit(0);
19   }
20   printf("xxx\n");
21   longjmp(mark, 5);
22   return 0;
23 }

執行結果:
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 暫存器)
src/newlib/libc/machine/x86_64/setjmp.S
 1 /*
 2  * ====================================================
 3  * Copyright (C) 2007 by Ellips BV. All rights reserved.
 4  *
 5  * Permission to use, copy, modify, and distribute this
 6  * software is freely granted, provided that this notice
 7  * is preserved.
 8  * ====================================================
 9  */
10 
11  /*
12  **  jmp_buf:
13  **   rbx rbp r12 r13 r14 r15 rsp rip
14  **   0   8   16  24  32  40  48  56
15  */
16 
17   #include "x86_64mach.h"
18 
19   .global SYM (setjmp)
20   .global SYM (longjmp)
21   SOTYPE_FUNCTION(setjmp)
22   SOTYPE_FUNCTION(longjmp)
23 
24 SYM (setjmp):
25   movq    rbx,  0 (rdi)
26   movq    rbp,  8 (rdi)
27   movq    r12, 16 (rdi)
28   movq    r13, 24 (rdi)
29   movq    r14, 32 (rdi)
30   movq    r15, 40 (rdi)
31   leaq    8 (rsp), rax
32   movq    rax, 48 (rdi)
33   movq    (rsp), rax
34   movq    rax, 56 (rdi)
35   movq    $0, rax
36   ret
37 
38 SYM (longjmp):
39   movq    rsi, rax        /* Return value */
40 
41   movq     8 (rdi), rbp
42 
43   __CLI
44   movq    48 (rdi), rsp
45   pushq   56 (rdi)
46   movq     0 (rdi), rbx
47   movq    16 (rdi), r12
48   movq    24 (rdi), r13
49   movq    32 (rdi), r14
50   movq    40 (rdi), r15
51   __STI
52 
53   ret

msp430 (不知道在寫什麼, 跳過)
src/newlib/libc/machine/msp430/setjmp.S
  1 /* Copyright (c) 2013  Red Hat, Inc. All rights reserved.
  2 
  3    This copyrighted material is made available to anyone wishing to use,
  4    modify, copy, or redistribute it subject to the terms and conditions
  5    of the BSD License.   This program is distributed in the hope that
  6    it will be useful, but WITHOUT ANY WARRANTY expressed or implied,
  7    including the implied warranties of MERCHANTABILITY or FITNESS FOR
  8    A PARTICULAR PURPOSE.  A copy of this license is available at
  9    http://www.opensource.org/licenses. Any Red Hat trademarks that are
 10    incorporated in the source code or documentation are not subject to
 11    the BSD License and may only be used or replicated with the express
 12    permission of Red Hat, Inc.
 13 */
 14 
 15 # setjmp/longjmp for msp430.  The jmpbuf looks like this:
 16 #
 17 # Register Jmpbuf offset
 18 #               small   large 
 19 # r0 (pc) 0x00     0x00
 20 # r1 (sp) 0x02  0x04 
 21 # r4  0x04  0x08
 22 # r5  0x06  0x0c
 23 # r6  0x08  0x10
 24 # r7  0x0a  0x14
 25 # r8  0x0c  0x18
 26 # r9  0x0e  0x1c
 27 # r10  0x10  0x20
 28 
 29  .text
 30  .global setjmp
 31 setjmp:
 32  ; Upon entry r12 points to the jump buffer.
 33  ; Returns 0 to caller.
 34  
 35 #if   defined __MSP430X_LARGE__
 36  mova   @r1, r13
 37  mova    r13, 0(r12)
 38  mova r1,  4(r12)
 39  mova r4,  8(r12)
 40  mova r5,  12(r12)
 41  mova r6,  16(r12)
 42  mova r7,  20(r12)
 43  mova r8,  24(r12)
 44  mova r9,  28(r12)
 45  mova r10, 32(r12)
 46  clr    r12
 47  reta
 48 #else
 49  ;; Get the return address off the stack
 50  mov.w  @r1,  r13
 51  mov.w   r13, 0(r12)
 52  mov.w r1,  2(r12)
 53  mov.w r4,  4(r12)
 54  mov.w r5,  6(r12)
 55  mov.w r6,  8(r12)
 56  mov.w r7,  10(r12)
 57  mov.w r8,  12(r12)
 58  mov.w r9,  14(r12)
 59  mov.w r10, 16(r12)
 60  clr    r12
 61  ret
 62 #endif 
 63 
 64  
 65  .global longjmp
 66 longjmp:
 67  ; Upon entry r12 points to the jump buffer and
 68         ; r13 contains the value to be returned by setjmp.
 69 
 70 #if   defined __MSP430X_LARGE__
 71  mova @r12+, r14
 72  mova @r12+, r1
 73  mova @r12+, r4
 74  mova @r12+, r5
 75  mova @r12+, r6
 76  mova @r12+, r7
 77  mova @r12+, r8
 78  mova @r12+, r9
 79  mova @r12+, r10
 80 #else
 81  mov.w @r12+, r14
 82  mov.w @r12+, r1
 83  mov.w @r12+, r4
 84  mov.w @r12+, r5
 85  mov.w @r12+, r6
 86  mov.w @r12+, r7
 87  mov.w @r12+, r8
 88  mov.w @r12+, r9
 89  mov.w @r12+, r10
 90 #endif
 91  ; If caller attempts to return 0, return 1 instead.
 92  cmp.w   #0, r13
 93  jne .Lnot_zero
 94  mov.w #1, r13
 95 .Lnot_zero:
 96  mov.w r13, r12
 97 
 98 #if   defined __MSP430X_LARGE__
 99  adda     #4, r1
100  mova r14, r0
101 #else
102  add.w    #2, r1
103  mov.w r14, r0
104 #endif

x86, 跳過 ... A, 再跳這篇就結束了!

終於來到我熟悉的 32bit x86, 來看看 jmp_buf 到底是什麼?真是令人意外, 就是一塊記憶體區域, 用來存這些暫存器的值, 不是什麼複雜的資料結構。jmp_buf 可以看成一個指標指向一塊記憶體, setjmp 會接收這個指標當參數, 然後把暫存器存在這裡 (L24~L34 那些暫存器)。

include/machine/setjmp-dj.h

 1 /*
 2  * Copyright (C) 1991 DJ Delorie
 3  * All rights reserved.
 4  *
 5  * Redistribution, modification, and use in source and binary forms is permitted
 6  * provided that the above copyright notice and following paragraph are
 7  * duplicated in all such forms.
 8  *
 9  * This file is distributed WITHOUT ANY WARRANTY; without even the implied
10  * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11  */
12 
13 /* Modified to use SETJMP_DJ_H rather than SETJMP_H to avoid
14    conflicting with setjmp.h.  Ian Taylor, Cygnus support, April,
15    1993.  */
16 
17 #ifndef _SETJMP_DJ_H_
18 #define _SETJMP_DJ_H_
19 
20 #ifdef __cplusplus
21 extern "C" {
22 #endif
23 
24 typedef struct {
25   unsigned long eax;
26   unsigned long ebx;
27   unsigned long ecx;
28   unsigned long edx;
29   unsigned long esi;
30   unsigned long edi;
31   unsigned long ebp;
32   unsigned long esp;
33   unsigned long eip;
34 } jmp_buf[1];
35 
36 extern int setjmp(jmp_buf);
37 extern void longjmp(jmp_buf, int);
38 
39 #ifdef __cplusplus
40 }
41 #endif
42 
43 #endif

所以 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 第一行開始執行。

list 1
1 0x80484d8 main+20     push   $0x804a060
2 0x80484dd main+25     call   0x8048515 [setjmp]
3 0x80484e2 main+30     add    $0x10,%esp 


list 2 setjmp 部份程式碼
11 0x8048515 setjmp       push   %ebx
12 0x8048516 setjmp+1     mov    0x8(%esp),%ebx
13 0x804851a setjmp+5     mov    %eax,(%ebx)
14 0x804851c setjmp+7     pop    %eax
15 0x804851d setjmp+8     mov    %esp,0x1c(%ebx)


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




src/newlib/libc/sys/sysvi386/_setjmp.S

 1 /
 2 / our buffer looks like:
 3 /  eax,ebx,ecx,edx,esi,edi,esp,ebp,pc
 4 
 5  .globl _setjmp
 6  .globl setjmp
 7 _setjmp:
 8 setjmp:
 9  pushl %ebx
10  movl 8(%esp), %ebx # 取得 setjmp 參數, 也就是 jmp_buf (是一個指標), jmp_buf 存到 %ebx
11  movl %eax, (%ebx)
12  popl %eax
13  movl %eax, 4(%ebx)
14  movl %ecx, 8(%ebx)
15  movl %edx, 12(%ebx)
16  movl %esi, 16(%ebx)
17  movl %edi, 20(%ebx)
18  movl %esp, 24(%ebx)
19  movl %ebp, 28(%ebx)
20  movl (%esp), %eax
21  movl %eax, 32(%ebx)
22  xorl %eax, %eax
23  ret


總之, setjmp 就是把的當時的 stack frame 的狀態存起來,但是只有暫存器的部份, 一旦用 longjmp 回到那時候的 stack frame, 但是 stack frame 裡頭的資料和當時儲存的時候不同, 那就 ... 嘿嘿, 因為 setjmp 並沒有保存 stack, 這是和 context switch 的不同之處。

longjmp 自然做的是相反的事情, 把 jmp_buf 裡頭的值設定給所有暫存器, 還原當時 setjmp 的執行環境。

src/newlib/libc/sys/sysvi386/_longjmp.S

 1 /
 2 / our buffer looks like:
 3 /  eax,ebx,ecx,edx,esi,edi,esp,ebp,pc
 4 /
 5 / _longjmp is called with two parameters:  jmp_buf*,int
 6 / jmp_buf* is at 4(%esp), int is at 8(%esp)
 7 / retaddr is, of course, at (%esp)
 8 
 9  .globl _longjmp
10  .globl longjmp
11 _longjmp:
12 longjmp:
13  movl 4(%esp), %ebx / address of buf
14  movl 8(%esp), %eax / store return value
15 
16  movl 24(%ebx), %esp / restore stack
17  movl 32(%ebx), %edi
18 / Next line sets up return address.
19  movl %edi, 0(%esp) 
20  movl 8(%ebx), %ecx
21  movl 12(%ebx), %edx
22  movl 16(%ebx), %esi
23  movl 20(%ebx), %edi
24  movl 28(%ebx), %ebp
25  movl 4(%ebx), %ebx
26  testl %eax,%eax
27  jne bye
28  incl %eax  / eax hold 0 if we are here
29 bye:
30  ret


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)。

objdump -dS a.out
181    int ret = setjmp(mark);
182  80484c6:    c7 04 24 20 98 04 08    movl   $0x8049820,(%esp)
183  80484cd:    e8 9e fe ff ff          call   8048370 <_setjmp@plt>
184  80484d2:    89 44 24 1c             mov    %eax,0x1c(%esp) // 把 %eax 設定給 ret


L19 設定這次的 function return address, 這樣離開時, 就會回到 setjmp 存的位址。

知一可以求五, 舉一可以反三, 相信你可以做出 arm cortex m3 的版本。

這裡 /arm-eabi-toolchain/newlib-2013.05/newlib/libc/machine/arm/setjmp.S 可以偷看哦!你要自己試試看, 還是等我下篇文章呢?

我打算在某聚會介紹這個, 順便提提為什麼 sjlj_1.c 是有問題的, 但在我的平台可以執行這程式而沒有錯誤。

sjlj_1.c

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <setjmp.h>
 4
 5 jmp_buf mark;
 6
 7 int ret;
 8
 9 void f1(void)
10 {
11   char arr[1000];
12   int i=0;
13   for (i=0 ; i < 1000; ++i)
14     arr[i] = 0xff;
15 }
16
17 void f(void)
18 {
19   ret = setjmp(mark);
20 }
21
22 int main(int argc, char *argv[])
23 {
24   f();
25   printf("xx ret: %d\n", ret);
26   if (ret == 0)
27   {
28      printf("init setjmp\n");
29   }
30   else
31   {
32     printf("exit\n");
33     exit(0);
34   }
35   printf("xxx\n");
36   longjmp(mark, 5);
37   return 0;
38 }


20140227 補充: 這個版本有些錯誤, 在遇到 register 變數時有可能會出錯。

這是修改後的版本: https://github.com/descent/progs/blob/master/coroutine/my_setjmp.S

https://github.com/descent/progs/blob/master/coroutine/sample.c 就是讓這個實作出錯的例子, 我已經修改完成, 目前沒問題了, 但不保證沒有其他問題。

ref:
  1. 全面了解setjmp与longjmp(C语言异常处理机制)
  2. 第15集 C语言中的异常处理机制, 15~19 在講怎麼用這個作類似 c++ exception 的功能
  3. SETJMP & LONGJMP Implementation Analysis
  4. Understanding Setjmp/Longjmp (pdf)

2 則留言:

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

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