[С] тупик дебаг и релиз сборок

Werdna

Пока медленно, но верно движется курс, буду выносить на обсуждение интересные вопросы. Интересные они тем, что нет однозначного ответа на них.
Кто-то когда-то придумал делать сборки debug и release, отличающиеся тем, что в первом случае код компилируется годный для отладки, а во втором — оптимизированный для продакшена. Я выскажу всем очевидное требование, но далеко не всегда выполняемое: debug и release сборки обязаны иметь одинаковое поведение на одинаковых исходных данных. В первую очередь это касается, конечно же, сегволтов и прочих экстремальных ситуаций.
Теперь возьмём классические примеры, когда они могут нарушаться: assert'ы и debug-вывод в логи. К сожалению, моя практика показывает, что уследить за ассёртами ещё как-то можно, то за всем остальным — нет.
Рассмотрим такой пример:

#ifdef DEBUG
void debug(const char* format, ...)
{
va_list vl;
va_start(vl, format);
char buf[BUF_SZ];
vsnprintf(buf, BUF_SZ, format, vl);
fprintf(stderr, "%s\n", buf);
va_end(vl);
}
#else
void debug(const char* format, ...)
{
}
#endif

Что тут совсем плохо? Прежде всего то, что код может упать как раз на неверном формате и данных. В релизе проскочит и пойдёт дальше:

int a;
char* b;
...
debug("%d: %s\n", b, a);
run_proc_with_error(a, b);

Если в продакшене всё будет падать в функции с ошибкой, то в дебаге — ДО этого, причем не сразу догадаешься где, если кода много. Конечно же, это лечится, и определение функции debug можно довесить __attribute__ (__printf__, 1, 2... она будет пищать ворнингами, но это не единственная ситуация.
Другой пример, придумывать специально лень, но очень реалистичен и часто бывает на практике. После отработки дебаг-функции чудесным образом чистится стек, который в релизной сборке приводит к падению. Да, где-то дальше идёт ошибка выхода за стек, но получив сегфолт в релизе вы его не воспроизведёте в дебаге.
Холивар-тема для обсуждения: все программы собирать и эксплуатировать в среднем варианте, когда не теряются таблицы символов и не делаются чудовищно медленные проверки везде и всюду в виде ассёртов. Этот вариант использовать и при разработке, и на продакшене, имея всегда предсказуемо одинаковое поведение программ.

doublemother

Спасибо, капитан. Осталось рассказать ещё про стрип дебаг-символов в отдельный пакет, который клиенту, например, вообще отправлять не обязательно.

Werdna

Спасибо, капитан.
Многие просили материалы из курса, который вяло, но идёт. Мне показалась эта тема интересной, я ещё хочу запостить некоторые.

vall

у нас долго боролись с тем что дебаг часто ломают и не замечают, в итоге придумали что-то типа этого:

#ifdef DEBUG
# define BUG_ON(x) if (x) { падаем }
#elif
# define BUG_ON(x) void)sizeof(x
#endif

ava3443

Кто-то когда-то придумал делать сборки debug и release, отличающиеся тем, что в первом случае код компилируется годный для отладки, а во втором — оптимизированный для продакшена.
имхо:
1) это разделение было придумано (и до сих пор в основном используется) на винде
2) ключевое отличие (на винде про которое ты не упомянул (видимо потому что винда не актуальна) - использование этими сборками разных рантайм-библиотек.
P.S. на юниксах/линуксе всегда всё собираю с -g и -fno-omit-frame-pointer (аналог для SunCC: -xregs=no%frameptr)

Werdna

в итоге придумали что-то типа этого
Так вычисление x как бы повлечёт падение производительности. Ну не упал ты, и что? Разницы никакой.

ava3443

Так вычисление x как бы повлечёт падение производительности.
а ты замерял это падение именно на своём софте? может, оно вообще не актуально?

Werdna

ключевое отличие (на винде про которое ты не упомянул (видимо потому что винда не актуальна) - использование этими сборками разных рантайм-библиотек
А уже под Линуксом это тоже стало нормой, даже пакеты идут отдельные дебажные.
[pollstart]
[polltitle=Что вы думаете о debug и release сборках?]
[polloption=Я использую их, это удобно и позволяет эффективно отлаживать]
[polloption=У нас так принято в команде, но я особо не пользуюсь данной фичей]
[polloption=У нас на проекте это не навязывается, я и не использую]
[polloption=Я не знаю что такое debug и release]
[polloption=Пунктик для гостей и троллей]
[pollstop]

ava3443

Пунктик для гостей и троллей
придётся сюда записаться хотя мне бы подошёл вариант "использую debug-сборку когда нужен debug heap или его аналоги" (под аналогами на юниксах подразумеваю опции компилятора, включающие различные runtime-проверки, в основном по памяти - типа +check у HP aCC)

vall

ты не понял. если кто-то патчит код и собирает его без дебага то он может сломать компиляцию кода с дебагом. а этот sizeof проверяет что выражение валидное но не вычисляет его.

bleyman

То, что программа может падать в дебаге и релизе в разных местах само по себе не страшно. Неприятно только когда она падают в релизе, но не падает в дебаге. Противоположная ситуация наоборот крайне приятна, но ты мешаешь всё в одну кучу, поэтому твой первый пример — тупой.
Далее, неприятный вариант конечно тоже случается, но крайне редко. Хотя, конечно, такие случаи запоминаются намного лучше, чем в сотни или тысячи раз более частые баги, которые удаётся легко и приятно отловить в дебаг режиме — благодаря присутствующим там проверкам. Это называется selection bias.
Итак, идеальная ситуация это когда падения в релизе являются подмножеством падений в дебаге, причём последние триггерятся как можно большим количеством багов. Правильные компиляторы (например, Microsoft Visual C++) прилагают целенаправленные усилия для продвижения в этом направлении: так, неинициализированная память инициализируется специальными значениями (а вовсе не нулями обращение к неинициализированной переменной как правило приводит к немедленному падению (уж не знаю, как они этого добиваются вокруг аллоцированных блоков и между стекфреймами тоже оставляются проинициализированные магическими значениями зазоры (которые автоматически проверяются на предмет коррапшена и так далее. GCC, кажется, не настолько продвинут в этом смысле, возможно, в этом твоя проблема! =)

elenangel

инициализация неинициализированной памяти значением 0xCC - это та магия про которую ты говоришь?
она поможет всего лишь всплыть отладчику, когда управление случайно передастся на данные и возможно значение указателя 0xCCCCCCCC показывает в область, которая сразу даст segfault при обращении. Просто "переменную" ты этим никак не спасешь.

Serab

это еще теоретически помогает всплать багам с неинициализированными данным: нуль чаще бывает допустимым значением, чем что-нибудь большое.

vall

stack-protection в gcc тоже есть. фиксированный poison для неинициализированных данных не лучшая идея — может так получится что с ним всё работает. уж лучше случайно замазывать.

doublemother

это еще теоретически помогает всплать багам с неинициализированными данным:
Как правило, этому багу помогает всплыть повышение уровня предупреждений компилятора (а вот за его понижение уже надо бить). Меня иногда задалбывает, что гцц от меня требует даже в конструкторе класса проинициализировать все поля — даже те, которые являются объектами — но мне кажется, что это в целом совершенно правильно.

Maurog

Правильные компиляторы (например, Microsoft Visual C++) прилагают целенаправленные усилия для продвижения в этом направлении:
т.к. это негативно влияет на производительность, то производят эти действия только в дебаге
хотя включить можно и в релизе из кода
пример: http://msdn.microsoft.com/en-us/library/5at7yxcs%28v=vs.71%2...

Maurog

по теме:
1) пример неудачный
2) ненавижу #ifdef DEBUG
3) ненавижу функции с переменным числом аргументов

Werdna

ненавижу функции с переменным числом аргументов
почему? а как же функции типа printf?

Maurog

а как же функции типа printf?
я ее ненавижу тоже
из-за отсутствия проверки типов и крашей
функция, которая не может что-то сделать должна вернуть код ошибки\исключение, а не падать

ava3443

из-за отсутствия проверки типов и крашей
приличные современные компиляторы таки проверяют типы аргументов printf на этапе компиляции
ах, да, в хвалёном (выше) Microsoft Visual C++ это только начиная с версии 1600 (VC10 только с включённым static analyzer, и только при 32-битной сборке (ни в 64-битном компиляторе, ни в кросс-компиляторе static analyzer не доступен).

Werdna

из-за отсутствия проверки типов и крашей
Может тебе ещё выход за пределы массива нужен?
Ты не забывай, что этот системное программирование, а не кодирование на высоком уровне. Проверка типов — накладной расход, который, кстати, ничего не гарантирует! Ты же можешь подсунуть ту же const char* совершенно не валидную.

Werdna

приличные современные компиляторы таки проверяют типы аргументов printf на этапе компиляции
gcc это давно делает, по-моему с 4 версии. Уже есть даже резервные слова для того, чтобы формат и параметры компилятор проверял в произвольных функциях.

vall

Проверка типов — накладной расход, который, кстати, ничего не гарантирует!
Всё ещё хуже. Например абсолютно безопасный memcpy можно реализовать только сисколом, иначе никак не убрать рэйсы с unmap в соседнем трэде.

Maurog

Ты не забывай, что этот системное программирование, а не кодирование на высоком уровне.
неверно: это мрачный язык С, а не системное программирование
С - язык общего назначения, а printf кривая функция ядра языка, которая еще и с локалями неявно работает, которые нафиг не нужны в системном программировании
Проверка типов — накладной расход, который, кстати, ничего не гарантирует!
я не предлагал вводить рефлекшен и в рантайме проверять типы
с этим должен справляться компилятор
гарантировать отсутствие крашей вполне можно было на уровне дизайна
возможность проверки компилятором формата с типами аргументов - это костыль, который далеко не сразу прикрутили и который не решает проблему полностью: выводится ворнинг вместо ошибки компиляции + невозможно проверить формат, определяемый в рантайме
и людям приходится выпиливать велосипеды из гранита: http://www.fastformat.org/

Werdna

гарантировать отсутствие крашей вполне можно было на уровне дизайна
никак ты это не гарантируешь, просто неверная const char* — и ты в сегфолте.
А что тебе не нравится в сегфолтах? Это нормальное состояние программы на момент написания. По сути это аппаратная проверка условий так срабатывает. :)

Werdna

возможность проверки компилятором формата с типами аргументов - это костыль, который далеко не сразу прикрутили и который не решает проблему полностью: выводится ворнинг вместо ошибки компиляции + невозможно проверить формат, определяемый в рантайме
Это не костыль, это ворнинг. А ты разве компилируешь программы без -Wall? Все ворнинги надо вычищать, иначе это плохо написанная программа.
Что такое «формат, определяемый в рантайме»? :confused: :confused: :confused:

Maurog

Все ворнинги надо вычищать, иначе это плохо написанная программа
мы и так переключились с дебуг\релиза на функции с ..., а ты еще один вброс делаешь :grin:

Maurog

Что такое «формат, определяемый в рантайме»?
как-то так: http://ideone.com/rC95M

Werdna

как-то так
:facepalm:
Никогда так не пиши! Это же ахтунг...

Maurog

Никогда так не пиши!
ОК

Werdna

Не, я серьёзно.
Если нужно много языков поддерживать, то gettext используй, по крайней мере будешь сразу видеть в одной строчке и формать, и данные какие передаёшь. А при переводе — на совести переводчика будут строчки.

printf(_("Hello, %s! You have %u points!" name, points);

Maurog

Не, я серьёзно.
я тебе на данном примере объяснил что значит «формат, определяемый в рантайме»
надеюсь, мне удалось
если бы все считали, что любой пример с динамическим форматом является "ахтунгом", то данное использование могли бы запретить и на уровне стандарта и на уровне компилятора: не компилировать такой код :grin:

zorin29

Хе-хе, я помню незабвенной памяти перевод X2: The Threat:
"За выполнение задания вы получите 13200 кредитов, а если успеете за 62140 кредитов, то получите дополнительно 12 дней".
(это, очевидно, "You will get %s, and %s more if you arrive in %s").

karkar

Может тебе ещё выход за пределы массива нужен?
Ты не забывай, что этот системное программирование, а не кодирование на высоком уровне. Проверка типов — накладной расход, который, кстати, ничего не гарантирует! Ты же можешь подсунуть ту же const char* совершенно не валидную.
Есть правильный Си, в котором отсутствие выходов за пределы массива и невалидных указателей проверяется-таки статически, без накладных расходов в рантайме. Называется ATS:
http://thedeemon.livejournal.com/41035.html

Ivan8209

> #ifdef DEBUG
Программистов сей, которые ещё не выучили, что существует выделенный
символ NDEBUG, надо убивать. Желательно --- как можно жесточе.
> она будет пищать ворнингами
Программистов сей, которые ещё не выучили, что программа должна
не только проходить lint (splint или иной анализатор но ещё и
собираться с флагами типа -Werror, надо убивать.
Желательно --- как можно жесточе.
> Холивар-тема для обсуждения: все программы собирать и
> эксплуатировать в среднем варианте, когда не теряются
> таблицы символов и не делаются чудовищно медленные проверки
> везде и всюду в виде ассёртов. Этот вариант использовать
> и при разработке, и на продакшене, имея всегда предсказуемо
> одинаковое поведение программ.
Я вот думаю: как же это так наши коллеги ваяют высоконагруженный
код на яве, да ещё и запихивают его во встраиваемую систему?
---
"Мы диалектику учили не по Гегелю.
Бряцанием боёв она врывалась в стих..."

doublemother

Программистов сей, которые ещё не выучили, что существует выделенный
символ NDEBUG, надо убивать. Желательно --- как можно жесточе.
NDEBUG — исключительно для ассертов, и то должен выставляться программистом. Что касается кастомных отладочных вызовов, то тут уже воля твоя, что использовать. Студия вон, дефайнит __DEBUG.
В остальном, впрочем, не спорю.

apl13

Программистов сей, которые ещё не выучили, что программа должна
не только проходить lint (splint или иной анализатор но ещё и
собираться с флагами типа -Werror, надо убивать.
Желательно --- как можно жесточе.
A narrowness of experience leads to the narrowness of imagination. :umnik:

Ivan8209

> NDEBUG — исключительно для ассертов, и то должен выставляться программистом.
Не программистом, а отдельным человеком, следящим за сборкой, от
программиста требуется только неизобретение самопальных ассертов
и тому подобной ерунды. (Или, если уж изобретает, пусть изобретает
более вменяемые ассерты, которые посылают стек в syslog, сохраняют
дамп памяти процесса и корректно всё перезапускают, не дожидаясь
сторожа.)
> Что касается кастомных отладочных вызовов, то тут уже воля твоя, что использовать.
Я вот думаю: что же вы такое пишете, что не является
лабораторной работой и можно просто так пересобрать для отладки?
Как только продукт уйдёт в поле, усилия на написание отладочного
кода с привлечением препроцессора будут бесполезны: никакой
заказчик не будет выполнять никаких телодвижений, чтобы
удовлетворить любопытство разработчика. Тот, который будет, это
очень редкое исключение. Так что все эти DEBUG и __DEBUG, если
они не управляют ассертами, должны быть переменными,
определяющими, что пишется в журнал, а это уже совсем другая
сказка.
---
"Vyroba umelych lidi, slecno, je tovarni tajemstvi."

doublemother

Я вот думаю: что же вы такое пишете, что не является
лабораторной работой и можно просто так пересобрать для отладки?
Как только продукт уйдёт в поле, усилия на написание отладочного
кода с привлечением препроцессора будут бесполезны: никакой
заказчик не будет выполнять никаких телодвижений, чтобы
удовлетворить любопытство разработчика. Тот, который будет, это
очень редкое исключение. Так что все эти DEBUG и __DEBUG, если
они не управляют ассертами, должны быть переменными,
определяющими, что пишется в журнал, а это уже совсем другая
сказка.
То есть, версии, что продукт может писаться не для заказчика, а для себя, ты не допускаешь? Или что производительность может быть критична настолько, что нельзя в методе, вызываемом >1М раз в секунду, лишний раз проверять какую-то переменную? Особенно, если она может лежать в конфиге, который может кем-то в рантайме меняться.

Ivan8209

> То есть, версии, что продукт может писаться не для заказчика, а для себя, ты не допускаешь?
А работает-то она где? В лаборатории?
> Или что производительность может быть критична настолько, что
> нельзя в методе, вызываемом >1М раз в секунду, лишний раз
> проверять какую-то переменную?
Это теоретические измышления или у вас практика такая?
Как такой код ушёл в производство, если в нём время от времени
для отладки приходится вставлять учёт событий? Или вы "железо"
так проверяете?
> Особенно, если она может лежать в конфиге, который может
> кем-то в рантайме меняться.
Как это мешает? Или вы конфигурационный файл перечитываете раз в секунду?
---
...Я работаю антинаучным аферистом...

doublemother

> А работает-то она где? В лаборатории?
У тех, кто пишет код. Не в лаборатории.
> Как такой код ушёл в производство, если в нём время от времени
> для отладки приходится вставлять учёт событий? Или вы "железо"
> так проверяете?
Не учёт, а вывод дополнительной информации о них на консоль, например. Вы, очевидно, адепты практики дебага методом пристального разглядывания? Или настоящие джедаи дебажат только в гдб? Ну так поведение программы в гдб отличается от нормального. Я что-то плохо понимаю, какой способ отладки ты пропагандируешь.
> Как это мешает? Или вы конфигурационный файл перечитываете раз в секунду?
Это мешает как минимум тем, что ты не имеешь права закэшировать параметры конфига локально, например, прямо в функции, а будешь вынужден обращаться каждый раз к классу конфига, просить у него выдать соответствующий параметр. Все эти вызовы тоже будут давать накладные расходы.
Оставить комментарий
Имя или ник:
Комментарий: