Почему в некоторых случаях типы нулевого размера вызывают реальное распределение? - PullRequest
6 голосов
/ 23 октября 2019

Я играл с типами нулевого размера (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

Итак, вот вещи, которые я не могу понять:

  1. Почему при распределении по ZST в штучной упаковке используется тупой адрес 0x1 вместо чего-то более значимого, как в случае значений "в стеке"?

  2. Почему необходимо выделять реальное пространство для значений ZST в стеке, когда на них есть изменяемые необработанные указатели?

  3. Почемуровно восемь байтов, используемых для изменяемого распределения в стеке? Должен ли я рассматривать этот размер как «0 байтов фактического размера шрифта + 8 байтов выравнивания»?

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...