fanotify(7)めも

fapolicydがどのようにアプリケーションの実行を禁止しているのだろうか?と思って調べためもです。

fapolicydがアプリケーションの実行を禁止する仕組み

仕組みとしてはfanotifyの仕組みを使ってファイルが実行のために開かれた場合に通知を受け取り、設定されたルールを調べて実行可能かどうかを返すということをしてます。

fanotify

fanotifyの機能を使うには2つの関数があります。fanotify_init(2)fanotify_mark(2)の2つです。fanotify_init(2)で初期化をしてファイルディスクリプタを得ます。次にfanotify_mark(2)で通知を受け取りたいイベントなどを設定します。イベントの受信はpoll(2)を使います。pollfd構造体の配列のうち1つはfanotify_init(2)によって初期化したfdを設定します。

実行の許可

fanotify_response構造体のresponse変数にFAN_ALLOWもしくはFAN_DENYの値を設定することでその後の処理の継続を許可するか拒否するか設定します。レスポンスはfdに対して書き込みます。

サンプル

fanotify(7)にあるサンプルコードベースに作ってみました。実行するアプリケーションが/tmp/lsの場合だけ許可しません。

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/fanotify.h>
#include <fcntl.h>
#include <poll.h>
#include <errno.h>
#include <limits.h>
#include <unistd.h>

static void handle_events(int fd)
{
    struct fanotify_event_metadata *metadata;
    struct fanotify_event_metadata buf[256];
    struct fanotify_response response;
    ssize_t len;

    while (1) {
        len = read(fd, buf, sizeof(buf));
        if (len == -1 && errno != EAGAIN) {
            perror("read()");
            exit(-1);
        }

        if (len <= 0)
            break;

        metadata = buf;

        while (FAN_EVENT_OK(metadata, len)) {
            if (metadata->vers != FANOTIFY_METADATA_VERSION) {
                fprintf(stderr, "fanotify version mismatch\n");
                exit(-1);
            }

            if (metadata->fd >= 0) {
                if (metadata->mask & FAN_OPEN_EXEC_PERM) {
                    char *proc_path, path[PATH_MAX] = { '\0' };
                    ssize_t path_len;
                    
                    asprintf(&proc_path, "/proc/self/fd/%d", metadata->fd);
                    path_len = readlink(proc_path, path, sizeof(path) - 1);
                    if (path_len == 1) {
                        perror("readlink()");
                        exit(-1);
                    }

                    if (!strcmp(path, "/tmp/ls")) {
                        printf("[+]Deny execute application %s (pid:%d)\n", path, metadata->pid);
                        response.response = FAN_DENY;
                    } else
                        response.response = FAN_ALLOW;

                    response.fd = metadata->fd;
                    write(fd, &response, sizeof(response));

                    free(proc_path);
                }
            }

            close(metadata->fd);
            metadata = FAN_EVENT_NEXT(metadata, len);
        }
    }
}

int main(int argc, char **argv)
{
    int fd;
    int poll_num;
    struct pollfd fds[2];
    
    fd = fanotify_init(FAN_CLOEXEC | FAN_NONBLOCK | FAN_CLASS_CONTENT,
        O_RDONLY | O_LARGEFILE | O_CLOEXEC | O_NOATIME);
    
    if (fd == -1) {
        perror("fanotify_init()");
        exit(-1);
    }

    if (fanotify_mark(fd, FAN_MARK_ADD | FAN_MARK_MOUNT,
        FAN_OPEN_EXEC_PERM, -1, "/tmp") == -1 ) {
        perror("fanotify_mark()");
        exit(-1);
    }

    fds[0].fd = STDIN_FILENO;
    fds[0].events = POLLIN;
    fds[1].fd = fd;
    fds[1].events = POLLIN;

    printf("[+]Press ctrl-c to quit program\n");

    while (1) {
        poll_num = poll(fds, 2, -1);
        if (poll_num == -1) {
            if (errno == EINTR)
                continue;
            perror("poll()");
            exit(-1);
        }

        if (poll_num > 0) {
            if (fds[1].revents & POLLIN)
                handle_events(fd);
        }
    }

    return 0;
}

このコードを動かすとこんな感じです。

masami@kerntest:~/prevent_exec$ sudo ./a.out
[+]Press ctrl-c to quit program
[+]Deny execute application /tmp/ls (pid:3058)

実行を止められる側はこんな感じ。

masami@kerntest:~/prevent_exec$ /tmp/ls2
a.out  prevent_exec.c
masami@kerntest:~/prevent_exec$ /tmp/ls
-bash: /tmp/ls: Operation not permitted

Linuxプログラミングインタフェース

Linuxプログラミングインタフェース

マイペースなOSSコントリビュート生活 fedora編

この記事はLinux Advent Calendar 2020の17日目の記事です。

OSSのコントリビュート方法はpatchを書いたりドキュメントを翻訳したり、バグレポートしたりと色々な方法がありますね。fedoraでテストというとリリース前のベータ版のテスト、パッケージがアップデートされた際のテスト(QA:Updates Testing - Fedora Project Wiki)、テストイベントのTest Days(QA/Test Days - Fedora Project Wiki)などがあります。

updates-testing

このなかで手軽なものとしてはupdates testingがあります。fedoranはパッケージがアップデートされる場合、最初にupdates-testingリポジトリに更新されたパッケージが入ります。その後、基本的には以下のルールに則ってパッケージがupdateリポジトリに入るか更新がリジェクトされるかのいずれかになります。

By default, test updates with karma of 3 are automatically sent out as full official updates, while test updates with karma of -3 are automatically withdrawn from the testing repository. 

テストした人が実際にパッケージを使ってみて問題なかった・修正されたと書かれているバグが直ってたなんてことを確認したりしてみて結果をフィードバックします。これはコマンドラインのアプリでも出来ますし、bodhiと呼ばれるWebインターフェースもあります。

bodhi.fedoraproject.org

bodhiのパッケージの画面ではこのような感じでチェック項目があります。これはkernel-5.9.14-200.fc33のときのものです。バグフィックスに関しては自分の環境だと再現できないとかあると思うのでそういう場合はグレーの👍を選択します。問題が合ったときは👎を選択し、特に問題なかったら緑の👍を選択する感じです。

f:id:masami256:20201227232230p:plain

テストの内容はカーネルの例のように指定されている場合もありますし、無い場合もあります。基本的には指定されたテストケース以外のことについては実際に使ってみてOKかどうかってところでみんなやってると思います。全部のアップデートをテストしようとか気張らずに自分がよく使ってるものだけでも簡単にやろうくらい気軽な感じでできます😊 セキュリティアップデートもupdates-testingリポジトリに最初に入るので更新して使ってみた結果のフィードバックをするのって割と大事かななんて思います。

Test Days(Test Week)

こちらはカーネルGnomeなんかの主要なパッケージのメジャーバージョンが上がるような時に行われることが多いです。カーネルですとfedoraはリリース後にもカーネルのメジャーバージョンは上がりますのでイベントに参加して自分の環境で問題ないか確認しておくのは良いかもしれません。Test Daysではiosイメージ、リリースしているfedoraのバージョン用のrpmファイルが用意されますのでお好きな方法を選択できます。カーネルだとテスト内容はupdates-testingと一緒です。こちらも割と手軽ですが通常のupdates-testingではなく専用のイベントが用意されるのでシステム的にインパクトの大きな変更が入るということなので場合によっては自分の環境だと問題があるってこともあると思います。まあ、そういうのを見つけるためのイベントなわけですが😃

