Почему компиляторы не предупреждают о недопустимых индексах статических массивов? - PullRequest
19 голосов
/ 20 декабря 2008

Мой коллега недавно сильно укусил, записав границы в статический массив в стеке (он добавил в него элемент, не увеличивая размер массива). Разве компилятор не должен ловить такого рода ошибки? Следующий код корректно компилируется с помощью gcc, даже с параметрами -Wall -Wextra, и все же он явно ошибочен:

int main(void)
{
  int a[10];
  a[13] = 3;  // oops, overwrote the return address
  return 0;
}

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

Ответы [ 10 ]

27 голосов
/ 20 декабря 2008

GCC предупреждает об этом . Но вам нужно сделать две вещи:

  1. Включить оптимизацию. Без, по крайней мере, -O2, GCC не проводит достаточного анализа, чтобы понять, что такое a и что вы сбежали с края.
  2. Измените ваш пример так, чтобы фактически использовался [], в противном случае GCC генерирует неоперативную программу и полностью откажется от вашего назначения.

.

$ cat foo.c 
int main(void)
{
  int a[10];
  a[13] = 3;  // oops, overwrote the return address
  return a[1];
}
$ gcc -Wall -Wextra  -O2 -c foo.c 
foo.c: In function ‘main’:
foo.c:4: warning: array subscript is above array bounds

Кстати: если вы вернули [13] в своей тестовой программе, это тоже не сработало бы, так как GCC снова оптимизирует массив.

10 голосов
/ 20 декабря 2008

Вы пробовали -fmudflap с GCC? Это проверки во время выполнения, но они полезны, поскольку чаще всего вам все равно приходится иметь дело с вычисляемыми во время выполнения показателями. Вместо того, чтобы молча продолжать работать, он сообщит вам об этих ошибках.

-fmudflap -fmudflapth -fmudflapir Для интерфейсов, которые его поддерживают (C и C ++), инструмент все рискованно разыменование указателя / массива операции, некоторые стандартные строковые / кучевые функции библиотеки и некоторые другие связанные конструирует с тестами диапазона / валидности. Модули с инструментами должен быть защищен от переполнения буфера, недопустимого использования кучи и некоторых другие классы программирования C / C ++ ошибки. Инструмент tation полагается на отдельную библиотеку времени выполнения (libmudflap), которая будет связан с программой, если -fmudflap дается по ссылке время. Поведение инструментальной программы во время выполнения контролируется средой MUDFLAP_OPTIONS переменная. Смотри "env MUDFLAP_OPTIONS = -help a.out "для его параметров.

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

Вот то, что дает мне брызговик для вашего примера:

[js@HOST2 cpp]$ gcc -fstack-protector-all -fmudflap -lmudflap mudf.c        
[js@HOST2 cpp]$ ./a.out
*******
mudflap violation 1 (check/write): time=1229801723.191441 ptr=0xbfdd9c04 size=56
pc=0xb7fb126d location=`mudf.c:4:3 (main)'
      /usr/lib/libmudflap.so.0(__mf_check+0x3d) [0xb7fb126d]
      ./a.out(main+0xb9) [0x804887d]
      /usr/lib/libmudflap.so.0(__wrap_main+0x4f) [0xb7fb0a5f]
Nearby object 1: checked region begins 0B into and ends 16B after
mudflap object 0x8509cd8: name=`mudf.c:3:7 (main) a'
bounds=[0xbfdd9c04,0xbfdd9c2b] size=40 area=stack check=0r/3w liveness=3
alloc time=1229801723.191433 pc=0xb7fb09fd
number of nearby objects: 1
[js@HOST2 cpp]$

У него есть куча вариантов. Например, он может отключить процесс GDB при нарушениях, может показать вам, где произошла утечка вашей программы (используя -print-leaks) или обнаружить неинициализированные чтения переменных. Используйте MUDFLAP_OPTIONS=-help ./a.out, чтобы получить список опций. Поскольку mudflap выводит только адреса, а не имена файлов и строки источника, я написал небольшой скрипт gawk:

/^ / {
    file = gensub(/([^(]*).*/, "\\1", 1);
    addr = gensub(/.*\[([x[:xdigit:]]*)\]$/, "\\1", 1);
    if(file && addr) {
        cmd = "addr2line -e " file " " addr
        cmd | getline laddr
        print $0 " (" laddr ")"
        close (cmd)
        next;
    }
}

1 # print all other lines

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

Также -fstack-protector[-all]:

-fstack-protector Выдать дополнительный код для проверки переполнения буфера, например, атак с разбиванием стека. Это делается путем добавления переменной защиты к функциям с уязвимыми объектами. Это включает в себя функции, которые вызывают alloca, и функции с буферами, размер которых превышает 8 байтов. Защитные устройства инициализируются при входе в функцию, а затем проверяются при выходе из функции. Если контрольная проверка не пройдена, выводится сообщение об ошибке и программа завершается.

-fstack-protector-all Аналогично -fstack-protector за исключением того, что все функции защищены.

7 голосов
/ 20 декабря 2008

Вы правы, поведение не определено . Указатели C99 должны указывать внутри или только на один элемент за пределами объявленных или выделенных структур кучи данных.

Мне никогда не удавалось понять, как люди gcc решают, когда следует предупреждать. Я был потрясен, узнав, что -Wall сам по себе не будет предупреждать о неинициализированных переменных; как минимум вам нужно -O, и даже тогда предупреждение иногда опускается.

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

I секунда рекомендации valgrind . Если вы программируете на C, вы должны запускать valgrind для каждой программы все время , пока вы не сможете больше длится удар по производительности.

5 голосов
/ 20 декабря 2008

Это не статический массив.

Неопределенное поведение или нет, это запись по адресу 13 целых чисел от начала массива. За что вы несете ответственность? Существует несколько методов Си, которые намеренно неправильно выделяют массивы по разумным причинам. И эта ситуация не является чем-то необычным в неполных блоках компиляции.

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

Это путь Си. Это ваш массив, ваша память, делайте с ней что хотите. :)

(Существует множество инструментов lint, которые помогут вам подобного рода вещи; и вы должны использовать их свободно. Хотя не все они работают через компилятор; компиляция и компоновка часто бывают достаточно утомительными.)

4 голосов
/ 21 декабря 2008

Философия C заключается в том, что программист всегда прав . Таким образом, он молча позволит вам получить доступ к любому адресу памяти, который вы там даете, при условии, что вы всегда знаете, что делаете, и не будете беспокоить вас предупреждением.

4 голосов
/ 21 декабря 2008

Причина, по которой С этого не делает, заключается в том, что у С нет информации. Заявление типа

int a[10];

делает две вещи: он выделяет sizeof(int)*10 байтов пространства (плюс, возможно, небольшое мертвое пространство для выравнивания) и помещает запись в таблицу символов, которая концептуально читает

a : address of a[0]

или в терминах C

a : &a[0]

и все. Фактически, в C вы можете поменять *(a+i) на a[i] в (почти *) всех случаях без эффекта ПО ОПРЕДЕЛЕНИЮ. Таким образом, ваш вопрос эквивалентен вопросу «почему я могу добавить любое целое число к этому значению (адреса)?»

* Популярная викторина: какой единственный случай в этом, это не правда?

2 голосов
/ 20 декабря 2008

Я считаю, что некоторые компиляторы делают в определенных случаях. Например, если память мне не изменяет, у новых компиляторов Microsoft есть опция «Проверка безопасности буфера», которая обнаруживает тривиальные случаи переполнения буфера.

Почему не все компиляторы делают это? Либо (как упоминалось ранее) внутреннее представление, используемое компилятором, не поддается такому типу статического анализа, либо оно просто недостаточно высоко в списке приоритетов авторов. Что честно, в любом случае это позор.

2 голосов
/ 20 декабря 2008

не должен ли компилятор, по крайней мере, выдавать предупреждение?

Нет; Компиляторы C, как правило, не предварительно проверяют границы массивов. Как вы упоминаете, очевидным отрицательным эффектом является ошибка с неопределенным поведением, которую очень сложно найти.

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

0 голосов
/ 20 декабря 2008

-fbounds-check опция доступна с gcc.

стоит пройти через эту статью http://www.doc.ic.ac.uk/~phjk/BoundsChecking.html

«le dorfier» дал верный ответ на ваш вопрос, однако, это ваша программа и способ поведения C.

0 голосов
/ 20 декабря 2008

Для этого есть расширение в gcc (со стороны компилятора) http://www.doc.ic.ac.uk/~awl03/projects/miro/

с другой стороны, шину, крысу и многие другие инструменты для статического анализа кода нашел, что.

Вы также можете использовать valgrind для своего кода и посмотреть вывод. http://valgrind.org/

похоже, что другая широко используемая библиотека - libefence

Это просто дизайнерские решения, которые были приняты. Который сейчас приводит к этим вещам.

С уважением Friedrich

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