はじめに
Rust をやっていると「結局このデータはどんな感じでメモリ上に配置されているのか?」ということが気になってきたので、百聞は一見に如かずということでバイト列として眺めてみたい。
ソースコード
use std::mem;
fn get_raw_bytes_with_size<T: ?Sized>(p: &T, size: usize) -> Vec<u8> {
let mut buf = Vec::with_capacity(size);
let view = p as *const _ as *const u8;
for i in 0..size {
buf.push(unsafe {*view.offset(i as isize)});
}
buf
}
fn get_raw_bytes<T>(p: &T) -> Vec<u8> {
get_raw_bytes_with_size(p, mem::size_of::<T>())
}
fn print_bytes(bytes: Vec<u8>) {
for (i, x) in bytes.iter().enumerate() {
if i % 16 == 0 {
if i != 0 { println!(); }
print!("{:04x}| ", i);
}
print!("{:02x} ", x);
}
println!();
}
macro_rules! print_raw_bytes {
($e: expr) => {
{
let p = & $e;
println!("----- {:p}: {}", p, stringify!($e));
print_bytes(get_raw_bytes(p));
}
};
($e: expr; $n: expr) => {
{
let p = & $e;
println!("----- {:p}: {}", p, stringify!($e));
print_bytes(get_raw_bytes_with_size(p, $n));
}
};
}
- 以下この
print_raw_bytes!
マクロを使っていく-
print_raw_bytes!(hoge; N)
とすると hoge をバイト列として N バイト分表示する - hoge が Sized なら単に
print_raw_bytes!(hoge)
と書ける
-
- 参考: Viewing any object as a raw memory array? : rust - Reddit
- 2021-06-10 追記: 今更ですがこのマクロを清書して binspect crate として公開しました
使用例
コード
print_raw_bytes!('あ');
出力例
----- 0x563e641344: '\u{3042}'
0000| 42 30 00 00
-
'あ'
('\u{3042}'
)は42 30 00 00
のような形で格納されている(リトルエンディアン) - アドレス
0x563e641344
を先頭として 4 バイト分のメモリが割り当てられている
注意
最初に断っておきますが、基本的には Rust でデータレイアウトに依存するようなコードを書くべきではありません。
例えば、
struct A { a: i32, b: u64 }
struct B { a: i32, b: u64 }
という構造体を定義した場合に、A と B のデータレイアウトが同じになることは言語上は保証されていません(ただし実際にはそうしない理由はない)。このような落とし穴については The Rustonomicon の Data Layout の章に記載されています。
-
- "The Dark Arts of Advanced and Unsafe Rust Programming"
Rust does not currently guarantee that an instance of A has the same field ordering or padding as an instance of B, though in practice there's no reason why they wouldn't.
これから見ていくデータレイアウトはターゲットのアーキテクチャによって変わり、言語上の保証は無いかもしれず、将来的に変わるかもしれないということに十分留意してください。
実際に眺めてみる
手元の Linux (x86_64) 上の rustc 1.24.0-nightly で unoptimized でコンパイルして実行して確認しています(optimized でも結果はほぼ変わらない)。また、Wandbox (rustc HEAD 1.24.0-dev) に全コードを置いています。
プリミティブ型
手始めにプリミティブ型から。
fn show_primitives() {
print_raw_bytes!(false);
print_raw_bytes!(true);
print_raw_bytes!(127_i8);
print_raw_bytes!(0xabcd_u16);
print_raw_bytes!(0xefbeadde_u32);
print_raw_bytes!(5000_0000_0000_0000_i64);
print_raw_bytes!('A');
print_raw_bytes!('あ');
print_raw_bytes!('😇');
print_raw_bytes!(0.25_f32);
print_raw_bytes!(0.1_f64);
}
----- 0xe493276418: false
0000| 00
----- 0xe4932764a8: true
0000| 01
----- 0xe4932764ad: 127i8
0000| 7f
----- 0xe4932764b4: 43981u16
0000| cd ab
----- 0xe4932764c0: 4022250974u32
0000| de ad be ef
----- 0xe493276508: 5000000000000000i64
0000| 00 80 e0 37 79 c3 11 00
----- 0xe4932764c4: 'A'
0000| 41 00 00 00
----- 0xe4932764c8: '\u{3042}'
0000| 42 30 00 00
----- 0xe4932764cc: '\u{1f607}'
0000| 07 f6 01 00
----- 0xe4932764d0: 0.25f32
0000| 00 00 80 3e
----- 0xe493276510: 0.1f64
0000| 9a 99 99 99 99 99 b9 3f
- 筆者の環境ではデータはリトルエンディアンで格納されている(ターゲット依存)
- char は 4 バイト
- Unicode scalar value を表す(Unicode code point ではない)
- char - Rust
- f32, f64 は IEEE 754 準拠
ちなみに、
if cfg!(target_endian = "big") {
...
} else {
...
}
とするとターゲットのエンディアンネスで分岐できます。あるいは byteorder などのライブラリを使うとよさげ。
固定長配列
fn show_arrays() {
print_raw_bytes!([true, false, false, true]);
print_raw_bytes!([0xadde_u16, 0xefbe]);
print_raw_bytes!(*b"\xde\xad\xbe\xef");
print_raw_bytes!([0_u64; 0]);
print_raw_bytes!([[1_i8, 2, 3], [4, 5, 6], [7, 8, 9]]);
}
----- 0x563e641350: [true, false, false, true]
0000| 01 00 00 01
----- 0x563e641354: [44510u16, 61374]
0000| de ad be ef
----- 0x563e641358: *b"\xde\xad\xbe\xef"
0000| de ad be ef
----- 0x563e641448: [0u64; 0]
----- 0x563e641451: [[1i8, 2, 3], [4, 5, 6], [7, 8, 9]]
0000| 01 02 03 04 05 06 07 08 09
- プリミティブ型がそのまま並んだようになる
-
b"..."
リテラルの型は&[u8; N]
(固定長配列へのポインタ)- 文字列リテラルとは事情が異なる(
"..."
は&str
)(後述)
- 文字列リテラルとは事情が異なる(
- 0 要素の固定長配列のサイズ(
mem::size_of::<[T; 0]>()
)は 0 となる - 多次元の固定長配列もデータ上は 1 次元の固定長配列のように見える
タプル
fn show_tuples() {
print_raw_bytes!(());
print_raw_bytes!((0xefbeadde_u32));
print_raw_bytes!((0x1111111111111111_u64, 0x22222222_u32, 0x_3333u16, 0x44_u8));
print_raw_bytes!((0x11_u8, 0x2222_u16, 0x33_u8));
print_raw_bytes!(((), (0xefbeadde_u32), (0x11_u8, 0x2222_u16, 0x33_u8)));
}
----- 0x563e641483: ()
----- 0x563e641340: (4022250974u32)
0000| de ad be ef
----- 0x563e641498: (1229782938247303441u64, 572662306u32, 13107u16, 68u8)
0000| 11 11 11 11 11 11 11 11 22 22 22 22 33 33 44 00
----- 0x563e64135c: (17u8, 8738u16, 51u8)
0000| 22 22 11 33
----- 0x563e641390: ((), (4022250974u32), (17u8, 8738u16, 51u8))
0000| de ad be ef 22 22 11 33
- 0 要素タプルのサイズは 0
- 3 番目の例はそのままだとサイズが奇数バイトになるので最後にパディングが入っている
- 4 番目と最後の例ではパディングは入らず順序が入れ替わっている
struct
repr(Rust)
struct S1;
struct S2 { x: u32 }
struct S3 { x: u64, y: u32, z: u16, w: u8 }
struct S4 { x: u8, y: u16, z: u8 }
struct S5 { s1: S1, s2: S2, s4: S4 }
fn show_structs() {
print_raw_bytes!(S1 {});
print_raw_bytes!(S2 { x: 0xefbeadde_u32 });
print_raw_bytes!(S3 {
x: 0x1111111111111111_u64, y: 0x22222222_u32, z: 0x_3333u16, w: 0x44_u8
});
print_raw_bytes!(S4 { x: 0x11_u8, y: 0x2222_u16, z: 0x33_u8 });
print_raw_bytes!(S5 {
s1: S1 {},
s2: S2 { x: 0xefbeadde_u32 },
s4: S4 { x: 0x11_u8, y: 0x2222_u16, z: 0x33_u8 }
});
}
----- 0x563e641483: S1{}
----- 0x563e641340: S2{x: 4022250974u32,}
0000| de ad be ef
----- 0x563e641498: S3{x: 1229782938247303441u64, y: 572662306u32, z: 13107u16, w: 68u8,}
0000| 11 11 11 11 11 11 11 11 22 22 22 22 33 33 44 00
----- 0x563e64135c: S4{x: 17u8, y: 8738u16, z: 51u8,}
0000| 22 22 11 33
----- 0x563e641390: S5{s1: S1{}, s2: S2{x: 4022250974u32,}, s4: S4{x: 17u8, y: 8738u16, z: 51u8,},}
0000| de ad be ef 22 22 11 33
-
タプルと同じような結果となっている
- 前述の注意の通り同じレイアウトになることが言語上保証されている訳ではない
repr(C)
repr(C)
を付けて C 互換のレイアウトにしてみる。
#[repr(C)] struct CS1;
#[repr(C)] struct CS2 { x: u32 }
#[repr(C)] struct CS3 { x: u64, y: u32, z: u16, w: u8 }
#[repr(C)] struct CS4 { x: u8, y: u16, z: u8 }
#[repr(C)] struct CS5 { s1: CS1, s2: CS2, s4: CS4 }
fn show_c_structs() {
print_raw_bytes!(CS1 {});
print_raw_bytes!(CS2 { x: 0xefbeadde_u32 });
print_raw_bytes!(CS3 {
x: 0x1111111111111111_u64, y: 0x22222222_u32, z: 0x_3333u16, w: 0x44_u8
});
print_raw_bytes!(CS4 { x: 0x11_u8, y: 0x2222_u16, z: 0x33_u8 });
print_raw_bytes!(CS5 {
s1: CS1 {},
s2: CS2 { x: 0xefbeadde_u32 },
s4: CS4 { x: 0x11_u8, y: 0x2222_u16, z: 0x33_u8 }
});
}
----- 0x563e641483: CS1{}
----- 0x563e641360: CS2{x: 4022250974u32,}
0000| de ad be ef
----- 0x563e641498: CS3{x: 1229782938247303441u64, y: 572662306u32, z: 13107u16, w: 68u8,}
0000| 11 11 11 11 11 11 11 11 22 22 22 22 33 33 44 00
----- 0x563e6416a6: CS4{x: 17u8, y: 8738u16, z: 51u8,}
0000| 11 00 22 22 33 00
----- 0x563e6416d4: CS5{s1: CS1{}, s2: CS2{x: 4022250974u32,}, s4: CS4{x: 17u8, y: 8738u16, z: 51u8,},}
0000| de ad be ef 11 00 22 22 33 00 00 00
-
4 番目と最後の例ではフィールドの順序が入れ替わる代わりにパディングが入っている
- 確かに C/C++er にとってはおなじみのレイアウトになっている
enum
enum E1 { V1, V2, V3 }
enum E2 { V1(u32), V2(u8, u16, u8), V3 }
fn show_enums() {
print_raw_bytes!(E1::V1);
print_raw_bytes!(E1::V2);
print_raw_bytes!(E1::V3);
print_raw_bytes!(E2::V1(0xefbeadde));
print_raw_bytes!(E2::V2(0x11, 0x2222, 0x33));
print_raw_bytes!(E2::V3);
}
----- 0x563e64173b: E1::V1
0000| 00
----- 0x563e641742: E1::V2
0000| 01
----- 0x563e641749: E1::V3
0000| 02
----- 0x563e641398: E2::V1(4022250974)
0000| 00 00 00 00 de ad be ef
----- 0x563e6413a0: E2::V2(17, 8738, 51)
0000| 01 11 33 00 22 22 00 00
----- 0x563e6413a8: E2::V3
0000| 02 00 00 00 00 00 00 00
- 値を持たないいわゆる普通の列挙体はただの 1 バイトの整数のように見える
- 257 個以上の variant を持つ enum だと 2 バイトになる(試した)
- 値を持つ方は tagged union の名の通り tag っぽいところ(先頭の 00, 01, 02)と union っぽいところを目視できる
スライス・文字列
fn show_slices() {
let bs: &[u16] = &[0xadde_u16, 0xefbe];
print_raw_bytes!(bs);
print_raw_bytes!(*bs; mem::size_of::<u16>() * bs.len());
let s = "Hello, world!";
print_raw_bytes!(s);
print_raw_bytes!(*s; s.len());
let s = "ハロー、ワールド!";
print_raw_bytes!(s);
print_raw_bytes!(*s; s.len());
}
----- 0x7ffc14dbca80: bs
0000| d8 97 1b 04 2f 00 00 00 02 00 00 00 00 00 00 00
----- 0x2f041b97d8: *bs
0000| de ad be ef
----- 0x7ffc14dbcb98: s
0000| c1 9c 1b 04 2f 00 00 00 0d 00 00 00 00 00 00 00
----- 0x2f041b9cc1: *s
0000| 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 21
----- 0x7ffc14dbccb0: s
0000| e0 9c 1b 04 2f 00 00 00 1b 00 00 00 00 00 00 00
----- 0x2f041b9ce0: *s
0000| e3 83 8f e3 83 ad e3 83 bc e3 80 81 e3 83 af e3
0010| 83 bc e3 83 ab e3 83 89 ef bc 81
- [u16] や str は Sized を実装しない型で、&[u16] と &str は fat pointer になっている
- 通常のポインタの 2 倍のサイズ(
2 * mem::size_of::<usized>()
)を持っている- 筆者の環境では 16 バイト
- 最初の 8 バイトがデータへのポインタ、残りの 8 バイトがデータのサイズを表している
- 通常のポインタの 2 倍のサイズ(
- 文字列は UTF-8 で格納されている
- 参考: RustのSizedとfatポインタ - 簡潔なQ
トレイトオブジェクト
#![feature(raw)]
use std::raw;
trait T1 {
fn m1(&self) { println!("trait T1"); }
}
impl T1 for S4 {
fn m1(&self) { println!("impl T1 for S4"); }
}
fn show_trait_objects() {
let t: &T1 = & S4 { x: 0x11_u8, y: 0x2222_u16, z: 0x33_u8 };
print_raw_bytes!(t);
let t: raw::TraitObject = unsafe { mem::transmute(t) };
print_raw_bytes!(* unsafe { &*t.data }; mem::size_of::<S4>());
print_raw_bytes!(* unsafe { &*t.vtable }; 32);
}
----- 0x7ffd9a588e38: t
0000| 5c 13 64 3e 56 00 00 00 40 c3 85 3e 56 00 00 00
----- 0x563e64135c: *unsafe { &*t.data }
0000| 22 22 11 33
----- 0x563e85c340: *unsafe { &*t.vtable }
0000| 40 fd 5d 3e 56 00 00 00 04 00 00 00 00 00 00 00
0010| 02 00 00 00 00 00 00 00 10 c2 5d 3e 56 00 00 00
- 構造体への参照をトレイトにキャストするとトレイトオブジェクトを得られる
- 参考: トレイトオブジェクト
- 原文: Trait Objects
- fat pointer の一種
- 最初の 8 バイトがデータへのポインタ、残りの 8 バイトが vtable へのポインタを表している
- std::raw::TraitObject として transmute すると data, vtable フィールドとしてアクセスできる
-
#![feature(raw)]
が必須 - std::raw::TraitObject - Rust
-
- 参考: トレイトオブジェクト
vtable のサイズとレイアウトについては cargo rustc -- --emit=llvm-ir
すると (cargoプロジェクトのルート)/target/debug/deps 以下に *.ll が吐かれるのでそれを見るとわかる。
@vtable.2P = private unnamed_addr constant {
void (%S4*)*,
i64,
i64,
void (%S4*)*
} {
void (%S4*)* @_ZN4core3ptr13drop_in_place17ha4ad1a86f705d507E,
i64 4,
i64 2,
void (%S4*)* @"_ZN37_$LT$miru..S4$u20$as$u20$miru..T1$GT$2m117h1c51970c5378281eE"
}, align 8, !dbg !0
この例では関数ポインタ・i64・i64・関数ポインタを含んでいるので vtable のサイズは 32 バイト。
Box
fn show_boxes() {
let s = Box::new(S4 { x: 0x11_u8, y: 0x2222_u16, z: 0x33_u8 });
print_raw_bytes!(s);
print_raw_bytes!(* unsafe { &*Box::into_raw(s) });
let t: Box<T1> = Box::new(S4 { x: 0x11_u8, y: 0x2222_u16, z: 0x33_u8 });
print_raw_bytes!(t);
print_raw_bytes!(* unsafe { &*Box::into_raw(t) }; mem::size_of::<S4>());
}
----- 0x7ffd9a588d80: s
0000| 20 20 62 17 32 7f 00 00
----- 0x7f3217622020: *unsafe { &*Box::into_raw(s) }
0000| 22 22 11 33
----- 0x7ffd9a588e90: t
0000| 28 20 62 17 32 7f 00 00 40 c3 85 3e 56 00 00 00
----- 0x7f3217622028: *unsafe { &*Box::into_raw(t) }
0000| 22 22 11 33
- Box が保持するポインタがヒープ領域上にある実際のデータを指していることがわかる
-
into_raw
でそのポインタを取得できるがそれを参照にするには unsafe が必要
-
- Box<T1>(T1 はトレイト)は fat pointer になっている
おわりに
Rust のような言語の各種データ型がメモリ上にどう配置されているのかはボンヤリとは理解していたが、実際に見てみると意外と発見があり、わかりがより深まってよかった。