カーネルのTest Weekで自分が実際にバグに当たってupstreamに報告とパッチを投げたり・テストしたり、fedoraで修正されたことをupdates-testingで確認したときのログが↓ですので興味がある人は読んでみてください🙋 kernhack.hatenablog.com


fedoraに限らず使ってみて結果をフィードバック(特にバグレポート)するというのは重要な貢献だと思いますので、OSSに貢献したいけど何をすれば良いか🤔と考えている人は新しいバージョンを使ってみて結果をフィードバックするというのも検討してみてはいかがでしょうか👍

OSSライセンスの教科書

OSSライセンスの教科書

1999年発行(日本語翻訳版)のLinuxカーネル解説本を見返してみる

この記事はLinux Advent Calendar 2020の24日目の記事です。

自分がLinuxを使いだした頃、OSはどうやってブートしてんるんだろう🤔とか色々知りたくて買ったのが↓の本です。

f:id:masami256:20201225194100j:plain
Linuxカーネルインターナル

買った当時は知識がなくて読むのが大変だったけど今はそれなりに読めるようになり、さすがに成長したなとも思ってみたり😃 初版が1999年6月25日と書いてあるので2000年台の始めの頃に買ったんでしょう。それにしても翻訳版の初版が発行された時点からでも21年前の本ということか。

この本が対象としているのはLinux 2.0です。目次は次のようになってます。メモリ管理、ファイルシステム、モジュール、ネットワーク等々ありますね。基本的な機能は変わってないので現在のカーネルでこの目次の内容に沿ってコードを読んでみるというのは面白いかもしれません。この記事を書いている時点での最新のLinuxのバージョンは5.10.2です。本で説明されてるこの構造体は今でもあるのかな?と思ったときは5.10.2で調べてます。本書は付属としてLinux 2.0のソースコード入りCDが付いてます。

序文

本書の序文はLinusさんが書いています。「OSの開発は今も昔もエキサイティングなプロジェクトだ。でも、カーネルの内部を詳しく知ろうとするとドキュメントが足りないのが難点で、ハックしようと思ったらコードを読むしかなかった。それは良いことなんだけど、本書のようにLinuxの使用とカーネルにつて説明する本書のようなドキュメントが出たのは良いね」といった感じのことが書かれてます。

はじめに

本書は著者さんによる同名の著書の第2版で、Linux2.0をカバーしていると書いてあるので第1版は1.xの解説をしてたということなんでしょう。本書の方針として上げている「Linux はオープンなOS。隠された秘密は何もない。問題に遭遇したらソースコードに当たれば良い」ってのが個人的に好きです。

第1章 Linuxオペレーティングシステム

Linuxがどんなものかの説明です。機能や、Linuxディストリビューションとは?といったことの説明が書かれています。

第2章 カーネルコンパイル

カーネルハックをする上でカーネルコンパイルは必須ですからね。最近のカーネルに関しては@progrunner17さんによるLinuxカーネルビルド大全が詳しいです。本書ではカーネルのコードの場所は/usr/src/linuxを使用しています。最近は任意なところにgitでコードをクローンしてビルドできますが当時は/usr/src/linuxが標準だったんですね。この章ではこのディレクトリにはこの機能のコードがあるよなんてのも軽く解説しています。本章ではカーネルコンパイル方法、i386以外のarchを使いたい場合のMakefileの変更方法なんかも説明されてます。

第3章 カーネルの概要

章の流れとしてはカーネルとはなにか?といったところの説明、プロセスとタスク、システムコール、割り込みルーチン、プロセスとスレッドなどの用語の説明カーネルのデータ構造、シグナル、割り込み、カーネルの最初のc関数、スケジューラ、システムコールの実装(仕組み、fork()などいくつかのシステムコールの説明)など盛りだくさんです。この章で説明されている構造体のtask_struct、mm_struct、fs_struct、inode、fileなどは今でも重要な立ち位置ですね。それとは逆にプロセスを登録するプロセステーブルは今ではもうありませんね。メモリの確保・解放だと__get_free_pages()、kmalloc()、kfree()なんかも今でも使われてますね。

第4章 メモリ管理

Linux 2.0の時点で複数のアーキテクチャに対応しているのでまずはアーキテクチャ非依存のモデルについて説明しています(仮想アドレス空間など)。次はi386のページテーブルの説明がきて、LinuxのPGD・PMD・PTEの説明になります。そしてvm_area_struct構造体(これも今もありますね)、brkシステムコールmmapシステムコールの説明ときて、カーネルが使うセグメント(x86のセグメント)の説明があります。つぎにkmallo()・kfree()やvmalloc()・vfree()の説明です。この次にバッファキャッシュの説明があります。ここで説明されているbuffer_head構造体もいまでもあります。bdflushの説明もあります。バッファキャッシュの管理・利用方法の説明もここで行っています。あとはswap処理、空きページの管理(今でもページの管理に使うpage構造体はこの当時からあります)。最後にページフォルトの処理を説明して本章は終わりです。

第5章 プロセス間通信

まずはカーネル内の処理で同期を取る方法の説明があります。キューを使って寝て待って、処理が終わったら起こしてもらったり、セマフォを使うなどの説明です。fcntl(2)によるファイルのロックの処理についても説明されてます。あとはpipeの実装、ptrace(2)の仕様、System V IPC、共有メモリ、Unixドメインソケットを使ったプロセス間通信などです。この辺りの機能は今でも使われますね。

第6章 Linuxファイルシステム

VFSの説明、カーネルでのファイルシステムの表現、マウント処理、スーパーブロックの処理(super_operations構造体に含まれる各種関数の説明)、inodeの処理(inode_operations構造体に含まれる各種関数の説明)、file構造体の説明、ファイル操作(file_operations構造体に含まれる各種関数の説明)、ファイルを開くときの処理、procファイルシステムext2ファイルシステムのデータ構造、ブロックへの配置アルゴリズムなどの説明があります。ジャーナリングファイルシステムについての説明はさすがにないですが今だと当たり前にある機能とも言えるので現在カーネルの解説本を書くなら必須項目になるんでしょうね。

第7章 Linuxデバイスドライバ

ブロックデバイス・キャラクターデバイス、メジャー・マイナー番号の説明があり、割り込み・ポーリングの説明、ハードウェアの検出などの説明があります。ドライバの実装の説明としてはPC内蔵スピーカーを例にしたりしてます。デバドラの実装の説明では初期化、open・read/write・ioctlの実装方法など説明してます。また、DMAの操作も説明してます。

第8章 ネットワークの実装

ネットワークの実装ではsk_buff構造体の説明、proto構造体の説明など構造体の説明の他に、ネットワークデバイスの実装の説明があります。プロトコルの説明としてはarp、ipの説明があります。ルーティングやパケットフィルタについても説明されています。そして、tcpの説明ももちろんあります。

第9章 モジュールとデバッグ

カーネルモジュールの説明です。モジュールがどのようにロードされるか、モジュールのタイプ別にどのような関数を実装するべき(ファイルシステムならregister_filesystem()と言った感じで)かといった説明などあります。 モジュールの例としてはPCMCIAのモジュールを使って説明してます。デバッグでは今でも使われるprintkデバッグgdbを使ったデバッグ方法について触れられています。printkデバッグでは最良のデバッガ:printk()と書かれています。この当時だとカーネルデバッグというとprintk()というのが主だったんでしょうね。カーネルのビルド・再起動に時間はかかるけど手軽さは確かにありますが今ならftrace、perf、eBPFなどがお薦めの手段でしょうか。

第10章 マルチプロセッシング

この章ではSMPの実装におけるカーネルの初期化、cpu間の通信、割り込み処理についての説明があります。smp対応のカーネルコンパイルする場合はMakefileSMP = 1の部分のコメントを削除してねって説明がありますが、時代を感じるところですね。

