Tek bağımsız değişkenli (dönüşüm belirteçleri olmadan) printf neden kullanımdan kaldırıldı?


102

Okuduğum bir kitapta, printftek bir argümanın (dönüşüm belirteçleri olmadan) kullanımdan kaldırıldığı yazılıyor . Değiştirilmesini önerir

printf("Hello World!");

ile

puts("Hello World!");

veya

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

Birisi bana neden printf("Hello World!");yanlış olduğunu söyleyebilir mi? Güvenlik açıkları içerdiği kitapta yazılmıştır. Bu güvenlik açıkları nelerdir?


34
Not: printf("Hello World!")ile aynı değildirputs("Hello World!") . puts()ekler a '\n'. Bunun yerine karşılaştırmak printf("abc")içinfputs("abc", stdout)
chux - Eski Monica

5
O kitap ne? printfÖrneğin getsC99'da kullanımdan kaldırılanla aynı şekilde kullanımdan kaldırıldığını düşünmüyorum , bu nedenle sorunuzu daha kesin olacak şekilde düzenlemeyi düşünebilirsiniz.
el.pescado

14
Görünüşe göre okuduğunuz kitap pek iyi değil - iyi bir kitap sadece böyle bir şeyin "kullanımdan kaldırıldığını" söylememeli (yazar kendi fikrini açıklamak için kelimeyi kullanmadığı sürece bu gerçekte yanlıştır) ve hangi kullanımı açıklamalıdır güvenli / geçerli kodu "yapmamanız gereken" bir şeye örnek olarak göstermek yerine aslında geçersiz ve tehlikelidir.
R .. GitHub BUZA YARDIM ETMEYİ DURDUR

8
Kitabı tanımlayabilir misin?
Keith Thompson

7
Lütfen kitabın başlığını, yazarını ve sayfa referansını belirtin. Teşekkürler.
Greenonline

Yanıtlar:


122

printf("Hello World!"); IMHO savunmasız değil mi, ancak şunu düşünün:

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

Biçim belirteçleri striçeren bir dizgeye işaret ederse %s, programınız tanımlanmamış davranış sergileyecek (çoğunlukla bir çökme), ancak puts(str)dizeyi olduğu gibi gösterecektir.

Misal:

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

21
Programın çökmesine neden olmanın yanı sıra, biçim dizeleri ile olası başka birçok açık vardır. Daha fazla bilgi için buraya bakın: en.wikipedia.org/wiki/Uncontrolled_format_string
e.dan

9
Diğer bir neden putsde muhtemelen daha hızlı olacak.
edmz

38
@black: puts"muhtemelen" daha hızlıdır ve bu muhtemelen insanların tavsiye etmelerinin başka bir nedenidir, ancak aslında daha hızlı değildir . Her "Hello, world!"iki şekilde de 1.000.000 kez bastım. Onunla printf0.92 saniye sürdü. Onunla puts0.93 saniye sürdü. Orada o verimlilik söz konusu olduğunda yaklaşık endişe şeyler vardır, ama printfvs putsbunlardan biri değildir.
Steve Summit

10
@KonstantinWeitz: Ama (a) gcc kullanmıyordum ve (b) " daha hızlı" iddiasının nedenputs yanlış olduğu önemli değil, yine de yanlış.
Steve Summit

6
@KonstantinWeitz: Kanıt sağladığım iddia, siyah kullanıcısının yaptığı iddiaydı (tam tersi). Programcıların putsbu nedenle arama konusunda endişelenmemesi gerektiğini açıklamaya çalışıyorum . (Ama bunun hakkında tartışmak isterseniz: Herhangi bir modern makine için herhangi bir koşuldan putsçok daha hızlı olan herhangi bir modern derleyici bulabilirseniz şaşırırdım printf.)
Steve Summit

75

printf("Hello world");

iyidir ve güvenlik açığı yoktur.

Sorun şununla yatıyor:

