ksh with gdb

Table of Contents

gdb and target

Q: gdbとそのターゲットの関係について質問します。 ターゲットはdashとkshです。下記の様に、ターゲットを起動した後、Ctl-Cで gdbに制御権が移行する(dash)ものと、しない(ksh)ものがあります。 何故この様な違いが発生するか、gdbの制御機構をふまえて説明してください。 環境は、GNU gdb (GDB) 16.3 と、OpenBSD 7.8(ksh)です。

ob$ gdb -q dash
Reading symbols from dash...
(gdb) r
Starting program: /home/sakae/dash-0.5.12/src/dash
$ ^C
Program received signal SIGINT, Interrupt.
_thread_sys_read () at /tmp/-:2
warning: 2      /tmp/-: No such file or directory
ob$ gdb -q ksh
Reading symbols from ksh...
(gdb) r
Starting program: /home/sakae/my-ksh/ksh
ob$   ### ^C ignore, don't switch gdb
ob$
ob$ pstree
 :
 | \-+= 37903 sakae -ksh (ksh)
 |   \-+= 00506 sakae gdb ksh (egdb)
 |     \--= 93037 sakae /home/sakae/my-ksh/ksh

ターゲットはgdbの子供になっている。

A: いい質問 — 振る舞いの差は 「Ctrl-C(SIGINT)がどこに届くか」ターゲット自身がそのシグナルをどう扱うか の組合せに由来します。以下、gdb の制御機構(端末・プロセス群・ptrace/wait)を踏まえて順を追って説明します。最後に確認用の実験コマンドと回避策も書きます。


## 要点(結論)

  • 端末からの Ctrl-C はカーネルが foreground process group(フォアグラウンド pgid)SIGINT を送る。
  • gdb は通常、実行中のインフェリア(ターゲット)に端末の制御を渡し(=tcsetpgrp= 相当の操作を行う)、かつ =ptrace=(もしくは OS の追跡機構)で子の状態変化(停止やシグナル)を監視する。
  • /dash の場合/:=SIGINT= が子(dash)で「止まる/デフォルト動作で止まる」ため、ptrace 経由で gdb にその停止が伝わり Program received signal SIGINT と報告され、gdb に制御が戻る。
  • ksh の場合/:ksh が SIGINT を自分で /捕まえて(ハンドラで処理し) 継続する/シグナルを吸収する(たとえばプロンプト再表示、=read()= を再開する、あるいは SA_RESTART 相当でシステムコールを再開する)ため、プロセスが止まらず /gdb に「停止通知」が来ない/。結果として gdb 側は「切り替わらない」ように見える。

## もう少し詳しく:仕組みを順に追う

  1. 端末と SIGINT の発生
    • ユーザーが端末で Ctrl-C を押すと、端末ドライバがその端末の フォアグラウンドプロセスグループ (fg pgrp) 全体に SIGINT を送る。
    • どのプロセスがその fg-pgrp を持っているかが重要。gdb が端末を持っている時と、gdb が子に端末を渡している時で挙動は異なる。
  2. gdb と端末制御(tcsetpgrp)
    • 通常 gdb は run 時に(子を)フォアグラウンドにし、端末制御を子に渡すので(子が端末を所有)、=Ctrl-C= は子に届く。
    • その上で gdb は ptrace/wait 系で子のイベント(停止やシグナル)を待ち、子がシグナルで停止すると wait が返って gdb がその事実を報告する。
  3. ptrace(=デバッガが子を監視する仕組み)とシグナルの伝播
    • 子に SIGINT が来て 子が停止状態(SIGINT によって止まる) になれば、ptrace/wait によって親(gdb)にイベントが届き、gdb が Program received signal SIGINT を出す。これが dash のケース。
    • /一方、子が SIGINT を捕まえて自前の処理で継続する場合/(シェルがプロンプトを再表示する、read を再開する等)、子は停止しないので gdb に停止イベントが届かない。従って gdb のプロンプトに切り替わったように見えない(ksh のケース)。
  4. シェル固有の振る舞い(ksh vs dash)
    • シェルは /インタラクティブな振る舞い/(プロンプト表示、行編集、子プロセスの起動時の信号伝播など)を実装しており、/SIGINT に対するデフォルト動作を自前で処理する/ことが多い。
    • 例えば「プロンプト待ちのときに Ctrl-C を押すとプロンプトを再表示する」等はシェル側でシグナルを捕まえて処理しているため、プロセスが停止しない。
    • 実装差で dash は read() を割り込ませて停止したり(結果として gdb がシグナルを検知する状況になる)、ksh は SIGINT をハンドラで処理して read を再スタートする(=SARESTART=)ため見た目が違っている――という可能性が高いです。

