Think Essentially

計算機テクノロジー全般が好きです

【Rust】与えられた浮動小数点数が整数か確認する

結論

浮動小数点数系列型のtrunc()を使う

概説

truncは、計算機科学の領域において端数処理という意味らしい。以下は公式のtrunc()の説明文。非整数値は、常に0に向かって切り捨てられることがポイントのよう。

Returns the integer part of `self`. 
This means that non-integer numbers are always truncated towards zero.

題目を関数にするならば、以下のようになる

fn is_int(value: f64) -> bool {
    let int_part = value.trunc(); // 浮動小数点数から整数部分を取得
    int_part == value             // 整数部分と元の値を比較
}

補足

Rustの浮動小数点数の扱いに関しては、以下の記事が非常に有用

rsk0315.hatenablog.com

x86_64 Linux アセンブリ入門

体裁

以下は"hello, world!"を出力するhello.asmの内容である

section .data
    text db "Hello, world"

section .text
    global _start  ; リンカがエントリポイントを識別するために設定される

; 以下はラベル, 慣例的に_を接頭辞とする
_start:
    mov rax, 1     ; ID: 1 --> sys_write
    mov rdi, 1     ; ファイルディスクリプタ, 1なので標準出力
    mov rsi, text  ; textはアドレスである。つまり"hello, world"が書き込まれたアドレスを指定している。
    mov rdx, 12    ; 指定アドレスから読み込む文字数
    syscall        ; システムコール発行

; ここからシステム終了のためのsysコールを呼ぶ
    mov rax, 60    ; ID: 60 --> sys_exit
    mov rdi, 0     ; エラーコード = 0で終了。つまり正常終了
    syscall

アセンブル

nasm -f elf64 -o hello.o hello.asm
  • -f: アセンブルターゲットファイルの指定
  • -o: 出力ファイルの名前指定

リンク

ただ.asmをアセンブルをしただけでは実行は出来ない。リンカを用いて関連ファイルをリンクすることで初めて実行ファイルとなる。 恐らくシステムコールのラベルと命令フローを定義したファイル等をリンクしていると思われる。

ld hello.o 

上記により、helloファイルが生成される

実行

./hello
Hello, world% 

参考

x86_64 Linux Assembly #1 - "Hello, World!" - YouTube

マイクロカーネル HinaOS にシステムコールを実装する

はじめに

前回に引き続き、怒田晟也さん著“自作OSで学ぶマイクロカーネルの設計と実装”のIDEAS.mdより、マイクロカーネルHinaOSにシステムコールを追加します。

実装概要

割り込みの発生回数を取得するシステムコールを実装します。

あたりをつける

kernel/syscall.cシステムコール定義場所になっています。このファイル内容を見る限り、sys_uptime関数がカーネル内の処理に関連する統計情報を返す関数になっているので、この関数まわりを踏襲することで、目的を達成できそうです。

// 起動してからの経過時間をミリ秒単位で返す。
static int sys_uptime(void) {
    return uptime_ticks / TICK_HZ;
}

実装

1. ユーザランド側の関数呼び出し定義

システムコールは、32bitのシステムコール番号で識別され、指定のレジスタに格納されたシステムコール番号に応じてカーネルモード時に処理を行います。libs/common/types.h にその定義場所があるので、定義します。

#define SYS_INTERRUPT_COUNT    18

次に、libs/user/syscall.c に以下の関数を定義します。特に渡したい値はないので、0 ~ 4番目の引数は0にします。最後の引数はカーネルモード時に関数を実行する識別子になるので、先ほど定義した変数を入れます。

// interruptシステムコール: システムの割り込み回数の取得
int sys_interrupt_count(void) {
    return arch_syscall(0, 0, 0, 0, 0, SYS_INTERRUPT_COUNT);
}

2. 割り込み回数カウントを定義

