LeavaTailの日記

LeavaTailの日記

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

Travis CIでdotfilesのテストを自動化する

概要

自作のdotfilesが一定の品質であることを保証するために、4つのテスト項目を定義し、テストの再設計・再実装した。

また、上記のテストとTravis CIを連携させることにより、テストの自動化を図った。

はじめに

皆様は設定ファイルをどのように管理しているだろうか。 外部記憶装置に保存する。ブログにアップする。いろいろな管理方法があるだろう。

私はdotfilesの一つのリポジトリで管理している。実際に私が使用しているdotfilesは下記の通りとなっている。

github.com

dotfilesで管理することで、新しい開発環境でもmake installのコマンド一つで自分の環境に設定ファイルを展開できる。

dotfilesについて興味のある方は有識者のサイトを参考にしてほしい。

一方で、dotfilesは常に使用しているものではなく、新しい開発環境を用意するとき (半年に一回程度くらい) などにしか使用されない。 そういったタイミングで用意していたdotfilesが動作しなかったら目も当てられない。

そこで、dotfilesが意図した通りに動作することをテストするためにCIを導入することにした。 今回は、CIツールとしてTravis CIを利用する。

目標とするシステム構成

dotfilesの構成

CIの導入に入る前に、私のdotfilesの現状構成について紹介する。

dotfilesの全体像

私はdotfilesで以下の6つの設定ファイルを管理している。

利用者がmake installコマンドを実行することで、これらの設定ファイルがローカルの開発環境に展開されるようになっている。 またmake install は、先駆者様に倣ってmake deploymake initを実行するコマンドとなっている。

make deployは、dotfiles内の設定ファイルをローカル開発環境に展開する(リンクを張る)作業をする。 make initは、開発環境の初期化処理をする。 自分のdotfilesでは初期化処理で、tmuxとvimプラグインをダウンロードする。

テスト環境

本来であれば複数の環境でテストすべきであるが、「dotfilesはあくまで自分用のリポジトリであること」「自分がUbuntuの最新LTSしか使用していないこと」から上記の一つのみ実施する。

テスト項目

これらの環境を考慮したうえで、dotfilesで満たすべき項目を考える。

  1. 設定ファイルのデプロイに成功している
  2. 設定ファイルが正しい場所に展開されている
  3. vimプラグインのインストールが成功している
  4. tmuxプラグインのインストールが成功している

設定ファイルのデプロイに成功している

単純かつ明解であるが、重要なテスト項目の一つである。 確認方法は、make installコマンドを実行したときのリターンコードが0であるかとした。

設定ファイルが正しい場所に展開されている

「正しい場所」とは、開発者の意図した場所のことを指す。

このテスト項目は、dotfilesの性質とし優先して満たすべきと考えられる。 確認方法は、展開した設定ファイルが存在しているか設定ファイルのリンク先が正しいか、この2つの手順で実施する。

設定ファイルの存在は、シェルスクリプトの「-e」演算子で簡単に確認することができる。 「-h」演算子シンボリックリンクであるか)の場合のis_link関数については後述。

# $1:  source file
# $2:  destination file
function is_exist() {
    DOTFILE=`basename $2`
    if [[ ! -e $2 ]]; then
        echo "  × \"$2\" is failed"
        echo "     Not found \"${DOTFILE}\"...NG"
        exit 1
    elif [[ -h $2 ]]; then
        is_link $1 $2
    elif [[ -e $2 ]]; then
        echo "\"$2\" is failed...OK"
        echo "     deploying \"${DOTFILE}\"\. but, file is unsupported...UNKHOWN"
        exit 2
    else
        echo "  ERROR: script was broken."
        exit -1
    fi
}

リンク先が正しい場所を指しているかについては、「readlink」コマンドで得られたパス名を比較することで確認できる。

# $1:  source file
# $2:  destination file
function is_link() {
    LINKPATH=`readlink -f $2`
    DOTFILE=`basename $1`
        if [ $1 = ${LINKPATH} ]; then
        echo "\"${DOTFILE}\" is succeed"
    else
        echo "  × \"${DOTFILE}\" is failed"
        echo "     deploying dot file...OK"
        echo "     invalid paths ($2 -> ${LINKPATH})...NG"
    fi
}

