Фон
Я писал код, который использует функции из ctype.h
для идентификации объектов в строках. Я случайно передал строку (char*
) функциям, которые принимают и int
печатают, что привело к сбою программы. Было достаточно легко увидеть, что я забыл разыменовать указатель строки, но GCC не выдал мне никаких предупреждений даже при компиляции со следующими аргументами:
gcc -o main main.c -Wall -Wextra -Werror -pedantic -pedantic-errors -std=c99 -Wconversion
Я использую Debian GNU/Linux bookworm 12.5 x86_64
и gcc (Debian 12.2.0-14) 12.2.0
, все актуально. Вот пример проблемы:
/* main.c */
#include <ctype.h>
#include <stdio.h>
int main(void)
{
char msg[] = "hello";
int res = isspace(msg); // char* gets cast to int without warning
// It should be `isspace(*msg)`
// This also segfaults
printf("%i\n", res);
return 0;
}
Вопросы
"Why does this even segfault in the first place?"
-- Поскольку аргумент функции isspace
должен находиться в диапазоне unsigned char
или иметь значение EOF
, в противном случае вызывается неопределенное поведение в соответствии с §7.4 ¶1 стандарта ISO C11.
2) Эти функции могут быть реализованы с помощью поиска по таблице. Значения int охватывают значительно больший диапазон, чем ожидаемые допустимые входные значения. Индексирование в массив с таким выходным значением может привести к сбою сегмента. «Поведение не определено, если значение ch не может быть представлено как беззнаковый символ и не равно EOF».
Я получаю ошибку error: incompatible pointer to integer conversion passing 'char[6]' to parameter of type 'int' [-Wint-conversion]
@Barmar: При использовании какого компилятора вы получаете это сообщение об ошибке?
лязг, как я сказал выше. @АндреасВензель
Теперь мы понимаем, почему здесь не следует задавать сразу два разных вопроса. Есть один ответ, который объясняет причину сбоя, а другой объясняет, почему нет предупреждения. Ты можешь принять только один, это выбор Софи.
Если вы напишете (isspace)(msg)
вместо isspace(msg)
, чтобы гарантировать, что вызывается функция, а не макрос, то компилятор gcc выдаст желаемое предупреждение/ошибку . См. §7.1.4 ¶1 стандарта ISO C11 предложение 5, где объясняется, почему это предотвращает вызов макроверсии функции.
Вы передаете значение, выходящее за пределы диапазона значений, ожидаемых функцией. Это вызывает неопределенное поведение, как указано в разделе 7.4p1 стандарта C относительно функций, определенных в ctype.h:
Заголовок <ctype.h> объявляет несколько функций, полезных для классификации. и отображение символов. Во всех случаях аргументом является
int
, значение которого должно быть представлено какunsigned char
или должно равно значению макросаEOF
. Если аргумент имеет любое другое значение, поведение неопределенное
А поскольку это неопределенное поведение, одним из возможных последствий является сбой.
Что касается того, почему компилятор не выдает предупреждение, нам нужно посмотреть на выходные данные препроцессора. Вызов isspace
после препроцессора преобразуется в следующее:
int res = ((*__ctype_b_loc ())[(int) ((msg))] & (unsigned short int) _ISspace);
Отсюда мы видим, что isspace
реализован как макрос, который использует таблицу поиска с данным аргументом в качестве индекса, и мы видим, что аргумент явно приводится к int
. Это явное приведение объясняет, почему нет предупреждения.
Вышеупомянутое также объясняет сбой, поскольку значение указателя, скорее всего, будет далеко за пределами этой таблицы поиска и, следовательно, попытается получить доступ к памяти, к которой у него нет доступа.
Библиотечные функции, реализованные в виде макросов, фактически соответствуют стандарту C. Кроме того, такие функции, определенные как макросы, также должны быть определены как реальные функции. Это продиктовано разделом 7.1.4p1 стандарта C:
Любая функция, объявленная в заголовке, может быть дополнительно реализована как функциональный макрос, определенный в заголовке, поэтому, если библиотечная функция объявляется явно при включении его заголовка, один из методов показано ниже, можно использовать, чтобы гарантировать, что на декларацию не повлияют такой макрос. Любое макроопределение функции может быть подавлено. локально, заключив имя функции в круглые скобки, потому что тогда за именем не следует левая скобка, указывающая расширение имени макрофункции. По той же синтаксической причине это разрешено брать адрес библиотечной функции, даже если он также определяется как макрос. 185)
- Это означает, что реализация должна обеспечивать фактическое функция для каждой библиотечной функции, даже если она также предоставляет макрос для этой функции.
Выше также упоминалось, что использование макроверсии функции можно запретить, поместив имя функции в круглые скобки:
int res = (isspace)(msg);
И в этом случае компилятор выдаст предупреждение о преобразовании указателя в целое число.
(На самом деле это не может быть такой формы, поскольку EOF
должно обрабатываться в дополнение ко всем значениям unsigned char
, но в макросе все же может быть какое-то приведение типов, которое эффективно обходит нарушение ограничений.)
@EricPostpischil Наверное, это что-то вроде c != EOF && Table[(unsigned char)c] & CTYPE_SPACE
@Barmar Кажется, в ctype.h
есть несколько вариантов в зависимости от разных определений. Один из них — #define isspace(c) __isctype((c), _ISspace)
, где __isctype
приводится c
к int
вот так: #define __isctype(c, type) ((*__ctype_b_loc ())[(int) (c)] & (unsigned short int) type)
@TedLyngmo "что-то вроде" - я хочу сказать, что он может проверять наличие EOF, прежде чем делать что-то, что может выйти за пределы массива.
@Barmar Да, я не имел в виду это как поправку. Я просто нырнул, чтобы посмотреть, как это выглядит.
@EricPostpischil Почему? c
и EOF
должны быть int
.
@EricPostpischil Но функция принимает int
, поэтому она преобразуется. Если это макрос, то он, вероятно, имеет (int)c != EOF
, как указано в другом ответе (перед этим ответом я написал свой первоначальный комментарий).
Скорее всего, в вашем компиляторе isspace()
реализован как макрос, включающий приведение типов любого аргумента, к которому он попадает char
или int
.
Очевидно, что когда компилятор увидит приведение типов, он просто предположит: «ну, он так сказал». Макросы вообще не проверяются по типу (ну тип указать нельзя, так как же компилятору его проверять).
Как уже отмечалось, проблема в том, что стандарт C позволяет реализовывать библиотечные функции в виде макросов. И что макросы, используемые стандартной библиотекой gcc, были плохо написаны с грязным внутренним приведением.
Аналогично isspace( &(bananas_t){} )
тоже проходит молча... не идеально.
В то время как пользователи качественной реализации ожидают, что функции/подобные функциям макросы будут вести себя в соответствии с правилами присваивания, которые не допускают таких неявных преобразований в int
.
Вместо этого они могли бы написать макрос LUT, показанный в ответе @dbush, вот так:
[
_Generic((msg),
char: (unsigned char)msg,
unsigned char: (unsigned char)msg,
int: (unsigned char)msg)
+ (msg)==EOF
]
Чтобы охватить все поддерживаемые целочисленные типы, но не более того. А также исправить присутствующую здесь вторую ошибку библиотеки, а именно запретить отрицательную индексацию в случае передачи отрицательного значения. Достойные реализации ctype.h, вероятно, приводят к unsigned char
внутри, а не вызывают ошибки сегмента UB просто так.
А затем, в случае EOF, мы получаем EOF
-> -1
-> 0xFFFFFFFF
, а затем приводим к unsigned char
= 0xFF.
Таким образом, часть + (msg)==EOF
дает нам номер индекса 256 в LUT в случае EOF
. Потому что делать ЛУТ размером 0xFFFFFFFF
— это совсем несексуально.
Теперь придется обойти это с точки зрения прикладного программиста:
Раздел библиотеки стандарта C (глава 7.1.4) содержит некоторые советы о том, как обращаться с подобными функциями.
Любой функция, объявленная в заголовке, может быть дополнительно реализована как макрос, подобный функции, определенный в заголовке, поэтому, если библиотечная функция объявлена явно при включении ее заголовка, один из методы, показанные ниже, можно использовать, чтобы гарантировать, что такой макрос не повлияет на объявление.
Любой макроопределение функции можно подавить локально, заключив имя функции в круглые скобки, потому что тогда за именем не следует левая скобка, указывающая на расширение имя макрофункции. По той же синтаксической причине допускается брать адрес библиотеки функция, даже если она также определена как макрос. Использование
#undef
для удаления любого определения макроса. также гарантирует, что имеется ссылка на реальную функцию.
Итак, возможные исправления могут выглядеть так:
#undef isspace
...
int res = isspace(msg);
Или
int res = (isspace)(msg);
Любой из них обеспечит вызов функции, а не макроса, подобного функции. И тогда компилятор должен оставить диагностическое сообщение о недопустимом преобразовании.
Мы также можем отметить, что хорошей практикой является всегда приводить аргумент, передаваемый в функции ctype.h, к unsigned char
, потому что мы не можем быть уверены, что библиотека делает это внутри себя так, как должна.
Похоже на ошибку. Я получаю ожидаемую фатальную ошибку от clang.