デバッグ: VirtualBox +CentOS7 で kdb/kgdb の 素振り

Linux カーネルデバッグ方法を各種 抑えておきたいと思って kdb/kgdb を扱う方法を調べていた (正確には kgdboc = kgdb over consol を試した )

kdb/kgdb とは

kgdb, kdb の使い方と、カーネルデバッガーの内部 - kandamotohiro から引用

Kdb は、単純なシェルスタイルのインタフェースであり、キーボードのあるシステムコンソールあるいはシリアルコンソールで使えます。メモリーレジスター、プロセス一覧、dmesg を見ることができます。指定した位置で止まるようにブレークポイントをかけることもできます。ブレークポイントをかけたり、基本的なカーネルの実行制御ができますが、Kdb は、ソースレベルデバッガーではありません


Kgdb は、 Linux カーネルのためのソースレベルデバッガーとして使われるためのものです。Linux カーネルデバッグするために、 gdb とともに使われます。アプリケーションの開発者が、アプリケーションをデバッグするために gdb を使うのと同じように、 gdbカーネルに「割り込んで」メモリーや変数を調べ、コールスタック情報を見ることができるようにしてあります。カーネルコードにブレークポイントをかけたり、いくつかの制限された実行ステップをすることができます。

kdb/kgdb を試す環境を作る

kgdb, kdb の使い方と、カーネルデバッガーの内部 - kandamotohiro から引用

kgdb を使うには、2つのマシンが必要です。1つは開発マシン、もうひとつは、ターゲットマシンです。デバッグされるカーネルは、ターゲットマシンで動きます。開発マシンは、 vmlinux に対して、gdb を動かします

... と言ったものの、楽をしたいので VirtualBox でやる方法はないかとググった

VirtualBoxでLinuxカーネルのデバッグ環境を作る – Masafumi's Blog

ここに書かれているのと同等なことを Vagrantfile (box は CentOS7) に書き起こした。感謝!!!

# vi: set ft=ruby :

Vagrant.configure("2") do |config|
  config.vm.box = "centos/7"

  config.vm.define "primary" do |c|
    c.vm.provider :virtualbox do |vb|
      vb.gui = true
      vb.customize ["modifyvm", :id, "--uart2", "0x2F8", "3"]
      vb.customize ["modifyvm", :id, "--uartmode2", "server", "/tmp/vagrant-ttyS0"]
    end
  end

  config.vm.define "secondary" do |c|
    c.vm.provider :virtualbox do |vb|
      vb.gui = true
      vb.customize ["modifyvm", :id, "--uart2", "0x2F8", "3"]
      vb.customize ["modifyvm", :id, "--uartmode2", "client", "/tmp/vagrant-ttyS0"]
    end
  end

  config.vm.provision "shell", inline: <<-SHELL
    yum update
    yum groupinstall -y 'Development Tools' yum-plugin-changelog
    yum install      -y strace ltrace lsof vim-enhanced kernel kernel-devel
    debuginfo-install --enablerepo=base-debuginfo --nogpgcheck --skip-broken -y kernel
  SHELL
end
📝
CONFIG_HAVE_ARCH_KGDB=y
CONFIG_KGDB=y
CONFIG_KGDB_SERIAL_CONSOLE=y
CONFIG_KGDB_TESTS=y
# CONFIG_KGDB_TESTS_ON_BOOT is not set
CONFIG_KGDB_LOW_LEVEL_TRAP=y
CONFIG_KGDB_KDB=y
  • ttyS0 を使うとデフォルト設定のコンソールと干渉して、意図せぬ読み/書きが発生して gdb/kgdb の動きが不安定になってしまったので ttyS1 としている
  • kernel や kernel-debuginfo 等のパッケージが巨大なため、初回の provisioning に時間を要する。
    • ターゲットで debuginfo を入れる必要は無いかな。同一にしておくと混乱が無いと思うが ...

その他

Vagrantfile でシリアルコンソールの設定方法をするあたりが文献が少なくつまづきポイント。拙著の過去エントリも参考に挙げておく

d.hatena.ne.jp

d.hatena.ne.jp


⚠ 表記について

  • ターゲット ... kgdb で待機してデバッグされる VM
  • ホスト ... ターゲットに gdb でアタッチする VM

