Следующая новость
Предыдущая новость

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

11.11.2020 13:22

Содержание статьи

  • Строгая типизация указателей
  • Умные указатели
  • Линейные типы (borrow checker)
  • Заключение

Ос­новные проб­лемы с управле­нием памятью мож­но раз­делить на два клас­са. Пер­вый — утеч­ки памяти. Утеч­ка воз­ника­ет, если прог­рамма зап­рашива­ет у ядра ОС блок памяти и забыва­ет ее осво­бодить. Проб­лема с утеч­ками памяти неп­рият­ная, но отно­ситель­но безопас­ная — в край­нем слу­чае ее мож­но «решить» пери­оди­чес­ким переза­пус­ком прог­раммы. Вто­рой класс — проб­лемы безопас­ности памяти, которые мы и обсу­дим в этой статье.

К проб­лемам безопас­ности отно­сят­ся:

  • об­ращение к невер­ному ука­зате­лю (в том чис­ле перепол­нение буфера и разыме­нова­ние NULL);
  • оши­боч­ное обра­щение к уже осво­бож­денной памяти (use after free);
  • оши­боч­ная попыт­ка осво­бодить ранее осво­бож­денный ука­затель (double free).

info

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

Как мы видели на при­мере с исполь­зовани­ем 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);

Продолжение доступно только участникам

Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».

Присоединяйся к сообществу «Xakep.ru»!

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

1 год

7690 р.

1 месяц

720 р.

Я уже участник «Xakep.ru»

Источник

Последние новости