home ホーム search 検索 -  login ログイン  | reload edit datainfo version cmd icon diff delete  | help ヘルプ

C言語系/NetBSD1.6における setjmp(), longjmp() の実装

C言語系/NetBSD1.6における setjmp(), longjmp() の実装

C言語系 / NetBSD1.6における setjmp(), longjmp() の実装
id: 535 所有者: msakamoto-sf    作成日: 2010-01-04 01:50:02
カテゴリ: Assembler BSD C言語 

引き続き「エキスパートCプログラミング」ネタ。setjmp()とlongjmp()が出てきたので、早速実験。
ちょうど「デーモン君のソース探検」ようにCVSからソースを落としてきたNetBSD1.6があったので、setjmp()/longjmp()の中身を調べ、動作原理をgdbで追いつめていきたい。

まずは「エキスパートCプログラミング」に載っていたサンプルプログラム。
test7.c:

#include <stdio.h>
#include <setjmp.h>
 
jmp_buf buf;
 
void banana() {
        printf("in banana()\n"); /* (2) */
        longjmp(buf, 1);
        /* NOTREACHED */
        printf("you'll never see this, because I longjmp'd.\n");
}
 
int main() {
        if (setjmp(buf)) {
                printf("back in main\n"); /* (3) */
        } else {
                printf("first time through...\n"); /* (1) */
                banana();
        }
        return 0;
}

そのまま動かすと、上の"(1)","(2)","(3)"の順で処理が進むことが確認出来る。

$ ./test7
first time through...
in banana()
back in main

では早速gdbで追っていきたい。


主要なシンボルアドレスのメモ

gdbの前に、nmコマンドで主要なシンボルのアドレスを押さえておく。

$ nm test7
(主要シンボルだけ抽出)
         U __longjmp14
         U __setjmp14
08048974 T banana
08049d60 B buf
080489b0 T main
         U printf

実際に使われるのが__setjmp14, __longjmp14になっている。なぜそうなってしまうのかも謎だが、一旦そちらの謎は置いておいて、とりあえず素直に __setjmp14, __longjmp14のソースコードがどこにあるか確認する。

$ locate setjmp

あらかじめソースをCVSからチェックアウト後、"/etc/weekly"でlocateのデータベースを構築しておいた。アーキテクチャ依存などかなり大量のファイルがヒットするが、i386用のソースファイルは以下に見つかった。

/usr/src/lib/libc/arch/i386/gen/__setjmp14.S

このファイルは後々、すこしだけちら見することになる。実際は、動作原理を把握する分であればgdb上での逆アセンブルだけで大凡分かる。

main関数~setjmp(__setjmp14)関数直前迄

ではgdbを始める。mainにブレークポイントを置き実行する。なお今回は "*main" という風にアスタリスクを頭に置き、インストラクション単位でmainに入った段階で停まるようにした。単に"main"とすると、アセンブリ言語レベルでの関数の先頭ではなく、ソースコードに対応する少し後のアドレスになってしまうからだ。

$ gdb test7
GNU gdb 5.0nb1
...
This GDB was configured as "i386--netbsdelf"...
(gdb) b *main
Breakpoint 1 at 0x80489b0: file test7.c, line 11.
(gdb) run
Starting program: /home/msakamoto/lang.c/test7

Breakpoint 1, 0x80489b0 in main () at test7.c:11
11      }

ここで逆アセンブルしてみる。

