W książce, którą czytam, jest napisane, że printf
pojedynczy argument (bez specyfikatorów konwersji) jest przestarzały. Zaleca się zastąpić
printf("Hello World!");
z
puts("Hello World!");
lub
printf("%s", "Hello World!");
Czy ktoś może mi powiedzieć, dlaczego printf("Hello World!");
się myli? W książce jest napisane, że zawiera luki. Co to za luki?
printf("Hello World!")
to nie to samo coputs("Hello World!")
.puts()
dołącza plik'\n'
. Zamiast tego porównajprintf("abc")
zfputs("abc", stdout)
printf
jest przestarzałe w taki sam sposób, jak na przykładgets
w C99, więc możesz rozważyć edycję pytania, aby było bardziej precyzyjne.Odpowiedzi:
printf("Hello World!");
czy IMHO nie jest wrażliwe, ale rozważ to:Jeśli
str
zdarzy się, że wskaże łańcuch zawierający%s
specyfikatory formatu, Twój program będzie wykazywał niezdefiniowane zachowanie (głównie awarię), podczas gdyputs(str)
po prostu wyświetli ciąg taki, jaki jest.Przykład:
źródło
puts
prawdopodobnie będzie to szybsze.puts
jest „prawdopodobnie” szybszy i prawdopodobnie jest to kolejny powód, dla którego ludzie go polecają, ale w rzeczywistości nie jest szybszy. Właśnie wydrukowałem"Hello, world!"
milion razy w obie strony. Dziękiprintf
temu zajęło to 0,92 sekundy. Dziękiputs
temu zajęło to 0,93 sekundy. Są rzeczy, o które należy się martwić, jeśli chodzi o wydajność, aleprintf
vs.puts
nie jest jednym z nich.puts
jest szybsze” jest fałszywe, nadal jest fałszywe.puts
z tego powodu. (Ale gdybyś chciał się o to spierać: byłbym zaskoczony, gdybyś znalazł jakikolwiek nowoczesny kompilator dla dowolnej nowoczesnej maszyny, w którejputs
jest znacznie szybszy niżprintf
w jakichkolwiek okolicznościach.)printf("Hello world");
jest w porządku i nie ma luki w zabezpieczeniach.
Problem polega na:
gdzie
p
jest wskaźnikiem do wejścia kontrolowanego przez użytkownika. Jest podatny na ataki typu string : użytkownik może wstawiać specyfikacje konwersji, aby przejąć kontrolę nad programem, np. W%x
celu zrzucenia pamięci lub%n
nadpisania pamięci.Zwróć na to uwagę
puts("Hello world")
w zachowaniu nie jest to równoważne z,printf("Hello world")
ale zprintf("Hello world\n")
. Kompilatory są zwykle na tyle sprytne, aby zoptymalizować to drugie wywołanie i zastąpić jeputs
.źródło
printf(p,x)
byłoby równie problematyczne, gdyby użytkownik miał nad nimi kontrolęp
. Tak więc problem nie polega na używaniuprintf
tylko jednego argumentu, ale raczej w przypadku ciągu formatu kontrolowanego przez użytkownika.printf(p)
, dzieje się tak, ponieważ nie zdają sobie sprawy, że jest to ciąg formatu, po prostu myślą, że drukują literał.W nawiązaniu do innych odpowiedzi,
printf("Hello world! I am 50% happy today")
jest to łatwy błąd do zrobienia, potencjalnie powodujący wszelkiego rodzaju nieprzyjemne problemy z pamięcią (to UB!).Po prostu prostsze, łatwiejsze i bardziej niezawodne jest „wymaganie” od programistów absolutnej jasności, gdy chcą otrzymać dosłowny ciąg znaków i nic więcej .
I to właśnie
printf("%s", "Hello world! I am 50% happy today")
cię dostaje. Jest całkowicie niezawodny.(Steve, oczywiście,
printf("He has %d cherries\n", ncherries)
to absolutnie nie to samo; w tym przypadku programista nie jest nastawiony na „ciąg znaków”; jest w nastawieniu „ciąg formatu”).źródło
printf
” jest prawie tak samo, jak powiedzenie „zawsze piszif(NULL == p)
. Te reguły mogą być przydatne dla niektórych programistów, ale nie dla wszystkich. W obu przypadkach (niezgodneprintf
formaty i warunki Yoda), nowoczesne kompilatory i tak ostrzegają przed błędami, więc sztuczne zasady są jeszcze mniej ważneprintf("%s", "hello")
będzie wolniejsze niżprintf("hello")
, więc jest wada . Mały, ponieważ IO jest prawie zawsze wolniejszy niż takie proste formatowanie, ale ma wadę.gcc -Wall -W -Werror
zapobiegnie złym konsekwencjom takich błędów.Dodam tutaj tylko trochę informacji dotyczących części dotyczącej luki .
Mówi się, że jest podatny na atak z powodu luki w formacie ciągu printf. W twoim przykładzie, gdzie ciąg jest zakodowany na stałe, jest nieszkodliwy (nawet jeśli ciągi takie jak ten nie są w pełni zalecane). Ale określanie typów parametrów jest dobrym nawykiem. Weź ten przykład:
Jeśli ktoś umieści znak ciągu formatu w twoim printf zamiast zwykłego łańcucha (powiedzmy, jeśli chcesz wydrukować program stdin), printf weźmie wszystko, co tylko może na stosie.
Był (i nadal jest) bardzo używany do wykorzystywania programów do przeszukiwania stosów w celu uzyskania dostępu do ukrytych informacji lub na przykład ominięcia uwierzytelniania.
Przykład (C):
jeśli wstawię jako dane wejściowe tego programu
"%08x %08x %08x %08x %08x\n"
To instruuje funkcję printf, aby pobrać pięć parametrów ze stosu i wyświetlić je jako 8-cyfrowe wypełnione liczbami szesnastkowymi. Możliwe wyjście może wyglądać następująco:
Zobacz to, aby uzyskać pełniejsze wyjaśnienie i inne przykłady.
źródło
Wywołanie
printf
przy użyciu ciągów formatu literału jest bezpieczne i wydajne, a istnieją narzędzia, które automatycznie ostrzegają użytkownika, jeśli wywołanieprintf
ciągów formatu dostarczonych przez użytkownika jest niebezpieczne.Najpoważniejsze ataki
printf
wykorzystują specyfikator%n
formatu. W przeciwieństwie do wszystkich innych specyfikatorów formatu, np%d
,%n
faktycznie zapisuje wartość do pamięci adres podany w jednym z argumentów wielkoformatowych. Oznacza to, że osoba atakująca może nadpisać pamięć, a tym samym potencjalnie przejąć kontrolę nad Twoim programem. Wikipedia zawiera więcej szczegółów.Jeśli wywołujesz
printf
z literalnym ciągiem formatu, osoba atakująca nie może wkraść się%n
do twojego ciągu formatu, a zatem jesteś bezpieczny. W rzeczywistości gcc zmieni twoje wywołanieprintf
na wywołanie doputs
, więc nie ma żadnej różnicy ( sprawdź to uruchamiającgcc -O3 -S
).Jeśli wywołujesz
printf
ciąg formatu dostarczony przez użytkownika, osoba atakująca może potencjalnie wkraść się%n
do ciągu formatu i przejąć kontrolę nad Twoim programem. Twój kompilator zwykle ostrzega cię, że jego jest niebezpieczny, widzisz-Wformat-security
. Istnieją również bardziej zaawansowane narzędzia, które zapewniają, że wywołanie programuprintf
jest bezpieczne nawet w przypadku ciągów formatu dostarczonych przez użytkownika, a nawet mogą one sprawdzać, czy przekazujesz odpowiednią liczbę i typ argumentówprintf
. Na przykład w przypadku Javy jest Google Error Prone i Checker Framework .źródło
To jest błędna rada. Tak, jeśli masz do wydrukowania ciąg czasu wykonywania,
jest dość niebezpieczny i zawsze należy go używać
zamiast tego, ponieważ generalnie nigdy nie wiadomo, czy
str
może zawierać%
znak. Jeśli jednak masz ciąg znaków stałych czasu kompilacji , nie ma w tym nic złego(Między innymi jest to najbardziej klasyczny program w C, jaki kiedykolwiek powstał, dosłownie z książki Genesis o programowaniu w C. Więc każdy, kto potępia to użycie, jest raczej heretycki, a ja byłbym trochę obrażony!)
źródło
because printf's first argument is always a constant string
Nie jestem do końca pewien, co masz na myśli."He has %d cherries\n"
jest ciągiem stałym, co oznacza, że jest to stała czasu kompilacji. Ale, żeby być uczciwym, rada autora nie brzmiała „nie podawaj stałych ciągów jakoprintf
pierwszego argumentu”, lecz „nie przekazuj ciągów bez%
jakoprintf
pierwszego argumentu”.literally from the C programming book of Genesis. Anyone deprecating that usage is being quite offensively heretical
- tak naprawdę nie czytałeś K&R w ostatnich latach. Jest tam mnóstwo porad i stylów kodowania, które są obecnie nie tylko przestarzałe, ale po prostu po prostu złe praktyki.int
”, przychodzi na myśl).Raczej nieprzyjemnym aspektem
printf
jest to, że nawet na platformach, na których zabłąkane odczyty pamięci mogą spowodować tylko ograniczoną (i akceptowalną) szkodę, jeden ze znaków formatujących%n
powoduje, że następny argument jest interpretowany jako wskaźnik do zapisywalnej liczby całkowitej i powoduje liczba wypisywanych dotychczas znaków, które mają być zapisane w zidentyfikowanej w ten sposób zmiennej. Nigdy sam nie korzystałem z tej funkcji, a czasami używam lekkich metod w stylu printf, które napisałem, aby zawierały tylko funkcje, których faktycznie używam (i nie zawierają tego ani niczego podobnego), ale dostarczają otrzymane standardowe ciągi funkcji printf z niewiarygodnych źródeł może ujawnić luki w zabezpieczeniach wykraczające poza możliwość odczytu dowolnej pamięci.źródło
Ponieważ nikt o tym nie wspomniał, dodałbym uwagę dotyczącą ich wydajności.
W normalnych okolicznościach, zakładając, że nie są używane żadne optymalizacje kompilatora (tj.
printf()
Faktycznie wywołuje,printf()
a niefputs()
), spodziewałbymprintf()
się działać mniej wydajnie, szczególnie w przypadku długich ciągów. Dzieje się tak, ponieważprintf()
musi przeanalizować ciąg, aby sprawdzić, czy istnieją jakiekolwiek specyfikatory konwersji.Aby to potwierdzić, przeprowadziłem kilka testów. Testowanie odbywa się na Ubuntu 14.04, z gcc 4.8.4. Mój komputer korzysta z procesora Intel i5. Testowany program wygląda następująco:
Oba są kompilowane z
gcc -Wall -O0
. Czas mierzy się za pomocątime ./a.out > /dev/null
. Poniżej przedstawiono wynik typowego przebiegu (uruchomiłem je pięć razy, wszystkie wyniki mieszczą się w ciągu 0,002 sekundy).Dla
printf()
wariantu:Dla
fputs()
wariantu:Ten efekt jest wzmacniany, jeśli masz bardzo długą strunę.
Dla
printf()
wariantu (uruchomiony trzykrotnie, rzeczywisty plus / minus 1,5 s):Dla
fputs()
wariantu (uruchomiony trzykrotnie, rzeczywisty plus / minus 0,2 s):Uwaga: Po sprawdzeniu zestawu wygenerowanego przez gcc zdałem sobie sprawę, że gcc optymalizuje
fputs()
wywołaniefwrite()
wywołania, nawet z-O0
. (printf()
Wywołanie pozostaje niezmienione.) Nie jestem pewien, czy spowoduje to unieważnienie mojego testu, ponieważ kompilator oblicza długość ciągu dlafwrite()
w czasie kompilacji.źródło
fputs()
często stosować stałych łańcuchowych i ta możliwość optymalizacji jest częścią momencie chciał make.This powiedział, dodając próbny z dynamicznie generowanych ciągiem zfputs()
ifprintf()
byłoby miło miejski punkt danych ./dev/null
w pewnym sensie sprawia to, że jest to zabawka, ponieważ zwykle podczas generowania sformatowanych danych wyjściowych celem jest, aby dane wyjściowe gdzieś trafiły, a nie zostały odrzucone. Gdy dodasz czas „faktycznie nie odrzuca danych”, jak się one porównują?automatycznie kompiluje się do odpowiednika
możesz to sprawdzić, demontując swój plik wykonywalny:
za pomocą
doprowadzi do problemów z bezpieczeństwem, nigdy nie używaj printf w ten sposób!
więc twoja książka jest rzeczywiście poprawna, używanie printf z jedną zmienną jest przestarzałe, ale nadal możesz używać printf ("mój ciąg \ n"), ponieważ automatycznie stanie się to puts
źródło
A compiles to B
, ale w rzeczywistości masz na myśliA and B compile to C
.W przypadku gcc możliwe jest włączenie określonych ostrzeżeń do sprawdzania
printf()
iscanf()
.Dokumentacja gcc stwierdza:
To,
-Wformat
co jest włączone w tej-Wall
opcji, nie włącza kilku specjalnych ostrzeżeń, które pomagają znaleźć te przypadki:-Wformat-nonliteral
ostrzeże, jeśli nie przekażesz łańcucha liter jako specyfikatora formatu.-Wformat-security
ostrzeże, jeśli przekażesz ciąg, który może zawierać niebezpieczną konstrukcję. To podzbiór-Wformat-nonliteral
.Muszę przyznać, że włączenie
-Wformat-security
ujawniło kilka błędów, które mieliśmy w naszej bazie kodu (moduł logowania, moduł obsługi błędów, moduł wyjściowy xml, wszystkie miały pewne funkcje, które mogłyby robić niezdefiniowane rzeczy, gdyby zostały wywołane z% znakami w swoim parametrze. nasza baza kodów ma teraz około 20 lat i nawet jeśli byliśmy świadomi tego rodzaju problemów, byliśmy bardzo zaskoczeni, gdy włączyliśmy te ostrzeżenia, ile z tych błędów nadal znajduje się w bazie kodu).źródło
Oprócz innych dobrze wyjaśnionych odpowiedzi, z uwzględnieniem wszelkich wątpliwości pobocznych, chciałbym udzielić dokładnej i zwięzłej odpowiedzi na zadane pytanie.
printf
Wywołanie funkcji za pomocą jednego argumentu w ogóle jest nie przestarzałe i również nie ma słabych punktów , gdy stosowane właściwie jak zawsze będą kodować.C Użytkownicy z całego świata, od początkujących do ekspertów zajmujących się statusem, używają
printf
tego sposobu, aby przekazać do konsoli prostą frazę tekstową.Co więcej, ktoś musi rozróżnić, czy ten jedyny argument jest literałem ciągu, czy wskaźnikiem do łańcucha, który jest prawidłowy, ale często nie jest używany. W tym ostatnim przypadku mogą oczywiście wystąpić niewygodne dane wyjściowe lub dowolny rodzaj niezdefiniowanego zachowania , gdy wskaźnik nie jest ustawiony prawidłowo, aby wskazywał na prawidłowy ciąg, ale te rzeczy mogą również wystąpić, jeśli specyfikatory formatu nie pasują do odpowiednich argumentów, dając wiele argumentów.
Oczywiście nie jest również właściwe i właściwe, aby łańcuch, podany jako jeden i jedyny argument, miał jakikolwiek specyfikator formatu lub konwersji, ponieważ konwersja nie będzie miała miejsca.
To powiedziawszy, podając prosty literał ciągu, taki jak
"Hello World!"
jako jedyny argument bez żadnych specyfikatorów formatu wewnątrz tego ciągu, tak jak podałeś go w pytaniu:nie jest przestarzały ani nie jest „ złą praktyką” ”, ani nie ma żadnych luk w zabezpieczeniach.
W rzeczywistości wielu programistów C zaczyna i zaczyna uczyć się i używać języka C, a nawet ogólnie języków programowania, z tym programem HelloWorld i tym
printf
oświadczeniem jako pierwszymi w swoim rodzaju.Nie byłyby takie, gdyby zostały wycofane.
Cóż, wtedy skupiłbym się na książce lub samym autorze. Jeśli autor naprawdę robi takie, moim zdaniem, błędne twierdzenia, a nawet naucza, że bez wyraźnego wyjaśnienia, dlaczego to robi (jeśli te twierdzenia są naprawdę dosłownie równoważne w tej książce), uznałbym ją za złą książkę. W przeciwieństwie do tego dobra książka powinna wyjaśniać, dlaczego należy unikać pewnego rodzaju metod lub funkcji programowania.
Zgodnie z tym, co powiedziałem powyżej, używanie
printf
tylko jednego argumentu (literału ciągu znaków) i bez żadnych specyfikatorów formatu nie jest w żadnym wypadku uznawane za przestarzałe ani uważane za „złą praktykę” .Powinieneś zapytać autora, co miał na myśli, a nawet lepiej, zwróć uwagę, aby wyjaśnił lub poprawił odpowiednią sekcję dla następnego wydania lub w ogóle.
źródło
printf("Hello World!");
jest to równoznaczne z iputs("Hello World!");
tak, co mówi coś o autorze rekomendacji.