Почему printf с одним аргументом (без спецификаторов преобразования) устарел?


102

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

printf("Hello World!");

с участием

puts("Hello World!");

или

printf("%s", "Hello World!");

Может кто подскажет, почему printf("Hello World!");не так? В книге написано, что в нем есть уязвимости. Что это за уязвимости?


34
Примечание: printf("Hello World!")это не то же самое , как puts("Hello World!"). puts()добавляет '\n'. Вместо этого сравните printf("abc")сfputs("abc", stdout)
chux

5
Что это за книга? Я не думаю, что printfон устарел так же, как, например, getsне рекомендуется в C99, поэтому вы можете подумать о том, чтобы отредактировать свой вопрос, чтобы быть более точным.
el.pescado 09

14
Похоже, книга, которую вы читаете, не очень хороша - в хорошей книге не должно быть просто сказано, что что-то вроде этого «устарело» (это фактически неверно, если автор не использует это слово для описания своего собственного мнения), и в ней следует объяснить, какое использование на самом деле недействителен и опасен, а не показывает безопасный / действительный код в качестве примера того, что вы «не должны делать».
R .. GitHub НЕ ПОМОГАЕТ ICE

8
Вы можете опознать книгу?
Кейт Томпсон

7
Укажите название книги, автора и ссылку на страницу. Спасибо.
Greenonline

Ответы:


122

printf("Hello World!"); IMHO не уязвим, но учтите это:

const char *str;
...
printf(str);

Если strпроизойдет указание на строку, содержащую %sспецификаторы формата, ваша программа будет демонстрировать неопределенное поведение (в основном сбой), тогда puts(str)как строка будет отображаться как есть.

Пример:

printf("%s");   //undefined behaviour (mostly crash)
puts("%s");     // displays "%s\n"

21
Помимо сбоя программы, существует множество других уязвимостей, связанных с форматными строками. См. Здесь для получения дополнительной информации: en.wikipedia.org/wiki/Uncontrolled_format_string
e.dan

9
Другая причина в том puts, что предположительно будет быстрее.
edmz 08

38
@black: puts"предположительно" быстрее, и это, вероятно, еще одна причина, по которой его рекомендуют, но на самом деле это не так . Я только что напечатал "Hello, world!"1000000 раз в обе стороны. С printfэто ушло 0,92 секунды. С putsэто ушло 0,93 секунды. Когда дело доходит до эффективности, есть о чем беспокоиться, но printfvs. putsне входит в их число.
Стив Саммит

10
@KonstantinWeitz: Но (а) я не использовал gcc, и (б) не имеет значения, почему утверждение « putsбыстрее» ложно, оно все равно ложно.
Стив Саммит,

6
@KonstantinWeitz: Утверждение, которое я предоставил доказательством, было (противоположно) заявлению, которое делал пользователь Блэк. Я просто пытаюсь уточнить, что программисты не должны беспокоиться о звонках putsпо этой причине. (Но если вы хотите поспорить об этом: я был бы удивлен, если бы вы могли найти любой современный компилятор для любой современной машины, putsкоторая значительно быстрее, чем printfпри любых обстоятельствах.)
Стив Саммит

75

printf("Hello world");

в порядке и не имеет уязвимости в системе безопасности.

Проблема заключается в следующем:

printf(p);

где p- указатель на ввод, которым управляет пользователь. Он подвержен атакам на строки форматирования : пользователь может вставлять спецификации преобразования, чтобы получить контроль над программой, например, %xдля дампа памяти или %nперезаписи памяти.

Обратите внимание, что puts("Hello world")поведение не эквивалентно, printf("Hello world")но printf("Hello world\n"). Компиляторы обычно достаточно умны, чтобы оптимизировать последний вызов, чтобы заменить его puts.


10
Конечно, printf(p,x)было бы так же проблематично, если бы пользователь мог контролировать p. Таким образом, проблема заключается не в использовании printfодного аргумента, а в использовании строки формата, управляемого пользователем.
Hagen von Eitzen

2
@HagenvonEitzen Технически это правда, но немногие намеренно использовали бы строку формата, предоставленную пользователем. Когда люди пишут printf(p), это потому, что они не понимают, что это строка формата, они просто думают, что печатают литерал.
Barmar

