以前、Golangから物理メモリを読み書きするという記事を書きました。似たようなことをRustでやってみました。ただし今回は /dev/mem
をmmapするのでroot権限が必要です。
ソースコードはgistに貼りました。ここ
使用例
アクセスサイズ、物理アドレス、長さを引数で指定します。
アクセスサイズは 1,2,4,8 のいずれか。物理アドレスと長さは16進数です。 先頭に0x
をつけてもつけなくてもOK。
# ./dump_pmem
Dump physical memory by specified size.
Usage: ./dump_pmem size address [len]
where size={1,2,4,8}, address and len in hexadecimal.
表示は一行に一個づつです。
# ./dump_pmem 4 0x12030078 0x8
0000000012030078: 02780278
000000001203007c: 02780279
0000000012030080: 02780278
0000000012030084: 02790279
0000000012030088: 00000000
000000001203008c: 00000000
0000000012030090: 00000010
0000000012030094: 00000000
# ./dump_pmem 2 0x12030078 4
0000000012030078: 0278
000000001203007a: 0278
000000001203007c: 0278
000000001203007e: 0278
#
(2018.9.28: アドレスの表示が間違っていたので修正しました。)
mmapの方法
Rustでmmapシステムコールを使うには以下の3つの方法が考えられます。
- libcクレートでmmapを呼び出す
- mmapクレートを使用する
- memmapクレートを使用する
1.の方法が一番原始的で後始末のmunmapも自分で明示的に行う必要があります。
2. と 3. を試したのですが3.が一番使い勝手がよかったのでこれを採用しました。
memmap クレートを使用する
以下の関数で、引数で指定したaddr, len で物理メモリを読み取り用にマッピングできます。
extern crate memmap;
use memmap::Mmap;
use std::fs::File;
use std::error::Error;
fn map_physical_mem(addr: usize, len: usize) -> Result<Mmap, Box<Error>> {
let m = unsafe {
memmap::MmapOptions::new()
.offset(addr as u64)
.len(len)
.map(&File::open("/dev/mem")?)?
};
Ok(m)
}
取得できたm
は[u8]
(u8のスライス)として扱うことができます。アクセスするときに unsafe
で囲む必要もありません。
これのスコープが外れたときに自動的にmunmapが行われます。
実際のmmapはページサイズ単位で行われますが、ここで指定するアドレスはそれを意識する必要はありません。
バイトアクセスでダンプするコードはこちら。m[x]
の部分がバイト単位のアクセスです。
簡単ですね!
fn dump_mem_u8(addr: usize, len: usize) {
let m = match map_physical_mem(addr, len) {
Ok(m) => m,
Err(err) => {
panic!("Failed to mmap: Err={:?}", err);
}
};
(0..len).for_each(|x| println!("{:016x}: {:02x}", addr + x, m[x]));
}
物理メモリ空間上のレジスタを読む場合は、アクセスサイズは重要です。必ずスペックシートで指定されたサイズでアクセスしなければなりません。
バイト以外でアクセスしたい場合は、ポインタを取得してキャストします。ポインタの演算に +
は使えないのでoffset()
を使用します。
m
をmemmap::Mmap
とすると以下のような感じになります。
let p = m.as_ptr() as *const u32;
let value = unsafe {*p.offset(x as isize)};
ここでのp
はm
が生きている間だけ有効です。p
だけ取っておいて後で使おうとすると、m
のスコープが外れたときにmunmapされているのでSEGVを引き起こしますので注意が必要です。(実際にそれをやってしまって少し悩みました。:)
それを考えると、バイトアクセスのときにm
がそのままu8のスライスとして使えるようになっているのはよくできています。このようなミスの発生を未然に防いでいます。
実際に作った関数はこちら。ジェネリクスを使ってみました。
fn dump_mem<T>(addr: usize, len: usize)
where
T: std::fmt::LowerHex,
{
let sz = std::mem::size_of::<T>();
let m = match map_physical_mem(addr, len * sz) {
Ok(m) => m,
Err(err) => {
panic!("Failed to mmap: Err={:?}", err);
}
};
let p = m.as_ptr() as *const T;
(0..len).for_each(|x| unsafe {
let xi = x as isize;
match sz {
1 => println!("{:016x}: {:02x}", addr + sz * x, *p.offset(xi)),
2 => println!("{:016x}: {:04x}", addr + sz * x, *p.offset(xi)),
4 => println!("{:016x}: {:08x}", addr + sz * x, *p.offset(xi)),
8 => println!("{:016x}: {:016x}", addr + sz * x, *p.offset(xi)),
_ => println!("{:016x}: {:x}", addr + x, *p.offset(xi)),
}
});
}
余談
println!
などのformat!
マクロでは最初の引数は必ずstringのリテラルでなくてはいけません。ここに変数を置くとコンパイルエラーになります。そのために、このように match
でずらずらと並べざるを得ませんでした。
しかしよく考えてみると、このsz
はコンパイル時には定数の扱いですから、matchの選択肢は一つだけ除いて残りは最適化で消えます。そうするとすっきりしたコードが残ることがわかります。
Rustのコンパイルを通すのはなかなかつらいけど、苦労した後でできたコードがすっきりするのが気持ちいいですね。
2018.9.28 更新
dump_pmem
コマンドでアドレスの表示が間違っていたので修正しました。
fmtで表示幅をパラメータで渡す方法があることを教えていただきました。
https://doc.rust-lang.org/std/fmt/#width
物理メモリから読むときにはstd::ptr::read_volatile
を使うようにしました。
ということで、更新した関数はこちら。
fn dump_mem<T>(addr: usize, len: usize)
where
T: std::fmt::LowerHex,
{
let sz = std::mem::size_of::<T>();
let m = match map_physical_mem(addr, len * sz) {
Ok(m) => m,
Err(err) => {
panic!("Failed to mmap: Err={:?}", err);
}
};
let p = m.as_ptr() as *const T;
(0..len).for_each(|x| unsafe {
println!(
"{:016x}: {:02$x}",
addr + sz * x,
std::ptr::read_volatile(p.offset(x as isize)),
sz * 2
);
});
write_pmem
コマンドも作ってgistに貼りました。ここ