unameコマンドから始めるデバッグ&カーネルハック入門

この記事はLinux Advent Calendar 2019の1日目の記事です。

はじめに

本記事ではLinuxサーバのホスト名、Linuxカーネルのバージョン、cpuアーキテクチャなどのシステム情報を表示するuname(1)を利用してLinux環境でのデバッグカーネルハックについて説明していきます。本記事ではコマンドやツールの使い方の説明ではなくて、それらを使ってどのようにデバッグするのかというところを説明します。

環境

ディストリビューションにはFedora 31(x86_64)を利用します。動作環境はQEMUlibvirtOracle 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)

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の出力内容」です。

出力内容 該当するunameコマンドのオプション 意味
Linux -s カーネル
unamebook -n ホスト名
5.3.11-300.fc31.x86_64 -r カーネルリリース
#1 SMP Tue Nov 12 19:08:07 UTC 2019 -v カーネルバージョン*2
x86_64 -m マシンのハードウェア名
x86_64 -p プロセッサ名
x86_64 -i ハードウェアプラットフォーム
GNU/Linux -o オペレーティングシステム

表1 unameの出力内容

uname(1)が出力する内容

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です。grepuname(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箇所はfedoracoreutilsパッケージが独自当てているpatchです。このpatchは~/rpmbuild/SOURCES/coreutils-8.2-uname-processortype.patchです。興味のある方は確認してみてください。

gdb(1)

ソースコードを読んで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の略でPythonc言語を使ってeBFPを利用するプログラムを作ることが出来ます。そしてbpftraceですがBCCをバックエンドとして使った高レベルのトレース用言語と説明されています。bpftraceではカーネルレベルのトレースを取ることが出来ます。Fedoraにはパッケージがあるのでインストールしましょう。

[masami@unamebook ~]$ sudo dnf install -y bpftrace

インストールが完了したらuname(2)のトレースを行ってみましょう。まずはシステムコールの呼び出しをトレースします。 この他に、カーネルのソースも読みたいのでこちらもインストールしましょう。今回はdebuginfo-installコマンドを使用してデバッグ情報とソースをインストールします。coreutilsのときは単にソースを取得したいだけだったのでsrpmパッケージをインストールしましたが、debuginfoパッケージの場合はデバッグ情報とソースコードをインストールできます。gdb(1)の章でcoreutilsglibcの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/

まずはunamegrepをかけましょう。

[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を見てみます。

/* obsolete: kernel/sys.c */
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

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を設定してFedorarpmと区別できるようにします。お好みのエディタで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としているRHELCentOSuname(1)の挙動も紹介します。

RHEL 8.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

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)を使ってユーザーランドでのデバッグからカーネルを弄るところまでを行いました(_´Д`)ノ~~オツカレー

Raspberry Piで学ぶコンピュータアーキテクチャ (Make:PROJECTS)

Raspberry Piで学ぶコンピュータアーキテクチャ (Make:PROJECTS)

*1:1というのはmanセクションの番号です。1は通常のコマンドです。

*2:カーネルバージョンにある#1というのはビルド環境にてビルドした回数です。

*3:システムコールのmanセクションは2です

Fedora Kernel test week参加記録

FedoraではkernelのテストイベントとかGnome 3.34 Test DayI18N Test Day などのテストイベントがちょくちょくあります。 今回はKernel 5.3 Test Weekの備忘録です。ちなみに、この手のイベントはfedora MAGAZINEで紹介されることがあります。

今回の参加結果の最終成果はこちらです。

github.com

参加ε≡≡ヘ( ´Д`)ノ

今回のtest weekは開催時期が9月末から10月の最初の週でちょうどfedora 31のリリースが近かったころです。当時のfedora 29と30はカーネルは5.2系が使われていて、fedora 31が5.3系って感じでした。メインラインは5.4のrc1が出てた辺りです。それでfedora 31もそろそろ出るし、ベータとかrcのテストも参加してなかったし、メインラインのカーネルも最近使ってないなーなんてことでテストに参加しとくかーって感じで参加したわけです。

バグ発見/(^o^)\

テストに使うカーネルrpmをデスクトップ環境のfedora 30マシンにインストールして再起動すると、本来GDMのログイン画面がでるところでスクリーンがブリンクし続ける状態でした。一応メインラインのカーネル(5.4-rc1)も試してみたけど結果は同じでした。ここまでで5.3〜で症状が出るらしいってことは確定。というわけでここまでの内容で一旦bugzillaにバグレポートしました。

1758831 – GDM login screen doesn't show up on kernel 5.3.2

その後ですが、じゃあ、fedora 31ならどうよ?と思ってテスト用にビルドされたライブデスクトップのisoイメージを使ってみたところこちらも同様の現象。ただブートメニューからTroubleshooting" -> "Start kernel in basic graphic mode"を選ぶと問題なし。間違いなくGPU関連のバグですねって感じです。この情報もbugzillaに追記しました。

debug🐝

一応ここまででバグを見つけた人として一応のことはしたのですが、自分のPCで新しいカーネルが使えないのは最高に困るのでここからはbisectして原因を探りました。バグに当たっている人がテスト時点では自分くらいしかいなかったし、他のディストリビューションを見てもArch Linuxで同じような現象の人が1人くらいいたかなーって感じでした。そうするとバグは機種依存っぽいし自分でデバッグしたほうが良さげだったんですね。

ということでbisectを開始したわけですがバグが実機じゃないと再現できないのでダラダラやりつつ土曜日の朝から夕方まで使った気がします。そして、first bad commitは36a0f92020dc8794d3aa69b7fb4c5d2bf99b0099というのがわかりました。ちなみにこのpatchは一連のシリーズの中の一つなのと、このコミット以降でリファクタリングされていてファイルのパスやヘルパー関数が追加されています。さて、差分ですがpatchだけ見るとわかりにくいので自分の環境で動かなくなった原因のsanitize_aux_ch()より主要部分のbefore/afterを示すとこうなります。

before

       const struct ddi_vbt_port_info *info =
        &dev_priv->vbt.ddi_port_info[port];

    for (p = PORT_A; p < I915_MAX_PORTS; p++) {
        struct ddi_vbt_port_info *i = &dev_priv->vbt.ddi_port_info[p];

        if (p == port || !i->present ||
            info->alternate_aux_channel != i->alternate_aux_channel)
            continue;

        i->supports_dp = false;
        i->alternate_aux_channel = 0;
    }

after

        struct ddi_vbt_port_info *info = &dev_priv->vbt.ddi_port_info[port];

    for (p = PORT_A; p < I915_MAX_PORTS; p++) {
        struct ddi_vbt_port_info *i = &dev_priv->vbt.ddi_port_info[p];

        if (p == port || !i->present ||
            info->alternate_aux_channel != i->alternate_aux_channel)
            continue;

        info->supports_dp = false;
        info->alternate_aux_channel = 0;
    }

大きな違いはfor文中のif文でcontinueしなかった場合に使用している変数です。変更前は配列のp番目の要素をiにセットしてその変数の構造体のデータに値を設定していますが、変更後の方は最初にinfoに設定した値をそのまま使っていてp番目の要素に対する変更は行っていません。そんなわけで、変更前と同様の挙動にしたらどう?と思って作ったのが以下の修正です。

diff --git a/drivers/gpu/drm/i915/display/intel_bios.c b/drivers/gpu/drm/i915/display/intel_bios.c
index efb39f350b19..c886dae82aa7 100644
--- a/drivers/gpu/drm/i915/display/intel_bios.c
+++ b/drivers/gpu/drm/i915/display/intel_bios.c
@@ -1313,6 +1313,8 @@ static void sanitize_aux_ch(struct drm_i915_private *dev_priv,
 
        p = get_port_by_aux_ch(dev_priv, info->alternate_aux_channel);
        if (p != PORT_NONE) {
+               info = &dev_priv->vbt.ddi_port_info[p];
+
                DRM_DEBUG_KMS("port %c trying to use the same AUX CH (0x%x) as port %c, "
                              "disabling port %c DP support\n",
                              port_name(port), info->alternate_aux_channel,
@@ -1330,6 +1332,7 @@ static void sanitize_aux_ch(struct drm_i915_private *dev_priv,
                info->supports_dp = false;
                info->alternate_aux_channel = 0;
        }
+
 }
 
 static const u8 cnp_ddc_pin_map[] = {

このpatchをstableの5.3.2に当てたところちゃんと動くようになりましたワーイヽ(゚∀゚)メ(゚∀゚)メ(゚∀゚)ノワーイ これでとりあえず自分の環境では新しいカーネルでもなんとか動くようになったわけですが、ローカルパッチを当て続けるとかあり得ないですね。なので次はupstreamへの報告となります。

36a0f92020dc8794d3aa69b7fb4c5d2bf99b0099を見るとsanitize_ddc_pin()も同じような変更が入ってるのですが、こちらは自分の環境では影響出てなかったです。GPUわからん😨

freedesktop.orgのbugzillaに苦戦😭

upstreamにバグ報告と言っても報告先として考えられるのはlkml、サブシステムのメーリングリストkernelのbugzilla、またはそれ以外とあります。GPU(i915)のバグ報告先をさがしたところ↓が見つかりました。ここにレポートの仕方が書かれていたのでまずはそれに従います。drm-tipツリーをcloneしてきてこのツリーの最新コードを試したり。。

How to report bugs | 01.org

バグのレポートはfreedesktop.orgのbugzillaを使うということだったのでアカウントを作りました。そしていざレポートってところで色々書いてsubmitボタンを押すと空白ページになるだけでバグが登録できません😨 たまたまバグったのかな?と思ってもう一度試してもダメ、じゃあ時間を置いてと思って次の日に試してもダメ。。。バグ報告したいのにバグ報告できないというバグにハマったわけです\(^o^)/オワタ こんなのわからんよと思ってbugzillaについて聞けるところはないかな?と探したところircで聞けるというのがわかったのでircクライアントをインストールし、freenodeにある#freedesktopに入って状況説明したところ中の人がログを確認したりデバッグを手伝ってくれました。このときのバグレポートはfedoraと同じ感じで書いていたのでタイトルにloginという単語が入っていたのですが、これがバグの原因というのがわかりました。だいたい2時間位色々と試したんじゃないかな。。 ちなみにLoginだと大丈夫で小文字のloginという文字列がバグレポートの新規作成時に入っているとダメという状況でした。既存のバグレポートの編集時だと大丈夫という謎。 それはともかく、手伝ってくれた方には感謝しかありません👃

upstreamへの報告

なんだかんだで無事に報告できました。タイトルについてるbisectedというのはbisect結果を貼ったあとに変更されました。ちゃんと見てくれてますね( ̄ー ̄)bグッ!

111966 – [bisected] GDM Login screen doesn't show up on kernel 5.3 or later

まず普通にバグの報告したあとでbisectの結果やquick hackなパッチをレポートしました。その後、同じような現象の人が現れ、この方も自分が作ったパッチでバグが直ったという報告をしてくれました👃 その後のちゃんとしたパッチの作成までは結構速く進み、コミット36a0f92020dc8794d3aa69b7fb4c5d2bf99b0099の設定方法だとダメな機種があるということでコメント等適切に直したパッチが作成されました。自分ともう一人の方はこのパッチをテストして動くのを確認しました。

drm/i915: Favor last VBT child device with conflicting AUX ch/DDC pin · torvalds/linux@0336ab5 · GitHub

修正方法は自分がやった方法と同じなので自分のパッチも暫定修正とはいえ正しい修正方法にたどり着けててエライって感じです😃

サブシステムへの取り込み -> mainlineへの取り込み -> stable treeへの取り込み待ち

修正パッチがサブシステムのメーリングリストに投げられたのであとは次の3個の待ちとなります。とくに自分がすることはありません。

  1. サブシステムのtreeへの取り込み
  2. mainlineへの取り込み
  3. stable treeへの取り込み

3のときに Patch "drm/i915: Favor last VBT child device with conflicting AUX ch/DDC pin" has been added to the 5.3-stable tree ってメールが届いて5.3.8(5.3.7はすでにリリースされてたので次は5.3.8のときだった)にパッチが入るんだなーってのがわかりました。

freedesktop.orgのbugzillaは1か2のタイミングでcloseされたと思います。

fedoraへの取り込み待ち

新しいstable releaseが出れば fedoraカーネルが最新のstable releaseに追従したときにfedoraカーネルでもバグが直ります。なのでこちらも待ちです(-_-)zzz fedoraのパッケージは最初updates-testingというリポジトリにアップロードされます。そこで人間がテストするか一定期間経てばupdatesリポジトリにパッケージが入ります。で、fedoraカーネル5.3.8のパッケージがアップロードされたので早速テストして結果を書きました。

https://bodhi.fedoraproject.org/updates/FEDORA-2019-0e85bbd15b

あと、fedoraのbugzillaのほうにも5.3.8のカーネルパッケージで直ったという報告を書いておきました。で、こちらもクローズとなりました。

終わり

というわけでkernel test weekへの参加とバグ発見、bisectとローカルパッチの作成、upstreamへの報告とupstreamへの協力、fedoraでの修正確認ということを最近しました( ´ー`)フゥー... 今回一番難しかったのはbugzillaでバグ報告できない問題でしたね。自分の環境で動かせられないし/(^o^)\

超例解Linuxカーネルプログラミング~最先端Linuxカーネルの修正コードから学ぶソフトウェア品質~

超例解Linuxカーネルプログラミング~最先端Linuxカーネルの修正コードから学ぶソフトウェア品質~

雑文:Linuxとか低レイヤが好きで趣味で色々やってたら趣味が仕事になったのと、知識が足りないとこを勉強しようと思って学生にもなった件

このブロクで使ってるカテゴリは↓のようになっていて、こいつLinuxとかカーネル好きだなって感じなんですが、前まではLinux自体は使うけどそれくらいでガッツリとLinuxに絡んだりとかはしてませんでした。

f:id:masami256:20190905215910p:plain

前職はこんな感じのところでnodejsなんかのコード書いてわけですね。 f:id:masami256:20140629161407j:plain

web系っぽい記事もたまに書いてましたよw

qiita.com

とまあ、前職はweb系だったわけですが今はLinuxがっつりな仕事になってます。転職したのは去年の12月ですけどね。Linuxが好きでいろいろやってきて、そこでやったことをblogに書いたりしたわけですが、それも今の仕事で役立つことがあったりとまあ何が役立つかわからないものですね。メモリ管理に興味を持ったり自作OSに興味を持ったり、CTFだったりと低レイヤ周辺をウロウロしてたけど基本的な部分はブレなかったのが良かった気がします。 継続は力なりなんて格言もあるし、続けるってのは大事っすね。とゆーか、ものすごく運が良かったとは思ってます。すごい人達とも知り合うことができたし。

もともとソフトウェアエンジニアになったのも趣味を仕事にしたら一石二鳥じゃね?ってところから仕事にした感じですね。今流行りの未経験からエンジニアになった系ですw もうかなり前だけどw で、そんなことはどうでもよくて、趣味&独学でやってきたので自分の好きな分野はまあわかるけど情報理論とか理論系よくわからん😭という状況だったりというのもありました。じゃあ、ちゃんと勉強しちゃうか?ってことで2019年4月から放送大学の情報コースに全科履修生で入学したりしました。まあ、この辺の決断はかなり軽くて昼に町に出てどのごはん屋さんに行くかを決めるよりは軽い感じで決めてます。会社員兼学生という感じなので履修ペースは遅いけどそこはしょうがない。。。今のところ1学期に4科目くらいのペースですね。

2019度第1学期にとったこれは自分のわかる分野だったので単位認定試験で一番良い評価とれました😃

f:id:masami256:20190905222227j:plain

だがしかし、こっちはギリギリ単位取れた。。。数学苦手😨

f:id:masami256:20190905222242j:plain

ちなみに、コンピュータの動作と管理を見てたらこんなことが😄

\177ELF on Twitter: "@uchan_nos @d_kami 放送大学の情報コースの教科書に見慣れたお名前を発見😇… "

さて、2019度第2学期も無事に単位を取ることができるのか💀

Joel on Software

Joel on Software

UEFIのSecure boot + kdump・kexec

UEFIのSecure boot + kdump・kexecの動作確認

uefiのSecure bootが有効な環境でkdump・kexecは動作するのか?ってところの確認です。

Secure bootとは?というところはこちらのドキュメントを参照してください。 access.redhat.com

テスト環境

テスト環境にはlibvirtを使います。ホストはFedora 30 x86_64、ゲストもFedora 30 x86_64です。

テスト環境セットアップ

まずはSecure bootを有効にした環境を作ります。まずは普通にlibvirtを使ってOSをインストールします。 このときに、UEFIを使うように設定します。そのため、edk2-ovmfパッケージをインストールしておく必要があります。

f:id:masami256:20190830235150p:plain

インストール段階だとセキュアブート用のファームウェアは指定できないのでとりあえずはセキュアブートなしでインストールします。

kdumpの設定

インストールが終わったら普通に設定しましょう。

access.redhat.com

この時点で動作確認もしてkdumpが正常動作することを確認しておきます。

Secure bootの設定

virsh editでxmlファイルを編集します。VMは一旦shutdownしておきましょう。

まず、OVMF_VARS.secboot.fdをコピーします。コピー先の名前はなんでも良いです。ここではvm名と合わせてます。

$ sudo cp /usr/share/edk2/ovmf/OVMF_VARS.secboot.fd /var/lib/libvirt/qemu/nvram/secureboot-test_VARS_secboot.fd

そしたらvirsh editで編集します。以下の部分を探します。

  <os>
    <type arch='x86_64' machine='pc-q35-3.1'>hvm</type>
    <loader readonly='yes' type='pflash'>/usr/share/edk2/ovmf/OVMF_CODE.fd</loader>
    <nvram>/var/lib/libvirt/qemu/nvram/secureboot-test_VARS.fd</nvram>
    <boot dev='hd'/>
  </os>

そして、以下のように編集します。変更したのはloaderのところでOVMF_CODE.secboot.fdを指定したのと、nvramには先ほどコピーしたファイルを指定しました。

  <os>
    <type arch='x86_64' machine='pc-q35-3.1'>hvm</type>
    <loader readonly='yes' type='pflash'>/usr/share/edk2/ovmf/OVMF_CODE.secboot.fd</loader>
    <nvram>/var/lib/libvirt/qemu/nvram/secureboot-test_VARS_secboot.fd</nvram>
    <boot dev='hd'/>
  </os>

セキュアブート有効で起動

普通にvmを起動すればセキュアブートが有効な状態で立ち上がります。 dmesgで確認するとこんなふうになります。

[masami@secureboot-test ~]$ dmesg | grep secureboot
[    0.000000] secureboot: Secure boot enabled
[    0.928493] systemd[1]: Set hostname to <secureboot-test>.
[    3.770079] systemd[1]: Set hostname to <secureboot-test>.
[masami@secureboot-test ~]$ 

これでvmのセキュアブートが有効になりました。

kdumpのテスト

セキュアブートが有効な状態でkdumpをスタートすると失敗します。エラーメッセージはこのようになってます。

[masami@secureboot-test ~]$ sudo systemctl status kdump
● kdump.service - Crash recovery kernel arming
   Loaded: loaded (/usr/lib/systemd/system/kdump.service; enabled; vendor preset: disabled)
   Active: failed (Result: exit-code) since Fri 2019-08-30 22:29:52 JST; 12s ago
  Process: 1316 ExecStart=/usr/bin/kdumpctl start (code=exited, status=1/FAILURE)
 Main PID: 1316 (code=exited, status=1/FAILURE)

Aug 30 22:29:51 secureboot-test systemd[1]: Starting Crash recovery kernel arming...
Aug 30 22:29:52 secureboot-test kdumpctl[1316]: Secure Boot is enabled. Using kexec file based syscall.
Aug 30 22:29:52 secureboot-test kdumpctl[1316]: kexec_file_load failed: Operation not permitted
Aug 30 22:29:52 secureboot-test kdumpctl[1316]: kexec: failed to load kdump kernel
Aug 30 22:29:52 secureboot-test kdumpctl[1316]: Starting kdump: [FAILED]
Aug 30 22:29:52 secureboot-test systemd[1]: kdump.service: Main process exited, code=exited, status=1/FAILURE
Aug 30 22:29:52 secureboot-test systemd[1]: kdump.service: Failed with result 'exit-code'.
Aug 30 22:29:52 secureboot-test systemd[1]: Failed to start Crash recovery kernel arming.

dmesgでカーネルのエラーを確認するとこのようなメッセージを発見できます。

[  173.215036] Lockdown: kexec: /proc/kcore is restricted; see man kernel_lockdown.7

カーネルのコンフィグを確認するとLockdown機能が有効になってますね。

[masami@secureboot-test ~]$ grep LOCK_DOWN /boot/config-5.2.9-200.fc30.x86_64 
CONFIG_LOCK_DOWN_KERNEL=y
# CONFIG_LOCK_DOWN_KERNEL_FORCE is not set
CONFIG_LOCK_DOWN_IN_EFI_SECURE_BOOT=y

Lockdownの解除

手元にmanがなかったので検索するとこちらが見つかりました。

lwn.net

manによると物理的に接続されたキーボードからのsysrq + xでlockdownが解除できるとあります。というわけで、virsh send-keyでsysrq + xを送ります。

$ sudo virsh send-key secureboot-test KEY_LEFTALT KEY_SYSRQ KEY_X

そうすると以下のようにlockdownを解除したよってメッセージがでます。

[masami@secureboot-test ~]$ dmesg
[ 2236.328389] sysrq: Disabling Secure Boot restrictions
[ 2236.328934] Lifting lockdown

kdumpのテスト

まずセキュアブートが有効か確認します。

[masami@secureboot-test ~]$ sudo mokutil --sb-state
SecureBoot enabled
[masami@secureboot-test ~]$ 

で、kdumpを起動

[masami@secureboot-test ~]$ sudo systemctl start kdump

エラーが出なければOKなので、クラッシュさせてみると /var/crash/に該当時刻のディレクトリが出来ていてコアダンプが取得できてるのが確認できます😃

[masami@secureboot-test ~]$ sudo sh -c "echo c > /proc/sysrq-trigger"
client_loop: send disconnect: Broken pipe
masami@moon:~$ ssh secureboot-fedora 
Web console: https://secureboot-test:9090/ or https://192.168.122.191:9090/

Last login: Fri Aug 30 22:58:25 2019 from 192.168.122.1
[masami@secureboot-test ~]$ ls /var/crash/
127.0.0.1-2019-08-30-01:05:24  127.0.0.1-2019-08-30-23:38:24
[masami@secureboot-test ~]$ date
Fri 30 Aug 2019 11:38:53 PM JST
[masami@secureboot-test ~]$ 

まとめ

セキュアブートが有効な環境でもkdump・kexecは使えますが、カーネルのLockdown機能が有効な場合は使えません。もしkdump・kexecを使用したい場合はLockdownを止める必要があるということでした。

追記1 2019/08/31

kexecとセキュアブート

セキュアブートが有効な場合と無効な場合ではkexec実行時のオプションがちょっと違います。では具体的に何が違うのか?というとセキュアブートが有効な場合は-sオプションを使います。 Fedoraの場合はkdumpctlコマンドがこの辺の処理をしていて、以下のようなコードとなってます。

    
    # For secureboot enabled machines, use new kexec file based syscall.
    # Old syscall will always fail as it does not have capability to
    # to kernel signature verification.
    if is_secure_boot_enforced; then
        echo "Secure Boot is enabled. Using kexec file based syscall."
        KEXEC_ARGS="$KEXEC_ARGS -s"
    fi
 
    $KEXEC $KEXEC_ARGS $standard_kexec_args \
        --command-line="$KDUMP_COMMANDLINE" \
        --initrd=$TARGET_INITRD $kdump_kernel
    if [ $? == 0 ]; then
        echo "kexec: loaded kdump kernel"
        return 0
    else
        echo "kexec: failed to load kdump kernel" >&2
        return 1
    fi

セキュアブートが有効な場合は-sオプション付けて使用するシステムコールを変える必要があります。

f:id:masami256:20190831104347p:plain

超例解Linuxカーネルプログラミング~最先端Linuxカーネルの修正コードから学ぶソフトウェア品質~

超例解Linuxカーネルプログラミング~最先端Linuxカーネルの修正コードから学ぶソフトウェア品質~

raspberry pi 3 b+ and u-boot and mainline kernel, and meta-fedora

Raspberry Pi(以下rpi)のカーネルではなくて、mainline等のカーネルを使いたい場合のメモです。

TL;DR

基本的に次のwebサイトを見れば事足ります。

elinux.org

概要

rpi向けのカーネルは以下なのですが、mainlineのカーネルを使いたい等の理由がある場合の環境構築方法のメモです。

github.com

uart、wifihdmiを使えるところまでが対象です。archはaarch64を対象とします。使っているのはRaspberry Pi 3 B+です。

f:id:masami256:20190821212408j:plain

必要なもの

これらが必要です。

最初の2つはそりゃそうだなって感じですね。次のrpiで〜というところはstart.elfやfixup.datといったブートに必要なバイナリファイル群です。最後のfirmwarewifiを使うのに必要になります。

u-boot

RPi U-Boot - eLinux.orgを見てビルドしてください。

Linux Kernel

適当なaarch64環境でコンパイルするか、クロスコンパイルします。

Fedoraならgcc-aarch64-linux-gnuあたりのパッケージをインストールします。クロスコンパイル時のmakeコマンドは基本的に次の通りです。これにoldconfigだったりがオプションで付きます。

make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- 

defconfig

make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- defconfig

configオプションで設定するものがいくつかあります。defconfigでmakeしたあとに以下のスクリプトを実行し、最後にmake oldconfigを実行します。ここで設定しているのは有線LAN、wifiドライバ、HDMI出力、broadcomのドライバ、フレームバッファです。面倒なのでモジュールではなくてカーネルに組み込んでます。

#!/bin/bash

configs=(USB_LAN78XX
RFKILL
BRCMFMAC
RCMFMAC_PROTO_BCDC
BRCMFMAC_PROTO_MSGBUF
BRCMFMAC_SDIO
BCMFMAC_USB
BRCMFMAC_PCIE
CFG80211
CFG80211_REQUIRE_SIGNED_REGDB
CFG80211_USE_KERNEL_REGDB_KEYS
CFG80211_DEFAULT_PS
CFG80211_DEBUGFS
CFG80211_CRDA_SUPPORT
CFG80211_WEXT
ARCH_BCM2835
SERIAL_8250_BCM2835AUX
HW_RANDOM_BCM2835
I2C_BCM2835
SPI_BCM2835
SPI_BCM2835AUX
PINCTRL_BCM2835
BCM2835_THERMAL
BCM2835_WDT
SND_BCM2835_SOC_I2S
MMC_BCM2835
DMA_BCM2835
BCM2835_VCHIQ
SND_BCM2835
VIDEO_BCM2835
BCM2835_MBOX
BCM2835_POWER
PWM_BCM2835
HW_RANDOM_BCM2835
ROCKCHIP_DW_HDMI
ROCKCHIP_INNO_HDMI
ROCKCHIP_RK3066_HDMI
DRM_SUN4I_HDMI_CEC
DRM_MSM_HDMI_HDCP
DRM_VC4_HDMI_CEC
HDMI
FB_SIMPLE
LOGO
LOGO_LINUX_CLUT224
)

for config in ${configs[@]};
do
    ./scripts/config --enable $config
done

HDMIで画面出力する場合はフレームバッファが必要で、mainlineのカーネルだとsimple-framebuffer(CONFIG_FB_SIMPLE)を有効にする必要があります。

.configが作れたらカーネルとdevicetreeをビルドします。

Imageのビルド

make -j$(nproc) ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-

DeviceTreeのビルド

make -j$(nproc) ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- dtbs

カーネルのビルドはこれにて終了です。

Raspberry Pifirmwareダウンロード

ラズパイ用のfirmwareをgitリポジトリからcloneしましょう。

github.com

Linuxfirmwareダウンロード

wifiを動かすのに必要なfirmwareを一式gitリポジトリからcloneしましょう。

git.kernel.org

sdカードの準備

こんな感じで最初のパーティションfat32で作ります。起動はここからです。もう一つのほうはroot filesystemです。

root@qemuarm64:~# fdisk -l
Disk /dev/mmcblk0: 30 GB, 31914983424 bytes, 62333952 sectors
973968 cylinders, 4 heads, 16 sectors/track
Units: cylinders of 64 * 512 = 32768 bytes

Device       Boot StartCHS    EndCHS        StartLBA     EndLBA    Sectors  Size Id Type
/dev/mmcblk0p1    4,4,1       261,4,2           2048     133119     131072 64.0M  c Win95 FAT32 (LBA)
Partition 1 does not end on cylinder boundary
/dev/mmcblk0p2    261,5,1     1023,254,2      133120    2097151    1964032  959M 83 Linux
Partition 2 does not end on cylinder boundary

bootパーティションの設定

firmwareのコピー

/mnt/rpiにマウントしているとして、cloneしてきたrpiのfirmwareディレクトリに移動しファイルをコピーします。

$ cd firmware
$ cp -a boot/start* /mnt/rpi/.
$ cp -a boot/fixup* /mnt/rpi/.
$ cp -a boot/overlays/ /mnt/rpi/.
$ cp -a boot/bootcode.bin /mnt/rpi/.

カーネルのコピー

arch/arm64/boot/Imageとarch/arm64/boot/dts/broadcom/bcm2837-rpi-3-b-plus.dtbを/mnt/rpiにコピーします。

u-bootのコピー

ビルドしたu-boot.binを/mnt/rpiにコピーします。

config.txtの作成

つぎに/mnt/rpi/config.txtを以下の内容で作ります。

[all]
boot_delay=1
kernel=u-boot.bin
arm_control=0x200
upstream_kernel=1
audio_pwm_mode=0
enable_uart=1
gpu_mem=16
mask_gpu_interrupt1=0x100

u-bootの設定

以下のようなファイルを(boot.cmd)を作ります。作成する場所は任意です。

fatload mmc 0:1 ${kernel_addr_r} Image
fatload mmc 0:1 ${fdt_addr} bcm2837-rpi-3-b-plus.dtb
setenv bootargs dwc_otg.lpm_enable=0 earlyprintk root=/dev/mmcblk0p2 rootfstype=ext4 rootwait
booti ${kernel_addr_r} - ${fdt_addr}

そして以下のコマンドでboot.scrを/mnt/rpiに作成します。

$ mkimage -C none -A arm64 -T script -d ./boot.cmd /mnt/rpi/boot.scr

上記のmkimageコマンドはfedoraならuboot-toolsパッケージにあります。

uartの設定

u-bootの段階からuartでログを表示したい場合は以下のようにします。

$ sed -i -e "s/BOOT_UART=0/BOOT_UART=1/" /mnt/rpi/bootcode.bin 

と、ここまででブートローダカーネルの準備はOKです。が、これだとルートファイルシステムがないので面白くないですよね。なのでルートファイルシステムも用意しましょう。

ルートファイルシステム

ルートファイルシステムのビルド

まあなんでもいいのですが、せっかくなので自作のyoctoのレイヤーを使いましょう(´ω`)

github.com

MACHINEにはqemuarm64を指定してcore-image-minimalでビルドしてください。build/tmp-glibc/deploy/images/qemuarm64/にルートファイルシステムのtarファイルがあるのでそれをルートファイルシステムパーティションに展開します。

/mnt/rootfsがルートファイルシステムパーティションだとしてこうします。

$ sudo tar xvf build/tmp-glibc/deploy/images/qemuarm64/core-image-minimal-qemuarm64.tar.bz2 -C /mnt/rootfs/

firmwareのコピー

broadcomファームウェアをルートファイルシステムにコピーしましょう。こんな感じです。

$ sudo mkdir /mnt/rootfs/lib/firmware
$ sudo cp -a linux-firmware/brcm/ /mnt/rootfs/lib/firmware/.

起動

ここまででホストでの作業は完了したのであとはsdカードから起動させましょう。

TV側

起動時の🐧

f:id:masami256:20190821211838j:plain

ログイン

f:id:masami256:20190821211912j:plain

uartでもログインしてるのでttyS1もありますね。

uart側

f:id:masami256:20190821212710p:plain

有線LAN、wifiが認識されてます。

wlan0

root@qemuarm64:~# ip link show wlan0
2: wlan0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop qlen 1000

eth0

root@qemuarm64:~# ip link show wlan0
2: wlan0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop qlen 1000

まとめ

rpiのカーネルでなくてもhdmiを使ったり有線・wifi使えるようになりましたΣ(゚∀゚ノ)ノキャー tftpブートすればrpiでのカーネルハックが捗ると思います😃

自分用にLinuxカーネルのBuildbot + LAVAでCI環境を作るめも

ちょっとCI環境でも作るか〜ということで。

CI

だいたいこんな構成です。

f:id:masami256:20190714155852p:plain
構成

いまのところすべてローカル環境で閉じてます。うちはマンションタイプのBフレッツでそんなに速いわけでもないからローカルで閉じてるほうがテスト時間が短くて済むというとこです。AWSとかVPSを使っても良いんですけどね。

とりあえず動くようにはなったので、ブラッシュアップするところはたくさんあります😨

CIのシステムで使用するソフトウェア

次の2つを使います。

Buildbotは有名ですよね。LAVAはkernelci.orgで使われています。実ボードとかqemuなどの環境でLinuxをテストするのに向いているツールです。

インストール

buildbot

pythonのvirtualenvを使って専用のpython環境作ってインストールして使ってます。

LAVA

LAVAは一回スクラッチから設定したことあるんですが面倒なのでdockerを使います。githubkernelciのプロジェクトのgitリポジトリがあるのでcloneして使いました。

Webサーバ

Webサーバも使うんですがこれはLAVAからカーネルなどのファイルをダウンロードするのに使うだけなのでpythonワンライナーで済ませてます。

流れ

ファイルを編集してコミットしたらbuildbotがリポジトリをポーリングしているので、変更を検知したらビルド開始します。make bzImageとmake modules、make modules_installまで成功したらdracutでinitramfsを作り、bzImageとinitramfsをwebサーバのディレクトリに置きます。そしたらbuildbotのジョブからLAVAのAPIを叩いてboot testを実行します。buildbotはLAVAのテストが終わるのを待って、LAVAのテスト結果を最終的なビルドの結果とします。

画面

画面はこんな感じですね。

buildbot

f:id:masami256:20190714154349p:plain
buildbot

lavaテスト実行画面

f:id:masami256:20190714154731p:plain
lava

lavaテスト実行画面の最下部

f:id:masami256:20190714154753p:plain
lava

設定

buildbot

buildbotでのテストステップはmaster.cfgで完結してますが、テストステップの記述はファイルを分けたほうが綺麗でしょうね。

pollingの設定はこんな感じでローカルのディレクトリを直に設定。このディレクトリはgit --bare initしたリモートリポジトリではなく、単純にcloneしてきたディレクトリです。ポーリング間隔はもっと短くて良いかな。

c['change_source'].append(changes.GitPoller(
        '/home/masami/projects/linux-kernel',
        workdir='linux-workdir', branch='ktest',
        pollInterval=300))

スケジューリングの設定はこんな感じ。forceビルドもしますよねということで。

c['schedulers'].append(schedulers.SingleBranchScheduler(
                            name="ktest branch test",
                            change_filter=util.ChangeFilter(branch='ktest'),
                            treeStableTimer=None,
                            builderNames=["linux-master-builder"]))
c['schedulers'].append(schedulers.ForceScheduler(
                            name="force",
                            builderNames=["linux-master-builder"]))

テストステップはこうです。

 check out the source
factory.addStep(steps.Git(repourl='/home/masami/projects/linux-kernel', mode='incremental'))

# start build

factory.addStep(steps.ShellCommand(command=["make", "mrproper"]))
factory.addStep(steps.ShellCommand(command=["cp", "/home/masami/projects/build-test/vm-config", ".config"]))

config_path = "%s/.config" % tmpdir_path
factory.addStep(steps.ShellCommand(command=["./scripts/config", "--set-str", "CONFIG_LOCALVERSION", "-build-test"]))

import multiprocessing
parallel_opt = '-j%s' % multiprocessing.cpu_count()


# build kernel and modules
factory.addStep(steps.ShellCommand(command=["make", parallel_opt, "bzImage"]))
factory.addStep(steps.ShellCommand(command=["make", parallel_opt, "modules"]))

# install modules
factory.addStep(steps.ShellCommand(command=["make", "INSTALL_MOD_PATH=/tmp/linux-build-test", "modules_install"]))

factory.addStep(steps.ShellCommand(command=["cp", "-f", "/home/masami/projects/builder/worker/linux-master-builder/build/arch/x86/boot/bzImage", "/home/masami/projects/web/bzImage"]))
                
factory.addStep(steps.ShellCommand(command=["dracut", "--force", "-k", "/tmp/linux-build-test/lib/modules/5.2.0-build-test+", "--kernel-image", "/home/masami/projects/web/bzImage", "--kver", "5.2.0-build-test+", "/home/masami/projects/web/initramfs-build-test.img"]))

factory.addStep(steps.ShellCommand(command=["/home/masami/projects/build-test/boot-test.py", "/home/masami/projects/build-test/boot-test.yaml"]))

factory.addStep(steps.ShellCommand(command=["rm", "-fr", "/tmp/linux-build-test"]))

やってることはこんな感じです。

  1. git cloneする
  2. make mrproper
  3. 別のディレクトリからカーネルのコンフィグをコピーする
  4. CONFIG_LOCALVERSIONをセット
  5. 使えるcpu数を調べる
  6. make bzImage
  7. make modules
  8. make modules_install 9 bzImageをwebサーバのディレクトリにコピー
  9. dracutでinitramfsを作る
  10. 作ったinitramfsをwebサーバのディレクトリにコピー
  11. LAVAのAPIを実行するスクリプトを実行してLAVAでテスト
  12. 後始末

カーネルのバージョンを直書きしてるのは直さないとねとか色々。

LAVA

LAVAはdockerイメージをそのまま使っていて特に設定の変更はしてません。テストはAPIから実行するのでAPI用のトークンを作ったくらいですね。APIを実行するスクリプトは↓です。テストはjobという形で登録します。jobを記述するときのフォーマットはyaml形式で、APIから登録するときはyaml形式の文字列を送ります。

#!/usr/bin/env python3

import xmlrpc.client
import sys
import time
import yaml

def create_server_instance():
    username = 'user name'
    token = 'access token'
    hostname = 'localhost:10080'
    return xmlrpc.client.ServerProxy('http://%s:%s@%s/RPC2' % (username, token, hostname), allow_none=True)

def submit_job(server, job):

    return server.scheduler.submit_job(job)
    
def read_job_yaml(filepath):
    with open(filepath, 'r') as f:
        return f.read()

def wait_until_job_finish(server, job_id):

    while True:
        ret = server.scheduler.job_health(job_id)
        health = ret['job_health']

        if not health == 'Unknown':
            return
        time.sleep(5)

def check_job_result(server, job_id):
    results_raw = server.results.get_testjob_results_yaml(job_id)
    try:
        from yaml import CLoader as Loader
    except ImportError:
        from yaml import Loader
        
    results = yaml.load(results_raw, Loader=Loader)
    
    for ret in results:
        r = ret['result']
        if r == 'fail':
            return False

    return True

if __name__ == '__main__':
    if not len(sys.argv) == 2:
        print('[-]usage: %s <path to job definition yaml file>' % sys.argv[0])
        exit(1)

    server = create_server_instance()        
    job = read_job_yaml(sys.argv[1])
    job_id = submit_job(server, job)

    print('job: %d created' % job_id)

    wait_until_job_finish(server, job_id)

    ret = check_job_result(server, job_id)

    if ret:
        print('Test: success')
        exit(0)

    print('Test: fail')
    exit(1)

上記のスクリプトはscheduler.submit_job()でjobを登録し、scheduler.job_health()で終わるのを待っています。その後、get_testjob_results_yaml()でテスト結果をステップごとに見ていってるんですが、これは実際不要ですね。scheduler.job_health()の結果としては以下の4つで、Unkownはテスト実行中に返ってくるので結果のステータスは3個。なのでComplete以外ならエラーとしても大丈夫だと思います。

  • UNKONW(実行中はこれが返ってくる)
  • Complete(成功時)
  • Incomplete(失敗時)
  • Cancel
LAVA test case

APIでjob登録するとして、どんなyamlを書いてるのかというとこんなのです。

# simple boot test
device_type: qemu
job_name: kernel build test x86-64 , boot test

timeouts:
  job:
    minutes: 15
  action:
    minutes: 10
  connection:
    minutes: 10
priority: medium
visibility: public

context:
  arch: amd64
  memory: 4096
  extra_options:
    - --append "root=/dev/sda1 audit=0 console=tty0 console=ttyS0,115200"
    - -serial stdio
actions:
- deploy:
    timeout:
      minutes: 3
    to: tmpfs

    images:
      kernel:
        image_arg: -kernel {kernel}
        url: http://192.168.11.18:12080/bzImage
      initrd: 
        url: http://192.168.11.18:12080/initramfs-build-test.img
        image_arg: -initrd {initrd}        
      rootfs:
        image_arg: -drive file={rootfs} 
        url: http://192.168.11.18:12080/ktest.qcow2
- boot:
    timeout:
      minutes: 2
    method: qemu
    media: tmpfs
    prompts: 
      - "Last login:"
      - "root@ktest:"
    auto_login:
      login_prompt: "ktest login:"
      username: "root"

contextのところでqemuに渡すパラーメータを設定しています。actionsはテスト対象のカーネル、initrdなどの取得先とqemuに渡すパラメータの設定です。rootfsはその名の通りroot filesystemです。これはfedora30のserver版を使って作りました。ディスクに3GB割り当てたけど1GBでも充分だったな。 bootのところがbootテストの内容です。auto_loginのところで指定したログインプロンプトが表示されたらusernameを送ってます。rootfsのほうでrootのパスワードは無しに設定しているのでパスワード無しでログインしてます。もちろんパスワードプロンプトとパスワードを設定することできます。promptsは期待値になる部分で、ログインに成功したときに表示されるものをリストしてます。

lavaの出力のこの辺です。

f:id:masami256:20190714162213p:plain
lava

まとめ

BuildbotとLAVAを使い、ここまでの設定でカーネルを弄ってコミットするとビルド・テストが自動で実行できるようになりました🎉

meta-fedoraなんてものを作り始めた(*ノω・*)テヘ

meta-fedora

はじめに

もし、meta-fedoraと聞いてピンときた場合はその感は当たりですw

主に組み込み向けのLinuxディストリビューションを作るためのものとしてyocto projectがありますね。yoctoプロジェクトのリファレンスディストリビューションとしてpokyがあります。そして、pokyのカスタムレイヤーでDebianのソースパッケージを利用するmeta-debianがあります。

ここで、例えばfedoraとかでもできるんじゃないだろうか?ということでyoctoの勉強も兼ねて作り出したのがmeta-fedoraです。今日の時点ではbusyboxをpokyからmeta-fedoraに置き換えが出来た程度です。busyboxfedoraのsrc.rpmにはパッチが無かったのでfedora用のパッチを当てるdo_fedora_patchタスクは作ったけど特に処理がない状態です。

イデアはもちろんmeta-debianから来てます。なので、基本的な動作もmeta-debianと同じ感じになるようにしました。ソースパッケージに関する情報はrecipes-fedora/sources/*.incファイルに書きます。

fedoraもaarch64版はあるし行けるんじゃね?みたいなところですね。

.incファイル

src.rpmファイルに関する情報を書きます。このファイルは後述するfedora-source.bbclassが作成します。パッケージを追加する場合は空のファイルを作成します。touch recipes-fedora/sources/<source package name>.inc という感じです。

内容はこんな感じです。

# This file is generated by fedora-source.bbclass

LICENSE = "GPLv2"
FPV = "1.28.33.fc30"
FPV_EPOCH = "1"
REPACK_PV = "3.fc30"
PV = "1.28.3"

FEDORA_SRC_URI = " \
    ${FEDORA_MIRROR}/pub/linux/Fedora/fedora/linux/releases/${DISTRO_VERSION}/Everything/source/tree/Packages/b/busybox-1.28.3-3.fc30.src.rpm;name=busybox-1.28.3-3.fc30.src.rpm \
"

SRC_URI[busybox-1.28.3-3.fc30.src.rpm.sha256sum] = "cb03d63ee3867d406786a0e118540900f8bd9d047637501a271e82755291d23f"

ソースパッケージに関する情報はsha256sum-primary.xmlというファイルに書かれているんですが、パッケージのライセンスもこのファイルから取得できるのでライセンス情報も一緒に書いてます。

classes

fedora-source.bbclass

ソースパッケージの情報をrecipes-fedora/sources/*.incに書くためのクラスです。

まずFedoraのミラーサーバからrepomd.xmlを取得します。この時に読み込むファイルは2つ有って、release時のパッケージ情報があるreleasesディレクトリ以下のファイルと、パッケージの更新版が置かれるupdatesディレクトリ以下のファイルの2つがあります。

これらを読んでprimary.xmlファイルのパスを取得します。

次にprimary.xmlを読みます。このときはreleases -> updatesの順番で読んでsrc.rpmファイルのデータを組み立てて行きます。データはパッケージ名をキーとした連想配列で作っていて、同名のパッケージは後から出てきたほうで上書きしてます。なので最初にリリース時点でのファイルを読んで、次に更新版パッケージのファイルを読んでます。2つのファイルのデータをマージするなら、こうしたほうが単純で楽というのがでかいですね。

その次はrecipes-fedora/sourcesディレクトリ以下に有る.incファイルを調べます。ここで見つかったincファイルに対してprimary.xmlで読んだデータを書き込みます。

このクラスは次のように設定していて、レシピのパース時に実行されます。

fedora_source_eventhandler[eventmask] = "bb.event.ParseStarted"

inheritはディストリビューションの設定ファイルのほうで次のようにしています。

INHERIT += "fedora-source"

最低限の機能はできてますが、meta-debianのようにDEBIAN_SOURCE_ENABLEDなどの環境変数を使ったダウンロードの制御はまだありません。

fedora-package.bbclass

こちらはレシピの中でinheritして使うものです。このファイルではunpackの処理を行ったり、fedoraのパッチを当てるなどの処理を行います。 1つハマってとりあえずのハックでしのいだのがあって、bitbakeのunpackタスクはrpmファイルにも対応しています。rpmファイルの展開はrpm2cpioコマンドではなくてrpm2.cpio.shが使われます。ただ、このファイルだとfedora30のsrc.rpmを展開しようとするとエラーになってしまったので(CentOS7のsrc.rpmは大丈夫でした)、同名のファイルを作って、PATHを通してpokyのrpm2cpio.shではなくて自前のrpm2cpio.shが呼ばれるようにしました。自前のrpm2cpio.shはrpmファイルの展開にホストのrpm2cpioコマンドを利用します。そのため、ディストリビューションの設定ファイルに次の設定を追加しました。

HOSTTOOLS += "rpm2cpio"

動作環境

x86-64とaarch64で動作します。

x86-64

[    5.603340] Run /sbin/init as init process
[    5.610576] IPv6: ADDRCONF(NETDEV_CHANGE): eth0: link becomes ready
INIT: version 2.88 booting
Starting udev
[    6.165200] udevd[116]: starting version 3.2.7
[    6.226804] udevd[116]: specified group 'kvm' unknown
[    6.290799] udevd[117]: starting eudev-3.2.7
[    6.484025] udevd[117]: specified group 'kvm' unknown
[    7.985039] uvesafb: SeaBIOS Developers, SeaBIOS VBE Adapter, Rev. 1, OEM: SeaBIOS VBE(C) 2011, VBE v3.0
[    8.079341] uvesafb: no monitor limits have been set, default refresh rate will be used
[    8.082405] uvesafb: scrolling: redraw
[    8.111438] Console: switching to colour frame buffer device 80x30
[    8.115704] uvesafb: framebuffer at 0xfd000000, mapped to 0x00000000076c66c4, using 16384k, total 16384k
[    8.116032] uvesafb: fb0: VESA VGA frame buffer device
[    8.200808] EXT4-fs (vda): re-mounted. Opts: (null)
INIT: Entering runlevel: 5
Configuring network interfaces... ip: RTNETLINK answers: File exists
Starting syslogd/klogd: done

Fedy 30 qemux86-64 /dev/ttyS0

qemux86-64 login: root
root@qemux86-64:~# strings /bin/busybox | grep "1.28"
syslogd started: BusyBox v1.28.3
BusyBox v1.28.3 (2019-06-02 13:34:16 UTC)
udhcp 1.28.3
started, v1.28.3
fsck (busybox 1.28.3)
root@qemux86-64:~#

aarch64

[    2.756128] Freeing unused kernel memory: 1024K
[    2.764907] Run /sbin/init as init process
INIT: version 2.88 booting
[    2.883961] usb 1-2: new high-speed USB device number 3 using xhci_hcd
[    3.041754] input: QEMU QEMU USB Keyboard as /devices/platform/4010000000.pcie/pci0000:00/0000:00:02.0/usb1/1-2/1-2:1.0/0003:0627:0001.0002/input/input1
[    3.100694] hid-generic 0003:0627:0001.0002: input: USB HID v1.11 Keyboard [QEMU QEMU USB Keyboard] on usb-0000:00:02.0-2/input0
Starting udev
[    3.240365] IPv6: ADDRCONF(NETDEV_CHANGE): eth0: link becomes ready
[    3.406764] udevd[106]: starting version 3.2.7
[    3.436698] udevd[106]: specified group 'kvm' unknown
[    3.467389] udevd[107]: starting eudev-3.2.7
[    3.621474] udevd[107]: specified group 'kvm' unknown
[    4.426004] EXT4-fs (vda): re-mounted. Opts: (null)
INIT: Entering runlevel: 5
Configuring network interfaces... ip: RTNETLINK answers: File exists
Starting syslogd/klogd: done

Fedy 30 qemuarm64 /dev/ttyAMA0

qemuarm64 login: root
INIT: Id "hvc0" respawning too fast: disabled for 5 minutes
root@qemuarm64:~# uname -a
Linux qemuarm64 5.0.7-yocto-standard #1 SMP PREEMPT Sun Jun 2 14:47:16 UTC 2019 aarch64 GNU/Linux
root@qemuarm64:~# strings /bin/busybox | grep "1.28"
udhcp 1.28.3
started, v1.28.3
syslogd started: BusyBox v1.28.3
fsck (busybox 1.28.3)
1.28.3
BusyBox v1.28.3 (2019-06-02 14:54:32 UTC)
root@qemuarm64:~#

開発環境

pokyのブランチはwarriorを使ってます。ホストのディストリビューションfedora 30ですが、ビルドにはfedora 28のdockerイメージを使ってます。マニュアルをよくよく見たらyocto 2.7だとfedora 29もサポートしていた。

meta-fedoraで使用するfedoraのソースはfedora 30のsrc.rpmを使っています。

todo

まだfedora用パッチを当てるロジックがないので追加しないとダメっす。あとはcore-image-minimalに入るパッケージをfedora化していったり、initシステムはsystemdに切り替えようとか。。。 fedoraはsysvinitのパッケージはもう無いのでcore-image-minimalを完全にfedoraのソースに置き換えるにはinitシステムをsystemdに変えるしかないんですよね。