メモリをダンプしてRustのsliceとVecを理解する

タイトルの通りなんですが、RustのsliceやVecがしっくり来ていないので、メモリ上でどのように表現されているのかという観点で理解を深めようと思います。

環境

❯ cargo version
cargo 1.32.0

❯ rustc --version
rustc 1.33.0

また今回の環境ではusizeは8バイト長です。

println!("size of usize: {}", std::mem::size_of::<usize>());
// size of usize: 8

事前準備

メモリ上でどのように表現されているか確認するため、RustのSizedとfatポインタ - 簡潔なQas_raw_bytesを利用します。 引数x*const Tでキャストして、生ポインタからstd::mem::size_of_val(x)で得たバイト長を読み出します。

fn as_raw_bytes<'a, T: ?Sized>(x: &'a T) -> &'a [u8] {
    unsafe {
        std::slice::from_raw_parts(
            x as *const T as *const u8,
            std::mem::size_of_val(x))
    }
}

結論

  • [T]はスタック領域に連続して表現される
  • Vec<T>はスタック領域に「実データへのポインタ」「len」「cap」を持つ
  • &strはスタック領域に「実データへのポインタ」「len」を持つ

具体例

[i32]

まずは[i32]i32のslice)でメモリ上の内容を見てみましょう。

let a: [i32; 5] = [255, 256, 1023, 1024, 1025];
println!("{:x?}", as_raw_bytes(&a));

実行結果は以下のようになります。 読みやすいように改行などの整形を入れています。 以降でも同じように整形して読みやすくしています。

[ff, 0, 0, 0,
 0, 1, 0, 0,
ff, 3, 0, 0,
 0, 4, 0, 0,
 1, 4, 0, 0]

これだけだと分かりづらいので、as_raw_bytesの返り値の型が[u8]であることを考慮して0を補完します。

[ff, 00, 00, 00,
 00, 01, 00, 00,
 ff, 03, 00, 00,
 00, 04, 00, 00,
 01, 04, 00, 00]

トルエンディアンで表現されていることに注意すれば、 

0d255 = 0x000000ff
0d256 = 0x00000100
0d1023 = 0x000003ff
0d1024 = 0x00000400
0d1025 = 0x00000401

となることがわかると思います。 またヒープ領域へのポインタなどもなく、スタック領域に確保もされていることも確認できました。

Vec<i32>

次にVec<i32>を見ていきます。

let a: Vec<i32> = vec![1, 2, 3, 4];
println!("{:x?}", as_raw_bytes(&a));

結果は以下のようになりました。

[d0, 2c, c0, d5, a8, 7f, 00, 00,
 04, 00, 00, 00, 00, 00, 00, 00,
 04, 00, 00, 00, 00, 00, 00, 00]

ここでVec<T>の実装を見てみましょう。 vec.rsの実装は以下のようになっています。

#[stable(feature = "rust1", since = "1.0.0")]
pub struct Vec<T> {
    buf: RawVec<T>,
    len: usize,
}

またRawVecraw_vec.rsで実装されています。

#[allow(missing_debug_implementations)]
pub struct RawVec<T, A: Alloc = Global> {
    ptr: Unique<T>,
    cap: usize,
    a: A,
}

このようにVec<T>lencapを持ちます。

改めて結果を見てみると後半の4から始まる8バイト2行はlencapであることがわかります。

// (再掲)
[d0, 2c, c0, d5, a8, 7f, 00, 00,
 04, 00, 00, 00, 00, 00, 00, 00,
 04, 00, 00, 00, 00, 00, 00, 00]

最初の8バイトが実データへのポインタになっているのですが、本当にそうなっているか参照外しをして確認しましょう。

let a = vec![4, 1, 2, 3];

unsafe {
    let p = a.as_ptr();
    println!("{:?}", p); // 0x7feba05027c0
    println!("{:?}", *p); // 4 <- 先頭のデータ
    let data: &[u8] = std::slice::from_raw_parts(p, a.len());
    println!("{:?}", data); // [4, 1, 2, 3]
}

*pdataの出力結果から、確かに実データへのポインタとなっていました。

文字列まわり

