Skip to content

Instantly share code, notes, and snippets.

@Hmiya6
Last active Dec 22, 2021
Embed
What would you like to do?
Rust で MikanOS を再実装 (カーネルの呼び出しまで)

この記事は 自作OS Advent Calendar 2021 の 22 日目の記事として書かれています.

Rust で MikanOS を再実装 (カーネルの呼び出しまで)

ここ数ヶ月 Rust で MikanOS の再実装をしてきました. ここまでの開発で Rust による OS 実装特有の事情を理解できた気がするので, その知見をまとめておきます.

この OS は ゼロからの OS 自作入門 の内容に沿ったものです.

0. Rust で OS を書くときの参考資料

最初に, Rust で OS を書く過程で参考になると思われるドキュメントを最初に紹介しておきます.

0.1. ゼロからの OS 自作入門 (書籍)

『ゼロからの OS 自作入門』(以下, みかん本) は, 段階を踏んで OS (MikanOS) をつくっていく本です. ただ段階を踏んで開発を進めるだけでなく, OS の仕組みについても理解しやすく解説しています. そのほとんどの部分が C++ で記述されています.

この OS はみかん本を参考に MikanOS を Rust での再実装するものです.

0.2. Writing an OS in Rust

Writing an OS in Rust は, 私が MikanOS を移植する過程で (みかん本以外で) 最も参考にしたドキュメントです. Rust での OS の書き方をテーマごと (ex. CPU 例外, ページング, ヒープアロケーション等) に解説しているので, C++ と Rust での実装の違いで悩んだときに参照しています.

0.3. Rust そのものについて

0.4. OS について

  • OSDev.org: コードは C 言語によるものも多いですが, Rust での解説がない場合の参考になります.

0.5. Rust で書く OS について

  • Writing an OS in Rust 3rd edition: Writing an OS in Rust の第3版 (書きかけ) です. 第3版では UEFI ブートローダで起動する OS を書いているようなので, もしかすると参考になるかもしれません.
  • meg-os/maystorm: A hobby OS written in Rust: Rust で書かれた x86_64 アーキテクチャの OS です.
  • Redox: 現時点で最も洗練されていると思われる Rust 製 OS です.

0.6. Rust 再実装の先駆者様

MikanOS を Rust で再実装するプロジェクトはすでに多くあります:


1. 何もしない UEFI アプリ

まずは何もしない、QEMU 上で起動のみが可能な UEFI アプリをつくり, それからブートローダとして必要な機能をひとつづつ実装していく方針で進めていきます.

1.1. 新しいプロジェクトをつくる

まず, cargo new で新しいプロジェクトをつくります.

# 新しいプロジェクトをつくる
cargo new os 
cd os
# ブートローダのパッケージをつくる
cargo new bootloader --bin

結果, ディレクトリ構造は以下のようになります:

.
├── bootloader
│   ├── Cargo.toml
│   └── src
│       └── main.rs
├── Cargo.toml
└── src
    └── main.rs

以下では, ブートローダ (bootloader) 部分を書き進めていきます. まず, Rust の各種コンフィグファイルについて説明し, 次にコードを書いていきます.

1.2. コンフィグファイルの設定

Rust には複数のコンフィグファイルがあります. OS 開発やブートローダでは, 通常の開発では使用しないような特殊な設定がいくつか必要となるので, それらをファイルに書き込んでいきます.

bootloader/Cargo.toml の設定

UEFI アプリケーションの開発をより簡単にするための uefi クレートを追加します.

# bootloader/Cargo.toml: 

[dependencies]
# 以下を追加
uefi = { version = "0.12.0" } 

bootloader/.cargo/config.toml の設定

次に, .cargo/config.toml を記述して cargo の振る舞いをカスタマイズします. cargo build などのコマンドを実行する時に必要な引数をファイルで設定できるので便利です.

# bootloader/.cargo/config.toml: 

[build] # cargo build などのコンパイル先を指定する
target = "x86_64-unknown-uefi" # x86_64 アーキテクチャの UEFI バイナリを指定

[unstable] # nightly channel のみで利用可能な機能の設定
build-std = ["core", "compiler_builtins", "alloc"]
build-std-features = ["compiler-builtins-mem"]

このファイルのおかげで cargo build

cargo build --target x86_64-unknown-uefi -Z build build-std=core,compiler_builtins,alloc -Z build-std-features=compiler-builtins-mem

と同等の意味を持つようになります.

build-stdcore, compiler_builtins を省略すると何もしないバイナリであってもコンパイルできなくなってしまいます.

bootloader/rust-toolchain.toml の設定

次に, rust-toolchain.toml を設定します. rust-toolchain.toml は, どのバージョンの Rust を使うかを指定するファイルです. 今回は Rust の試験的な機能を用いるため Nightly Rust というバージョンの Rust を使用しているため, そのことを書き込んでおきます.

# bootloader/rust-toolchain.toml: 

[toolchain]
channel = "nightly" # 試験的な (安定化されていない) 機能が使用可能になる

上のファイルのおかげで, このファイルのあるプロジェクトではコンパイルの際に nightly の Rust が使用されるようになります.

1.3. 各コンフィグファイルの役割の違い

ここまでに登場した 3 種類の設定ファイルの役割についてまとめておきます:

  • Cargo.toml: 使用外部クレートの記載等、このプロジェクトに関する設定をするファイルです.
  • .cargo/config.toml: cargo (Rust のビルドシステム) のコマンドの挙動をカスタマイズするファイルです (cargo コマンドを使うときに引数で渡す情報をあらかじめファイルに記載することができます).
  • rust-toolchain.toml: rustup (Rust のツールチェーンを管理するツール, どの Rust を使うのかを決定するためのツール) の挙動をカスタマイズするファイルです (rustup コマンドの内容をあらかじめファイルに記載することができます).

1.4. bootloader/src/main.rs

ブートローダ本体も記述していきます.

// bootloader/src/main.rs: 

#![no_std]
#![no_main]
#![feature(abi_efiapi)] // "efiapi" 呼出規約を有効にする

#[no_mangle]
pub extern "efiapi" fn efi_main(
    image: uefi::Handle,
    mut system_table: uefi::table::SystemTable<uefi::table::Boot>,
) -> uefi::Status {

    loop {} // Bootloaders typically pass control to a OS kernel and never return.
}

use core::panic::PanicInfo;

#[panic_handler] // パニック時に実行される関数を指定する
fn panic(_info: &PanicInfo) -> ! {
    loop {} 
}

Rust 特有の事情についていくつか解説を加えます.

まず、#![no_std] についてです. Rust では no_std 属性を使って標準ライブラリをリンクしないことを明示します. Rust の標準ライブラリ (std) は、プログラムが OS 上で実行されることを前提に作られており, OS の機能を使用できない OS 開発や UEFI アプリケーション開発では標準ライブラリをリンクすることは適当ではありません. なので, ここでは no_std 属性を付与します.

その一方で、std の OS 機能に依存しないの一部は core クレートとして利用可能となっています. そのため、OS 開発等では std の代わりに core クレートが使用されます. コードを読んでいて core クレートが出てきたときは std のサブセットだと考えると理解が早まると思います.

次は, #![no_main] についてです. UEFI アプリケーション開発では main 関数ではなく efi_main 関数が UEFI ファームウェアによって呼び出されます. そのため, このコードに main 関数は不要であり、#![no_main] 属性を付与することで main 関数を実装しなくてもコンパイル可能なように設定しています.

さて, efi_main 関数は UEFI ファームウェアによって呼び出される関数です. この呼び出しを可能にするため, efi_main#[no_mangle]extern "efiapi" を設定します.

#[no_mangle]名前修飾 (name mangling) と呼ばれる機能を無効にする識別子です. 名前修飾が有効な場合、関数名 efi_main はコンパイルによって別の名前に変更されます. 関数名が変更されると UEFI ファームウェアが efi_main 関数を呼び出すことができなくなるため、efi_main 関数では名前修飾を無効にしています.

extern "efiapi" は、関数の呼出規約を efiapi とするものです. Rust が提供する efiapi 呼出規約は UEFI 仕様書にある UEFI インターフェースに適合するものなので、extern "efiapi"efi_main 関数に記述することで、UEFI ファームウェアからも関数が呼び出せるようになります.

efi_mainloop {} としています. これはブートローダが OS へコントロールを渡したままリターンしないままであることが多いためです.

#[panic_handler] はパニック時の振る舞いを決定する関数を指定するための属性です. パニック時に実行される関数とその属性 #[panic_handler] がない場合, コンパイルができないので簡単に実装しています.

panic 関数の戻り値には -> ! となっています. これはこの関数がリターンしないことを表しています (そのため, loop {} で関数実行が続くように記述しています).

1.5. QEMU 上で実行する

cargo run すれば QEMU で実行されると便利なので, cargo run したときに bootloader/run-qemu.sh が実行されるように設定します.

# bootloader/.cargo/config.toml

# 以下を追加

[target.x86_64-unknown-uefi]
runner = "sh run-qemu.sh target/x86_64-unknown-uefi/debug/bootloader.efi"

runner を追加することでカスタムランナーを設定することができます.

bootloader/run-qemu.sh については以下の内容にしました:

# bootloader/run-qemu.sh

MNT=./target/mnt
DISK=./target/disk.img
TARGET=$1
OVMF_PATH=/your/ovmf/path

qemu-img create -f raw $DISK 200M
mkfs.fat -n 'OS' -s 2 -f 2 -R 32 -F 32 $DISK

mkdir -p $MNT
sudo mount -o loop $DISK $MNT

sudo mkdir -p $MNT/EFI/BOOT
sudo cp $TARGET $MNT/EFI/BOOT/BOOTX64.EFI

sleep 0.5
sudo umount $MNT

qemu-system-x86_64 -bios $OVMF_PATH -drive format=raw,file=$DISK

最後に, Hello, world! して正常に実行されるか確認します.

1.6. "Hello, world!" する

以下のコードで "Hello, world" できます:

// bootloader/src/main.rs: 

#[no_mangle]
pub extern "efiapi" fn efi_main(
    image: uefi::Handle,
    mut system_table: uefi::table::SystemTable<uefi::table::Boot>,
) -> uefi::Status {

    use core::fmt::Write; // writeln! マクロを使うために必要
    system_table.stdout().clear().unwrap();
    writeln!(system_table.stdout(), "Hello, world!").unwrap();

    loop {}
}

このコードを書いたうえで, bootloader ディレクトリ内で cargo run して, QEMU 上に Hello, world! が表示されれば成功です.

参考


2. 何もしないカーネルの起動

次に, カーネルを起動できるように準備を進めていきます.

2.1. カーネルの用意

まず, ブートローダから呼び出すための何もしないカーネルをつくります.

// src/main.rs: 

#![no_std]
#![no_main]

use core::panic::PanicInfo;

#[no_mangle]
extern "C" fn kernel_main() -> ! {
    loop {}
}

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

kernel_main 関数は, ブートローダから呼び出せるように extern "C" で C の呼出規約で呼び出せるようにします. また, #[no_mangle] で名前修飾を行わないようコンパイラに伝えます.

カーネルについてもブートローダ同様にコンフィグファイルを記述していきます.

まず, .cargo/config.toml を記述します.

# .cargo/config.toml: 

[build]
target = "kernel_target.json" # kernel_target.json については後述します

[unstable]
build-std-features = ["compiler-builtins-mem"]
build-std = ["core", "compiler_builtins"]

[target.kernel_target]
runner = "sh run-qemu.sh" # run-qemu.sh についても後述します

今回はビルドターゲットに JSON ファイル (kernel_target.json) を使用しています (参考: Custom Targets).

kernel-target.json は以下のようになっています:

{
  "arch": "x86_64",
  "data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128",
  "disable-redzone": true,
  "dynamic-linking": false,
  "exe-suffix": ".elf",
  "executables": true,
  "features": "-mmx,-sse,+soft-float",
  "linker": "rust-lld",
  "linker-flavor": "ld.lld",
  "post-link-args": {
    "ld.lld": [
      "--entry", "kernel_main",
      "-z", "norelro",
      "--image-base", "0x100000",
      "--static"
    ]
  },
  "llvm-target": "x86_64-unknown-none",
  "os": "none",
  "panic-strategy": "abort",
  "relocation-model": "static",
  "relro-level": "full",
  "target-endian": "little",
  "target-pointer-width": "64",
  "target-c-int-width": "32"
}

kernel_target.json の内容は Writing an OS in Rust の JSON ファイル の内容を参考に, みかん本の内容に沿うように変更したものです.

rust-toolchain.toml も追加します.

# rust-toolchain.toml:

[toolchain]
channel = "nightly"
components = ["rust-src"]

カーネルを含めて QEMU 上で実行するための run-qemu.sh も追加します.

# run-qemu.sh: 

#! bin/bash

MNT=./target/mnt
DISK=./target/disk.img
OVMF_PATH=/your/path/to/OVMF.fd
BOOTLOADER_EFI_PATH=./bootloader/target/x86_64-unknown-uefi/debug/bootloader.efi
KERNEL_ELF_PATH=./target/kernel_target/debug/os.elf

# build bootloader
cd bootloader
cargo build 
cd ../

qemu-img create -f raw $DISK 200M
mkfs.fat -n 'POTATO OS' -s 2 -f 2 -R 32 -F 32 $DISK

mkdir -p $MNT
sudo mount -o loop $DISK $MNT

sudo mkdir -p $MNT/EFI/BOOT

sudo cp $BOOTLOADER_EFI_PATH $MNT/EFI/BOOT/BOOTX64.EFI
sudo cp $KERNEL_ELF_PATH $MNT/kernel.elf

sleep 0.5
sudo umount $MNT

qemu-system-x86_64 -bios $OVMF_PATH -drive format=raw,file=$DISK -s -S

2.2. カーネルファイルの読み込み

まず, ルートディレクトリを開くための関数を用意しておきます. これはカーネルのファイルを開く際に必要となります.

// bootloader/src/main.rs: 

use uefi::{
    prelude::*,
    proto::{
        loaded_image::LoadedImage,
        media::fs::SimpleFileSystem,
        media::file::Directory,
    }
};

fn open_root(image: Handle, system_table: &SystemTable<Boot>) -> Directory {
    let loaded_image = system_table
        .boot_services()
        .handle_protocol::<LoadedImage>(image)
        .unwrap_success()
        .get();
    let device = unsafe { (*loaded_image).device() };
    let file_system = system_table
        .boot_services()
        .handle_protocol::<SimpleFileSystem>(device)
        .unwrap_success()
        .get();
    unsafe { (*file_system).open_volume().unwrap_success() }   
}

次のコードは kernel.elf を開いて kernel_file_buf へと読み込む部分です. efi_main 関数内に記述します:

// bootloader/src/main.rs: 

use uefi::{
    proto::media::file::{ File, FileAttribute, FileInfo, FileMode, FileType}
    table::boot::MemoryType,
}

#[no_mangle]
pub extern "efiapi" fn efi_main(
    image: uefi::Handle,
    mut system_table: uefi::table::SystemTable<uefi::table::Boot>,
) -> ! {

    // [...]

    // カーネルファイルを開く
    let mut kernel_file = {
        let mut root = open_root(image, &system_table);
        let file_handle = root
            .open("kernel.elf", FileMode::Read, FileAttribute::READ_ONLY)
            .unwrap_success();
        match file_handle.into_type().expect_success("Failed into_type") {
            FileType::Regular(file) => Some(file),
            _ => None,
        }.expect("kernel file is not regular file")
    };

    // カーネルファイルを読み込むのに必要なバッファのサイズ
    let kernel_file_size = {
        let info_buf = &mut [0u8; 4000];
        let info: &mut FileInfo = kernel_file.get_info(info_buf)
            .unwrap_success();
        info.file_size() as usize
    };

    // カーネルファイルを読み込むためのバッファ
    let kernel_file_buf = {
        let addr = system_table.boot_services()
            .allocate_pool(MemoryType::LOADER_DATA, kernel_file_size)
            .unwrap_success();
        unsafe {
            core::slice::from_raw_parts_mut(addr, kernel_file_size)
        }
    };

    // バッファへの読み込み
    let read_size = kernel_file.read(kernel_file_buf).unwrap_success();
    kernel_file.close();

    // 実際に読み込んだサイズと用意したバッファのサイズを確認
    assert_eq!(read_size, kernel_file_size);

    loop {}
}

しかしこれだけでは実行できないので, 必要な uefi-rs の機能を導入します.

uefi-rsalloc を使う

カーネルファイルの読み込み時に uefi-rsalloc 機能を使うため, bootloader/Cargo.toml の設定に features = ["alloc"] を追加します:

# bootloader/Cargo.toml

[dependencies]
uefi = { version = "0.12.0", features = ["alloc"] }

