2016-09-28 8 views
20

В документации для mem::uninitialized указано, почему это опасно/небезопасно для использования этой функции: вызов drop в неинициализированной памяти является неопределенным поведением.Как Rust знает, следует ли запускать деструктор во время стека?

Так что этот код должен быть, как мне кажется, не определено:

let a: TypeWithDrop = unsafe { mem::uninitialized() }; 
panic!("=== Testing ==="); // Destructor of `a` will be run (U.B) 

Однако, я написал этот кусок кода, который работает в безопасном Rust и, кажется, не страдает от неопределенного поведения:

#![feature(conservative_impl_trait)] 

trait T { 
    fn disp(&mut self); 
} 

struct A; 
impl T for A { 
    fn disp(&mut self) { println!("=== A ==="); } 
} 
impl Drop for A { 
    fn drop(&mut self) { println!("Dropping A"); } 
} 

struct B; 
impl T for B { 
    fn disp(&mut self) { println!("=== B ==="); } 
} 
impl Drop for B { 
    fn drop(&mut self) { println!("Dropping B"); } 
} 

fn foo() -> impl T { return A; } 
fn bar() -> impl T { return B; } 

fn main() { 
    let mut a; 
    let mut b; 

    let i = 10; 
    let t: &mut T = if i % 2 == 0 { 
     a = foo(); 
     &mut a 
    } else { 
     b = bar(); 
     &mut b 
    }; 

    t.disp(); 
    panic!("=== Test ==="); 
} 

Кажется, что он выполняет правильный деструктор, игнорируя другой. Если я попытался использовать a или b (например, a.disp() вместо t.disp()), он правильно ошибается, говоря, что, возможно, я могу использовать неинициализированную память. Меня поразило panic король, он всегда запускает правый деструктор (печатает ожидаемую строку) независимо от того, что такое значение i.

Как это происходит? Если среда выполнения может определить, какой деструктор будет запущен, должна ли быть извлечена часть из памяти, которая обязательно должна быть инициализирована для типов с Drop, из документации mem::uninitialized(), как указано выше?

+2

Как Раймонд Чен любит указывать, потому что «неопределенное поведение» означает «что-либо может случиться и все еще быть действительным», * один из действительных последствий заключается в том, что все должно работать правильно. * –

+0

A.K.A. «ошибочное отсутствие доказательств для доказательства отсутствия» - в печально известном «Черном лебеде». – mickeyf

+1

@mickeyf @MasonWheeler: Извините, если я не понял вас, но в Rust я ожидал бы, если бы я не использовал какой-либо «небезопасный» код (которого я не делаю в основном примере выше), что бы наблюдалось (даже в первом запуске) в значительной степени четко определено - это будет (одна из) главная причина, по которой многие выбрали бы Rust в первую очередь (по крайней мере, я сделал). – ustulation

ответ

20

Rust (вплоть до версии 1.12) содержит булевский флаг в каждом значении, тип которого реализует Drop (и, таким образом, увеличивает размер этого типа на один байт). Этот флаг определяет, следует ли запускать деструктор. Поэтому, когда вы делаете b = bar(), он устанавливает флаг для переменной b и, таким образом, работает только деструктор b. И наоборот: a.

Обратите внимание, что начиная с версии Rust версии 1.13 (на момент написания бета-компилятора) этот флаг не хранится в типе, а в стеке для каждой переменной или временной. Это стало возможным благодаря появлению MIR в компиляторе Rust. MIR значительно упрощает перевод кода Rust на машинный код и, таким образом, позволяет этой функции перемещать флаги drop в стек. Оптимизации обычно устраняют этот флаг, если они могут определить во время компиляции, когда какой объект будет удален.

Вы можете «наблюдать» этот флаг в Rust компилятор до версии 1.12, глядя на размер типа:

struct A; 

struct B; 

impl Drop for B { 
    fn drop(&mut self) {} 
} 

fn main() { 
    println!("{}", std::mem::size_of::<A>()); 
    println!("{}", std::mem::size_of::<B>()); 
} 

печатает 0 и 1 соответственно до флагов стека и 0 и 0 со стеком флаги.

Использование mem::uninitialized все еще небезопасно, поскольку компилятор все еще видит назначение переменной a и устанавливает флаг капли. Таким образом, деструктор будет вызван в неинициализированную память. Обратите внимание, что в вашем примере Drop impl не имеет доступа к какой-либо памяти вашего типа (кроме флага падения, но это вам незаметно). Поэтому вы не получаете доступ к неинициализированной памяти (размер которой равен нулю, так как ваш тип представляет собой структуру с нулевым размером). Насколько я знаю, это означает, что ваш код unsafe { std::mem::uninitialized() } на самом деле безопасен, потому что после этого не может произойти ненадежность памяти.

+0

Справа - мой drop() impl тривиально, чтобы просто показывать распечатки, из которых выполняются, но я говорил об общих случаях курса. Один вопрос, но почему 'mem :: uninitialized()' рассматривается как назначение для установки флага падения? Если бы я устанавливал его в определенном месте (например, 'operator new()'), тогда это имело бы смысл, но назначение случайной ячейки памяти (где бы она ни находилось в стеке) не должно было устанавливать флаг drop? Я имею в виду, когда это было бы значимым? (Он может установить его при следующем назначении, как и в случае 'b', когда' i' является нечетным). – ustulation

+1

хорошо ... разница в том, что 'let x = mem :: uninitialized()' позволяет вам ссылаться на 'x', а' let x; 'не позволяет этого. Поэтому вы можете передать эту ссылку на функцию, которая только записывает в эту память. Общим примером является 'let mut x: [i8; 42] = mem :: uninitialized(); some_slice_function (& mut x) ' –

3

Во-первых, есть drop flags - информация о времени выполнения для отслеживания переменных, которые были инициализированы. Если переменная не была назначена, drop() для нее не будет выполнена.

В стабильном состоянии флаг капли сохраняется в самом типе. Написание неинициализированной памяти на него может привести к неопределенному поведению относительно того, будет ли или не вызываться drop(). Это скоро будет устаревшей информацией, потому что флаг капли переносится из самого типа в ночное время.

В ночной ржавчине, если вы присваиваете переменную неинициализированную память, было бы безопасно предположить, что будет выполнен drop(). Однако любая полезная реализация drop() будет действовать на значение. Невозможно определить, правильно ли инициализирован тип или нет в реализации функции Drop: это может привести к попытке освободить недействительный указатель или любую другую случайную вещь в зависимости от реализации типа Drop. В любом случае присвоить неинициализированную память типу с Drop.

16

Есть два вопроса, скрытых здесь:

  1. Как трек компилятор, который переменная инициализируется или нет?
  2. Почему инициализация с помощью mem::uninitialized() приводит к неопределенному поведению?

Давайте рассмотрим их по порядку.


Как трек компилятор, который переменная инициализируется или нет?

Компилятор впрыскивает так называемые «DROP флаги»: для каждой переменной, для которой Drop должен работать в конце области, логическое значение флага вводится в стек, указав, нуждается ли эта переменная быть утилизированы ,

Флаг начинается с «нет», переходит в «да», если переменная инициализируется, и возвращается к «нет», если переменная перемещается из.

Наконец, когда приходит время, чтобы сбросить эту переменную, флаг проверяется и при необходимости отбрасывается.

Это не связано с тем, удовлетворяет ли анализ потока компилятора потенциально неинициализированными переменными: только при выполнении анализа потока генерируется код.


Почему может инициализацию с mem::uninitialized() привести к непредсказуемому поведению?

При использовании mem::uninitialized() вы делаете обещание компилятора: не волнуйтесь, я определенно инициализирую этот.

Что касается компилятора, значит, переменная полностью инициализирована и флаг падения установлен на «да» (пока вы не выйдете из него).

Это, в свою очередь, означает, что будет называться Drop.

Использование неинициализированного объекта - это неопределенное поведение, а вызов компилятора Drop в неинициализированном объекте от вашего имени считается «использованием его».


Бонус:

В моих тестах, ничего странного не произошло!

Обратите внимание, что Undefined Behavior означает, что все может случиться; что-то, к сожалению, также включает «кажется, что работает» (или даже «работает по назначению, несмотря на шансы»).

В частности, если вы НЕ обращаетесь к памяти объекта в Drop::drop (просто печатаете), то очень вероятно, что все будет работать. Однако, если вы делаете доступ к нему, вы можете увидеть странные целые числа, указатели, указывающие на дикую природу и т. Д.

И если оптимизатор умный, даже без доступа к нему, это может сделать странные вещи! Поскольку мы используем LLVM, я приглашаю вас прочитать What every C programmer should know about Undefined Behavior Крисом Латтнером (отцом LLVM).

+0

Дальнейший вопрос: если' i' является четным, а компилятор может отслеживать, что 'b' не инициализирован и не вызывает его dtor, то он не может отслеживать что-то вроде' let u: Type = mem: : uninitialized(); 'тоже неинициализирован (infact я явно указываю его как таковой), а ** не ** вызывает его dtor во время стека, если он не назначен? – ustulation

+1

На бонусной части - UB не должно быть возможным без «небезопасного» кода, поэтому, если он работает, лучше быть определенным поведением, так как я не использовал какой-либо небезопасный код в основном примере в OP (в случае, если вы ссылаясь на него) – ustulation

+0

(Кстати, ваш ответ довольно объяснительный/полезный, но он принял другое, потому что он был поставлен 1-й и был в равной степени полезен) – ustulation