Категория > Новости > Безопасность памяти. Учимся использовать указатели и линейные типы - «Новости»

Безопасность памяти. Учимся использовать указатели и линейные типы - «Новости»


17-11-2020, 00:01. Автор: Ogden
пре­дыду­щей стать­ей, где я рас­смат­ривал механиз­мы сбор­ки мусора и их осо­бен­ности.

Как мы видели на при­мере с исполь­зовани­ем Boehm GC в C, сбор­ка мусора сама по себе реша­ет толь­ко проб­лему с утеч­ками памяти. Безопас­ность памяти обес­печива­ют уже свой­ства самого язы­ка.


Ас­соци­ация меж­ду сбор­кой мусора и безопас­ностью памяти воз­ника­ет от того, что мно­гие популяр­ные прик­ладные язы­ки не раз­реша­ют руч­ное управле­ние памятью и адресную ариф­метику вов­се, — в этих усло­виях у поль­зовате­ля прос­то нет воз­можнос­ти выпол­нить небезо­пас­ную опе­рацию. Одна­ко и воз­можнос­ти осво­бодить память тоже нет, поэто­му нужен ка­кой-то механизм авто­мати­чес­кого управле­ния, и сбор­ка мусора — самый популяр­ный.


Са­мый популяр­ный не зна­чит единс­твен­ный и уни­вер­саль­ный. Пер­вая и глав­ная проб­лема язы­ков с при­нуди­тель­ной сбор­кой мусора — на них мож­но писать толь­ко прог­раммы, которые выпол­няют­ся в прос­транс­тве поль­зовате­ля. Ядру опе­раци­онной сис­темы или про­шив­ке мик­рокон­трол­лера не на кого положить­ся, они вынуж­дены управлять памятью самос­тоятель­но, а зна­чит, и язык дол­жен под­держи­вать ука­зате­ли и адресную ариф­метику.


Вто­рая проб­лема — потеря про­изво­дитель­нос­ти и пред­ска­зуемос­ти вре­мени выпол­нения. Клас­сичес­кие одно­поточ­ные сбор­щики мусора соз­дают паузы в выпол­нении прог­раммы, которые могут быть замет­ны поль­зовате­лю. При исполь­зовании мно­гопо­точ­ных алго­рит­мов и вер­ной нас­трой­ке тай­меров под задачи кон­крет­ного при­ложе­ния мож­но свес­ти паузы к миниму­му, но свес­ти зат­раты ресур­сов на сбор­ку мусора к нулю невоз­можно.


Впол­не логич­но, что раз­работ­чики язы­ков ищут аль­тер­натив­ные и про­межу­точ­ные вари­анты. Давай пос­мотрим, какими спо­соба­ми раз­ные язы­ки пыта­ются обес­печить безопас­ность памяти.


Строгая типизация указателей


Проб­лемы с безопас­ностью памяти в C воз­ника­ют в пер­вую оче­редь из-за отсутс­твия стро­гой типиза­ции. Фун­кция malloc() воз­вра­щает нетипи­зиро­ван­ный ука­затель (void*), который поль­зователь может при­вес­ти к любому типу. Сле­дить за сов­падени­ем раз­мера бло­ка памяти с раз­мером дан­ных тоже обя­зан­ность поль­зовате­ля. К при­меру, пор­тирова­ние ста­рого кода на 64-бит­ные плат­формы может при­нес­ти мно­го веселых минут, если его авто­ры жес­тко про­писа­ли раз­мер ука­зате­ля 32 бит.


Ну и самая клас­сичес­кая ошиб­ка, конеч­но, — слу­чай­ное обра­щение к нулево­му ука­зате­лю.


#include <stdio.h>
void main(void) {
char* str = NULL;
printf("%sn", str);
}


$ gcc -o segfault ./segfault.c
$ ./segfault
Segmentation fault (core dumped)



Бо­лее сов­ремен­ные язы­ки для сис­темно­го прог­рамми­рова­ния отно­сят­ся к это­му воп­росу более ответс­твен­но.


Нап­ример, в аде нетипи­зиро­ван­ные ука­зате­ли — осо­бый и ред­кий слу­чай. Обыч­ные ука­зате­ли всег­да типизи­рован­ные. Вмес­то malloc() при­меня­ется опе­ратор new с явным ука­зани­ем типа. Прос­того спо­соба осво­бодить память «вооб­ще» там тоже нет, вмес­то это­го есть обоб­щенная фун­кция (дже­нерик) Ada.Unchecked_Deallocation, которую перед исполь­зовани­ем нуж­но спе­циали­зиро­вать под кон­крет­ный тип дан­ных.


Та­ким спо­собом зап­росить или осво­бодить невер­ный объ­ем памяти гораз­до слож­нее. Исполь­зование пос­ле осво­бож­дения, впро­чем, так­же будет обна­руже­но толь­ко во вре­мя выпол­нения прог­раммы.



info


Ука­зате­ли в аде называ­ются access types. Нап­ример, access integer — ука­затель на целое чис­ло.



Для демонс­тра­ции сох­раним сле­дующий код в файл access_example.adb (имя фай­ла дол­жно сов­падать с наз­вани­ем основной про­цеду­ры).


with Ada.Unchecked_Deallocation;
procedure Access_Example is
type Int_Ptr is access Integer;
-- Специализация дженерика под Int_Ptr
procedure Free_Integer is new Ada.Unchecked_Deallocation
(Object => Integer, Name => Int_Ptr);
P : Int_Ptr;
I : Integer;
begin
-- Запрашиваем память под целое число с помощью new
-- и сохраняем туда значение 42
P := new Integer'(42);
-- Освобождаем память, теперь P = null
Free_Integer(P);
-- Пробуем получить значение по указателю
I := P.all;
end Access_Example;

Те­перь ском­пилиру­ем с помощью GNAT и запус­тим.



$ gnatmake ./access_example.adb
gcc -c -I./ -I- ./access_example.adb
gnatbind -x access_example.ali
gnatlink access_example.ali


$ ./access_example
raised CONSTRAINT_ERROR : access_example.adb:17 access check failed



Как видим, тип ука­зате­ля access Integer не защитил нас от обра­щения к осво­бож­денной памяти. Одно хорошо: хотя бы исклю­чение, а не segmentation fault, как в C, так что наша проб­лема прос­то баг, а не потен­циаль­ная уяз­вимость.


Од­нако начиная с Ada 2005 под­держи­вает­ся и про­вер­ка, что ука­затель ненуле­вой. Для это­го нуж­но испра­вить type Int_Ptr is access Integer на type Int_Ptr is not null access Integer. В этом слу­чае наша прог­рамма перес­танет ком­пилиро­вать­ся.



$ gnatmake ./access_example.adb
gcc -c -I./ -I- ./access_example.adb
access_example.adb:8:35: non null exclusion of actual and formal "Name" do not match
access_example.adb:10:04: warning: (Ada 2005) null-excluding objects must be initialized
access_example.adb:10:04: warning: "Constraint_Error" will be raised at run time
access_example.adb:15:04: warning: freeing "not null" object will raise Constraint_Error
gnatmake: "./access_example.adb" compilation error



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


type Int_Ptr is access Integer;
subtype Initialized_Int_Ptr is not null Int_Ptr;
procedure Some_Proc(Arg: Initialized_Int_Ptr);


Перейти обратно к новости