Appendix A システムコール

この章は色々なシステムコールについての説明です。例えばfork()ならカーネルのどのファイルにこのシステムコールの実装が合って、プロトタイプはこうでシステムコールの仕様はこんなんだよといった感じの説明があります。カーネルよりなmanページと言った感じでしょうか。システムコールはプロセス管理系、ファイルシステム系、プロセス間通信系、メモリ管理系というような感じでカテゴライズされています。

Appendix B カーネル関連のコマンド

manのセクション8というかpsだったりfreeだったりといったコマンドの説明があります。また、pid1のinitプロセスについても説明があります。/etc/inittabの説明もあります。ほかにはstrace、shutdown、ifconfig、traceroute、mountコマンドのなどの説明があります。

Appendix C Procファイルシステム

/procにあるファイルの説明です。14ページほどあるので結構なボリュームですよね。

Appendix D ブートプロセス

BIOSから始まるブートプロセスの説明です。ブートローダーのLILOについての説明もあります。当時はブートローダーといえばLILOでしたね。カーネルの再構築後にLILOのコマンド忘れて起動できないとかよくやったもんです😭 LILOの説明もファイルの書式、オプション、エラーメッセージの意味、起動時の表示メッセージの意味などの説明がありかなり親切です。LIで止まっている場合の原因はXXXだよみたいな説明があってホント親切だと思います。

Appendix E 重要なカーネル関数

カーネルのコードを書く時に使う関数の説明ですmanのセクション9に近いでしょうか。


というわけで1999年発行のLinuxカーネル解説本を見返してみたわけですが、基本的な機能は変わってないのでというかメモリ管理とかファイルシステムは今でも重要な機能ですが実装についてはだいぶ変わっているのでその辺の差分を見ていくとカーネルの進化の歴史を辿れますね。

この目次の内容でLinux 5.10版も見てみたいですね👍 

vgrep便利ですねという話

この記事はLinuxその2 Advent Calendar 2020の4日目の記事です。

今回はtoolネタです。vgrepというgrep系のツールがあって結構便利です。これは今年のOpen Source Summit + Embedded Linux Conference North America 2020でのAsk the Expert SessionでGreg KHさんがコードを調べる時にvgrepを使ってると言っていたので知りました。

使い方はわりと簡単なのでREADME.mdを見ればOKだと思います。

grepとの比較

grep系のコマンドなので速さはどうなの?というところですが、qemu上のこんなriscv64環境(cpu4個、メモリ8GB)で試してみます。fedoraのriscv64版にもvgrepパッケージあります。

masami@fedora-riscv:~/linux-kernel$ uname -a
Linux fedora-riscv 5.10.0-rc7-ktest+ #8 SMP Mon Dec 7 10:35:20 EST 2020 riscv64 riscv64 riscv64 GNU/Linux

vgrepの実行。

masami@fedora-riscv:~/linux-kernel$ sudo sh -c "echo 3 > /proc/sys/vm/drop_caches"
masami@fedora-riscv:~/linux-kernel$ time vgrep --no-less "kmem_cache_alloc(" >/dev/null

real    0m49.499s
user    0m20.440s
sys     1m2.571s

find+grep(with xargs)の実行。

masami@fedora-riscv:~/linux-kernel$ sudo sh -c "echo 3 > /proc/sys/vm/drop_caches"
masami@fedora-riscv:~/linux-kernel$ time (find . -name "*.c" -o -name "*.h" | xargs grep "kmem_cache_alloc(" >/dev/null)

real    1m0.191s
user    0m4.412s
sys     0m48.670s

find+grepはコマンドをパイプ繋いでいるしというのはありますがvgrep良い感じですね。

使ってみる

まずは普通に検索してみます(ここからは普通にx86_64のデスクトップ環境で)。

