LeavaTailの日記

LeavaTailの日記

Linuxエンジニアを目指した技術者の備忘録

VirtualBox上の仮想マシン同士でkgdbを使ってカーネルをデバッグする

関連記事

概要

linuxカーネル (v5.3.9)をデバッグするために、kgdbを実行するための手順を示す。

ホストOSに依存せずにカーネルデバッグするために、仮想マシン (VirtualBox) 上に環境を構築する。

はじめに

VitualBox上の仮想マシン同士をシリアルコンソールの設定し、kgdbでLinuxカーネルデバッグをする。

kgdbは、Linuxカーネルの機能の一つであり、gdbと同様のインターフェースでカーネルデバッグすることができる。

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

kgdb, kdb の使い方と、カーネルデバッガーの内部 - kandamotohiro (google.com)

先駆者の方がすでに手順をまとめてあるので、自分の環境でも動作できるように残していく。

hiboma.hatenadiary.jp

目標

利用イメージは下記の通りとなっている。ゲスト1からゲスト2のLinuxカーネルデバッグできるようにする。

f:id:LeavaTail:20191106000253p:plain

カーネルデバッグの利用イメージ

VirtualBox上の仮想マシン同士でカーネルデバッグすることで、以下のような利点がある。

  • 利用しているマシンの環境(OSやハードウェアなど)の差を意識することなくデバッグすることができる
  • デバッガ環境の構築が容易になる
  • ホストOSの環境を汚す必要がない。*1

ここでは、デバッグ対象のカーネル(ターゲットカーネル)にアクセスする仮想マシン(ゲスト1)をdebuggerと、デバッグ対象の仮想マシン(ゲスト2)をdebuggeeと呼ぶことにする。*2

また、ホストOSはLinuxを使用しているが、WindowsMacでも利用することができる。*3

環境の構築

VirtualBoxのインストール

公式サイトに手順が記載されている。

  1. /etc/apt/source.listに下記を追記する。
    deb [arch=amd64] https://download.virtualbox.org/virtualbox/debian bionic contrib
  2. 下記のコマンドを実行し、VirtualBox用の公開鍵を追加する。
    $ wget -q https://www.virtualbox.org/download/oracle_vbox_2016.asc -O- | sudo apt-key add -
  3. 以下のコマンドを実行し、パッケージ一覧を更新する。
    $ sudo apt update
  4. 以下のコマンドを実行し、VirtualBoxをインストールする。
    $ sudo apt install virtualbox-6.0

Vagrantのインストール(任意)

Vagrantのインストールは必須ではないが、設定が容易になるので入れることをオススメする。

apt経由でインストールしてもよいが、バージョンが古いので公式サイトからダウンロードする。

  1. 自分の環境にあった (今回はdebian 64bit) をダウンロードする
    $ wget https://releases.hashicorp.com/vagrant/2.2.6/vagrant_2.2.6_x86_64.deb
  2. ダウンロードしたパッケージファイルからVagrantをインストールする
    $ sudo dpkg -i vagrant_2.2.6_x86_64.deb

こちらも必須ではないが、カーネルのアップデートでGuest Additionsの不整合で共通ドフォルダが設定できなくなるので、Vagrantプラグインvagrant-vbguestのインストールもオススメする。

vagrant-vbguest is a Vagrant plugin which automatically installs the host's VirtualBox Guest Additions on the guest system.

GitHub - dotless-de/vagrant-vbguest: A Vagrant plugin to keep your VirtualBox Guest Additions up to date

プラグインのインストールには下記のコマンドを実行すればよい。

$ vagrant plugin install vagrant-vbguest

仮想マシンの作成

ubuntuイメージをインストールする

  1. イメージファイルを作成する作業用ディレクトリ(場所は任意)を作成する。
    $ mkdir -p ~/Vagrant/ubuntu18
  2. 作業用ディレクトリに移動する。
    $ cd Vagrant/ubuntu18
  3. Vagrant環境
    $ vagrant init ubuntu/bionic64

シリアルコンソールの設定

    1. Vagrantの設定ファイルを下記の修正する。
      # -*- mode: ruby -*-
      # vi: set ft=ruby :
      
      Vagrant.configure("2") do |config|
        config.vm.box = "ubuntu/bionic64"
      
        config.vm.define "debugger" do |c|
          c.vm.provider "virtualbox" do |vb|
            vb.customize ["modifyvm", :id, "--uart2", "0x2F8", "3"]
            vb.customize ["modifyvm", :id, "--uartmode2", "server", "/tmp/vagrant-ttyS1"]
          end
        end
      
        config.vm.define "debuggee" do |c|
          c.vm.provider "virtualbox" do |vb|
            vb.customize ["modifyvm", :id, "--uart2", "0x2F8", "3"]
            vb.customize ["modifyvm", :id, "--uartmode2", "client", "/tmp/vagrant-ttyS1"]
          end
        end
      end
      
    2. ゲスト2(debuggee)の仮想端末の設定を追加する。
      debuggee:~$ sudo systemctl enable serial-getty@ttyS1.service
      
    3. 仮想マシンを再起動する。
      $ vagrant reload 
      
    4. ゲスト1(debugger)からシリアル通信ができるか確認する。
      debugger:~$ sudo screen /dev/ttyS1
      <ENTRYを押下>
      Ubuntu 18.04.3 LTS ubuntu-bionic ttyS1
      
      ubuntu-bionic login:
      

kgdbの設定

使用したBoxイメージでは、カーネルがkgdbに対応していなかったのでカーネルを再ビルドする。カーネルコンフィグレーションは下記の項目を設定する。*4

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
# CONFIG_RANDOMIZE_BASE is not set

特に、KASLRは最近のディストリビューションでは有効になっているのでカーネルコンフィグを流用する場合、offにするのを忘れないように。

kernhack.hatenablog.com

せっかくなので、ゲスト2(debuggee)用のカーネルをゲスト1(debugger)でビルドする。

  1. ゲスト1(debugger)にカーネルの開発環境を構築する。
    debugger:~$ sudo apt-get install git build-essential kernel-package fakeroot libncurses5-dev libssl-dev ccache bison flex gdb
  2. ホームディレクトリにカーネルソースコードをダウンロードする。(今回はversion 5.3.9を対象にする)
    debugger:~$ cd $HOME; wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.3.9.tar.xz
  3. ダウンロードしたtarballを解凍する。
    debugger:~$ tar Jxfv ./linux-5.3.9.tar.xz
  4. 既存のカーネルのコンフィグを流用する。
    debugger:~$ cd linux-5.3.9
    debugger:~$ cp /boot/config-`uname -r` .config
  5. 最新のカーネルにコンフィグを設定する。(追加コンフィグはデフォルトで設定)
    debugger:~$ yes '' | make oldconfig
  6. kgdb用にコンフィグファイルを修正する。
    debugger:~$ make menuconfig
    以下の設定を確認する。
    Kernel hacking -> [*]KGDB: kernel debugger
    Kernel hacking -> KGDB: kernel debugger -> [*]KGDB: use kgdb over the serial console
    Kernel hacking -> KGDB: kernel debugger -> [*]KGDB: internal test suite
    Kernel hacking -> KGDB: kernel debugger -> [*]KGDB: Allow debugging with traps in notifiers
    Kernel hacking -> KGDB: kernel debugger ->  [*]KGDB_KDB: include kdb frontend for kgdb
    Processor type and features -> [ ]Randomize the address of the kernel image (KASLR)
  7. ソースコードをクリーンな状態にする。
    debugger:~$ make clean
  8. カーネルソースコードをビルドする。(時間がかかるので、仮想マシンのCPU数やメモリ量など増やす方が良い)
    debugger:~$ make -j `getconf _NPROCESSORS_ONLN` deb-pkg
  9. カーネル用のgdbスクリプトが生成されているので、設定ファイルに追記する。(任意)
    debuggee:~$ cat < ~/.gdbinit
    add-auto-load-safe-path /home/vagrant/linux-5.3.9/scripts/gdb/vmlinux-gdb.py
    set auto-load safe-path /
    EOF
  10. ビルドして生成されたパッケージをゲスト2(debuggee)に共有する。(VirtualBoxの共有フォルダ設定を利用する)
    debugger:~$ mv ../linux-headers-5.3.9_5.3.9-1_amd64.deb /vagrant
    debugger:~$ mv ../linux-libc-dev_5.3.9-1_amd64.deb /vagrant
    debugger:~$ mv ../linux-image-5.3.9_5.3.9-1_amd64.deb /vagrant
    debugger:~$ mv ../linux-image-5.3.9-dbg_5.3.9-1_amd64.deb /vagrant
  11. ゲスト2(debuggee)でカーネルパッケージをインストールする。
    debuggee:~$ sudo dpkg -i /vagrant/linux-headers-5.3.9_5.3.9-1_amd64.deb
    debuggee:~$ sudo dpkg -i /vagrant/linux-libc-dev_5.3.9-1_amd64.deb
    debuggee:~$ sudo dpkg -i /vagrant/linux-image-5.3.9_5.3.9-1_amd64.deb
    debuggee:~$ sudo dpkg -i /vagrant/linux-image-5.3.9-dbg_5.3.9-1_amd64.deb

