[C, C++, D, Отладка] Баги, которые разрушили ваш замок (перевод)
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Уолтер Брайт — «великодушный пожизненный диктатор» языка программирования D и основатель Digital Mars. За его плечами не один десяток лет опыта в разработке компиляторов и интерпретаторов для нескольких языков, в числе которых Zortech C++ — первый нативный компилятор C++. Он также создатель игры Empire, послужившей основным источником вдохновения для Sid Meier’s Civilization. Данная публикация — первая в серии статей о режиме Better C в языке D.
Цикл статей о Better C
SPL
- D как улучшенный C
- Баги, которые разрушили ваш замок
- Портируем make.c на D
Вы устали от багов, которые легко сделать и трудно найти, которые часто не всплывают во время тестирования и уничтожают так тщательно построенный вами замок после того, как код ушёл в производство? Снова и снова они стоят вам много времени и денег. Ах, если бы только вы были по-настоящему хорошим программистом, то этого бы не происходило, верно?
А может, дело не в вас? Я покажу вам, что эти ошибки не ваша вина: это вина инструментов, и если только улучшить инструменты, то ваш замок будет в безопасности.
И вам даже не придётся идти ни на какие компромисы.
Выход за границы массива
Возьмём обычную программу для подсчёта суммы значений массива:
#include <stdio.h>
#define MAX 10
int sumArray(int* p) {
int sum = 0;
int i;
for (i = 0; i <= MAX; ++i)
sum += p[i];
return sum;
}
int main() {
static int values[MAX] = { 7,10,58,62,93,100,8,17,77,17 };
printf("sum = %d\n", sumArray(values));
return 0;
}
Программа должна напечатать:
sum = 449
И именно это она и делает — на моей машине с Ubuntu Linux: и в gcc, clang, и даже с флагом -Wall. Уверен, вы уже догадались, в чём ошибка:
for (i = 0; i <= MAX; ++i)
^^
Это классическая ошибка на единицу. Цикл выполняется 11 раз вместо 10. Должно быть так:
for (i = 0; i < MAX; ++i)
Обратите внимание, что несмотря на баг, программа всё равно вывела правильный результат! Во всяком случае, на моей машине. И я бы не обнаружил этой ошибки. А вот на машине пользователя она бы загадочно всплыла, и я бы столкнулся с «багом Гейзенберга» на удалённой машине. Я уже съёживаюсь в предвкушении того, сколько времени и денег мне это будет стоить.
Это настолько мерзкий баг, что за годы я перепрограммировал свой мозг на то, чтобы:
- никогда-никогда не использовать промежутки, включающие верхнюю границу;
- никогда-никогда не использовать <= в проверке цикла.
Став лучшим программистом, я решил проблему! Или нет? На самом деле нет. Давайте посмотрим на этот код с точки зрения бедняги, которому придётся его проверять. Он хочет убедиться, что sumArray работает корректно. Для этого он должен:
- Найти все функции, вызывающие sumArray, и проверить, что за указатель они передают.
- Убедиться, что указатель действительно указывает на массив.
- Убедиться, что размер массива действительно MAX.
И хотя в этой тривиальной программе это сделать просто, такой подход плохо масштабируется с возрастанием сложности программы. Чем больше функций обращаются к sumArray и чем более сложными становятся структуры данных, тем труднее делать, по сути, целый анализ потока данных у себя в уме, чтобы убедиться, что всё работает правильно.
Даже если вы не ошибётесь — насколько вы можете быть уверены, что всё будет нормально? Если кто-то другой внесёт изменения, то ошибок всё ещё не будет? Хотите заново делать весь этот анализ? Уверен, вам и так есть чем заняться. Это дело инструментов.
Фундаментальная проблема состоит в том, что массивы в С преобразуются в указатели, когда используются как аргумент функции, даже если параметр функции определён как массив. Этой проблемы никак не избежать. И её никак не обнаружить. (По крайней мере, ни gcc, ни clang не умеют обнаруживать эту проблему, но может, кто-то разработал анализатор, который умеет это делать).
Инструмент, который решает эту проблему — это компилятор D с опцией -betterC. В D есть такое понятие, как динамический массив, который на самом деле просто толстый указатель, который определён примерно вот так:
struct DynamicArray {
T* ptr;
size_t length;
}
Он объявляется вот так:
int[] a;
И наш пример становится таким:
import core.stdc.stdio;
extern (C): // use C ABI for declarations
enum MAX = 10;
int sumArray(int[] a) {
int sum = 0;
for (int i = 0; i <= MAX; ++i)
sum += a[i];
return sum;
}
int main() {
__gshared int[MAX] values = [ 7,10,58,62,93,100,8,17,77,17 ];
printf("sum = %d\n", sumArray(values));
return 0;
}
Компилируем:
dmd -betterC sum.d
Запускаем:
./sum
Assertion failure: 'array overflow' on line 11 in file 'sum.d'
Так-то лучше. Заменяем <= на < и получаем:
./sum
sum = 449
Что здесь произошло? В динамическом массиве a хранится его длина, и компилятор вставляет проверки выхода за границы массива.
Но подождите, это ещё не всё.
Ещё там есть вот это вот досадное MAX. Поскольку массив a знает свою длину, то вместо этого можно написать так:
for (int i = 0; i < a.length; ++i)
Это настолько частая идиома, что в D для неё имеется специальный синтаксис:
foreach (value; a)
sum += value;
Теперь вся функция sumArray выглядит вот так:
int sumArray(int[] a) {
int sum = 0;
foreach (value; a)
sum += value;
return sum;
}
и теперь sumArray можно рассматривать отдельно от всей остальной программы. Вы сделали большее за меньшее время и с большей надёжностью, и теперь можете рассчитывать на повышение зарплаты. Или по крайне мере вам не придётся приезжать на срочный вызов в свой выходной, чтобы исправить ошибку.
— Протестую! — скажете вы. — Передача массива a в sumArray требует двух проталкиваний в стек, тогда как указатель p требовал только одного. Ты обещал, что не придётся идти на компромиссы, но здесь я жертвую скоростью.
Это правда — в случае, если MAX является константой, а не передаётся в функцию, как здесь:
int sumArray(int *p, size_t length);
Но я же обещал, что не придётся идти ни на какие компромиссы. D позволяет предавать параметры по ссылке, в том числе и массивы фиксированной длины.
int sumArray(ref int[MAX] a) {
int sum = 0;
foreach (value; a)
sum += value;
return sum;
}
Что здесь происходит? Массив a, будучи параметром с аттрибутом ref, во время выполнения становится всего лишь указателем. Однако он типизирован как указатель на массив из MAX элементов, что позволяет при обращении к нему делать проверки границ. Вам не придётся проверять функции, вызывающие sumArray, потому что система типов гарантируют, что они могут передавать только массивы правильного размера.
— Протестую! — скажете вы. — D поддерживает указатели. Разве я не могу написать так же, как было? Что меня остановит? Я думал, что речь идёт о механических гарантиях!
Да, вы можете написать вот так:
import core.stdc.stdio;
extern (C): // use C ABI for declarations
enum MAX = 10;
int sumArray(int* p) {
int sum = 0;
for (int i = 0; i <= MAX; ++i)
sum += p[i];
return sum;
}
int main() {
__gshared int[MAX] values = [ 7,10,58,62,93,100,8,17,77,17 ];
printf("sum = %d\n", sumArray(&values[0]));
return 0;
}
И компилятор проглотит это без нареканий, и этот ужасный баг там останется. Правда, на этот раз мне выдало:
sum = 39479
что выглядит подозрительно, но с таким же успехом могло выйти 449, и тогда я был бы ни слухом ни духом.
Как можно гарантировать, что этого не произойдёт? Добавить в код аттрибут @safe:
import core.stdc.stdio;
extern (C): // use C ABI for declarations
enum MAX = 10;
@safe int sumArray(int* p) {
int sum = 0;
for (int i = 0; i <= MAX; ++i)
sum += p[i];
return sum;
}
int main() {
__gshared int[MAX] values = [ 7,10,58,62,93,100,8,17,77,17 ];
printf("sum = %d\n", sumArray(&values[0]));
return 0;
}
Попытка скомпилировать выдаст следующее:
sum.d(10): Error: safe function 'sum.sumArray' cannot index pointer 'p'
Конечно, во время код-ревью надо будет будет сделать grep, чтобы удостовериться, что используется @safe, но на этом всё.
В общем и целом, этот баг можно победить, не дав массиву преобразоваться в указатель при передаче в функцию в качестве аргумента, и уничтожить насовсем, запретив небезопасные операции над указателями. Я уверен, что мало кто из вас никогда не сталкивался с ошибками переполнения буфера. Ждите следующей части этого цикла. Может быть в следующий раз мы разберёмся с багом, который преодолел ваш ров! (Если в вашем инструментарии вообще есть ров).
===========
Источник:
habr.com
===========
===========
Автор оригинала: Уолтер Брайт (Walter Bright)
===========Похожие новости:
- [IT-компании, Системное администрирование, Софт] Microsoft признала, что в Windows 10 версии 2004 системный статус сетевого соединения может глючить. Это можно исправить
- [Data Engineering, Data Mining, Matlab, Алгоритмы, Математика] Симуляционное моделирование механической системы средствами визуального программирования Scilab\Xcos
- [Open source, Rust, Компиляторы, Программирование, Системное программирование] Rust 1.45.0: стабилизация функциональных процедурных макросов, исправление дефектов преобразования (перевод)
- [PHP] Как я писал кодогенератор на PHP и что из этого получилось
- [Информационная безопасность, CTF] HackTheBox. Прохождение Sauna. LDAP, AS-REP Roasting, AutoLogon, DCSync атака
- [Финансы в IT] Истории с Уолл-стрит: как компания с нулевой выручкой может оцениваться в $34 млрд, а ее акции показывать взрывной рост
- [DIY или Сделай сам, Компьютерное железо] FPV Квадрокоптер: Фильтрация в Betaflight
- [C#, F#, Программирование] Асинхронность в C# и F#. Подводные камни асинхронности в C #
- Инцидент в Twitter, ставший причиной компрометации 130 популярных Twitter-аккаунтов
- [JavaScript, PHP, Ненормальное программирование, Программирование, Разработка веб-сайтов] Inertia.js – современный монолит
Теги для поиска: #_c, #_c++, #_d, #_otladka (Отладка), #_dlang, #_betterc, #_c, #_c++, #_d, #_otladka (
Отладка
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 14:15
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Уолтер Брайт — «великодушный пожизненный диктатор» языка программирования D и основатель Digital Mars. За его плечами не один десяток лет опыта в разработке компиляторов и интерпретаторов для нескольких языков, в числе которых Zortech C++ — первый нативный компилятор C++. Он также создатель игры Empire, послужившей основным источником вдохновения для Sid Meier’s Civilization. Данная публикация — первая в серии статей о режиме Better C в языке D. Цикл статей о Better CSPL
Вы устали от багов, которые легко сделать и трудно найти, которые часто не всплывают во время тестирования и уничтожают так тщательно построенный вами замок после того, как код ушёл в производство? Снова и снова они стоят вам много времени и денег. Ах, если бы только вы были по-настоящему хорошим программистом, то этого бы не происходило, верно? А может, дело не в вас? Я покажу вам, что эти ошибки не ваша вина: это вина инструментов, и если только улучшить инструменты, то ваш замок будет в безопасности. И вам даже не придётся идти ни на какие компромисы. Выход за границы массива Возьмём обычную программу для подсчёта суммы значений массива: #include <stdio.h>
#define MAX 10 int sumArray(int* p) { int sum = 0; int i; for (i = 0; i <= MAX; ++i) sum += p[i]; return sum; } int main() { static int values[MAX] = { 7,10,58,62,93,100,8,17,77,17 }; printf("sum = %d\n", sumArray(values)); return 0; } Программа должна напечатать: sum = 449
И именно это она и делает — на моей машине с Ubuntu Linux: и в gcc, clang, и даже с флагом -Wall. Уверен, вы уже догадались, в чём ошибка: for (i = 0; i <= MAX; ++i)
^^ Это классическая ошибка на единицу. Цикл выполняется 11 раз вместо 10. Должно быть так: for (i = 0; i < MAX; ++i)
Обратите внимание, что несмотря на баг, программа всё равно вывела правильный результат! Во всяком случае, на моей машине. И я бы не обнаружил этой ошибки. А вот на машине пользователя она бы загадочно всплыла, и я бы столкнулся с «багом Гейзенберга» на удалённой машине. Я уже съёживаюсь в предвкушении того, сколько времени и денег мне это будет стоить. Это настолько мерзкий баг, что за годы я перепрограммировал свой мозг на то, чтобы:
Став лучшим программистом, я решил проблему! Или нет? На самом деле нет. Давайте посмотрим на этот код с точки зрения бедняги, которому придётся его проверять. Он хочет убедиться, что sumArray работает корректно. Для этого он должен:
И хотя в этой тривиальной программе это сделать просто, такой подход плохо масштабируется с возрастанием сложности программы. Чем больше функций обращаются к sumArray и чем более сложными становятся структуры данных, тем труднее делать, по сути, целый анализ потока данных у себя в уме, чтобы убедиться, что всё работает правильно. Даже если вы не ошибётесь — насколько вы можете быть уверены, что всё будет нормально? Если кто-то другой внесёт изменения, то ошибок всё ещё не будет? Хотите заново делать весь этот анализ? Уверен, вам и так есть чем заняться. Это дело инструментов. Фундаментальная проблема состоит в том, что массивы в С преобразуются в указатели, когда используются как аргумент функции, даже если параметр функции определён как массив. Этой проблемы никак не избежать. И её никак не обнаружить. (По крайней мере, ни gcc, ни clang не умеют обнаруживать эту проблему, но может, кто-то разработал анализатор, который умеет это делать). Инструмент, который решает эту проблему — это компилятор D с опцией -betterC. В D есть такое понятие, как динамический массив, который на самом деле просто толстый указатель, который определён примерно вот так: struct DynamicArray {
T* ptr; size_t length; } Он объявляется вот так: int[] a;
И наш пример становится таким: import core.stdc.stdio;
extern (C): // use C ABI for declarations enum MAX = 10; int sumArray(int[] a) { int sum = 0; for (int i = 0; i <= MAX; ++i) sum += a[i]; return sum; } int main() { __gshared int[MAX] values = [ 7,10,58,62,93,100,8,17,77,17 ]; printf("sum = %d\n", sumArray(values)); return 0; } Компилируем: dmd -betterC sum.d
Запускаем: ./sum
Assertion failure: 'array overflow' on line 11 in file 'sum.d' Так-то лучше. Заменяем <= на < и получаем: ./sum
sum = 449 Что здесь произошло? В динамическом массиве a хранится его длина, и компилятор вставляет проверки выхода за границы массива. Но подождите, это ещё не всё. Ещё там есть вот это вот досадное MAX. Поскольку массив a знает свою длину, то вместо этого можно написать так: for (int i = 0; i < a.length; ++i)
Это настолько частая идиома, что в D для неё имеется специальный синтаксис: foreach (value; a)
sum += value; Теперь вся функция sumArray выглядит вот так: int sumArray(int[] a) {
int sum = 0; foreach (value; a) sum += value; return sum; } и теперь sumArray можно рассматривать отдельно от всей остальной программы. Вы сделали большее за меньшее время и с большей надёжностью, и теперь можете рассчитывать на повышение зарплаты. Или по крайне мере вам не придётся приезжать на срочный вызов в свой выходной, чтобы исправить ошибку. — Протестую! — скажете вы. — Передача массива a в sumArray требует двух проталкиваний в стек, тогда как указатель p требовал только одного. Ты обещал, что не придётся идти на компромиссы, но здесь я жертвую скоростью. Это правда — в случае, если MAX является константой, а не передаётся в функцию, как здесь: int sumArray(int *p, size_t length);
Но я же обещал, что не придётся идти ни на какие компромиссы. D позволяет предавать параметры по ссылке, в том числе и массивы фиксированной длины. int sumArray(ref int[MAX] a) {
int sum = 0; foreach (value; a) sum += value; return sum; } Что здесь происходит? Массив a, будучи параметром с аттрибутом ref, во время выполнения становится всего лишь указателем. Однако он типизирован как указатель на массив из MAX элементов, что позволяет при обращении к нему делать проверки границ. Вам не придётся проверять функции, вызывающие sumArray, потому что система типов гарантируют, что они могут передавать только массивы правильного размера. — Протестую! — скажете вы. — D поддерживает указатели. Разве я не могу написать так же, как было? Что меня остановит? Я думал, что речь идёт о механических гарантиях! Да, вы можете написать вот так: import core.stdc.stdio;
extern (C): // use C ABI for declarations enum MAX = 10; int sumArray(int* p) { int sum = 0; for (int i = 0; i <= MAX; ++i) sum += p[i]; return sum; } int main() { __gshared int[MAX] values = [ 7,10,58,62,93,100,8,17,77,17 ]; printf("sum = %d\n", sumArray(&values[0])); return 0; } И компилятор проглотит это без нареканий, и этот ужасный баг там останется. Правда, на этот раз мне выдало: sum = 39479
что выглядит подозрительно, но с таким же успехом могло выйти 449, и тогда я был бы ни слухом ни духом. Как можно гарантировать, что этого не произойдёт? Добавить в код аттрибут @safe: import core.stdc.stdio;
extern (C): // use C ABI for declarations enum MAX = 10; @safe int sumArray(int* p) { int sum = 0; for (int i = 0; i <= MAX; ++i) sum += p[i]; return sum; } int main() { __gshared int[MAX] values = [ 7,10,58,62,93,100,8,17,77,17 ]; printf("sum = %d\n", sumArray(&values[0])); return 0; } Попытка скомпилировать выдаст следующее: sum.d(10): Error: safe function 'sum.sumArray' cannot index pointer 'p'
Конечно, во время код-ревью надо будет будет сделать grep, чтобы удостовериться, что используется @safe, но на этом всё. В общем и целом, этот баг можно победить, не дав массиву преобразоваться в указатель при передаче в функцию в качестве аргумента, и уничтожить насовсем, запретив небезопасные операции над указателями. Я уверен, что мало кто из вас никогда не сталкивался с ошибками переполнения буфера. Ждите следующей части этого цикла. Может быть в следующий раз мы разберёмся с багом, который преодолел ваш ров! (Если в вашем инструментарии вообще есть ров). =========== Источник: habr.com =========== =========== Автор оригинала: Уолтер Брайт (Walter Bright) ===========Похожие новости:
Отладка ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 14:15
Часовой пояс: UTC + 5