editor
Table of Contents
FreeBSD
sh集中講座なんでFreeBSD方面も視察しとく。デフォはcshらしいけど、スクリプトは やっぱり、/bin/shだ。勿論同梱されてる。そして歴史を俯瞰すると
HISTORY
A sh command, the Thompson shell, appeared in Version 1 AT&T UNIX. It
was superseded in Version 7 AT&T UNIX by the Bourne shell, which
inherited the name sh.
This version of sh was rewritten in 1989 under the BSD license after the
Bourne shell from AT&T System V Release 4 UNIX and first appeared in
4.3BSD-Net/2.
AUTHORS
This version of sh was originally written by Kenneth Almquist.
こんなのが出てきたぞ。
それはいいんだけど、ソースを眺めるとTOURなんていう300行の解説書が出てきた。 チャッピー君に翻訳依頼。当時の解説書なんで、現状と合致しない部分も多分に あるが、参考になる事は間違いない。たとえば、C言語には例外が無いんで、 setjump/longjumpで賄いましたとか、ね。ちゃんと文書の背景を理解した上での 翻訳なんで、不自然さは全くない。よい時代だなあ。
editor
前前回はチャッピー(ChatGPTの愛称)に dash/kshの関数と変数の扱かいについて 質問した。丁寧な回答があって、これって個人がライターを雇って特定なお題で 記事を執筆して貰う行為だなあと思った次第。でも、きっと間違いが含まれているに 違いない。雇った者の責任として検証が必要だろう。いわば、出版業界のeditor 編集者になって、確認するんだ。ってな事で、viとかemacsとかの、あっち方面を 想像してた人、ごめんなさい、です。
縁の下
sh/TOURの資料によれば、最初にこんな説明が来ていた。
SOURCE CODE GENERATORS: Files whose names begin with "mk" are
programs that generate source code. A complete list of these
programs is:
program input files generates
------- ----------- ---------
mkbuiltins builtins.def builtins.h builtins.c
mknodes nodetypes nodes.h nodes.c
mksyntax - syntax.h syntax.c
mktokens - token.h
そんな訳なんで、 dash村を影で支える神達に焦点を当てます。dash村が正常に作成されれば、元の 存在はすっかり忘れられてしまう者達です。 普通にコンパイルした時のログ
CC builtins.def GEN builtins.h CC mknodes GEN nodes.h GEN token.h CC mksyntax GEN syntax.h : GEN init.c CC init.o CC nodes.o CC mksignames GEN signames.c CC signames.o CC syntax.o CCLD dash
何やらソースを自作してる様です。コンパイル後に詳細を確認
: -rw-r--r-- 1 sakae wheel 23300 Dec 11 2022 eval.c -rw-r--r-- 1 sakae wheel 28384 Dec 11 2022 Makefile.in -rw-r--r-- 1 sakae wheel 26951 Oct 18 06:50 Makefile -rw-r--r-- 1 sakae wheel 826 Oct 18 06:51 builtins.def -rw-r--r-- 1 sakae wheel 2089 Oct 18 06:51 builtins.c -rw-r--r-- 1 sakae wheel 1471 Oct 18 06:51 builtins.h -rwxr-xr-x 1 sakae wheel 19884 Oct 18 06:51 mknodes* -rw-r--r-- 1 sakae wheel 2532 Oct 18 06:51 nodes.h -rw-r--r-- 1 sakae wheel 9499 Oct 18 06:51 nodes.c -rw-r--r-- 1 sakae wheel 477 Oct 18 06:51 token.h -rw-r--r-- 1 sakae wheel 788 Oct 18 06:51 token_vars.h -rwxr-xr-x 1 sakae wheel 18260 Oct 18 06:51 mksyntax* -rw-r--r-- 1 sakae wheel 1725 Oct 18 06:51 syntax.h -rw-r--r-- 1 sakae wheel 13152 Oct 18 06:51 syntax.c drwxr-xr-x 3 sakae wheel 512 Oct 18 06:52 bltin/ -rwxr-xr-x 1 sakae wheel 25788 Oct 18 06:52 mkinit* -rw-r--r-- 1 sakae wheel 6838 Oct 18 06:52 init.c -rwxr-xr-x 1 sakae wheel 9820 Oct 18 06:52 mksignames* -rw-r--r-- 1 sakae wheel 643 Oct 18 06:52 signames.c -rwxr-xr-x 1 sakae wheel 343556 Oct 18 06:52 dash*
確かに*.h,*.cを作成して、それをコンパイルして補助ユーティリティーを 作成してました。そこん所をもう少し詳しく。
gmake V=1 >LOG (普通は、これで十分なログを採取できるはずだけど、何も反応が 無い場合は、ドライラン gmake -n がお勧め。それでも駄目なら、gmake -d)
gmake all-recursive gmake[1]: Entering directory '/tmp/dash-0.5.12' Making all in src gmake[2]: Entering directory '/tmp/dash-0.5.12/src' cc -DHAVE_CONFIG_H -I. -I.. -include ../config.h -DBSD=1 -DSHELL -Wall -g -E -x c -o builtins.def builtins.def.in /bin/sh mkbuiltins builtins.def cc -I. -I.. -DBSD=1 -DSHELL -g -O2 -Wall -o mknodes mknodes.c ./mknodes nodetypes nodes.c.pat /bin/sh mktokens cc -I. -I.. -DBSD=1 -DSHELL -g -O2 -Wall -o mksyntax mksyntax.c ./mksyntax : cc -I. -I.. -DBSD=1 -DSHELL -g -O2 -Wall -o mkinit mkinit.c ./mkinit alias.c arith_yacc.c arith_yylex.c cd.c error.c eval.c exec.c expand.c histedit.c input.c jobs.c mail.c main.c memalloc.c miscbltin.c mystring.c option s.c parser.c redir.c show.c trap.c output.c bltin/printf.c system.c bltin/test.c bltin/times.c var.c depbase=`echo init.o | sed 's|[^/]*$|.deps/&|;s|\.o$||'`;\ cc -DHAVE_CONFIG_H -I. -I.. -include ../config.h -DBSD=1 -DSHELL -Wall -g -MT init.o -MD -MP -MF $depbase.Tpo -c -o init.o init.c &&\ mv -f $depbase.Tpo $depbase.Po depbase=`echo nodes.o | sed 's|[^/]*$|.deps/&|;s|\.o$||'`;\ cc -DHAVE_CONFIG_H -I. -I.. -include ../config.h -DBSD=1 -DSHELL -Wall -g -MT nodes.o -MD -MP -MF $depbase.Tpo -c -o nodes.o nodes.c &&\ mv -f $depbase.Tpo $depbase.Po cc -I. -I.. -DBSD=1 -DSHELL -g -O2 -Wall -o mksignames mksignames.c ./mksignames depbase=`echo signames.o | sed 's|[^/]*$|.deps/&|;s|\.o$||'`;\ cc -DHAVE_CONFIG_H -I. -I.. -include ../config.h -DBSD=1 -DSHELL -Wall -g -MT signames.o -MD -MP -MF $depbase.Tpo -c -o signames.o signames.c &&\ mv -f $depbase.Tpo $depbase.Po depbase=`echo syntax.o | sed 's|[^/]*$|.deps/&|;s|\.o$||'`;\ cc -DHAVE_CONFIG_H -I. -I.. -include ../config.h -DBSD=1 -DSHELL -Wall -g -MT syntax.o -MD -MP -MF $depbase.Tpo -c -o syntax.o syntax.c &&\ mv -f $depbase.Tpo $depbase.Po cc -Wall -g -o dash alias.o arith_yacc.o arith_yylex.o cd.o error.o eval.o exec.o expand.o histedit.o input.o jobs.o mail.o main.o memalloc.o miscbltin.o mystring.o options.o parser.o redir.o show.o trap.o output.o bltin/printf.o system.o bltin/test.o bltin/times.o var.o builtins.o init.o nodes.o signames.o syntax.o
後は、丁寧にコードを見ていくだけだな。
INTOFF/INTON
GENしてるコードとは、ちと違うんだけど???な部分を発見しちゃったので、忘れない うちに聞いておく。
Q: コードを見ていたら、INTOFF … INTON と言うマクロを発見しました。 単方向リストの更新の様なクリチカルな部分で、割り込みから保護していると 判断しました。 このマクロ部分、特にbarrierについて、詳しく説明してください。
A: 良い発見です — INTOFF / INTON はまさに「短いクリティカルセクションを作って、シェルの内部状態をシグナル(割り込み)ハンドラから保護する」ための極めて軽量な仕掛けです。順を追って細かく説明します。
## 全体の意図(高レベル)
suppressintは「割り込み(シグナル)処理の実際の後処理(=onint()= を呼ぶなど)を抑止するカウンタ」です。INTOFFでsuppressint++して抑止を開始し、=INTON= で--suppressintして抑止を解除します。- シグナルハンドラ自体は(おそらく)直接
onint()を実行せず、かわりにintpendingを立てて戻る設計になっていて、INTONのタイミングでonint()を呼ぶようにしています。 (このパターンにより、ハンドラ中で制約の多い処理を避け、安全な文脈で後処理を行えます。) suppressintをカウンタにしているのはネスト対応(複数箇所でINTOFFしても最後に一度だけonint()が走る)にするためです。
## 重要な点:=barrier()= の意味と効果
あなたが示した barrier() はこう定義されています:
#define barrier() ({ __asm__ __volatile__ ("": : :"memory"); })
これがやっていること:
- コンパイラバリア(compiler barrier)
- 空のインラインアセンブリ文(=""
)に =volatileを付け、="memory"= を clobber に指定しているため、/コンパイラはこの命令の前後でメモリアクセスや命令の順序を入れ替えたり、省略したりしない/と考えます。 - つまり、=barrier()= の前にある読み書きは
barrier()の後に移動しないし、その逆もありません。コンパイラ最適化による並べ替えを防ぎます。
- 空のインラインアセンブリ文(=""
- しかしハードウェアのメモリ順序は保証しない
- これはあくまで「コンパイラに対する命令/アクセス順序の約束」であって、CPUのメモリモデル(store/load のハードウェア順序)を制御する フルメモリバリア(fence) ではありません。
- マルチプロセッサ環境で他の CPU に対して完全な順序保証が必要なら、=_syncsynchronize()= や C11 の
atomic_thread_fence(memory_order_seq_cst)=、あるいは実機の =mfence=/=dmb相当が必要です。
- シグナル文脈での効果
- シグナルは「非同期で実行される」ため、コンパイラがメモリアクセスを最適化して隠してしまうと、シグナルハンドラと本流での確認タイミングが狂います。=barrier()= によってコンパイラが
suppressint++等を先送りしないようにし、意図したタイミングでアクセスが行われるようにします。
- シグナルは「非同期で実行される」ため、コンパイラがメモリアクセスを最適化して隠してしまうと、シグナルハンドラと本流での確認タイミングが狂います。=barrier()= によってコンパイラが
## INTOFF / INTON の振る舞い(細部)
#define INTOFF \
({ \
suppressint++; \
barrier(); \
0; \
})
#define INTON \
({ \
barrier(); \
if (--suppressint == 0 && intpending) onint(); \
0; \
})
({ ... ; 0; })は GNU の /statement expression/。マクロを式として使えるように0を返しています(評価値は無視されることが多い)。- =INTOFF=:
- =suppressint++=:抑止カウンタを増やす(ネスト対応)。
- =barrier()=:コンパイラに対してこの増分が「この位置で」確実に評価されたとみなさせる(最適化で移動されない)。
- =INTON=:
- =barrier()=:クリティカル領域の直前までのメモリアクセス等を完了させる(コンパイラ順序)。
--suppressint =0 && intpending=:最後のINTONでintpendingが立っていればonint()を呼ぶ。
この順序は重要で、=INTOFF= の後で行う処理が suppressint が増えたことを前提に安全に実行され、=INTON= の barrier() によって「クリティカル領域でやったこと」が onint() 実行前に見える(コンパイラが勝手に移動しない)ようにしています。
## 実装上の注意点・安全性の指摘
volatile sig_atomic_tを使っていますか?- シグナルハンドラからアクセスされる共有変数(典型的には
intpendingやsuppressint=)は、C 標準では =sig_atomic_t型で扱い、可能ならvolatileを付けるのが安全です。 - 例:
volatile sig_atomic_t intpending = 0, suppressint = 0; sig_atomic_tは signal ハンドラとメイン実行スレッド間で「原子的に読み書きできる」ことを保証する最小型です(通常は =int=)。完全な原子性は環境依存ですが、標準的な C の signal-safe の指針です。
- シグナルハンドラからアクセスされる共有変数(典型的には
suppressint++=/–suppressint= は本当に安全か?- 単一スレッド + 非同期シグナルという環境では普通うまく動くことが多いですが、=++/–= は読んで書く二段操作なので、マルチスレッド環境では競合になります。
- OpenBSD のユーザランドシェルは通常マルチスレッドではない(標準ビルド)ので実用上は問題にならないことが多いですが、スレッド化した場合は
__atomic操作等に置き換えるべきです。
barrier()はシグナルの「到達順」を保証しないbarrier()はコンパイラ最適化を止めるだけで、割り込み(シグナル)がいつ発生するか自体は制御できません。つまり、シグナルがINTOFFの直後に来る可能性はありますが、そのハンドラはintpendingを立てておくだけにしておけば安全です。
- 代替手段(より堅牢)
- シグナルそのものを本当にブロックしたければ、=sigprocmask()= でマスク(ブロック)する方法が標準的で強固です。=sigprocmask()= はシグナルの配送自体をブロックします。
- ただし
sigprocmask()はシステムコールでコストが高いので、短いクリティカルセクション(非常に頻繁に呼ばれる箇所)ではsuppressint=+=intpendingの方式が高速で使われることがあります。
## まとめ(実務的アドバイス)
barrier()=(空アセンブラ+"memory"=)は/コンパイラの並べ替えを防ぐ/「コンパイラバリア」です。CPU のメモリ順序を制御する完全な fence ではない点に注意。INTOFF=/=INTONは /ネスト可能な割り込み抑止機構/。シグナルハンドラ側はintpendingを立てるだけにしておき、=INTON= 側が安全な文脈でonint()を呼ぶ設計になっています。- 変数は
volatile sig_atomic_tを使うのが望ましい。マルチスレッドを考えるならより強力な原子操作(=_atomic=)やsigprocmask()に切り替えるべきです。
fuga='This is var'
先にvarの扱かいを調査する。構成を変更するのは編集者の特権だ。チャッピーが提示 してきた原稿では、setvarで変数が登録されるはずなんだけど、、、そうなっていない。 はてどうする? ソースの海をただよって、溺れるか?
いや、変数を定義するって事は、そのエリアを確保する必要が有る。エリアの確保って事は mallocとかだな。dashではmallocをラップしたckmallocが用意されてる。そこにBPを 置けば、きっと変数誕生の現場に遭遇できるだろう。
(gdb) bt #0 ckmalloc (nbytes=16) at memalloc.c:55 #1 0x16155933 in setvareq (s=0x361330d8 <stackbase+76> "fuga=This is var fuga", flags=0) at var.c:291 #2 0x1613d34b in evalcommand (cmd=0x361330b8 <stackbase+44>, flags=0) at eval.c:859 #3 0x1613cb53 in evaltree (n=0x361330b8 <stackbase+44>, flags=0) at eval.c:299 #4 0x16149166 in cmdloop (top=1) at main.c:241 #5 0x16148ef5 in main (argc=1, argv=0xcf7d3284) at main.c:176
ビンゴでしたね。16バイトのエリア要求が、setvareqって関数から発行されている。 この関数の出口では、
(gdb) p *vp
$1 = {
next = 0x0,
flags = 0,
text = 0x4bbcf760 "fuga=This is var fuga",
func = 0x0
}
これが変数の正体だな。
そんじゃ、変数の参照系は? echo $fuga で、確認すれば良いな。
#0 lookupvar (name=0x361330aa <stackbase+30> "fuga=") at var.c:314 #1 0x16143556 in varvalue (name=0x361330aa <stackbase+30> "fuga=", varflags=1, flags=513, quoted=0) at expand.c:950 #2 0x161425cf in evalvar (p=0x361330af <stackbase+35> "", flag=1) at expand.c:716 #3 0x1614149e in argstr (p=0x361330a9 <stackbase+29> "!fuga=", flag=1) at expand.c:332 #4 0x16140f88 in expandarg (arg=0x361330b0 <stackbase+36>, arglist=0xcf7d3144, flag=3) at expand.c:192 #5 0x1613d0fc in evalcommand (cmd=0x361330c0 <stackbase+52>, flags=0) at eval.c:807 #6 0x1613cb53 in evaltree (n=0x361330c0 <stackbase+52>, flags=0) at eval.c:299 #7 0x16149166 in cmdloop (top=1) at main.c:241 #8 0x16148ef5 in main (argc=1, argv=0xcf7d3284) at main.c:176
更新系は? 例えば、fuga=5555 。更新するには、その場所を割り出さなければならない。 きっと、変数名のハッシュ値が必要なんだろう。その役目をするのは、hashbarなんで、 そこにBPを置く。
#0 hashvar (p=0x3a4260d0 <stackbase+68> "fuga=5555") at var.c:626
#1 0x1a44874b in setvareq (s=0x3a4260d0 <stackbase+68> "fuga=5555", flags=0)
at var.c:250
#2 0x1a43034b in evalcommand (cmd=0x3a4260b0 <stackbase+36>, flags=0)
at eval.c:859
#3 0x1a42fb53 in evaltree (n=0x3a4260b0 <stackbase+36>, flags=0) at eval.c:299
#4 0x1a43c166 in cmdloop (top=1) at main.c:241
#5 0x1a43bef5 in main (argc=1, argv=0xcf7d1864) at main.c:176
hashvarが少々気になるので、閲覧。
621 STATIC struct var **
622 hashvar(const char *p)
623 {
624 unsigned int hashval;
625
626 hashval = ((unsigned char) *p) << 4;
627 while (*p && *p != '=')
628 hashval += (unsigned char) *p++;
629 return &vartab[hashval % VTABSIZE];
630 }
mine() { echo my func; }
今度は、関数系の追跡。変数の追跡と同様の手口を適用。これって汎用的な手法だな。 dashを一度起動して(初期化で頻繁に環境変数やら、組み込みコマンドの登録が 行なわれるのでウザイ)、落ち着いたら、やおらBPを設定する。
(gdb) r
Starting program: /home/sakae/dash-0.5.12/src/dash
$ ^C
Program received signal SIGINT, Interrupt.
(gdb) b ckmalloc
Breakpoint 1 at 0x1b52158d: file memalloc.c, line 55.
(gdb) c
Continuing.
mine() { echo my func; } ## at dash side
Breakpoint 1, ckmalloc (nbytes=110) at memalloc.c:55
55 p = malloc(nbytes);
(gdb) bt
#0 ckmalloc (nbytes=110) at memalloc.c:55
#1 0x1b52eea9 in copyfunc (n=0x3b50b098 <stackbase+12>) at nodes.c:111
#2 0x1b518798 in defun (func=0x3b50b098 <stackbase+12>) at exec.c:736
#3 0x1b514be0 in evaltree (n=0x3b50b098 <stackbase+12>, flags=0) at eval.c:315
#4 0x1b521166 in cmdloop (top=1) at main.c:241
#5 0x1b520ef5 in main (argc=1, argv=0xcf7c4ff4) at main.c:176
実行の方は、こちら
#0 find_command (name=0x369d10c0 <stackbase+52> "mine", entry=0xcf7cd944,
act=16,
path=0xcf7cdc86 "/usr/local/go/bin:....:/usr/local/sbin") at exec.c:343
#1 0x169daf8f in evalcommand (cmd=0x369d10a8 <stackbase+28>, flags=0)
at eval.c:780
#2 0x169dab53 in evaltree (n=0x369d10a8 <stackbase+28>, flags=0) at eval.c:299
#3 0x169e7166 in cmdloop (top=1) at main.c:241
#4 0x169e6ef5 in main (argc=1, argv=0xcf7cda94) at main.c:176
table
さて、本来の主題である、各種テーブルの存在。ソースから軽く探してみたら、 下記が見付かった。エイリアスもテーブルを持っているのね。ついでに、テーブルサイズも 調べておいた。
vartab[hashval % VTABSIZE] # VTABSIZE=39 cmdtable[hashval % CMDTABLESIZE] # CMDTABLESIZE=31 atab[ATABSIZE] # ATABSIZE=39
alias.cは登録されてる関数も少なく、単純なんで参照するには好都合。setaliasにBP を置いて実観測するのが良いだろう。
(gdb) r
Starting program: /home/sakae/dash-0.5.12/src/dash
$ ^C
(gdb) print *atab@39
$1 = {0x0 <repeats 39 times>}
(gdb) c
Continuing.
alias p='echo'
$ ^C
(gdb) print *atab@39
$2 = {0x0 <repeats 32 times>, 0x4a305540, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}
(gdb) print *atab[32]
$3 = {next = 0x0, name = 0x4a312cb0 "p", val = 0x4a3217d0 "echo", flag = 0}
エイリアスは全く登録されていない。echoと打鍵するなんて、かったるいんで、p で、 代行させよう。ひとつだけ登録されたんで、それを確認してみた。
そんじゃ、変数の方は? dashが起動完了した時の変数テーブルの状態。
(gdb) set print symbol off
(gdb) print *vartab@39
$5 = {0x0, 0x7b41b9a0, 0x3bf7d91c, 0x0, 0x0, 0x0, 0x7b445a00, 0x7b41baf0, 0x0,
0x0, 0x3bf7d97c, 0x3bf7d95c, 0x0, 0x3bf7d96c, 0x0, 0x7b447cc0, 0x7b447590,
0x0, 0x7b445a10, 0x0, 0x0, 0x3bf7d93c, 0x0, 0x0, 0x0, 0x7b447160, 0x0,
0x7b41b640, 0x0, 0x3bf7d90c, 0x3bf7d92c, 0x3bf7d98c, 0x0, 0x7b4131a0,
0x7b41bad0, 0x7b445cf0, 0x0, 0x7b445a20, 0x0}
テーブルの3スロット目を適当に選んでみた。
(gdb) print *vartab[2]
$6 = {
next = 0x7b445300,
flags = 13,
text = 0xcf7c86bb "MAIL=/var/mail/sakae",
func = 0x1bf93b60 <changemail>
}
(gdb) p *$.next
$7 = {
next = 0x7b4479e0,
flags = 9,
text = 0xcf7c86d0 "SSH_CLIENT=127.0.0.1 51452 22",
func = 0x0
}
(gdb)
$8 = {
next = 0x0,
flags = 9,
text = 0xcf7c87db "USER=sakae",
func = 0x0
}
ハッシュが衝突しているので、リンクになってる。
(gdb) set print array
(gdb) print *vartab@10
$11 = {
0x0,
0x7b41b9a0,
0x3bf7d91c,
0x0,
0x0,
0x0,
0x7b445a00,
0x7b41baf0,
0x0,
0x0
}
配列のエレメントは、通常、詰めて表示されるんだけど、上記の様にのびのびと表示 させる事も出来る。なお、@10ってのは、10個まで表示してねって指示になる。
(gdb) c
Continuing.
mine() { echo function; }
$ ^C
(gdb) print *cmdtable@31
$13 = {
0x0 <repeats 30 times>,
0x7b413f50
}
(gdb) print *cmdtable[30]
$14 = {
next = 0x0,
param = {
index = 2067845600,
cmd = 0x7b40d1e0,
func = 0x7b40d1e0
},
cmdtype = 1,
rehash = 0 '\000',
cmdname = "m"
}
最後は関数の確認。そして、その型。
struct tblentry {
struct tblentry *next; /* next entry in hash chain */
union param param; /* definition of builtin function */
short cmdtype; /* index identifying command */
char rehash; /* if set, cd done since entry created */
char cmdname[ARB]; /* name of command */
};
OpenBSD 7.8は予定日より早く世に出てきたんで、これぐらいにしておこう。 怒涛のアップデートだな。
watch
ちょっと難題をふっかけてみる。
Q: 配列のエレメントのどれかについて、書き込みが発生した時にbreakさせたいです。 どうしたら良いですか?
A: 良い狙いです — 「どこかで配列のスロットが最初に書かれる瞬間」を捕まえれば、初期化コードが確実に見つかります。以下に gdb を使ってメモリ書き込みで止める方法(watchpoint) と、実務上の制約・回避策を整理します。具体的な gdb コマンド例付きです。
gdb の watch は指定した式の 値が書き変わったときに停止 します。=rwatch= は読み込み、=awatch= は読み書き両方を捕まえます。
あなたの場合、=vartab= は struct var /vartab[VTABSZ] のような配列(ポインタ配列)なので、/各スロット(要素)のアドレスそのもの*を監視すれば「そのスロットに何か書き込まれた」ことを検出できます。
例:=&vartab[2]= のスロットにポインタが書かれる瞬間に止める:
(gdb) watch *(struct var **)&vartab[2] Hardware watchpoint 1: *(struct var **)&vartab[2]
あるいは単に(型が分かっていれば)
(gdb) watch vartab[2]
でも OK(gdb が式を解釈してメモリ参照にする)。
awatch を使えば読み書き両方で止まります:
(gdb) awatch vartab[2]
## 注意点(ハードウェアウォッチポイントの制約)
- 多くのプラットフォームで 同時に使えるハードウェアウォッチポイント数は制限(通常 2〜4) されています。配列全要素(39個)に逐一 watch を置くのは現実的でないことが多いです。
- ソフトウェア的にエミュレートされた watchpoint(gdb がメモリスキャンする方式)は遅く、実行が極端に遅くなるか無効化されることがあります。
- 最終的に watchpoint は「実行されたメモリ書き込み」に対して働くので、最適化で変数がレジスタに置かれ、メモリに書かれないパスがあると検出できないことがあります(ただし配列スロット自体はメモリ操作なので大抵は検出できます)。
### A. まずは高レベル関数にブレークポイント
配列に書き込む低レベルの箇所を直接探す代わりに、/変数を登録/代入する関数/にブレークを置く手が最も確実で低コストです。dash の場合、典型的には以下のような関数を狙います(ビルドやバージョンで名前が多少違うかも):
setvar/setvar()— 変数設定lookupvar/lookupvar()— 参照(読み取り)時の探索hashvar/varhash/hashcmd— ハッシュテーブルに登録する処理defun/hashcmd— 関数定義をコマンドテーブルに登録する箇所
例:
(gdb) break setvar (gdb) break defun
これで「誰が最初にスロットに書いているか」を関数レベルで捕まえられます。止まったらローカル変数やスタックを調べ、どの vartab スロットが操作されるか確認できます。
### B. 範囲を絞って watchpoint を置く
もしハードウェア watchpoint が 4 個使えるなら、配列をチャンクに分けて試す方法が有効です。たとえば最初は &vartab[0]=〜=&vartab[9] に 4 個の watch を置き、実行。何も起きなければ次のチャンクに移す、といった二分探索風の手法で「最初に書かれるスロット」を特定できます。
- 配列のアドレス確認
(gdb) p &vartab $1 = (struct var **)&vartab[0] # 例: 0x7b445000 (gdb) p sizeof vartab $2 = 156 # 39 * sizeof(pointer) 等
- ブレークポイント(まずは高レベル)
(gdb) b setvar (gdb) b defun (gdb) run
止まったら:
(gdb) p index # もし関数に index があれば (gdb) p &vartab[index] (gdb) x/5x &vartab[0] # 配列先頭数スロットを見る
- 直接 watch(特定スロット)
(gdb) watch *(struct var **)&vartab[2] (gdb) continue # 書き込みがあればここで停止する
- 複数を一括で置きたい時(gdb の python を使う)
(gdb) python
> for i in range(0, 39):
> try:
> gdb.execute("watch *(struct var **)&vartab[%d]" % i)
> except gdb.error:
> print("watchpoint limit reached at i=", i)
> break
> end
注意:上のスクリプトはハードウェア制限に引っかかると例外を投げます。
- /関数ブレークに頼る/(上で述べた
setvar=/=defunなど)。最も手堅い。 - /printf デバッグ/:初期化関数の前後にログを挿入してビルドして試す(手作業だが確実)。
- /シンボルを辿る/:=disassemble= /
listで該当関数を読んで、どのメモリアドレスに書いているか調べ、そのアドレスへ watchpoint を置く。 - /動的トレースツール/:OpenBSD 環境で使える DTrace 系ツールがあれば(有効なら)メモリ写し込みを検出できるが、環境依存で利用可能か確認が必要。
- 最初に書き込みをキャッチしたいなら、/プログラム開始直後にブレーク/して
vartabの初期状態(ゼロ)を確認→その後continueして先に進めるのが良い(初期化がどの時点で起きるか追える)。 - 最適化されたビルド(=-O2= 等)だとローカルキャッシュやインライン化で見かけの挙動が変わることがあります。必要ならデバッグビルド(=-g -O0=)で実行するのが確実です。
- watchpoint はプロセス全スレッドに対して有効です。スレッド環境では誰が書いたかはコールスタックで確認してください。
exec sample
ものすごく遅いので覚悟しろ。
(gdb) watch vartab[2] Hardware watchpoint 1: vartab[2] (gdb) r Starting program: /home/sakae/dash-0.5.12/src/dash warning: watchpoint 1 downgraded to software watchpoint Watchpoint 1: vartab[2] Old value = (struct var *) 0x0 New value = (struct var *) 0x3874091c <varinit+16> initvar () at var.c:176 176 } while (++vp < end); (gdb) bt #0 initvar () at var.c:176 #1 0x18764946 in init () at init.c:189 #2 0x18756d48 in main (argc=1, argv=0xcf7f4ba4) at main.c:146
small shell
Dash files 今回俎上にのせたのは、Ubuntu 24.04で採用されてる 0.5.12 なんだけど、 弛まぬ更新が続いているな。
README
シジュウカラと言う小鳥が、言語を持つという驚くべき発見をした若き 動物学者の物語。
古代ギリシャ時代から現代まで、言葉を持つのは人間だけだと決めつけられきた。 軽井沢でシジュウカラやゴジュウカラなどの小鳥の鳴き声を観察するうちに、 鳴き方に意味が有るんじゃねぇ、と疑問を持った。
冬とかの餌がとぼしい季節に、バードウォッチャーの人が巻いたひまわりの種を シジュウカラが発見。ジジジジと何度も鳴いた。それを聞きつけて類縁の小鳥が 集合。こういうのに何度も遭遇。皆が集まってくると、鳴く頻度は低下。
これって、餌が有るんで集まっておいでって広告してんじゃね、こういう仮説も 考えられるけど、それじゃ、餌を一人じめできなくて動物の行動としては矛盾 だよね。じっと観察してると、餌を食べながら、天敵のタカと警戒して空を 見上げる頻度が大幅に減った。これって、皆で監視すれば安心と言う表われ ではなかろうか。
空にタカとかを発見すると、ヒヒヒと鳴く。ヘビを発見した時はジャージャー と鳴くなんて事も発見した。言葉を持っているんだろう。どうやってそれを 証明する? ここから試行錯誤で色々と挑戦。とんでもない挑戦である。
中学校の教科書にも掲載された物語になってるんで、楽しく読むが宜しい。