## 実験・検証方法(推奨)

次のコマンドで「どのプロセスがどの pgid を持っているか」「端末は誰のものか」「シグナルが到達しているか」を調べられます(OpenBSD 想定の例)。

  1. 実行前後のプロセス群情報:
# gdb を起動・run の最中に別端末から:
ps -o pid,ppid,pgrp,sid,tty,stat,command -ww -p <gdb-pid>,<child-pid>

(=pgrp= が誰のものかを確認)

  1. OpenBSD なら truss でシグナルの様子を見る:
# 別端末で truss を attach
truss -p <child-pid>
# Ctrl-C を押して、child に SIGINT が到っているか/ハンドラが走っているかを確認
  1. gdb 側で端末割当を明示してみる(別 pty を与える):
(gdb) tty /dev/pts/5
(gdb) run

これで gdb が子に別の pty を与え、端末制御の振る舞いを変えられます(比較実験に有用)。

  1. シェル内で trap を確認(ksh の場合): ksh のソースや実行時トラップ設定を見たいなら、=trap= コマンドや起動オプション/ソースを確認します(ソースコードレベルで SIGINT の扱いを見るのが確実)。

## 回避策(期待する動作に合わせる)

  • /常に gdb に戻したい(Ctrl-C を gdb で捕りたい)/なら、gdb 側で端末を渡さない/別の pty を使う、あるいは gdb のシグナルハンドリングを変更する方法を検討します。ただし普段は「端末を child に渡す」方がユーザに自然です。
  • /子の SIGINT を確実に gdb に通知させたい/場合:子を停止状態にするようにシグナルを送る(例:キーボードからではなく gdb の interrupt=/=signal を使う)、あるいは truss で挙動確認する。
  • gdb のコマンドで handle SIGINT stop 等の設定で動作を調整できます(gdb が受け取るべきシグナル動作の設定;ただしこれは主に gdb が ptrace で検出したシグナルをどう扱うかの指定です)。

## 補足(短く)

  • 本質は「/誰が端末のフォアグラウンド pgid を持っているか/」と「/ターゲットが SIGINT を止める(stop)か自力で処理するか/」の組合せです。
  • dash 側では SIGINT によってプロセス停止扱い→ gdb が報告。ksh 側ではシェルが SIGINT を捕まえて吸収/継続→ gdb に停止イベントが来ない。これが観測された違いの原因として最も妥当です。

もしよければ、あなたの環境で試せる具体的コマンドをこちらで整えます。たとえば

A: 出してくれた ps の出力は決定的です — これで動きの違いがほぼ説明できます。まず出力を手短に解釈して、なぜ Ctrl-C が gdb に戻ってこないかを説明します。

  PID  PPID  PGID TTY      STAT   COMMAND
  506 37903   506 ttyp2    I      egdb
93037   506 93037 ttyp2    IX+p   ksh

### 重要な観察点(要点)

  1. =egdb=(gdb)のプロセスグループ(PGID)は =506=。
  2. ksh の PGID は 93037 — /gdb と別のプロセスグループ/になっている。
  3. kshSTAT+ が付いている (IX+p)。BSD 系の ps ではこの +そのプロセスが端末のフォアグラウンドプロセスグループに属している ことを示します。 → つまり現在端末(=ttyp2=)のフォアグラウンド pgid は =93037=(ksh)になっている。

### 端末と Ctrl-C(SIGINT)の振る舞い

  • 端末で Ctrl-C を押すと、カーネルはその端末の フォアグラウンドプロセスグループ全体SIGINT を送ります。
  • 今回のケースではフォアグラウンド pgid が ksh=(93037)なので、=SIGINT は /ksh に届く/(gdb には届かない/gdb はそのシグナルを受けない)。
  • さらに重要なのは ksh がその SIGINT をどう扱うか です:
    • ksh が SIGINT を捕まえて(ハンドラで)自前に処理し、プロセスを停止させない場合、プロセスは停止しないので gdb(親)は ptrace/wait で停止イベントを受け取らず、結果として gdb のプロンプトに「切り替わらない」ように見えます。
    • dash のときは(あなたの最初の例)子が SIGINT によって停止扱いになったため、ptrace/wait が返り、gdb が Program received signal SIGINT と表示した —— という違いです。

### まとめ(なぜ ksh では切り替わらないか)

  • 今回 ksh は端末のフォアグラウンド(=+=)になっている → Ctrl-C は ksh に送られる。
  • ksh がその SIGINT を吸収/処理して停止しない → gdb に停止通知が来ない。
  • 結果:gdb 側には制御が戻らず、観測された通り Ctrl-C を押しても gdb プロンプトに切り替わらない。

(対照的に dash のケースはプロセスが停止扱いになったため gdb が通知を受け取り、=Program received signal SIGINT= を出した。)


### 実験/確認に有用なコマンド(そのまま試せます)

  • フォアグラウンドプロセスグループの確認(既に得られていますが補助):
ps -o pid,ppid,pgid,tt,stat,comm -p 506,93037
  • 別ターミナルから SIGINT を直接送って挙動を見る(gdb が検知するか確認):
kill -SIGINT 93037   # ksh に直接 SIGINT を送る
# あるいは子を一度停止させてから gdb が拾うか見る:
kill -STOP 93037
# (gdb が ptrace で attach しているなら wait イベントが来るはず)
  • truss でシグナル処理を観察(OpenBSD):
truss -p 93037
# Ctrl-C を端末で押して、ksh がどのようにシグナルを扱うか確認する
  • gdb に別の pty を与えて試す(gdb が端末を渡していない状態にする):
(gdb) tty /dev/ttypX   # 適当な未使用の pty を指定
(gdb) run

→ こうするとフォアグラウンドが誰かが変わり、Ctrl-C の行き先が変わります。

### 回避案(目的別)

  • 「Ctrl-C を押したら必ず gdb に戻したい」なら:
    • gdb がターゲットに端末を渡さない(あるいは別 pty を使う)ようにする。
    • または別端末から kill でシグナルを送る(デバッグ用)。
  • 「ターゲットの SIGINT 挙動を確認したい」なら trussstrace 相当でハンドラの動きを追うと確実。

try STOP

SIGINTが駄目なら、変わりにSIGSTOPで代行したらいいじゃんって提案してきた。 やってみるか。

te$ gdb -q /home/sakae/ksh-7.7/ksh
Reading symbols from /home/sakae/ksh-7.7/ksh...
(gdb) r
Starting program: /home/sakae/ksh-7.7/ksh
te$

こんな風にターゲットのkshを起動する。

te$ ps
  PID TT  STAT        TIME COMMAND
79597 p1  S        0:01.26 gdb -q /home/sakae/ksh-7.7/ksh (egdb)
 4634 p1  SX+p     0:00.05 /home/sakae/ksh-7.7/ksh
te$ kill -STOP 4634

別端末で、ターゲットのkshのPIDを確認。そしてそいつにSIGSTOPを投げる。

Program received signal SIGSTOP, Stopped (signal).
_thread_sys_read () at /tmp/-:2
warning: 2      /tmp/-: No such file or directory
(gdb) bt
#0  _thread_sys_read () at /tmp/-:2
#1  0x03510aa6 in _libc_read_cancel (fd=0, buf=0xcf7c79b3, nbytes=1)
    at /usr/src/lib/libc/sys/w_read.c:27
#2  0x146efc32 in blocking_read (fd=0,
    buf=0xcf7c79b3 "4\226R\225d\004:k4\320y|\317\006\"m\024\004:k4\027",
    nbytes=1) at misc.c:1092
#3  0x146ca01f in x_getc () at edit.c:123
#4  0x146d2206 in x_e_getc () at emacs.c:1835
#5  0x146cd086 in x_e_getu8 (buf=0xcf7c7a3f "", off=0) at emacs.c:1845
#6  0x146cca58 in x_emacs (buf=0x42f94008 "", len=4096) at emacs.c:319
#7  0x146c9cf5 in x_read (buf=0x42f94008 "", len=4096) at edit.c:101
#8  0x146ea896 in getsc_line (s=0x6f2650f8) at lex.c:1099
#9  0x146ea482 in getsc__ () at lex.c:988
#10 0x146e83e9 in getsc_bn () at lex.c:1659
#11 0x146e4f41 in yylex (cf=44) at lex.c:167
#12 0x146f4256 in get_command (cf=0) at syn.c:206
#13 0x146f3fef in pipeline (cf=0) at syn.c:77
#14 0x146f3df9 in andor () at syn.c:98
#15 0x146f392f in c_list (multi=0) at syn.c:118
#16 0x146f3850 in yyparse () at syn.c:64
#17 0x146f37fd in compile (s=0x6f2650f8) at syn.c:769
#18 0x146ecbc7 in shell (s=0x6f2650f8, toplevel=1) at main.c:628
#19 0x146ec43e in main (argc=1, argv=0xcf7c9324) at main.c:452

SIGSTOPはkshが注目していないので(食べちゃわない)、gdbがキャッチしてくれた。 ハンドラーのテーブルを確認。

(gdb) p sigtraps[2]
$5 = {
  signal = 2,
  name = 0x3473a8b "INT",
  mess = 0x3471815 "Interrupt",
  trap = 0x0,
  set = 0,
  flags = 192,   # = xc0
  cursig = 0x0,
  shtrap = 0x0
}
(gdb) p sigtraps[17]
$6 = {
  signal = 17,
  name = 0x3472162 "STOP",
  mess = 0x3471107 "Suspended (signal)",
  trap = 0x0,
  set = 0,
  flags = 0,
  cursig = 0x0,
  shtrap = 0x0
}

sh.h

/* values for Trap.flags */
#define TF_SHELL_USES   BIT(0)  /* shell uses signal, user can't change */
#define TF_USER_SET     BIT(1)  /* user has (tried to) set trap */
#define TF_ORIG_IGN     BIT(2)  /* original action was SIG_IGN */
#define TF_ORIG_DFL     BIT(3)  /* original action was SIG_DFL */
#define TF_EXEC_IGN     BIT(4)  /* restore SIG_IGN just before exec */
#define TF_EXEC_DFL     BIT(5)  /* restore SIG_DFL just before exec */
#define TF_DFL_INTR     BIT(6)  /* when received, default action is LINTR */
#define TF_TTY_INTR     BIT(7)  /* tty generated signal (see j_waitj) */
#define TF_CHANGED      BIT(8)  /* used by runtrap() to detect trap changes */
#define TF_FATAL        BIT(9)  /* causes termination if not trapped */

trap.flagsを変更したぐらいでは、SIGINTがgdbに届かないなあ。

Linux シグナルの基本と仕組み (カーネル v5.5 時点) こちらを見れば参考になるかな?

trap

linuxな人に教えられて、sigaction(int sig, const struct sigaction *act, struct sigaction *oact); なんてのを知った。オイラーの持っている本では、こんなの無かったぞ。進化してん ですかね。関連品には、

SEE ALSO
     kill(1), kill(2), ptrace(2), sigaltstack(2), sigprocmask(2),
     sigsuspend(2), wait(2), setjmp(3), sigaddset(3), sigblock(3),
     sigpause(3), sigvec(3), tty(4)

こんなのが有るとな。このコマンド(システムコール)は、割り込みハンドラーの登録 なんだな。プチ、低位の奴で嬉しいぞ。kshの中で、そのハンドラーを探して、そこに BPを置いてみた。

(gdb) bt 5
#0  trapsig (i=2) at trap.c:117
#1  0x19c81d59 in x_emacs (buf=0x4eeec008 "", len=4096) at emacs.c:385
#2  0x19c7ecf5 in x_read (buf=0x4eeec008 "", len=4096) at edit.c:101
#3  0x19c9f896 in getsc_line (s=0x51fefaf8) at lex.c:1099
#4  0x19c9f482 in getsc__ () at lex.c:988
(More stack frames follow...)

そしてCtl-CでSIGINTを送信。ちゃんとハンドラーに制御が渡ってきた。中にやってる 事は、きわめて僅か。長居は禁物です。フラグをセットしてるだけ。

実働部分は、 emacs.c に有った。

                case KINTR:
                        trapsig(SIGINT);
=>                      x_mode(false);
                        unwind(LSHELL);
                        x_arg = -1;
                        break;

emacs似の入力編集を実現する為のキー入力の一部になってた。そして、それを追跡 してくと unwindの中でsigsetjmp.sに突入してる。これで準初期化をしてんだな。こういう仕組み だと、gdbが付け入る隙が無いな。

stty

まあ、Ctl-C なんて文字は、emacs(もどき)にとっては、普通の文字だ。にも かかわらず、特別扱かいしてるのは、こんなアプリのせい。特殊文字ってか コントロール文字に対して、特別扱いしてねって設定がされているからに杉ない。

ad$ stty -e
 :
