sigaction, eisl

Advent Calendar

Qiita Advent Calendar 2020

pythonは現代のBASICなんだな。投稿希望者殺到。もう一つ大人気なのがgolang。C語の置き換え人気なのでしょうか。パッケージも豊富だし、出来上がるのがシングルバイナリーって所が受けているんですかね?

来年は何をやるか? じっくり探してみよう。

signal and sigaction

前回、nprologでsignalが使われていたのを見た。どんな具合に動くのか追ってみたい(kernel観光ね)。nprologをそのままqemu上で動いているOpenBSDにかけるわけにもいかないので、簡易版を作った。

#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

void reset(int i){
  exit(i);
}

int main(void){
  sleep(1);
//  printf("%p\n", reset);
  signal(SIGINT, reset);
  for (int i=0; i<10; i++)
    sleep(1);
  return(0);
}

メイン内で割り込みを登録。その後、10秒待ってから終了。10秒以内にCtl-Cすると、resetが呼び出されて、割り込みSIGの番号を終了ステータスとして、アプリは終了。たわいも無いコードである。

これをktraceの元で走らせて、挙動を確認。

30540 a.out    CALL  nanosleep(0xcf7f4c38,0xcf7f4c28)
30540 a.out    RET   nanosleep 0
30540 a.out    CALL  kbind(0xcf7f4be8,12,0x58737154fad7d79)
30540 a.out    RET   kbind 0
30540 a.out    CALL  sigaction(SIGINT,0xcf7f4c38,0xcf7f4c28)
30540 a.out    RET   sigaction 0
30540 a.out    CALL  nanosleep(0xcf7f4c38,0xcf7f4c28)
30540 a.out    RET   nanosleep 0
30540 a.out    CALL  nanosleep(0xcf7f4c38,0xcf7f4c28)
30540 a.out    RET   nanosleep -1 errno 4 Interrupted system call
30540 a.out    CALL  kbind(0xcf7f4a98,12,0x58737154fad7d79)
30540 a.out    RET   kbind 0
 :
30540 a.out    CALL  exit(2)

nanosleepの後にsigactionが呼ばれている。そして、ループ内のsleepを実行中にシステムコールの割り込みが入って、sleepは失敗してる。Ctl-Cが原因なんだな。最後は、SIGINTに相当する番号を返して終了。

と言う事で、signal(3)は、sigactionシステムコールに置き換えられていました。物の本によると、signalは、移植性の面で非推奨らしいです。若しsigactionを使いたいなら、下記のように書き換えれば良いそうです。

#include <string.h>       // need for memset
   :
  struct sigaction act;

  memset(&act, 0, sizeof act);
  act.sa_handler = reset;
  sigaction(SIGINT, &act, NULL);

と、長い前ぶりの後、 sys_action を追いかけてみました。

(gdb) p signum
$1 = 2
(gdb) p *nsa
$3 = {__sigaction_u = {__sa_handler = 0x1b3b3290, __sa_sigaction = 0x1b3b3290}, sa_mask = 0, sa_flags = 2}
(gdb) p *osa
$4 = {__sigaction_u = {__sa_handler = 0x33, __sa_sigaction = 0x33}, sa_mask = 3481195068, sa_flags = 48664816}

下記は、サンプルプログラムのprintfを有効にして、reset関数のアドレスを表示したものです。OpenBSDは、セキュリティ向上させる為、アプリの実行毎にロードアドレスが変わる仕組みになってます。

ob$ ./a.out
0x1b3b3290

下記は、pgsignalにBPを置いておいて、Ctl-Cした時の図。

(gdb) bt
#0  ptsignal (p=0xd17e3904, signum=2, type=SPROCESS) at /usr/src/sys/kern/kern_sig.c:890
#1  0xd0a0d99c in pgsignal (pgrp=0xd17d2d20, signum=2, checkctty=1) at /usr/src/sys/kern/kern_sig.c:749
#2  0xd0ad3b0e in ttyinput (c=3, tp=0xd1882c00) at /usr/src/sys/kern/tty.c:359
#3  0xd08acc94 in comsoft (arg=0xd184d000) at /usr/src/sys/dev/ic/com.c:1039
#4  0xd0ccde6c in softintr_dispatch (which=2) at /usr/src/sys/arch/i386/i386/softintr.c:97
#5  0xd09b21e2 in Xsofttty ()
#6  0x00000002 in ?? ()
#7  0xf1cb0020 in ?? ()
#8  0xd0e524e7 in acpicpu_idle () at /usr/src/sys/dev/acpi/acpicpu.c:1187
#9  0xd09af7e8 in cpu_idle_cycle ()
#10 0xd0aa0154 in sched_idle (v=0xd10ffff8 <cpu_info_full_primary+8184>) at /usr/src/sys/kern/kern_sched.c:193
#11 0xd09af481 in proc_trampoline ()

物の本では、システムコールの終了時にsignalが発生してるか確認し、signal-handlerに飛び込むらしい。コンテキストに保存してるスタックを操作して実現してるらしいけど、その現場に到達出来ず。ゆっくり追ってみような。

exampele of sigaction

何気にリナのsigactionのmanを見てたんだ。そしたらサンプルがmprotectに有るよと案内が出てた。で、そのコードをパクって、前回のヌルポと組み合わせてみた。

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>

void handler(int sig, siginfo_t *si, void *unused) {
   printf("SIGSEGV at address: 0x%lx\n",  (long) si->si_addr);
   exit(1);
}

int main(void){
  struct sigaction sa;

  sa.sa_flags = SA_SIGINFO;
  sigemptyset(&sa.sa_mask);
  sa.sa_sigaction = handler;
  sigaction(SIGSEGV, &sa, NULL);

  *(int*)256 = 777;
}

実行結果。

debian:tmp$ ./a.out
SIGSEGV at address: 0x100

SEGVになるとCoreが作成されるのが普通だけど、それは既定の動作。プログラムでsignalをフックした場合は、その限りでは無い。

procmap

procmapなんて言う面白いコマンドを見つけてしまったので、試してみた。

vbox$ ./a.out &
[1] 94597
vbox$ 0x14efa600
doas procmap -p 94597
00001000      0K                     [ anon ]
0212A000      4K read/exec           [ uvm_aobj ]
036AE000    132K read              /usr/lib/libc.so.96.0
036CF000    572K read/exec         /usr/lib/libc.so.96.0
065C6000      8K read              /usr/libexec/ld.so
065C8000     48K read/exec         /usr/libexec/ld.so
14EF9000      4K read              /tmp/a.out
14EFA000      4K read/exec         /tmp/a.out
236AE000      8K read              /usr/lib/libc.so.96.0
236B0000      8K read/write        /usr/lib/libc.so.96.0
 :
4AE45000     28K read              /var/run/ld.so.hints
 :
73938000      4K read                [ uvm_aobj ]
 :
CD6E3000  29696K                     [ stack ]
CF3E3000   4092K read/write          [ anon ]
CF7E2000      4K                     [ anon ]
 total     5144K

プチ、リナのlsofに似ているな。lsofは例によって機能がてんこ盛りだけど、こちらはすっきり目のオプションしかない。それに特権コマンドだし。

measure in nprolog

nprologのコードをブラウジングしてたら、面白い物を見つけた。shellとかにあるtime相当品。まずは、どんな風に動くか試してみる。

vbox$ rlwrap ./npl -c example/fib.pl
N-Prolog Ver 1.66
?- measure(fib(5,Ans)).
Elapsed Time=0.000722 (second)  60948(LIPS) 44
Ans = 5 .
yes
?- measure(fib(10,Ans)).
Elapsed Time=0.008101 (second)  65424(LIPS) 530
Ans = 55 .
yes

rlwrapをかませて起動してるのは、edit機能を殺している為。起動時にfibをロードしてる。 そしてmeasureを使って、fibの実行時間を計測。

arg1 = car(arglist);
proof = 0;
start_time = getETime(); //time_flag on and it store start time
res = prove_all(arg1,sp,0);
end_time = getETime();
time = end_time - start_time;
lips = (double)proof / time;
ESCFGREEN;
printf("Elapsed Time=%.6f (second)  %.0f(LIPS) %d\n", time,lips,proof);

主要なコードの抜粋(ちょいとオリジナル版に手を入れている)。現在時刻を浮動小数で返してくれる関数に挟まれて、 prove_all を実行してる。時刻の差分を取れば実行時間になる。proofは、 probe_all が呼ばれた回数だ。

その回数を実行時間で割れば、1秒当たりの呼び出し回数lipsになる。オリジナル版には無かったけど、回数も表示するようにした。

ここで注目すべきは、 prove_all だ。lispで言う所のevalだな。そう思うと第一引数は、評価したい式。第二引数はspってなってるけど、これは環境変数相当だな。spってのは、nprologが自前で用意したスタックだ。という事は、ローカル環境と思えばいいのだな。第三変数には0を指定してる。lispのevalには無いものだ。以前に調べた事があるけど、再帰が深くなってスタックを食いつぶさないようにする為の、リミットカウンターだ。