また, bootloader/src/main.rs も変更します. 具体的には, alloc 機能の初期化と alloc_error_handler を追加します.

// bootloader/src/main.rs

#![feature(alloc_error_handler)]

#[no_mangle]
pub extern "efiapi" fn efi_main(
    image: uefi::Handle,
    mut system_table: uefi::table::SystemTable<uefi::table::Boot>,
) -> ! {

    // 追加: alloc の初期化
    unsafe {
        uefi::alloc::init(system_table.boot_services());
    }

    // [...]

}

// [...]

use core::alloc::Layout;

// 追加: alloc_error_handler
#[alloc_error_handler]
fn alloc_error(_layout: Layout) -> ! {
    panic!("alloc error")
}

これでカーネルファイルを読み込めるようになりました. 次は読み込んだカーネルをメモリへロードしていきます.

2.3. ELF ファイルの解析・ロード

次に, 読み込んだカーネルファイルから必要な部分をロードしていきます.

ELF ファイルのパースには goblin を使用します.

# bootloader/Cargo.toml

[dependencies]
uefi = { version = "0.12.0", features = ["alloc"] }
# 以下を追加
goblin = { version = "0.4", features = ["elf32", "elf64", "endian_fd"], default-features = false }

ELF ファイルの解析とロード部分を記述していきます.

// bootloader/src/main.rs: 

extern crate alloc;

#[no_mangle]
pub extern "efiapi" fn efi_main(
    image: uefi::Handle,
    mut system_table: uefi::table::SystemTable<uefi::table::Boot>,
) -> ! {

    // [...]

    use goblin::elf;
    // ELF ファイルのパース
    let kernel_elf = elf::Elf::parse(kernel_file_buf)
        .expect("Failed parse kernel file");

    use alloc::vec::Vec;
    let pt_load_headers = kernel_elf
            .program_headers
            .iter()
            .filter(|ph| ph.p_type == elf::program_header::PT_LOAD)
            .collect::<Vec<_>>();

    // カーネルの LOAD セグメントのサイズ取得する
    let (kernel_start, kernel_end) = {
        use core::cmp;
        let (mut start, mut end) = (usize::MAX, usize::MIN);
        for &pheader in &pt_load_headers {
            start = cmp::min(start, pheader.p_vaddr as usize);
            end = cmp::max(end, (pheader.p_vaddr + pheader.p_memsz) as usize);
        }
        (start, end)
    };

    writeln!(
        system_table.stdout(), 
        "Kernel: {:#x} - {:#x}", kernel_start, kernel_end,
    ).unwrap();

    // LOAD セグメントのメモリへのコピーで使う領域の確保
    system_table.boot_services().allocate_pages(
        uefi::table::boot::AllocateType::Address(kernel_start),
        MemoryType::LOADER_DATA,
        (kernel_end - kernel_start + 0xfff) / 0x1000,
    ).unwrap_success();

    // LOAD セグメントをメモリへコピー
    for &pheader in &pt_load_headers {
        let offset = pheader.p_offset as usize; // offset in file
        let file_size = pheader.p_filesz as usize; // LOAD segment file size
        let mem_size = pheader.p_memsz as usize; // LOAD segment memory size
        // コピー先を配列として確保
        let load_dest = unsafe { 
            core::slice::from_raw_parts_mut(
                pheader.p_vaddr as *mut u8, 
                mem_size
            ) 
        };
        // コピー
        load_dest[..file_size].copy_from_slice(&kernel_file_buf[offset..offset + file_size]);
        load_dest[file_size..].fill(0);
    }

    loop {}
}

これでカーネルをメモリへロードすることができました. 次はカーネルを呼び出します.

2.4. kernel_main の実行

カーネルの ELF ファイルのエントリーポイントを関数へと変換 (core::mem::transmute) して, kernel_main を呼び出します.

// bootloader/src/main.rs: 

type EntryFn = extern "sysv64" fn();

#[no_mangle]
pub extern "efiapi" fn efi_main(
    image: uefi::Handle,
    mut system_table: uefi::table::SystemTable<uefi::table::Boot>,
) -> ! {

    // [...]

    let entry_point = {
        let addr = kernel_elf.entry;
        unsafe { core::mem::transmute::<u64, EntryFn>(addr) }
    };

    entry_point();

    loop {}
}

