Як зрозуміти пам’ять вашої програми

Під час кодування такою мовою, як C або C ++, ви можете взаємодіяти з пам'яттю більш низьким рівнем. Іноді це створює багато проблем, яких раніше не виникало: сегментації . Ці помилки досить дратують і можуть доставити вам багато клопоту. Вони часто є показниками того, що ви використовуєте пам’ять, яку не слід використовувати.

Однією з найпоширеніших проблем є доступ до вже звільненої пам'яті. Це пам’ять, з якою ви або звільнили free, або пам’ять, яку програма автоматично звільнила, наприклад зі стеку.

Розуміти все це насправді просто, і це, безумовно, змусить вас програмувати краще та розумніше.

Як поділяється пам’ять?

Пам'ять розділена на кілька сегментів. Два найважливіші з них - це стек і купа . Стек - це впорядковане місце вставки, тоді як купа вся випадкова - ви розподіляєте пам’ять, де тільки можете.

Пам'ять стека має безліч способів і операцій для своєї роботи. Тут зберігається частина інформації про регістри вашого процесора. І саме туди надходить відповідна інформація про вашу програму - які функції викликаються, які змінні ви створили та ще деяку інформацію. Ця пам’ять також управляється програмою, а не розробником.

Купа часто використовується для виділення великої кількості пам'яті, яка повинна існувати стільки, скільки хоче розробник. Тим не менш, робота розробника - контролювати використання пам’яті в купі . Створюючи складні програми, вам часто потрібно виділяти великі фрагменти пам'яті, і саме тут ви використовуєте купу. Ми називаємо це динамічною пам'яттю .

Ви розміщуєте речі в купі кожного разу, коли використовуєте mallocдля виділення пам’яті. Будь-який інший виклик, який int i;виконується, є стековою пам'яттю. Знання цього насправді важливо, щоб ви могли легко знаходити помилки у своїй програмі та ще більше вдосконалювати пошук помилок Segfault.

Розуміння стека

Хоча ви можете про це не знати, ваша програма постійно виділяє стекову пам’ять для її роботи. Кожна локальна змінна та кожна викликана функція переходять туди. Завдяки цьому ви можете зробити багато речей - більшість з них - це речі, про які ви не хотіли, щоб це сталося - наприклад, переповнення буфера та доступ до неправильної пам'яті.

То як це насправді працює?

Стек - це структура даних LIFO (Last-In-First-Out). Ви можете розглядати це як коробку з ідеально підігнаними книгами - остання книга, яку ви розміщуєте, є першою, яку ви дістаєте. Використовуючи цю структуру, програма може легко керувати всіма своїми операціями та сферами дії за допомогою двох простих операцій: push та pop .

Ці двоє роблять абсолютно протилежне один одному. Натискання вставляє значення у верх стека. Поп бере з нього найвище значення.

Для відстеження поточного місця пам'яті існує спеціальний регістр процесора, який називається Stack Pointer . Кожного разу, коли вам потрібно щось зберегти - наприклад, змінну або адресу повернення від функції - вона штовхає і переміщує вказівник стека вгору. Кожного разу, коли ви виходите з функції, вона вискакує все з покажчика стека до збереженої адреси повернення із функції. Це просто!

Щоб перевірити, чи зрозуміли ви, скористаємось наступним прикладом (спробуйте знайти помилку самостійно ☺️):

Якщо ви його запустите, програма просто відключить сегментацію. Чому так трапляється? Все виглядає на своїх місцях! За винятком приблизно ... стека.

Коли ми викликаємо функцію createArray, стек:

  • зберігає зворотну адресу,
  • створює arrв пам’яті стека і повертає її (масив - це просто вказівник на місце пам’яті з його інформацією)
  • але оскільки ми не використовували, mallocвін зберігається в пам'яті стека.

Після того, як ми повертаємо вказівник, оскільки ми не маємо ніякого контролю над операціями стека, програма витягує інформацію зі стеку і використовує її за потребою. Коли ми намагаємося заповнити масив після повернення з функції, ми пошкоджуємо пам'ять, роблячи програму сегментальною.

Розуміння купи

На противагу стеку, купа - це те, чим ви користуєтесь, коли хочете, щоб щось існувало певний час незалежно від функцій та обсягу. Щоб використовувати цю пам’ять, мова stdlib на мові C справді хороша, оскільки вона має дві чудові функції: mallocі free.

Malloc (виділення пам’яті) запитує в системі обсяг пам’яті, який був запитаний, і повертає покажчик на початкову адресу. Безкоштовно повідомляє системі, що пам'ять, про яку ми просили, більше не потрібна, і її можна використовувати для інших завдань. Виглядає насправді просто - поки ви уникаєте помилок.

Система не може замінити те, що просили розробники. Тож від нас, людей, залежить управління цим двома функціями, наведеними вище. Це відкриває двері для однієї людської помилки: Витоки пам'яті.

Витік пам'яті - це пам’ять, яку користувач запитував і яка ніколи не була звільнена - коли програма закінчилася або вказівники на її розташування були втрачені. Це змушує програму використовувати набагато більше пам'яті, ніж те, що вона повинна була. Щоб уникнути цього, ми щоразу, коли нам більше не потрібен елемент, виділений купою, звільняємо його.

На малюнку вище, поганий спосіб ніколи не звільняє пам’ять, яку ми використовували. Це в кінцевому підсумку витрачає 20 * 4 байтів (розмір int у 64-бітах) = 80 байт. Це може виглядати не так вже й сильно, але уявіть, що не робитимете це у гігантській програмі. У підсумку ми можемо витратити гігабайти!

Управління своєю купою пам'яті має важливе значення для підвищення ефективності пам'яті програм. Але також потрібно бути обережним, як ви ним користуєтесь. Так само, як у пам’яті стека, після звільнення пам’яті доступ до неї або її використання може спричинити сегментацію.

Бонус: Структури та купа

Однією з найпоширеніших помилок при використанні структур є просто звільнення структури. Це добре, якщо ми не виділяли пам’ять для покажчиків всередині структури. Якщо пам’ять виділяється покажчикам всередині структури, спочатку їх потрібно звільнити. Тоді ми можемо звільнити всю структуру.

Як я вирішую проблеми з витоком пам’яті

Більшу частину часу, коли я програмую на мові C, я використовую структури. Тому у мене завжди є дві обов’язкові функції для використання зі своїми конструкціями : конструктор та деструктор .

Ці дві функції є єдиними, де я використовую mallocs і frees на структурі. Це робить дуже простим і простим вирішення мого витоку пам’яті.

(Якщо ви хочете дізнатись більше про полегшення читання коду, перегляньте мій пост про абстракцію).

Чудовий інструмент управління пам’яттю - Valgrind

Важко керувати своєю пам’яттю та переконатися, що ви все правильно обробили. Чудовим інструментом для перевірки правильності поведінки вашої програми є Valgrind. Цей інструмент перевіряє вашу програму, повідомляючи вам, скільки пам'яті ви виділили, скільки було звільнено, якщо ви намагалися писати в неправильній області пам'яті ... Використання - чудовий спосіб перевірити, чи все в порядку, і потрібно використовувати його, щоб уникнути компроміси з безпекою.

Не забудьте піти за мною!

Окрім публікації тут на Medium, я ще й у Twitter.

Якщо у вас є якісь запитання чи пропозиції, не соромтеся зв’язуватися зі мною.