Русский
Русский
English
Статистика
Реклама

Перевод Баги, которые разрушили ваш замок

Уолтер Брайт великодушный пожизненный диктатор языка программирования D и основатель Digital Mars. За его плечами не один десяток лет опыта в разработке компиляторов и интерпретаторов для нескольких языков, в числе которых Zortech C++ первый нативный компилятор C++. Он также создатель игры Empire, послужившей основным источником вдохновения для Sid Meiers Civilization. Данная публикация первая в серии статей о режиме Better C в языке D.


Цикл статей о Better C
  1. D как улучшенный C
  2. Баги, которые разрушили ваш замок
  3. Портируем make.c на D

Вы устали от багов, которые легко сделать и трудно найти, которые часто не всплывают во время тестирования и уничтожают так тщательно построенный вами замок после того, как код ушёл в производство? Снова и снова они стоят вам много времени и денег. Ах, если бы только вы были по-настоящему хорошим программистом, то этого бы не происходило, верно?


А может, дело не в вас? Я покажу вам, что эти ошибки не ваша вина: это вина инструментов, и если только улучшить инструменты, то ваш замок будет в безопасности.


И вам даже не придётся идти ни на какие компромисы.


Выход за границы массива


Возьмём обычную программу для подсчёта суммы значений массива:


#include <stdio.h>#define MAX 10int 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)

Обратите внимание, что несмотря на баг, программа всё равно вывела правильный результат! Во всяком случае, на моей машине. И я бы не обнаружил этой ошибки. А вот на машине пользователя она бы загадочно всплыла, и я бы столкнулся с багом Гейзенберга на удалённой машине. Я уже съёживаюсь в предвкушении того, сколько времени и денег мне это будет стоить.


Это настолько мерзкий баг, что за годы я перепрограммировал свой мозг на то, чтобы:


  1. никогда-никогда не использовать промежутки, включающие верхнюю границу;
  2. никогда-никогда не использовать <= в проверке цикла.

Став лучшим программистом, я решил проблему! Или нет? На самом деле нет. Давайте посмотрим на этот код с точки зрения бедняги, которому придётся его проверять. Он хочет убедиться, что sumArray работает корректно. Для этого он должен:


  1. Найти все функции, вызывающие sumArray, и проверить, что за указатель они передают.
  2. Убедиться, что указатель действительно указывает на массив.
  3. Убедиться, что размер массива действительно 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 declarationsenum 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

Запускаем:


./sumAssertion failure: 'array overflow' on line 11 in file 'sum.d'

Так-то лучше. Заменяем <= на < и получаем:


./sumsum = 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 declarationsenum 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 declarationsenum 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
К списку статей
Опубликовано: 19.07.2020 00:15:31
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

C

C++

D

Отладка

Dlang

Betterc

Категории

Последние комментарии

© 2006-2021, personeltest.ru