Каков самый простой стандартный способ создания Segfault в C?

Я думаю, что вопрос говорит сам за себя. Был бы полезен пример, охватывающий большинство стандартов от C89 до C11. Я думаю об этом, но я думаю, что это просто неопределенное поведение:

#include <stdio.h>

int main( int argc, char* argv[] )
{
  const char *s = NULL;
  printf( "%c\n", s[0] );
  return 0;
}

РЕДАКТИРОВАТЬ:

Поскольку некоторые голоса требовали разъяснений: я хотел, чтобы у программы была обычная программная ошибка (самое простое, о чем я мог подумать, - это segfault), которая (по стандарту) гарантированно прервалась. Это немного отличается от вопроса минимального сегфо, который не заботится об этой страховке.

11 ответов

Решение

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

Если мы посмотрим на неопределенное поведение проекта стандарта C99 §3.4.3, которое подпадает под раздел " Термины, определения и символы " в параграфе 1, в нем говорится (выделение мое в дальнейшем):

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

и в пункте 2 говорится:

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

Если, с другой стороны, вам просто нужен метод, определенный в стандарте, который вызовет ошибку сегментации в большинстве Unix-подобных систем, тогда raise(SIGSEGV) должен достичь этой цели. Хотя, строго говоря, SIGSEGV определяется следующим образом:

SIGSEGV неверный доступ к хранилищу

и §7.14 Обработка сигналов<signal.h> говорит:

Реализация не должна генерировать какие-либо из этих сигналов, кроме как в результате явных обращений к функции повышения. Дополнительные сигналы и указатели на необъявленные функции, с определениями макросов, начинающимися соответственно с букв SIG и заглавной буквы или с SIG_ и заглавной буквы 219), также могут быть определены реализацией. Полный набор сигналов, их семантика и обработка по умолчанию определяется реализацией; все номера сигналов должны быть положительными.

raise() можно использовать для поднятия сегфо:

raise(SIGSEGV);

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

Тем не менее, самый короткий способ вызвать ошибку сегментации на архитектурах, которые действительно генерируют такие ошибки, был бы:

int main()
{
    *(int*)0 = 0;
}

Почему это обязательно вызывает сегфо? Поскольку доступ к адресу памяти 0 всегда отслеживается системой; это никогда не может быть действительным доступом (по крайней мере, по коду пользовательского пространства.)

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

Правильная программа не производит segfault. И вы не можете описать детерминированное поведение неверной программы.

"Ошибка сегментации" - это то, что делает процессор x86. Вы получаете это, пытаясь ссылаться на память неверным способом. Это может также относиться к ситуации, когда доступ к памяти вызывает сбой страницы (то есть, пытается получить доступ к памяти, которая не загружена в таблицы страниц), и ОС решает, что вы не имеете права запрашивать эту память. Чтобы вызвать эти условия, вам нужно программировать непосредственно для вашей ОС и вашего оборудования. Это ничего, что указано языком Си.

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

Но это самое короткое, что я могу получить в этой системе.

main(){main();}

(Я собираю с gcc а также -std=c89 -O0).

И, кстати, действительно ли эта программа вызывает неопределенное поведение?

 main;

Вот и все.

В самом деле.

По сути, то, что это делает, это определяет main как переменная. В Си переменные и функции являются символами - указателями в памяти, поэтому компилятор не различает их, и этот код не выдает ошибку.

Однако проблема заключается в том, как система запускает исполняемые файлы. Короче говоря, стандарт C требует, чтобы все исполняемые файлы C имели встроенную в них точку входа для подготовки среды, которая в основном сводится к вызову main".

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

Чтобы проиллюстрировать, вот (часть) objdump результирующего файла:

# (unimportant)

Disassembly of section .text:

0000000000001020 <_start>:
    1020:   f3 0f 1e fa             endbr64 
    1024:   31 ed                   xor    %ebp,%ebp
    1026:   49 89 d1                mov    %rdx,%r9
    1029:   5e                      pop    %rsi
    102a:   48 89 e2                mov    %rsp,%rdx
    102d:   48 83 e4 f0             and    $0xfffffffffffffff0,%rsp
    1031:   50                      push   %rax
    1032:   54                      push   %rsp
    1033:   4c 8d 05 56 01 00 00    lea    0x156(%rip),%r8        # 1190 <__libc_csu_fini>
    103a:   48 8d 0d df 00 00 00    lea    0xdf(%rip),%rcx        # 1120 <__libc_csu_init>

    # This is where the program should call main
    1041:   48 8d 3d e4 2f 00 00    lea    0x2fe4(%rip),%rdi      # 402c <main> 
    1048:   ff 15 92 2f 00 00       callq  *0x2f92(%rip)          # 3fe0 <__libc_start_main@GLIBC_2.2.5>
    104e:   f4                      hlt    
    104f:   90                      nop

