SEGV and …

読書用眼鏡

Go to トラベルは Go to トラブルなんで、別の事を考える。たかが数日の快楽で、人生を棒に振るのは、もったいないですからね。

で、知的な旅のお供に、読書用のメガネを新調する事にした。読書用なんて言って気取っているけど、老眼鏡の事である。

今まで使ってた奴は、10年以上も前に作ったもの。さすがに眼も歳を喰って変化してるからね。意を決してね。

レンズは球面収差補正済みで、セット価格20K。鯖江製のチタンフレームが14K。これで少しは快適になれば、御の字だ。

Gauche 0.9.10

半年ぶりぐらいにgaucheの新しい版が出た。嬉しいのは

sakae@pen:~$ gosh -f read-edit
gosh$ ,help edit
Usage: edit [on|off]
Query, or turn on/off input editing mode.
Without arguments, show the current input editing mode.  With on or off, turns
on or off input editing mode.  Note that if gosh hasn't been started with input
editing feature, you can't turn it on afterwards.

こんな具合に、replに編集モードが付いた事。編集モードは、起動時にフラグで指定するか、環境変数で有効にする。編集モードが有効なら、プロンプトが gosh$ になる。

gosh$ (cons
......1
......'())
(1)
gosh$ M-h h
Gauche input editing quick cheat sheet:
  M-h b                  for keymap
  M-h k <keystroke> ...  for help of specific keystroke

  C-f/C-b   forward/backward char        M-f/M-b   forward/backward word
  C-p/C-n   prev/next line or history    M-p/M-n   prev/next history
  C-a/C-e   beginning/end line           M-</M->   beginning/end buffer
  C-@       mark                         C-w       kill region
  C-k       kill line                    M-d       kill word
  C-y       yank                         M-Y       yank pop
  C-_       undo                         M-C-x     commit input
  C-l       refresh disiplay
 To disable input editing: Type ,edit off

こんな具合に、emacs似のキーバインドが採用されてる。また、地味に嬉しいのが、M-( だ。 ()が自動入力されて、カーソルがカッコの中に鎮座してくれる。詳しい事は、 3.2.2 入力の編集 を参照。

gosh> (ed "hoge.scm")
Reload "hoge.scm"? [y/N]: y
#t

また、外部editorを呼び出せるようになった。外部editorの指定は

The editor keyword argument, if it’s a string.
The value of *editor* in the user module, if defined.
The value of the environment variable GAUCHE_EDITOR.
The value of the environment variable EDITOR.
If none works, the behavior depends on the value of :editor keyword argument:

このように、多用な方法で指定出来る。また、pagerも使えるようになって便利。

光回線が遅い? ならば

SEGV 詰め合わせ

正月も近づいて来たので、勝手にSEGVの詰め合わせセットを作ってみた。何と言ってもSEGVには、縁がありましたからね。

int main(void){
    int aa[8];
    aa[8888] = 0xbeef;
}

小さい入れ物を宣言。欲深い誰かさんは、そこに大きな牛を入れようとした。大きいやつなので、サイズを勝手に大きな所に取った。

vbox$ cc ary.c
ary.c:3:5: warning: array index 8888 is past the end of the array (which
      contains 8 elements) [-Warray-bounds]
    aa[8888] = 0xbeef;
    ^  ~~~~
ary.c:2:5: note: array 'aa' declared here
    int aa[8];
    ^
1 warning generated.

警告を出してきたけど、素直にバイナリーになった。実行すると、結果は勿論SEGV。

ary.c:3:5: warning: array index 8 is past the end of the array (which contains 8
      elements) [-Warray-bounds]
    aa[8] = 0xbeef;
    ^  ~

8888なんて言う超エリア外を指定しなくても、こういうのは、ついやっちまう間違い。Off by oneとか言ったけな。名前が付いているぐらいだから、誰もが経験してるだろう。オイラーの所では、実験したら

vbox$ ./a.out
Abort trap (core dumped)

虎視眈々と狙っている人がいるぞ。同じ事をリナでやってみたら、見逃し三振。バッファーオーバーフロー成立で、悪い人を呼び込む元になる。恐いOSだな。次は、超有名なやつ。

int main(void){
  *(int*)0 = 777;
}

ヌルポを使って、目出度い数値を書き込むやつ。

null.c:2:3: warning: indirection of non-volatile null pointer will be deleted,
      not trap [-Wnull-dereference]
  *(int*)0 = 777;
  ^~~~~~~~
null.c:2:3: note: consider using __builtin_trap() or qualifying pointer with
      'volatile'
1 warning generated.

これも、ちゃんと警告が出てきた。 この類のやつと言えば、最近、またOpenSSLでヌルポが見つかっている。 OpenSSL における NULL ポインタ参照の脆弱性 尽きませんなあ。

例では、正に0番地へのアタックなんだけど、256番地とかへのアタックだと、さすがにコンパイラーは検出してくれない。でも、走らせるとSEGVだ。

そもそも、リアルCPUだと、0番地から1000番地ぐらいは、特殊な目的に使われている(割り込みベクターとかね)。そんなエリアを破壊したらいかんぜよって事か? 普通に考えるとそうだけど、OSってかMMUでマッピングする時、その近辺には近寄らないようにするはず。だから、理論的には、普通に使って良いはず。但し0番地は、何も意味ある場所じゃ無いですよって事にしてるんだな。

伝統的に、コード領域は、メモリーエリアの低い所から使うって事にしてるんで、そこには書き込んでいけないと言う制限に触れるのだろう。これは下衆の勘繰りです。

int main(void){
  char *s = "happy";
  *s = 'H';
}

happyより、Happyの方が、幸せになるだろうって意図。神社で、大吉が出るまで、おみくじを引き続ける、あれである。特に咎められる行為ではない。けど、実行すると閻魔様が出現する。

charの前に、書き換え禁止のconstを付けると、

vbox$ cc ro.c
ro.c:3:6: error: read-only variable is not assignable
  *s = 'H';
  ~~ ^
1 error generated.

意図に反した行為って事で、バイナリー作成が拒否される。

int main(void){
  main();
}

lisper御用達の、歯止めが無い再帰呼び出し。素直にコンパイルと実行は出来る。けど、stackを食いつぶして爆発するぞ。

#include <unistd.h>
int main(void){
  sleep(1);
  main();
}

ついでなので、ゆったり動く奴も作っておく。(後で使う)

coredumpを追う

ヌルポで(も)coredumpするので、実験。

(gdb) bt
#0  coredump (p=0xd1867608) at /usr/src/sys/kern/kern_sig.c:1522
#1  0xd0612742 in sigexit (p=0xd1867608, signum=11) at /usr/src/sys/kern/kern_sig.c:1496
#2  0xd061334c in postsig (p=0xd1867608, signum=11) at /usr/src/sys/kern/kern_sig.c:1431
#3  0xd0614415 in userret (p=0xd1867608) at /usr/src/sys/kern/kern_sig.c:1890
#4  0xd0d05249 in trap (frame=0xf1ccf3c0) at /usr/src/sys/arch/i386/i386/trap.c:499
#5  0xd0e4c269 in calltrap ()
#6  0xf1ccf3c0 in ?? ()
#7  0x1b8e029a in ?? ()
Backtrace stopped: previous frame inner to this frame (corrupt stack?)

緊急のtrapから呼ばれている。形式的には終了するってカテゴリーになってて、後始末をするようにしてるのね。

(gdb) up
#3  0xd0614415 in userret (p=0xd1867608) at /usr/src/sys/kern/kern_sig.c:1890
1890                            postsig(p, signum);
(gdb) p signum
$4 = 11

死亡診断書には、SIGSEGVと記されるのか。

kill -9 pid

上で作った、1秒毎に再帰するのを走らせる。そのPIDを調べておいて、即死信号の9を送る。これは遺書を作る暇も与えない、暗殺者用だ。

(gdb) bt
#0  sys_kill (cp=0xd172348c, v=0xf1d37b94, retval=0xf1d37b8c) at /usr/src/sys/kern/kern_sig.c:578
#1  0xd0d059d9 in mi_syscall (p=0xd172348c, code=122, callp=0xd110543c <sysent+1464>, argp=0xf1d37b94, retval=0xf1d37b8c) at /usr/src/sys/sys/syscall_mi.h:92
#2  0xd0d05696 in syscall (frame=0xf1d37bd0) at /usr/src/sys/arch/i386/i386/trap.c:597
#3  0xd0e4c395 in Xsyscall_untramp ()
#4  0xf1d37bd0 in ?? ()
#5  0x16825ba3 in ?? ()
Backtrace stopped: previous frame inner to this frame (corrupt stack?)
(gdb) p pid
$5 = 38275
(gdb) p signum
$6 = 9

もう少し穏やかな奴、 Ctl+\ (sigquit) 信号送ってから、coreを採取するやつ。

(gdb) bt
#0  coredump (p=0xd1867608) at /usr/src/sys/kern/kern_sig.c:1522
#1  0xd0612742 in sigexit (p=0xd1867608, signum=3) at /usr/src/sys/kern/kern_sig.c:1496
#2  0xd061334c in postsig (p=0xd1867608, signum=3) at /usr/src/sys/kern/kern_sig.c:1431
#3  0xd0614415 in userret (p=0xd1867608) at /usr/src/sys/kern/kern_sig.c:1890
#4  0xd0d05a5d in mi_syscall_return (p=0xd1867608, code=91, error=4, retval=0xf1ccfa6c) at /usr/src/sys/sys/syscall_mi.h:112
#5  0xd0d05747 in syscall (frame=0xf1ccfab0) at /usr/src/sys/arch/i386/i386/trap.c:619
#6  0xd0e4c395 in Xsyscall_untramp ()
#7  0xf1ccfab0 in ?? ()
#8  0x08087627 in ?? ()
Backtrace stopped: previous frame inner to this frame (corrupt stack?)

syscallの中からcoredumpが呼ばれている。

(gdb) up
#2  0xd061334c in postsig (p=0xd1867608, signum=3) at /usr/src/sys/kern/kern_sig.c:1431
1431                    sigexit(p, signum);
(gdb) p signum
$7 = 3

確かにSIGQUITが来ているな。

Ctl-C

走っているアプリ(kernel屋さんはプロセスって言うな)を止める時は、普通Ctl-Cする。日常生活で、何気なく使っている。どういう仕組みなの?

勝手なタイミングでキーボードを叩く訳なんで、きっとキーボード割り込みに違いない。割り込みは、unix的にはシグナルに一本化される(はず)。ハード屋だと、小難しく、割り込みマスクがどうだとか優先度がどうだとか頭に浮かぶけど、そんなの取り合えず無視。で、 signal(3)をマニュアルから引く。

Name         Default Action       Description
SIGHUP       terminate process    terminal line hangup
SIGINT       terminate process    interrupt program
SIGQUIT      create core image    quit program
 :

どうやらSIGINTがそれっぽい。

debian:kern$ grep SIGINT *.c
kern_sig.c:             case SIGINT:
tty.c:                      pgsignal(tp->t_pgrp, SIGINT, 1);
tty.c:                              CCEQ(cc[VINTR], c) ? SIGINT : SIGQUIT, 1);

取り合えずkernelのソース群の中で、検索してみた。tty.cにSIGINT,SIGQUITってのの切り替えが出てきた。という事は、処理経路はSIGQUITと一緒の感じがする。

上で使ったゆっくり再帰するプログラムを動かしておいて、Ctl-Cしてみた。

(gdb) bt
#0  sigexit (p=0xd171fa8c, signum=2) at /usr/src/sys/kern/kern_sig.c:1486
#1  0xd061334c in postsig (p=0xd171fa8c, signum=2) at /usr/src/sys/kern/kern_sig.c:1431
#2  0xd0614415 in userret (p=0xd171fa8c) at /usr/src/sys/kern/kern_sig.c:1890
#3  0xd0d05a5d in mi_syscall_return (p=0xd171fa8c, code=91, error=4, retval=0xf1d16f9c) at /usr/src/sys/sys/syscall_mi.h:112
#4  0xd0d05747 in syscall (frame=0xf1d16fe0) at /usr/src/sys/arch/i386/i386/trap.c:619
#5  0xd0e4c395 in Xsyscall_untramp ()
#6  0xf1d16fe0 in ?? ()
#7  0x023b6627 in ?? ()
Backtrace stopped: previous frame inner to this frame (corrupt stack?)

ビンゴですね。signumも2(sigint)を示している。

一方、起動したら直ぐに終了するアプリ(main(){ return(0); } は、どうなっているかと言うと、

(gdb) bt
#0  sys_exit (p=0xd171fa8c, v=0xf1d179d4, retval=0xf1d179cc) at /usr/src/sys/kern/kern_exit.c:92
#1  0xd0d059d9 in mi_syscall (p=0xd171fa8c, code=1, callp=0xd1104e90 <sysent+12>, argp=0xf1d179d4, retval=0xf1d179cc) at /usr/src/sys/sys/syscall_mi.h:92
#2  0xd0d05696 in syscall (frame=0xf1d17a10) at /usr/src/sys/arch/i386/i386/trap.c:597
 :

暗黙的にexitのシステムコールが呼ばれている。このシステムコール内で、プロセスの後片付けが精力的に行われている(sigintとかも、同様だけど)。

上記の説明は半分嘘

Ctl-Cを押せば、強制的にアプリは停止するのか? 身近な例で、nprologでは停止しない。Ctl-Cは、単に無視される。そんな魔法はどんな仕組み?

debian:nprolog$ grep SIGINT *.c
main.c:    signal(SIGINT,reset);

main()関数の冒頭に宣言されてる。俗に言う割り込みハンドラーだ。SIGINT信号を受信したら、resetって関数を実行してねって言うカーネルへのお願いだ。

void reset(int i){
    ctrl_c_flag = 1;
}

アプリ内でやってる事は、これだけだ。ただ、signalの登録で、kernelが勝手に終了処理をしなくなる効果が有るんだ。前項の説明時の挙動は、このハンドラーの登録が無かった場合の挙動になる。

再びnprologに戻ると、セットされたフラグは、prove関数内で使われている。最終的にはホットスタート動作を引き起こす事になる。

debian:nprolog$ ./npl
N-Prolog Ver 1.66
?- Quit             %% Ctl-\ (sigquit)
debian:nprolog$

Ctl-\ で、coreを採取しようとしたけど、リナではcore作成が禁止されてて発動せず。

debian:nprolog$ ulimit -c 1000000
debian:nprolog$ ./npl
N-Prolog Ver 1.66
?- Quit (core dumped)
debian:nprolog$

core採取を許可したら、ちゃんとcoreが作成されて終了した。

in tty.c

上でSIGINTを探した時にtty.cが出てきた。現場に当たっておく。

pgsignal(tp->t_pgrp,
    CCEQ(cc[VINTR], c) ? SIGINT : SIGQUIT, 1);

pgsignal関数の引数の一部になってる。そこでpgsignal(9)を引く。

NAME
     psignal, pgsignal, pgsigio – post signal to a process

SYNOPSIS
     void
     pgsignal(struct pgrp *pgrp, int signum, int checkctty);

     The pgsignal() function posts signal number signum to each member of the
     process group described by pgrp.  If checkctty is non-zero, the signal
     will be posted only to processes that have a controlling terminal.  If
     pgrp is NULL no action is taken.

CODE REFERENCES
     These functions are implemented in the file sys/kern/kern_sig.c.

ターミナルの制御権を持ってる(フォグランドで動いてる)プロセスに、signumを送る。割り込みの送信元って事だな。納得しました。OpenBSDのマニュアルは、重要なカーネル内関数も解説されているので、大助かりです。 御親切に、ここ見ろワンワンも書いてあったぞ。


This year's Index

Home