Я играл с типами нулевого размера (ZST), поскольку мне было любопытно, как они на самом деле реализованы под капотом. Учитывая, что для ZST не требуется никакого пространства в памяти, и использование необработанного указателя является безопасной операцией, меня интересовало, какие необработанные указатели я получу из различных типов «распределений» ZST и насколько странными (для безопасного Rust) будут результаты.
Моей первой попыткой (test_stk.rs
) было использование константных указателей для нескольких экземпляров ZST в стеке:
struct Empty;
struct EmptyAgain;
fn main() {
let stk_ptr: *const Empty = &Empty;
let stk_ptr_again: *const EmptyAgain = &EmptyAgain;
let nested_stk_ptr = nested_stk();
println!("Pointer to on-stack Empty: {:?}", stk_ptr);
println!("Pointer to on-stack EmptyAgain: {:?}", stk_ptr_again);
println!("Pointer to Empty in nested frame: {:?}", nested_stk_ptr);
}
fn nested_stk() -> *const Empty {
&Empty
}
Компиляция и запуск этого приводили к следующему результату:
$ rustc test_stk.rs -o test_stk
$ ./test_stk
Pointer to on-stack Empty: 0x55ab86fc6000
Pointer to on-stack EmptyAgain: 0x55ab86fc6000
Pointer to Empty in nested frame: 0x55ab86fc6000
Краткий анализ карты памяти процесса показал, что 0x55ab86fc6000
на самом деле было не выделением стека, а самым началом раздела .rodata
. Это кажется логичным: компилятор делает вид, что для каждого ZST существует одно значение нулевого размера, известное во время компиляции, и каждое из этих значений находится в .rodata
, как это делают константы во время компиляции.
вторая попытка была в штучных ZST (test_box.rs
):
struct Empty;
struct EmptyAgain;
fn main() {
let ptr = Box::into_raw(Box::new(Empty));
let ptr_again = Box::into_raw(Box::new(EmptyAgain));
let nested_ptr = nested_box();
println!("Pointer to boxed Empty: {:?}", ptr);
println!("Pointer to boxed EmptyAgain: {:?}", ptr_again);
println!("Pointer to boxed Empty in nested frame: {:?}", nested_ptr);
}
fn nested_box() -> *mut Empty {
Box::into_raw(Box::new(Empty))
}
Запуск этого фрагмента дал:
$ rustc test_box.rs -o test_box
$ ./test_box
Pointer to boxed Empty: 0x1
Pointer to boxed EmptyAgain: 0x1
Pointer to boxed Empty in nested frame: 0x1
Быстрая отладка показала, что так работает распределитель для ZST (Rust's *)1022 *):
unsafe fn exchange_malloc(size: usize, align: usize) -> *mut u8 {
if size == 0 {
align as *mut u8
} else {
// ...
}
}
Минимально возможное выравнивание равно 1 (согласно Nomicon), поэтому для ZST оператор box
вызывает exchange_malloc(0, 1)
, а результирующий адрес - 0x1
.
Заметив, что into_raw()
возвращает изменяемый указатель, я решил повторить предыдущий тест (в стеке) с изменяемыми указателями (test_stk_mut.rs
):
struct Empty;
struct EmptyAgain;
fn main() {
let stk_ptr: *mut Empty = &mut Empty;
let stk_ptr_again: *mut EmptyAgain = &mut EmptyAgain;
let nested_stk_ptr = nested_stk();
println!("Pointer to on-stack Empty: {:?}", stk_ptr);
println!("Pointer to on-stack EmptyAgain: {:?}", stk_ptr_again);
println!("Pointer to Empty in nested frame: {:?}", nested_stk_ptr);
}
fn nested_stk() -> *mut Empty {
&mut Empty
}
И, выполнив это, напечатал следующее:
$ rustc test_stk_mut.rs -o test_stk_mut
$ ./test_stk_mut
Pointer to on-stack Empty: 0x7ffc3817b5e0
Pointer to on-stack EmptyAgain: 0x7ffc3817b5f0
Pointer to Empty in nested frame: 0x7ffc3817b580
Оказывается, на этот раз у меня было реальных значений, выделенных стеком , каждое из которых имеет свой собственный адрес! Когда я попытался объявить их последовательно (test_stk_seq.rs
), я обнаружил, что каждое из этих значений занимает восемь байтов:
struct Empty;
fn main() {
let mut stk1 = Empty;
// <4 times more>
let stk_ptr1: *mut Empty = &mut stk1;
// <4 times more>
println!("Pointer to on-stack Empty: {:?}", stk_ptr1);
// <4 times more>
}
Выполнить:
$ rustc test_stk_seq.rs -o test_stk_seq
$ ./test_stk_seq
Pointer to on-stack Empty: 0x7ffdba303840
Pointer to on-stack Empty: 0x7ffdba303848
Pointer to on-stack Empty: 0x7ffdba303850
Pointer to on-stack Empty: 0x7ffdba303858
Pointer to on-stack Empty: 0x7ffdba303860
Итак, вот вещи, которые я не могу понять:
Почему при распределении по ZST в штучной упаковке используется тупой адрес 0x1
вместо чего-то более значимого, как в случае значений "в стеке"?
Почему необходимо выделять реальное пространство для значений ZST в стеке, когда на них есть изменяемые необработанные указатели?
Почемуровно восемь байтов, используемых для изменяемого распределения в стеке? Должен ли я рассматривать этот размер как «0 байтов фактического размера шрифта + 8 байтов выравнивания»?