なぜ .data と .bss を Rust で初期化しないのか
この本の以前のバージョンでは、.data セクションと .bss セクションを Rust コードで初期化していました。
しかし、これは健全性に疑義があることが判明しており、今日では、これらのセクションの初期化を行う推奨される方法はアセンブリに依存しています。
この章では、cortex-m-rt や riscv-rt のような さまざまな crate が、これらのセクションの初期化をアセンブリで行う方式へ移行することを決定した理由について説明します。こうしたコードの健全性については、これまでに a decent number of threads で疑問が呈されてきました。これらをこの章で要約します。
この本で Rust によるグローバルデータ初期化に使われていた元のコードを以下に示します。
#![no_std]
use core::panic::PanicInfo;
use core::ptr;
#[unsafe(no_mangle)]
#[allow(static_mut_refs)]
pub unsafe extern "C" fn Reset() -> ! {
// NEW!
// Initialize RAM
unsafe extern "C" {
static mut _sbss: u8;
static mut _ebss: u8;
static mut _sdata: u8;
static mut _edata: u8;
static _sidata: u8;
}
let count = unsafe { &_ebss as *const u8 as usize - &_sbss as *const u8 as usize };
unsafe { ptr::write_bytes(&mut _sbss as *mut u8, 0, count) };
let count = unsafe { &_edata as *const u8 as usize - &_sdata as *const u8 as usize };
unsafe { ptr::copy_nonoverlapping(&_sidata as *const u8, &mut _sdata as *mut u8, count) };
// Call user entry point
unsafe extern "Rust" {
safe fn main() -> !;
}
main()
}
特定のメモリ位置を参照するために、5 つの extern "C" 変数が宣言されています。
リンカスクリプトが各シンボルを定義しているため、それらの正確な配置を心配する必要はありません。
ポインタ provenance
.bss セクションを初期化するために、.bss セクションの先頭を指す _sbss u8 変数のアドレスを取得します。
そして、その場所に任意の量のデータを書き込みます。_sbss は u8 変数として宣言されており、
ポインタ provenance の規則では、_sbss 変数の allocation に収まる量のデータしか書き込めません。
それにもかかわらず、私たちはその 1 バイトを超えて(少なくとも Rust から見れば、このアドレスには 1 バイトしか割り当てられていません)、
_ebss の位置に達するまで書き込んでいます。
別の問題として、.bss セクションの 1 バイト外を指す _ebss 変数が実際に存在しています。
特定の実装では、.bss セクションが使用可能なメモリを使い切っている場合、このバイトにはアクセスすらできない可能性があります。
理想的には、_ebss は ZST として宣言される必要があります。そしてその延長として、.bss セクションは空である可能性があるため、
_sbss も ZST であるべきです。なぜなら、この場合 _sbss も .bss 用に予約された領域の外側に位置することになるからです。
エイリアシング
上のコードにおけるもう 1 つの潜在的な問題はエイリアシングです。リンカスクリプトを考えてみましょう。
.bss :
{
_sbss = .;
*(.bss .bss.*);
_ebss = .;
} > RAM
.data : AT(ADDR(.rodata) + SIZEOF(.rodata))
{
_sdata = .;
*(.data .data.*);
_edata = .;
} > RAM
次のような状況が起こりえます。
.bssセクションが空でない場合、_sbssは.bssセクション内の最初の変数と同じアドレスに配置される可能性があります。_ebssは_sdataと同じアドレスに配置され、さらに言えば、.dataセクション内の最初の変数とも同じアドレスに配置されます。.bssセクションが空の場合、_sbssと_ebssは互いにエイリアスします。.dataセクションが空の場合、_sdataと_edataは互いにエイリアスします。
Rust では、同じアドレスに複数の変数を配置することは許されていません (ZST は重要な例外です)。しかし、たとえ許されていたとしても、私たちはこれらの変数を使って グローバルメモリ領域全体に書き込んでおり、これは実質的にプログラム内で定義された すべてのグローバルデータを可変にエイリアスしていることになります。
抽象マシンの初期化
もう 1 つの疑問は、Rust の抽象マシンが完全に初期化される前に Rust コードへ入っても安全かどうかです。 グローバルメモリがまだ初期化されていない間、Rust がそれを一切使わないと期待してよいのでしょうか。 この問いに対する答えは明確ではありません(少なくとも、この節の執筆時点で著者には明確ではないようです)。
provenance に関するさらなる潜在的な問題
気の利く読者なら、_ebss と _sbss の間のオフセットをどのように計算しているかを見て、
代わりにポインタの offset_from
メソッドを使えないだろうか、と思ったかもしれません。
しかしこの方法の問題は、前述したように、_ebss と
_sbss は異なる allocation に属しているため、同じポインタ
provenance を共有していないことです。これは、それらが互いにエイリアスしていて、
たまたま同じアドレスに配置されている場合(つまり .bss セクションが空の場合)でも同じです。
この Rust Playground のスニペット で Miri を実行すると、未定義動作が示されます。
なるほど、でも動いてはいるんですよね?
はい。この章の冒頭で示したコードは、Rust 1.89 時点では正しい 挙動を示します。しかし問題は、この挙動が将来のリリースでも維持されることに頼れない ということです。さらには、将来オプティマイザが何か妙なことをしないとも限りません。
そのため、全体として、この本の推奨は、この目的のための初期化を Rust コードで行わないことです。