masami@moon:~/linux-kernel$ vgrep "kmem_cache_alloc("
Index File                                                     Line Content
    0 Documentation/core-api/memory-allocation.rst              163 cache allocator. The cache should be set up with kmem_cache_create() or
    1 arch/arm64/mm/pgd.c                                        54 pgd_cache = kmem_cache_create("pgd_cache", PGD_SIZE, PGD_SIZE,
    2 arch/powerpc/kernel/rtas_flash.c                          713 flash_block_cache = kmem_cache_create("rtas_flash_cache",
    3 arch/powerpc/kvm/book3s_64_mmu_radix.c                   1442 kvm_pte_cache = kmem_cache_create("kvm-pte", size, size, 0, pte_ctor);
    4 arch/powerpc/kvm/book3s_64_mmu_radix.c                   1448 kvm_pmd_cache = kmem_cache_create("kvm-pmd", size, size, 0, pmd_ctor);
    5 arch/powerpc/kvm/book3s_mmu_hpte.c                        377 hpte_cache = kmem_cache_create("kvm-spt", sizeof(struct hpte_cache),
    6 arch/powerpc/mm/init-common.c                             127 new = kmem_cache_create(name, table_size, align, 0, ctor(shift));
    7 arch/powerpc/perf/hv-24x7.c                              1732 hv_page_cache = kmem_cache_create("hv-page-4096", 4096, 4096, 0, NULL);
    8 arch/powerpc/platforms/cell/spufs/inode.c                 786 spufs_inode_cache = kmem_cache_create("spufs_inode_cache",
    9 arch/powerpc/platforms/pseries/setup.c                    315 dtl_cache = kmem_cache_create("dtl", DISPATCH_LOG_BYTES,
   10 arch/powerpc/sysdev/xive/native.c                         518 xive_provision_cache = kmem_cache_create("xive-provision",
   11 arch/s390/kernel/nmi.c                                     86 mcesa_cache = kmem_cache_create("nmi_save_areas", size, size, 0, NULL);
   12 arch/s390/mm/pgalloc.c                                    557 base_pgt_cache = kmem_cache_create("base_pgt", sz, sz, 0, NULL);
   13 arch/s390/pci/pci.c                                       782 zdev_fmb_cache = kmem_cache_create("PCI_FMB_cache", sizeof(struct zpci_fmb),
   14 arch/s390/pci/pci_dma.c                                   635 dma_region_table_cache = kmem_cache_create("PCI_DMA_region_tables",
   15 arch/s390/pci/pci_dma.c                                   641 dma_page_table_cache = kmem_cache_create("PCI_DMA_page_tables",
   16 arch/sh/kernel/cpu/sh4/sq.c                               379 sq_cache = kmem_cache_create("store_queue_cache",
   17 arch/sh/kernel/dwarf.c                                   1171 dwarf_frame_cachep = kmem_cache_create("dwarf_frames",
   18 arch/sh/kernel/dwarf.c                                   1175 dwarf_reg_cachep = kmem_cache_create("dwarf_regs",
   19 arch/sh/kernel/process.c                                   58 task_xstate_cachep = kmem_cache_create("task_xstate", xstate_size,
   20 arch/sh/mm/pgtable.c                                       22 pgd_cachep = kmem_cache_create("pgd_cache",
   21 arch/sh/mm/pgtable.c                                       26 pmd_cachep = kmem_cache_create("pmd_cache",
   22 arch/sparc/mm/tsb.c                                       345 pgtable_cache = kmem_cache_create("pgtable_cache",
   23 arch/sparc/mm/tsb.c                                       358 tsb_caches[i] = kmem_cache_create(name,
   24 arch/x86/events/intel/lbr.c                              1596 return kmem_cache_create("x86_lbr", size, align, 0, NULL);
   25 arch/x86/kvm/mmu/mmu.c                                   5876 pte_list_desc_cache = kmem_cache_create("pte_list_desc",
   26 arch/x86/kvm/mmu/mmu.c                                   5882 mmu_page_header_cache = kmem_cache_create("kvm_mmu_page_header",
   27 arch/x86/kvm/x86.c                                       7876 x86_fpu_cache = kmem_cache_create("x86_fpu", sizeof(struct fpu),
   28 arch/x86/mm/pgtable.c                                     382 pgd_cache = kmem_cache_create("pgd_cache", PGD_SIZE, PGD_ALIGN,
   29 block/bio-integrity.c                                     471 bip_slab = kmem_cache_create("bio_integrity_payload",
   30 block/bio.c                                               107 slab = kmem_cache_create(bslab->name, sz, ARCH_KMALLOC_MINALIGN,

検索結果はデフォルトではページャとしてlessが使われます。表示内容は次のような感じです。

  1. 見つかったファイルの検索結果の番号
  2. ファイルのpath
  3. 見つかった行の行番号
  4. 検索結果の文字列

これだけだと単に検索しただけですが、この検索結果を利用してさらに操作ができます。ここで検索結果27番のファイルをエディタで見たいとします。その場合は次のように-sオプションに検索結果の番号を引数で渡すとEDITOR環境変数に設定されているエディタで該当ファイルの該当行の辺りを開いてくれます。

masami@moon:~/linux-kernel$ vgrep -s 27

全体的に前後の行をみたいという時(grepコマンドの-A、-Bオプションみたいに)はc/contextコマンドを使います。つぎの例だと検索で見つかった行を中心として前後5行ずつ表示します。

masami@moon:~/linux-kernel$ vgrep -s c5
~~~
--- 26 arch/x86/kvm/mmu/mmu.c ---------------------------------------
5877                                        sizeof(struct pte_list_desc),
5878                                        0, SLAB_ACCOUNT, NULL);
5879    if (!pte_list_desc_cache)
5880            goto out;
5881 
5882    mmu_page_header_cache = kmem_cache_create("kvm_mmu_page_header",
5883                                              sizeof(struct kvm_mmu_page),
5884                                              0, SLAB_ACCOUNT, NULL);
5885    if (!mmu_page_header_cache)
5886            goto out;
5887 
--- 27 arch/x86/kvm/x86.c ------------------------------------------
7871            r = -EOPNOTSUPP;
7872            goto out;
7873    }
7874 
7875    r = -ENOMEM;
7876    x86_fpu_cache = kmem_cache_create("x86_fpu", sizeof(struct fpu),
7877                                      __alignof__(struct fpu), SLAB_ACCOUNT,
7878                                      NULL);
7879    if (!x86_fpu_cache) {
7880            printk(KERN_ERR "kvm: failed to allocate cache for x86 fpu\n");
7881            goto out;

特定のファイル(1つもしくは複数)のみを対象にするなら検索結果の番号をさらに引数として渡します。この例では27と28、それに30番を指定してます。

masami@moon:~/linux-kernel$ vgrep -s c3 27-28,30
--- 27 arch/x86/kvm/x86.c ------------------------------------------
7873    }
7874 
7875    r = -ENOMEM;
7876    x86_fpu_cache = kmem_cache_create("x86_fpu", sizeof(struct fpu),
7877                                      __alignof__(struct fpu), SLAB_ACCOUNT,
7878                                      NULL);
7879    if (!x86_fpu_cache) {
--- 28 arch/x86/mm/pgtable.c ---------------------------------------
 379     * page for pgd. We are able to just allocate a 32-byte for pgd.
 380     * During boot time, we create a 32-byte slab for pgd table allocation.
 381     */
 382    pgd_cache = kmem_cache_create("pgd_cache", PGD_SIZE, PGD_ALIGN,
 383                                  SLAB_PANIC, NULL);
 384 }
 385 
--- 30 block/bio.c ------------------------------------------------
 104    bslab = &bio_slabs[entry];
 105 
 106    snprintf(bslab->name, sizeof(bslab->name), "bio-%d", entry);
 107    slab = kmem_cache_create(bslab->name, sz, ARCH_KMALLOC_MINALIGN,
 108                             SLAB_HWCACHE_ALIGN, NULL);
 109    if (!slab)
 110            goto out_unlock;

ディレクトリごとに何個のhitが合ったかなんてのも見れます。

masami@moon:~/linux-kernel$ vgrep -s tree
Matches Directory
    493 
      1 Documentation
      1 Documentation/core-api
     28 arch
      1 arch/arm64
      1 arch/arm64/mm
      9 arch/powerpc
      1 arch/powerpc/kernel
      3 arch/powerpc/kvm
      1 arch/powerpc/mm
      1 arch/powerpc/perf
      2 arch/powerpc/platforms
      1 arch/powerpc/platforms/cell
      1 arch/powerpc/platforms/cell/spufs
      1 arch/powerpc/platforms/pseries
      1 arch/powerpc/sysdev
      1 arch/powerpc/sysdev/xive
      5 arch/s390
      1 arch/s390/kernel
      1 arch/s390/mm
      3 arch/s390/pci
      6 arch/sh
      4 arch/sh/kernel
      1 arch/sh/kernel/cpu
      1 arch/sh/kernel/cpu/sh4
      2 arch/sh/mm
      2 arch/sparc
      2 arch/sparc/mm
      5 arch/x86
      1 arch/x86/events
      1 arch/x86/events/intel
      3 arch/x86/kvm
      2 arch/x86/kvm/mmu
      1 arch/x86/mm
      6 block
    141 drivers
      1 drivers/acpi
      9 drivers/block
      1 drivers/block/aoe
      4 drivers/block/drbd
      1 drivers/block/xen-blkback
      7 drivers/crypto
      1 drivers/crypto/axis
      2 drivers/crypto/caam

別の検索を行いたい場合は最初にやったみたいにvgrep 検索ワードでも出来ますし、g/grepコマンドを利用して再検索することもできます。

masami@moon:~/linux-kernel$ vgrep -s g "bootconfig"
Index File                                                   Line Content
    0 Documentation/admin-guide/bootconfig.rst                  3 .. _bootconfig:
    1 Documentation/admin-guide/bootconfig.rst                 81 overriding the default value by adding (partial) custom bootconfigs
    2 Documentation/admin-guide/bootconfig.rst                 82 without parsing the default bootconfig.
    3 Documentation/admin-guide/bootconfig.rst                126 /proc/bootconfig
    4 Documentation/admin-guide/bootconfig.rst                129 /proc/bootconfig is a user-space interface of the boot config.
    5 Documentation/admin-guide/bootconfig.rst                143 [initrd][bootconfig][padding][size(le32)][checksum(le32)][#BOOTCONFIG\n]
    6 Documentation/admin-guide/bootconfig.rst                149 (``\0``) will be added. Thus the ``size`` is the length of the bootconfig
    7 Documentation/admin-guide/bootconfig.rst                157 loader passes a longer size, the kernel feils to find the bootconfig data.
    8 Documentation/admin-guide/bootconfig.rst                159 To do this operation, Linux kernel provides "bootconfig" command under
    9 Documentation/admin-guide/bootconfig.rst                160 tools/bootconfig, which allows admin to apply or delete the config file
   10 Documentation/admin-guide/bootconfig.rst                163 # make -C tools/bootconfig
   11 Documentation/admin-guide/bootconfig.rst                165 To add your boot config file to initrd image, run bootconfig as below
   12 Documentation/admin-guide/bootconfig.rst                168 # tools/bootconfig/bootconfig -a your-config /boot/initrd.img-X.Y.Z
   13 Documentation/admin-guide/bootconfig.rst                172 # tools/bootconfig/bootconfig -d /boot/initrd.img-X.Y.Z
   14 Documentation/admin-guide/bootconfig.rst                174 Then add "bootconfig" on the normal kernel command line to tell the
   15 Documentation/admin-guide/bootconfig.rst                175 kernel to look for the bootconfig at the end of the initrd file.
   16 Documentation/admin-guide/bootconfig.rst                190 Anyway, since bootconfig command verifies it when appending a boot config
   17 Documentation/admin-guide/bootconfig.rst                237 .. kernel-doc:: include/linux/bootconfig.h
   18 Documentation/admin-guide/bootconfig.rst                238 .. kernel-doc:: lib/bootconfig.c
   19 Documentation/admin-guide/index.rst                      70 bootconfig
   20 Documentation/admin-guide/kernel-parameters.txt         446 bootconfig    [KNL]
   21 Documentation/admin-guide/kernel-parameters.txt         450 See Documentation/admin-guide/bootconfig.rst
   22 Documentation/trace/boottime-trace.rst                   17 this uses bootconfig file to describe tracing feature programming.
   23 Documentation/trace/boottime-trace.rst                   27 .. [1] See :ref:`Documentation/admin-guide/bootconfig.rst <bootconfig>`
   24 Documentation/translations/zh_CN/admin-guide/index.rst   69 bootconfig

vgrepはこんな感じの使い方です。わりとシンプルで使いやすいので気になった人は使ってみてください。

HiFive1 Rev Bを買ったのでRISC-V実機に入門する

この記事はRISC-V Advent Calendar 2020の1日目の記事です。

RISC-Vの実機としてHiFive1 Rev Bを勢いで買いました😊 

f:id:masami256:20201126200139j:plain
HiFive1 Rev B

64bitのRISC-V64GCならLinuxも動くんですけど、HiFive UnleashedはディスコンだしHiFive Unmatchedはまだ出てないので手軽に遊べそうなHiFive1 Rev Bで良いじゃんって感じです。手頃なサイズ感のRISC-V64GCが乗ったボードがあれば欲しいかも。

開発環境を整える

それはさておき、うちのメイン環境はfedora 33です。そして開発環境を整えようと思ってこれらをダウンロードしてセットアップします。

Linux向けと言ってもCentOS版、Ubuntu版とありましたのでCentOS版のほうを選びました。あとはrpmじゃなくてtar.gz形式のファイルを選んで/optに置く感じにしました。 で、SDKはビルドが必要なんですが、fedora33環境でSDKをビルドしようとしたんだけどFIX: build on cpython master branch by tacaswell · Pull Request #128 · python/typed_ast · GitHubに当たってtyped_astの新しいバージョンが出るのを待つか、vmかコンテナ使うかという感じだったのですが、第3の選択肢としてSDKを使わない方法で遊んでみました。

コードを動かしてみる

SDKでビルドしないとなると他に適当なものが必要なので探してみたところサイズも手頃なRustのコードをriscv-rust-quickstartを見つけました。

github.com

動かし方はREADME.md通りにやれば簡単です。

JLinkGDBServerを起動しておいて、

masami@moon:~/projects/hello$ sudo /opt/risc-v-tools/JLink_Linux_V688a_x86_64/JLinkGDBServer -device FE310 -if JTAG -speed 4000 -port 3333 -nogui                                                                                             

cargoコマンドでビルドして実行

masami@moon:~/projects/hello$ cargo run --example hello_world

そうするとgdb server側ではこんな感じにバイナリがダウンロードされ、

J-Link found 1 JTAG device, Total IRLen = 5
JTAG ID: 0x20000913 (RISC-V)
Connected to target
Waiting for GDB connection...Connected to 127.0.0.1                                   
Reading all registers
Received monitor command: reset halt
Expected an decimal digit (0-9)
Downloading 15074 bytes @ address 0x20010000
Downloading 3204 bytes @ address 0x20013AF0
Comparing flash   [....................] Done.
Writing register (pc = 0x20010000)
Starting target CPU...

cargoを実行した方ではこんな感じの出力になります。

masami@moon:~/projects/hello$ cargo run --example hello_world
    Finished dev [unoptimized + debuginfo] target(s) in 0.06s
     Running `riscv64-unknown-elf-gdb -q -x gdb_init target/riscv32imac-unknown-none-elf/debug/examples/hello_world`
/home/masami/.gdbinit:8: Error in sourced command file:
No symbol table is loaded.  Use the "file" command.
Reading symbols from target/riscv32imac-unknown-none-elf/debug/examples/hello_world...
0x20010974 in main () at examples/hello_world.rs:25
25          loop {}
Expected an decimal digit (0-9)
Loading section .text, size 0x3ae2 lma 0x20010000
Loading section .rodata, size 0xc84 lma 0x20013af0
Start address 0x20010000, load size 18278
Transfer rate: 5949 KB/sec, 9139 bytes/write.

そして、uartでデバイスに接続しといたほうではこんな感じで文字列がでます。

masami@moon:~$ sudo picocom -b 115200 /dev/ttyACM0 -q
hello world!

簡単😊

実行の仕組みを調べる

cargo runを実行すると次のログが出ているのでgdbを起動してるのがわかります。

riscv64-unknown-elf-gdb -q -x gdb_init target/riscv32imac-unknown-none-elf/debug/examples/hello_world

この時にgdb_initファイルを指定しています。gdb_initは次のような内容でgdb serverに接続してバイナリを送って処理を続けるような流れになっていました。

set history save on
set confirm off
set remotetimeout 240
target extended-remote :3333
set print asm-demangle on
monitor reset halt
load
continue
# quit

cargoでrunを指定した時にどう動くのかよくわかってないので、どこでgdbコマンドライン作ってんだと思って調べて見ると.cargo/にconfigファイルがありそこに書かれてました。リンカーの設定もありますね。

masami@moon:~/projects/hello$ cat .cargo/config 
[target.riscv32imac-unknown-none-elf]
runner = "riscv64-unknown-elf-gdb -q -x gdb_init"
rustflags = [
  "-C", "link-arg=-Thifive1-link.x",
]

[build]
target = "riscv32imac-unknown-none-elf"

サンプルコードを読む

hello_world.rsを見てみます。30行もありません。

#![no_std]
#![no_main]

extern crate panic_halt;

use riscv_rt::entry;
use hifive1::hal::prelude::*;
use hifive1::hal::DeviceResources;
use hifive1::{sprintln, pin};

#[entry]
fn main() -> ! {
    let dr = DeviceResources::take().unwrap();
    let p = dr.peripherals;
    let pins = dr.pins;

    // Configure clocks
    let clocks = hifive1::clock::configure(p.PRCI, p.AONCLK, 320.mhz().into());

    // Configure UART for stdout
    hifive1::stdout::configure(p.UART0, pin!(pins, uart0_tx), pin!(pins, uart0_rx), 115_200.bps(), clocks);

    sprintln!("hello world!");

    loop {}
}

hello worldの出力に必要なことはriscvとかhifeve1のcrateでサポートされてるんですね。コードとしてはクロックの設定とUARTで出力するための設定してから文字列表示させてるだけですね。この単純明快さは😃

bootの仕組みを調べる

.cargo/confgにリンカの指定があってhifive1-link.xが指定されているのでこれを見てみます。

masami@moon:~/projects/hello$ cat ./target/riscv32imac-unknown-none-elf/debug/build/hifive1-d6f67d95492879dd/out/hifive1-link.x
INCLUDE hifive1-memory.x
INCLUDE link.x

2つのファイルがincludeされてます。

masami@moon:~/projects/hello$ cat ./target/riscv32imac-unknown-none-elf/debug/build/hifive1-d6f67d95492879dd/out/hifive1-memory.x 
INCLUDE memory-fe310.x
MEMORY
{
    FLASH : ORIGIN = 0x20000000, LENGTH = 4M
}

REGION_ALIAS("REGION_TEXT", FLASH);
REGION_ALIAS("REGION_RODATA", FLASH);
REGION_ALIAS("REGION_DATA", RAM);
REGION_ALIAS("REGION_BSS", RAM);
REGION_ALIAS("REGION_HEAP", RAM);
REGION_ALIAS("REGION_STACK", RAM);

/* Skip first 64k allocated for bootloader */
_stext = 0x20010000;

このファイルも最初にmemory-fe310.xをincludeしてます。これはというと、RAMのアドレスが0x80000000から0x80000000+16Kとなってますね。

masami@moon:~/projects/hello$ cat ./target/riscv32imac-unknown-none-elf/debug/build/e310x-9f201268daf213f7/out/memory-fe310.x
MEMORY
{
    RAM : ORIGIN = 0x80000000, LENGTH = 16K
}

そして、hifive1-memory.x に戻ると最初のほうはこんな感じで、FLASHは0x20000000から始まってサイズは4Mとなっていて、TEXT領域やRODATA領域はFLASHのアドレス範囲、BSSなどはmemory-fe310.xで設定してたRAMのアドレス範囲に置かれるというのがわかりました。

MEMORY
{
    FLASH : ORIGIN = 0x20000000, LENGTH = 4M
}

REGION_ALIAS("REGION_TEXT", FLASH);
REGION_ALIAS("REGION_RODATA", FLASH);
REGION_ALIAS("REGION_DATA", RAM);
REGION_ALIAS("REGION_BSS", RAM);
REGION_ALIAS("REGION_HEAP", RAM);
REGION_ALIAS("REGION_STACK", RAM);

最後のところで0x20010000が出てきています。

/* Skip first 64k allocated for bootloader */
_stext = 0x20010000;

このアドレスはcargo runした時に表示されていたstart addressですね。

Loading section .text, size 0x3ae2 lma 0x20010000
Loading section .rodata, size 0xc84 lma 0x20013af0
Start address 0x20010000, load size 18278
Transfer rate: 5949 KB/sec, 9139 bytes/write.

このアドレスはHiFive1 Rev B Getting Started Guideに次のように書かれています。

f:id:masami256:20201203000340p:plain
9.1 Bootloader recovery

bootloarderは0x20000000から始まり、ユーザーのコードは0x20010000から始まると書かれているのでリンカースクリプトのコメントに書いてあった /* Skip first 64k allocated for bootloader */ の意味がわかりますね。

まとめ

Rust良い👍

bootの仕組みもなんとなくわかったので今度はなにか作っていこう…

Tiny Core LinuxでLinuxのinitプロセスが実行されるあたりを調べる

この記事はLinux Advent Calendar 2020 - Qiitaの1日目の記事です。

Tiny Core Linux(以下tcl)を使ってLinuxのブートプロセスを見てましょう。Tiny Core Linuxは軽量ディストリビューションで最小のisoイメージだと11MBほどです😃 ブートプロセスを見ると言っても電源onからの流れではなくてinitプロセスの実行に関する部分です。

この記事ではバージョン11.1のCore-current.iso を利用しています。

www.tinycorelinux.net

isoファイルの構成

まずはtclのisoがどんな感じで構成されていて、Linuxを起動させるのか確認しましょう。 isoファイルの構成はこのようになっています。

$ sudo mount -o loop Core-current.iso ./mnt
$ tree ./mnt/
./mnt/
└── boot
    ├── core.gz
    ├── isolinux
    │   ├── boot.cat
    │   ├── boot.msg
    │   ├── f2
    │   ├── f3
    │   ├── f4
    │   ├── isolinux.bin
    │   └── isolinux.cfg
    └── vmlinuz

2 directories, 9 files

vmlinuzはカーネル、core.gzはinitramfsのファイル、そして起動にはisolinuxを使っているのでそのファイル類が含まれています。

起動の設定はisolinux.cfgに書かれています。

display boot.msg
default microcore
label microcore
        kernel /boot/vmlinuz
        initrd /boot/core.gz
        append loglevel=3

label mc
        kernel /boot/vmlinuz
        append initrd=/boot/core.gz loglevel=3
implicit 0
prompt 1
timeout 300
F1 boot.msg
F2 f2
F3 f3
F4 f4

Tiny Core Linuxnのinitramfsの中身

今回はisoファイルの内容を作業用のディレクトリを作ってそこでファイルを読んだりしていきます。

$ mkdir tcl-work
$ cp -a mnt/boot/ tcl-work/.

core.gzの中も展開します。core.gzはcpioファイルをgzipで圧縮したものなので展開してからcpioコマンドで中身を取り出します。

$ cd tcl-work
$ mkdir core-work
$ cd core-work
$ zcat ../../mnt/boot/core.gz | sudo cpio -i -H newc -dv

展開するとこのようになります。

$ ls -la
total 72
drwxrwxr-x. 17 masami masami 4096 Nov 22 10:30 ./
drwxrwxr-x.  4 masami masami 4096 Nov 22 10:30 ../
drwxr-xr-x.  2 masami masami 4096 Nov 22 10:30 bin/
drwxrwxr-x.  7 masami masami 4096 Nov 22 10:30 dev/
drwxr-xr-x.  8 masami masami 4096 Nov 22 10:30 etc/
drwxrwxr-x.  2 masami masami 4096 Nov 22 10:30 home/
-rwxr-xr-x.  1 masami masami  496 Nov 22 10:30 init*
drwxr-xr-x.  4 masami masami 4096 Nov 22 10:30 lib/
lrwxrwxrwx.  1 masami masami   11 Nov 22 10:30 linuxrc -> bin/busybox*
drwxrwxr-x.  2 masami masami 4096 Nov 22 10:30 mnt/
drwxrwsr-x.  2 masami masami 4096 Nov 22 10:30 opt/
drwxrwxr-x.  2 masami masami 4096 Nov 22 10:30 proc/
drwxrwxr-x.  2 masami masami 4096 Nov 22 10:30 root/
drwxrwxr-x.  3 masami masami 4096 Nov 22 10:30 run/
drwxr-xr-x.  2 masami masami 4096 Nov 22 10:30 sbin/
drwxrwxr-x.  2 masami masami 4096 Nov 22 10:30 sys/
drwxrwxrwt.  2 masami masami 4096 Nov 22 10:30 tmp/
drwxr-xr-x.  7 masami masami 4096 Nov 22 10:30 usr/
drwxrwxr-x.  8 masami masami 4096 Nov 22 10:30 var/

システムの起動に関連しそうなファイルとして /linuxrc/initがありますね。linuxrcはbusyboxコマンドへのリンクで/initはシェルスクリプトになっています。このファイルの内容は後で見てみます。

ちなみにfedora 33のinitramfsはこのようになっていて/linuxrcはありません。

$ /usr/lib/dracut/skipcpio ./initramfs-5.9.9-200.fc33.x86_64.img | zcat | cpio -idv
$ ls -la
total 32680
drwxrwxr-x. 12 masami masami     4096 Nov 22 02:07 ./
drwxrwxr-x.  5 masami masami     4096 Nov 22 12:45 ../
lrwxrwxrwx.  1 masami masami        7 Nov 22 02:07 bin -> usr/bin/
drwxr-xr-x.  2 masami masami     4096 Nov 22 02:07 dev/
drwxr-xr-x. 11 masami masami     4096 Nov 22 02:07 etc/
lrwxrwxrwx.  1 masami masami       23 Nov 22 02:07 init -> usr/lib/systemd/systemd*
lrwxrwxrwx.  1 masami masami        7 Nov 22 02:07 lib -> usr/lib/
lrwxrwxrwx.  1 masami masami        9 Nov 22 02:07 lib64 -> usr/lib64/
drwxr-xr-x.  2 masami masami     4096 Nov 22 02:07 proc/
drwxr-xr-x.  2 masami masami     4096 Nov 22 02:07 root/
drwxr-xr-x.  2 masami masami     4096 Nov 22 02:07 run/
lrwxrwxrwx.  1 masami masami        8 Nov 22 02:07 sbin -> usr/sbin/
-rwxr-xr-x.  1 masami masami     3418 Nov 22 02:07 shutdown*
drwxr-xr-x.  2 masami masami     4096 Nov 22 02:07 sys/
drwxr-xr-x.  2 masami masami     4096 Nov 22 02:07 sysroot/
drwxr-xr-x.  2 masami masami     4096 Nov 22 02:07 tmp/
drwxr-xr-x.  8 masami masami     4096 Nov 22 02:07 usr/
drwxr-xr-x.  3 masami masami     4096 Nov 22 02:07 var/

では/linuxrcと/initはどのように使われるのでしょうか。/initや/linuxrcが利用される仕組みはEarly userspace support — The Linux Kernel documentationに説明があります。tclではinitramfsが利用されてます。この場合、以下の部分が該当します。/initが必須ということですね。

using initramfs. The call to prepare_namespace() must be skipped. This means that a binary must do all the work. 
Said binary can be stored into initramfs either via modifying usr/gen_init_cpio.c or via the new initrd format, an cpio archive. It must be called “/init”. 
This binary is responsible to do all the things prepare_namespace() would do.

起動時のログからもinitramfsが利用されていることがわかります。

tc@box:~$ dmesg | grep initramfs
Trying to unpack rootfs image as initramfs...

Tiny Linux Coreで起動シーケンスの確認

ではこれらの動作を確認してみましょう。 と、その前にqemuGUIを立ち上げるとコンソール出力が見にくいのでシリアルコンソールを有効にしてしまいます。 編集するのはisolinux.confです。このファイルにラベルは2つありますがデフォルトはmicrocoreのほうなのでこちらに設定しましょう。console=ttyS0,115200をappendの行に追加します。 ついでなのでログレベルも最大にしてしまいます。そうするとこのような感じになります。

append loglevel=7 console=ttyS0,115200

つぎにinitramfsのファイルを弄りましょう。シリアルコンソールの設定をetc/inittabに追加します。

ttyS0::respawn:/sbin/getty -nl /sbin/autologin 115200 ttyS0 vt102

そうしたら本題の/iniitと/linuxrcをrenameしてみます。

-rwxr-xr-x.  1 root   root    496 Nov 22 12:14 init.bk*
drwxr-xr-x.  4 root   root   4096 Nov 22 12:14 lib/
lrwxrwxrwx.  1 root   root     11 Nov 22 12:14 linuxrc.bk -> bin/busybox*

core.gzを作成しましょう。

$ sudo find | sudo cpio -o -H newc | gzip -2 > ../boot/core.gz

これで新しいintramfsのイメージもできたのでisoイメージを作成します。 実行はtcl-workディレクトリの1つ上の場所で行います。

$ sudo mkisofs -l -J -R -r -V TC-custom -no-emul-boot -boot-load-size 4 \
 -boot-info-table -b boot/isolinux/isolinux.bin \
 -c boot/isolinux/boot.cat -o test.iso tcl-work/

これでisoファイルが出来たのでqemuで起動します。

$ qemu-system-x86_64 -boot d -cdrom test.iso -m 512 -nographic

見事にカーネルパニックします😃

Floppy drive(s): fd0 is 2.88M AMI BIOS
FDC 0 is a S82078B
blk_update_request: I/O error, dev fd0, sector 0 op 0x0:(READ) flags 0x0 phys_seg 1 prio class 0
floppy: error 10 while reading block 0
VFS: Cannot open root device "(null)" or unknown-block(2,0): error -6
Please append a correct "root=" boot option; here are the available partitions:
0100            8192 ram0 
 (driver?)
0101            8192 ram1 
 (driver?)
0102            8192 ram2 
 (driver?)
0103            8192 ram3 
 (driver?)
0104            8192 ram4 
 (driver?)
0105            8192 ram5 
 (driver?)
0106            8192 ram6 
 (driver?)
0107            8192 ram7 
 (driver?)
0b00         1048575 sr0 
 driver: sr
0200               4 fd0 
 driver: floppy
Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(2,0)
Kernel Offset: disabled
Rebooting in 60 seconds..

まずは/linuxrcの名前だけ戻して起動させます。

VFS: Cannot open root device "(null)" or unknown-block(2,0): error -6
Please append a correct "root=" boot option; here are the available partitions:
0100            8192 ram0 
 (driver?)
0101            8192 ram1 
 (driver?)
0102            8192 ram2 
 (driver?)
0103            8192 ram3 
 (driver?)
0104            8192 ram4 
 (driver?)
0105            8192 ram5 
 (driver?)
0106            8192 ram6 
 (driver?)
0107            8192 ram7 
 (driver?)
0b00         1048575 sr0 
 driver: sr
0200               4 fd0 
 driver: floppy
Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(2,0)
Kernel Offset: disabled

そうするとカーネルパニックとなりました。ではlinuxrcの名前は変更した状態で、/initのほうは名前を戻して起動してみましょう。

   ( '>')
  /) TC (\   Core is distributed with ABSOLUTELY NO WARRANTY.
 (/-_--_-\)           www.tinycorelinux.net

tc@box:~$ e1000 0000:00:03.0 eth0: (PCI:33MHz:32-bit) 52:54:00:12:34:56
e1000 0000:00:03.0 eth0: Intel(R) PRO/1000 Network Connection
e1000: eth0 NIC Link is Up 1000 Mbps Full Duplex, Flow Control: RX

tc@box:~$ ls -la /
total 4
drwxrwxr-x   17 1000     1000           380 Nov 22 05:28 ./
drwxrwxr-x   17 1000     1000           380 Nov 22 05:28 ../
drwxr-xr-x    2 root     root          1380 Nov 22 03:14 bin/
drwxrwxr-x   12 root     staff         4260 Nov 22 05:32 dev/
drwxr-xr-x    8 root     root           680 Nov 22 05:32 etc/
drwxrwxr-x    3 root     staff           60 Nov 22 05:32 home/
-rwxr-xr-x    1 root     root           496 Nov 22 03:14 init
drwxr-xr-x    4 root     root           800 Nov 22 03:14 lib/
lrwxrwxrwx    1 root     root            11 Nov 22 03:14 linuxrc.bk -> bin/busybox
drwxrwxr-x    4 root     staff           80 Nov 22 05:32 mnt/
drwxrwsr-x    2 root     staff          160 Nov 22 03:14 opt/
dr-xr-xr-x   64 root     root             0 Nov 22 05:32 proc/
drwxrwxr-x    2 root     staff           80 Nov 22 03:14 root/
drwxrwxr-x    4 root     staff           80 Nov 22 05:32 run/
drwxr-xr-x    2 root     root          1200 Nov 22 03:14 sbin/
dr-xr-xr-x   12 root     root             0 Nov 22 05:32 sys/
drwxrwxrwt    4 root     staff          120 Nov 22 05:32 tmp/
drwxr-xr-x    7 root     root           140 Nov 22 03:14 usr/
drwxrwxr-x    8 root     staff          180 Nov 22 03:14 var/

今度は起動します。ということでinitramfsを利用する環境では/linuxrcなくてもOKというのが実際の動作からわかりました。

/initの処理

/initは次のような短いシェルスクリプトです。

#!/bin/sh
mount proc
grep -qw multivt /proc/cmdline && sed -i s/^#tty/tty/ /etc/inittab
if ! grep -qw noembed /proc/cmdline; then

  inodes=`grep MemFree /proc/meminfo | awk '{print $2/3}' | cut -d. -f1`

  mount / -o remount,size=90%,nr_inodes=$inodes
  umount proc
  exec /sbin/init
fi
umount proc
if mount -t tmpfs -o size=90% tmpfs /mnt; then
  if tar -C / --exclude=mnt -cf - . | tar -C /mnt/ -xf - ; then
    mkdir /mnt/mnt
    exec /sbin/switch_root mnt /sbin/init
  fi
fi
exec /sbin/init

execコマンドの実行箇所が3箇所ありますが、今回の環境では最初のifブロック部分が実行されます。tclの環境でmountコマンドの出力を見るとこんなふうになってます。

tc@box:~$ mount
rootfs on / type rootfs (rw,size=400480k,nr_inodes=141441)
proc on /proc type proc (rw,relatime)
sysfs on /sys type sysfs (rw,relatime)
devpts on /dev/pts type devpts (rw,relatime,mode=600,ptmxmode=000)
tmpfs on /dev/shm type tmpfs (rw,relatime)
fusectl on /sys/fs/fuse/connections type fusectl (rw,relatime)

この環境では搭載されているメモリサイズに応じたinode数やメモリの何%をファイルシステム容量上限とするかを設定した上でルートファイルシステムを再マウントし直しています。そして/sbin/initを実行します。

先に引用したinitramfsnの説明文で次のような一文がありました。

This binary is responsible to do all the things prepare_namespace() would do.

ここでのnamespaceはコンテナの文脈で使うnamepspaceとは別のものです。prepare_namespace()はルートファイルシステムをマウントしてそこにchrootする処理を行います。initramfsを使う場合はその処理は/initで行ってねということです。

カーネルの動作

起動処理において/initが必要というのはわかりましたが、カーネルからはどう動くのか?というのを見ていきます。tclの11.1ではカーネル5.4.3が使われているのでこのバージョンで確認しましょう。/initを実行するのはinit/main.cのrun_init_process()にて行います。このときの関数の呼ばれ方は次のようになります。

  1. start_kernel()
  2. arch_call_rest_init()
  3. rest_init()
  4. kernel_init()
  5. run_init_process()

start_kernel()の一番最後でarch_call_rest_init()を呼び出して流れていきます。arch_call_rest_init()は名前からしアーキテクチャ毎に実装しする感じですが、s390以外は独自の実装はなくてinit/main.cにあるarch_call_rest_init()を利用しています。

run_init_process()は次のように実行します。

 if (ramdisk_execute_command) {
        ret = run_init_process(ramdisk_execute_command);
        if (!ret)
            return 0;
        pr_err("Failed to execute %s (error %d)\n",
               ramdisk_execute_command, ret);
    }

ramdisk_execute_commandはkernel_init_freeable()にて次のように/initを設定します。

 if (!ramdisk_execute_command)
        ramdisk_execute_command = "/init";

ramdisk_execute_commandのデフォルト値はNULLですが、カーネルコマンドラインパラメータでrdinit=で指定することができます。

static int __init rdinit_setup(char *str)
{
    unsigned int i;

    ramdisk_execute_command = str;
    /* See "auto" comment in init_setup */
    for (i = 1; i < MAX_INIT_ARGS; i++)
        argv_init[i] = NULL;
    return 1;
}
__setup("rdinit=", rdinit_setup);

特に指定がなければ/initが使われます。ということでinitramfsに/initがないとエラーになるのはこれってことがわかります。/initの実行に成功したら0を返してkernel_init()の処理は終了です。 /initは/sbin/initを実行するので本当のinit処理は/initから実行される感じです。

kdump: fedora系カーネルとupstreamカーネルの微妙な差

kdumpで利用するcrashkernelパラメータに渡せる値はfedoraカーネルとupstreamカーネルの微妙な差があるので自分でビルドしたカーネルを使う場合は気をつけろという自分へのメモです。

kdumpをfedorarhelなどでdumpを利用する場合、crashkernelに渡す値にautoが利用できます。rhel8のドキュメント(How should the crashkernel parameter be configured for using kdump on Red Hat Enterprise Linux 8 ? - Red Hat Customer Portal)だと通常はautoを指定するのを勧めてますね。autoを指定するとcrashkernelに指定するメモリ量をよしなに設定しれくれます。それってどこでやってんの?ということですが、fedoraの場合は0001-kdump-add-support-for-crashkernel-auto.patchが処理してます。このパッチの中の↓の部分がautoを指定された場合の処理です。

diff --git a/kernel/crash_core.c b/kernel/crash_core.c
index d631d22089ba..c252221b2f4b 100644
--- a/kernel/crash_core.c
+++ b/kernel/crash_core.c
@@ -258,6 +258,20 @@ static int __init __parse_crashkernel(char *cmdline,
    if (suffix)
        return parse_crashkernel_suffix(ck_cmdline, crash_size,
                suffix);
+
+   if (strncmp(ck_cmdline, "auto", 4) == 0) {
+#ifdef CONFIG_X86_64
+       ck_cmdline = "1G-64G:160M,64G-1T:256M,1T-:512M";
+#elif defined(CONFIG_S390)
+       ck_cmdline = "4G-64G:160M,64G-1T:256M,1T-:512M";
+#elif defined(CONFIG_ARM64)
+       ck_cmdline = "2G-:512M";
+#elif defined(CONFIG_PPC64)
+       ck_cmdline = "2G-4G:384M,4G-16G:512M,16G-64G:1G,64G-128G:2G,128G-:4G";
+#endif
+       pr_info("Using crashkernel=auto, the size choosed is a best effort estimation.\n");
+   }
+
    /*
    * if the commandline contains a ':', then that's the extended
    * syntax -- if not, it must be the classic syntax

見ての通りコマンドラインの内容を書き換えてるだけですね。設定内容はドキュメントにも追加されてます。

diff --git a/Documentation/admin-guide/kdump/kdump.rst b/Documentation/admin-guide/kdump/kdump.rst
index 2da65fef2a1c..d53a524f80f0 100644
--- a/Documentation/admin-guide/kdump/kdump.rst
+++ b/Documentation/admin-guide/kdump/kdump.rst
@@ -285,6 +285,17 @@ This would mean:
     2) if the RAM size is between 512M and 2G (exclusive), then reserve 64M
     3) if the RAM size is larger than 2G, then reserve 128M

+Or you can use crashkernel=auto if you have enough memory.  The threshold
+is 2G on x86_64, arm64, ppc64 and ppc64le. The threshold is 4G for s390x.
+If your system memory is less than the threshold crashkernel=auto will not
+reserve memory.
+
+The automatically reserved memory size varies based on architecture.
+The size changes according to system memory size like below:
+    x86_64: 1G-64G:160M,64G-1T:256M,1T-:512M
+    s390x:  4G-64G:160M,64G-1T:256M,1T-:512M
+    arm64:  2G-:512M
+    ppc64:  2G-4G:384M,4G-16G:512M,16G-64G:1G,64G-128G:2G,128G-:4G


 Boot into System Kernel

upstreamカーネルの場合はautoは当然解釈されないので適切にメモリ量を設定できません😨 ということで、利用したい機能がどこからきているか調べないとハマる場合があるということでした。