Есть ли какое-то правило, которому мы не смогли следовать?
Да, вы не смогли проверить это правильно. Ты не один, и ты в правильном месте, чтобы учиться :)
C ++ имеет много неопределенного поведения, неопределенное поведение проявляется тонкими и раздражающими способами.
Вы, вероятно, никогда не сможете написать 100% безопасный код C ++, но вы, безусловно, можете уменьшить вероятность случайного введения Undefined Behavior в вашу кодовую базу, используя ряд инструментов.
- Предупреждения компилятора
- Статический анализ (расширенная версия предупреждений)
- Инструментальные тестовые двоичные файлы
- Закаленные производственные двоичные файлы
В вашем случае я сомневаюсь, что (1) и (2) очень помогли бы, хотя в целом я советую использовать их. А пока давайте сосредоточимся на двух других.
И gcc, и Clang имеют -fsanitize
флаг, который используется инструментами, которые вы компилируете, для проверки множества проблем. -fsanitize=undefined
например, будет перехватывать целое число переполнения / переполнения, сдвига на слишком большое количество и т. д. ... В вашем конкретном случае, -fsanitize=address
и -fsanitize=memory
, скорее всего , возникнет проблема ... при условии, что у вас есть тест, вызывающий функцию. Для полноты, -fsanitize=thread
стоит использовать, если у вас есть многопоточная кодовая база. Если вы не можете реализовать двоичный файл (например, у вас есть сторонние библиотеки без их источника), то вы также можете использовать valgrind
его, хотя в целом он медленнее.
Последние компиляторы также показывают возможности повышения благосостояния . Основное отличие от инструментальных бинарных файлов заключается в том, что проверки на закалку предназначены для того, чтобы оказывать низкое влияние на производительность (<1%), что делает их пригодными для производственного кода в целом. Самыми известными являются проверки CFI (целостность потока управления), которые предназначены для предотвращения атак с разбивкой стека и хай-джеккинга виртуальных указателей, в том числе для подрыва потока управления.
Цель обоих (3) и (4) - преобразовать прерывистый отказ в определенный отказ : оба они следуют принципу быстрого отказа . Это значит, что:
- это всегда терпит неудачу, когда вы наступаете на мины
- он немедленно завершается неудачно , указывая на ошибку, а не на случайное повреждение памяти и т. д.
Объединение (3) с хорошим тестовым покрытием должно выявить большинство проблем, прежде чем они попадут в производство. Использование (4) в производстве может быть различием между надоедливой ошибкой и эксплойтом.