34

В дополнение к другим ответам, printf("Hello world! I am 50% happy today")это простая ошибка, потенциально вызывающая всевозможные неприятные проблемы с памятью (это UB!).

Просто проще, проще и надежнее «требовать» от программистов, чтобы они были абсолютно ясны, когда им нужна дословная строка и ничего больше .

И это то, что printf("%s", "Hello world! I am 50% happy today")вас заводит. Это полностью надежно.

(Стив, конечно printf("He has %d cherries\n", ncherries), совершенно не то же самое; в данном случае программист не придерживается мышления «дословная строка»; она придерживается мышления «строка формата».)


2
Это не стоит спорить, и я понимаю, что вы говорите о мышлении дословно и форматной строки, но, ну, не все так думают, и это одна из причин, по которой универсальные правила могут раздражать. Сказать «никогда не печатать постоянные строки с помощью printf» - это примерно то же самое, что сказать «всегда писать» if(NULL == p). Эти правила могут быть полезны для некоторых программистов, но не для всех. И в обоих случаях (несоответствие printfформатов и условные выражения Йоды) современные компиляторы все равно предупреждают об ошибках, так что искусственные правила еще менее важны.
Стив Саммит

1
@Steve Если у использования чего-то ровно ноль плюсов, но есть немало минусов, то да, действительно нет причин использовать это. Yoda условия, с другой стороны делать имеют недостаток , что они делают код сложнее читать (вы интуитивно сказать « если р равно нулю» нет « если нуль р»).
Voo

2
@Voo printf("%s", "hello")будет медленнее printf("hello"), поэтому есть обратная сторона. Небольшой, потому что ввод-вывод почти всегда медленнее, чем такое простое форматирование, но это недостаток.
Yakk - Adam Nevraumont 09

1
@Yakk Я сомневаюсь, что это будет медленнее
MM

gcc -Wall -W -Werrorпредотвратит плохие последствия от таких ошибок.
chqrlie

17

Я просто добавлю здесь немного информации об уязвимости .

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

Если кто-то помещает символ строки формата в ваш printf вместо обычной строки (скажем, если вы хотите распечатать программу stdin), printf возьмет все, что сможет, в стек.

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

Пример (C):

int main(int argc, char *argv[])
{
    printf(argv[argc - 1]); // takes the first argument if it exists
}

если я поставлю как вход этой программы "%08x %08x %08x %08x %08x\n"

printf ("%08x %08x %08x %08x %08x\n"); 

Это дает указание функции printf извлечь пять параметров из стека и отобразить их в виде 8-значных шестнадцатеричных чисел с дополнениями. Таким образом, возможный результат может выглядеть так:

40012980 080628c4 bffff7a4 00000005 08059c04

Смотрите это для более полного объяснения и других примеров.


13

Вызов printfс использованием буквальных строк формата безопасен и эффективен, и существуют инструменты, которые автоматически предупреждают вас, если ваш вызов printfс использованием строк формата, предоставленных пользователем, небезопасен.

Самые серьезные атаки на printfиспользование %nспецификатора формата. В отличие от всех других спецификаторов формата, например %d, %nфактически записывает значение в адрес памяти, указанный в одном из аргументов формата. Это означает, что злоумышленник может перезаписать память и таким образом потенциально получить контроль над вашей программой. Википедия предоставляет более подробную информацию.

Если вы вызываете printfс буквальной строкой формата, злоумышленник не сможет проникнуть %nв вашу строку формата, и, таким образом, вы в безопасности. Фактически, gcc изменит ваш вызов printfна вызов puts, поэтому практически нет никакой разницы (проверьте это, запустив gcc -O3 -S).

Если вы вызываете printfс помощью предоставленной пользователем строки формата, злоумышленник потенциально может проникнуть %nв вашу строку формата и получить контроль над вашей программой. Ваш компилятор обычно предупреждает вас, что это небезопасно, см -Wformat-security. Существуют также более продвинутые инструменты, которые гарантируют, что вызов printfбезопасен даже с предоставленными пользователем строками формата, и они могут даже проверить, что вы передаете правильное количество и тип аргументов printf. Например, для Java есть Google's Error Prone и Checker Framework .


12

