Linux 5.14.4のregressionがどんな感じだったのか調べる

Linux 5.14.4のリリースして同日にregressionが報告されて次の日には5.14.5がリリースされてたんですが、これがどんなバグだったのかなというのを調べたメモです。

バグ報告の内容としては5.14.4でNextcloudっていうphpのwwebアプリケーションを実行するとハングアップするということでした。このときにbisectも行われていて、[PATCH 5.14 011/334] posix-cpu-timers: Force next expiration recalc after itimer resetがbad commitというところまで判明してました。スレッドではpatchの作者さんによる修正patchを送ったよというメールもありますが、5.1.4.5では関連するpatchのrevertにて対応されています。

5.14.5でrevertされたのは2つのpatchでした。5.14.5はこのregressionの修正のみのリリースでした。

まずbad commitのほうでこれはposix-cpu-timers: Force next expiration recalc after itimer resetですね。こちらはこのようなpatchでした。

diff --git a/kernel/time/posix-cpu-timers.c b/kernel/time/posix-cpu-timers.c
index 517be7fd175e..a002685f688d 100644
--- a/kernel/time/posix-cpu-timers.c
+++ b/kernel/time/posix-cpu-timers.c
@@ -1346,8 +1346,6 @@ void set_process_cpu_timer(struct task_struct *tsk, unsigned int clkid,
            }
        }
 
-       if (!*newval)
-           return;
        *newval += now;
    }

もう一つはtime: Handle negative seconds correctly in timespec64_to_ns()です。こちらはこんな感じのpatchでした。