# (nice things we still don't care about)

Disassembly of section .data:

0000000000004018 <__data_start>:
    ...

0000000000004020 <__dso_handle>:
    4020:   20 40 00                and    %al,0x0(%rax)
    4023:   00 00                   add    %al,(%rax)
    4025:   00 00                   add    %al,(%rax)
    ...

Disassembly of section .bss:

0000000000004028 <__bss_start>:
    4028:   00 00                   add    %al,(%rax)
    ...

# main is in .bss (variables) instead of .text (code)

000000000000402c <main>:
    402c:   00 00                   add    %al,(%rax)
    ...

# aaand that's it! 

PS: это не сработает, если вы скомпилируете в плоский исполняемый файл. Вместо этого вы будете вызывать неопределенное поведение.

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

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

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

SIGSEGV сигнал может быть поднят явно, но нет SIGSEGV символ в стандартной библиотеке C.

(В этом ответе "соответствующий стандарту" означает: "Используются только функции, описанные в некоторой версии стандарта ISO C, избегая неопределенного, определяемого реализацией или неопределенного поведения, но не обязательно ограниченного минимальными пределами реализации".)

Самая простая форма с учетом наименьшего количества символов:

++*(int*)0;

В большинстве ответов на этот вопрос обсуждается ключевой момент, а именно: стандарт C не включает концепцию ошибки сегментации. (Начиная с C99 он включает номер сигнала SIGSEGV, но он не определяет каких-либо обстоятельств, когда этот сигнал доставляется, кроме raise(SIGSEGV)что, как обсуждалось в других ответах, не считается.)

Следовательно, не существует "строго соответствующей" программы (т. Е. Программы, использующей только конструкции, поведение которых полностью определяется стандартом C, один), который гарантированно вызовет ошибку сегментации.

Ошибки сегментации определяются другим стандартом, POSIX. Эта программа гарантированно провоцирует либо ошибку сегментации, либо функционально эквивалентную "ошибку шины" (SIGBUS), в любой системе, полностью соответствующей POSIX.1-2008, включая параметры защиты памяти и расширенного реального времени, при условии, что вызовы sysconf, posix_memalign а также mprotect добиться успеха. Мое чтение C99 - то, что у этой программы есть поведение, определяемое реализацией (не неопределенное!), Учитывающее только этот стандарт, и поэтому оно соответствует, но не строго соответствует.

#define _XOPEN_SOURCE 700
#include <sys/mman.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>

int main(void)
{
    size_t pagesize = sysconf(_SC_PAGESIZE);
    if (pagesize == (size_t)-1) {
        fprintf(stderr, "sysconf: %s\n", strerror(errno));
        return 1;
    }
    void *page;
    int err = posix_memalign(&page, pagesize, pagesize);
    if (err || !page) {
        fprintf(stderr, "posix_memalign: %s\n", strerror(err));
        return 1;
    }
    if (mprotect(page, pagesize, PROT_NONE)) {
        fprintf(stderr, "mprotect: %s\n", strerror(errno));
        return 1;
    }
    *(long *)page = 0xDEADBEEF;
    return 0;
}

Трудно определить метод ошибки сегментации программы на неопределенных платформах. Ошибка сегментации - это свободный термин, который не определен для всех платформ (например, простых небольших компьютеров).

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

Кроме того, ограничение операционных систем "Unix-подобными" ОС, надежный способ для процесса, чтобы получить сигнал SIGSEGV kill(getpid(),SIGSEGV)

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

Но, чтобы быть практичным, текущая операционная система Mac, Linux и Win будет работать на

*(int*)0 = 0;

Кроме того, поведение segfault не является плохим поведением. Некоторые реализации assert() вызвать сигнал SIGSEGV, который может создать файл ядра. Очень полезно, когда нужно вскрыть.

Что хуже, чем вызывать сегфоут, это скрывать:

try
{
     anyfunc();
}
catch (...) 
{
     printf("?\n");
}

которая скрывает источник ошибки, и все, что вам нужно сделать, это:

?

,

Вот еще один способ, который я не видел здесь:

      int main() {
    void (*f)(void);
    f();
}

В этом случаеfявляется неинициализированным указателем на функцию, который вызывает ошибку сегментации при попытке его вызова.

Другие вопросы по тегам