C exceptions in linux

Обработка ошибок в C

Введение

Переменная errno и коды ошибок

errno – переменная, хранящая целочисленный код последней ошибки. В каждом потоке существует своя локальная версия errno, чем и обусловливается её безопасность в многопоточной среде. Обычно errno реализуется в виде макроса, разворачивающегося в вызов функции, возвращающей указатель на целочисленный буфер. При запуске программы значение errno равно нулю.

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

Стандарт ISO C определяет следующие коды:

  • EDOM – (Error domain) ошибка области определения.
  • EILSEQ – (Error invalid sequence) ошибочная последовательность байтов.
  • ERANGE – (Error range) результат слишком велик.

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

Нехитрый скрипт печатает в консоль коды ошибок, их символические имена и описания:

Если вызов функции завершился ошибкой, то она устанавливает переменную errno в ненулевое значение. Если же вызов прошёл успешно, функция обычно не проверяет и не меняет переменную errno. Поэтому перед вызовом функции её нужно установить в 0 .

Как видите, описания ошибок в спецификации функции iconv() более информативны, чем в .

Функции работы с errno

Получив код ошибки, хочется сразу получить по нему её описание. К счастью, ISO C предлагает целый набор полезных функций.

void perror(const char *s);

Печатает в stderr содержимое строки s , за которой следует двоеточие, пробел и сообщение об ошибке. После чего печатает символ новой строки ‘\n’ .

char* strerror(int errnum);
Возвращает строку, содержащую описание ошибки errnum . Язык сообщения зависит от локали (немецкий, иврит и даже японский), но обычно поддерживается лишь английский.

strerror() не безопасная функция. Во-первых, возвращаемая ею строка не является константной. При этом она может храниться в статической или в динамической памяти в зависимости от реализации. В первом случае её изменение приведёт к ошибке времени выполнения. Во-вторых, если вы решите сохранить указатель на строку, и после вызовите функцию с новым кодом, все прежние указатели будут указывать уже на новую строку, ибо она использует один буфер для всех строк. В-третьих, её поведение в многопоточной среде не определено в стандарте. Впрочем, в QNX она объявлена как thread safe.

Поэтому в новом стандарте ISO C11 были предложены две очень полезные функции.

size_t strerrorlen_s(errno_t errnum);

Возвращает длину строки с описанием ошибки errnum .

errno_t strerror_s(char *buf, rsize_t buflen, errno_t errnum);

Копирует строку с описание ошибки errnum в буфер buf длиной buflen .

Функции входят в Annex K (Bounds-checking interfaces), вызвавший много споров. Он не обязателен к выполнению и целиком не реализован ни в одной из свободных библиотек. Open Watcom C/C++ (Windows), Slibc (GNU libc) и Safe C Library (POSIX), в последней, к сожалению, именно эти две функции не реализованы. Тем не менее, их можно найти в коммерческих средах разработки и системах реального времени, Embarcadero RAD Studio, INtime RTOS, QNX.

Стандарт POSIX.1-2008 определяет следующие функции:

char *strerror_l(int errnum, locale_t locale);

Возвращает строку, содержащую локализованное описание ошибки errnum , используя locale . Безопасна в многопоточной среде. Не реализована в Mac OS X, FreeBSD, NetBSD, OpenBSD, Solaris и прочих коммерческих UNIX. Реализована в Linux, MINIX 3 и Illumos (OpenSolaris).

Читайте также:  Asus tuf gaming fa506 bios utility ez mode установка windows

int strerror_r(int errnum, char *buf, size_t buflen);

Копирует строку с описание ошибки errnum в буфер buf длиной buflen . Если buflen меньше длины строки, лишнее обрезается. Безопасна в многоготочной среде. Реализована во всех UNIX.

Увы, никакого аналога strerrorlen_s() в POSIX не определили, поэтому длину строки можно выяснить лишь экспериментальным путём. Обычно 300 символов хватает за глаза. GNU C Library в реализации strerror() использует буфер длиной в 1024 символа. Но мало ли, а вдруг?

Макрос assert()

Макрос, проверяющий условие expression (его результат должен быть числом) во время выполнения. Если условие не выполняется ( expression равно нулю), он печатает в stderr значения __FILE__ , __LINE__ , __func__ и expression в виде строки, после чего вызывает функцию abort() .

Функции atexit(), exit() и abort()

int atexit(void (*func)(void));

Регистрирует функции, вызываемые при нормальном завершении работы программы в порядке, обратном их регистрации. Можно зарегистрировать до 32 функций.

_Noreturn void exit(int exit_code);

