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
📝
- CentOS7 x86_64 の .config は KGDB の設定を有効にしている
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 でシリアルコンソールの設定方法をするあたりが文献が少なくつまづきポイント。拙著の過去エントリも参考に挙げておく
⚠ 表記について
として以降の説明を続ける。なお gdb の操作方法や Linux カーネルの用語についての説明は省略する
📝 kgdb ノウハウ
ブート初期段階で kgdb にアタッチしたい場合
- ターゲットのブートパラメータに
kgdbcon=<tty>,[bau]
とkgdbwait
の指定して起動 - ホストからターゲットに gdb でアタッチ
ブート後、任意のタイミングでアタッチしたい場合
- ターゲット
/sys/module/kgdboc/parameters/kgdboc
にttyS<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 # ここでブロックする
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_caches
に 2
を 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 ()
ターゲットが ヌルポ でしんだ 💀
ターゲットは Oops しているのに、 ホストの gdb は SIGSEGV としてキャッチするんだなぁ