Скомпилированный отладочный исполняемый файл: почему бы не прервать корректно при некорректной записи в NULL? - PullRequest
2 голосов
/ 15 февраля 2009

Что я не понимаю в C / C ++:

Да, все используют его для получения невероятно быстрых исполняемых файлов, поэтому они компилируются с включенной оптимизацией.

Но для компиляции с включенной отладочной информацией нам не важна скорость. Так почему бы не включить больше информации в этот режим компиляции, например, обнаружить некоторые ошибки, прежде чем они произойдут? По сути, вставьте assert(ptr != NULL) перед каждым доступом к указателю ptr. Почему компилятор не может этого сделать? Опять же, это должно быть отключено по умолчанию, но такая возможность, я думаю, должна быть.

РЕДАКТИРОВАТЬ: Некоторые люди говорили, что обнаружение, которое я предложил, не имеет смысла или не делает ничего, что отчет о segmentation fault уже не сделал бы. Но что я имею в виду, это просто более изящный и информативный прерывание, которое печатает имя файла и номер строки кода, вызывающего нарушение, как это делал бы assert().

Ответы [ 9 ]

9 голосов
/ 15 февраля 2009

Есть несколько серьезных проблем с вашим предложением:

Какие условия вы хотите, чтобы компилятор обнаружил? В Linux / x86 не выровненный доступ может вызвать SIGBUS, а переполнение стека может вызвать SIGSEGV, но в обоих случаях технически возможно написать приложение для обнаружения этих условий и «изящного» сбоя. NULL проверки указателей могут быть обнаружены, но наиболее коварные ошибки связаны с неправильным доступом к указателям, а не с NULL указателями.

Языки программирования C и C ++ обеспечивают достаточную гибкость, поэтому среда выполнения не может с 100% успехом определить, является ли заданный случайный адрес допустимым указателем произвольного типа.

Что бы вы хотели, чтобы среда выполнения выполняла при обнаружении этой ситуации? Это не может исправить поведение (если вы не верите в магию). Он может только продолжить выполнение или выйти. Но подождите ... это то, что уже происходит, когда сигнал доставлен! Программа закрывается, генерируется дамп ядра, и этот дамп ядра может быть использован разработчиками приложения для определения состояния программы в случае сбоя.

То, что вы защищаете, на самом деле звучит так, как будто вы хотите запустить свое приложение в отладчике (gdb) или с помощью какой-либо формы виртуализации (valgrind). Это уже возможно, но не имеет смысла делать это по умолчанию, потому что это не приносит пользы не-разработчикам.

Обновление для ответа на комментарии:

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

8 голосов
/ 15 февраля 2009

Что должна делать программа в этом случае? Если он информирует пользователя об ошибке, то именно это и делает segfault.

Если он должен продолжать и избегать ошибок, как он узнает, что делать?

Не говоря уже о том, что если он каким-то волшебным образом знал, как правильно продолжать работу, то у вас есть ошибка в вашей сборке релиза (отладочные сборки предназначены для того, чтобы помочь вам идентифицировать и исправлять ошибки, а не скрывать их).


В ответ на дополнительную информацию, добавленную к вопросу (думаю, я неправильно понял ваше намерение):

Я имею в виду просто более изящный и информативный прерывание, которое печатает имя файла и номер строки нарушающего кода, как это делает assert ().

Это то, что компилятор может сделать - как вы говорите, компилятор, по сути, будет автоматически вставлять assert() везде, где указатель разыменовывается. Это может значительно увеличить размер отладочной сборки, но, вероятно, все же будет приемлемым для многих (или большинства) целей. Я думаю, что это было бы разумным вариантом для компилятора.

Я не уверен, что скажут производители компиляторов ... Возможно, опубликуйте запрос на сайте Microsoft Connect для продукта VC ++ и посмотрите, что они скажут.

1 голос
/ 15 февраля 2009

Есть еще одна причина, по которой простой assert(ptr != NULL) не будет работать до того, как разыменование указателя не будет работать: не каждый неверный указатель (даже тот, который начал жизнь как NULL) фактически равен 0.

Сначала рассмотрим случай, когда у вас есть структура с несколькими членами:

struct mystruct {
    int first;
    int second;
    int third;
    int fourth;
};

Если у вас есть указатель ptr на mystruct и вы пытаетесь получить доступ к ptr->second, компилятор сгенерирует код, который рекламирует 4 (при условии 32-разрядных целых) для ptr, и получит доступ к этой области памяти , Если ptr равно 0, то фактическое расположение в памяти будет равно 4. Это по-прежнему недопустимо, но простое утверждение не поймет. (Можно разумно ожидать, что компилятор проверит адрес ptr перед добавлением 4, и в этом случае утверждение перехватит его.)

Во-вторых, рассмотрим случай, когда у вас есть массив struct mystruct, и вы передаете произвольный элемент другой функции. Если вы попытаетесь получить доступ ко второму элементу массива, он начнется с 16 байтов после первого указателя. В любом случае нельзя ожидать, что компилятор будет делать то, что вам нужно, надежно во всех случаях, без перехвата правильной арифметики с указателями.

