Rust - 메모리를 덤프해서 slice와 Vec를 이해한다

출처

사전 준비

메모리 상에서 어떻게 되어 있는지 표현 되어 있는가를 확인하기 위해 아래 함수를 이용한다.
인수 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

다음으로 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,
}

또 RawVec는 raw_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>
  • String, Vec<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바이트 길이이다.

다음으로 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]

lencap이 3(뒤에서 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으로 구성된다.

이번 회의 표시된 대상은 [&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

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]eh [&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> 타입이기 때문에 실 데이터로의 포인터, len, cap를 가진 결과로 되어 있는 것을 알 수 있다.
타입을 위해 실 데어터로의 포인터, len, cap 을 가진 결과로 되어 있는 것을 알 수 있다.

a.as_ptr()(0x00007fd5d0c02da0)을 참조를 빼내면 “a”를 얻을 수 있다. a[0].as_ptr()(0x00007fd5d0c02cd0)는 다른 어드레스이지만 같은 “a”를 참조 하고 있다.


이 글은 2020-04-20에 작성되었습니다.