kpatchはhot patchをどうやって作っているのか調べてみる

Red HatLinuxカーネルにライブパッチするツールとしてkpatchを公開したのでそれはどうやってhot patchを作るのかを調べてみる。 kpatchのソースコードgithubに置かれている。 kpatchはパッチの作成を行うモジュール群、パッチを当てるモジュール、両者を行うためのkpatchというbashスクリプトの3種類で構成されているけど、そのうちkpatchコマンドはラッパーみたいなものなので特に気にしない。kpatchの場合、patchを当てるモジュールはカーネルモジュール(kpatch.ko)、パッチの形式もカーネルモジュール(kpatch-$PATCHNAME.ko)となっている。 あ、hot patch作成の流れではソースコードレベルのpatchと、バイナリパッチの2種類があるので前者はpatch、後者はhot patchと呼ぶことにする。

基本的な流れは以下の3ステップ

  1. kpatch.koをinsmodする
  2. hot patchを作る
  3. 作ったhot patchをinsmodする

kpatch.koはgithubからコードをcloneしたあとにmakeして作られたもの。

kpatchでhot patchを作るときにソースコードレベルのpatchファイル(ふつうの*.patch)が必要で、このpatchは複数個ではなくて1つのパッチじゃないといけない。 hot patchの作成はkpatch-build/にあるkpatch-buildが主担当。kpatch-buildの実行にはpatchファイルのパスを渡す。

最初にカーネルのソースがあるかをチェック。これは$HOME/.kpatch/`uname -r`/srcを想定していて、このディレクトリがない場合yumdownloaderでuname -rに該当するカーネルの.src.rpmをダウンロードしにいく。このディレクトリは$SRCDIRで参照される。引数で受け取ったpatchファイルはapplied-patchという名前で$SRCDIRに置かれて、hot patch作成後にパッチを外すのとapplied-patchの削除が行われるんだけど、万が一、applied-patchがあったらパッチを外して(patch -R -p1 < applied-patch)からapplied-patchも削除する。 これで$SRCDIRにあるソースにはhot patch用のpatchが当たっていない状態になる。

次に、この版でvmlinuxを作成する。ここで作ったvmlinuxは後のステップでも使うのでビルドが終わったら$TEMPDIRにコピーしておく。vmlinuxをmakeするときはmake "-j$CPUS" vmlinux "O=$OBJDIR"という感じで作っていて、オブジェクトファイルは$SRCDIRに作られないようにしている。 また、hot patch用のカーネルモジュールのソース一式(kmod/patch)も$TEMPDIRにコピーする。

そうしたら次はpatch適用したvmlinuxの作成を行う。これは先ほどと同様のmakeの仕方で作成。ここで作ったvmlinuxは使わない。ここのステップはpatchを適用したことで再ビルドされたオブジェクトファイルを見つけることが目的。こんな感じでビルドのログからversion.oを除いてビルドされたファイルを取得しておく。

grep CC "$TEMPDIR/patched_build.log" | grep -v init/version.o | awk '{print $2}' >> "$TEMPDIR/changed_objs"

次のステップでは変更の合ったものだけ(changed_objsにあるファイル)を再ビルドするという作業。この時にgccコンパイルオプションで-ffunction-sections -fdata-sectionsをつけて不要な関数の削除等最適化させる(オプションの説明はここに「第16回 GCC2.95から追加変更のあったオプションの補足と検証(その4)」。ここでコンパイルされた*.oファイルは$TEMPDIR/patchedに置かれる。

次のステップはpatchを外して(patch -R)から上でやったのと同じ手順でchanged_objsにあるものを再コンパイルコンパイルしてできた*.oは$TEMPDIR/origに置く。

ここまででpatch適用により更新の合った.oファイルと、patch未適用のオリジナルの.oの2種類のオブジェクトファイルができた状態。 * $TEMPDIR/patched/にパッチを当てた版 * $TEMPDIR/orig/にパッチを当ててない版

次に個々の*.oファイルごとにバイナリdiffを取っていく。ここで使うdiffツールはcreate-diff-objectでkpatch-build/の下のcreate-diff-object.cがそれ。ここで取ったdiffは$TEMPDIR/outputに置かれる。

diffが取り終わったらldコマンドで$TEMP/outputにあるファイルをひとまとめにする。ldで作ったファイルはoutput.oで$TEMPDIR/patch/に置かれる。このpatchディレクトリは前の方の手順で出てきたhot patch用のカーネルモジュールのソースコードがある場所。

次はadd-patches-sectionコマンド(kpatch-build/add-patches-section.c)を使ってoutput.oに.patchesと.rela.patchesという2個のsectionを作る。add-patches-section.cを見るとこのように説明が。

/*
 * This tool takes an elf object, the output of create-diff-object
 * and the base vmlinux as arguments and adds two new sections
 * to the elf object; .patches and .rela.patches.
 *
 * These two sections allow the kpatch core modules to know which
 * functions are overridden by the patch module.
 *
 * For each struct kpatch_patch entry in the .patches section, the core
 * module will register the new function as an ftrace handler for the
 * old function.  The new function will return to the caller of the old
 * function, not the old function itself, bypassing the old function.
 */

そうしたらpatchディレクトリでhot patch(*.ko)の作成。

KPATCH_BUILD="$SRCDIR" KPATCH_NAME="$PATCHNAME" make "O=$OBJDIR" >> "$LOGFILE"

kpatch-buildに渡したpatchファイル名がfoobar.patchだったらhot patchファイル名はkpatch-foobar.koとなる。 作成したhot patchをstripコマンドにかける(strip -d --keep-file-symbols)。

そしてhot patch作成の最後の処理になるlink-vmlinux-symsコマンド(kpatch-build/link-vmlinux-syms.c)を実行。これはhot patchのシンボルテーブルにvmlinuxからエクスポートされていないグローバルシンボルのアドレスを書き込む。これもファイルの先頭の方に概要が書かれている。

/*
 * This tool takes the nearly complete hotfix kernel module and
 * the base vmlinux. It hardcodes the addresses of any global symbols
 * that are referenced by the output object but are not exported by
 * vmlinux into the symbol table of the kernel module.
 *
 * Global symbols that are exported by the base vmlinux can be
 * resolved by the kernel module linker at load time and are
 * left unmodified.
 */

これでhot patchが完成したのでこの.koをinsmodするとカーネルにhot patchを当てることができる。これはkpatch.koの仕事なのでkmod/coreにあるファイルが読むべきソースコード