LeavaTailの日記

LeavaTailの日記

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

Linuxカーネルのビルドの一部最適化を無効化する

関連記事

概要

最近のLinuxカーネルでは、デフォルトで最適化オプションが付与されている。

そのため、kgdbやその他のデバッグツールを利用する際に、不都合が生じることがあり、デバッグの間だけが無効化したいことがある。

そこで、本記事では、Makefileを書き換える方法と関数にattributeを付与する方法により最適化を無効化して、kgdbで変数の値を確認する方法をまとめた。

はじめに

前回、VirtualBox仮想マシン同士でkgdbでデバッグできるようにした。

leavatail.hatenablog.com

kgdbを使えば、指定した関数内の変数を参照したり、ステップ実行でコードフローを追っていくことができるのでソースコードを読むときにも役立つ。

しかし、Linuxカーネルはビルド時にデフォルトで最適化オプションを付与しているので、kgdbでソースコードリーディングをするにはいくつかの問題点がある。 そこで、kgdbでも比較的簡単にソースコードリーディングができるようにいくつかの施策をする。

現状の問題点

gdbを使用した人ならわかるが、最適化オプションを付与したオブジェクトファイルをgdbにかけると、以下の問題がある。

  • 一部の変数がと表示され参照できなくなる
  • ステップ実行とソースコードが対応しなくなる

<optimized out>されてしまう変数の実例

そこで、ソースコードリーディングをしている間はカーネルのビルド時に最適化オプションを無効にしたい(-O0)のだが、どうやらLinuxカーネル-O0コンパイルできないらしい。

The kernel will not run with -O0, sorry, just live with the build optimization levels that is currently used and you should be fine.

Yes, it doesn't work :) How to compile Linux kernel with -O0 flag

そのため、別の方法で変数などを確認する必要がある。

ファイル単位で最適化を無効化する

ファイル単位で無効化する場合は、該当するMakefileに下記の内容を追記することでできる。

CFLAGS_(オブジェクトファイル名) = コンパイルオプション

例えば、fs/fat/dir.cの最適化のみ無効にする場合、fs/fat/Makefileを下記のように修正する。

# SPDX-License-Identifier: GPL-2.0
#
# Makefile for the Linux fat filesystem support.
#

obj-$(CONFIG_FAT_FS) += fat.o
obj-$(CONFIG_VFAT_FS) += vfat.o
obj-$(CONFIG_MSDOS_FS) += msdos.o

CFALGS_dir.o = -O0   # 最適化を無効化するために追加した行

fat-y := cache.o dir.o fatent.o file.o inode.o misc.o nfs.o
vfat-y := namei_vfat.o
msdos-y := namei_msdos.o

Makefileを修正後、再ビルドをして対象マシンに再インストールする。

ファイル単位で最適化を無効にした例

関数単位で最適化を無効化する

関数単位で無効化する場合は、該当するソースコードを修正する必要がある。 最適化を無効にしたい関数に対して、下記の修正をすればよい。

型  __attribute__((optimize("コンパイルオプション"))) 関数名(引数)

例えば、ext4_readpages()の最適化のみ無効にする場合、下記のように記述する。

// 3365:
static int __attribute__((optimize("O0")))
ext4_readpages(struct file *file, struct address_space *mapping,
        struct list_head *pages, unsigned nr_pages)

ソースコードを修正後、再ビルドをして対象マシンに再インストールする。

関数単位で最適化を無効にした例

おわりに

kgdbでカーネルソースコードを読むために必要最低限、変数の値やフローを追うための準備をした。 まずファイル単位で最適化オプションを無効化する方法は、再ビルドが必要になるが、容易に解決できるためオススメである。

変更履歴

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

参考

補足

kgdbで特定の変数を参照したいだけであれば、ビルド最適化を必ずしも無効化する必要はない

マシン命令に変換してレジスタを表示する

関数に対応するマシン命令とレジスタにより、特定の変数を確認することはできる。

これは、デバッグ時にも有効な手段で、指定範囲をマシン命令や、アセンブリコードを出力することでフローを追っていくことができる。

注意

ここで紹介する方法はx86_64カーネルのケースである。

その他のアーキテクチャの場合には、レジスタ規則が異なるため注意。

stackoverflow.com

アセンブリコードの出力にはdisassembleコマンド、命令単位で実行するにはsiコマンドを実行していく。 その他のコマンドも含めてgdbの使い方をまとめてあるサイトはたくさん見つかるので一度目を通しておくことを推奨する。

例えば、fat_parse_short()をマシン命令単位の実行フローを見てみる。

アセンブリコードを出力

また、レジスタから引数のunsigned char *nameを参照する。

すべてのレジスタの値を表示

ここに関しては、プロセッサ毎のレジスタ規則を把握している必要がある。*1

レジスタを参照する

これらの結果から、fat_parse_short()ではAというファイルのエントリをパースしていたようだ。 実際に、対象マシンの方の実行結果を見てみても同様のことがわかる。

vagrant@ubuntu-bionic:~$ ls /mnt
A

*1:x86_64の場合、第3引数はrdxレジスタに格納されると思うのですが、詳しい人いたら教えてください

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させることも可能