FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

setjmpとlongjmp はどのように実現されているのか

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()も、このような非常に単純なアセンブリコードで実現されているかと考えると、ちょっと自分の頭の硬さに嫌気が差してくる次第だ。