たまにOSの標準コマンドの内部動作が気になることがあります。基本的にはソースコードを読んでゆく形になるのですが、実際にプログラムを動かしながら確認すると理解が捗るケースがよくあります。
NetBSDをインストールすると、シンボル情報が残った状態の標準コマンドがインストールされるという利点(?)があります。
そこでNetBSD Advent Calendar 2016 8日目の今日は、NetBSDの標準コマンドをデバッガで追いかけてみる手順を紹介してみようと思います。
標準コマンドをgdbから触ってみる
環境はNetBSD-7.0_RC1-evbarm
です(Raspberry-PiでPCルータとしてずっと動かしているのでバージョンが古いです...)。
$ uname -a
NetBSD cirlarko 7.0_RC1 NetBSD 7.0_RC1 (CIRLARKO) #3: Fri Jun 26 10:52:08 JST 2015 root@cirlarko2:/usr/src/sys/arch/evbarm/compile/CIRLARKO evbarm
このevbarmな環境でfile /bin/date
してみます。"not stripped"と表示されているので、何らかのシンボルは残ったままのようです。
$ file `which date`
/bin/date: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /libexec/ld.elf_so, for NetBSD 7.99.9, compiled for: earmv6hf, not stripped
nm
コマンドでシンボル情報を確認してみます。関数名の情報が残っていますね。
$ nm `which date` | grep ' [Tt] '
00010da0 T ___start
00011478 t __do_global_ctors_aux
00022570 t __init_array_end
0002256c t __init_array_start
0002256c t __preinit_array_end
0002256c t __preinit_array_start
00010d24 T __start
00010d24 T _start
00011048 t badcanotime
000114cc T main
0001109c T netsettime
00010ffc t usage
さっそくこの関数名を使って、gdbでブレークポイントを設定するようにしてみます。
$ nm `which date` | grep ' [Tt] ' | grep -v _ | awk '{ print "break " $3 }' | tee break.gdb
break badcanotime
break main
break netsettime
break usage
デバッガから実行してみます。
$ gdb -x break.gdb /bin/date
GNU gdb (GDB) 7.7.1
...
Breakpoint 1 at 0x1104c
Breakpoint 2 at 0x114dc
Breakpoint 3 at 0x110ac
Breakpoint 4 at 0x11008
(gdb) info b
Num Type Disp Enb Address What
1 breakpoint keep y 0x0001104c <badcanotime+4>
2 breakpoint keep y 0x000114dc <main+16>
3 breakpoint keep y 0x000110ac <netsettime+16>
4 breakpoint keep y 0x00011008 <usage+12>
実行してみると、首尾よくmain()
でブレークします。が、continue
するとそのまま正常終了しています。
(gdb) run
Starting program: /bin/date
Breakpoint 2, 0x000114dc in main ()
(gdb) c
Continuing.
Thu Dec 8 23:04:59 JST 2016
[Inferior 1 (process 4867) exited normally]
(gdb)
main()
で処理が完結している...?と思いつつ、いよいよソースコードを見てみます。
おもむろにソースコードを取得して展開します。
$ wget http://ftp.jaist.ac.jp/pub/NetBSD/NetBSD-7.0/source/sets/src.tgz
$ tar zxvf src.tgz
...
$ cd ./usr/src
$
$ which date
/bin/date
$ cd ./bin/date
date.c:main()
ではlocaltime()
→strftime()
呼び出ししているだけなので、単にdate
コマンドを実行するとmain()
で処理が完結していることが分かります。
date.c:
75 int
76 main(int argc, char *argv[])
77 {
...
88 while ((ch = getopt(argc, argv, "ad:jnr:u")) != -1) {
...
124 }
147 if ((buf = malloc(bufsiz = 1024)) == NULL)
148 goto bad;
149
150 if ((tm = localtime(&tval)) == NULL)
151 err(EXIT_FAILURE, "localtime %lld failed", (long long)tval);
152
153 while (strftime(buf, bufsiz, format, tm) == 0)
154 if ((buf = realloc(buf, bufsiz <<= 1)) == NULL)
155 goto bad;
156
157 (void)printf("%s\n", buf + 1);
158 free(buf);
159 return 0;
まとめ
かなり駆け足気味ですが、NetBSDの標準コマンドには関数名のシンボル情報が残っているので、それをもとに動作を追いかける手順を紹介してみました。
ブレークポイントを設定できる状態だと、そこから関数のコールグラフとかも作成できるので応用が聞きそうです。
(関数のコールグラフを生成するネタをQiitaに書いていなかったので、今年のAdvent Calendarで記事にできればと思います)