2026年2月16日月曜日

Rust@naked関数

Rust@naked関数

multiboot仕様でブートローダを自作せずGRUBから自作カーネル(もどき)を起動できたので、起動情報も色々受け取りたい。multiboot準拠していれば、起動直前にブートローダがレジスタEAXにマジック値を、レジスタEBXに起動情報へのアドレスを設定してくれているそう。

やりたいこと

  • レジスタEAXに設定されたマジック値を変数に代入する
  • レジスタEBXに設定された起動情報アドレスを変数に代入する

_start関数をnaked関数化

普通に_start関数が動くとコンパイラ君が色々設定してくれる(スタックとか?)のでCPUレジスタが破壊されてしまう。chatGPT君に聞いたらnaked関数にすればいいよ!と教えてくれた。
naked関数はコンパイラ君が色々設定してくれるコードを生成しない関数のことなのだけど、実際はどう違うんだろうか。

通常関数のコード

#![no_std]
#![no_main]

#[unsafe( no_mangle )]
pub extern "C" fn _start() -> !
{
    let num = 0xDEAD_BEEF;
    test( num );
}

#[unsafe( no_mangle )]
fn test( _num: u32 ) -> !
{
    loop {}
}

_startをloopだけにしたらjmp命令しか生成してくれなかったので、一個関数かますことに・・・。

naked関数のコード

#![no_std]
#![no_main]

use core::arch::naked_asm;

#[unsafe( naked )]
#[unsafe( no_mangle )]
pub unsafe extern "C" fn _start() -> !
{
    naked_asm!( "
        cli
        hlt
    " )
}

ディスアセンブリしてみる

以下のようなコマンドで。

objdump -S -M intel kernel | less

通常関数

00100030 <_start>:
  100030:       53                      push   ebx
  100031:       83 ec 08                sub    esp,0x8
  100034:       e8 00 00 00 00          call   100039 <_start+0x9>
  100039:       5b                      pop    ebx
  10003a:       81 c3 2f 00 00 00       add    ebx,0x2f
  100040:       c7 44 24 04 ef be ad    mov    DWORD PTR [esp+0x4],0xdeadbeef
  100047:       de
  100048:       c7 04 24 ef be ad de    mov    DWORD PTR [esp],0xdeadbeef
  10004f:       e8 0c 00 00 00          call   100060 <test>
  100054:       cc                      int3
  100055:       cc                      int3
  100056:       cc                      int3
  100057:       cc                      int3
  100058:       cc                      int3
  100059:       cc                      int3
  10005a:       cc                      int3
  10005b:       cc                      int3
  10005c:       cc                      int3
  10005d:       cc                      int3
  10005e:       cc                      int3
  10005f:       cc                      int3

00100060 <test>:
  100060:       8b 44 24 04             mov    eax,DWORD PTR [esp+0x4]
  100064:       eb fe                   jmp    100064 <test+0x4>

100030はebxがcallee-savedなのでEBXの保存。
100031は_start関数のスタック確保。
100034は何がしたいんだろう…スタックに戻り先アドレス(100039)を入れたい?んだろうけど
100039で戻り先アドレス(100039)をebxに入れている。ここもまだこれだけだと謎。
10003aでebx+=0x2F。100068になるわけだけど、それはtest関数のちょうど終わりのアドレスだ。謎。お試しコードが特殊(無限ループ)すぎて本当はなにをしようとしているかわからない。忘れよう。
100040はlet numに0xDEAD_BEEFを代入しているところ。
100048はtest関数の引数用にnumをコピーしているとこかな。

naked関数

00100030 <_start>:
  100030:       fa                      cli
  100031:       f4                      hlt

うん、なんにもしないね。

EAXとEBXを引数で渡す

rustのABIは未安定(今後変わるかもしれない)なので、CのABI(もっと明示的にcdeclじゃなくていいんかな・・・)を指定した関数mainを関数_startから呼び出してやる。
EAXとEBXはスタックに右から引数として積んであげれば渡せる。

#[unsafe( naked )]
#[unsafe( no_mangle )]
pub unsafe extern "C" fn _start() -> !
{
    naked_asm!( "
        push ebx
        push eax
        call main
        cli
        hlt" );
}

#[unsafe( no_mangle )]
extern "C" fn main( eax: u32,
                    ebx: u32  ) -> !
{
    let magic = eax;
    loop {}
}

アセンブリで見る

00100030 <_start>:
  100030:       53                      push   ebx
  100031:       50                      push   eax
  100032:       e8 09 00 00 00          call   100040 <main>
  100037:       fa                      cli
  100038:       f4                      hlt
  100039:       cc                      int3
  10003a:       cc                      int3
  10003b:       cc                      int3
  10003c:       cc                      int3
  10003d:       cc                      int3
  10003e:       cc                      int3
  10003f:       cc                      int3

00100040 <main>:
        hlt
  100040:       8b 44 24 08             mov    eax,DWORD PTR [esp+0x8]
  100044:       8b 44 24 04             mov    eax,DWORD PTR [esp+0x4]
  100048:       eb fe                   jmp    100048 <main+0x8>

たぶんいい感じ。

2026年2月8日日曜日

Rust@Hello x86!!

Rust@Hello x86!!

OS自作をRustに切り替えるかはまだ模索中だけれど、まずはx86-pc上で「Hello x86!!」を表示するカーネルこと始めをしてみた。

やりたいこと

  • OSとカーネルはGitリポジトリを別にする
  • IBM PC/AT互換機上(QEMU)で動作させる
  • ブートにGNU GRUBを使用する
  • カーネルのファイル形式はELFを使用する

環境を作る

nightly版rustをインストールしておく

参考:付録G: Rustの作られ方と“Nightly Rust”

$ rustup install nightly

ディレクトリを作る

MochiOSというプロジェクトディレクトリ配下に作っていく。
とりあえず、OSで使用するカーネルは別プロジェクトとしたいので、depsディレクトリ内にkernelプロジェクトcargoで作る。
また、kernelはnightly版rustを使用する前提にしておく。

$ mkdir MochiOS
$ cd MochiOS
$ git init
$ mkdir deps
$ cd deps
$ touch .gitkeep
$ cargo new kernel
$ cd kernel
$ rustup override set nightly

OSプロジェクトではdeps配下のディレクトリを管理したくないので、.gitignore、deps/.gitkeepを作成しておく。

# .gitignore

/deps/*

今は作成しないけど、deps配下の各サブプロジェクトはクローンしてくるシェルスクリプトを作る予定。

「Hello x86!!」を表示するコードを書く

標準ライブラリの無効化、Panic処理の実装、VGAのテキストバッファに文字列「Hello x86!!」の書込みを行って無限ループ。

// MochiOS/deps/kernel/src/main.rs

#![no_std]
#![no_main]

use core::panic::PanicInfo;

const VGA_BUFFER: *mut u8 = 0xB8000 as *mut u8;

#[panic_handler]
fn panic( _info: &PanicInfo ) -> !
{
    loop {}
}

#[unsafe( no_mangle )]
pub extern "C" fn _start() -> !
{
    let str = b"Hello x86!!";

    for ( idx, &byte ) in str.iter().enumerate() {

        unsafe {
            *VGA_BUFFER.add( idx * 2 )     = byte;
            *VGA_BUFFER.add( idx * 2 + 1 ) = 0x0F;
        }

    }

    loop {}
}

multibootヘッダを書く

これ書いとけばGRUBがカーネルをロードしてくれるってよ。(自前でローダ用意しなくていいから楽だなぁ)

// MochiOS/deps/kernel/src/multiboot.rs

#[repr( C ) ]
pub struct MultibootHeader {
    magic   : u32,
    flags   : u32,
    checksum: i32,
}

#[unsafe( link_section = ".multiboot" )]
#[unsafe( no_mangle )]
pub static _MULTIBOOT_HEADER: MultibootHeader = MultibootHeader {
    magic   : 0x1BADB002,
    flags   : 0,
    checksum: -( 0x1BADB002 as i32 ),
};

ファイル追加したのでmain.rsにモジュール宣言を追記。

// MochiOS/deps/kernel/src/main.rs

module multiboot

コンパイルする

ターゲットファイルを作る

MochiOS/deps/kernel/src/x86_32.json(JSONってコメント書けないからパス表示を欄外に・・・)

{
    "llvm-target"         : "i686-unknown-none",
    "data-layout"         : "e-m:e-p:32:32-p270:32:32-p271:32:32-p272:64:64-i128:128-f64:32:64-f80:32-n8:16:32-S128",
    "target-pointer-width": 32,
    "arch"                : "x86",
    "vendor"              : "unknown",
    "os"                  : "none",
    "linker"              : "rust-lld",
    "linker-flavor"       : "ld.lld",
    "disable-redzone"     : true,
    "panic-strategy"      : "abort"
}

リンカスクリプトを書く

// MochiOS/deps/kernel/src/x86_32.lds

ENTRY( _start )

MEMORY {
    KERNEL : ORIGIN = 0x00100000, LENGTH = 0x100000
}

SECTIONS {
    .multiboot : {
        KEEP( *( .multiboot ) )
    } > KERNEL

    .text : {
        *( .text* )
    } > KERNEL

    .rodata : {
        *( .rodata* )
    } > KERNEL

    .data : {
        *( .data* )
    } > KERNEL

    .bss : {
        *( .bss* )
        *( COMMON )
    } > KERNEL
}

cargoの設定を書く

# MochiOS/deps/kernel/.cargo/config.toml

[unstable]
build-std        = [ "core" ]
json-target-spec = true

[build]
target = "./src/x86_32.json"

[target.'cfg( all( target_arch = "x86" ) )']
rustflags = [ "-C", "link-arg=-Tsrc/x86_32.lds" ]

実行する

イメージファイルのディレクトリを作る

$ cd MochiOS
$ mkdir -p iso/boot/grub

カーネルを置く

$ copy deps/kernel/target/x86_32/debug/kernel iso/boot/

grub.confを作る

# MochiOS/iso/boot/grub/grub.cfg

set timeout=0
set default=0

menuentry "MochiOS" {
    multiboot /boot/kernel
    boot
}

イメージファイルを作る

$ grub-mkrescue -o mochios.iso iso/

QEMUで実行する

デスクトップ環境でなはないので、VNCサーバを立ち上げて別PCからリモート接続。

$ qemu-system-i386 -cdrom mochios.iso -vnc :0

enter image description here
できた。

手こずりpoints

ldスクリプトが反映されない

試行錯誤中にreadelf -a kernelして確認すると.multibootセクションが作られていなくて。
原因は、cargo君がファイル依存関係でldスクリプトを知らないので、更新されてもコンパイルし直しが発生しなかった。
なので、下記コマンドで対応。

$ cargo clean
$ cargo build

(っていうかtargetディレクトリの容量がえげつないなrust・・・coreとかbinutlもクロス先用にコンパイルしているからなんだろうけど)

流石に、手で毎回clean打つのはめんどくさいので、ldスクリプトファイルをcargo君に監視してもらうようにした。

# MochiOS/deps/kernel/Cargo.toml

[package]
build = "build.rs"
# MochiOS/deps/kernel/build.rs

fn main()
{
    println!( "cargo::rerun-if-changed=./src/x86_32.lds" );
}

grub-rescue実行してもISOファイルが生成されない

下記インストールで対応

$ sudo apt install grub-pc-bin xorriso
  • grub-pc-binは、「DebianやUbuntuなどのLinux環境において、BIOSベースのPCでGRUB 2ブートローダを使用するために必要な、GRUBのコアイメージ(モジュール)群を収録したバイナリパッケージ」だそう。
  • xorriso(えぐぞりっそ)は、「ISO 9660(Rock Ridge拡張対応)イメージの作成、操作、およびCD/DVD/BDメディアへの書き込みをコマンドラインで行う多機能ツール」だそう。

Rust@naked関数

Rust@naked関数 multiboot仕様でブートローダを自作せずGRUBから自作カーネル(もどき)を起動できたので、起動情報も色々受け取りたい。multiboot準拠していれば、起動直前にブートローダがレジスタEAXにマジック値を...