Edited at
RustDay 23

Rust の struct, enum, &str, TraitObject などをバイト列として眺めてみる

More than 1 year has passed since last update.


はじめに

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


使用例


コード

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 の章に記載されています。


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 バイトがデータのサイズを表している





  • 文字列は 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


  • 構造体への参照をトレイトにキャストするとトレイトオブジェクトを得られる


    • 参考: トレイトオブジェクト



    • fat pointer の一種


      • 最初の 8 バイトがデータへのポインタ、残りの 8 バイトが vtable へのポインタを表している

      • std::raw::TraitObject として transmute すると data, vtable フィールドとしてアクセスできる







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 のような言語の各種データ型がメモリ上にどう配置されているのかはボンヤリとは理解していたが、実際に見てみると意外と発見があり、わかりがより深まってよかった。