discard dsusp   eof     eol     eol2    erase   intr    kill    lnext
^O      ^Y      ^D      <undef> <undef> ^?      ^C      ^U      ^V
min     quit    reprint start   status  stop    susp    time    werase
1       ^\      ^R      ^Q      <undef> ^S      ^Z      0       ^W

Ctl-CでSIGINTを発生させてねって事だ。これって世界的(posix的)に決められて いる事なんだろうね。オイラー的には、

stty intr ^P

なんてやると、対応を変更できるぞ。

ad$ kill -l
 1    HUP Hangup                        17   STOP Suspended (signal)
 2    INT Interrupt                     18   TSTP Suspended
 3   QUIT Quit                          19   CONT Continued
 :

また、シグナル名は、こんな風にして確認できる。sttyで表示されるのとは、ちと 違うけど、名前が統一されていないのは、sttyの方言だ(統一性が無いな)。

syscall sigaction

あちら側(kernel)を追ってみる。kshを起動。

(gdb) bt 3
#0  sys_sigaction (p=0xd4614328, v=0xf57633b0, retval=0xf57633a8) at /usr/src/s\
ys/kern/kern_sig.c:264
#1  0xd06be629 in mi_syscall (p=0xd4614328, code=<optimized out>, argp=<optimiz\
ed out>, retval=<optimized out>, callp=<optimized out>) at /usr/src/sys/sys/sys\
call_mi.h:179
#2  syscall (frame=0xf57633f0) at /usr/src/sys/arch/i386/i386/trap.c:558
(More stack frames follow...)

ハンドラーの設置が開始されました。

        signum = SCARG(uap, signum);
        nsa = SCARG(uap, nsa);
        osa = SCARG(uap, osa);

        if (signum <= 0 || signum >= NSIG ||
            (nsa && (signum == SIGKILL || signum == SIGSTOP)))
                return (EINVAL);
=>      sa = &vec;
        if (osa) {
                mtx_enter(&p->p_p->ps_mtx);
                sa->sa_handler = ps->ps_sigact[signum];
                sa->sa_mask = ps->ps_catchmask[signum];

最初の設置は、こんなのでした。ご丁寧な事に、昔の設置情報もご希望とあらば、取得(osa) できるみたいです。

(gdb) p/x signum
$2 = 0xd

kill

殺人である。普通の人が聞いたらちょっと吃驚する様な語句。まあ業界用語だから その積りで。これって、プロセスを指定して、そいつに刺客を送るコマンドだ。

sys_kill にBPを置いて、起動してるdashに刺客を送信。ad$ kill -INT 21825

=>      if (pid > 0) {
                if ((pr = prfind(pid)) == NULL) {
                        if ((pr = zombiefind(pid)) == NULL)
                                return (ESRCH);
                        else
                                zombie = 1;
                }
(gdb) p pid
$1 = 21825
(gdb) p signum
$2 = 2

本当に面白い(読むべき)部分は、こちらだ。

/*
 * type = SPROCESS      process signal, can be diverted (sigwait())
 * type = STHREAD       thread signal, but should be propagated if unhandled
 * type = SPROPAGATED   propagated to this thread, so don't propagate again
 */
void
ptsignal(struct proc *p, int signum, enum signal_type type)
=>

README

日本手話がおしえてくれること なんて本を読んだ。NHKの手話ニュースとかで どんな風なものかは見ていたけど、それはうわべだけだった。

日本語 英語 ドイツ語 、、日本手話 アメリカ手話、、、のように、れっきとした 言語。日本語を誰が作った? 自然発生。じゃ、日本手話は? 自然発生。

私はパンを食べる って日本語は、手話で、私 パン 食べる の様に、てにをはが無い 表現。単語を提示してく形になるそうな。そして、その単語は、手の形 位置 動き の組み合わせで表現するそうだ。

それから、それらを補って、顔の表情や、頭の素振りで、疑問とか否定とか、時制 (過去、現在、未来)を表現するそうだ。

昔は多数の話者が居たけど、どんどん話者が減ってきて、絶滅言語になるのではと 心配されている。

ろう者のオリンピック、いわゆるデフリンピックが11月20日だったかに、 日本では初めて開催 されるそうだ。選手は公平の為、練習中及び試合中は、補聴器の使用が禁止だ そうだ。どんな展開になるか、観戦してみたいものだ。応援で大声を出しても それは選手に届かない。身振り手振りのジェスチャーは手話では無いからなあ。 それとも、手話方言が新らしく誕生する?


This year's Index

Home