この記事はLinux Advent Calendar 2019の1日目の記事です。
はじめに
本記事ではLinuxサーバのホスト名、Linuxカーネルのバージョン、cpuアーキテクチャなどのシステム情報を表示するuname(1)を利用してLinux環境でのデバッグとカーネルハックについて説明していきます。本記事ではコマンドやツールの使い方の説明ではなくて、それらを使ってどのようにデバッグするのかというところを説明します。
環境
ディストリビューションにはFedora 31(x86_64)を利用します。動作環境はQEMUやlibvirt、Oracle VM VirtualBoxなどの仮想環境でも良いですし、物理マシンにインストールしても構いませんが仮想環境を利用したほうが手軽に環境構築できるのでおすすめです。ユーザーはsudoを使えるようにしておいてください。インストール時もしくはインストール後に以下のグループをインストールすると後で楽かもしれません。
- C Development Tools and Libraries
- Development Tools
- RPM Development Tools
- System tools
- Guest Agents(仮想環境にインストールする場合)
パッケージグループのインストールは以下のコマンドで行えます。
[masami@unamebook ~]$ sudo dnf group install -y "C Development Tools and Libraries" "Development Tools" "RPM Development Tools" "System tools" "Guest Agents"
ファイルの編集をするのにエディタが必要です。これは好きなエディタを使ってください。
おことわり
本記事で行うツールの利用方法などはディストリビューションに依存しないことが多いと思いますが、コマンドの出力結果やソースコードについてはディストリビューション固有のpatchが当たっている場合もあり、fedora 31以外のディストリビューション(場合によってはfedora 31でもパッケージのバージョン違い)と違うことがあります。
uname(1)*1を実行したことのある方は多いと思いますが、まずは普通にuname(1)を実行しましょう。このコマンドはcoreutilsパッケージに含まれています。
[masami@unamebook ~]$ uname -a
Linux unamebook 5.3.11-300.fc31.x86_64 #1 SMP Tue Nov 12 19:08:07 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
unameコマンドに渡した-aオプションはすべての情報を表示させるオプションです。上記の出力結果を分解したものが以下の「表1 unameの出力内容」です。
表1 unameの出力内容
uname(1)を実行してシステム情報を見ることができましたがこの情報はどこから取得したのでしょうか?この情報はLinuxカーネルが持っていて、uname(1)はシステムコールを通じてカーネルに問い合わせを行い、カーネルから受け取った結果を表示しています。このときに利用するシステムコールはuname(2)*3です。uname(2)のプロトタイプは次のようになっています。カーネルは引数で渡されたbuf変数にシステム情報をセットします。
int uname(struct utsname *buf)
uname(2)のインターフェース
uname(2)の詳細はmanページで確認してください。
[masami@unamebook ~]$ sudo dnf install -y man-pages
strace(1)
strace(1)は、uname(1)の実行時にuname(2)が呼ばれていることを確認しましょう。ここではデバッガは使わずにstrace(1)を利用します。strace(1)は簡単に言うと指定したコマンド・プロセスによるシステムコール呼び出しをトレースするコマンドです。まずはstraceパッケージをインストールします。
[masami@unamebook ~]$ sudo dnf install -y strace
straceパッケージをインストールしたら実行してみましょう。オプションの細かい内容は説明しませんが、ここではuname(2)に絞って表示させるように実行しました。
[masami@unamebook ~]$ strace -v -s 1024 -e trace=uname -C uname -a
uname({sysname="Linux", nodename="unamebook", release="5.3.11-300.fc31.x86_64", version="#1 SMP Tue Nov 12 19:08:07 UTC 2019", machine="x86_64", domainname="(none)"}) = 0
uname({sysname="Linux", nodename="unamebook", release="5.3.11-300.fc31.x86_64", version="#1 SMP Tue Nov 12 19:08:07 UTC 2019", machine="x86_64", domainname="(none)"}) = 0
uname({sysname="Linux", nodename="unamebook", release="5.3.11-300.fc31.x86_64", version="#1 SMP Tue Nov 12 19:08:07 UTC 2019", machine="x86_64", domainname="(none)"}) = 0
Linux unamebook 5.3.11-300.fc31.x86_64 #1 SMP Tue Nov 12 19:08:07 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
+++ exited with 0 +++
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
100.00 0.000007 2 3 uname
------ ----------- ----------- --------- --------- ----------------
100.00 0.000007 3 total
uname(2)の呼び出しが3回ありますね。なぜ3回uname(2)を実行しているのでしょうか?これはソースコードを確認しないとわかりません。unameコマンドを提供しているのはcoreutilsパッケージなのでcoreutilsパッケージのソースコードをダウンロードしましょう。利用しているのはfedoraにインストールされているcoreutilsパッケージなのでupstreamではなくfedoraのソースコードを取得します。
[masami@unamebook ~]$ dnf download --source coreutils
ソースパッケージ(.src.rpm)をダウンロードしたらインストールします。
まず展開先のディレクトリをセットアップします。
[masami@unamebook ~]$ rpmdev-setuptree
そしてrpmコマンドでインストールします。
[masami@unamebook ~]$ rpm -i coreutils-8.31-6.fc31.src.rpm
インストール時に次のようなwarningが出ますが無視して大丈夫です。
warning: user mockbuild does not exist - using root
warning: user mockbuild does not exist - using root
これで~/rpmbuildにソースパッケージがインストールされましたが、ソースコードはまだ展開されていないでソースコードの展開&patchを当てましょう。と、その前にいくつか必要なパッケージをインストールします。
[masami@unamebook ~]$ sudo dnf builddep -y coreutils
これで大丈夫です。ではソースコードの準備をしましょう。
[masami@unamebook ~]$ cd rpmbuild/SPECS/
[masami@unamebook SPECS]$ rpmbuild -bp coreutils.spec
ソースコードを展開しpatchが適用されたソースコードは~/rpmbuild/BUILDにあります。
[masami@unamebook SPECS]$ cd ~/rpmbuild/BUILD/coreutils-8.31/
[masami@unamebook coreutils-8.31]$ ls
ABOUT-NLS autom4te.cache build-aux configure DIR_COLORS dist-check.mk GNUmakefile lib Makefile.am NEWS src thanks-gen THANKStt.in
aclocal.m4 bootstrap cfg.mk configure.ac DIR_COLORS.256color doc init.cfg m4 Makefile.in po tests THANKS.in TODO
AUTHORS bootstrap.conf ChangeLog COPYING DIR_COLORS.lightbgcolor gnulib-tests INSTALL maint.mk man README THANKS THANKS-to-translators
uname(1)のソースコードはsrc/uname.cです。grepでuname(2)の呼び出し箇所を調べると3箇所あることがわかります。
[masami@unamebook coreutils-8.31]$ grep -n "uname *(" src/uname.c
286: if (uname (&name) == -1)
313: uname(&u);
364: uname(&u);
286行目付近を見るとこのようになっています。toprint変数はuname(1)の引数に応じてビットが立ちます。オプションに-aを渡した場合はこのif文は真になるので286行目のuname(2)が実行されます。
280 if (toprint
281 & (PRINT_KERNEL_NAME | PRINT_NODENAME | PRINT_KERNEL_RELEASE
282 | PRINT_KERNEL_VERSION | PRINT_MACHINE))
283 {
284 struct utsname name;
285
286 if (uname (&name) == -1)
287 die (EXIT_FAILURE, errno, _("cannot get system name"));
313行目はどうかというと、ここは-pオプションの処理部分です。マクロのelseブロックが実行されるようです。
304 #if HAVE_SYSINFO && defined SI_ARCHITECTURE
305 {
306 static char processor[257];
307 if (0 <= sysinfo (SI_ARCHITECTURE, processor, sizeof processor))
308 element = processor;
309 }
310 #else
311 {
312 static struct utsname u;
313 uname(&u);
314 element = u.machine;
315 }
残りの364行目も確認しましょう。こちらは-iオプションのハードウェアプラットフォーム取得部分です。こちらもマクロのelseブロックが呼ばれているようです。
354 #if HAVE_SYSINFO && defined SI_PLATFORM
355 {
356 static char hardware_platform[257];
357 if (0 <= sysinfo (SI_PLATFORM,
358 hardware_platform, sizeof hardware_platform))
359 element = hardware_platform;
360 }
361 #else
362 {
363 static struct utsname u;
364 uname(&u);
365 element = u.machine;
366 if(strlen(element)==4 && element[0]=='i' && element[2]=='8' && element[3]=='6')
367 element[1]='3';
368 }
369 #endif
この3箇所のuname(2)の呼び出しですが、286行目はupstreamのコードにもあります。しかし、残りの2箇所はupstreamのコードには存在しません。この2箇所はfedoraのcoreutilsパッケージが独自当てているpatchです。このpatchは~/rpmbuild/SOURCES/coreutils-8.2-uname-processortype.patchです。興味のある方は確認してみてください。
ソースコードを読んでuname(2)の呼び出し箇所が3箇所あることはわかったのですが、一応実際の動作を見てみましょう。ここではgdbを利用します。gdbを使ってデバッグをする場合はデバッグ情報があると便利です。通常のパッケージに含まれるバイナリファイルはデバッグ情報が存在しないため、デバッグが不便です。fedoraの場合はデバッグ情報はdebuginfoパッケージとして存在しているのでこれをインストールします。glibcのdebuginfoパッケージも合わせてインストールします。
と、その前にdebuginfo-installコマンドが必要なのでパッケージをインストールしましょう。
[masami@unamebook ~]$ sudo dnf install -y dnf-utils
dnf-utilsパッケージをインストールしたらdebuginfo-installコマンドが利用できますので、これでdebuginfoのパッケージをインストールします。
[masami@unamebook ~]$ sudo debuginfo-install -y coreutils glibc
debuginfoパッケージをインストールしたらgdbを起動しましょう。gdbは自動的にデバッグ情報を読み込んでくれます。
[masami@unamebook ~]$ gdb -q /usr/bin/uname
Reading symbols from /usr/bin/uname...
Reading symbols from /usr/lib/debug/usr/bin/uname-8.31-6.fc31.x86_64.debug...
(gdb)
gdbが立ち上がったらとりあえず280行目のif文あたりにブレークポイントを張りましょう。listコマンドで280行目付近を表示します。
(gdb) list 280
275 toprint = decode_switches (argc, argv);
276
277 if (toprint == 0)
278 toprint = PRINT_KERNEL_NAME;
279
280 if (toprint
281 & (PRINT_KERNEL_NAME | PRINT_NODENAME | PRINT_KERNEL_RELEASE
282 | PRINT_KERNEL_VERSION | PRINT_MACHINE))
283 {
284 struct utsname name;
breakコマンド(略性はb)で280行目にブレークポイントをセットします。
(gdb) b 280
Breakpoint 1 at 0x27b4: /usr/src/debug/coreutils-8.31-6.fc31.x86_64/separate/../src/uname.c:280. (2 locations)
runコマンド(略称はr)でunameコマンドを実行します。uname(1)のオプションには-aを渡しています。unameコマンドを実行すると先程設定したブレークポイントで止まるので、あとはnでステップ実行していけばuname(2)の呼び出しが3回行われるのがわかります。
(gdb) r -a
Starting program: /usr/bin/uname -a
Breakpoint 1, main (argc=2, argv=0x7fffffffe448) at ../src/uname.c:280
280 if (toprint
(gdb) n
286 if (uname (&name) == -1)
(gdb) n
~略~
301 if (toprint & PRINT_PROCESSOR)
(gdb)
313 uname(&u);
(gdb) n
~略~
(gdb) n
364 uname(&u);
(gdb) c
Continuing.
Linux unamebook 5.3.11-300.fc31.x86_64 #1 SMP Tue Nov 12 19:08:07 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
[Inferior 1 (process 6522) exited normally]
今回はプログラムが単純で短いのでステップ実行で済ませています。
ここまででstrace(1)、ソースコードリーディング、gdb(1)を使いuname -aの実行時にuname(2)が3回呼ばれる謎を調べることに成功しました。
bpftrace
bpftraceはLinuxのeBPFを使用したトレースツールです。eBFPとはextended Berkeley Packet Filterの略で元来はパケットフィルタの機能ですがLinuxではこのパケットフィルタ機能が進化し、パケットフィルタだけでは収まらなくなった機能です。Linuxではbpf(2)があり、c言語でeBPFの機能を使ったプログラムを作成できますが、これは大変なので普通はBCCを使うことが多いのではないでしょうか。BCCはBPF Compiler Collectionの略でPythonとc言語を使ってeBFPを利用するプログラムを作ることが出来ます。そしてbpftraceですがBCCをバックエンドとして使った高レベルのトレース用言語と説明されています。bpftraceではカーネルレベルのトレースを取ることが出来ます。Fedoraにはパッケージがあるのでインストールしましょう。
[masami@unamebook ~]$ sudo dnf install -y bpftrace
インストールが完了したらuname(2)のトレースを行ってみましょう。まずはシステムコールの呼び出しをトレースします。
この他に、カーネルのソースも読みたいのでこちらもインストールしましょう。今回はdebuginfo-installコマンドを使用してデバッグ情報とソースをインストールします。coreutilsのときは単にソースを取得したいだけだったのでsrpmパッケージをインストールしましたが、debuginfoパッケージの場合はデバッグ情報とソースコードをインストールできます。gdb(1)の章でcoreutilsとglibcのdebuginfoパッケージをインストールしましたが、このときにソースコードもインストールされいます。じゃあ、この2つの使い分けは?という疑問があると思いますが、ソースを変更して再ビルドする用途にはsrpmパッケージを、デバッグ情報のみが必要な場合はdebuginfoパッケージを使用すれば良いと思います。では、カーネルのdebuginfoパッケージをインストールします。
[masami@unamebook ~]$ sudo debuginfo-install -y kernel-debuginfo-common-x86_64
debuginfoパッケージに含まれるソースコードは/usr/src/debug以下にインストールされます。
[masami@unamebook ~]$ ls /usr/src/debug/
coreutils-8.31-6.fc31.x86_64 glibc-2.30-13-g919af705ee kernel-5.3.fc31
インストールできたらカーネル側のuname(2)を処理する関数を探します。システムコールはinclude/linux/syscalls.hで探すことが出来ます。カーネルのソースコードは以下の場所にあります。
[masami@unamebook ~]$ cd /usr/src/debug/kernel-5.3.fc31/linux-5.3.11-300.fc31.x86_64/
まずはunameでgrepをかけましょう。
[masami@unamebook linux-5.3.11-300.fc31.x86_64]$ grep uname include/linux/syscalls.h
asmlinkage long sys_newuname(struct new_utsname __user *name);
asmlinkage long sys_memfd_create(const char __user *uname_ptr, unsigned int flags);
asmlinkage long sys_uname(struct old_utsname __user *);
asmlinkage long sys_olduname(struct oldold_utsname __user *);
いくつか見つかりますね。sys_のprefixはシステムコールを表しています。名前からsys_newuname、sys_unameのどちらかだろうと想像できますね。ファイルを実際に読んで確認しましょう。まずはそのままな名前のsys_unameを見てみます。
asmlinkage long sys_gethostname(char __user *name, int len);
asmlinkage long sys_uname(struct old_utsname __user *);
asmlinkage long sys_olduname(struct oldold_utsname __user *);
obsoleteってコメントがありますね。こちらではなさそうです。ではsys_newunameはどうでしょうか。こちらは特にコメントもありません。uname(2)の実行時はsys_newuname()が実行されるようですね。
asmlinkage long sys_setpriority(int which, int who, int niceval);
~略~
asmlinkage long sys_newuname(struct new_utsname __user *name);
コメントにkernel/sys.cとあるので実装はこのファイルにあるようです。では、このファイルも見てみましょう。SYSCALL_DEFINE1というのはシステムコールの関数に使われるマクロです。SYSCALL_DEFINE1の1は引数を一つ受け取るという意味です。実装は比較的単純ですね。
SYSCALL_DEFINE1(newuname, struct new_utsname __user *, name)
{
struct new_utsname tmp;
down_read(&uts_sem);
memcpy(&tmp, utsname(), sizeof(tmp));
up_read(&uts_sem);
if (copy_to_user(name, &tmp, sizeof(tmp)))
return -EFAULT;
if (override_release(name->release, sizeof(name->release)))
return -EFAULT;
if (override_architecture(name))
return -EFAULT;
return 0;
}
ここまででユーザー空間でuname(2)を実行するとカーネル空間のsys_newuname()が実行されることがわかりました。前置きが長くなりましたが実際にbfptrace(1)を使って見ましょう。まずは単純にsys_newutsname()の実行をトレースします。この関数が実行されたときに呼び出し元のコマンド名を表示させるのが以下のコマンドラインです。
[masami@unamebook ~]$ sudo bpftrace -e 'tracepoint:syscalls:sys_enter_newuname { printf("%s\n", comm); }'
コマンドを入力してちょっとすると次のようなメッセージが表示されます。終了したい場合はCtrl-Cで終了できます。
Attaching 1 probe...
メッセージが表示されたら別の端末からuname -aを実行すると以下のようにunameが3回表示されます。uname -aを実行するとuname(2)が3回実行されるというのはstrace(1)の章で調べましたね。
[masami@unamebook ~]$ sudo bpftrace -e 'tracepoint:syscalls:sys_enter_newuname { printf("%s\n", comm); }'
Attaching 1 probe...
uname
uname
uname
^C
systemtapはbpftrace同様にカーネルのトレーシングを行うことができるツールです。systemtapを応用するとトレース以上のこともできます。まずはインストールしましょう。
[masami@unamebook ~]$ sudo dnf install -y systemtap
systemtapはstap(1)がコマンドラインのプログラムとなります。systemtapは専用のスクリプト言語があり、c言語のコードを埋め込むこともできます。本章ではsystemtapを使ってsys_newutsname()の挙動を変更してみましょう。uname(1)で表示するノード名などはutsname()から取得しています。utsname()の戻り値をtmp変数に一旦保存し、それをcopy_to_user()でユーザー空間のプログラムから渡されたnameにコピーします。
SYSCALL_DEFINE1(newuname, struct new_utsname __user *, name)
{
struct new_utsname tmp;
down_read(&uts_sem);
memcpy(&tmp, utsname(), sizeof(tmp));
up_read(&uts_sem);
if (copy_to_user(name, &tmp, sizeof(tmp)))
return -EFAULT;
if (override_release(name->release, sizeof(name->release)))
return -EFAULT;
if (override_architecture(name))
return -EFAULT;
return 0;
}
では、systemtapを使ってプログラムにnodenameを書き換えてみましょう。systemtapを利用したライブパッチ機能の実装という感じです。systemtapのプログラムは次のようになります。
#!/usr/bin/env stap
%{
#include <linux/string.h>
#include <linux/uaccess.h>
#include <uapi/linux/utsname.h>
%}
function set_dummy_uname:long(name:long)
%{
struct new_utsname u = { 0 };
if (copy_from_user(&u, (struct new_utsname *) STAP_ARG_name, sizeof(u)))
STAP_RETURN(-EFAULT);
strcpy(u.nodename, "livepatched");
if (copy_to_user((struct new_utsname *) STAP_ARG_name, &u, sizeof(u)))
STAP_RETURN(-EFAULT);
STAP_RETURN(0);
%}
probe begin {
printf("start livepatch code. Ctrl-C to stop\n")
}
probe kernel.function("__do_sys_newuname").return {
// Only set dummy nodename to test program
if (execname() == "uname-test") {
// Use @entry() to get name parameter in return probe.
set_dummy_uname(@entry($name))
}
}
probe end {
printf("Done\n")
}
このプログラムではシステムコールのreturn時に処理を入れています。まずシステムコールを呼び出したプログラム名を調べ、テストプログラムでなければ特に変更は行わないで終了します。nodenameを設定しているのはset_dummy_uname()で呼び出し時にシステムコールのパラメータとして渡されたname変数をset_dummy_uname()の引数として渡しています。システムコールを呼び出したのがテストプログラムならnodenameをlivepatchedに変更します。systamtapスクリプトの特徴として、自分で実装した関数の引数にアクセスする場合はSTAP_ARG_変数名でアクセスします。このスクリプトではnameという変数名で引数を受け取っているためSTAP_ARG_nameと書いています。
ここで使用するテストプログラムですが以下のような簡単なものです。
#include <stdio.h>
#include <sys/utsname.h>
int main(int argc, char **argv)
{
struct utsname name = { 0 };
if (uname(&name)) {
perror("uname");
return -1;
}
printf("nodename: %s\n", name.nodename);
return 0;
}
このcコードをコンパイルしましょう。
[masami@unamebook ~]$ gcc uname-test.c -o uname-test
では、このsystemtapのスクリプトを実行しましょう。ここでも端末を2つ使います。1つはuname-testプログラムを実行します。もう1つは次のようにsystemtapのスクリプトを実行します。start livepatch ~と出たら準備完了です。
[masami@unamebook ~]$ sudo stap -g uname_livepatch.stp
start livepatch code. Ctrl-C to stop
別の端末でテストします。
[masami@unamebook ~]$ uname -n
unamebook
[masami@unamebook ~]$ ./uname-test
nodename: livepatched
[masami@unamebook ~]$ uname -n
unamebook
[masami@unamebook ~]$
次にsystemtapのプログラムをCtrl-Cで止めてuname-testを実行してみます。systemtapのスクリプトを終了したことでsys_newuname()の挙動がもとに戻ったことでuname(2)の戻り値がもとに戻りましたね。
[masami@unamebook ~]$ ./uname-test
nodename: unamebook
Kernel Hack
前章のsystemtapではsystemtapを利用してカーネルの挙動を変えてみました。本章ではカーネルのソースコードを変更し、前章と同様の挙動に変えてみます。カーネルのソースパッケージにパッチを当てるにはgit-am(1)で当てることができるパッチを作る必要があります。パッチの作り方としてメインラインのカーネルを変更してパッチをつくるかFedoraのカーネルソースを変更するの2パターンがあります。FedoraのカーネルにはFedoraプロジェクトが適用したパッチも含まれているため、メインラインのカーネルを変更した場合、変更内容によってはFedoraのカーネルソースとコンフリクトが発生する可能性もあります。そこで本章ではFedoraのカーネルソースを変更してパッチを作ります。まずはカーネルのソースパッケージをダウンロードしてインストールします。以前coreutilsのソースパッケージをインストールしているので綺麗なrpmbuildディレクトリを作りたいところです。よって、既存のrpmbuildディレクトリはリネームして置いておき、新たにrpmbuildディレクトリを作成します。そしてソースパッケージをダウンロードしてインストールします。
[masami@unamebook ~]$ dnf download --source kernel
[masami@unamebook ~]$ rpm -i kernel-5.3.11-300.fc31.src.rpm
カーネルパッケージのビルドに必要な依存パッケージのインストールを行います。
[masami@unamebook ~]$ sudo dnf builddep -y kernel
[masami@unamebook ~]$ sudo dnf install -y pesign
これでカーネルソースパッケージをビルドする準備は整ったのでですが、specファイルにあるbuildidを設定してFedoraのrpmと区別できるようにします。お好みのエディタでkernel.specを開き# define buildid .localの下に%define buildid .unametestを追加します。
# define buildid .local
%define buildid .unametest
この段階ではまだ~/rpmbuild/SOURCESにカーネルのコードやパッチが置かれているだけなのでソースコードを展開します。
[masami@unamebook SPECS]$ rpmbuild -bp kernel.spec
展開に成功すると~/rpmbuild/BUILD/kernel-5.3.fc31/linux-5.3.11-300.unametest.fc31.x86_64/にパッチが当てられたカーネルのコードが置かれます。また通常のカーネルソースツリーとは違いconfigsディレクトリが存在します。このディレクトリにはfedoraの各アーキテクチャ向けのconfigファイルが存在します。rpmbuildではなく、ローカル環境でビルドを試す際はこのディレクトリにあるconfigを使用してmake oldconfigを行うか、使用しているカーネルのコンフィグファイルが/bootにあるのでそれをコピーして使いましょう。
では、ここからはカーネルのソースコードを弄っていきます。まずは~/rpmbuild/BUILD/kernel-5.3.fc31に移動し、 linux-5.3.11-300.unametest.fc31.x86_64をコピーして作業ディレクトリを作ります。コピー先の名前は任意です。ここでは linux-5.3.11-300.unametest.fc31.x86_64.hackとしました。
[masami@unamebook kernel-5.3.fc31]$ cp -a linux-5.3.11-300.unametest.fc31.x86_64 linux-5.3.11-300.unametest.fc31.x86_64.hack
一旦ソースをいじっていない状態でビルドができることを確認しましょう。この段階でビルドに成功することを確認しておけば、ソースの変更後にビルドが通らなくなった場合の問題の切り分けに役立ちます。この段階でgitのブランチも作っておきます。 linux-5.3.11-300.unametest.fc31.x86_64には.gitディレクトリがあり、gitがすでに利用できるようになっています。
[masami@unamebook kernel-5.3.fc31]$ cd linux-5.3.11-300.unametest.fc31.x86_64.hack/
[masami@unamebook linux-5.3.11-300.unametest.fc31.x86_64.hack]$ git checkout -b uname-hack
[masami@unamebook linux-5.3.11-300.unametest.fc31.x86_64.hack]$ cp configs/kernel-5.3.11-x86_64.config .config
[masami@unamebook linux-5.3.11-300.unametest.fc31.x86_64.hack]$ make oldconfig
scripts/kconfig/conf --oldconfig Kconfig
#
# configuration written to .config
#
make oldconfigは特に問題なく完了すると思います。では、カーネルとカーネルモジュールをビルドします。bzImageで圧縮されたカーネルのイメージをビルドし、modulesでカーネルモジュールをビルドしています。-j$(nproc)は並列ビルドのオプションです。ログインしている環境で利用可能なcpu数をもとにビルドを並列実行します。カーネルのビルドは最後にarch/x86/boot/bzImage is readyというメッセージが出れば成功です。rpmのソースパッケージを使ってるのでrpmbuildでビルドしても良いのですが、それだと時間がかかるのでまずは手軽にmakeでビルドしています。
[masami@unamebook linux-5.3.11-300.unametest.fc31.x86_64.hack]$ make -j$(nproc) bzImage
~略~
BUILD arch/x86/boot/bzImage
Setup is 17692 bytes (padded to 17920 bytes).
System is 9085 kB
CRC dde74ef3
Kernel: arch/x86/boot/bzImage is ready (#1)
モジュールのビルドも実行します。モジュールのビルドは成功時にこれといったログはありません。次のようにしれっとプロンプトが表示されていたら成功しています。
[masami@unamebook linux-5.3.11-300.unametest.fc31.x86_64.hack]$ make -j$(nproc) modules
~略~
LD [M] sound/x86/snd-hdmi-lpe-audio.ko
LD [M] virt/lib/irqbypass.ko
さて、カーネルとモジュールのビルドに成功したら準備は完了です。実際にカーネルのソースに手を加えましょう。変更するのはkernel/sys.cです。カレントプロセス名を調べてプロセス名がuname-testならnodenameを変更します。やっていることはsystemtapの場合と同様です。変更内容は次のようになります。
diff --git a/kernel/sys.c b/kernel/sys.c
index 2969304..908ce7f 100644
--- a/kernel/sys.c
+++ b/kernel/sys.c
@@ -1243,6 +1243,10 @@ SYSCALL_DEFINE1(newuname, struct new_utsname __user *, name)
down_read(&uts_sem);
memcpy(&tmp, utsname(), sizeof(tmp));
up_read(&uts_sem);
+
+ if (!strcmp(current->comm, "uname-test"))
+ strcpy(tmp.nodename, "livepatched");
+
if (copy_to_user(name, &tmp, sizeof(tmp)))
return -EFAULT;
systemtapの場合は関数の途中にコードを入れていないので関数から返るところに処理を入れ込みましたが、今回は直接カーネルのコードを弄っているので素直なコードの変更になってますね。変更したらカーネルがビルドできるか確認しましょう。ビルド方法は先ほどと同じです。
[masami@unamebook linux-5.3.11-300.unametest.fc31.x86_64.hack]$ make -j$(nproc) bzImage
~略~
BUILD arch/x86/boot/bzImage
Setup is 17692 bytes (padded to 17920 bytes).
System is 9089 kB
CRC 27223b7c
Kernel: arch/x86/boot/bzImage is ready (#2)
今回も成功したらarch/x86/boot/bzImage is readyというメッセージが出ます。このメッセージの最後にある#2というのはこのカーネルをビルドしたのが2回目という意味です。ビルドに成功したのでパッチを作成しましょう。まず最初にgitのメールアドレスと名前を変更します。これはFedoraのカーネルの場合、デフォルトではFedoraのカーネルチームのメールアドレスが使われるので上書きして自分のメールアドレスと名前に変更します。そうしたら変更をコミットし、git-format-patch(1)でパッチを作ります。出来たパッチはカレントディレクトリに置かれます。
[masami@unamebook linux-5.3.11-300.unametest.fc31.x86_64.hack]$ git config --local --add user.email <your email>
[masami@unamebook linux-5.3.11-300.unametest.fc31.x86_64.hack]$ git config --local --add user.name "your name"
[masami@unamebook linux-5.3.11-300.unametest.fc31.x86_64.hack]$ git add kernel/sys.c
[masami@unamebook linux-5.3.11-300.unametest.fc31.x86_64.hack]$ git commit -m "uname: add test code"
[uname-hack 9ea2b59] uname: add test code
1 file changed, 4 insertions(+)
[masami@unamebook linux-5.3.11-300.unametest.fc31.x86_64.hack]$ git format-patch -s master
0001-uname-add-test-code.patch
[masami@unamebook linux-5.3.11-300.unametest.fc31.x86_64.hack]$
ここで出来たパッチは次のようになります。コミットメッセージ、変更内容、メール送信用のサブジェクトなどが設定されています。最後の2.23.0というのはgitのバージョンです。
From 9ea2b595c1c246a7ff94e6c4c721896ffcf9f5a6 Mon Sep 17 00:00:00 2001
From: Your Name <your email>
Date: Mon, 25 Nov 2019 23:26:46 +0900
Subject: [PATCH] uname: add test code
Signed-off-by:Your Name <your email>
---
kernel/sys.c | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/kernel/sys.c b/kernel/sys.c
index 2969304..908ce7f 100644
--- a/kernel/sys.c
+++ b/kernel/sys.c
@@ -1243,6 +1243,10 @@ SYSCALL_DEFINE1(newuname, struct new_utsname __user *, name)
down_read(&uts_sem);
memcpy(&tmp, utsname(), sizeof(tmp));
up_read(&uts_sem);
+
+ if (!strcmp(current->comm, "uname-test"))
+ strcpy(tmp.nodename, "livepatched");
+
if (copy_to_user(name, &tmp, sizeof(tmp)))
return -EFAULT;
--
2.23.0
では、このパッチをFedoraのカーネルソースパッケージに追加してrpmbuild(8)でビルドしましょう。パッチを~/rpmbuild/SOURCESディレクトリにコピーします。
[masami@unamebook linux-5.3.11-300.unametest.fc31.x86_64.hack]$ cp 0001-uname-add-test-code.patch ../../../SOURCES/.
そして~/rpmbuild/SPECSに移動します。kernel.specファイルをエディタで開き、# END OF PATCH DEFINITIONSの行まで移動します。PatchXXX(Xは数字)の行がありますが、この数字は任意です。ここでは999としました。次のように# END OF PATCH DEFINITIONSの上に2行を追加します。
# uname hack patch
Patch999: 0001-uname-add-test-code.patch
# END OF PATCH DEFINITIONS
そうしたらビルドしましょう。ここでは必要最小限のカーネルパッケージだけ作成します。次のようなコマンドライン引数でビルドを行います。
[masami@unamebook SPECS]$ rpmbuild -bb --with baseonly --without debuginfo --without debug --target=$(uname -m) kernel.spec
~略~
Executing(%clean): /bin/sh -e /var/tmp/rpm-tmp.0BA8HX
+ umask 022
+ cd /home/masami/rpmbuild/BUILD
+ cd kernel-5.3.fc31
+ /usr/bin/rm -rf /home/masami/rpmbuild/BUILDROOT/kernel-5.3.11-300.unametest.fc31.x86_64
+ RPM_EC=0
++ jobs -p
+ exit 0
出来上がったrpmファイルは~ rpmbuild/RPMS/x86_64/に置かれます。
[masami@unamebook x86_64]$ ls
kernel-5.3.11-300.unametest.fc31.x86_64.rpm kernel-devel-5.3.11-300.unametest.fc31.x86_64.rpm kernel-modules-extra-5.3.11-300.unametest.fc31.x86_64.rpm
kernel-core-5.3.11-300.unametest.fc31.x86_64.rpm kernel-modules-5.3.11-300.unametest.fc31.x86_64.rpm
出来上がったrpmをインストールします。
[masami@unamebook x86_64]$ sudo dnf localinstall -y ./*.rpm
インストールしたらカーネルの優先順を確認しましょう。通常はインストールしたカーネルで起動できると思いますが、出来なかった場合はここで調べたindexを使ってgrub2-rebootコマンドで起動するカーネルを設定してリブートします。
[masami@unamebook ~]$ sudo grubby --info=ALL
index=0
kernel="/boot/vmlinuz-5.3.11-300.unametest.fc31.x86_64"
args="ro resume=UUID=c98315dc-7144-427b-a6a0-2aad53d8ebc5 rhgb quiet"
root="UUID=0b584087-de7b-42f8-8dea-d3b679d22613"
initrd="/boot/initramfs-5.3.11-300.unametest.fc31.x86_64.img"
title="Fedora (5.3.11-300.unametest.fc31.x86_64) 31 (Thirty One)"
id="24b7d4741c7c4982889ac4d4acb27ae1-5.3.11-300.unametest.fc31.x86_64"
再起動したらuname(1)でカーネルを確認しましょう。unametestが入っているのでビルドしたカーネルで正しく起動したかわかりますね。
[masami@unamebook ~]$ uname -a
Linux unamebook 5.3.11-300.unametest.fc31.x86_64 #1 SMP Mon Nov 25 23:32:51 JST 2019 x86_64 x86_64 x86_64 GNU/Linux
そして、systemtapのときにも使ったuname-testを実行してみましょう。
[masami@unamebook ~]$ ./uname-test
nodename: livepatched
nodenameが変わりましたね。ここでuname-testをuname-test2としてコピーして実行すると、オリジナルのnodenameがでましたね。ということでカーネルに手を入れてuname(2)の挙動を変更することにも成功しました🎉
[masami@unamebook ~]$ cp uname-test uname-test2
[masami@unamebook ~]$ ./uname-test2
nodename: unamebook
Livepatch
systemtapの章ではsystemtapを利用してカーネルの挙動を変えてみましたが、LinuxカーネルにはLivepatchという機能があり、実行中のカーネルの挙動を再起動なしに変更する(patchを当てる)ことができる機能があります。本章ではこの機能でunameシステムコールの挙動を変更してみます。ただしFedoraのカーネルではLivepatch機能は有効になっていないためカーネルのソースパッケージよりLivepatchを有効にしたカーネルを作る必要があります。カーネルのソースは前章でダウンロードしてきてあるのでこれに対してコードを弄っていきましょう。ここで本当はunameの挙動を変更するのがベターなところですが、unameの実装をLivepatchするのはちょっと手間なので単純に弄ることができるgetpid(2)を使います。
まず~/rpmbuild/SOURCESに移動し、Livepatchの機能を有効にする設定を追加します。
[masami@unamebook ~]$ cd rpmbuild/SOURCES/
[masami@unamebook SOURCES]$ echo "CONFIG_LIVEPATCH=y" >> kernel-local
[masami@unamebook SOURCES]$ echo "CONFIG_TEST_LIVEPATCH=m" >> kernel-local
kernel-localファイルはこのようになります。
[masami@unamebook SOURCES]$ cat kernel-local
# This file is intentionally left empty in the stock kernel. Its a nicety
# added for those wanting to do custom rebuilds with altered config opts.
CONFIG_LIVEPATCH=y
CONFIG_TEST_LIVEPATCH=m
Fedoraではkernel-localファイルにカーネルのコンフィギュレーションを設定することでベースとなる設定ファイル(x86_64ならkernel-x86_64.config)に設定を追加できます。
設定を追加したら~/rpmbuild/SPECに移動し依存パッケージのインストールを行います。kernel-localファイルを作ったらあとはビルドするだけです。
[masami@unamebook SOURCES]$ cd ~/rpmbuild/SPECS/
先程はunametestという名称をカーネルパッケージに追加しましたが、今度はgetpidtestという名称を付けましょう。kernel.specを以下のように変更します。
# define buildid .local
%define buildid .getpidtest
specファイルを変更したらカーネルをビルドします。
[masami@unamebook SPECS]$ rpmbuild -bb --with baseonly --without debuginfo --without debug --target=$(uname -m) kernel.spec
~略~
+ cd kernel-5.3.fc31
+ /usr/bin/rm -rf /home/masami/rpmbuild/BUILDROOT/kernel-5.3.11-300.getpidtest.fc31.x86_64
+ RPM_EC=0
++ jobs -p
+ exit 0
ビルドが終了したら~/rpmbuild/RPMS/x86_64に移動してlsするとgetpidtestと名前の付いたrpmが出来ているのが確認できます。
[masami@unamebook x86_64]$ ls
kernel-5.3.11-300.getpidtest.fc31.x86_64.rpm kernel-core-5.3.11-300.unametest.fc31.x86_64.rpm kernel-modules-5.3.11-300.getpidtest.fc31.x86_64.rpm kernel-modules-extra-5.3.11-300.unametest.fc31.x86_64.rpm
kernel-5.3.11-300.unametest.fc31.x86_64.rpm kernel-devel-5.3.11-300.getpidtest.fc31.x86_64.rpm kernel-modules-5.3.11-300.unametest.fc31.x86_64.rpm
kernel-core-5.3.11-300.getpidtest.fc31.x86_64.rpm kernel-devel-5.3.11-300.unametest.fc31.x86_64.rpm kernel-modules-extra-5.3.11-300.getpidtest.fc31.x86_64.rpm
新しくビルドしたrpmをインストールします。
[masami@unamebook x86_64]$ sudo dnf localinstall -y kernel-*.getpidtest.*
次にgrubのエントリを確認します。
[masami@unamebook x86_64]$ sudo grubby --info=ALL | grep index -A1
index=0
kernel="/boot/vmlinuz-5.3.11-300.unametest.fc31.x86_64"
--
index=1
kernel="/boot/vmlinuz-5.3.11-300.getpidtest.fc31.x86_64"
--
index=2
kernel="/boot/vmlinuz-5.3.11-300.fc31.x86_64"
--
index=3
kernel="/vmlinuz-0-rescue-24b7d4741c7c4982889ac4d4acb27ae1"
getpidtestのカーネルはindexが1となっているのでこのカーネルで起動するようにします。
[masami@unamebook x86_64]$ sudo grub2-reboot 1
これで準備完了なので再起動します。再起動したらもちろんuname(1)で正しいカーネルで起動したか確認します😄
[masami@unamebook ~]$ uname -a
Linux unamebook 5.3.11-300.getpidtest.fc31.x86_64 #1 SMP Tue Nov 26 23:10:42 JST 2019 x86_64 x86_64 x86_64 GNU/Linux
カーネルビルド時にライブパッチ機能が有効になっていたか確認します。fedoraは/bootにカーネルのコンフィグが置かれるのでこれで確認できます。
[masami@unamebook ~]$ grep CONFIG_LIVEPATCH /boot/config-5.3.11-300.getpidtest.fc31.x86_64
CONFIG_LIVEPATCH=y
ではライブパッチを作りましょう。カーネルのライブパッチはカーネルモジュールとして作成します。まず作業ディレクトリを作ります。
[masami@unamebook ~]$ mkdir getpid-livepatch-module
そして、getpid-livepatch.cを以下の内容で作成します。your nameのところは適当に変えてください。
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/livepatch.h>
#include <linux/string.h>
#include <linux/sched.h>
MODULE_DESCRIPTION("getpid livepatch test module");
MODULE_AUTHOR("your name");
MODULE_LICENSE("GPL");
MODULE_INFO(livepatch, "Y");
asmlinkage long livepatch_getpid(void)
{
pr_info("%s\n", __func__);
if (unlikely(!strcmp(current->comm, "getpid-test")))
return 1;
return task_tgid_vnr(current);
}
static struct klp_func funcs[] = {
{
.old_name = "__x64_sys_getpid",
.new_func = livepatch_getpid,
}, {}
};
static struct klp_object objs[] = {
{
.funcs = funcs,
}, {}
};
static struct klp_patch patch = {
.mod = THIS_MODULE,
.objs = objs,
};
static int getpid_livepatch_init(void)
{
int ret = 0;
ret = klp_enable_patch(&patch);
if (ret) {
pr_info("failed to enable getpid live patch\n");
return ret;
}
pr_info("getpid livepatch test module is enabled\n");
return ret;
}
static void getpid_livepatch_cleanup(void)
{
}
module_init(getpid_livepatch_init);
module_exit(getpid_livepatch_cleanup);
このライブパッチではgetpid(2)を実行したプログラム名がgetpid-testの場合に1を返し、それ以外の場合は普通にpidを返しています。このファイルをビルドするのにMakefileが必要ですのでこれも作成します。Makefileのインデントはtabなんで気をつけてください。
KERNDIR := /lib/modules/`uname -r`/build
BUILD_DIR := $(shell pwd)
VERBOSE = 0
obj-m := getpid-livepatch.o
smallmod-objs := getpid-livepatch.o
all:
make -C $(KERNDIR) M=$(BUILD_DIR) KBUILD_VERBOSE=$(VERBOSE) modules
clean:
rm -f *.o
rm -f *.ko
rm -f *.mod.c
rm -f *~
ビルドします。ビルドに成功するとgetpid-livepatch.koというファイルができます。
[masami@unamebook getpid-livepatch-module]$ make
make -C /lib/modules/`uname -r`/build M=/home/masami/getpid-livepatch-module KBUILD_VERBOSE=0 modules
make[1]: Entering directory '/usr/src/kernels/5.3.11-300.getpidtest.fc31.x86_64'
CC [M] /home/masami/getpid-livepatch-module/getpid-livepatch.o
Building modules, stage 2.
MODPOST 1 modules
CC /home/masami/getpid-livepatch-module/getpid-livepatch.mod.o
LD [M] /home/masami/getpid-livepatch-module/getpid-livepatch.ko
make[1]: Leaving directory '/usr/src/kernels/5.3.11-300.getpidtest.fc31.x86_64'
次にテストプログラムを用意しましょう。getpid-test.cという名前でファイルを作り、以下の内容で保存します。
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc, char **argv)
{
printf("pid is %d\n", getpid());
return 0;
}
ビルドします。
[masami@unamebook getpid-livepatch-module]$ gcc getpid-test.c -o getpid-test
実行すると以下のようにpidが表示されます。
[masami@unamebook getpid-livepatch-module]$ ./getpid-test
pid is 1435
では、ライブパッチのモジュールをロードします。
[masami@unamebook getpid-livepatch-module]$ sudo insmod ./getpid-livepatch.ko
dmesgコマンドでカーネルのログを見ると以下のような行があるのが確認できます。
[ 959.659727] getpid_livepatch: loading out-of-tree module taints kernel.
[ 959.659729] getpid_livepatch: tainting kernel with TAINT_LIVEPATCH
[ 959.659787] getpid_livepatch: module verification failed: signature and/or required key missing - tainting kernel
[ 959.668116] livepatch: enabling patch 'getpid_livepatch'
[ 959.670655] livepatch: 'getpid_livepatch': starting patching transition
[ 959.670802] getpid_livepatch: getpid livepatch test module is enabled
ただ、livepatch_getpid()の最初にpr_info()で関数名を表示させているので以下のようなログが大量に出てると思いますが。
[ 1042.927756] getpid_livepatch: livepatch_getpid
それはさておきgetpid-testを再度実行するとpidとして1が返ってきます。ということでカーネルのライブパッチ機能を使ったライブパッチも成功です🎉
[masami@unamebook getpid-livepatch-module]$ ./getpid-test
pid is 1
プログラム名が違えばもちろんpidは普通に返ってきます。
[masami@unamebook getpid-livepatch-module]$ cp getpid-test getpid-test2
[masami@unamebook getpid-livepatch-module]$ ./getpid-test2
pid is 1455
おまけ
FedoraをupstreamとしているRHELとCentOSのuname(1)の挙動も紹介します。
バージョンは8.1です。これでuname(1)を実行するとuname(2)が3回呼ばれているのがわかりますね。
[masami@rhel8 ~]$ cat /etc/os-release | grep VERSION_ID
VERSION_ID="8.1"
[masami@rhel8 ~]$ strace -v -s 1024 -e trace=uname -C uname -a
uname({sysname="Linux", nodename="rhel8", release="4.18.0-147.0.3.el8_1.x86_64", version="#1 SMP Mon Nov 11 12:58:36 UTC 2019", machine="x86_64", domainname="(none)"}) = 0
uname({sysname="Linux", nodename="rhel8", release="4.18.0-147.0.3.el8_1.x86_64", version="#1 SMP Mon Nov 11 12:58:36 UTC 2019", machine="x86_64", domainname="(none)"}) = 0
uname({sysname="Linux", nodename="rhel8", release="4.18.0-147.0.3.el8_1.x86_64", version="#1 SMP Mon Nov 11 12:58:36 UTC 2019", machine="x86_64", domainname="(none)"}) = 0
Linux rhel8 4.18.0-147.0.3.el8_1.x86_64 #1 SMP Mon Nov 11 12:58:36 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
+++ exited with 0 +++
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
100.00 0.000068 22 3 uname
------ ----------- ----------- --------- --------- ----------------
100.00 0.000068
coreutilsのバージョンは8.30です。
[masami@rhel8 ~]$ rpm -qi coreutils | grep -e Version -e Release
Version : 8.30
Release : 6.el8
CentOSのバージョンは8です。こちらもuname(2)が3回呼ばれてますね。
masami@centos8 ~]$ cat /etc/os-release | grep VERSION_ID
VERSION_ID="8"
[masami@centos8 ~]$ strace -v -s 1024 -e trace=uname -C uname -a
uname({sysname="Linux", nodename="centos8", release="4.18.0-80.11.2.el8_0.x86_64", version="#1 SMP Tue Sep 24 11:32:19 UTC 2019", machine="x86_64", domainname="(none)"}) = 0
uname({sysname="Linux", nodename="centos8", release="4.18.0-80.11.2.el8_0.x86_64", version="#1 SMP Tue Sep 24 11:32:19 UTC 2019", machine="x86_64", domainname="(none)"}) = 0
uname({sysname="Linux", nodename="centos8", release="4.18.0-80.11.2.el8_0.x86_64", version="#1 SMP Tue Sep 24 11:32:19 UTC 2019", machine="x86_64", domainname="(none)"}) = 0
Linux centos8 4.18.0-80.11.2.el8_0.x86_64 #1 SMP Tue Sep 24 11:32:19 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
+++ exited with 0 +++
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
100.00 0.000013 4 3 uname
------ ----------- ----------- --------- --------- ----------------
100.00 0.000013 3 total
coreutilsのバージョンはこちらも8.30です。
[masami@centos8 ~]$ rpm -qi coreutils | grep -e Version -e Release
Version : 8.30
Release : 6.el8
まとめ
最後はgetpid(2)を使いましたが、主にuname(1)とuname(2)を使ってユーザーランドでのデバッグからカーネルを弄るところまでを行いました(_´Д`)ノ~~オツカレー