Українська
Діагностичні досліди
Дослід із буфером: до і після звільнення
Ручний динамічний масив має щонайменше три окремі характеристики:адресу буфера, число сконструйованих елементів і місткість. У простому new int[n] ці числа часто збігаються, але у вектора size може бути меншим за capacity. Не можна переносити правило «пам’ять виділена» на твердження «об’єкт на кожній позиції вже існує і його можна читати».
Після delete[] змінна p може зберігати те саме числове подання адреси. У вікні Memory навіть можуть бути старі значення. Проте право доступу до цих об’єктів уже втрачено. Наступне виділення здатне повторно використати ділянку, і старий запис зіпсує вже інший об’єкт. Саме тому тест «прочитав старе число без аварії» не доводить безпеки.
Таблиця 5.2. Копія адреси не є копією ресурсу
| Операція | Стан після неї |
|---|---|
p = new int[3]{} | p володіє трьома нульовими int; допустимі індекси 0–2. |
q = p | Дві адресні змінні, але модель має визначити одного власника. |
delete[] p | Об’єкти знищені; і p, і q більше не дають доступу до них. |
p = nullptr | p порожній;q усе ще містить стару адресу. |
delete[] q | Повторне звільнення; неприпустима дія. |
Для рядків C додається ще одна умова:існування завершального нуля в доступному буфері. Якщо скопіювати рівно п’ять літер Hello у п’ять комірок, це ще не нуль-термінований рядок. Друк через API рядків може читати далі, шукаючи нуль у чужій пам’яті. Тому місткість потрібно враховувати разом із символом завершення.
Як виконувати діагностичний експеримент
Навмисно неправильний код потрібно відокремити від робочого прикладу. Збережіть копію, позначте очікуваний тип дефекту й запускайте її з діагностичними параметрами. Один експеримент має містити один дефект: вихід за межі, доступ після звільнення або неправильну пару виділення і звільнення. Якщо поєднати всі три, програма може завершитися на першому і не досягти інших.
Для heap-buffer-overflow потрібно показати розмір виділення та індекс помилкового доступу. Для heap-use-after-free – місце виділення, звільнення і наступного читання. Для невідповідності new[] та delete – обидві операції та тип ресурсу. Збережіть текст звіту або справжній знімок, але не підмінюйте його очікуваним повідомленням.
Після виправлення повторіть той самий сценарій, потім звичайний і граничний. Якщо «виправлення» лише прибрало проблемний рядок із потрібною функціональністю, задача ще не виконана. Безпечний варіант повинен зберігати правильний результат і мати зрозуміле звільнення ресурсів.
Leak-звіт і ASan-звіт не взаємозамінні. Не припускайте, що кожна реалізація AddressSanitizer автоматично знаходить усі витоки. У MSVC для навчального демонстрування витоку використовуйте описаний CRT Debug Heap і перевіряйте момент отримання звіту. Об’єкт, який ще має чинного власника, не обов’язково є витоком.
Вилучення вузла без втрати ланцюга
У списку head→A→B→C потрібно видалити A. Якщо просто reset голови, власник A знищить також B, а той C. Це правильно для очищення всього списку, але не для вилучення одного елемента. Спочатку потрібно передати володіння рештою ланцюга новій голові.
Безпечна послідовність у прикладі використовує окремий removed. Після першого move removed володіє A, head порожній. Після другого head володіє B, а next вузла A порожній. Наприкінці блока знищується лише A. Така проміжна змінна робить порядок життя очевидним.
Для очищення дуже довгого списку повторіть цей крок у циклі, поки head не стане порожнім. Кожна ітерація знищує один вузол без рекурсивного руйнування всього хвоста. Для дерева аналогічна задача складніша: потрібно явно зберігати ще не оброблені піддерева.
Перевірка передачі ресурсу між власниками
Уявімо інвентар двох гравців. Предмет належить рівно одному з них, тому його зберігають через unique_ptr. Операція передачі повинна мати визначену поведінку, коли місце одержувача вже зайняте. Якщо спочатку перемістити джерело, а потім перевірити ліміт, можна втратити початковий стан. Спочатку перевіряють можливість операції, потім виконують передачу.
Після успіху джерело має бути порожнім, одержувач має містити той самий предмет, а невласницький ідентифікатор предмета не повинен змінитися. Після відмови обидва власники залишаються у початковому стані. Ці властивості можна перевірити без знання конкретної адреси в пам’яті.
Якщо колекція одержувача є вектором, додавання може потребувати виділення пам’яті. Правильність при невдалому виділенні є окремим питанням безпеки винятків, яке розглядатимемо в наступній темі. Розумний вказівник захищає ресурс від витоку, але узгодженість кількох колекцій потребує продуманого порядку операцій.
Для спільного ресурсу сценарій інший: додавання до другого плейлиста не повинне прибирати пісню з першого. Тому копіюється shared_ptr, а не переміщується виключний власник. Після очищення одного списку пісня продовжує існувати, поки є другий власник. Це не «повільніший unique_ptr», а інша модель життя.
Спостереження через weak_ptr також потребує тестів. Перевірте lock поки власник існує, після знищення одного з кількох власників та після знищення останнього. Лише останній випадок повинен дати порожній результат. Не використовуйте use_count як заміну цьому інтерфейсу.
У звіті про пам’ять відділяйте виміряні факти від припущень. «ASan не повідомив про дефект на таких-то тестах» є перевіреним твердженням. «Програма ніколи не має помилок пам’яті» з одного запуску не випливає. Поєднуйте тести, читання коду та схему володіння.