思わぬ所で、evalの使い方が分かってしまった。

eisl

時代はnprologからISLISPに動いているらしい。正直、prologの変態な文法には付き合いきれませんです。番外編で知ったんだけど、'.'が文脈に於いてはconsになったり、文字通り式の終わりを示したりと勝手気まま。付き合いきれませんです。

ISLisp星より愛をこめて -ISLispのご紹介ー

Welcome to ISLisp

お気楽 ISLisp プログラミング超入門

ISLISPの処理系

https://github.com/sasagawa888/eisl

ISLISPを一言で言うと、CommonLispのぐちゃぐちゃな仕様をすっきりさせましたって事だな。

eisl on OpenBSD

早速OpenBSDに移植してみる。makefile内でifによって切り分けしてる部分が有るんだけど、それが BSD makeでは認識しないでエラーになるんで、変更。

ob$ make
gcc -O2 -pipe   -c main.c
main.c:12:17: error: omp.h: No such file or directory
 :

どうもこのヘッダーファイルは、OpenMPを使う時に必要そう。だって、オイラーはGPGPUなんて言う豪華な設備持っていないもの。

次のエラーは、edit.cの中で使われている UP/DOWN/LEFT/RIGHTが分からんと言うやつ。右往左往したよ。色々調べたら、eisl.hの中で、 __linux に囲まれたものを発見。作者様は、WindowsとLinuxの両刀使いされてるんで、それの切り分けだな。

eisl.h/function.c/syntax.cで切り分けが有った。BSDとリナは多分、同じ種類でしょって想像を働かせ、 __OpenBSD__ に変更。

ob$ ./eisl
Easy-ISLisp Ver1.66
> (load "tests/verify.lsp")
enter M&S-GC free=19998175
exit  M&S-GC free=19998201
All tests are done!
T

これで移植完了、かな? まて、眼玉のcompilerはどうした。

ob$ ./eisl -c
Can't open a file at load "/home/sakae/eisl/library/compiler.lsp"
debug mode ?(help)
>>

main.cの環境変数HOMEを見てる所を /tmpに変更。compiler.lspの中に埋め込まれている $HOMEも /tmp に変更。だって、$HOMEは、SSDの中。頻繁に更新したくない。劣化が進むから。だから、working-dirは、RAM-DISK仕立ての/tmpを常用してるんだ。ここならDISKがすり減る心配をしなくても良いからね。

> (load "tests/tarai.o")
T
> (time (tarai 12 6 0))
Elapsed Time(second)=0.142353
<undef>

後は、heapdumpの表示を20セル分に変更。更にedlisを止めて、emacsに変更。リナ上では素直にedlisを使う事にしたんだけど、pageup/downのキーバインドが、emacsと逆になってた。edlis.cをちょっと変更してemacsに合わせておいた。

次は、和田先生曰く式年遷宮式なGC、オイラー的には、焼き畑農業式なGC、そう、copy-GCを試してみるか。YouTube上の報告では、M&S-GCより性能が高いらしいからね。

ob$ ./eisl
functionSegmentation fault (core dumped)

あらま、起動せぇへん。

(gdb) bt
#0  __vfprintf (fp=0x0, fmt0=0xd6ce77ad65e "<class %s>", ap=0x7f7ffffde700)
    at /usr/src/lib/libc/stdio/vfprintf.c:458
#1  0x00000d6f0302d382 in _libc_vfprintf (fp=0x0,
    fmt0=0xd6ce77ad65e "<class %s>", ap=0x7f7ffffde700)
    at /usr/src/lib/libc/stdio/vfprintf.c:263
#2  0x00000d6f0303df38 in _libc_fprintf (fp=0x0,
    fmt=0xd6ce77ad65e "<class %s>") at /usr/src/lib/libc/stdio/fprintf.c:44
#3  0x00000d6ce77bb3b0 in printclass (addr=9) at main.c:1275
#4  0x00000d6ce77b9ee0 in print (addr=9) at main.c:1045
#5  0x00000d6ce77ddae8 in signal_condition (x=1103, y=0) at error.c:387
#6  0x00000d6ce77dc6d7 in error (errnum=113, fun=0xd6ce77ad695 "apply",
    arg=1091) at error.c:154
