setjump(), longjump()
について基本的なことを調査するために、まずは以下のページなどを読んで勉強した。
http://www.nurs.or.jp/~sug/soft/super/longjmp.htm
(制限はあるが)どのような場所からでも、setjump()
を実行した場所に戻ってくることが出来る。これにより、例外処理もどきのようなものが作れる。
#include <setjmp.h> #include <stdio.h> #include <stdlib.h> jmp_buf jmp_div; void divide_test (int a, int b) { if (b == 0) { longjmp(jmp_div, 1); } int div = a / b; printf ("divide_test (%d, %d) = %d\n", a, b, div); return; } int main() { if (setjmp(jmp_div) == 0) { divide_test (223, 7); divide_test (571, 13); divide_test (311, 0); } else { fprintf (stderr, "divide_test failure\n"); return EXIT_FAILURE; } return 0; }
上記のプログラムはdivide_test()
という除算を実行する関数を三回呼び出している。
ただし三回目はゼロで除算するため、本来は実行してはいけない演算だ。
このプログラムでは、setjmp()
により現在の位置を記憶し、divide_test()
内でlongjmp()
が呼ばれた場合(この場合ゼロで除算する判定に引っかかった場合)に、setjmp()
の場所に戻ってくる。
さらにsetjmp()
の戻り値がlongjmp()
により設定されており(この場合は1)、条件判定として利用できる。
これだけでは関数の戻り値として制御すればよいではないか、ということになる。
setjmp()
、longjmp()
の威力を発揮するのはどのような領域だろうかといろいろ探っていたのだが、スレッドもどきのような物も作れるらしい。
http://blog.bugyo.tk/lyrical/archives/572
じゃあ、これをどのようにしてアセンブラは実現しているのか。
ちなみにこれ、アセンブラ言語じゃないと実現は不可能なことは簡単に分かる。コンパイルした結果を観察することで、どのようにして実現されているのかを観察してみる。
RISC-VのGCCを利用して上記のプログラムをコンパイルし、中身を見てみよう。
riscv64-unknown-elf-gcc -o longjump_test.riscv longjump_test.c riscv64-unknown-elf-objdump -D longjump_test.riscv | less
setjmp()が呼ばれる。
101d8: 01010413 addi s0,sp,16 101dc: 93818513 addi a0,gp,-1736 # 1b968 <jmp_div> 101e0: 328000ef jal 10508 <setjmp>
まず、main()
の中でsetjmp()
が呼ばれる。setjmp()
の中身は実はこうなっている。
0000000000010508 <setjmp>: 10508: 00153023 sd ra,0(a0) 1050c: 00853423 sd s0,8(a0) 10510: 00953823 sd s1,16(a0) 10514: 01253c23 sd s2,24(a0) 10518: 03353023 sd s3,32(a0) 1051c: 03453423 sd s4,40(a0) 10520: 03553823 sd s5,48(a0) 10524: 03653c23 sd s6,56(a0) 10528: 05753023 sd s7,64(a0) 1052c: 05853423 sd s8,72(a0) 10530: 05953823 sd s9,80(a0) 10534: 05a53c23 sd s10,88(a0) 10538: 07b53023 sd s11,96(a0) 1053c: 06253423 sd sp,104(a0) 10540: 003026f3 frsr a3 10544: 08853027 fsd fs0,128(a0) 10548: 08953427 fsd fs1,136(a0) 1054c: 09253827 fsd fs2,144(a0) 10550: 09353c27 fsd fs3,152(a0) 10554: 0b453027 fsd fs4,160(a0) 10558: 0b553427 fsd fs5,168(a0) 1055c: 0b653827 fsd fs6,176(a0) 10560: 0b753c27 fsd fs7,184(a0) 10564: 0d853027 fsd fs8,192(a0) 10568: 0d953427 fsd fs9,200(a0) 1056c: 0da53827 fsd fs10,208(a0) 10570: 0db53c27 fsd fs11,216(a0) 10574: 06d53c23 sd a3,120(a0) 10578: 00000513 li a0,0 1057c: 00008067 ret
現在のレジスタの内容を(コンテキスト)を退避しているだけである。同様に、longjmp()は以下のようになる。
0000000000010580 <longjmp>: 10580: 00053083 ld ra,0(a0) 10584: 00853403 ld s0,8(a0) 10588: 01053483 ld s1,16(a0) 1058c: 01853903 ld s2,24(a0) 10590: 02053983 ld s3,32(a0) 10594: 02853a03 ld s4,40(a0) 10598: 03053a83 ld s5,48(a0) 1059c: 03853b03 ld s6,56(a0) 105a0: 04053b83 ld s7,64(a0) 105a4: 04853c03 ld s8,72(a0) 105a8: 05053c83 ld s9,80(a0) 105ac: 05853d03 ld s10,88(a0) 105b0: 06053d83 ld s11,96(a0) 105b4: 06853103 ld sp,104(a0) 105b8: 07853683 ld a3,120(a0) 105bc: 08053407 fld fs0,128(a0) 105c0: 08853487 fld fs1,136(a0) 105c4: 09053907 fld fs2,144(a0) 105c8: 09853987 fld fs3,152(a0) 105cc: 0a053a07 fld fs4,160(a0) 105d0: 0a853a87 fld fs5,168(a0) 105d4: 0b053b07 fld fs6,176(a0) 105d8: 0b853b87 fld fs7,184(a0) 105dc: 0c053c07 fld fs8,192(a0) 105e0: 0c853c87 fld fs9,200(a0) 105e4: 0d053d07 fld fs10,208(a0) 105e8: 00369073 fssr a3 105ec: 0015b513 seqz a0,a1 105f0: 00b50533 add a0,a0,a1 105f4: 00008067 ret
これはsetjmp()
によって作成したコンテキストを元に戻しているだけである。非常に単純!
setjmp()
はreturn address register (ra)を保存しており、これはsetjmp()
内では「setjmp()を呼び出したプログラムの直後」に設定されている。longjmp()
によってコンテキストが戻され、ret
命令が実行されるので、longjmp()
が終了するとsetjmp()
の実行直後に戻されるというわけだ。
また、2番目の引数(a1レジスタ)を戻り値に設定しているため、longjmp()
の引数がsetjmp()
の戻り値に設定されていることも分かる。
この仕組みは、言われてみれば確かにまったく驚くにあたらない。
ただし、一見複雑そうな動きをするsetjmp()
とlongjmp()
も、このような非常に単純なアセンブリコードで実現されているかと考えると、ちょっと自分の頭の硬さに嫌気が差してくる次第だ。