Это ошибочный совет. Да, если у вас есть строка времени выполнения для печати,

printf(str);

довольно опасно, и вы всегда должны использовать

printf("%s", str);

вместо этого, потому что, как правило, никогда нельзя узнать, strможет ли он содержать %знак. Однако, если у вас есть постоянная строка времени компиляции , нет ничего плохого в

printf("Hello, world!\n");

(Среди прочего, это самая классическая программа на языке C, буквально из книги по программированию на языке C в Genesis. Так что любой, кто не одобряет такое использование, является довольно еретическим, и я, например, был бы несколько оскорблен!)


because printf's first argument is always a constant stringЯ не совсем понимаю, что вы имеете в виду.
Себастьян Мах

Как я уже сказал, "He has %d cherries\n"это постоянная строка, что означает, что это константа времени компиляции. Но, честно говоря, совет автора заключался не в том, чтобы «не передавать константы в качестве printfпервого аргумента», а в том, чтобы «не передавать строки без первого аргумента %as printf».
Стив Саммит

literally from the C programming book of Genesis. Anyone deprecating that usage is being quite offensively heretical- вы фактически не читали K&R в последние годы. Там есть масса советов и стилей кодирования, которые в наши дни не только устарели, но и являются просто плохой практикой.
Voo

@Voo: Ну, давайте просто скажем, что не все, что считается плохой практикой, на самом деле является плохой практикой. (На intум приходит совет «никогда не использовать простые ».)
Стив Саммит,

1
@ Стив, я понятия не имею, где вы это слышали, но это определенно не та плохая (плохая?) Практика, о которой мы говорим. Не поймите меня неправильно, в то время код был в полном порядке, но в наши дни вы действительно не хотите смотреть на k & r как на историческую заметку. «Это в k & r» просто не показатель хорошего качества в наши дни, вот и все
Voo

9

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


8

Поскольку никто не упомянул, я бы добавил примечание об их производительности.

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

Чтобы подтвердить это, я провел несколько тестов. Тестирование проводится на Ubuntu 14.04 с gcc 4.8.4. Моя машина использует процессор Intel i5. Тестируемая программа выглядит следующим образом:

#include <stdio.h>
int main() {
    int count = 10000000;
    while(count--) {
        // either
        printf("qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM");
        // or
        fputs("qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM", stdout);
    }
    fflush(stdout);
    return 0;
}

Оба скомпилированы с использованием gcc -Wall -O0. Время измеряется с помощью time ./a.out > /dev/null. Ниже приведен результат типичного запуска (я запускал их пять раз, все результаты в пределах 0,002 секунды).

Для printf()варианта:

real    0m0.416s
user    0m0.384s
sys     0m0.033s

Для fputs()варианта:

real    0m0.297s
user    0m0.265s
sys     0m0.032s

Этот эффект усиливается, если у вас очень длинная струна.

#include <stdio.h>
#define STR "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM"
#define STR2 STR STR
#define STR4 STR2 STR2
#define STR8 STR4 STR4
#define STR16 STR8 STR8
#define STR32 STR16 STR16
#define STR64 STR32 STR32
#define STR128 STR64 STR64
#define STR256 STR128 STR128
#define STR512 STR256 STR256
#define STR1024 STR512 STR512
int main() {
    int count = 10000000;
    while(count--) {
        // either
        printf(STR1024);
        // or
        fputs(STR1024, stdout);
    }
    fflush(stdout);
    return 0;
}

Для printf()варианта (пробежал три раза, реальные плюс / минус 1,5 с):

real    0m39.259s
user    0m34.445s
sys     0m4.839s

Для fputs()варианта (запускался трижды, реальные плюс / минус 0,2 с):

real    0m12.726s
user    0m8.152s
sys     0m4.581s

Примечание: после проверки сборки, созданной gcc, я понял, что gcc оптимизирует fputs()вызов для fwrite()вызова даже с -O0. ( printf()Вызов остается неизменным.) Я не уверен, аннулирует ли это мой тест, поскольку компилятор вычисляет длину строки для fwrite()во время компиляции.


2
Это не сделает ваш тест недействительным, как fputs()это часто используется со строковыми константами, и эта возможность оптимизации является частью того, что вы хотели сделать. При этом добавление тестового прогона с динамически сгенерированной строкой с fputs()и fprintf()было бы хорошей дополнительной точкой данных. .
Патрик Шлютер