グローバル変数uptime_ticksは、interrupt.hに定義されているので、同様に割り込み回数カウントをinterrupt.hに定義します。ちなみに、extern 修飾子は外部ファイルから参照可能な変数であることを表します。

extern unsigned uptime_ticks;
extern unsigned interrupt_count; // 割り込み回数の総数

上記に加え、interrupt.cファイル内でもinterrupt_countを宣言&初期化しないと怒られます。これは、ヘッダファイル内のexternで定義と初期化が同時にできないためです。

unsigned interrupt_count = 0;

3. 統計情報取得実装

割り込み・例外ハンドラのエントリポイントとなるkernel/riscv32/trap.criscv32_handle_trap関数の頭に先ほど定義したinterrupt_countのインクリメント処理を加えます。

void riscv32_handle_trap(struct riscv32_trap_frame *frame) {
    stack_check();  // スタックオーバーフローをチェック
    interrupt_count++;
    ...

4. システムコール定義

libs/user/にて、ユーザランド側のシステムコールのインタフェースとなるAPIを定義します。

// libs/user/syscall.h

int sys_interrupt_count(void);
// libs/user/syscall.c

static int sys_interrupt_count(void) {
    return interrupt_count;
}

5. システムコールハンドラ追記

/kernel/syscall.chandle_syscallにて、システムコール番号のパターンマッチを行う箇所があるので、そこにケースを追記します。

// システムコールハンドラ
long handle_syscall(long a0, long a1, long a2, long a3, long a4, long n) {
    long ret;
    switch (n) {
          // ...
        case SYS_INTERRUPT_COUNT:
            ret = sys_interrupt_count();
            break;
          // ...
    }

    return ret;
}

6. コマンドを定義する

シェルサーバに割り込み回数を取得するコマンドを定義します。interruptは文字数が長く、タイポする可能性があるので、intptというコマンドとして定義します。

static void do_interrupt_count(struct args *args) {
    printf("%d times\n", sys_interrupt_count());
}

// ...

static struct command commands[] = {
        // ...
    {.name = "intpt", .run = do_interrupt_count, .help = "get interrupt count"},
    // ...
};

7. 実行

shell> intpt
15279 times
shell> intpt
18689 times
shell> intpt
36608 times

一秒あたり2000回ほど割り込まれています。タイマ割り込みも含まれているので、このような数字になるのだと思います。

マイクロカーネル HinaOS に新規サーバを実装する

はじめに

“自作OSで学ぶマイクロカーネルの設計と実装”におけるレポジトリ内のIDEAS.mdに非常に刺激的な課題が記載されていたので、チャレンジしようと思います。ちなみに、当書籍は懇切丁寧にソースコードの解説が記載されており、OS実装の学習に最適であると思います。

また、私は課題に対して頭を使って試行錯誤する過程が一番力を付くと考えているので、大まかに全体に目を通して読み、まだ完璧には内容を理解していない状態で、課題に取り組んでいます。

実装概要

初歩的な取り組みとして、int型のデータを送ると、インクリメントして返すincサーバを立てます。

実装フロー

IDEAS.md 参照より、具体的な実装フローは以下のようになります。

1. `messages.idl`に新しいメッセージを定義し、一回 `make` を実行してメッセージの型定義を自動生成する。
2. `servers`ディレクトリに新しいサーバのディレクトリを作成する (`servers/pong`をコピーアンドペーストするとよい)。
3. サーバの実装を書く。
3. Makefileの`BOOT_SERVERS`変数に新しいサーバを追加する。すると、起動時に自動的にサーバが起動するようになる。
4. クライアント側の実装を書く。たとえばコマンドラインシェルにサーバと通信する新しいコマンドを追加する。

この手順に従い、サーバを実装していきます。

1. messages.idlに新しいメッセージを定義する

リモートプロシージャコール (rpc) 用のメッセージを定義します。文法自体はRust言語の関数定義のようです。

rpc inc(sock: int) -> (sock: int);

上記のような関数を定義し、makeを実行します。すると、libs/common/ipcstub.h において、

  • サーバへのメッセージ
  • サーバの返答用メッセージ

の2つに対応する構造体と定数が自動生成されました。

// libs/common/ipcstub.h 

struct inc_fields {
    int value;
};
struct inc_reply_fields {
    int value;
};

...

#define INC_MSG 63
#define INC_REPLY_MSG 64

2. serversディレクトリに新しいサーバのディレクトリを作成する

ヒントに従いpongディレクトリをコピペし、名前をincに。build.mkの効用はまだ分からないですが、まとめてコピーします。

3. サーバの実装を書く。

pingの実装を踏襲します。内容は非常にシンプルなもので、受け取った値をインクリメントして返すというものです。

// servers/inc/main.c

void main(void) {
    ASSERT_OK(ipc_register("inc"));
    TRACE("ready");

    while (true) {
        struct message m;
        ASSERT_OK(ipc_recv(IPC_ANY, &m));
        switch (m.type) {
            case INC_MSG: {
                DBG("received ping message from #%d (value=%d)", m.src,
                    m.inc);

                m.type = INC_REPLY_MSG;
                m.inc_reply.value = m.inc.value++;
                ipc_reply(m.src, &m);
                break;
            }
            default:
                WARN("unhandled message: %s (%x)", msgtype2str(m.type), m.type);
                break;
        }
    }
}

主にIPCで扱うメッセージの構成は以下のようになります。union型内で示されているIPCSTUB_MESSAGE_FIELDSは、メッセージの種類、typeに対応する構造体になります。

struct message {
    int32_t type;  // メッセージの種類 (負の数の場合はエラー値)
    task_t src;    // メッセージの送信元
    union {
        uint8_t data[0];  // メッセージデータの先頭を指す
        /// 自動生成される各メッセージのフィールド定義:
        //
        //     struct { int x; int y; } add;
        //     struct { int answer; } add_reply;
        //     ...
        //
        IPCSTUB_MESSAGE_FIELDS
    };
};

4. シェルコマンドの実装

シェルから、incサーバにアクセスするコマンドを実装します。これもまた非常にシンプルで、inc <value> といった文法です。

// servers/shell/main.c

static void do_inc(struct args *args) {
    if (args->argc != 2) {
        WARN("Usage: inc <VALUE>");
        return;
    }

    task_t inc_server = ipc_lookup("inc");
    int value = atoi(args->argv[1]);

    // incサーバにメッセージを送信する
    struct message m;
    m.type = INC_MSG;
    m.inc.value = value;
    ASSERT_OK(ipc_call(inc_server, &m));

    // incサーバからの応答が想定されたものか確認する
    printf("reply: %d\n", m.inc_reply.value);
    ASSERT(m.type == INC_REPLY_MSG);
}

実装した関数をもとに、コマンド設定に追記します。.nameがシェルに打つコマンド名、.run_incがそのバイナリに対応する処理、.helpがhelpコマンドを打ち込んだ際に表示される説明文ですね。

// servers/shell/main.c

static struct command commands[] = {
        ...
    {.name = "inc", .run = do_inc, .help = "incremnt given number"},
    ...
};

自動サーバに追記を行います。

# Makefile

# 自動起動するサーバのリスト
BOOT_SERVERS ?= fs tcpip shell virtio_blk virtio_net pong inc

動作確認

いざ、実行! 正常に機能しました!これでHina OS のハックに成功しました!笑

まとめ

Hina OSは大変面白い題材です。是非次の課題にも取り組んでいこうと思います。

参考文献

https://www.amazon.co.jp/自作OSで学ぶマイクロカーネルの設計と実装-怒田晟也-ebook/dp/B0C52SFYDC/ref=sr_1_1?__mk_ja_JP=カタカナ&crid=17NO089IHFDL0&keywords=マイクロカーネル&qid=1687662077&s=books&sprefix=マイクロカーネル%2Cstripbooks%2C166&sr=1-1

【Rust】条件に応じた標準出力関数の挿入

概要

バイナリクレートを開発していて、特定の条件上でのみprintln!を用いたい時がある。そういう時は、debug-assersionを用いる。

手法

1. cargo.toml の追記

cargo.tomlにて、以下を追記する。

[profile.dev]
debug-assertions = true

[profile.dev]は、デフォルトの cargo run コマンドに適用されるprofile設定。debug-assertions の真偽値によって、表示するべきか、そうでないかが決定される。

2. #[cfg(debug_assertions)] の挿入

main.rsにて、以下のように条件に応じて表示切替を行いたいprintlnの上に#[cfg(debug_assertions)]を挿入。

#[cfg(debug_assertions)]
println!("debug print");

上記により、デバッグで何かしらの値を表示させたいときはcargo.tomlにてdebug-assertionsをいじればよい。 *1

*1:もしかしたら、dbg_println!みたいなcfg(debug_assetions)がprintlnに組み込まれたマクロが既にあるかもしれない。適宜調査を行う

JIT/AOTコンパイル

JITコンパイラと見聞してイマイチピンとこなかったので、まとめる

JIT;Just In Time

実行時に中間コード等のバイトコード機械語に変換すること。 実行時に変換・翻訳が含まれるので、当然後者と比べると実行時間は長くなる。 Java等がこれに含まれる。

AOT;Ahead Of Time

時前にコンパイルを行い、機械語を出力すること。 C, C++, Rust等がこれに当てはまる。

【コンピュータシステムの理論と実装】第4章

機械語に理解するために,以下の3つの抽象化についてだけ考えればよい.

1. メモリ

  • コンピュータで,データや命令を保存するハードウェアデバイスを指す
  • 各セルはワードロケーションとも呼ばれ,それぞれはユニークなアドレスを持つ
  • Memory[address], RAM[address], M[address]と表現する

2. プロセッサ

  • プロセッサ = 中央演算装置,central-processing-unit, CPU
  • このプロセッサの演算対象となる値のことをオペランド演算子という.

3. レジスタ

  • register, 登録所,仮置き場みたいな

個人的な例え

メモリ,プロセッサ,レジスタ,ストレージをビュッフェレストランで例える.

個人的な印象として,

  • プロセッサ → ご飯を食べる人
  • レジスタご飯を食べる人のテーブルの上のお皿
  • メモリ → フロアに置いてあるビュッフェ
  • ストレージ → 厨房の冷蔵庫

って感じがする.

長期的に保存する食べ物は冷蔵庫に貯蓄し,その冷蔵庫の中にある食べ物は何らかの処理・加工を施してビュッフェとしてフロアに配置される. ご飯を食べる人はそのビュッフェから一時的に自分のお皿に盛り付け,その食べ物を処理する(食べていく).ご飯を食べる人からすると,ビュッフェは少し遠いもので,直接処理することは出来ない.一方で 机上の食事はすぐ手の届く位置にあり,処理しやすい.

機械語

バイナリコードは,人にとっては暗号めいているため,通常,バイナリコードニーモニックの両方を用いて表される

ニーモニック

ADDとか,基本的に英語三文字の記号のこと.

  • レジスタは基本的にR0, R1, R2 っていう表現をするよ
  • 記号による抽象化によって,コードを読むだけじゃなくて,書くことも出来るようになるよ.
  • 記号による表記をアセンブリ言語っていうよ.単にassemblyっていうこともあるよ
  • アセンブリから機械語に変換するプログラムをassemblerっていうよ
ADD R0, R1, R2 // R0 = R1 + R2
AND R0, R1, R2 // R0 = R1 && R2

メモリアクセス

メモリアクセスを行うコマンドは2種類に分類される

  1. 算術演算,論理演算
  2. 読み込み・格納

1. 直接アドレシング (direct addressing)

アドレスを読み取って,メモリに格納されている情報を読み取る

LOAD R1, 67 // R1 = Memmory[67]
LOAD R1, foo // R1 = Memmory[67] (変数を読み込ませる)

2. イミディエイトアドレッシング (immediate addressing)

定数を読み込む.

LOADI R1, 54 // R1 = 54

3. 間接アドレッシング (indirect addressing)

  • ポインタを扱う際に用いるよ
  • 高水準コード(C言語)における特定の配列を操作する際に用いるよ.
コンパイラの配列操作の仕組みをおさらい
  1. 高水準言語でfooという変数名で配列を宣言し,コンパイラに通す
  2. コンパイラは,その foo と名付けられた 配列データ のメモリ領域(セグメント)を割り振る
  3. セグメント*1のベースを参照する foo という記号を作る

つまり, (高水準言語を書いた)人から見たfoo配列データを指すんだけど, 機械から見たfoo配列データの入口を示すアドレス値を指すんだね

// セグメントのベースアドレスを参照する例
// x = foo[j] もしくは,x = *(foo + j) の変換

ADD R1, foo, j // R1 = foo + j
LOAD R2, R1    // R2 = Memory[R1]
STR R2, x      // x = R2

分岐命令

分岐命令の3つの手段

  • 反復
  • 条件分岐
  • サブルーチン呼び出し
    • 機械語は,プログラムの指定された位置へ戻る手段を持つ必要がある
    • つまり,戻る指標が必要

Hack機械語の仕様

  • Hackコンピュータ = ノイマン型のプラットフォーム

Hack メモリアドレス空間

  • Hackは二つの異なるメモリ空間を持つ
    • 命令メモリ (instruction memory) → 読み込み専用,ROM (read only memory)
    • データメモリ (data memory)
  • 両方とも16bit幅,サイズは32K
  • CPUは,命令メモリだけ読み込める

Hack レジスタ

このHackレジスタを図書館の司書さんで例えてみる.

  • よく動く司書さん → Aレジスタ (以下,A司書さん)
  • あまり働かない司書さん → Dレジスタ (以下,D司書さん)

A司書さんはよく働きます.きびきびと,アドレス(番地)を読み取って,その番地に対応する本棚から本を探して抜き取ります(参照).そして自分で本を持ちます. D司書さんはそんなにきびきび動きません.いちいち動いたり,高い棚にある本を少し背伸びして取るなんて面倒なので,基本的に本を持つことしかしません.Aレジスタさんから「これちょっと持ってて~」と言われて本を持つだけです.

Hack記法

レジスタと同様に2種類の命令から構成されるよ

A命令 (アドレス命令,address instruction)

  • @hoge とかにあたる. 以下の3つの役割を持つ

  • 定数代入

  • メモリ操作
  • 移動命令

C命令 (計算命令,compute instruction)

  • bar;hoge がこれにあたる
  • 111a cccc ccdd djjj というフォーマット
    • 111 → これは C命令だよ,を表す
    • a cccc cc → comp 領域 =「何,どのように計算するのか」
    • dd d → dest 領域 = 「それをどこに格納するのか」→ Aレジスタ or Dレジスタ or Mレジスタ
    • jjj → jump領域 = 「次に何を行うのか」

コードの最小単位は

@hoge
M = 7
  • @hogeなどの@ + 文字列の場合,それはメモリのラベルを表す
  • @7などの@ + 数値の場合,Aに値そのものが入る
  • この2行でA司書さんは,番地を読み取って,その番地に対応する本棚から本を取り出して本を一時的に持ちます

その他やってて思ったこと

  • @hoge などのラベルでも,機械側は結局 @16 とかに変換している
  • 出力をきちんとRAM[2]に指定しないとアカン.テストで気づいた.
  • CPU emulater にて Speed を Max に設定すると なんか楽しい(高速で行ったり来たりする)

*1:セグメント = 領域

test

test

test