Вызывает нормальное завершение программы, возвращает в среду число exit_code . ISO C стандарт определяет всего три возможных значения: 0 , EXIT_SUCCESS и EXIT_FAILURE . При этом вызываются функции, зарегистрированные через atexit() , сбрасываются и закрываются потоки ввода — вывода, уничтожаются временные файлы, после чего управление передаётся в среду. Функция exit() вызывается в main() при выполнении return или достижении конца программы.

Главное преимущество exit() в том, что она позволяет завершить программу не только из main() , но и из любой вложенной функции. К примеру, если в глубоко вложенной функции выполнилось (или не выполнилось) некоторое условие, после чего дальнейшее выполнение программы теряет всякий смысл. Подобный приём (early exit) широко используется при написании демонов, системных утилит и парсеров. В интерактивных программах с бесконечным главным циклом exit() можно использовать для выхода из программы при выборе нужного пункта меню.

_Noreturn void abort(void);

Вызывает аварийное завершение программы, если сигнал не был перехвачен обработчиком сигналов. Временные файлы не уничтожаются, закрытие потоков определяется реализацией. Самое главное отличие вызовов abort() и exit(EXIT_FAILURE) в том, что первый посылает программе сигнал SIGABRT , его можно перехватить и произвести нужные действия перед завершением программы. Записывается дамп памяти программы (core dump file), если они разрешены. При запуске в отладчике он перехватывает сигнал SIGABRT и останавливает выполнение программы, что очень удобно в отладке.

Вывод в отладчике:

В случае критической ошибки нужно использовать функцию abort() . К примеру, если при выделении памяти или записи файла произошла ошибка. Любые дальнейшие действия могут усугубить ситуацию. Если завершить выполнение обычным способом, при котором производится сброс потоков ввода — вывода, можно потерять ещё неповрежденные данные и временные файлы, поэтому самым лучшим решением будет записать дамп и мгновенно завершить программу.

В случае же некритической ошибки, например, вы не смогли открыть файл, можно безопасно выйти через exit() .

Функции setjmp() и longjmp()

Вот мы и подошли к самому интересному – функциям нелокальных переходов. setjmp() и longjmp() работают по принципу goto, но в отличие от него позволяют перепрыгивать из одного места в другое в пределах всей программы, а не одной функции.

int setjmp(jmp_buf env);

Сохраняет информацию о контексте выполнения программы (регистры микропроцессора и прочее) в env . Возвращает 0 , если была вызвана напрямую или value , если из longjmp() .

void longjmp(jmp_buf env, int value);

Восстанавливает контекст выполнения программы из env , возвращает управление setjmp() и передаёт ей value .

Используя setjmp() и longjmp () можно реализовать механизм исключений. Во многих языках высокого уровня (например, в Perl) исключения реализованы через них.

Внимание! Функции setjmp() и longjmp () в первую очередь применяются в системном программировании, и их использование в клиентском коде не рекомендуется. Их применение ухудшает читаемость программы и может привести к непредсказуемым ошибкам. Например, что произойдёт, если вы прыгните не вверх по стеку – в вызывающую функцию, а в параллельную, уже завершившую выполнение?

Читайте также:  Синхронизация папок windows linux

Источник

Atomic Spin

Atomic Object’s blog on everything we find fascinating.

Catching Exceptions and Printing Stack Traces for C on Windows, Linux, & Mac

Debugging C can be a real pain, especially when all you have to go by is that it was a segfault. Great!

In an effort to make testing C code a little less painful, I’ve recently added stack trace support to Unity (for gcc on Windows or Posix systems). That way, when a test crashes, I will at least know where it crashed. I learned quite a bit in the process and thought I’d share.

A Troubled Program

First, let’s start with a test program that will emit some of the signals we want to catch. It is entirely self contained except for a set_signal_handler() that we will define separately for windows and Posix systems, with a slight variation on the Posix version for OS X.

The test program:

Just Catching Exceptions

There is basically no portable way to get stack traces. However, if you’re just interested in catching the exceptions and not doing much more that printing an error message, there is a semi-portable c99 solution.

Here’s a simple implementation:

You can paste this into our test program and compile it with the usual: gcc main.c . If you need anything more than that, you’re going to have to stick with something specific for your platform.

This being C, there are of course loads of caveats. The C99 standard guarantees basically nothing about the signals. The only mechanism for invoking your signal handlers that is guarantee to work is calling raise() . (Note, the abort() function must call raise(SIGABRT) , and assert() must call abort() .)

If your signal occurs as a result of an abort() or raise() , then your code in your signal handler is just as safe as normal C code is…

If your signal occurs in any other way, the only interactions with the outside world guaranteed to be safe inside your signal handler are writes to values of type sig_atomic_t and calls to _Exit() .

This pretty much means the only “guaranteed safe” thing you can do is change the exit code. However, most implementations let you get away with quite a bit more.