@ PatrickSchlüter Тестирование с динамически генерируемыми строками, похоже, противоречит цели этого вопроса ... OP, похоже, интересует только строковые литералы для печати.
user12205 09

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

1
/dev/nullотчасти делает это игрушкой, поскольку обычно при генерации форматированного вывода ваша цель состоит в том, чтобы вывод куда-то направлялся, а не был отброшен. Как они будут сравниваться после того, как вы добавите время «фактически не отбрасывая данные»?
Yakk - Adam Nevraumont 09

7
printf("Hello World\n")

автоматически компилируется в эквивалент

puts("Hello World")

вы можете проверить это с помощью дизассемблирования исполняемого файла:

push rbp
mov rbp,rsp
mov edi,str.Helloworld!
call dword imp.puts
mov eax,0x0
pop rbp
ret

с помощью

char *variable;
... 
printf(variable)

приведет к проблемам с безопасностью, никогда не используйте printf таким образом!

так что ваша книга на самом деле правильная, использование printf с одной переменной устарело, но вы все равно можете использовать printf ("моя строка \ n"), потому что она автоматически станет put


12
На самом деле это поведение полностью зависит от компилятора.
Jabberwocky

6
Это заблуждение. Вы утверждаете A compiles to B, а на самом деле имеете в виду A and B compile to C.
Себастьян Мах

6

Для gcc можно включить определенные предупреждения для проверки printf()и scanf().

В документации gcc указано:

-Wformatвходит в -Wall. Для большего контроля над некоторыми аспектами формата проверка, параметров -Wformat-y2k, -Wno-format-extra-args, -Wno-format-zero-length, -Wformat-nonliteral, -Wformat-security, и -Wformat=2доступны, но не включены в -Wall.

-Wformat, Который включен в -Wallопции не позволяет несколько специальных предупреждений , которые помогают найти эти случаи:

  • -Wformat-nonliteral выдаст предупреждение, если вы не передадите строковое буквенное обозначение в качестве спецификатора формата.
  • -Wformat-securityпредупредит, если вы передадите строку, которая может содержать опасную конструкцию. Это подмножество -Wformat-nonliteral.

Я должен признать, что включение -Wformat-securityвыявило несколько ошибок, которые у нас были в нашей кодовой базе (модуль регистрации, модуль обработки ошибок, модуль вывода xml, у всех были некоторые функции, которые могли бы делать неопределенные вещи, если бы они были вызваны с символами% в их параметре. Для информации, нашей кодовой базе сейчас около 20 лет, и даже если бы мы знали об этих проблемах, мы были чрезвычайно удивлены, когда включили эти предупреждения, сколько из этих ошибок все еще было в кодовой базе).


1

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


Почему printfиспользование одного аргумента (без спецификаторов преобразования) устарело?

printfВызов функции с одним аргументом в целом не устарела и не имеет также не уязвимостей при правильном использовании , как вы всегда должны кодировать.

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

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

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

Тем не менее, предоставление простого строкового литерала, такого "Hello World!"как единственный аргумент, без каких-либо спецификаторов формата внутри этой строки, как вы указали в вопросе:

printf("Hello World!");

не является устаревшим и не является " плохой практикой ", и не имеет никаких уязвимостей.

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

Их бы не было, если бы они были устаревшими.

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

Что ж, тогда я сосредоточился бы на книге или на самом авторе. Если автор действительно делает такие, на мой взгляд, неправильные утверждения и даже преподает это, не объясняя явно, почему он / она это делает (если эти утверждения действительно буквально эквивалентны приведенным в этой книге), я бы счел это плохой книгой. Хорошая книга, в отличие от этого, должна объяснить , почему , чтобы избежать определенного рода программирования методов или функций.

Согласно тому, что я сказал выше, использование printfтолько с одним аргументом (строковым литералом) и без каких-либо спецификаторов формата ни в коем случае не считается устаревшим и не считается «плохой практикой» .

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


Вы можете добавить, что printf("Hello World!");это не эквивалентно в puts("Hello World!");любом случае, что кое-что говорит об авторе рекомендации.
chqrlie
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.