Unsafe-контекст

Последнее обновление: 08.05.2021

Одна из отличительных особенностей Rust состоит в том, что он может быть использоваться в низкоуровневом системном программировании, то есть он позволяет напрямую обращаться к операционной системе, писать драйверы и даже свои операционные системы. Однако такие возможности в первую очередь реализуются через так называемый "небезопасный код" (unsafe). Почему "небезопасный"? Потому что при компиляции такой код позволяет избежать проверок компилятора при использовании определенных возможностей языка Rust. С одной стороны, написание небезопасного кода дает нам большие возможности, в частности, как было сказано выше, использовать низкоуровневые возможности языка. Но с другой стороны, лишает нас большего контроля со стороны компилятора и соответственно обеспечивает меньшую защиту от потенциальных багов и ошибок.

Для написания "небезопасного кода" применяется блок кода unsafe:

unsafe{
	// небезопасный код
}

При этом в блоке unsafe также проверяется владение переменных, использование ссылок и ряд других стандартных проверок Rust. Ключевое слово unsafe лишь дает доступ к пяти возможностям языка Rust, которые не проверяются компилятором на безопасность памяти:

  • Указатели

  • unsafe-функции

  • Статические переменные

  • unsafe-трейты

  • Тип union

Указатели

Указетели позволяют обращаться к значениями по определенным адресам в памяти. Для определения указателей применяется оператор разыменования (dereference operator) - оператор *. В Rust есть два типа указателей:

  • Неизменяемые или константны указатели. Они определяются в виде *const T, где T предоставляет конкретный тип.

  • Изменяемые указатели. Они определяются в виде *mut T.

Создадим по указателю для каждого типа:

fn main() {
    
	let mut num = 5;

    let n1 = &num as *const i32;
    let n2 = &mut num as *mut i32;
}

Здесь определены два указателя - n1 и n2. n1 - константый указатель на значение типа i32. n2 - изменяемый указатель на значение типа i32.

Для получения указателя на адрес переменной к ссылке на объект применяется операция as, которая преобразует объект к типу указателя.

Причем для создания изменяемого указателя сама переменная должна быть определена с ключевым словом mut:

let mut num = 5;

А для создания изменяемого указателя применяется изменяемая ссылка на эту переменную:

let n2 = &mut num as *mut i32;

Стоит отметить, что сами указатели можно определять вне блока unsafe. Однако вне этого блока нельзя обратиться к значению в области памяти, на которую указывает указатель.

Что хранят указатели? Они хранят адрес на некоторую область в памяти. Например, получим значения выше определенных указателей:

fn main() {
    
	let mut num = 5;

    let n1 = &num as *const i32;
    let n2 = &mut num as *mut i32;
	
	println!("n1={:p}", n1);
	println!("n2={:p}", n2);
}

Для вывода адреса, который хранит указатель, применяется спецификатор :p - он указывает, что здесь будет выводиться значение указателя.

Консольный вывод программы в моей случае:

n1=0x38b50ffa94
n2=0x38b50ffa94

Оба указателя имеют одно и то же значение, потому что они оба указывают на один и тот же адрес в памяти - адрес переменной num.

Операция разыменования

С помощью операции разыменования (операция *) можно обратиться к значению по адресу, который хранится в указателе. Однако это обращение должно производиться в блоке unsafe:

fn main() {
    
	let mut num = 5;

    let n1 = &num as *const i32;
	unsafe{
	
		println!("{}", *n1);	// 5
	}
}

Используя полученное значение в результате операции разыменования мы можем присвоить его другой переменной:

fn main() {
    
	let num = 5;

    let num_pointer = &num as *const i32;
	unsafe{
		let number: i32 = *num_pointer;
		println!("number: {}", number);
	}
}

Причем стоит отметить, что указатель возвращает значение того типа, на объект которого он указывает. Так, в данном случае указатель num_pointer представляет указатель на объект типа i32, поэтому операция разыменования возвратить значение типа i32

И также используя указатель, мы можем менять значение по адресу, который хранится в указателе:

fn main() {
    
	let mut num = 5;

    let num_pointer = &mut num as *mut i32;
	unsafe{
		*num_pointer  = 29;		// изменяем значение в памяти, на которую указывает указатель
	}
	println!("num: {}", num);	// num: 29
}

Стоит отметить, что если мы хотим изменять значение в памяти, на которую указывает указатель, это должен быть именно изменяемый указатель. Константный указатель не позволяет изменять значение.

Второй момент: при этом изменяется значение не самого указателя - он по прежнему хранит адрес в памяти, а изменяется значение, которое храниться по этому адресу.

Передача указателю произвольного адреса

Следует отметить, что в принципе мы можем передавать указателю произвольный адрес:

fn main() {
    
	let addr = 0x38b50ff4usize;
    let p = addr as *const i32;
	println!("Address: {:p}", p);
}

Адрес определяется как шестнадцатиричное число - в данном случае это адрес 0x38b50ff4, которое представляет тип usize. Но такие обращения к произвольным областям памяти программист делает на свой страх и риск.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850