組み込みでlongjmp/setjmpは使えるか?
構造化言語でなくなる、よく分からない等の理由でなんとなく嫌われ者のlongjmp/setjmpですが、 その気になれば例外処理やスレッドも実装できる知る人ぞ知る便利な関数でもあります。 そんなlongjmp/setjmpが組み込みでも使用できるのかを調べてみました。
longjmp/setjmpの動作
簡単にlogjmp/setjmpの動作を説明すると、setjmpでラベルを付けて、longjmpでそのラベルへ分岐します。 C言語のgotoは関数の外側へ分岐することはできないのですが、longjmpはどこへでも分岐できるところが違います。
longjmp/setjmpの使用例としてC++の例外処理のようなことをするプログラムを示します。
#include <setjmp.h> void func(); jmp_buf except; int main() { if (setjmp(except) == 0) { /* try */ func(); } else { /* catch */ // ... return 0; } return 1; } void func() { // ... if (fatal_error) { longjmp(except, 1); /* throw */ } // ... }
setjmpでの処理
longjmpで関数の外側へ分岐してしまうと関数は呼び出し元へ戻るものという構造が壊れてしまいます。 そこでsetjmpで戻り位置を決めたときタスクを切り替えるようにその場でレジスタを退避します。
実際にarm-none-eabi-gccのlongjmp/setjmpがどんな処理をしているかを調べた結果が次のアセンブラです。 setjmp関数の中でstm命令でスタックにレジスタを退避し、 longmp関数の中でldm命令で退避したレジスタを復旧していることが分かります。
0000802c <setjmp>: 802c: 46ec mov ip, sp 802e: e8a0 5ff0 stmia.w r0!, {r4, r5, r6, r7, r8, r9, sl, fp, ip, lr} 8032: f04f 0000 mov.w r0, #0 8036: 4770 bx lr 00008038 <longjmp>: 8038: e8b0 5ff0 ldmia.w r0!, {r4, r5, r6, r7, r8, r9, sl, fp, ip, lr} 803c: 46e5 mov sp, ip 803e: 0008 movs r0, r1 8040: bf08 it eq 8042: 2001 moveq r0, #1 8044: 4770 bx lr 8046: bf00 nop
組み込みでlongjmp/setjmpは使えるか?
ライブラリにlongjmp/setjmpが存在しているようなので、組み込みでも使えないことはなさそうです。
しかしよく見ると浮動小数点レジスタが退避されていません。 s0-s15は壊してもかまわない呼び出し規約になっているのですが、SIMD拡張を使うとs16-s31が使われるようになるのでこのとき問題になりそうです。
もしかするとPCで使うようなリッチなOSでは毎回浮動小数点レジスタを退避しているとオーバーヘッドになるので、 浮動小数点命令が実行されたときだけCPU例外を起こしてOSが浮動小数点レジスタの退避を代行するような仕組みがあるのかもしれません。
しかしRTOSでそういうことをしているのは見たことがありません。 タスクの属性にFPU例外が設定できるようになっているとか、 FPUにvfpv3-d16を明示してSIMD拡張を禁止していればまだましな方で、 最初から浮動小数点演算がハードでできるマイコンは考慮されていません、ましてやSIMD拡張なんて、というのがほとんどです。
まとめ
arm-none-eabi-gccでlongjmp/setjmpは使えそうです。 ですがSIMD拡張があるマイコンで浮動小数点レジスタをすべて使い切るようなヘビーなプログラムをするときは s16-s31が退避されないことを念頭において慎重にプログラミングしましょう。