(gdb) disas
Dump of assembler code for function main:
0x80489b0 <main>:       push   %ebp
0x80489b1 <main+1>:     mov    %esp,%ebp
0x80489b3 <main+3>:     sub    $0x8,%esp
0x80489b6 <main+6>:     add    $0xfffffff4,%esp
0x80489b9 <main+9>:     push   $0x8049d60
0x80489be <main+14>:    call   0x8048634 <__setjmp14>
0x80489c3 <main+19>:    add    $0x10,%esp
0x80489c6 <main+22>:    mov    %eax,%eax
0x80489c8 <main+24>:    test   %eax,%eax
0x80489ca <main+26>:    je     0x80489e0 <main+48>
0x80489cc <main+28>:    add    $0xfffffff4,%esp
0x80489cf <main+31>:    push   $0x8048bcd
0x80489d4 <main+36>:    call   0x8048644 <printf>
0x80489d9 <main+41>:    add    $0x10,%esp
0x80489dc <main+44>:    jmp    0x80489f5 <main+69>
0x80489de <main+46>:    mov    %esi,%esi
0x80489e0 <main+48>:    add    $0xfffffff4,%esp
0x80489e3 <main+51>:    push   $0x8048bdb
0x80489e8 <main+56>:    call   0x8048644 <printf>
0x80489ed <main+61>:    add    $0x10,%esp
0x80489f0 <main+64>:    call   0x8048974 <banana>
0x80489f5 <main+69>:    xor    %eax,%eax
---Type <return> to continue, or q <return> to quit---
0x80489f7 <main+71>:    jmp    0x80489fc <main+76>
0x80489f9 <main+73>:    lea    0x0(%esi),%esi
0x80489fc <main+76>:    leave
0x80489fd <main+77>:    ret
End of assembler dump.
(gdb) p/x $pc
$1 = 0x80489b0

ここで"__setjmp14"にブレークポイントを設定する。

(gdb) b *__setjmp14
Breakpoint 2 at 0x4808e24c

0x4808e24cと、上の逆アセンブルでのcall先アドレスとは異なる場所に設定されてしまった。これはダイナミックリンクによるPLTの影響なので、今は無視しても問題ないはず。

(gdb) b *0x80489be
Breakpoint 3 at 0x80489be: file test7.c, line 14.

→アセンブラレベルでの "__setjmp14" のcall命令にもブレークポイントを設置する。
"info break"コマンドでブレークポイントの設置状況を確認後、"c"でcontinueし、"__setjmp14"のcall命令まで一気に実行する。

(gdb) info break
Num Type           Disp Enb Address    What
1   breakpoint     keep y   0x080489b0 in main at test7.c:11
        breakpoint already hit 1 time
2   breakpoint     keep y   0x4808e24c  <__setjmp14>
3   breakpoint     keep y   0x080489be in main at test7.c:14
(gdb) c
Continuing.

