Чому 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

14
Це здається, що книга, яку ви читаєте, не дуже гарна - хороша книга не повинна просто говорити, що щось подібне є "застарілим" (це фактично помилково, якщо автор не використовує слово, щоб описати власну думку), а слід пояснити, яке використання насправді недійсний і небезпечний, а не показує безпечний / дійсний код як приклад того, що ви "не повинні робити".
R .. GitHub СТОП ДОПОМОГАЙТЕСЯ

8
Чи можете ви ідентифікувати книгу?
Кіт Томпсон

7
Вкажіть, будь ласка, назву книги, автора та посилання на сторінку. Дякую.
Greenonline

Відповіді:


122

printf("Hello World!"); ІМХО не вразливий, але врахуйте це:

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/Uncontroll_format_string
e.dan

9
Ще одна причина - це puts, мабуть, швидше.
edmz

38
@black: puts"імовірно" швидше, і це, мабуть, ще одна причина, яку люди рекомендують, але насправді це не швидше. Я щойно надрукував "Hello, world!"1 000 000 разів, обома способами. З printfним пішло 0,92 секунди. З putsним пішло 0,93 секунди. Є питання, про який слід хвилюватися, коли справа стосується ефективності, але printfпорівняно puts- це не одна з них.
Стів Самміт

10
@KonstantinWeitz: Але (а) я не використовував gcc, і (b) не має значення, чому твердження " 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лише одного аргументу, а скоріше з рядком формату, керованим користувачем.
Хаген фон Ейтцен

2
@HagenvonEitzen Це технічно правда, але мало хто навмисно використовує наданий користувачем рядковий формат. Коли люди пишуть printf(p), це тому, що вони не розуміють, що це форматний рядок, вони просто думають, що вони друкують літерал.
Бармар

33

Надалі інші відповіді 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формати та умовні умови Yoda) сучасні компілятори попереджають про помилки, тому штучні правила ще менш важливі.
Стів Самміт

1
@Steve Якщо у використанні чогось є рівно нуль, але досить багато недоліків, то так, справді немає причин використовувати його. Yoda умови, з іншого боку робити мають недолік , що вони роблять код складніше читати (ви інтуїтивно сказати «якщо р дорівнює нулю» немає «якщо нуль р»).
Voo

2
@Voo printf("%s", "hello")буде повільніше printf("hello"), тому є і зворотний бік. Невеликий, оскільки IO майже завжди проходить повільніше, ніж таке просте форматування, але недоліком.
Якк - Адам Невраумон

1
@Yakk Я сумніваюся, що це буде повільніше
ММ

gcc -Wall -W -Werrorзапобіжить погані наслідки від таких помилок.
chqrlie

17

Я просто додам трохи інформації стосовно частини вразливості .

Кажуть, що вона вразлива через вразливість формату printf string. У вашому прикладі, коли рядок жорстко кодується, вона нешкідлива (навіть якщо рядки жорсткого кодування, як це, ніколи не рекомендується повністю). Але вказувати типи параметрів - це корисна звичка. Візьмемо цей приклад:

Якщо хтось вводить символ рядка формату у ваш 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 у помилках і Checker Framework .


12

Це неправильна порада. Так, якщо для друку є рядок,

printf(str);

досить небезпечно, і завжди слід користуватися

printf("%s", str);

натомість, тому що взагалі ви ніколи не можете знати, чи strможе містити %знак. Однак якщо у вас є постійний рядок часу компіляції , з цим нічого поганого немає

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

(Крім усього іншого, це найкласичніша програма С на будь-який час, буквально з книги програмування на С Genesis. Тож, хто зневажає, що використання є досить єретичним, і я на одного був би дещо ображений!)


because printf's first argument is always a constant stringЯ не зовсім впевнений, що ти з цим маєш на увазі.
Себастьян Мах

Як я вже сказав, "He has %d cherries\n"це постійний рядок, це означає, що це константа часу компіляції. Але, щоб бути справедливим, поради автора не було «не проходять постійні рядки як printf« s перший аргумент », він« не передавати рядки без %як printf»s перший аргумент.»
Стів Саміт

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

@Voo: Ну, скажемо, що не все, що вважається поганою практикою, насправді є поганою практикою. (Пора «ніколи не користуватися простим int» випливає з розуму.)
Стів Самміт

1
@Steve Я не знаю, де ти це чув, але це, звичайно, не така погана (погана?) Практика, про яку ми говоримо там. Не розумійте мене неправильно, на той час код був чудово, але ви дійсно не хочете дивитися на 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 Тестування динамічно генерованих рядків, здається, перешкоджає меті цього питання, хоча ... ОП, здається, цікавить лише друковані рядки.
користувач12205

1
Він не говорить про це прямо, навіть якщо в його прикладі використовуються рядкові літерали. Насправді, я вважаю, що його плутанина щодо порад книги є результатом використання рядкових літералів у прикладі. Що стосується рядкових буквених порад, поради щодо книг є якось сумнівними, а з динамічними рядками - гарною порадою.
Патрік Шлютер

1
/dev/nullначебто це робить іграшкою, тому що зазвичай при створенні форматованого виходу ваша мета полягає в тому, щоб результат кудись пішов, а не відкидався. Як тільки ви додасте час "фактично не відкидаючи дані", як вони порівнюються?
Якк - Адам Невраумон

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"), оскільки воно автоматично стане ставитим


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 починають і починають вивчати і використовувати мови С або навіть мови програмування взагалі з цією програмою HelloWorld і цим printfтвердженням як першими в своєму роді.

Вони не були б такими, якби вони застаріли.

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

Ну, тоді я б зосередив увагу на книзі чи самому авторі. Якщо автор справді робить такі, на мій погляд, невірні твердження і навіть навчаючи, що не пояснюючи чітко, чому він / вона робить це (якщо ці твердження справді буквально еквівалентні, передбачені в цій книзі), я вважав би це поганою книгою. Хороша книга, на відміну від цього, повинна пояснити , чому , щоб уникнути певного роду програмування методів або функцій.

Згідно з тим, що я говорив вище, використання printfлише одного аргументу (рядкового літералу) і без будь-яких специфікаторів формату ні в якому разі не застаріло і не вважається "поганою практикою" .

Ви повинні запитати автора, що він мав на увазі з тим, а ще краще, подумайте його, щоб уточнити чи виправити відповідний розділ для наступного видання чи відбитки взагалі.


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