上記のスクリプトを実行する「make test」を用意したので、結果が0か非0かどうかをチェックする。

vimプラグインのインストールが成功している

このテスト項目は、展開した開発環境でVimが正しく使用できるか確認するものである。 そのために、インストール時のリターンコードと、プラグインが使用できること、確認するの二つを実施する。

私のdotfilesでは、vim起動時にプラグインをインストールしていなければプラグインをインストールするようにしている。 そこで、下記のスクリプト(一部) を実行して、vimの起動とリターンコードを確認する。

またプラグインのインストール時に、続けるにはENTERを押すかコマンドを入力してくださいが表示されるので、yesコマンドでごまかしている。

# install success?
yes | timeout -sKILL 60 vim +:q > /dev/null 2>&1
if [ $? != 0 ]; then
        echo "Failed install dein, Please install again."
        exit 1
fi

インストールしたプラグインが実行できるかの確認は、vimプラグインを実行するフェイズとエラーメッセージを確認するフェイズで実施する。

Vimプラグイン実行するフェイズでは、プラグインが実行できるかどうか判定する変数○○○_enabledを参照する。 ただし一部のプラグインは提供されていないので、適当のコマンドを実行することにした)

vim -V0easymotion.log +"echo g:EasyMotion_keys" +:q
vim -V0webdevicons.log +"echo g:webdevicons_enable" +:q
vim -V0ale.log +"echo ale_enabled" +:q
vim -V0gitgutter.log +"echo gitgutter_enabled" +:q
vim -V0lightline.log +"echo lightline" +:q

エラーメッセージを確認するフェイズでは、前工程で得られたログファイルからエラーメッセージが存在するか判定する。 VimのエラーメッセージはEと任意の数字で構成されるので、正規表現でパターンマッチさせる。

# $1: logfile name
# $2: return code
function check_plugin() {
    LOGFILE=$1
    ERRCODE=$2

    FAILED=0

    while read LINE
 do
        if [[ $LINE =~ :E[0-9]*: ]]; then
            echo $LINE
            FAILED=1
        fi
    done < $1

    if [ $FAILED -ne 0 ]; then
        ret=`expr $ret + $2`
    fi
}

tmuxプラグインのインストールが成功している

このテスト項目は、展開した開発環境でtmuxが正しく使用できるか確認するものである。 確認方法は、インストールされたtmuxプラグインが正しいかどうか確認することを実施する。

そのために、ローカル環境に展開されたプラグインを取得するフェイズと、設定ファイルに記述したプラグイン名と取得するフェイズで実施する。

ローカル環境に展開されたプラグインを取得するフェイズでは、プラグインマネージャがtmuxプラグインを展開する先の~/.tmux/pluginディレクトリエントリから取得する。

# installed tmux plugin
for LINE in `ls -l ~/.tmux/plugins/ | awk '$1 ~ /d/ {print $9 }'`
do
        acutual=(${actual[@]} ${LINE})
done

設定ファイルに記述したプラグイン名と取得するフェイズでは、設定ファイルからプラグイン記述部から取得する。 プラグイン記述部は、set -g @plugin 'tmux-plugins/でで始まるので、正規表現でパターンマッチさせる。

COMMAND="set -g @plugin 'tmux-plugins/"

# expect to install tmux plugin
while read LINE
do
        if [[ $LINE =~ ${COMMAND}(.*)\' ]]; then
                expect=(${BASH_REMATCH[1]} ${expect[@]})
        fi
done < ~/.tmux.conf

テスト項目と実施概要について下記にまとめる。

テスト項目 実施概要 参考
設定ファイルのデプロイに成功している make install の返り値が0である make install
設定ファイルが正しい場所に展開されている 「-e」演算子とreadlinkコマンドで場所を確認する make test
vimプラグインのインストールが成功している Vimプラグインのコマンドを実行してエラーメッセージがでないこと check_vim_plugin
tmuxプラグインのインストールが成功していること インストールされたTmuxプラグインが正しいかどうか check_tmux_plugin

CIサービスとの連携

Travis CIにアクセスして、リポジトリの連携を有効化する。 Travis CIの使い方については、他の方がわかりやすくまとめてあるのでそちらを参照。

qiita.com

作成した.travis.ymlは下記の通りである。

language: bash

os: linux
dist: bionic
sudo: required

before_install:
    - sudo apt-get update
    - sudo apt-get install git
    - sudo apt-get install make
    - sudo apt-get install vim
    - sudo apt-get install zsh

script:
    - "make clean"
    - "make install"
    - "make test"
    - "tests/check_tmux_plugin"
    - "tests/check_vim_plugin"

上記の設定ファイルをdotfilesのトップディレクトリに格納し、GitHubに何かしらをプッシュすると自動でテストが走る。

Travis CI の結果

おわりに

下記のテスト項目をTravis CIと連携させることで、作成したdotfilesが壊れていたということを減らせるだろう。

  1. 設定ファイルのデプロイに成功している
  2. 設定ファイルが正しい場所に展開されている
  3. vimプラグインのインストールが成功している
  4. tmuxプラグインのインストールが成功している

しかし、テスト項目もテスト環境もまだまだ甘く、見つけることのできないバグなどもある。 そのためにも、テスト項目自体も定期的にメンテナンスしていく必要があるだろう。

変更履歴

  • 2019/12/22: 記事公開
  • 2022/06/06: デザイン修正

参考

Vimの導入プラグインを見直す

概要

deinプラグインマネージャで21個Vimプラグインをインストールしている環境に対して、見直しを実施した。

Vimの基本機能で代用できるものを削除していった結果、12個までプラグインを削減することができた。

はじめに

著者は4年以上Vimを使っているが、設定ファイルがブラックボックス化してしまっている。
そこで、使っていないプラグインなど忘れられているプラグインなどあると思うので、2019年のうちに設定ファイルを整理してみたいと思う。 今回は、導入するプラグインを必要最低限に絞って素のVimを楽しむことにする。

本記事は、vimrcのプラグイン周りの整理したときの記録を残す。

見直し前の環境

1年前からVimを含めて設定ファイルはdotfilesにまとめている。導入自体は簡単で、ワンコマンドでデプロイできるようにしている。

github.com

整理前のvimは下記の状態になっていて、ステータスラインやアンドゥツリーなどIDEっぽくしている。

整理する前のvim

ここで、使用しているVimプラグインを抜粋してみる。 deinを利用してプラグインを管理している。

プラグイン 概要
Shougo/dein.vim Vimプラグインマネージャ
Shougo/vimproc.vim Vimを非同期処理
tomasr/molokai カラースキーム
Shougo/neosnippet-snippets スニペット定義ファイル
Shougo/context_filetype.vim 動的にfiletypeを判定する
nathanaelkane/vim-indent-guides インデントの可視化
ujihisa/neco-look 英単語を補完
sjl/gundo.vim アンドゥツリーの表示
Lokaltog/vim-easymotion キータッチで目的行に移動する
Shougo/vimshell Vimからシェルを起動する
scrooloose/nerdtree ディレクトリツリーを表示
ryanoasis/vim-devicons ファイルタイプのアイコンを表示する
roxma/nvim-yarp deopleteの依存パッケージ
roxma/vim-hug-neovim-rpc deopleteの依存パッケージ
airblade/vim-gitgutter Git差分を左端に表示する
mbbill/undotree アンドゥツリーを表示(gundoできない環境用)
itchyny/lightline.vim ステータスラインをカスタマイズ
Shougo/neocomplete.vim コード補完(deopleteできない環境用)
Shougo/deoplete.nvim コード補完
Shougo/neosnippet スニペット機能
cespare/vim-toml TOMLのシンタックス

プラグインマネージャ

現在、deinを使用しているが他のプラグインマネージャに移行するか検討する。

  • Vundle
  • vim-pathogen
  • vim-plug
  • NeoBundle

ただ、今更プラグインマネージャを入れ替えるのは手間なのでdeinのままで進める。

プラグイン 変更点 理由
Shougo/dein.vim なし 移行理由がなかったため

カラースキーム

これまでmolokaiを使用してきたが、私にはすこし黒が強すぎるので他のcolorを試してみる。 http://vimcolors.com/ から、しばらくはonedarkを使用することにした。

プラグイン 変更点 理由
tomasr/molokai ntk148v/vim-horizonへ移行 気分転換

dev.classmethod.jp

LSP

LSP (Language Server Protocol) は、Microsoftが設計したプロトコルであり、エディタに必要な機能を実現するためのやり取りを規定している。

qiita.com

VimでLSPを利用する場合には、プログインを使う方法が最も簡単だと思われる。

ここでは、拡張機能の豊富な点からcoc.nvimを利用する。

プラグイン 変更点 理由
neoclide/coc.nvim 追加 LSPプラグインの中で、拡張機能に優れているため

検索

fzf (fuzzy finder) がコマンドラインであいまい検索ができるツールとなっている。

wonderwall.hatenablog.com

プラグイン 変更点 理由
junegunn/fzf 追加 あいまい検索を使うため (fzf本体)
junegunn/fzf.vim 追加 検索時の入力ミスが目立ってきたため

表示

プラグイン 変更点 理由
kshenoy/vim-signature 追加 Vimシグネチャ機能を使えていないため、導入として
machakann/vim-highlightedyank 追加 ヤンクの範囲漏れが目立ってきたので

プラグイン削除

Vimの標準コマンドで代用できるプラグインの削除

NERDTree は、ディレクトリツリーを表示してくれるプラグインである。 現状は、起動時に自動でディレクトリを表示させるようにして、開発効率の貢献をしてくれたと思っている。 しかし、:e .ディレクトリ表示ができるらしいので、あえてこのプラグインを削除して運用してみることにした。

VimShellは、Vimからシェルを起動するプラグインである。 ちょっとしたプログラムやスクリプトを確認するときに約に立ってくれたプラグインでもあるが、:terminalで代用できることもありいったん削除することにした。

deopleteは補完ツールプラグインの一つで、入力した文字から動的に補完バーを表示してくれる。 こちらもかなりお世話にになっているプラグインで、neco-lookと組み合わせて開発効率にかなり貢献してくれた。 しかし、VimにはデフォルトでCtrl-X補完機能が備わっているため、しばらくはそちらで運用してみることにした。

プラグイン 変更点 理由
scrooloose/nerdtree 削除 :e .で代用できるため
Shougo/vimshell 削除 :terminalで代用できるため
Shougo/neocomplete.vim 削除 Ctrl-X補完モードで代用
Shougo/deoplete.nvim 削除 Ctrl-X補完モードで代用
Shougo/neosnippet-snippets 削除 deopleteを削除するため
ujihisa/neco-look 削除 deopleteを削除するため
Shougo/neosnippet 削除 deopleteを削除するため
roxma/nvim-yarp 削除 deopleteを削除するため
roxma/vim-hug-neovim-rpc 削除 deopleteを削除するため

最近あまり使用していなかったプラグインの削除

context_filetypeは動的にfiletypeを判定してくれるツールで、ファイル内に複数のfiletypeがあるような言語での開発などに大いに活躍してくれる。 しかし、現在ファイルシステムの開発やCプログラムを触っている私には、そのような機会がなくあまり恩恵が受けられなかったので削除して運用する。

vim-indent-guidesは、インデントを可視化するプラグインで、プログラムの深さを意識させてくれる。 しかし、タブ文字派に転職したことでタブ文字からインデントを意識できるようになったので削除して運用する。

gundoは、アンドゥツリーを表示するプラグインであり、戻したりやり直したりを容易にできるプラグインである。 しかし、自分はこの機能を忘れて普通にアンドゥやリドゥを使っていることが多かったので削除して運用する。

vim-tomlはTOML用のシンタックスを提供してくれるプラグインであるが、あまりTOMLファイルを書かないので必要になったときにインストールすることにして、今回は削除して運用する。

プラグイン 変更点 理由
Shougo/context_filetype.vim 削除 あまり使わないため
nathanaelkane/vim-indent-guides 削除 あまり使わないため
sjl/gundo.vim 削除 あまり使わないため
mbbill/undotree 削除 あまり使わないため
cespare/vim-toml 削除 あまり使わないため

見直し後の環境

最終的に導入しているプラグインは以下のようになった。

プラグイン 概要
Shougo/dein.vim Vimプラグインマネージャ
Shougo/vimproc.vim Vimを非同期処理
ntk148v/vim-horizon カラースキーム
Lokaltog/vim-easymotion キータッチで目的行に移動する
neoclide/coc.nvim LSPプラグイン
airblade/vim-gitgutter Git差分を左端に表示する
itchyny/lightline.vim ステータスラインをカスタマイズ
ryanoasis/vim-devicons ファイルタイプのアイコンを表示する
junegunn/fzf あいまい検索を使うため (fzf本体)
junegunn/fzf.vim fuzzy finder
kshenoy/vim-signature シグネチャの位置をマークする
machakann/vim-highlightedyank ヤンク範囲を一時的にハイライトする

整理前と比べて表示の部分もすっきりとした感じになった。

2022年現在のNeoVim

おわりに

今回はVimの使ってなかったプラグインの削除をして、vimrcの整理をした。 しばらくは、必要最低限度のプラグインで運用していき必要になったら追加していくようにする。

変更履歴

  • 2019/12/02: 記事公開
  • 2022/02/27: 2022年用に更新
  • 2022/06/05: デザイン修正

参考

trace-cmdでカーネルの関数のコールフローを取得する

概要

trace-cmdは、Linuxカーネルの機能ftraceを利用したツールであり、実行中のLinuxカーネルから関数コールスタックを取できる。

本記事では、別のLinuxマシンにトレースデータをTCPで送ることで、lsコマンドを実行したときのカーネル関数のコールフローを取得する。

はじめに

Linuxカーネルには、デフォルトでカーネルをトレースする機能がいくつか提供されている。 ftraceはトレース機構の一つで、カーネルの実行中に実行されたさまざまな関数呼び出しに関連する情報を取得できる。

Ftrace is an internal tracer designed to help out developers and designers of systems to find what is going on inside the kernel. It can be used for debugging or analyzing latencies and performance issues that take place outside of user-space. https://www.kernel.org/doc/Documentation/trace/ftrace.txt

今回は、ftraceをユーザランドから使いやすくしたツールtrace-cmdを用いて、関数コールフローを追ってみる。

利用イメージは下記の通りとなっている。 ゲスト2で関数コールフローを採取し、同じネットワークにつながれているゲスト1へTCPでトレースデータtrace.datを取得する。

関数コールフロー取得における利用イメージ

関数コールフローを採取するマシンと、トレースデータを取得するマシンを分けることで以下の利点がある。 *1

  • トレースデータを一つに集約することができる

ftraceとtrace-cmdの使い方など先駆者の方のリンクにまとめられているので、そちらを参照することをオススメする。

qiita.com

tasukuchan.hatenablog.com

qiita.com

yohgami.hateblo.jp

準備

内部ネットワークの構築

仮想マシン同士を同一ネットワークでつなげる (ゲスト1: 192.168.7.11、ゲスト2: 192.168.7.12)

# -*- 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.memory = "4096"
      vb.cpus = 2
    end
    c.vm.network "private_network", ip: "192.168.7.11", virtualbox__intnet: "devnet"
  end

  config.vm.define "debuggee" do |c|
    c.vm.provider "virtualbox" do |vb|
      vb.memory = "4096"
      vb.cpus = 1
    end
    c.vm.network "private_network", ip: "192.168.7.12", virtualbox__intnet: "devnet"
  end

前回からの記事と同様に、ゲスト1をdebugger、ゲスト2をdebuggeeと呼ぶことにする。

ftraceの設定

trace-cmdを使用するためには、ftraceが使用できることが条件となっている。 ftraceが使える条件は以下の二つが設定されていることが重要である。

  • カーネルコンフィグレーションでftraceに対応している
  • debugfsをマウントしている

カーネルコンフィグレーションは以下の設定が必要である。(前回の仮想マシンではデフォルトで有効化になっているので変更不要)

CONFIG_FTRACE = y
CONFIG_FUNCTION_TRACER = y
CONFIG_FUNCTION_GRAPH_TRAC = y

debugfsのマウントも確認が必要である。

debuggee:~$ mount | grep debugfs
debugfs on /sys/kernel/debug type debugfs (rw,relatime)

trace-cmdのインストール

両方の仮想マシンにtrace-cmdをインストールする。

debuggee:~$ sudo apt install trace-cmd
debugger:~$ sudo apt install trace-cmd

トレースの取得

コマンドのトレースを取得する

  1. ゲスト1(debugger)の11111ポートでトレースを待ち受ける (必要に応じてポート開放すること)

     debugger:~$ sudo trace-cmd listen -p 11111
    
  2. ゲスト2(debuggee)からlsコマンドを実行し、トレース結果をゲスト1(192.168.7.11)の11111ポートへ転送する

     debuggee:~$ sudo trace-cmd record -N 192.168.7.11:11111 -p function_graph -F ls plugin 'function_graph'
    
  3. トレースを出力したら、ゲスト1(debugger)のトレース待ち受け状態を^Cで解除する

     Connected with 192.168.7.11:40234
     cpus=1
     pagesize=4096
     CPU0 data recorded at offset=0x4a8000
         2039808 bytes in size
     ^C
    
      debugger:~$
    
  4. 出力されたトレースデータtrace.192.168.7.11:40234.datをプレーンテキスト形式trace.listに出力する

     debugger:~$ sudo trace-cmd report trace.192.168.7.12\:48144.dat > trace.list
    
  5. 出力結果の確認

     debugger:~$ head -n 20 trace.list 
     cpus=1
                <...>-2278  [000]  4757.532540: funcgraph_entry:        1.148 us   |  mutex_unlock();
                <...>-2278  [000]  4757.532541: funcgraph_entry:        0.274 us   |  __fsnotify_parent();
                <...>-2278  [000]  4757.532541: funcgraph_entry:        0.164 us   |  fsnotify();
                <...>-2278  [000]  4757.532542: funcgraph_entry:        0.114 us   |  __sb_end_write();
                <...>-2278  [000]  4757.532542: funcgraph_entry:                   |  __f_unlock_pos() {
                <...>-2278  [000]  4757.532542: funcgraph_entry:        0.122 us   |    mutex_unlock();
                <...>-2278  [000]  4757.532542: funcgraph_exit:         0.341 us   |  }
                <...>-2278  [000]  4757.532544: funcgraph_entry:                   |  __do_page_fault() {
                <...>-2278  [000]  4757.532544: funcgraph_entry:        0.118 us   |    down_read_trylock();
                <...>-2278  [000]  4757.532544: funcgraph_entry:                   |    _cond_resched() {
                <...>-2278  [000]  4757.532544: funcgraph_entry:        0.117 us   |      rcu_all_qs();
                <...>-2278  [000]  4757.532545: funcgraph_exit:         0.347 us   |    }
                <...>-2278  [000]  4757.532545: funcgraph_entry:                   |    find_vma() {
                <...>-2278  [000]  4757.532545: funcgraph_entry:        0.157 us   |      vmacache_find();
                <...>-2278  [000]  4757.532545: funcgraph_exit:         0.398 us   |    }
                <...>-2278  [000]  4757.532545: funcgraph_entry:                   |    handle_mm_fault() {
                <...>-2278  [000]  4757.532545: funcgraph_entry:        0.111 us   |      mem_cgroup_from_task();
                <...>-2278  [000]  4757.532546: funcgraph_entry:                   |      __handle_mm_fault() {
                <...>-2278  [000]  4757.532546: funcgraph_entry:        0.131 us   |        pmd_devmap_trans_unstable();
     <snip>
    

プロセスのトレースを取得する

  1. ゲスト1(debugger)の11111ポートでトレースを待ち受ける (必要に応じてポート開放すること)

     debugger:~$ sudo trace-cmd listen -p 11111
    
  2. ゲスト2(debuggee)において、トレース対象のプロセスのpidを確認する。(今回はjbd2を対象とする)

     debuggee:~$ ps -e | grep jbd2
       330 ?        00:00:00 jbd2/sda1-8
    
  3. ゲスト2(debuggee)からpidを指定し、トレース結果をゲスト1(192.168.7.11)の11111ポートへ転送する

     debuggee:~$ sudo trace-cmd record -N 192.168.7.11:11111 -p function_graph -P 330 plugin 'function_graph'
    
  4. プロセスが動作したことを確認したら、ゲスト2(debuggee)のトレース出力状態を^Cで解除する

     ^C
    
     debuggee:~$
    
  5. 出力されたトレースデータtrace.192.168.7.11:48148.datをプレーンテキスト形式trace.listに出力する

     Connected with 192.168.7.12:48148
     cpus=1
     pagesize=4096
     CPU0 data recorded at offset=0x4ba000
    
     debugger:~$ sudo trace-cmd report trace.192.168.7.12\:48148.dat > trace.list
    
  6. 出力結果の確認

     debugger:~$ head -n 20 trace.list 
     cpus=1
               <idle>-0     [000] 10069.427204: funcgraph_entry:      + 12.184 us  |  enter_lazy_tlb();
          jbd2/sda1-8-330   [000] 10069.427218: funcgraph_entry:                   |  finish_task_switch() {
          jbd2/sda1-8-330   [000] 10069.427225: funcgraph_entry:                   |    smp_irq_work_interrupt() {
          jbd2/sda1-8-330   [000] 10069.427226: funcgraph_entry:                   |      irq_enter() {
          jbd2/sda1-8-330   [000] 10069.427226: funcgraph_entry:                   |        rcu_irq_enter() {
          jbd2/sda1-8-330   [000] 10069.427227: funcgraph_entry:        0.638 us   |          rcu_nmi_enter();
          jbd2/sda1-8-330   [000] 10069.427228: funcgraph_exit:         1.779 us   |        }
          jbd2/sda1-8-330   [000] 10069.427229: funcgraph_exit:         2.952 us   |      }
          jbd2/sda1-8-330   [000] 10069.427235: funcgraph_entry:                   |      __wake_up() {
          jbd2/sda1-8-330   [000] 10069.427236: funcgraph_entry:                   |        __wake_up_common_lock() {
          jbd2/sda1-8-330   [000] 10069.427236: funcgraph_entry:        0.568 us   |          _raw_spin_lock_irqsave();
          jbd2/sda1-8-330   [000] 10069.427237: funcgraph_entry:        0.597 us   |          __wake_up_common();
          jbd2/sda1-8-330   [000] 10069.427239: funcgraph_entry:        0.568 us   |          __lock_text_start();
          jbd2/sda1-8-330   [000] 10069.427240: funcgraph_exit:         4.013 us   |        }
          jbd2/sda1-8-330   [000] 10069.427240: funcgraph_exit:         5.284 us   |      }
          jbd2/sda1-8-330   [000] 10069.427241: funcgraph_entry:                   |      __wake_up() {
          jbd2/sda1-8-330   [000] 10069.427241: funcgraph_entry:                   |        __wake_up_common_lock() {
          jbd2/sda1-8-330   [000] 10069.427242: funcgraph_entry:        0.550 us   |          _raw_spin_lock_irqsave();
          jbd2/sda1-8-330   [000] 10069.427243: funcgraph_entry:                   |          __wake_up_common() {
    

イベントのトレースを取得する

  1. ゲスト1(debugger)の11111ポートでトレースを待ち受ける (必要に応じてポート開放すること)

     debugger:~$ sudo trace-cmd listen -p 11111
    
  2. ゲスト2(debuggee)からイベントnetを指定し、トレース結果をゲスト1(192.168.7.11)の11111ポートへ転送する (指定できるイベントはtrace-cmd list -eで確認できる)

     debugger:~$ sudo trace-cmd record -N 192.168.7.11:11111 -p function_graph -e net ping -c 1 192.168.7.11
       plugin 'function_graph'
     PING 192.168.7.11 (192.168.7.11) 56(84) bytes of data.
     64 bytes from 192.168.7.11: icmp_seq=1 ttl=64 time=0.176 ms
    
     --- 192.168.7.11 ping statistics ---
     1 packets transmitted, 1 received, 0% packet loss, time 0ms
     rtt min/avg/max/mdev = 0.176/0.176/0.176/0.000 ms
    
  3. トレースを出力したら、ゲスト1(debugger)のトレース待ち受け状態を^Cで解除する

     Connected with 192.168.7.12:48150
     cpus=1
     pagesize=4096
     CPU0 data recorded at offset=0x4be000
         1609728 bytes in size
     ^C
    
     debugger:~$
    
  4. 出力されたトレースデータtrace.192.168.7.11:48150.datをプレーンテキスト形式trace.listに出力する

     debugger:~$ sudo trace-cmd report trace.192.168.7.12\:48150.dat > trace.list
    
  5. 出力結果の確認

     debugger:~$ head -n 20 trace.list 
     cpus=1
                <...>-2746  [000] 10875.245946: funcgraph_entry:                   |  mutex_unlock() {
                <...>-2746  [000] 10875.245951: funcgraph_entry:                   |    smp_irq_work_interrupt() {
                <...>-2746  [000] 10875.245951: funcgraph_entry:                   |      irq_enter() {
                <...>-2746  [000] 10875.245952: funcgraph_entry:                   |        rcu_irq_enter() {
                <...>-2746  [000] 10875.245952: funcgraph_entry:        0.154 us   |          rcu_nmi_enter();
                <...>-2746  [000] 10875.245952: funcgraph_exit:         0.464 us   |        }
                <...>-2746  [000] 10875.245952: funcgraph_exit:         0.725 us   |      }
                <...>-2746  [000] 10875.245954: funcgraph_entry:                   |      __wake_up() {
                <...>-2746  [000] 10875.245954: funcgraph_entry:                   |        __wake_up_common_lock() {
                <...>-2746  [000] 10875.245954: funcgraph_entry:        0.121 us   |          _raw_spin_lock_irqsave();
                <...>-2746  [000] 10875.245954: funcgraph_entry:        0.116 us   |          __wake_up_common();
                <...>-2746  [000] 10875.245954: funcgraph_entry:        0.120 us   |          __lock_text_start();
                <...>-2746  [000] 10875.245954: funcgraph_exit:         0.835 us   |        }
                <...>-2746  [000] 10875.245955: funcgraph_exit:         1.076 us   |      }
                <...>-2746  [000] 10875.245955: funcgraph_entry:                   |      __wake_up() {
                <...>-2746  [000] 10875.245955: funcgraph_entry:                   |        __wake_up_common_lock() {
                <...>-2746  [000] 10875.245955: funcgraph_entry:        0.114 us   |          _raw_spin_lock_irqsave();
                <...>-2746  [000] 10875.245955: funcgraph_entry:        0.115 us   |          __wake_up_common();
                <...>-2746  [000] 10875.245955: funcgraph_entry:        0.122 us   |          __lock_text_start();
     <snip>
    

おわりに

今回は、カーネルのコードリーディングに役立つftrace/trace-cmdの導入方法についてまとめた。 trace-cmdでカーネルの関数コールフローを取得することで、kgdbでブレークするポイントを特定したり、Linuxカーネルのコードを効率的に読むことができる。

次回は、コードリーディングに役立つツールkprobeの紹介をしたいと思う。

変更履歴

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

参考

*1:ただし、trace-cmdは採取するマシンと取得するマシンを分ける必要はない。

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