として以降の説明を続ける。なお gdb の操作方法や Linux カーネルの用語についての説明は省略する

📝 kgdb ノウハウ

ブート初期段階で kgdb にアタッチしたい場合
  • ターゲットのブートパラメータに kgdbcon=<tty>,[bau]kgdbwait の指定して起動
  • ホストからターゲットに gdb でアタッチ
ブート後、任意のタイミングでアタッチしたい場合
  • ターゲット
    • /sys/module/kgdboc/parameters/kgdbocttyS<int> を write(2) する
    • echo g | /proc/sysrq-trigger で kgdb が待機する
  • ホストからターゲットに gdb でアタッチ

CentOS7 でのデモ

実際に動かしてみると理解がはやい

ターゲットの準備

ブート後にアタッチしてみる。VM を起動した後に下記を実行する

$ echo ttyS1,115200 | sudo tee /sys/module/kgdboc/parameters/kgdboc
$ echo g | sudo tee /proc/sysrq-trigger  # ここでブロックする

f:id:hiboma:20161111195758p:plain

Entering kdb (current=....) の文字列が見えたら kgdb を待機している状態

ホスト
$ sudo gdb /usr/lib/debug/lib/modules/$( uname -r )/vmlinux

シリアルコンソール /dev/ttyS1 経由でターゲットにアタッチする

(gdb) target remote /dev/ttyS1
Remote debugging using /dev/ttyS1
kgdb_breakpoint () at kernel/debug/debug_core.c:1043
1043            wmb(); /* Sync point after breakpoint */

ターゲットが kgdb_breakpoint() で停止しているところにアタッチできた

(gdb) bt
#0  kgdb_breakpoint () at kernel/debug/debug_core.c:1043
#1  0xffffffff8110fb2c in sysrq_handle_dbg (key=<optimized out>) at kernel/debug/debug_core.c:802
#2  0xffffffff813ba332 in __handle_sysrq (key=103, check_mask=<optimized out>) at drivers/tty/sysrq.c:533
#3  0xffffffff813ba80f in write_sysrq_trigger (file=<optimized out>, buf=<optimized out>, count=2, ppos=<optimized out>) at drivers/tty/sysrq.c:1030
#4  0xffffffff8124938d in proc_reg_write (file=<optimized out>, buf=<optimized out>, count=<optimized out>, ppos=<optimized out>) at fs/proc/inode.c:224
#5  0xffffffff811ded6d in vfs_write (file=file@entry=0xffff88001c486c00, buf=buf@entry=0x7ffdc1a6abd0 "g\n", count=count@entry=2, pos=pos@entry=0xffff88001f01bf48)
    at fs/read_write.c:501
#6  0xffffffff811df80f in SYSC_write (count=2, buf=0x7ffdc1a6abd0 "g\n", fd=<optimized out>) at fs/read_write.c:549
#7  SyS_write (fd=<optimized out>, buf=140727852379088, count=2) at fs/read_write.c:541
#8  <signal handler called>
#9  0x00007fc4dab41500 in ?? ()
#10 0x0000000000000000 in ?? ()

ブレークポイントをセットする

通常のプロセスを扱うのと同じようにブレークポイントを仕掛けられる。試しに rename(2) で止めてみよう

ホスト
(gdb) b sys_rename
Breakpoint 2 at 0xffffffff811f0f60: file fs/namei.c, line 4369.
(gdb) c
Continuing.
ターゲット
$ mv hoge hige

# mv は 内部で rename(2) を実行するはず
ターゲット

SyS_rename で停止した

[New Thread 2266]
[Switching to Thread 2266]

Breakpoint 2, SyS_rename (oldname=140732801030148, newname=0) at fs/namei.c:4369
4369    SYSCALL_DEFINE2(rename, const char __user *, oldname, const char __user *, newname)

ソースもリストできる。ソースコードリーディングにも便利そう

(gdb) list
4364                    int, newdfd, const char __user *, newname)
4365    {
4366            return sys_renameat2(olddfd, oldname, newdfd, newname, 0);
4367    }
4368
4369    SYSCALL_DEFINE2(rename, const char __user *, oldname, const char __user *, newname)
4370    {
4371            return sys_renameat2(AT_FDCWD, oldname, AT_FDCWD, newname, 0);
4372    }
4373
(gdb) 

ステップ実行もできるので、システムコールをエントリポイントとして内部実装を追いかけるのにも役立つだろう

One More Kgdb

もうちょっとカーネルの奥っぽいところにブレークポイントを仕掛ける

ホスト

Slab キャッシュを消す shrink_slabブレークポイントを仕掛ける

(gdb) b shrink_slab
Breakpoint 1 at 0xffffffff8117c7a0: file mm/vmscan.c, line 231.
(gdb) c
Continuing.
ターゲット

/proc/sys/vm/drop_caches2 を write する ( 詳細は man 5 proc )

$ echo 2 | sudo tee /proc/sys/vm/drop_caches 
2
ホスト

shrink_slab() で停止した。うーん これは楽しい

[New Thread 2349]
[Switching to Thread 2349]

Breakpoint 1, shrink_slab (shrink=shrink@entry=0xffff880019f17e68, nr_pages_scanned=nr_pages_scanned@entry=1000, lru_pages=lru_pages@entry=1000) at mm/vmscan.c:231
231     {
(gdb) bt
#0  shrink_slab (shrink=shrink@entry=0xffff880019f17e68, nr_pages_scanned=nr_pages_scanned@entry=1000, lru_pages=lru_pages@entry=1000) at mm/vmscan.c:231
#1  0xffffffff8123efc3 in drop_slab () at fs/drop_caches.c:48
#2  drop_caches_sysctl_handler (table=<optimized out>, write=1, buffer=<optimized out>, length=<optimized out>, ppos=<optimized out>) at fs/drop_caches.c:68
#3  0xffffffff81255223 in proc_sys_call_handler (filp=<optimized out>, buf=0x7ffc43f87c80, count=2, ppos=0xffff880019f17f48, write=write@entry=1) at fs/proc/proc_sysctl.c:506
#4  0xffffffff81255254 in proc_sys_write (filp=<optimized out>, buf=<optimized out>, count=<optimized out>, ppos=<optimized out>) at fs/proc/proc_sysctl.c:524
#5  0xffffffff811ded6d in vfs_write (file=file@entry=0xffff88001f5b8f00, buf=buf@entry=0x7ffc43f87c80 "2\n", count=count@entry=2, pos=pos@entry=0xffff880019f17f48)
    at fs/read_write.c:501
#6  0xffffffff811df80f in SYSC_write (count=2, buf=0x7ffc43f87c80 "2\n", fd=<optimized out>) at fs/read_write.c:549
#7  SyS_write (fd=<optimized out>, buf=140721448844416, count=2) at fs/read_write.c:541
#8  <signal handler called>
#9  0x00007f4cc1ada500 in ?? ()
#10 0x0000000000000001 in irq_stack_union ()
#11 0x0000000000000001 in irq_stack_union ()
#12 0x0000000000000000 in ?? ()

ぶっこわす

変数もいじれる。まずは適当な関数にブレークポイントを仕掛けてみる

ホスト
(gdb) b shrink_dentry_list
Breakpoint 1 at 0xffffffff811f73e0: file fs/dcache.c, line 794.
(gdb) c
Continuing.
[New Thread 4]
[Switching to Thread 4]

本来はポインタを保持するはずの list 変数に 0 をいれると ...

Breakpoint 1, shrink_dentry_list (list=list@entry=0xffff88001d7dfd20) at fs/dcache.c:794
794     {
(gdb) p list
$1 = (struct list_head *) 0xffff88001d7dfd20

(gdb) p list=0 🍣
$2 = (struct list_head *) 0x0 <irq_stack_union>

(gdb) c
Continuing.

おっと SIGSEGV だ🔥

Program received signal SIGSEGV, Segmentation fault.
shrink_dentry_list (list=0x0 <irq_stack_union>, list@entry=0xffff88001d7dfd20) at fs/dcache.c:799
799                     dentry = list_entry_rcu(list->prev, struct dentry, d_lru);
(gdb) c
Continuing.
KGDB only knows signal 9 (pass) and 15 (pass and disconnect)
Executing a continue without signal passing

Program received signal SIGSEGV, Segmentation fault.
0x000000000000000b in irq_stack_union ()

ターゲットが ヌルポ でしんだ 💀

f:id:hiboma:20161111195411p:plain

ターゲットは Oops しているのに、 ホストの gdb は SIGSEGV としてキャッチするんだなぁ