Какова основная разница между printf (s) и printf ("%s", s)?

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

char* s = "abcdefghij\n";
printf(s);

// Warning raised with gcc -std=c11: 
// format not a string literal and no format arguments [-Wformat-security]

// On the other hand, if I use 

char* s = "abc %d efg\n";
printf(s, 99);

// I get no warning whatsoever, why is that?

// Update, I've tested this:
char* s = "random %d string\n";
printf(s, 99, 50);

// Results: no warning, output "random 99 string".

так в чем же разница между printf(s) и printf("%s", s) и почему я получаю предупреждение в одном случае?

5 ответов


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

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

во-первых, это уязвимость 'format string'. Вы можете найти дополнительную информацию по этой теме.

GCC знает, что в большинстве случаев сингл аргумент printf() С не-символьная строка формата-это приглашение к неприятностям. Вы могли бы использовать puts() или fputs() вместо. Достаточно опасно, что GCC генерирует предупреждения с минимумом провокации.

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

из комментариев:

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

из трех printf() операторы, учитывая жесткий контекст, что переменная s как назначено непосредственно над вызовом, фактической проблемы нет. Но вы могли бы использовать puts(s) если вы опустили строки из строки или fputs(s, stdout) как и получить тот же результат, без издержек printf() разбор всей строки, чтобы узнать, что все это простые символы для печати.

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

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

С printf() спецификация, связанная с вверху:

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

во всех этих случаях нет сильного указания на то, почему строка формата не является литералом. Однако одной из причин, по которой требуется строка не-литерального формата, может быть то, что иногда вы печатаете числа с плавающей запятой в %f обозначения и иногда в %e нотация, и вам нужно выбрать, что во время выполнения. (Если он просто основан на значении,%g может быть уместно, но там бывают случаи, когда вы хотите явный контроль-всегда %e или %f.)


предупреждение говорит все это.

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

Итак, a пылесос (или безопаснее) подход (печать строки, которая не нуждается в спецификации формата) будет puts(s); над printf(s); (первый не обрабатывает s для любых спецификаторов преобразования, удаляя причину возможного UB в более позднем случае). Вы можете выбрать fputs(), если вы беспокоитесь о конечной новой строке, которая автоматически добавляется в puts().


что сказал, относительно опции предупреждения,-Wformat-security из онлайн-gcc руководство

в настоящее время, это предупреждает о призывах к printf и scanf функции, где строка формата не является строковым литералом и нет аргументов формата, как в printf (foo);. Это может быть дыра в безопасности, если строка формата пришла из ненадежного ввода и содержит %n.

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

во втором случае прилагаемый аргумент предоставляется, спецификатор формата не является только аргумент передан printf(), поэтому первый аргумент не нужно проверять. Следовательно, предупреждение не там.


обновление:

относительно третьего, с лишние аргумент, который требуется для поставляемой строки формата

printf(s, 99, 50);

цитирую C11 глава §7.21.6.1

[...] Если формат исчерпан, а аргументы остаются, избыточные аргументы оценивается (как всегда), но в остальном игнорируется. [...]

так, проходя мимо лишние


в вашем вопросе есть две вещи.

первый кратко покрывается Джонатан Леффлер - предупреждение, которое вы получаете, потому что строка не является литералом и в ней нет спецификаторов формата.

другая загадка заключается в том, почему компилятор не выдает предупреждение о том, что ваше количество аргументов не соответствует числу спецификаторов. Короткий ответ: "потому что это не так", но более конкретно, printf-это вариационная функция. Он принимает любое количество аргументов после начальной спецификации формата-от 0 и выше. Компилятор не может проверить, дали ли вы правильную сумму; это зависит от самой функции printf и приводит к неопределенному поведению, которое Иоахим упомянул в комментариях.

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

в чем разница между printf(s) и printf("%s", s)? Простой - в последнем случае вы используете printf, как он объявлен. "%s" это const char *, и он впоследствии не будет генерировать предупреждающее сообщение.

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

ваша проблема может быть решена в несколько направлений.

const char* s = "abcdefghij\n";
printf(s);

разрешит предупреждение, потому что теперь вы используете указатель const, и нет никаких опасностей, о которых упоминал Джонатан. (Вы также можете объявить его как const char* const s, но не обязательно. Первый const важно, потому что он затем соответствует объявлению printf, а потому const char* s означает, что символы, на которые указывает s, не могут изменяться, т. е. строка является литералом.)

или, еще проще, просто сделать:

printf("abcdefghij\n");

этот неявно указатель const, а также не проблема.


причина: printf объявляется как:

int printf(const char *fmt, ...) __attribute__ ((format(printf, 1, 2)));

Это говорит gcc, что printf - это функция с интерфейсом в стиле printf, где строка формата приходит первой. ИМХО это должно быть буквальным; я не думаю, что есть способ сказать хорошему компилятору, что s на самом деле является указателем на литеральную строку, которую он видел раньше.

подробнее о __attribute__ здесь.


Итак, какова основная разница между printf (s) и printf ("%s", s)

" printf (s)" будет рассматривать s как строку формата. Если s содержит спецификаторы формата, printf интерпретирует их и ищет varargs. Поскольку ни с varargs на самом деле существует этот, вероятно, вызовет неопределенное поведение.

если злоумышленник управляет "s", то это, вероятно, будет дыра в безопасности.

printf ("%s", s) просто напечатает то, что находится в строке.

и почему я получаю предупреждение в одном случае?

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

C программисты находятся в habbit использования printf и различных printf как функции* в качестве общих функций печати, даже если они на самом деле не нуждаются в форматировании. В этой среде для кого-то легко сделать ошибку написания printf(s), не думая о том, откуда s пришел. С форматирование довольно бесполезно без каких-либо данных для форматирования printf(S) имеет мало законного использования.

printf (s,format, arguments) с другой стороны, указывает, что программист намеренно намеревался форматировать.

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

* как стандартные функции C, такие как sprintf и fprintf, так и функции в третьем партийные библиотеки.