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>
은 len
와 cap
을 가진다.
표현을 다시 해보면 후반의 4에서 시작하는 8바이트 2행은 len
와 cap
이 있는 것을 알 수 있다.
[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]
}
*p
나 data
의 출력 결과에서 확실하게 실 데이터로의 포인터로 되어 있다.
문자열 관련
[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]
len
과 cap
이 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]
a
는 Vec<String>
타입이기 때문에 실 데이터로의 포인터, len, cap를 가진 결과로 되어 있는 것을 알 수 있다.
타입을 위해 실 데어터로의 포인터, len, cap 을 가진 결과로 되어 있는 것을 알 수 있다.
a.as_ptr()(0x00007fd5d0c02da0)
을 참조를 빼내면 “a”를 얻을 수 있다. a[0].as_ptr()(0x00007fd5d0c02cd0)
는 다른 어드레스이지만 같은 “a”를 참조 하고 있다.
이 글은 2020-04-20에 작성되었습니다.