Что вы действительно хотите сделать, так это использовать операционную систему и аппаратные средства, чтобы перехватить недопустимый и невыровненный доступ к памяти и уничтожить ваше приложение, а затем выяснить, как получить необходимую информацию для отладки. Самый простой способ - просто запустить внутри отладчика. Если вы используете gcc в Linux, см. , как генерировать стековую запись при сбое моего приложения C ++ . Я предполагаю, что есть аналогичные способы сделать то же самое с другими компиляторами.

1 голос
/ 15 февраля 2009

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

Если вы разрабатываете на C / C ++, менеджер отладочной памяти может сэкономить вам массу времени. Переполнения буфера, доступ к удаленной памяти, утечки памяти и т. Д. Довольно легко найти и исправить. На рынке их несколько, или вы можете потратить 2 или 3 дня, чтобы написать свой собственный и получить 90% необходимой функциональности. Если вы пишете приложения без них, вы делаете свою работу намного сложнее, чем нужно.

1 голос
/ 15 февраля 2009

Я согласен с Майклом Берром, что это на самом деле ничего не делает или не помогает.

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

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

0 голосов
/ 16 февраля 2009

Хорошая идея, но только в одном конкретном случае. Это до того, как вы разыменуете указатель на функцию. Причина в том, что отладчик будет всегда включаться, но после разыменования нулевого указателя функции стек снимается. Следовательно, у вас есть проблемы с поиском нарушающего кода. Если вызывающий проверит перед вызовом, отладчик сможет дать вам полный стек.

Более общей проверкой было бы проверить, указывает ли указатель на память, которая является исполняемой. NULL нет, но многие операционные системы также могут использовать функции ЦП, чтобы сделать определенные сегменты памяти неисполняемыми.

0 голосов
/ 15 февраля 2009

Учитывая, что у вас есть файл символов для вашего исполняемого файла, можно сопоставить местоположение сбоя с номером строки. Отладчик делает это за вас, если вы запускаете его в отладчике, как уже упоминали другие. Visual C ++ даже предлагает отладку «точно в срок», когда в случае сбоя программы вы можете присоединить отладчик к аварийному процессу, чтобы увидеть, в чем проблема.

Однако, если вы хотите использовать эту функцию на машинах, где Visual C ++ не установлен, все еще возможно сделать это с некоторым кодированием. Вы можете настроить обработчик исключений, используя SetUnhandledExceptionFilter, который будет вызываться при сбое вашей программы. В обработчике вы можете просмотреть запись исключения и использовать SymGetLineFromAddr64, чтобы определить, какая строка источника выполнялась. В библиотеке «справка отладки» есть много функций, которые позволяют вам извлекать любую информацию. См. статьи на MSDN , а также статьи на www.debuginfo.com .

0 голосов
/ 15 февраля 2009

Уже есть эквивалент assert (prt! = NULL) как часть ОС. Вот почему вы получаете segfault вместо того, чтобы просто перезаписывать важные важные данные по адресу 0, а затем действительно портить систему.

0 голосов
/ 15 февраля 2009

Итак, вы говорите, что перед тем, как система выдаст ошибку, она должна выдать ошибку, чтобы ... предупредить вас о надвигающейся ошибке?

Какой в ​​этом смысл? Когда я получаю segfault, я знаю, что это означает, что я получил segfault. Мне не нужно отдельное сообщение, вначале говорящее: «Теперь вы получите ошибку».

Я полностью упускаю суть здесь? : Р

Edit: Я понимаю, что вы имеете в виду в своем редактировании, но это нелегко реализовать. Проблема в том, что не компилятор, не язык или среда выполнения не решают, что должно произойти, если вы обращаетесь к неверному указателю. Язык официально не дает никаких обещаний или гарантий по этому поводу. Вместо этого ОС выдает ошибку, не зная, что это исполняемый файл отладки, не зная, какой номер строки вызвал проблему или что-то еще. Единственное, что говорит эта ошибка: «Вы пытались получить доступ к адресу X, и я не могу этого допустить. Умри». Что должен с этим сделать компилятор?

Так, кто должен генерировать это полезное сообщение об ошибке? И как? Конечно, компилятор может это сделать, но при обработке ошибок обернуть каждый доступ по одному указателю, чтобы гарантировать, что в случае нарушения segfault / access мы его перехватим и вместо этого вызовем assert. Проблема в том, что это будет смехотворно медленным . Не только «слишком медленный для выпуска», но «слишком медленный, чтобы его можно было использовать». Предполагается также, что у компилятора есть доступ к всему коду, который вы вызываете. Что если вы вызываете функцию из сторонней библиотеки? Доступ к указателю внутри, который не может быть включен в код обработки ошибок, потому что компилятор не генерирует код для этой библиотеки.

ОС может сделать это, предполагая, что она готова / может загрузить соответствующие файлы символов, каким-то образом определить, запускаете ли вы отладочный исполняемый файл и т. Д. распечатать номер строки. Поговорим о переобработке. Это вряд ли работа ОС.

И, наконец, что бы вы получили, сделав это? Почему бы просто не запустить свой отладчик? Он автоматически прерывается, когда происходит что-то подобное, давая вам точный номер строки и все остальное.

Это можно было бы сделать, но это было бы ужасно сложно и включало бы и компилятор, и ОС, и выгода была бы чрезвычайно мала. Вы получите всплывающее окно с информацией о том, что ваш отладчик уже может сообщить вам. И с этой информацией вы затем ... запустите свой отладчик в любом случае , чтобы узнать, что пошло не так.

...