Linux kernelのデバッグ

7 min read読了の目安(約6800字

きっかけ

Linux kernelをいじる機会がでてきたのでLinux kernelを読まなければならなくなりました。あの膨大なソースコードをどう読むべきかということを調べたところ、gdbが使えるらしいのでそれをお供にしてみました。

環境

ホストマシン:Ubuntu 20.04
デバッグするLinux kernel: 5.8.3
gcc:9.3.0
gdb:GNU gdb (Ubuntu 9.2-0ubuntu1~20.04) 9.2

linux kernelのビルド

Linux kernelのダウンロード

まずはlinux kernelをgitを使ってダウンロードします。

git clone https://github.com/torvalds/linux.git

次にバージョンを合わせます。

cd linux
git checkout v5.8-rc3
git switch -c v5.8.3

configの変更

デフォルトのconfigをセットします。

make defconfig

configを変更します。

make menuconfig

以下の4か所を変更します。

  • CONFIG_DEBUG_KERNELを有効にする。
Symbol: DEBUG_KERNEL [=y]
Type  : boolean
Prompt: Kernel debugging
  Location:
    -> Kernel hacking
  Defined at lib/Kconfig.debug:315
  Selected by: EXPERT [=n] 
  • CONFIG_DEBUG_INFOを有効にする。この設定でデバッグシンボルが生成される。
Symbol: DEBUG_INFO [=y]
Type  : boolean
Prompt: Compile the kernel with debug info
  Location:
    -> Kernel hacking
      -> Compile-time checks and compiler options
  Defined at lib/Kconfig.debug:120
  Depends on: DEBUG_KERNEL [=y] 
  • これを無効にしないとCONFIG_RELOCATABLEを無効にできなかった。[1]
Symbol: EFI [=n]
Type  : bool
Defined at arch/x86/Kconfig:1929
  Prompt: EFI runtime service support
  Depends on: ACPI [=y]
  Location:
    -> Processor type and features
  Selects: UCS2_STRING [=y] && EFI_RUNTIME_WRAPPERS [=y]
  • CONFIG_RELOCATABLEを無効にする。CONFIG_RELOCATABLEを無効にしない場合、 カーネルがメモリ上で再配置される。その際、デバッグシンボルに記載される アドレスと対応しなくなり、GDBがシンボルを解釈できなくなる。
Symbol: RELOCATABLE [=n]
Type  : boolean
Prompt: Build a relocatable kernel
  Location:
    -> Processor type and features
  Defined at arch/x86/Kconfig:1711

ビルド

以下のコマンドを打ってビルドします

make -j$(nproc)

x86かx86_64でビルドしていれば、arch/x86/boot/bzImageが作成されます(x86_64では、arch/x86_64/boot/bzImageが作成されますが、これはarch/x86/boot/bzImageを指すシンボリックなのでどちらでも大丈夫です)

QEMU実行

git cloneしたlinuxフォルダ内で以下を実行

mkdir test && cd test
cat <<EOF > init.c
#include <stdio.h>

int main() {
        printf("Hello World\n");
}
EOF

これでlinux/test以下にinit.cが作られます。これをlinux kernelが起動できるようにします。

gcc -static -o init -g -O0 init.c
echo init | cpio -o -H newc | gzip >init.gz

このinit.gzが一種の起動ディスクになります[2]
それではこれを起動してみましょう。

qemu-system-x86_64 -kernel ../arch/x86/boot/bzImage -initrd init.gz -append "console=ttyS0" -nographic
# qemu-system-x86_64がない場合はsudo apt install qemu等でインストールしてください

うまくいくと以下のようなログになると思います。

[    0.000000] Linux version 5.8.0-rc3 (dorakue@dorakue-virtual-machine) (gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0, GNU ld (GNU Binut0
[    0.000000] Command line: console=ttyS0
[    0.000000] x86/fpu: x87 FPU will use FXSAVE
[    0.000000] BIOS-provided physical RAM map:
[    0.000000] BIOS-e820: [mem 0x0000000000000000-0x000000000009fbff] usable
[    0.000000] BIOS-e820: [mem 0x000000000009fc00-0x000000000009ffff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000000f0000-0x00000000000fffff] reserved
[    0.000000] BIOS-e820: [mem 0x0000000000100000-0x0000000007fdffff] usable
[    0.000000] BIOS-e820: [mem 0x0000000007fe0000-0x0000000007ffffff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000fffc0000-0x00000000ffffffff] reserved
[    0.000000] NX (Execute Disable) protection: active
[    0.000000] SMBIOS 2.8 present.
・
・
・
・
・
[    2.043162] ALSA device list:
[    2.043330]   No soundcards found.
[    2.084180] Freeing unused kernel image (initmem) memory: 1136K
[    2.087724] Write protecting the kernel read-only data: 20480k
[    2.090620] Freeing unused kernel image (text/rodata gap) memory: 2044K
[    2.091620] Freeing unused kernel image (rodata/data gap) memory: 752K
[    2.092122] Run /init as init process
Hello World
[    2.130220] Kernel panic - not syncing: Attempted to kill init! exitcode=0x00000000
[    2.131036] CPU: 0 PID: 1 Comm: init Not tainted 5.8.0-rc3 #1
[    2.131296] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.13.0-1ubuntu1 04/01/2014
[    2.131642] Call Trace:
[    2.132503]  dump_stack+0x57/0x70
[    2.132664]  panic+0xf6/0x2b7
[    2.132760]  do_exit.cold+0xce/0xdb
[    2.132857]  ? vfs_write+0x172/0x1a0
[    2.132956]  do_group_exit+0x35/0x90
[    2.133157]  __x64_sys_exit_group+0xf/0x10
[    2.133293]  do_syscall_64+0x3e/0x70
[    2.133409]  entry_SYSCALL_64_after_hwframe+0x44/0xa9
[    2.133690] RIP: 0033:0x448016
[    2.133840] Code: Bad RIP value.
[    2.133938] RSP: 002b:00007ffdaa618938 EFLAGS: 00000246 ORIG_RAX: 00000000000000e7
[    2.134196] RAX: ffffffffffffffda RBX: 00000000004c2230 RCX: 0000000000448016
[    2.134339] RDX: 0000000000000000 RSI: 000000000000003c RDI: 0000000000000000
[    2.134490] RBP: 0000000000000000 R08: 00000000000000e7 R09: ffffffffffffffc0
[    2.134761] R10: 00000000004c0800 R11: 0000000000000246 R12: 00000000004c2230
[    2.134918] R13: 0000000000000001 R14: 0000000000000000 R15: 0000000000000001
[    2.135712] Kernel Offset: disabled
[    2.136068] ---[ end Kernel panic - not syncing: Attempted to kill init! exitcode=0x00000000 ]---

qemu with gdb

最後にgdbを使ってデバッグしていきます。
linux/testフォルダで以下を実行します。

qemu-system-x86_64 -kernel ../arch/x86/boot/bzImage -initrd init.gz -append "console=ttyS0" -nographic -gdb tcp::12345 -S

ターミナルを別に開き、linuxフォルダ内でgdbを実行します。

gdb

gdbが起動したら以下のように表示されます。

GNU gdb (Ubuntu 9.2-0ubuntu1~20.04) 9.2
Copyright (C) 2020 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".
(gdb)

そうしたら、次は以下を入力します

target remote localhost:12345
symbol-file vmlinux
b __do_sys_open #ここは任意のlinux kernel内の関数を指定
c

以下のように表示されたら成功です。あとは好きなようにgdbデバッグをしましょう!

Breakpoint 2, __do_sys_open (mode=<optimized out>, flags=<optimized out>,
    filename=<optimized out>) at fs/open.c:1192
1192            return do_sys_open(AT_FDCWD, filename, flags, mode);
(gdb)

おまけ

linux kernelは大抵の場合-O2オプションにより、最適化されてビルドされます。従って、上のようにデバッグ時にoptimized outと表示され値を見ることができない変数が発生します。nextコマンドを押していくうちにoptimized outされる場合もあります。また、-O0オプションでビルドするとエラーを吐くらしく、最適化なしでビルドできません。
うまくデバッグできる方がいれば教えていただけると幸いです。

参考サイト

QEMU上のLinuxカーネルをGDBでデバッグする

脚注
  1. EFIはPCの起動プロセスを制御するプログラムでUEFIに使われています。昔はBIOSが使われていましたが、近年ではEFIによる制御が行われています。 ↩︎

  2. 詳しく言うと、initramfsという圧縮ファイル形式になっていてqemuでinitrdオプションをつけることでファイルを読み込んでくれます。 ↩︎