Breakpoint 3, 0x80489be in main () at test7.c:14
14              if (setjmp(buf)) {
(gdb) c
Continuing.

Breakpoint 2, 0x4808e24c in __setjmp14 () from /usr/lib/libc.so.12

setjmp(__setjmp14)関数内部

__setjmp14の中に到達したので、逆アセンブルする。

(gdb) disas
Dump of assembler code for function __setjmp14:
0x4808e24c <__setjmp14>:        mov    0x4(%esp,1),%ecx
0x4808e250 <__setjmp14+4>:      mov    0x0(%esp,1),%edx
0x4808e254 <__setjmp14+8>:      mov    %edx,0x0(%ecx)
0x4808e257 <__setjmp14+11>:     mov    %ebx,0x4(%ecx)
0x4808e25a <__setjmp14+14>:     mov    %esp,0x8(%ecx)
0x4808e25d <__setjmp14+17>:     mov    %ebp,0xc(%ecx)
0x4808e260 <__setjmp14+20>:     mov    %esi,0x10(%ecx)
0x4808e263 <__setjmp14+23>:     mov    %edi,0x14(%ecx)
0x4808e266 <__setjmp14+26>:     lea    0x18(%ecx),%edx
0x4808e269 <__setjmp14+29>:     push   %ebx
0x4808e26a <__setjmp14+30>:     call   0x4808e26f <__setjmp14+35>
0x4808e26f <__setjmp14+35>:     pop    %ebx
0x4808e270 <__setjmp14+36>:     add    $0x5e305,%ebx
0x4808e276 <__setjmp14+42>:     push   %edx
0x4808e277 <__setjmp14+43>:     push   $0x0
0x4808e279 <__setjmp14+45>:     push   $0x0
0x4808e27b <__setjmp14+47>:     call   0x48072aa4 <_init+3016>
0x4808e280 <__setjmp14+52>:     add    $0xc,%esp
0x4808e283 <__setjmp14+55>:     pop    %ebx
0x4808e284 <__setjmp14+56>:     xor    %eax,%eax
0x4808e286 <__setjmp14+58>:     ret
0x4808e287 <__setjmp14+59>:     nop
---Type <return> to continue, or q <return> to quit---q
Quit

実際のアセンブラソースも載せておく。(BSDライセンスだから多丈夫なはず。)

ENTRY(__setjmp14)
        movl    4(%esp),%ecx
        movl    0(%esp),%edx
        movl    %edx, 0(%ecx)
        movl    %ebx, 4(%ecx)
        movl    %esp, 8(%ecx)
        movl    %ebp,12(%ecx)
        movl    %esi,16(%ecx)
        movl    %edi,20(%ecx)

        /* Get the signal mask. */
        leal    24(%ecx),%edx

        PIC_PROLOGUE
        pushl   %edx
        pushl   $0
        pushl   $0
#ifdef PIC
        call    PIC_PLT(_C_LABEL(__sigprocmask14))
#else
        call    _C_LABEL(__sigprocmask14)
#endif
        addl    $12,%esp
        PIC_EPILOGUE

        xorl    %eax,%eax
        ret

この時点でESP/EBP, そしてスタックの状況を確認してみる。

(gdb) p/x $esp
$1 = 0xbfbfdb40
(gdb) p/x $ebp
$2 = 0xbfbfdb5c
(gdb) x/40w $esp
0xbfbfdb40:     0x080489c3      0x08049d60      0xbfbfdbcc      0x0804001f
0xbfbfdb50:     0x4804001f      0xbfbfdbcc      0xbfbfdb78      0xbfbfdba8
0xbfbfdb60:     0x08048780      0x00000001      0xbfbfdbcc      0xbfbfdbd4
0xbfbfdb70:     0x00000246      0x48056200      0xbfbfdba8      0x08048769
0xbfbfdb80:     0x08048a34      0x48056200      0x480534a8      0x080486ea
0xbfbfdb90:     0xbfbfdff0      0x00000000      0x00000000      0xbfbfdc24
0xbfbfdba0:     0xbfbfdc1c      0x00000000      0x00000000      0x080486db
0xbfbfdbb0:     0x00000001      0xbfbfdbcc      0xbfbfdbd4      0x4804abc0
0xbfbfdbc0:     0x48056200      0xbfbfdff0      0x00000001      0xbfbfdc5c
0xbfbfdbd0:     0x00000000      0xbfbfdc79      0xbfbfdc94      0xbfbfdc9f

x86ではスタックはアドレスの小さい方に伸びていくので、0xbfbfdb40というのが間違いなくスタックのTOP。スタックフレームのTOP近くを解析してみる。

0xbfbfdb40: 0x080489c3 <= ESP, 戻り先アドレス→setjmp()の直後の命令を指す。
0xbfbfdb44: 0x08049d60 <= 引数, これはソースコードでの "jmp_buf buf"へのアドレス。(nmの結果を復習)

0xbfbfdb48以降は、ここに至るまでのスタックフレームになり、本題から外れるので解析は省略する。

  • ESPが戻り先アドレス = longjmp()で戻ってきて欲しい地点になっていること
  • ESP+4がjmp_buf型へのポインタ引数であること

この2点が把握できれば、__setjmp14の次のアセンブラコードの意味も明らかになる。

ENTRY(__setjmp14)
        movl    4(%esp),%ecx
        movl    0(%esp),%edx
        movl    %edx, 0(%ecx)
        movl    %ebx, 4(%ecx)
        movl    %esp, 8(%ecx)
        movl    %ebp,12(%ecx)
        movl    %esi,16(%ecx)
        movl    %edi,20(%ecx)

まずECXに jmp_buf型へのポインタを格納する。

movl    4(%esp),%ecx

続いて ESP の値 = longjmpでの戻り先アドレスをEDXに格納する。

movl    0(%esp),%edx

EDX(戻り先アドレス)に加えて、EBX, ESI, EDI, ESP, EBPを格納している。なお処理の流れ上、元のEAX, ECX, EDXはjmp_bufには保存されないらしい。

ちなみにjmp_buf型は long[JBLEN] というlong型配列がtypedefされたもので、NetBSD1.6のi386ではJBLEN=13となっている。

$ cd /usr/include
$ grep -r jmp_buf *
i386/setjmp.h:#define   _JBLEN  13              /* size, in longs, of a jmp_buf */
machine/setjmp.h:#define        _JBLEN  13              /* size, in longs, of a jmp_buf */
setjmp.h:typedef long sigjmp_buf[_JBLEN + 1] _JB_ATTRIBUTES;
setjmp.h:typedef long jmp_buf[_JBLEN] _JB_ATTRIBUTES;

まとめると、setjmp()ではjmp_bufの中に、longjmpで必要な戻り先アドレスや戻った時点で復元したいレジスタ値を保存していることがわかった。

longjmp(__longjmp14)関数内部

setjmp()の原理が分かったところで、longjmp()をデバッグする為、引き続きgdbに戻る。

(gdb) b *__longjmp14
Breakpoint 4 at 0x4808e288
(gdb) c
Continuing.
first time through...
in banana()

Breakpoint 4, 0x4808e288 in __longjmp14 () from /usr/lib/libc.so.12

"__longjmp14()"の中で停まったので、逆アセンブルする。

(gdb) disas
Dump of assembler code for function __longjmp14:
0x4808e288 <__longjmp14>:       mov    0x4(%esp,1),%ecx
0x4808e28c <__longjmp14+4>:     lea    0x18(%ecx),%edx
0x4808e28f <__longjmp14+7>:     push   %ebx
0x4808e290 <__longjmp14+8>:     call   0x4808e295 <__longjmp14+13>
0x4808e295 <__longjmp14+13>:    pop    %ebx
0x4808e296 <__longjmp14+14>:    add    $0x5e2df,%ebx
0x4808e29c <__longjmp14+20>:    push   $0x0
0x4808e29e <__longjmp14+22>:    push   %edx
0x4808e29f <__longjmp14+23>:    push   $0x3
0x4808e2a1 <__longjmp14+25>:    call   0x48072aa4 <_init+3016>
0x4808e2a6 <__longjmp14+30>:    add    $0xc,%esp
0x4808e2a9 <__longjmp14+33>:    pop    %ebx
0x4808e2aa <__longjmp14+34>:    mov    0x4(%esp,1),%edx
0x4808e2ae <__longjmp14+38>:    mov    0x8(%esp,1),%eax
0x4808e2b2 <__longjmp14+42>:    mov    0x0(%edx),%ecx
0x4808e2b5 <__longjmp14+45>:    mov    0x4(%edx),%ebx
0x4808e2b8 <__longjmp14+48>:    mov    0x8(%edx),%esp
0x4808e2bb <__longjmp14+51>:    mov    0xc(%edx),%ebp
0x4808e2be <__longjmp14+54>:    mov    0x10(%edx),%esi
0x4808e2c1 <__longjmp14+57>:    mov    0x14(%edx),%edi
0x4808e2c4 <__longjmp14+60>:    test   %eax,%eax
0x4808e2c6 <__longjmp14+62>:    jne    0x4808e2c9 <__longjmp14+65>
---Type <return> to continue, or q <return> to quit---
0x4808e2c8 <__longjmp14+64>:    inc    %eax
0x4808e2c9 <__longjmp14+65>:    mov    %ecx,0x0(%esp,1)
0x4808e2cd <__longjmp14+69>:    ret
0x4808e2ce <__longjmp14+70>:    mov    %esi,%esi
End of assembler dump.

対応する実際のアセンブラソースも載せておく。

ENTRY(__longjmp14)
        /* Restore the signal mask. */
        movl    4(%esp),%ecx
        leal    24(%ecx),%edx

        PIC_PROLOGUE
        pushl   $0
        pushl   %edx
        pushl   $3                      /* SIG_SETMASK */
#ifdef PIC
        call    PIC_PLT(_C_LABEL(__sigprocmask14))
#else
        call    _C_LABEL(__sigprocmask14)
#endif
        addl    $12,%esp
        PIC_EPILOGUE

        movl    4(%esp),%edx
        movl    8(%esp),%eax
        movl    0(%edx),%ecx
        movl    4(%edx),%ebx
        movl    8(%edx),%esp
        movl    12(%edx),%ebp
        movl    16(%edx),%esi
        movl    20(%edx),%edi
        testl   %eax,%eax
        jnz     1f
        incl    %eax
1:      movl    %ecx,0(%esp)
        ret

この時点でのESP/EBP/スタックフレーム/バックトレースを確認してみる。

(gdb) p/x $esp
$3 = 0xbfbfdb30
(gdb) p/x $ebp
$4 = 0xbfbfdb4c
(gdb) x/8w $esp
0xbfbfdb30:     0x08048999      0x08049d60      0x00000001      0xbfbfdb5c
0xbfbfdb40:     0x080489ed      0x08048bdb      0xbfbfdbcc      0xbfbfdb5c
(gdb) bt
#0  0x4808e288 in __longjmp14 () from /usr/lib/libc.so.12
#1  0x8048999 in banana () at test7.c:8
#2  0x80489f5 in main () at test7.c:18
#3  0x8048780 in ___start ()

元のC言語のソースを思い出すと、

longjmp(buf, 1);

なので、

(gdb) x/8w $esp
0xbfbfdb30:     0x08048999      0x08049d60      0x00000001      0xbfbfdb5c

のスタックフレームを逆算すると

戻り先(ESP)     : 0x08048999
引数1 (ESP + 4) : 0x08049d60 = "jmp_buf buf"
引数2 (ESP + 8) : 1

となり、C言語のソースと対応していることが分かる。ここまで解析出来れば、本体のアセンブラソースの意味も明らかになる。

        movl    4(%esp),%edx
        movl    8(%esp),%eax
        movl    0(%edx),%ecx
        movl    4(%edx),%ebx
        movl    8(%edx),%esp
        movl    12(%edx),%ebp
        movl    16(%edx),%esi
        movl    20(%edx),%edi
        testl   %eax,%eax
        jnz     1f
        incl    %eax
1:      movl    %ecx,0(%esp)
        ret

まず引数1で渡されたjmp_buf型のポインタをEDXに格納。

movl    4(%esp),%edx

続いて、引数2をEAXに格納。

movl    8(%esp),%eax

setjmp()の中でjmp_bufの中に格納された各レジスタ値を復元。

movl    0(%edx),%ecx
movl    4(%edx),%ebx
movl    8(%edx),%esp
movl    12(%edx),%ebp
movl    16(%edx),%esi
movl    20(%edx),%edi

次は、元のソースではなくgdb上で逆アセンブルしたソースの方が分かりやすい。

testl   %eax,%eax
jnz     1f
incl    %eax


0x4808e2c4 <__longjmp14+60>:    test   %eax,%eax
0x4808e2c6 <__longjmp14+62>:    jne    0x4808e2c9 <__longjmp14+65>
0x4808e2c8 <__longjmp14+64>:    inc    %eax
0x4808e2c9 <__longjmp14+65>:    mov    %ecx,0x0(%esp,1)
0x4808e2cd <__longjmp14+69>:    ret

まずEAX、つまり引数2のANDを行う(test命令)。

0x4808e2c4 <__longjmp14+60>:    test   %eax,%eax

その結果、もし引数2が0以外であればZF=0になるので、次のJNZ命令が実行される。

0x4808e2c6 <__longjmp14+62>:    jne    0x4808e2c9 <__longjmp14+65>

つまり、次の命令に進む。

0x4808e2c9 <__longjmp14+65>:    mov    %ecx,0x0(%esp,1)

さらにこの直後はret命令。
一方、もし引数2が0の場合はZF=1になるので、JNZ命令は無視され、次のINC命令が実行される。

0x4808e2c8 <__longjmp14+64>:    inc    %eax

つまりEAXは 0 + 1 = 1になる。

まとめると、引数2が0以外の時はEAX=longjmp()の戻り値はその値になり、引数2が0の時は1がlongjmp()の戻り値になる。
"longjmp()の戻り値"というよりは「戻る時のsetjmp()の戻り値」の方が正しい表記だろう。
つまり、初回のsetjmp()では0が戻り、longjmp()から戻る時のsetjmp()の戻り値は必ず0以外になり、初回と区別されるようになっている。

肝心の戻り先アドレスは以下の命令でセットされる。

0x4808e2c9 <__longjmp14+65>:    mov    %ecx,0x0(%esp,1)

ECXはこの上でjmp_buf型から復元されている。遡れば、__setjmp14()の中で格納された、setjmp()の直後のアドレス。
それをESP+0に転送している・・・ということは、戻り先アドレスを上書きしていることになる。

では実際にステップ実行して確認してみる。
先ほどのtest命令で一旦ブレークポイントで停め、ステップ実行に入る。

(gdb) b *0x4808e2c4
Breakpoint 5 at 0x4808e2c4
(gdb) c
Continuing.

Breakpoint 5, 0x4808e2c4 in __longjmp14 () from /usr/lib/libc.so.12
(gdb) p/x $eax
$6 = 0x1
(gdb) stepi
0x4808e2c6 in __longjmp14 () from /usr/lib/libc.so.12

これでtest命令が終わった。続いてjnz命令を実行する。

(gdb) stepi
0x4808e2c9 in __longjmp14 () from /usr/lib/libc.so.12

EAXが1だったので、ZFも0となり、inc命令は実行されず、mov命令まで進んだ。

(gdb) p/x $ecx
$7 = 0x80489c3
(gdb) stepi
0x4808e2cd in __longjmp14 () from /usr/lib/libc.so.12

ECXには setjmp() の次のアドレス(=0x080489c3)が復元されている。mov命令実行後のスタックフレームを覗いてみる。

(gdb) x/4w $esp
0xbfbfdb40:     0x080489c3      0x08048bdb      0xbfbfdbcc      0xbfbfdb5c

スタックのTOP(=戻り先アドレス)に、ちゃんと0x080489c3が設定されている。あとはret命令へ進めば、無事main関数の中へ戻る。

(gdb) stepi
0x80489c3 in main () at test7.c:14
14              if (setjmp(buf)) {
(gdb) p/x $pc
$8 = 0x80489c3
(gdb) c
Continuing.
back in main

Program exited normally.

以上で、無事setjmp/longjmpの流れをデバッグできた。

まとめると、setjmp/longjmpはアセンブラレベルでスタックフレームやスタックポインタを処理することで、関数の範囲を超えたジャンプを実現していることを確認できた。



プレーンテキスト形式でダウンロード
現在のバージョン : 1
更新者: msakamoto-sf
更新日: 2010-01-04 01:52:31
md5:64235245fbc8d9875eb9e5e567821e03
sha1:0989991a300400d0d650d42ae5dfbe5b03797589
コメント
コメントを投稿するにはログインして下さい。