ぱたへね

はてなダイアリーはrustの色分けができないのでこっちに来た

ゼロからのOS自作入門(その5)RustでCの構造体を受け取る

ゼロからのOS自作入門(その4)osbook_day03c の続き。

natsutan.hatenablog.com

4日目はUEFIアプリからグラフィックスの情報をカーネルに渡します。今、UEFIアプリがC、カーネルをRustで書いているので、CとRustの間で構造体のやりとりが必要です。

std::os::raw を使う

gihyo.jp

実践Rust入門を見ると、#[repr(C)]とstd::os::rawのやり方が紹介されています。

本を見ながら、こんな感じで書いてみました。

use std::os::raw::c_int;
use std::os::raw::c_uint;
use std::os::raw::c_uchar;

#[repr(C)]
struct FrameBufferConfig {
    frame_buffer : *mut c_uchar,
    pixels_per_scan_line : c_int,
    horizontal_reslution: c_uint,
    vertical_resolution: c_uint,
    pixel_format : PixelFormat
}

実際にやってみるとエラーがでます。

error[E0433]: failed to resolve: use of undeclared crate or module `std`
 --> src/main.rs:7:5
  |
7 | use std::os::raw::c_int;
  |     ^^^ use of undeclared crate or module `std`

#![no_std] を設定しているので、stdから始まるクレートが使えないようです。

困っていたらTwitterで的確なアドバイスをいただきました。いつもありがとうございます。

ctyを使う

使い方はとても簡単で、cty::から使いたい型をuseするだけ。

use cty::{uint32_t, c_uchar};

#[derive(Debug, Copy, Clone)]
#[repr(C)]
pub struct FrameBufferConfig {
    frame_buffer : *mut c_uchar,
    pixels_per_scan_line : uint32_t,
    horizontal_reslution: uint32_t,
    vertical_resolution: uint32_t,
    pixel_format : PixelFormat
}


#[no_mangle]
pub extern "C" fn KernelMain(frame_buffer_config: *mut FrameBufferConfig) -> ! {

    let fb_buffer_config = unsafe {*frame_buffer_config};

こんな記述でUEFI側を変更すること無く構造体の受け渡しが出来ました。

C言語側の構造体定義はこうなっています。

struct FrameBufferConfig {
    uint8_t *frame_buffer;
    uint32_t pixels_per_scan_line;
    uint32_t horizontal_reslution;
    uint32_t vertical_resolution;
    enum PixelFormat pixel_format;
};

C言語側もint等を使わずにビット幅を指定しているので、上手く渡せているようです。

これでUEFIからもらったグラフィックスの情報を使って、座標を指定して色を塗ることが出来るようになりました。

f:id:natsutan:20210509103009j:plain

全ソース

カーネル側の全ソースです。四日目分はまだ作業中。

#![no_std]
#![no_main]

#![feature(asm)]
#![feature(abi_efiapi)]

use cty::{uint32_t, c_uchar};

extern crate rlibc;
extern crate panic_halt;


#[derive(Debug, Copy, Clone)]
#[repr(C)]
enum PixelFormat {
    KPixelRGBResv8bitPerColor,
    KPixelBGRResv8BitPerColor
}

#[derive(Debug, Copy, Clone)]
#[repr(C)]
pub struct FrameBufferConfig {
    frame_buffer : *mut c_uchar,
    pixels_per_scan_line : uint32_t,
    horizontal_reslution: uint32_t,
    vertical_resolution: uint32_t,
    pixel_format : PixelFormat
}


#[derive(Debug, Copy, Clone)]
struct PixelColor {
    r : u8,
    g : u8,
    b : u8
}


fn pixel_offset(x:u32, y:u32, config: &FrameBufferConfig) -> u32 {
    4 * (config.pixels_per_scan_line * y + x)
}

fn write(x:u32, y:u32, pixel:PixelColor, config: &FrameBufferConfig) {
    let offset = pixel_offset(x, y, config);
    unsafe {
        *(config.frame_buffer).offset(offset as isize) = pixel.r;
        *(config.frame_buffer).offset(offset as isize + 1) = pixel.g;
        *(config.frame_buffer).offset(offset as isize + 2) = pixel.b;
    }
}


#[no_mangle]
pub extern "C" fn KernelMain(frame_buffer_config: *mut FrameBufferConfig) -> ! {

    let fb_buffer_config = unsafe {*frame_buffer_config};

    let white = PixelColor{r:255, b:255, g:255};
    for y in 0..fb_buffer_config.vertical_resolution {
        for x in 0..fb_buffer_config.horizontal_reslution {
            write(x, y, white, &fb_buffer_config);
        } 
    }
    
    let green = PixelColor{r:0, g:255, b:0};
    for y in 0..200 {
        for x in 0..200 {
            write(x, y, green, &fb_buffer_config);
        } 
    }


    loop {
        unsafe {
            asm!("hlt")
        }
    }
}

基礎から学ぶ組込Rust

基礎から学ぶ組込Rustの本を読みました。久しぶりに組込プログラマに戻れた気がしてとても楽しめました。

www.c-r.com

どんな本

Wio TerminalをRustで制御していく本です。

  • 組込がある程度分かっていて、組込Rustに興味がある人
  • Rustはある程度分かっていて、組込に興味ある人

この辺の人達がターゲットで、Rustに関しては別にもう一冊本があった方が良いです。 どちらも初めてだとだいぶきついと思います。

読む前は、組込向けの最適化の話、Cのライブラリとの連携とか込み入った話の本かなと思っていたのですが、いざ読んでみるとどちらかというと入門書と呼べる内容でした。

Rustの説明があっさりあったあと、6章のペリフェラル制御からが本番です。Rustの文法そのものよりはこういう形でRustの機能を上手く使ってますという事例が丁寧に説明されていました。

各ペリフェラル(周辺回路)の簡単な紹介、回路図を使った接続情報の確認、動かすために必要な設定、それをRustでどう書くのかが続いた後、練習問題的なやってみようがあります。順番にやるだけで、Wio Terminalのいろんな機能を実際に動かして試すことが出来ます。基本的に全てお膳立てされていて、本の通りにloopの中を記入すると動きます。順番に動かしていくだけでもとても楽しかったです。

Rustの記述について

基本的には実績のあるクレートを使っているため、あんまりハードウェアに密接なコードは出てこないですし、はまりがちな所有権の問題などもクリアーされているコードから始めています。

take()を使ったシングルトン(ペリフェラルの排他制御)の実装や、panic handlerでエラー情報を出す所は面白かったです。未だに仕組みがわからないのが、GPIOの所で出てきた型状態プログラミング(Typestate Programming)です。 これを使うと、GPIOの設定を出力にしていない状態で信号を出力すると「コンパイル時」にエラーになります。

割り込みの所は、メインルーチンと割り込みハンドラでシェアするオブジェクトの扱いが動くソースコードになっていて、実務で使うときは参考にしたいです。

unsafeにする所も、こういう単位でunsafeを使うんだと勉強になりました。

組込プログラミングに関する記述について

実は、こっちがメインじゃないかというくらい良かったです。

回路図の説明、データシートの記述から、デバイスを制御するために必要な情報が整理されています。 実際、初めてやる人がこの記述だけで他のデバイスを制御できるかというとだいぶ厳しいですが、それでもこういう所を確認しないといけないんだなというのはなんとなく伝わると思います。非組込プログラマ向けに、こういう所をやさしく書いている本は少ないのでとても貴重だと思います。

新人の頃、タイマーで時間を計ろうとしたらカウンターの説明にしか読めなくて途方にくれたことを思い出しました。(いや実際にカウンターなんですが・・・)

加速度センサーや光センサーなど、今まで業務で使ったことがないセンサーを動かせて楽しかったです。

出来た物

画像表示

グラフィックスのライブラリを使って、こんな表示が出来ます。 先にシミュレータで表示できることを確認し、クレイト化してから、実機に表示しています。

f:id:natsutan:20210426004301j:plain

スペアナ

謎のスペアナが出来ました。マイクから音を拾ってリアルタイムにFFTしています。

f:id:natsutan:20210426004317j:plain

まとめ

とても面白い本だったので、興味ある人はぜひ本を買って動かしてみてください。 実際にデバイスやセンサーが動く楽しさは読んでいるだけでは伝わらないです。

↓念のため正誤表も確認してください

WebAssembly.instantiateStreamingの第二引数

WebAssemblyがなかなか動かず、一つ分かったことがあるのでメモ書き。

emccが出力するファイル

www.manning.com

WebAssembly In ActionよりEmscriptenが出力するファイルは、以下の3つパターンのどれかになります。

  • WebAssembly module, JavaScript plumbing file, HTML template
  • WebAssembly module, JavaScript plumbing file
  • WebAssembly module

一番最後のWebAssembly Moduleだけを出力した場合は、実行時にdynamic linkingが行われます。dynamic linkingを実現するためには、side moduleを作る事必要があります。

side moduleを作るには、emccの-s SIDE_MODULE = 2 のオプションを指定し、Cの標準ライブラリとJavaScript JavaScript plumbing fileを使わない事を指示します。関数名をExportするために、emccに -s EXPORTED_FUNCTIONS=['_ai'] のようなオプションも必要です。

ここで、JavaScriptが良く分からないのでミニマムな環境を作ろうと思い、一番下の方法でやってみたらずいぶん長い間はまってしまいました。

WebAssembly.instantiateStreaming

本を見たり、他のサイトを見ているとこんな感じで、wasmファイルを呼び出していると思います。

const importObject = {
  env: {
    __memory_base: 0,
  }
};


WebAssembly.instantiateStreaming(fetch("ai.wasm"), importObject).then(result =>  {
    const value = result.instance.exports._ai();
    console.log("ai.wasm returns" + value)
  }).catch (err => {
    console.log(err)
  }
  )

これを実行すると、instantiateStreamingの第二引数(importObject)でエラーになります。

例えばこんなエラー

TypeError: WebAssembly.instantiate(): Import #3 module="GOT.mem" error: module is not an object or function

検索してみるとimportObject は省略可能と書いてあったり、env.tableが要るんだよとかいろんな情報が出てきます。

例えばこういうのです。

const importObj = {
  module: {},
  env: {
    memory: new WebAssembly.Memory({ initial: 256 }),
    table: new WebAssembly.Table({ initial: 0, element: 'anyfunc' })
  }
};

いろいろためしても全く動かないし、そもそもGOT.memって何だろうと悩み続けてようやく分かりました。

instantiateStreamingの二つめの引数は、一つ目の引数でfetchしたwasmファイルが動作するのに必要な環境を渡す必要があります。 何が正解かというのは、wasmファイルの作り方であったり、その内容によって変ってきます。

ちなみに、GOT.memでエラーが出ていたwasmファイルをwasm2watで見てみると、確かにmoduleの中で何か使おうとしています。

natu@Honoka:~/q/myproj/wasm-qumico/app_client/public$ wasm2wat ai.wasm
    (module
  (type (;0;) (func))
  (type (;1;) (func (result i32)))
  (import "env" "__stack_pointer" (global (;0;) (mut i32)))
  (import "env" "__memory_base" (global (;1;) i32))
  (import "env" "__table_base" (global (;2;) i32))
  (import "GOT.mem" "x" (global (;3;) (mut i32)))
  (import "env" "memory" (memory (;0;) 0))
  (import "env" "__indirect_function_table" (table (;0;) 0 funcref))
  (func (;0;) (type 0)
    call 1)

で、作ったwasmファイルに適切なimportObjectを作るのが大変なので、emccはwasmファイルと同時にJavaScript plumbing fileを生成してくれます。

実際に生成されたJavaScriptを見てみると、単にimportObjectを作るだけでなく、初期化など複雑な処理もやっていました。

というわけで振り出しに戻ります。

ゼロからのOS自作入門(その4)osbook_day03c

natsutan.hatenablog.com

の続きです。

    for i in 0..fb_size {
        unsafe {
            *adr.offset(i as isize) = (255 % i) as u8;
        }
    }

このループで暴走していると思ってましたが、(255 % i)が逆。

  (i % 256) as u8;

が正解でした。元のコードだとi=0で0除算が発生していて暴走していました。分かってしまえばたいした事はありませんでした。

無事Rustでフレームバッファの塗りつぶしに成功 f:id:natsutan:20210416231124p:plain

ゼロからのOS自作入門(その3) Rustで書いたカーネルをブートさせる

ゼロからのOS自作入門(その2) メモリマップの取得 の続きです。

natsutan.hatenablog.com

Cで書かれたフレームバッファを塗りつぶすカーネルを作っていたら、UEFIアプリは無理でもOSだけならRustで書けるんじゃと思ってやってみました。Rustで書かれたカーネルに制御が移りフレームバッファを書き換えて画面表示を変えるところまで動きました。 後から続く人に参考になると思い、試行錯誤の手順を書いておきます。Rustは初心者です。

参考Webサイト こちらの記事が非常に役に立ちました。ありがとうございます。

www.akiradeveloper.com

この記事のベースとなっているバージョンは day03bです。

github.com

UEFIアプリは本のコードを写経して、カーネルだけRustで書いています。 とりあずRustのカーネルがブートしたと自信持てるまでは、Cで書いた確実にブートするカーネルを用意して比較するのが良かったです。

UEFIアプリの修正

修正したのは二箇所です。

読み込むカーネルのファイル名を修正

  // #@@range_begin(read_kernel)
    EFI_FILE_PROTOCOL* kernel_file;
//    root_dir->Open(root_dir, &kernel_file, L"\\kernel.elf", EFI_FILE_MODE_READ, 0);
    root_dir->Open(root_dir, &kernel_file, L"\\rust_kernel", EFI_FILE_MODE_READ, 0);

Qemuの起動スクリプト内で、上手いことリネームしてくれていると勝手に思い込んでいました。ちゃんと自分で作ったカーネルのファイル名に変更しましょう。

EntryPointの表示

    UINT64 entry_addr = *(UINT64*)(kernel_base_addr + 24);

    typedef void EntryPointType(UINT64, UINT64);
    EntryPointType* entry_point = (EntryPointType *)entry_addr;
    Print(L"Entry point: 0x%0lx\n", entry_point);

    entry_point(gop->Mode->FrameBufferBase,  gop->Mode->FrameBufferSize);

読み込んだカーネルのエントリーポイントを表示するようにしました。ここが表示されなければ、カーネル(ファイル)を読み込めてません。 ブートするかどうかを確認するのにUEFIは止める必要が無いので、UEFIの無効化もいったんentry_point()の後に回しました。 カーネルのブートが確認出来たら元に戻しましょう。

Rust側

ここは動いたファイルだけあれば十分ですね。これを参考に動かしてください。(Rust初心者なので良く分からないです)

cargo.toml

[package]
name = "rust_kernel"
version = "0.1.0"
authors = ["natu"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
rlibc = "1"
panic-halt = "0.2"

.cargo/config.toml

[build]
target = "x86_64-unknown-linux-gnu"

[profile.release]
panic = "abort"

[profile.dev]
panic = "abort"

[target.x86_64-unknown-linux-gnu]
linker = "ld.lld"

rustflags = [
    # Build Options
    "-C", "no-redzone=yes",
    "-C", "relocation-model=static",

    # Linker Options
    "-C", "link-arg=--entry=KernelMain",
    "-C", "link-arg=--image-base=0x100000",
    # "-C", "link-arg=-n",
    # "-C", "link-arg=-nmagic",
    #"-C", "link-arg=-znorelro",
]

main.rs

参考BlogのAkira Hayakawaさんは、Rustで書いたUEFIアプリからRustのカーネルを呼び出していましたが、僕はCのUEFIアプリからRustのカーネルを呼び出そうとしています。 UFEIアプリからはCのカーネルと同じように見える必要があります。ですので、関数宣言はpub extern "C"としています。

どうもiterationで落ちるようで、ループ無しでフレームバッファをいじってみました。abi_efiapiは要らない気がしますが、とりあえず動いた状態そのまま。

#![no_std]
#![no_main]

#![feature(asm)]
#![feature(abi_efiapi)]

extern crate rlibc;
extern crate panic_halt;

#[no_mangle]
pub extern "C" fn KernelMain(fb_addr:u64, _fb_size: u64) -> ! {
    let adr = fb_addr as *mut u8;
    unsafe {
        *adr.offset(3200 + 0) = 255  as u8;
        *adr.offset(3200 + 1) = 255  as u8;
        *adr.offset(3200 + 2) = 255  as u8;
        *adr.offset(3200 + 3) = 255  as u8;
        *adr.offset(3200 + 4) = 255  as u8;
        *adr.offset(3200 + 5) = 255  as u8;
        *adr.offset(3200 + 6) = 255  as u8;
        *adr.offset(3200 + 7) = 255  as u8;
        *adr.offset(3200 + 8) = 255  as u8;
        *adr.offset(3200 + 9) = 255  as u8;
        *adr.offset(3200 + 10) = 255  as u8;
        *adr.offset(3200 + 11) = 255  as u8;
        *adr.offset(3200 + 12) = 255  as u8;
        *adr.offset(3200 + 13) = 255  as u8;
        *adr.offset(3200 + 14) = 255  as u8;
        *adr.offset(3200 + 15) = 255  as u8;
    }

//    for i in 0..fb_size {
//        unsafe {
//            *adr.offset(i as isize) = (255 % i) as u8;
//        }
//    }

    loop {
        unsafe {
            asm!("hlt")
        }
    }
}

動かすために確認したこと

ELFヘッダーの確認

生成されたELFファイルのヘッダーをCのカーネルと、Rustのカーネルで比較しました。 ヘッダーの表示には、readelfコマンドを使います。

だいたい一緒になるはずです。 ヘッダーに含まれるEntry point addressが、UFEIアプリの中でちゃんと読めていることを確認しました。

Cカーネルのヘッダー

natu@Honoka:~/q/myproj/mikanos/kernel$ readelf -h kernel.elf
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x101000
  Start of program headers:          64 (bytes into file)
  Start of section headers:          11936 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         4
  Size of section headers:           64 (bytes)
  Number of section headers:         16
  Section header string table index: 14

Rustカーネルのヘッダー

natu@Honoka:~/q/myproj/mikanos/rust_kernel$ readelf -h target/x86_64-unknown-linux-gnu/debug/rust_kernel
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x101000
  Start of program headers:          64 (bytes into file)
  Start of section headers:          10520 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         6
  Size of section headers:           64 (bytes)
  Number of section headers:         15
  Section header string table index: 13

Rustカーネルのエントリーポイントの確認

Rustのカーネルを逆アセンブルして、Entry pointがRustのKernelMainになっているかを確認しました。 逆アセンブル表示は、objdump -d を使います。

natu@Honoka:~/q/myproj/mikanos/rust_kernel$ objdump -d target/x86_64-unknown-linux-gnu/debug/rust_kernel

target/x86_64-unknown-linux-gnu/debug/rust_kernel:     file format elf64-x86-64


Disassembly of section .text:

0000000000101000 <KernelMain>:
  101000:       48 81 ec a8 00 00 00    sub    $0xa8,%rsp
  101007:       48 89 bc 24 80 00 00    mov    %rdi,0x80(%rsp)
  10100e:       00
  10100f:       48 89 bc 24 90 00 00    mov    %rdi,0x90(%rsp)

OKですね。

Qemuが動かなくなったとき

最初の頃Rustで作ったカーネルが暴走して、その後Qemuが起動中に止まってしまうようになりました。 実績のあるCカーネルも起動しなくなったのでQemuに問題があると判断、良く分からないですがOVMF_VARS.fdの内容が変ってしまっているようです。何気なくgit statusしてみたら気がつきました。

github.com

ここからOVMF_VARS.fdを持ってきて、$HOME/osbook/devenv のファイルを上書きすればOKです。無事qemuが立ち上がるようになりました。

ブート画面

f:id:natsutan:20210414094238p:plain

左上に一行開けて白い線が表示されています。やりました。

この後

どうも最初に暴走していた所が、このループのようです。

    for i in 0..fb_size {
        unsafe {
            *adr.offset(i as isize) = (255 % i) as u8;
        }
    }

逆アセを見ると、iterっぽい関数が呼ばれていてそこで死んでそうでした。(動いてからの想像)

stackが上手く設定で来てなくて関数呼び出しが出来ないのか、rangeで作られるIteratorがnew的なサポートが必要なのか、そんな想像はしています。 次はrangeを動かして、画面を塗りつぶしたいです。

追記:loopではなく、(255 % i) as u8で落ちてそうです。

pdf2audiobook

Twitter で気がついた佐藤さんのpdf読み上げシステムの動画を見ました。

www.youtube.com

PDFをテキストにして読み上げサービスを使用して音声ファイルに変換しています。

このシステムで使用しているGoogleのテクノロジーです。ひとつひとつは聞いたことあります。

  • Cloud functions
  • Cloud Storage
  • OCR with Vision API
  • AutoML Table
  • Text to Speech

ここで上手いなと思ったところがAutoML Tableによるtext(paragraph)の識別です。

20年くらい前に、FPGAのデータシートを読むのにPDFをテキストに変換してPDAで読むというのをやっていました。 ここで問題になるのが、各ページにはいるヘッダー、ページ番号、図表です。

最初は戸惑ったのですが、なれてくると自力で飛ばせるようになります。ヘッダーはほぼ同じ内容ですし、ページ番号は一定間隔で数字が出てきて、図表は改行だらけのテキストが来たら図表だなと自分で判断して飛ばすことが出来るようになります。今回、ここの部分をAutoML Tableで上手くやっているのが驚きのポイントですね。

自分が苦労しただけあって、あーここでDeep Learning使うんだという感動がありました。 社内のDX推進とかそういうのも良いけど、今の生活がちょっと良くなる技術の使い方はもっともっと可能性あると信じています。

詳しく知りたい人は今すぐGoogle I/Oに登録しましょう。

events.google.com

emsdk のアップデート

emsdk メモ

バージョン等を確認するとき

emsdk list

update するとき

emsdk update

githubから持ってきた時はgit pullで最新にできる。

WSLに入っていたemsdkをアップデートした

emsdk listでバージョンを確認。

アップデート前

The *recommended* precompiled SDK download is 2.0.9 (d8e430f9a9b6e87502f826c39e7684852f59624f).

アップデート後

The *recommended* precompiled SDK download is 2.0.16 (80d9674f2fafa6b9346d735c42d5c52b8cc8aa8e).

アップデート成功