[i32]Vec<i32>を確認できたので、おまけとして文字列を扱う際に登場する以下3つのメモリ内容も確認してみます。

  • [char]Vec<char>
  • [&str]Vec<&str>
  • StringVec<String>

char

[char]から見ていきましょう。

let a = ['a', 'b', 'c'];

println!("{:x?}", as_raw_bytes(&a));
// [61, 0, 0, 0, 62, 0, 0, 0, 63, 0, 0, 0]

[i32]と同じでスタック領域のみで表現されていることがわかります。 ちなみに、char - Rustにあるようにcharは4バイト長です。

char is always four bytes in size.

次にVec<char>です。

let a = vec!['a', 'b', 'c'];

println!("{:x?}", as_raw_bytes(&a));
// [d0, 2c, 40, d7, ad, 7f, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0]

unsafe {
    let p = a.as_ptr();
    println!("{:?}", p);
    // 0x7fadd7402cd0

    println!("{:?}", *p);
    // 'a'

    let data = std::slice::from_raw_parts(p, a.len());
    println!("{:?}", data);
    // ['a', 'b', 'c']
}

as_raw_bytes(&a)の出力結果を整形します。

[d0, 2c, 40, d7, ad, 7f, 00, 00,
 03, 00, 00, 00, 00, 00, 00, 00,
 03, 00, 00, 00, 00, 00, 00, 00]

lencap3(うしろ2行)で、1行目はヒープにある実データへのポインタになっています。 実際、unsafeの中で参照外しを行うと'a'が得られるため、実データの先頭アドレスであったことがわかります。

[&str]

続けて[&str]です。

let a: [&str; 3] = ["a", "b", "c"];

println!("{:x?}", as_raw_bytes(&a));
// [82, 59, 85, 4, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 80, 59, 85, 4, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 81, 59, 85, 4, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0]

今までとは様子が違いそうです。 理解を深めるため、別の例をもうひとつ表示してみます。

let a: [&str; 3] = ["a", "ab", "abc"];

println!("{:x?}", as_raw_bytes(&a));
// [35, 77, 46, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 30, 77, 46, 1, 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 32, 77, 46, 1, 1, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0]

上記2つの結果を見やすいように整形しました。

[82, 59, 85, 04, 01, 00, 00, 00,
 01, 00, 00, 00, 00, 00, 00, 00,
 80, 59, 85, 04, 01, 00, 00, 00,
 01, 00, 00, 00, 00, 00, 00, 00,
 81, 59, 85, 04, 01, 00, 00, 00,
 01, 00, 00, 00, 00, 00, 00, 00]
[35, 77, 46, 01, 01, 00, 00, 00,
 01, 00, 00, 00, 00, 00, 00, 00,
 30, 77, 46, 01, 01, 00, 00, 00,
 02, 00, 00, 00, 00, 00, 00, 00,
 32, 77, 46, 01, 01, 00, 00, 00,
 03, 00, 00, 00, 00, 00, 00, 00]

実は、str - Rustにあるように&strは実データへのポインタとlenで構成されます。

A &str is made up of two components: a pointer to some bytes, and a length.

今回の表示した対象は[&str]&strのslice)です。[i32]の例でみたようにsliceはスタック領域に連続してデータを格納します。 そして&strは上記の通り、実データへのポインタとlenで表現されます。 これらの組み合わせで、ポインタ+lenが3つ並ぶ結果となったのです。

念のため、3つの要素のうち最初の8バイトが実データへのポインタであることも確かめておきましょう。

let a = ["a", "ab", "abc"];

println!("{:x?}", as_raw_bytes(&a));
// [55, 8e, f9, 7, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 50, 8e, f9, 7, 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 52, 8e, f9, 7, 1, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0]

unsafe {
    for i in 0..a.len() {
        println!("-------");
        let p = a[i].as_ptr();
        println!("{:?}", p);
        println!("{:?}", *p);
        let data = std::slice::from_raw_parts(p, a[i].len());
        println!("{:?}", data);
    }
    // -------
    // 0x107f98e55
    // 97
    // [97]
    // -------
    // 0x107f98e50
    // 97
    // [97, 98]
    // -------
    // 0x107f98e52
    // 97
    // [97, 98, 99]
}