デバッグの実行

  1. ゲスト2(debuggee)をkgdbの準備をする。ここでゲスト2(debuggee)はwait状態になる。*5
    debuggee:~$ sudo echo ttyS1,115200 | sudo tee /sys/module/kgdboc/parameters/kgdboc
    debuggee:~$ sudo echo g | sudo tee /proc/sysrq-trigger
    
  2. ゲスト1(dedebber)からゲスト2(debuggee)にアタッチする。
    debugger:~$ sudo gdb ~/linux-5.3.9/vmlinux
    GNU gdb (Ubuntu 8.1-0ubuntu3.1) 8.1.0.20180409-git
    Copyright (C) 2018 Free Software Foundation, Inc.
    License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
    This is free software: you are free to change and redistribute it.
    There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
    and "show warranty" for details.
    This GDB was configured as "x86_64-linux-gnu".
    Type "show configuration" for configuration details.
    For bug reporting instructions, please see:
    <http://www.gnu.org/software/gdb/bugs/>.
    Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.
    For help, type "help".
    Type "apropos word" to search for commands related to "word"...
    Reading symbols from vmlinux...done.
    
    (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 */
    

バックトレースの出力

(gdb) bt
#0  kgdb_breakpoint () at kernel/debug/debug_core.c:1136
#1  0xffffffff81167bec in sysrq_handle_dbg (key=<optimized out>) at kernel/debug/debug_core.c:889
#2  0xffffffff8164c0b3 in __handle_sysrq (key=103, check_mask=false) at drivers/tty/sysrq.c:556
#3  0xffffffff8164c58f in write_sysrq_trigger (file=<optimized out>, buf=<optimized out>, count=0, 
    ppos=<optimized out=>) at drivers/tty/sysrq.c:1105
#4  0xffffffff81346dbe in proc_reg_write (file=<optimized out>, buf=<optimized out>, 
    count=<optimized out>, ppos=<optimized out>) at fs/proc/inode.c:238
#5  0xffffffff812b6deb in __vfs_write (file=<optimized out>, p=<ptimized out>, count=<ptimized out>, 
    pos=<optimized out>) at fs/read_write.c:494
#6  0xffffffff812b9d81 in vfs_write (file=0xffff88803b881000, buf=0x7ffe351d2710 "g\n", 
    count=<optimized out>, pos=0xffffc9000053bee8) at fs/read_write.c:558
#7  0xffffffff812ba087 in ksys_write (fd=<optimized out>, buf=0x7ffe351d2710 "g\n", count=2)
    at fs/read_write.c:611
#8  0xffffffff812ba0da in __do_sys_write (count=<optimized out>, buf=<optimized out>, 
    fd=<optimized out>) at fs/read_write.c:623
#9  __se_sys_write (count=<optimized out>, buf=<optimized out>, fd=<optimized out>)
    at fs/read_write.c:620
#10 __x64_sys_write (regs=<optimized out>) at fs/read_write.c:620
#11 0xffffffff8100434a in do_syscall_64 (nr=<optimized out>, regs=0x92) at arch/x86/entry/common.c:296
#12 0xffffffff81c0008c in entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:175
#13 0x0000000000000002 in fixed_percpu_data ()
#14 0x0000000000000002 in fixed_percpu_data ()
#15 0x00007ffe351d2710 in ?? ()
#16 0x0000000000000002 in fixed_percpu_data ()
#17 0x00000000000001b6 in ?? ()
#18 0x00007fafef72f540 in ?? ()
#19 0x0000000000000002 in fixed_percpu_data ()
#20 0x00007fafef22f154 in ?? ()
#21 0x0000000000000002 in fixed_percpu_data ()
#22 0x0000000000000003 in fixed_percpu_data ()
#23 0x00007fafef22f154 in ?? ()
#24 0x0000000000000033 in ?? ()
#25 0x0000000000000246 in ?? ()
#26 0x00007ffe351d2628 in ?? ()
#27 0x000000000000002b in fixed_percpu_data ()
Backtrace stopped: Cannot access memory at address 0xffffc9000053c000

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

  1. ゲスト1(debugger)で続けて操作する。
    (gdb) b ext4_readdir
    Breakpoint 1 at 0xffffffff813642f0: file fs/ext4/dir.c, line 107.
    
    (gdb) c
    Continuing.
    [Switching to Thread 919]
    
  2. ゲスト2(debuggee)のwait状態が解除されたので、lsコマンドを実行してext4_readdirを呼ぶ。
    debuggee:~$ ls -l
    
  3. ゲスト2(debuggee)がwait状態になり、ゲスト1(debugger)のwaitが解除されたので、バックトレースを出力させる。
    (gdb) bt
    #0  ext4_readdir (file=0xffff88803d243300, ctx=0xffffc90000563ec0) at fs/ext4/dir.c:107
    #1  0xffffffff812cfaea in iterate_dir (file=0xffff88803d243300, ctx=0xffffc90000563ec0)
        at fs/readdir.c:67
    #2  0xffffffff812d095b in __do_sys_getdents (count=<optimized out>, dirent=<optimized out>, 
        fd=<optimized out>) at fs/readdir.c:285
    #3  __se_sys_getdents (count=<optimized out>, dirent=<optimized out>, fd=<optimized out>)
        at fs/readdir.c:266
    #4  __x64_sys_getdents (regs=<optimized out>) at fs/readdir.c:266
    #5  0xffffffff8100434a in do_syscall_64 (nr=<optimized out>, regs=0xffffc90000563ec0)
        at arch/x86/entry/common.c:296
    #6  0xffffffff81c0008c in entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:175
    #7  0x000056410e34cc4a in ?? ()
    #8  0x000056410e98d5c0 in ?? ()
    #9  0x0000000000000000 in ?? ()
    

値の代入

(gdb) p file=0
$1 = (struct file *) 0x0 <fixed_percpu_data>

(gdb) c
Continuing.

Thread 121 received signal SIGSEGV, Segmentation fault.
[Switching to Thread 1855]
ext4_readdir (file=0x0 <fixed_percpu_data>, ctx=0xffffc90000563ec0) at fs/ext4/dir.c:112
112             struct inode *inode = file_inode(file);
(gdb) 

ファイルポインタに0を代入したので、Segmentation fault.になってゲスト2(debuggee)が落ちた。

おわりに

ゲスト1からゲスト2にアタッチすることができるようになったので、ブレークポイントをセットしたり、バックトレースを出力させるカーネルデバッグが可能となった。

しかし、最近のカーネルはビルド時に自動で最適化オプションを付与してしまうため、変数が省略されていたり、ステップ実行でコードが飛び飛びに実行されたりと呼んでいくうえでの問題はいくつか残っている。そういった問題点については、また別の記事にまとめていく予定。

変更履歴

  • 2019/11/10: 記事公開
  • 2022/06/05: デザイン修正

参考

*1:VirtualBoxなど別途ソフトウェアをインストールする必要はあるが...。

*2:debuggerとdebuggeeは同じOSを利用する必要はないが、運用が煩雑になるので統一したほうが良いと思われる。

*3:Windowsの場合は、Unix Domain Socketという概念がないのでフリーソフトcom0com」など利用する必要がある。

*4:カーネルコマンドラインオプションで無効にすることもできるが

*5:カーネルコマンドラインオプションでブート時にwaitさせることも可能