Things to Note

Even though SIGFPE stands for “Signal: Floating Point Exception,” this signal is generated on pretty much any arithmetic exception (floating point or integer).

Don’t expect to be able to catch stack overflows. Often the signal handlers are invoked on the same stack that caused the signal to occur. So when a stack overflow occurs, your signal handler is called immediately, causing another stack overflow, and your program just dies with a segfault. On Posix systems there is a way around this.

For more details, see section 7.14 (page 246) of the C99 standard.

Catching Exceptions in Posix

One of the nice things about the Posix signal handler is that we can define an alternate signal stack that our signal handler will use. This allows us to catch and handle things like a stack overflow that would normally kill our handler instantly.

Unfortunately, we have to disable the signal stack on OS X, or backtrace() won’t work. Apparently linux does some magic that lets backtrace still look at the right stack when called from the signal handler that OS X lacks. Turning off the signal stack lets us get our stack traces, but it means our signal handler will fail for stack overflows. We’ll just get a normal segfault. I don’t know how to fix this. If you know a way around this please let me know. Most other errors should still be caught just fine, though.

Читайте также:  Замена ключа windows 10 при установке

Stack Traces in Posix

The backtrace() function is the preferred method of getting a stack trace on Posix systems. However, as you’ll see in a second, we’ll need a little extra processing to get to line numbers.

Something like the following works nicely:

#include #include /* Resolve symbol name and source location given the path to the executable and an address */ int addr2line(char const * const program_name, void const * const addr) < char addr2line_cmd[512] = <0>; /* have addr2line map the address to the relent line in the code */ #ifdef __APPLE__ /* apple does things differently. */ sprintf(addr2line_cmd,»atos -o %.256s %p», program_name, addr); #else sprintf(addr2line_cmd,»addr2line -f -p -e %.256s %p», program_name, addr); #endif /* This will print a nicely formatted string specifying the function and source line of the address */ return system(addr2line_cmd); >

You can compile all this with gcc -g main.c in linux, but you’ll need gcc -g -fno-pie main.c in OS X. (Pie does address randomizing to prevent code injection attacks. It also happens to prevent us from resolving addresses.)

Note that the addresses from stack traces are the return addresses. These lines are actually where the functions are going to return to — not where they are called from! Often it’s just the line after where they are called.

Catching Exceptions in Windows (MinGW)

As usual, there is the way everyone else does things, and the way Windows does things. In Windows, you can catch exceptions using the SetUnhandledExceptionFilter() function like so:

The windows_print_stacktrace() is defined in the next section.

Stack Traces in Windows (MinGW)

With MinGW, we can use the same addr2line() function that we defined earlier once we have an address:

#include #include void windows_print_stacktrace(CONTEXT* context) < SymInitialize(GetCurrentProcess(), 0, true); STACKFRAME frame = < 0 >; /* setup initial stack frame */ frame.AddrPC.Offset = context->Eip; frame.AddrPC.Mode = AddrModeFlat; frame.AddrStack.Offset = context->Esp; frame.AddrStack.Mode = AddrModeFlat; frame.AddrFrame.Offset = context->Ebp; frame.AddrFrame.Mode = AddrModeFlat; while (StackWalk(IMAGE_FILE_MACHINE_I386 , GetCurrentProcess(), GetCurrentThread(), &frame, context, 0, SymFunctionTableAccess, SymGetModuleBase, 0 ) ) < addr2line(icky_global_program_name, (void*)frame.AddrPC.Offset); >SymCleanup( GetCurrentProcess() ); >

You can compile the windows version with gcc -g main.c -limagehlp . And you can use the same addr2line() as with the Posix examples. Yay MinGW!

You can also get this to work with the Visual C compiler if you use the SymGetSymFromAddr64() function instead of the addr2line utility as demonstrated here. That post is about trying to get stack traces to work in MinGW, but the code should work in Visual Studio.

If you want to print out a stack trace without having an exception, you’ll have to get the local context with the RtlCaptureContext() function.

I also imagine that if you’re using cygwin, you can just stick with the Posix versions of everything, and things should just work. But I haven’t tried it.

Does it work!?

Sample output on Windows when I uncomment cause_segfault() :

Error: EXCEPTION_ACCESS_VIOLATION
cause_segfault at Z:\Projects\stack_traces/c_signal.c:428
cause_calamity at Z:\Projects\\stack_traces/c_signal.c:414
main at Z:\Projects\stack_traces/c_signal.c:398
?? at crt1.c:0
??
. 0
??
. 0

A compete collection of all the variants in a single .c file that can be built on Window, Linux and OS X is here.

I hope this saves someone else a few hours of time. Happy hacking 🙂

Источник

Оцените статью