NetBSDの標準コマンドの処理をgdbでざっくりと把握してみる

たまに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()`で処理が完結していることが分かります。

```c
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で記事にできればと思います)