--- a/include/linux/time64.h
+++ b/include/linux/time64.h
@@ -25,7 +25,9 @@ struct itimerspec64 {
 #define TIME64_MIN            (-TIME64_MAX - 1)
 
 #define KTIME_MAX         ((s64)~((u64)1 << 63))
+#define KTIME_MIN          (-KTIME_MAX - 1)
 #define KTIME_SEC_MAX         (KTIME_MAX / NSEC_PER_SEC)
+#define KTIME_SEC_MIN          (KTIME_MIN / NSEC_PER_SEC)
 
 /*
  * Limits for settimeofday():
@@ -124,10 +126,13 @@ static inline bool timespec64_valid_sett
  */
 static inline s64 timespec64_to_ns(const struct timespec64 *ts)
 {
-   /* Prevent multiplication overflow */
-   if ((unsigned long long)ts->tv_sec >= KTIME_SEC_MAX)
+   /* Prevent multiplication overflow / underflow */
+   if (ts->tv_sec >= KTIME_SEC_MAX)
        return KTIME_MAX;
 
+   if (ts->tv_sec <= KTIME_SEC_MIN)
+       return KTIME_MIN;
+
    return ((s64) ts->tv_sec * NSEC_PER_SEC) + ts->tv_nsec;
 }

patchだけだとわかりにくいので全体を見てみましょう。。。まずはset_process_cpu_timer()から。revert後の5.14.5ではこんな関数です。

void set_process_cpu_timer(struct task_struct *tsk, unsigned int clkid,
               u64 *newval, u64 *oldval)
{
    u64 now, *nextevt;

    if (WARN_ON_ONCE(clkid >= CPUCLOCK_SCHED))
        return;

    nextevt = &tsk->signal->posix_cputimers.bases[clkid].nextevt;
    now = cpu_clock_sample_group(clkid, tsk, true);

    if (oldval) {
        /*
        * We are setting itimer. The *oldval is absolute and we update
        * it to be relative, *newval argument is relative and we update
        * it to be absolute.
        */
        if (*oldval) {
            if (*oldval <= now) {
                /* Just about to fire. */
                *oldval = TICK_NSEC;
            } else {
                *oldval -= now;
            }
        }

        if (!*newval)
            return;
        *newval += now;
    }

    /*
    * Update expiration cache if this is the earliest timer. CPUCLOCK_PROF
    * expiry cache is also used by RLIMIT_CPU!.
    */
    if (*newval < *nextevt)
        *nextevt = *newval;

    tick_dep_set_signal(tsk, TICK_DEP_BIT_POSIX_TIMER);
}

↓の部分がpatchが変更する箇所ですね。

     if (!*newval)
            return;
        *newval += now;

patchではif文のところを消して常にnewvalが指す値にnowの値を足すようになってます。元のコードではnewvalの指す値が0なら何もしないでreturnするんですが、patchではif文を消してるので常にその先が実行されるようになりますね。そうすると、今までは呼ばれることがなかったtick_dep_set_signal()が実行されるようになって本来送る必要のないシグナルを送ってしまうと修正patchのコミットメッセージにも書かれています。これがregressionとして現れてたんですかねぇ。

もう一つのrevertはtime: Handle negative seconds correctly in timespec64_to_ns()です。こちらの変更箇所はtimespec64_to_ns()です。これもrevert後のコードを見るとこんな感じになってます。

static inline s64 timespec64_to_ns(const struct timespec64 *ts)
{
    /* Prevent multiplication overflow */
    if ((unsigned long long)ts->tv_sec >= KTIME_SEC_MAX)
        return KTIME_MAX;

    return ((s64) ts->tv_sec * NSEC_PER_SEC) + ts->tv_nsec;
}

patchでの変更は次のようになっています。

-    /* Prevent multiplication overflow */
-   if ((unsigned long long)ts->tv_sec >= KTIME_SEC_MAX)
+   /* Prevent multiplication overflow / underflow */
+   if (ts->tv_sec >= KTIME_SEC_MAX)
        return KTIME_MAX;
 
+   if (ts->tv_sec <= KTIME_SEC_MIN)
+       return KTIME_MIN;
+

timespec64構造体のtv_secはtime64_tでこれは__s64のtypedefとなってます。ということでtv_secは符号ありの64bit整数値です。ということで、コミットメッセージにも書いてますが、もともとはs64の値をunsigned long long(64bitの符号なし整数ですね)にキャストしてチェックしているので負の整数の場合にKTIME_MAXを返すことになりますと。で、patchでは符号ありの整数としてKTIME_SEC_MAXを超えたり、KTIME_SEC_MIN以下になる場合には上限値や下限値を返す感じにしています。ここだけ見るとそうだなと思うんですが、今までと返る値が変わることによる影響もあったんでしょうか。。。

いずれにせよ、5.14.5では2つのpatchのrevertが行われてregressionが修正されたということでした。

Raspberry Piのdtoverlay・dtparam、dtbそしてブートプロセスのメモ

 はじめに

この記事はRaspberry Pi 3B+の実際の挙動と公式のドキュメントから大体こんな感じだろうというところで書いてるので正確さは期待しないでください。

下図のような構成でLinux kernel(Raspberry Pi向けのカーネルじゃなくて、mainlineとかstable treeのカーネル)を使うときにdtoverlay・dtparamを使う方法を調べたメモです。なので、自分でビルドしたカーネルを使う必要がなければRaspberry Pi OSとかmeta-raspberrypiを使うのが良いかと思います。

-> Raspberry Piのブートローダー  
  -> u-boot 
    -> Linux kernel

Linux kernel source とdtbファイル

mainlineのカーネルにはbcm2837-rpi-3-b-plus.dts等のdtsファイルがあるのでこれをビルドして使えばmainlineのカーネルでもRaspberry Piで動きます。だがしかし、config.txtでdtoverlay・dtparamを使うのはできませんでした。なんでかというと、Raspberry Piカーネルとmainlineのカーネルにあるdtsファイルを見比べるとRaspberry Piカーネルのほうにはoverridesがあって設定をoverrideできるようになってるんですね。例えばarm/boot/dts/bcm2710-rpi-3-b-plus.dtsだとこんな感じで設定があります。

/ {
    __overrides__ {
        act_led_gpio = <&act_led>,"gpios:4";
        act_led_activelow = <&act_led>,"gpios:8";
        act_led_trigger = <&act_led>,"linux,default-trigger";

        pwr_led_gpio = <&pwr_led>,"gpios:4";
        pwr_led_activelow = <&pwr_led>,"gpios:8";
        pwr_led_trigger = <&pwr_led>,"linux,default-trigger";

        eee = <&eth_phy>,"microchip,eee-enabled?";
        tx_lpi_timer = <&eth_phy>,"microchip,tx-lpi-timer:0";
        eth_led0 = <&eth_phy>,"microchip,led-modes:0";
        eth_led1 = <&eth_phy>,"microchip,led-modes:4";
        eth_downshift_after = <&eth_phy>,"microchip,downshift-after:0";
        eth_max_speed = <&eth_phy>,"max-speed:0";
    };
};

上記のブロックはmainlineのほうにはありません。そんなわけでdtoverlay・dtparamを手軽に使うならdtbファイルはブートローダーなんかと一緒に配布されてるdtbファイル(firmwareのリポジトリ)を使いましょうという感じです。

dtoverlay・dtparamはどのように処理されてるか

Raspberry Piブートローダーが自身のデバイスに合う適切なdtbファイルを読んでくれます(公式ドキュメントのどこかにそんなことが書いてありました)。そして、config.txtに記載されてるdtoverlay・dtparamの記述に沿ってメモリ上に読み込まれてるdtbファイルのデータを更新します。そしてそのメモリ上で更新したdtbファイルを使うという流れです。

u-bootをブートローダーとして使いたい場合

Raspberry Piブートローダーはすでにdtbファイルを読み込んでdtoverlay・dtparamの設定もメモリ上で反映させてます。そのため、u-bootがfatloadとかして自分でdtbファイルを読んじゃうと意味がありません。なので、config.txtでdtbファイルを読み込むアドレスを指定し、u-bootのほうはそのアドレスからdtbファイルを読む感じにします。例えばconfig.txtで下記のように読み込むアドレスを指定しておきます。

device_tree_address=0x02600000

u-bootのほうはこんな感じでfdtコマンドを使ってconfig.txtで指定したメモリアドレスを利用します。

setenv fdt_addr_r 0x02600000
fdt addr ${fdt_addr_r}

こんな感じにすればRaspberry Piブートローダーが設定したdtbファイルをu-bootから読めて、それを更にカーネルの起動に利用することができます。

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の仕組みもなんとなくわかったので今度はなにか作っていこう…