確かに、3つに分かれた各要素の先頭8バイトが実データへのポインタになっていました。

String

続いてStringです。

let a: [String; 3] = [
    "a".to_string(),
    "ab".to_string(),
    "abc".to_string()];

println!("{:x?}", as_raw_bytes(&a));
// [90, 2c, c0, 4a, 9b, 7f, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, a0, 2c, c0, 4a, 9b, 7f, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, b0, 2c, c0, 4a, 9b, 7f, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0]

例によって整形します。

[90, 2c, c0, 4a, 9b, 7f, 00, 00,
 01, 00, 00, 00, 00, 00, 00, 00,
 01, 00, 00, 00, 00, 00, 00, 00,
 a0, 2c, c0, 4a, 9b, 7f, 00, 00,
 02, 00, 00, 00, 00, 00, 00, 00,
 02, 00, 00, 00, 00, 00, 00, 00,
 b0, 2c, c0, 4a, 9b, 7f, 00, 00,
 03, 00, 00, 00, 00, 00, 00, 00,
 03, 00, 00, 00, 00, 00, 00, 00]

[String][&str]と同じようにスタック領域に3つのStringが並んでいます。 上述の通り&strは実データへのポインタとlenを持っていますが、Stringはこれに加えてcapを持ちます。 ちょうどVecに対応しています。

そうとなれば実データを指しているのか確認したくなりますね。

let a: [String; 3] = [
    "a".to_string(),
    "ab".to_string(),
    "abc".to_string()];

println!("{:x?}", as_raw_bytes(&a));
// [90, 2e, 40, 6f, 8c, 7f, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, a0, 2e, 40, 6f, 8c, 7f, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, b0, 2e, 40, 6f, 8c, 7f, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0]

unsafe {
    for i in 0..a.len() {
        println!("-------");
        let p = a[i].as_ptr();
        println!("{:?}", p);
        println!("{:?}", *p);
        let data = std::slice::from_raw_parts(p, a[i].len());
        println!("{:?}", data);
    }
    // -------
    // 0x7f8c6f402e90
    // 97
    // [97]
    // -------
    // 0x7f8c6f402ea0
    // 97
    // [97, 98]
    // -------
    // 0x7f8c6f402eb0
    // 97
    // [97, 98, 99]
}

確かに実データへのポインタとなっていました。

続けて、Vec<String>です。

let a: Vec<String> = vec![
    "a".to_string(),
    "ab".to_string(),
    "abc".to_string()];

println!("{:x?}", as_raw_bytes(&a));
// [a0, 2d, c0, d0, d5, 7f, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0]

unsafe {
    let p = a.as_ptr(); // p: *const String
    println!("{:?}", p);
    // 0x7fd5d0c02da0

    println!("{:?}", *p);
    // "a"

    let data = std::slice::from_raw_parts(p, a.len()); // data: &[String]
    println!("{:?}", data);
    // ["a", "ab", "abc"]

    for i in 0..a.len() {
        println!("-------");
        let p = a[i].as_ptr(); // p: *const u8
        println!("{:?}", p);
        println!("{:?}", *p);
        let data = std::slice::from_raw_parts(p, a[i].len()); // data: &[u8]
        println!("{:?}", data);
    }
    // -------
    // 0x7fd5d0c02cd0
    // 97
    // [97]
    // -------
    // 0x7fd5d0c02ce0
    // 97
    // [97, 98]
    // -------
    // 0x7fd5d0c02cf0
    // 97
    // [97, 98, 99]
}

一番最初のas_raw_bytes(&a)の結果を整形します。

[a0, 2d, c0, d0, d5, 7f, 00, 00,
 03, 00, 00, 00, 00, 00, 00, 00,
 03, 00, 00, 00, 00, 00, 00, 00]

aVec<String>型のため、実データへのポインタ、lencapを持つ結果となっていることがわかります。

a.as_ptr()(0x00007fd5d0c02da0)を参照外しすると"a"が得られました。 a[0].as_ptr()(0x00007fd5d0c02cd0)は違うアドレスですが、同じ"a"を参照しています。 図にすると以下のようになります。

f:id:cipepser:20190317174507p:plain

References

プログラミングRust

プログラミングRust