Pull to refresh

Comments 44

великодушный пожизненный диктатор
Что ни говорите, а вдумчивость во взгляде и стать приходят только с возрастом, достаточно одного
взгляда
«великодушный пожизненный диктатор»

Уолтер БрайтГвидо ван РоссумЛинус Торвальдс
А можно подписать, кто есть кто?

Для повышения образованности.
Бенито Мусолини, Уолтер Брайт(создатель D), Гвидо ван Россум(создатель Python), Линус Торвльдс(создатель Linux).
Спс, а то лица все знакомые, но никак не признаю. Муссолини вот только не знаком был )
А sanitizer же ловит эту ошибку без проблем:

$ gcc -fsanitize=address -fsanitize=undefined test.c
$ ./a.out 
=================================================================
==2193597==ERROR: AddressSanitizer: global-buffer-overflow on address 0x556c25d33128 at pc 0x556c25d30279 bp 0x7ffd00ade590 sp 0x7ffd00ade580
READ of size 4 at 0x556c25d33128 thread T0
    #0 0x556c25d30278 in sumArray (/tmp/a.out+0x1278)
    #1 0x556c25d302e6 in main (/tmp/a.out+0x12e6)
    #2 0x7f64a6712cfa in __libc_start_main ../csu/libc-start.c:308
    #3 0x556c25d300f9 in _start (/tmp/a.out+0x10f9)

0x556c25d33128 is located 0 bytes to the right of global variable 'values' defined in 'test.c:14:16' (0x556c25d33100) of size 40
0x556c25d33128 is located 56 bytes to the left of global variable '*.Lubsan_data1' defined in 'test.c' (0x556c25d33160) of size 16
SUMMARY: AddressSanitizer: global-buffer-overflow (/tmp/a.out+0x1278) in sumArray
Shadow bytes around the buggy address:
  0x0aae04b9e5d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0aae04b9e5e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0aae04b9e5f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0aae04b9e600: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0aae04b9e610: 00 02 f9 f9 f9 f9 f9 f9 00 02 f9 f9 f9 f9 f9 f9
=>0x0aae04b9e620: 00 00 00 00 00[f9]f9 f9 f9 f9 f9 f9 00 00 f9 f9
  0x0aae04b9e630: f9 f9 f9 f9 00 00 00 00 f9 f9 f9 f9 00 00 00 f9
  0x0aae04b9e640: f9 f9 f9 f9 00 00 00 f9 f9 f9 f9 f9 00 00 00 00
  0x0aae04b9e650: 00 f9 f9 f9 f9 f9 f9 f9 00 00 00 00 f9 f9 f9 f9
  0x0aae04b9e660: f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9
  0x0aae04b9e670: f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
  Shadow gap:              cc
==2193597==ABORTING



Unit-тесты с ASan'ом помогут избежать большинства подобных ошибок, и изучать другой язык не понадобится.
Ничуть не умаляю мощь санитайзеров/valgrind, но они ловят только то, что произошло. А для этого надо написать тесты на все-все случаи (что невозможно, но опыт помогает покрыть большинство пограничных случаев).
В случае, если размер массива — константа (как в статье), static code analysis эту ошибку легко находит. Ну а если размер динамический, то и для D надо писать тесты, чудес-то не бывает.

Проще использовать range for или другими словами for each синтаксис.

UFO landed and left these words here
Не совсем так — все, что могут гарантировать «более выразительные языки» (т.е. языки более высокого уровня чем C), это то, что ошибка выхода за границы массива приведет не к испорченной памяти, а к прерыванию во времени исполнения. Но ошибка то никуда не делась, и «тщательно построенный вами замок» по-прежнему разрушен (т.е. программа работает не так, как предполагалось).

С тем, что в высокоуровневых языках можно гарантировать, что программа никогда не крашится при ошибках какого-то рода (или при любых ошибках) — с этим никто не спорит. Но чуда все-таки нет, это может достигаться только добавлением проверок во время выполнения… которые никто не запрещает делать и в низкоуровневых языках (только там их надо делать руками), и логическую ошибку в программе это не исправляет (только делает слегка проще ее обнаружение).
UFO landed and left these words here
Ну так я примерно про это и говорю — либо доказательство правильности существует (и тогда умный static code analysis может найти его и в низкоуровневом языке), либо его нет (что бывает чаще), и тогда проверки времени выполнения неизбежны и это ответственность программиста обработать такую ситуацию, в любом языке.

P.S. Чтобы прояснить — я ведь вовсе не против использования высокоуровневых языков, и совсем не отрицаю, что они действительно упрощают процесс программирования. Все, что я хотел сказать — использование такого языка не является «серебряной пулей», которая магически решит все проблемы, следить за логикой использования индексов в программе все равно придется. И тесты для этого все равно писать надо, никуда не деться.
UFO landed and left these words here
> Если за логикой использования индексов следит компилятор, то и тесты не нужны. Раз вы говорите о статических анализаторах, то представьте себе такой, который гарантирует, что если он не ругнулся на вашу программу, то ошибок доступа за границы там просто нет как класс.

Да, это именно то, о чем мы спорим — с моей точки зрения, в общем случае компилятор просто не в состоянии «следить за логикой», и никто не в состоянии, эта задача алгоритмически неразрешима. Если размер массива и/или индекс определяется динамически по некоторому алгоритму, то в принципе невозможно гарантировать отсутствие ошибок индексации — все, что можно сделать, это гарантировать что программа на падает в результате такой ошибки. Но ошибка-то все равно есть — результат работы не тот, что ожидался, — и найти ее без тестирования в принципе на практике невозможно.

То, что язык автоматически добавляет проверки индексов — это хорошо — если мы не стараемся сэкономить наносекунды во внутреннем цикле, конечно ;) — но эти проверки помогут только тогда, когда этот код хоть раз выполнится, а это и означает, что тесты все равно нужны.
UFO landed and left these words here
С этим я не спорю — заметьте, я в конце первого абзаца «в принципе» зачеркнул и заменил на «на практике» ;).

На самом деле, я полностью согласен, что если цель — избежать ошибок индексации (или каких других) любой ценой, то язык уровня C для этого плохо приспособлен (по крайней мере, без дополнительного пре-процессора и еще каких-то инструментов). Вопрос тут скорее в том, какой ценой этого можно достичь — да, «достаточно выразительный язык» (кстати, я так понимаю, вы D к ним не относите?) может предоставить более или менее удобные инструменты для этого, но писать доказательства все равно придется программисту, и это увеличивает затраты на написание программы очень значительно. Иногда это оправдано (если цена ошибки очень велика), но для большинства приложений тестирование просто обходится дешевле.
UFO landed and left these words here
Вроде бы, мы почти пришли к согласию… за исключением:

> А для большинства приложений, увы, прокатывает «упало — да и хрен с ним, перезапустим».

Почему сразу «хрен с ним»? правильный подход — это «упало — это, конечно, плохо, мы извиняемся за причиненные неудобства и дадим вам новую версию с исправленной ошибкой завтра, больше падать не будет». К сожалению (тут я с «увы» согласен), такой подход значительно дешевле на практике, чем попытки написать идеальную программу.
UFO landed and left these words here
UFO landed and left these words here
Юниттесты — подразумевают отдельный ран.

А вот все проверки, которые могут быть вычислены в компайл-тайме будут именно тогда и вычислены, CTFE в D мощное — даже делали расчет рейтресинга в компайл-тайме.

Если кратко, то можно абстрактно сформулировать так:


Доказательство что индекс корректный вы можете провести у себя в голове. Или другими словами этот факт можно доказать математически. Математичекские доказательства (навреное не все но многие) компьютер умеет проверять сам — на хабре обсуждалось. Следовательно существует язык (например Idris — тоже на хабре мелькал) который может заставить вас всё это доказывать, а потом компилятор будет это проверять, и в итоге гарантировать что индекс никогда не выходит за границы массива.


Практическая польза всего этого, это отдельная тема ;)

Так я же 100% практик, я только с этой точки зрения и смотрю ;)

Если бы он "не выстрелил", то не было бы этого.


Да, он не на слуху и не очень популярен, но популярность необязательно свидетельствует о качестве и возможностях, и популярные технологии необязательно лучшие или оптимальные.

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

Так ведь проблема решается просто:


#include <array>

#define MAX 10

template <size_t ArraySize>
int sumArray(std::array<int, ArraySize> p) {
    int sum = 0;
    int i;
    for (i = 0; i <= ArraySize; ++i)
        sum += p.at(i);
    return sum;
}

int main() {
    static std::array<int, MAX> values = { 7,10,58,62,93,100,8,17,77,17 };
    printf("sum = %d\n", sumArray(values));
    return 0;
}

А если не нравятся шаблоны, то можно написать свой класс-обёртку с явной передачей размера массива. Было бы желание.

Кажется вы путаете C с другим языком с похожим названием.

Шаблоны, классы, мы точно о C говорим, или уже вляпались в ООП и плюсы со всякими шарпами?

Во-первых, обычно D сравнивают с C++, а не C.
А во-вторых, я посмотрел в тэги, увидел C++.

Цикл статей о BetterC
Это сильно урезанное подмножество D.

Несколько раз пытался использовать D в первые девять лет после его появления. В каждом случае переход на D обещал что-то хорошее, но всегда перевешивали затраты на переход (обучение разработчиков и переписывание), плюс еще какие-нибудь сложности. Всегда чего-то недоставало: то производительности, то привязки к железу и low-level контролю, то гибкости и библиотек, и т.д. Всегда оказывалось, что рациональнее использовать Python, TCL, Ruby, C# или просто остаться на C/C++.


Сейчас, после появления: Go, Rust, C++ 11/14/17/20, ASAN, Wasm и JS-наводнения — уже нет сомнений, что "D не взлетит". Поэтому остался лишь около-академический интерес разбора причин "почему не взлетело".

Ну а в итоге, то — на чем остановились из списка?
Python, TCL, Ruby, C#, C/C++ 11/14/17/20, Go, Rust, ASAN, Wasm, JS

Просто остался на C/C++ в обнимку с ASAN ;)

UFO landed and left these words here

На всякий, "в обнимку с ASAN" — это шутка на 50% ;)


C tsan у меня отношения не сложились, примерно как с drd и helgrind — либо не требуется, либо прилично фалсит (много false-positives) и приходиться разгребать руками:


  • Свой код уже автопилотно получается с минимизацией contention и кол-ва примитивов синхронизации, либо lock/wait-free если нужно.
  • Для чужого запутанного (особенно legacy) кода эти инструменты могут прилично фалсить. И вот после ReOpenLDAP в 2015 мне что-либо по-крупному чинить не приходилось (тьфу-тьфу).
UFO landed and left these words here
UFO landed and left these words here

Доступность санитайзеров и их дружба друг-с-другом зависит от платформы, версии софта и т.п. Например, если не ошибаюсь, в gcc 10.x на x86_64 можно включить UndefinedBehaviorSanitizer (ubsan) одновременно с AddressSanitizer (asan) или с MemorySanitizer. А при выключенном asan можно всегда включить отдельно LeakSanitizer c любым другим санитайзером. Но лучше смотреть https://github.com/google/sanitizers и доку по конкретному компилятору.


  1. Для памяти: LeakSanitizer по-умолчанию включается одновременно с AddressSanitizer. Использование AddressSanitizer + MemorySanitizer (т.е. две отдельные сборки и два прогона) примерно соответствуют прогону под Valgrind с трекингом аллокаций.
  2. Для гонок: ThreadSanitizer, но я им почти не пользуюсь (писал выше).
  3. UndefinedBehaviorSanitizer: отлавливает массу всякой фигни, особенно в легаси-коде писанном для x86. Мне много раз помогал с unaligned access.

Но в целом, это RTFM.

Sign up to leave a comment.

Articles