#7  0x00000d6ce77bc49f in apply (func=0, args=1089) at main.c:1497
#8  0x00000d6ce77bba9a in eval (addr=1064) at main.c:1362
#9  0x00000d6ce77bc5cc in evlis (addr=1085) at main.c:1539
#10 0x00000d6ce77bc5e6 in evlis (addr=1086) at main.c:1541
#11 0x00000d6ce77bba80 in eval (addr=1087) at main.c:1362
#12 0x00000d6ce77bef93 in initgeneric () at function.c:370
#13 0x00000d6ce77b7d08 in main (argc=1, argv=0x7f7ffffdec58) at main.c:243

最初の所からコケているようですねぇ。debian(64Bit)なマシンでも、同様な症状で落ちた。 コンパイラーの違いなんですかね?

sakae@pen:/tmp/eisl$ gcc -v
  :
gcc version 8.3.0 (Debian 8.3.0-6)

debianは、控え目なパッケージングだからなあ。

今度はi386なOpenBSDマシンで実行。

vbox$ ./eisl
Easy-ISLisp Ver1.66
> (load "tests/verify.lsp")
enter COPY-GC free=12998879
exit  COPY-GC free=7000000
(GENERIC-FUNCTION-P (FUNCTION READ-FOO-A)) is bad
(GENERIC-FUNCTION-P (FUNCTION WRITE-FOO-A)) is bad
(GENERIC-FUNCTION-P (FUNCTION ACCESS-FOO-A)) is bad
(GENERIC-FUNCTION-P (FUNCTION BOUNDP-FOO-A)) is bad
Segmentation fault (core dumped)

OpenBSDのLLVM由来のデフォルトコンパイラーでも、同様に落ちた。

vbox$ gdb -q eisl
Reading symbols from eisl...done.
(gdb) r
Starting program: /tmp/eisl/eisl
Easy-ISLisp Ver1.66
> (gbc)
enter COPY-GC free=12998884
exit  COPY-GC free=7000000
<class basic-array*>
> (gbc)
enter COPY-GC free=6999999
exit  COPY-GC free=7000000
<class basic-array*>

ここまでは良いな。それなりのセル数を表示している。

> (load "tests/verify.lsp")
enter COPY-GC free=6999994
exit  COPY-GC free=7000000
(GENERIC-FUNCTION-P (FUNCTION READ-FOO-A)) is bad
(GENERIC-FUNCTION-P (FUNCTION WRITE-FOO-A)) is bad
(GENERIC-FUNCTION-P (FUNCTION ACCESS-FOO-A)) is bad
(GENERIC-FUNCTION-P (FUNCTION BOUNDP-FOO-A)) is bad

Program received signal SIGSEGV, Segmentation fault.
0x158e451e in cdr (addr=2146435071) at data.c:414
414         return(GET_CDR(addr));
(gdb) bt
#0  0x158e451e in cdr (addr=2146435071) at data.c:414
#1  0x158c5301 in matchp (varlist=0, arglist=2146435071) at main.c:1555
#2  0x158c5313 in matchp (varlist=1065, arglist=796) at main.c:1555
#3  0x158c5313 in matchp (varlist=1066, arglist=0) at main.c:1555
#4  0x158c5313 in matchp (varlist=1067, arglist=13001037) at main.c:1555
#5  0x158c4c11 in apply (func=599068, args=13001037) at main.c:1459
#6  0x158bfb8c in eval (addr=13001033) at main.c:1359
#7  0x158ddc0d in f_defglobal (arglist=13001035) at syntax.c:404
#8  0x158c452d in apply (func=1019, args=13001035) at main.c:1379
#9  0x158bfa1f in eval (addr=13001036) at main.c:1348
#10 0x158d13ac in f_load (arglist=13000003) at function.c:2020
#11 0x158c4506 in apply (func=900, args=13000003) at main.c:1378
#12 0x158bf9c4 in eval (addr=13000002) at main.c:1346
#13 0x158d13ac in f_load (arglist=6000003) at function.c:2020
#14 0x158c4506 in apply (func=900, args=6000003) at main.c:1378
#15 0x158bf9c4 in eval (addr=6000002) at main.c:1346
#16 0x158beed2 in main (argc=1, argv=0xcf7f8be4) at main.c:326
(gdb) p/x 2146435071
$2 = 0x7fefffff

gdb上で動かしていると、SEGVをキャッチして止まってくれるので、わざわざsigactionを使って調べる必要も無い。

と言う事で、最後までSEGVに悩まされました。

来年はコロナもSEGVも、何それと言えるような年になって欲しいものです。 来年も宜しく。

Happy Hacking with sources.


This year's Index

Home