printf(p);

pkullanıcı tarafından kontrol edilen bir girdiye işaretçi nerede . Dizge saldırılarını biçimlendirme eğilimindedir : kullanıcı, programın kontrolünü ele geçirmek için dönüştürme özellikleri ekleyebilir, ör. %xBellek dökümü veya%n belleğin üzerine yazmak .

Not puts("Hello world")için davranış eşdeğer değildir printf("Hello world")ancak printf("Hello world\n"). Derleyiciler genellikle ikinci çağrıyı onunla değiştirecek şekilde optimize edecek kadar akıllıdır puts.


10
Elbette printf(p,x), kullanıcının kontrolü varsa, aynı sorunlu olacaktır p. Problemdir Yani değil kullanımı printfsadece bir bağımsız değişkenle ziyade kullanıcı tarafından kontrol edilen biçim dizesi ile.
Hagen von Eitzen

2
@HagenvonEitzen Bu teknik olarak doğru, ancak çok azı kasıtlı olarak kullanıcı tarafından sağlanan bir biçim dizesi kullanır. İnsanlar yazdıklarında printf(p), bunun bir biçim dizisi olduğunun farkında olmadıkları için, sadece birebir yazdırdıklarını düşünürler.
Barmar

34

Diğer yanıtların yanı sıra, printf("Hello world! I am 50% happy today")yapılması kolay bir hatadır ve potansiyel olarak her türden kötü bellek sorunlarına neden olur (UB!).

Programcıların kelimesi kelimesine bir dizgi isterken ve başka hiçbir şey istemediklerinde kesinlikle net olmalarını "istemek" daha basit, daha kolay ve daha güçlüdür .

Ve bu ne printf("%s", "Hello world! I am 50% happy today") seni anlayan bu. Tamamen kusursuz.

(Steve, elbette printf("He has %d cherries\n", ncherries)kesinlikle aynı şey değildir; bu durumda, programcı "kelimesi kelimesine dizgi" zihniyetinde değildir; "biçim dizgisi" zihniyetindedir.)


2
Bu bir tartışmaya değmez ve kelimesi kelimesine ve format dizgisi zihniyetiyle ilgili ne söylediğini anlıyorum, ama, pekala, herkes böyle düşünmüyor, bu da herkese uyan tek bir kuralın rütbe almasının bir nedenidir. "Asla sabit dizeleri basmayın printf" demek, tam olarak "her zaman yaz" demek gibidir if(NULL == p). Bu kurallar bazı programcılar için yararlı olabilir, ancak hepsi için değil. Her iki durumda da (uyumsuz printfformatlar ve Yoda koşullu), modern derleyiciler yine de hatalar konusunda uyarıyor, bu yüzden yapay kurallar daha da az önemli.
Steve Summit

1
@Steve Bir şeyi kullanmanın tam olarak sıfır avantajı varsa, ancak birkaç dezavantajı varsa, o zaman evet onu kullanmak için gerçekten bir neden yoktur. Öte yandan Yoda koşulları do onlar (değil "p sıfırsa" "sıfır p ise" sezgisel olarak söyleyebilirim) okumak için kod zorlaştırabilir o dezavantajı var.
Voo

2
@Voo printf("%s", "hello")daha yavaş olacak printf("hello"), bu yüzden bir dezavantajı var. Küçük, çünkü IO neredeyse her zaman bu kadar basit biçimlendirmeden çok daha yavaş ama bir dezavantajı.
Yakk - Adam Nevraumont

1
@Yakk Daha yavaş olacağından şüpheliyim
MM

gcc -Wall -W -Werrorbu tür hatalardan kaynaklanan kötü sonuçları önleyecektir.
chqrlie

17

Güvenlik açığı ile ilgili biraz bilgi ekleyeceğim kısmıyla .

Printf string format güvenlik açığı nedeniyle savunmasız olduğu söyleniyor. Örneğinizde, dizenin kodlanmış olduğu yerde, zararsızdır (bunun gibi dizelerin sabit kodlanması hiçbir zaman tam olarak önerilmese bile). Ancak parametrenin türlerini belirtmek iyi bir alışkanlıktır. Bu örneği ele alalım:

Eğer birisi printf'inize normal bir dizge yerine biçim dizesi karakteri koyarsa (örneğin, stdin programını yazdırmak istiyorsanız), printf yığın üzerinde alabildiği her şeyi alır.

Örneğin, gizli bilgilere erişmek veya kimlik doğrulamayı atlamak için programları istismar ederek yığınları keşfetmek için çok kullanılıyordu (ve hala da kullanılıyor).

Örnek (C):

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

bu programın girdisi olarak koyarsam "%08x %08x %08x %08x %08x\n"

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

Bu, printf işlevine yığından beş parametre alması ve bunları 8 basamaklı doldurulmuş onaltılık sayılar olarak görüntülemesi talimatını verir. Dolayısıyla olası bir çıktı şöyle görünebilir:

40012980 080628c4 bffff7a4 00000005 08059c04

Bkz bu daha tam bir açıklama ve diğer örnekler için.


13

printfDeğişmez biçim dizeleri ile arama yapmak güvenli ve etkilidir ve printfkullanıcı tarafından sağlanan biçim dizeleri ile yaptığınız çağrı güvenli değilse sizi otomatik olarak uyaran araçlar vardır .

En şiddetli saldırılar printf, %nbiçim belirleyiciden yararlanır. Diğer tüm biçim belirleyicilerinin aksine, örneğin %d, %nbiçim bağımsız değişkenlerinden birinde sağlanan bir bellek adresine aslında bir değer yazar. Bu, bir saldırganın belleğin üzerine yazabileceği ve böylece potansiyel olarak programınızın kontrolünü ele geçirebileceği anlamına gelir. Wikipedia daha fazla ayrıntı sağlar.

printfBirebir biçim dizesi ile çağırırsanız , bir saldırgan %nbiçim dizenize gizlice giremez ve bu nedenle güvendesiniz. Aslında gcc, aramanızı yapılacak printfbir çağrıya dönüştürecektir puts, bu nedenle herhangi bir farklılık yoktur (bunu,gcc -O3 -S ).

printfKullanıcı tarafından sağlanan bir biçim dizesiyle arama yaparsanız , bir saldırgan potansiyel olarak %nbiçim dizenize gizlice girebilir ve programınızın kontrolünü ele geçirebilir. Derleyiciniz genellikle güvensiz olduğu konusunda sizi uyaracaktır, bakın -Wformat-security. Ayrıca, printfkullanıcı tarafından sağlanan biçim dizeleri ile bile bir çağrının güvenli olmasını sağlayan daha gelişmiş araçlar da vardır ve bunlar, doğru sayıda ve türde bağımsız değişkenleri ilettiğinizi bile kontrol edebilir printf. Örneğin, Java için Google'ın Error Prone ve Checker Framework vardır .


12

Bu yanlış bir tavsiye. Evet, yazdırılacak bir çalışma zamanı dizeniz varsa,

printf(str);

oldukça tehlikelidir ve her zaman kullanmalısınız

printf("%s", str);

bunun yerine, çünkü genel strolarak bir %işaret içerip içermediğini asla bilemezsiniz . Bununla birlikte, derleme zamanı sabit bir dizeniz varsa ,

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

(Diğer şeylerin yanı sıra, bu şimdiye kadarki en klasik C programı, kelimenin tam anlamıyla Genesis'in C programlama kitabından. Bu nedenle, bu kullanımı reddeden herkes oldukça sapkın davranıyor ve ben biri için biraz kırılırım!)


because printf's first argument is always a constant stringBununla ne demek istediğinden tam olarak emin değilim.
Sebastian Mach

Dediğim gibi "He has %d cherries\n", sabit bir dizedir, yani derleme zamanı sabiti anlamına gelir. Ama dürüst olmak gerekirse, yazarın tavsiyesi "olarak sabit dizeleri geçemiyor değildi printf'", bu olmadan dizeleri geçemiyor edildi ilk argüman " %olarak printfilk argüman."'
Steve Summit

literally from the C programming book of Genesis. Anyone deprecating that usage is being quite offensively heretical- son yıllarda K & R'yi gerçekten okumadınız. Bu günlerde sadece kullanımdan kaldırılan değil, sadece kötü bir uygulama olan bir sürü tavsiye ve kodlama stili var.
Voo

@Voo: Pekala, kötü uygulama olarak kabul edilen her şeyin aslında kötü uygulama olmadığını söyleyelim . ("Asla sade kullanma" tavsiyesi intakla geliyor.)
Steve Summit

1
@Steve Bunu nereden duyduğuna dair hiçbir fikrim yok, ama bu kesinlikle orada bahsettiğimiz türden kötü (kötü?) Pratik değil. Beni yanlış anlamayın, çünkü kod mükemmeldi, ama gerçekten k & r'ye çok fazla bakmak istemiyorsunuz, ancak bugünlerde tarihi bir not olarak. "K & r'de" bu günlerde kaliteli bir gösterge değil, hepsi bu
Voo

9

Oldukça çirkin bir yönü printf, başıboş belleğin okuduğu platformlarda bile biçimlendirme karakterlerinden biri olan sınırlı (ve kabul edilebilir) hasara %nneden olabilir, bir sonraki argümanın yazılabilir bir tam sayıya işaretçi olarak yorumlanmasına neden olur ve burada tanımlanan değişkene kaydedilecek olan karakter sayısı çıktı. Bu özelliği kendim hiç kullanmadım ve bazen sadece gerçekten kullandığım özellikleri dahil etmek için yazdığım (ve bunu veya benzer herhangi bir şeyi içermeyen) ancak alınan standart printf işlevlerinin dizelerini beslemek için yazdığım hafif printf tarzı yöntemler kullanıyorum Güvenilir olmayan kaynaklardan, rastgele depolamayı okuma becerisinin ötesinde güvenlik açıkları ortaya çıkabilir.


8

Kimse bahsetmediği için performanslarıyla ilgili bir not ekleyeceğim.

Normal koşullar altında, derleyici optimizasyonlarının kullanılmadığını varsayarsak (yani printf()gerçekten çağırır printf()ve çağırmaz fputs()) printf(), özellikle uzun dizeler için daha az verimli bir şekilde çalışmayı beklerdim . Bunun nedeni iseprintf() , herhangi bir dönüşüm belirteci olup olmadığını kontrol etmek için dizeyi ayrıştırmak zorunda olmasıdır.

Bunu doğrulamak için bazı testler yaptım. Test, gcc 4.8.4 ile Ubuntu 14.04 üzerinde gerçekleştirilir. Makinem Intel i5 cpu kullanıyor. Test edilen program aşağıdaki gibidir:

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

Her ikisi de ile derlenmiştir gcc -Wall -O0. Zaman kullanılarak ölçülürtime ./a.out > /dev/null . Aşağıdakiler tipik bir çalıştırmanın sonucudur (onları beş kez çalıştırdım, tüm sonuçlar 0,002 saniye içinde).

İçin printf()varyant:

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

İçin fputs()varyant:

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

Çok uzun bir diziniz varsa bu etki güçlendirilir .

#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;
}

İçin printf()varyant (üç kez, gerçek artı / eksi 1.5s ran):

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

İçin fputs()varyant (üç kez, gerçek artı / eksi 0.2s ran):

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

Not: gcc tarafından oluşturulan derlemeyi inceledikten sonra, gcc'nin fputs()çağrıyı ile fwrite()bile optimize ettiğini fark ettim -O0. ( printf()Çağrı değişmeden kalır.) Derleyici fwrite()derleme zamanında dize uzunluğunu hesapladığından, bunun testimi geçersiz kılıp kılmayacağından emin değilim .


2
Sanki, Testinize geçersiz kılmaz fputs()genellikle dize sabitleri ile kullanılan ve optimizasyon fırsatı make.This söz konusu istediğini noktanın parçasıdır ile bir dinamik biçimde oluşturulan dize ile bir deneme sürüşü ekleyerek fputs()ve fprintf()güzel bir tamamlayıcı veri noktası olacağını .
Patrick Schlüter

@ PatrickSchlüter Dinamik olarak oluşturulmuş dizgelerle yapılan testler, bu sorunun amacını bozuyor gibi görünse de ... OP sadece basılacak dizgilerle ilgileniyor gibi görünüyor.
user12205

1
Örneği dize değişmezleri kullansa bile bunu açıkça belirtmiyor. Aslında, kitabın tavsiyesi konusundaki kafa karışıklığının, örnekte dizeli harflerin kullanılmasının bir sonucu olduğunu düşünüyorum. Dize değişmezlerinde, kitap tavsiyeleri bir şekilde şüpheli, dinamik dizelerle iyi bir tavsiye.
Patrick Schlüter

1
/dev/nullBunu bir tür oyuncak yapar, çünkü genellikle biçimlendirilmiş çıktı oluştururken amacınız çıktının bir yere gitmesi, atılmamasıdır. "Aslında verileri atmama" süresini ekledikten sonra, nasıl karşılaştırılırlar?
Yakk - Adam Nevraumont

7
printf("Hello World\n")

otomatik olarak eşdeğer şekilde derler

puts("Hello World")

çalıştırılabilir dosyanızı birleştirerek kontrol edebilirsiniz:

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

kullanma

char *variable;
... 
printf(variable)

güvenlik sorunlarına yol açacak, yol açacaksa, printf'i asla bu şekilde kullanmayın!

bu nedenle kitabınız aslında doğru, tek değişkenli printf kullanımı artık önerilmiyor, ancak yine de printf ("dizem \ n") kullanabilirsiniz, çünkü otomatik olarak koyar haline gelecektir


12
Bu davranış aslında tamamen derleyiciye bağlıdır.
Jabberwocky

6
Bu yanıltıcıdır. Belirtiyorsun A compiles to B, ama gerçekte demek istiyorsun A and B compile to C.
Sebastian Mach

6

Gcc için, kontrol için belirli uyarıları etkinleştirmek mümkündür printf()vescanf() .

Gcc dokümantasyonu şunları belirtir:

-Wformatdahildir -Wall. Seçenekleri işaretleyerek biçimi bazı yönleri üzerinde daha fazla denetim sağlamak için -Wformat-y2k, -Wno-format-extra-args, -Wno-format-zero-length, -Wformat-nonliteral, -Wformat-security, ve -Wformat=2kullanılabilir, ancak dahil değildir -Wall.

-Wformatİçinde etkin olduğu -Wallseçeneğiyle yardım bu davaları bulmak için birkaç özel uyarıları izin vermez:

  • -Wformat-nonliteral biçim belirteci olarak bir dizge harfini iletmezseniz uyarır.
  • -Wformat-securityTehlikeli bir yapı içerebilecek bir dizge geçerseniz uyarır. Bu bir alt kümesidir -Wformat-nonliteral.

Kabul etmeliyim ki -Wformat-securitykod tabanımızda bulunan birkaç hatayı açığa çıkardı (günlükleme modülü, hata işleme modülü, xml çıktı modülü, hepsinin parametrelerinde% karakterleri ile çağrılmışlarsa tanımsız şeyler yapabilecek bazı işlevler vardı. Bilgi için, kod tabanımız şu anda yaklaşık 20 yaşındadır ve bu tür sorunların farkında olsak bile, bu uyarıları etkinleştirdiğimizde bu hatalardan kaçının hala kod tabanında olduğunu belirterek çok şaşırdık.


1

Kapsanan herhangi bir yan endişeye sahip iyi açıklanmış diğer cevapların yanı sıra, verilen soruya kesin ve öz bir cevap vermek istiyorum.


Neden printftek bir bağımsız değişkenle (dönüşüm belirteçleri olmadan) kullanımdan kaldırılıyor?

Bir printfgenel bir tek argüman alan işlev çağrısı olduğu değil kaldırılan ve aynı zamanda hiçbir açığı içeren zaman her zaman kod edecektir olarak doğru kullanıldığında.

Durum başlangıcından durum uzmanı kullanımına kadar tüm dünyadaki C Kullanıcılar printf , konsola çıktı olarak basit bir metin cümlesi vermek bu yolu kullanır.

Ayrıca, birisinin bu tek ve tek bağımsız değişkenin bir dizge değişmezi mi yoksa geçerli olan ancak yaygın olarak kullanılmayan bir dizgeye işaretçi mi olduğunu ayırt etmesi gerekir. İkincisi için, elbette, uygunsuz çıktılar veya her türlü Tanımlanmamış Davranış ortaya çıkabilir. işaretçi geçerli bir dizgeyi gösterecek şekilde doğru bir şekilde ayarlanmadığında ortaya çıkabilir, ancak bu şeyler, biçim belirleyicileri ilgili bağımsız değişkenlerle eşleşmiyorsa da oluşabilir. çoklu argümanlar.

Elbette, tek ve tek argüman olarak sağlanan dizgenin herhangi bir format veya dönüşüm tanımlayıcısına sahip olması da doğru ve uygun değildir, çünkü dönüşüm olmayacaktır.

Bununla "Hello World!"birlikte, soruda sağladığınız gibi, bu dizede herhangi bir biçim belirticisi olmadan yalnızca bağımsız değişken gibi basit bir dize değişmezi vermek :

printf("Hello World!");

olduğu değil kaldırılan ya da " kötü uygulama " Hiç ne de herhangi bir güvenlik açıklarını sahiptir.

Aslında, birçok C programcısı bu HelloWorld programı ve bu printfifade türünün ilk örneği olarak C'yi ve hatta genel olarak programlama dillerini öğrenmeye ve kullanmaya başladı .

Kullanımdan kaldırılsalar öyle olmazlardı.

Okuduğum bir kitapta, printftek bir argümanın (dönüşüm belirteçleri olmadan) kullanımdan kaldırıldığı yazılıyor .

O zaman kitaba ya da yazara odaklanırım. Bir yazar gerçekten böyle yapıyorsa, bence, yanlış iddialar yapıyorsa ve hatta bunu neden yaptığını açık bir şekilde açıklamadan öğretiyorsa (eğer bu iddialar gerçekten o kitapta verilenler gerçekten eşdeğerse), onu kötü bir kitap olarak değerlendiririm. Bunun aksine iyi bir kitap, belirli türden programlama yöntemlerinden veya işlevlerinden neden kaçınılması gerektiğini açıklayacaktır .

Yukarıda söylediğime göre, printfyalnızca bir bağımsız değişkenle (bir dizge değişmezi) ve herhangi bir biçim belirticisi olmadan kullanmak, hiçbir durumda kullanımdan kaldırılmaz veya "kötü uygulama" olarak değerlendirilmez .

Yazara, bununla ne demek istediğini sormalısınız, hatta daha iyisi, bir sonraki baskı veya genel olarak diziler için ilgili bölümü açıklığa kavuşturmasını veya düzeltmesini sağlamalısınız.


Bunu ekleyebiliriz printf("Hello World!");olduğunu değil eşdeğer puts("Hello World!");öneri yazarı hakkında bir şeyler söyler, neyse.
chqrlie
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.