この記事は 自作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 そのものについて
- The Rust Programming Language 日本語版 (The Book): Rust の入門ドキュメントです. Rust の基本的な書き方を知りたい場合はここを参考にしています.
- The Rustonomicon: Unsafe Rust などの入門的でない内容を扱っているドキュメントです.
- The Rust Reference: the book より詳細な内容を扱うリファレンスです.
- Guide to Rustc Development: Rust のコンパイラについてのドキュメントです.
- The Rust RFC Book: Rust の機能の一部の仕様.
- Rust Language Cheat Sheet: Rust のチートシート.
- Rust for Rustaceans (書籍): 序盤までしか読み進められていないですが, 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 で再実装するプロジェクトはすでに多くあります:
- algon-320/mandarin: MikanOS in Rust
- ocadaruma/rumikan
- genms/my-mikanos-rust
- ゼロからのOS自作入門 カテゴリーの記事一覧 - gifnksmの雑多なメモ
- 【ゼロからのOS自作入門】MikanOSをRustに移植する 1章・2章 - 日記
- ゼロからのOS自作入門 in 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-std
の core
, 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 コマンドの内容をあらかじめファイルに記載することができます).
bootloader/src/main.rs
1.4. ブートローダ本体も記述していきます.
// 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_main
は loop {}
としています. これはブートローダが 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!
が表示されれば成功です.
参考
- みかん本 第 1 章
- uefi - Rust:
uefi
クレートの API ドキュメント - The Manifest Format - The Cargo Book:
Cargo.toml
の書き方 - Configuration - The Cargo Book:
.cargo/config.toml
の書き方 - Unstable Features - The Cargo Book:
.cargo/config.toml
の[unstable]
部分を書き方. - Overrides - The rustup book:
rust-toolcnain.toml
の書き方 - UEFI Booting (書きかけ):
uefi
クレートを使った UEFI アプリの書き方に関するドキュメント
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-rs
の alloc
を使う
カーネルファイルの読み込み時に uefi-rs
の alloc
機能を使うため, 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 {}
}
これでカーネルをメモリへロードすることができました. 次はカーネルを呼び出します.
kernel_main
の実行
2.4. カーネルの 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"
というパニックが発生したことがわかりました.
参考
- Debugging Rust apps with GDB
- Set Up GDB
- その他, 通常の GDB に関するドキュメントも参考になります.