GDB で kernel_main にブレークポイントを張り, そこまでたどり着けば成功です.

続く...

参考

  • みかん本: 3.3 初めてのカーネル, 4.5 ローダを改良する
  • UEFI Booting (書きかけ): uefi クレートを使った UEFI アプリの書き方に関するドキュメントです.

3. リポジトリ


おまけ: GDB でデバッグする

Rust で GDB を使う場合, rust-gdb を使用することができます. OS 開発の場合, print デバッグができない場合もあるので, GDB を重宝しています.

通常, 以下のコマンドで実行できます:

$ rgdb target/kernel_target/debug/os.elf 

QEMU 上での実行をデバッグする

QEMU の実行オプションに -s -S を追加することで, QEMU が GDB の接続を待ち受けるようになります.

# run-qemu.sh を変更: 

qemu-system-x86_64 -bios $OVMF_PATH -drive format=raw,file=$DISK -s -S

cargo run したあと, 別のターミナルを開いて,

$ rgdb target/kernel_target/debug/os.elf -ex "target remote :1234"

することで, GDB を QEMU に接続することができます. これで, QEMU を GDB でデバッグできるようになります.

例: カーネルで起きたパニックの原因を GDB で確認する

試しに, パニックの原因を GDB を使って探ってみます.

まず, kernel_main 関数をパニックさせます.

#[no_mangle]
extern "C" fn kernel_main() -> ! {
    panic!("test panic"); // 追加 (このコードでは 8 行目)
    loop {}
}

QEMU の実行オプションに -s -S をつけて cargo run したあと, 別画面で

$ rgdb target/kernel_target/debug/os.elf -ex "target remote :1234"

# [...]

# panic 関数 (例えば, main.rs の 14行目〜) にブレークポイントを張る
(gdb) b main.rs:14
Breakpoint 1 at 0x101365: file src/main.rs, line 14.
# panic 関数は rust_begin_unwind という名前でもブレークポイントを張ることができます
(gdb) b rust_begin_unwind
Note: breakpoint 1 also set at pc 0x101365.
Breakpoint 2 at 0x101365: file src/main.rs, line 14.
# 実行を進める
(gdb) c
Continuing.

# panic 関数に設定したブレークポイントで停止
Breakpoint 1, rust_begin_unwind (_info=0x7ebba20) at src/main.rs:14
14	    loop {}
(gdb) print _info # panic 関数の引数である _info (パニック情報) を表示してみます
$1 = (*mut core::panic::panic_info::PanicInfo) 0x7ebba20
(gdb) print *_info # $1 が PanicInfo のポインタであることがわかったので, 参照を外します 
$2 = core::panic::panic_info::PanicInfo {payload: &(dyn core::any::Any + core::marker::Send) {pointer: 0x100230 "\000", vtable: 0x100230}, message: core::option::Option<&core::fmt::Arguments>::Some(0x7ebba50), location: 0x100150}
(gdb) print $2.location # どこでパニックしたかの情報を持つ location フィールドを表示します
$3 = (*mut core::panic::location::Location) 0x100150
(gdb) print *$3
# ここで src/main.rs の 8 行目でパニックしていることがわかりました
$4 = core::panic::location::Location {file: "src/main.rs", line: 8, col: 5}
(gdb) print $2.message # パニックメッセージを確認します
$5 = core::option::Option<&core::fmt::Arguments>::Some(0x7ebba50)
(gdb) print $5 as &core::fmt::Arguments # Option を, その中身の &Arguments に変換します
$6 = (*mut core::fmt::Arguments) 0x7ebba50
(gdb) print *$6
$7 = core::fmt::Arguments {pieces: &[&str] {data_ptr: 0x100130, length: 1}, fmt: core::option::Option<&[core::fmt::rt::v1::Argument]>::Some(&[core::fmt::rt::v1::Argument] {data_ptr: 0x0, length: 0}), args: &[core::fmt::ArgumentV1] {data_ptr: 0x100140, length: 0}}
(gdb) print $7.pieces
$8 = &[&str] {data_ptr: 0x100130, length: 1}
(gdb) print $8.data_ptr[0] # 添字による表示も可能です
$9 = "test panic" # パニックメッセージが表示されました
(gdb)

これで, src/main.rs の 8 行目で "test panic" というパニックが発生したことがわかりました.

参考

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment