Bu dört zorlu kodun arkasındaki konsept


384

Bu kod neden çıktıyı veriyor C++Sucks? Arkasındaki kavram nedir?

#include <stdio.h>

double m[] = {7709179928849219.0, 771};

int main() {
    m[1]--?m[0]*=2,main():printf((char*)m);    
}

Burada test edin .


1
@BoBTFish teknik olarak, evet, ama aynı C99'da çalışır: ideone.com/IZOkql
nijansen

12
@nurettin Benzer düşüncelerim vardı. Ama bu OP'nin hatası değil, bu gereksiz bilgiye oy veren insanlar. Kabul, bu kod gizleme şeyler ilginç olabilir ama Google "gizleme" yazın ve aklınıza gelebilecek her resmi dilde tonlarca sonuç alırsınız. Beni yanlış anlamayın, burada böyle bir soru sormayı uygun buluyorum. Bu sadece abartılı çünkü çok yararlı bir soru değil.
TobiMcNamobi

6
@ detonator123 "Burada yeni olmalısınız" - kapanış nedenine bakarsanız, durumun böyle olmadığını öğrenebilirsiniz. Gerekli asgari anlayış sorunuzda açıkça eksik - "Bunu anlamıyorum, açıklayın" Stack Overflow'da hoş bir şey değil. İlk önce kendiniz bir şey denediyseniz , soru kapatılmazdı. Google "çift gösterimi C" veya benzerlerini kullanmak önemsizdir.

42
Big-endian PowerPC makinem çıktısını alıyor skcuS++C.
Adam Rosenfield

27
Sözüm, böyle sorulardan nefret ediyorum. Hafızada biraz aptalca dize ile aynı olan biraz desen. Kimseye yararlı bir amaca hizmet etmiyor ve yine de hem soru soran hem de cevaplayan için yüzlerce temsil puanı kazanıyor. Bu arada, insanlar için yararlı olabilecek zor sorular, eğer varsa, bir avuç puan kazanabilir. Bu, SO ile ilgili yanlış bir poster çocuğu.
Carey Gregory

Yanıtlar:


494

Sayı 7709179928849219.064 bit olarak aşağıdaki ikili gösterime sahiptir double:

01000011 00111011 01100011 01110101 01010011 00101011 00101011 01000011
+^^^^^^^ ^^^^---- -------- -------- -------- -------- -------- --------

+işaretin konumunu gösterir; ^üssü ve -mantis (yani üssü olmayan değer).

Temsili ikili üs ve mantis kullandığından, sayının ikiye katlanması üssü bir arttırır. Programınız tam olarak 771 kez yapar, böylece 1075'te başlayan (ondalık temsili 10000110011) üs sonunda 1075 + 771 = 1846 olur; 1846 ikili gösterimidir 11100110110. Ortaya çıkan desen şöyle görünür:

01110011 01101011 01100011 01110101 01010011 00101011 00101011 01000011
-------- -------- -------- -------- -------- -------- -------- --------
0x73 's' 0x6B 'k' 0x63 'c' 0x75 'u' 0x53 'S' 0x2B '+' 0x2B '+' 0x43 'C'

Bu desen, yalnızca geriye doğru yazdırıldığını gördüğünüz dizeye karşılık gelir. Aynı zamanda, dizinin ikinci öğesi sıfır olur, boş sonlandırıcı sağlar ve dizeyi geçmeye uygun hale getirir printf().


22
Dize neden geriye?
Derek

95
@Derek x86 küçük-endian
Angew artık 13

16
@Derek Bu, platforma özel ait endian : abstract IEEE 754 temsil bayt dize baskılar çok doğru azalan adreslerde bellekte depolanır. Büyük endianiteye sahip donanımlarda, farklı bir numara ile başlanması gerekir.
dasblinkenlight

14
@AlvinWong Doğru, standart IEEE 754 veya başka bir belirli biçim gerektirmez. Bu program olduğu gibi taşınabilir olmayan, ya da ona çok yakın :-)
dasblinkenlight

10
@GrijeshChauhan Çift kesinlikli bir IEEE754 hesap makinesi kullandım : 7709179928849219Değeri yapıştırdım ve ikili temsili geri aldım.
dasblinkenlight

223

Daha okunabilir versiyon:

double m[2] = {7709179928849219.0, 771};
// m[0] = 7709179928849219.0;
// m[1] = 771;    

int main()
{
    if (m[1]-- != 0)
    {
        m[0] *= 2;
        main();
    }
    else
    {
        printf((char*) m);
    }
}

Özyinelemeli olarak main()771 kez arar .

Başlangıçta, m[0] = 7709179928849219.0hangi standları için C++Suc;C. Her çağrıda, m[0]son iki harfi "onarmak" için iki katına çıkar. Son çağrıda, m[0]ASCII karakter temsilini içerir C++Sucksve m[1]yalnızca sıfır içerir, bu nedenle dize için boş bir sonlandırıcıya sahiptir C++Sucks. Hepsi m[0]8 baytta saklandığı varsayımıyla , her karakter 1 bayt alır.

Özyineleme ve yasadışı main()arama olmadan şöyle görünecektir:

double m[] = {7709179928849219.0, 0};
for (int i = 0; i < 771; i++)
{
    m[0] *= 2;
}
printf((char*) m);

8
Postfix azalması. Yani 771 kez denecek.
Jack Aidley

106

Feragatname: Bu cevap, sorunun sadece C ++ 'dan bahseden ve bir C ++ üstbilgisi içeren orijinal formuna gönderildi. Sorunun saf C'ye dönüşümü, orijinal askerin girdisi olmadan topluluk tarafından yapıldı.


Resmi olarak konuşursak, bu program hakkında akıl yürütmek imkansız çünkü kötü biçimlendirilmiş (yani yasal C ++ değil). C ++ 11 [basic.start.main] p3'ü ihlal eder:

Main işlevi bir programda kullanılmamalıdır.

Bu bir yana, tipik bir tüketici bilgisayarında, doublea'nın 8 bayt uzunluğunda olduğu ve belirli bir iyi bilinen iç temsili kullandığı gerçeğine dayanır . Dizinin başlangıç ​​değerleri, "algoritma" gerçekleştirildiğinde, birincinin son değeri double, iç gösterim (8 bayt) 8 karakterin ASCII kodları olacak şekilde hesaplanır C++Sucks. Bu durumda dizideki ikinci öğe 0.0, ilk baytı 0dahili gösterimde olup, bunu geçerli bir C stili dize yapar. Bu daha sonra kullanılarak çıktıya gönderilir printf().

Bunu, yukarıdakilerin bazılarının beklemediği HW'de çalıştırmak, bunun yerine çöp metniyle (hatta sınırların dışında bir erişimle) sonuçlanır.


25
Bu bir C ++ 11 icadı olmadığını eklemeliyim - C ++ 03 de basic.start.mainaynı ifadeye sahip 3.6.1 / 3 vardı .
sharptooth

1
Bu küçük örneğin amacı C ++ ile neler yapılabileceğini göstermektir. UB hileleri veya "klasik" kodun büyük yazılım paketlerini kullanan sihirli örnek.
SChepurin

1
@sharptooth Bunu eklediğiniz için teşekkür ederiz. Başka türlü ima etmek istememiştim, kullandığım standarda değindim.
Angew artık SO '

@Angew: Evet, anlıyorum, sadece ifadelerin oldukça eski olduğunu söylemek istedim.
sharptooth

1
@JimBalter Dikkat dedim ki "resmi olarak konuşursak, mantıklı olmak imkansız" değil "resmen mantıklı olmak imkansız" Program hakkında akıl yürütmenin mümkün olduğunu doğru söylüyorsunuz, ancak bunu yapmak için kullanılan derleyicinin ayrıntılarını bilmeniz gerekiyor. Sabit sürücüyü veya herhangi bir şeyi biçimlendirmek için çağrıyı ortadan kaldırmak veya bir API çağrısıyla değiştirmek derleyicinin hakları dahilinde olacaktır main().
Angew artık SO '

57

Belki de kodu anlamanın en kolay yolu şeyleri tersine çevirmektir. Yazdırmak için bir dize ile başlayacağız - denge için "C ++ Rocks" kullanacağız. Önemli nokta: tıpkı orijinali gibi, tam sekiz karakter uzunluğunda. Orijinali (kabaca) yapacağız ve ters sırada yazacağımız için, ters sırayla koyarak başlayacağız. İlk adımımız için, bu bit desenini a olarak göreceğiz doubleve sonucu yazdıracağız:

#include <stdio.h>

char string[] = "skcoR++C";

int main(){
    printf("%f\n", *(double*)string);
}

Bu üretir 3823728713643449.5. Bu yüzden, bunu açık olmayan ancak tersine çevrilmesi kolay olan bir şekilde manipüle etmek istiyoruz. Yarı keyfi olarak 256 ile çarpmayı seçeceğim, bu da bize veriyor 978874550692723072. Şimdi, 256'ya bölmek için bazı gizli kodlar yazmamız ve ardından bunun ayrı ayrı baytlarını ters sırada yazdırmamız gerekiyor:

#include <stdio.h>

double x [] = { 978874550692723072, 8 };
char *y = (char *)x;

int main(int argc, char **argv){
    if (x[1]) {
        x[0] /= 2;  
        main(--x[1], (char **)++y);
    }
    putchar(*--y);
}

Şimdi main, tamamen göz ardı edilen (özyinelemeli) argümanlara çok sayıda döküm yaptık (ancak artışı ve azalmayı elde etmek için değerlendirme tamamen önemlidir) ve elbette yaptığımız gerçeği örtbas etmek için tamamen keyfi görünümlü sayı gerçekten çok basit.

Tabii ki, bütün mesele gizleme olduğundan, eğer öyle hissedersek, daha fazla adım da atabiliriz. Örneğin, ififademizi tek bir ifadeye dönüştürmek için kısa devre değerlendirmesinden yararlanabiliriz , böylece ana gövde şöyle görünür:

x[1] && (x[0] /= 2,  main(--x[1], (char **)++y));
putchar(*--y);

Mantıksal işlem atarak - bu başlar aslında oldukça garip bakmaya Karartılmış kodu (ve / veya kod golf) alışık olmayan kimseye andnokta numarası ve dönüş değeri yüzen bazı Anlamsız mainbile a dönmüyor, değer. Daha da kötüsü, kısa devre değerlendirmesinin nasıl çalıştığını fark etmeden (ve düşünmeden), sonsuz özyinelemeden nasıl kaçınıldığı hemen belli olmayabilir.

Bir sonraki adımımız muhtemelen her karakteri yazdırmayı o karakteri bulmaktan ayırmak olacaktır. Geri dönüş değeri olarak doğru karakteri oluşturarak ve geri dönenleri mainyazdırarak bunu kolayca yapabiliriz main:

x[1] && (x[0] /= 2,  putchar(main(--x[1], (char **)++y)));
return *--y;

En azından bana, bu yeterince şaşırmış görünüyor, bu yüzden onu bırakacağım.


1
Adli tıp yaklaşımını seviyorum.
ryyker

24

Sadece bir char dizisi olarak yorumlanırsa, "C ++ Sucks" dizesi için ASCII kodlarını oluşturan bir çift dizi (16 bayt) oluşturmaktır.

Bununla birlikte, kod her sistemde çalışmıyor, aşağıdaki tanımlanmamış gerçeklerden bazılarına dayanıyor:

  • çift ​​tam 8 bayta sahiptir
  • endian

12

Aşağıdaki kod yazdırılır C++Suc;C, böylece tüm çarpma işlemi yalnızca son iki harf içindir

double m[] = {7709179928849219.0, 0};
printf("%s\n", (char *)m);

11

Diğerleri soruyu oldukça ayrıntılı bir şekilde açıkladılar, bunun standarda göre tanımlanmamış bir davranış olduğunu not etmek istiyorum .

C ++ 11 3.6.1 / 3 Ana işlev

Main işlevi bir programda kullanılmamalıdır. Ana bağlantı (3.5) uygulama tanımlıdır. Ana öğeyi silinmiş olarak tanımlayan veya ana öğenin satır içi, statik veya bağlamsal olduğunu bildiren bir program kötü biçimlendirilmiştir. Main adı başka şekilde ayrılmış değildir. [Örnek: üye işlevleri, sınıflar ve numaralandırmalar, diğer ad alanlarındaki varlıklar gibi ana olarak adlandırılabilir. —End örneği]


1
Ben bile kötü biçimlendirilmiş olduğunu söyleyebilirim (cevabımda yaptığım gibi) - bir "irade" yi ihlal eder.
Angew artık SO '

9

Kod şu şekilde yeniden yazılabilir:

void f()
{
    if (m[1]-- != 0)
    {
        m[0] *= 2;
        f();
    } else {
          printf((char*)m);
    }
}

Ne yapıyor bayt bir dizi üreten bir doubledizide mbir boşlukla terminatör ardından karakterlerin C ++ Sucks 'olarak eşleşecek şekilde olur. 771 kez ikiye katlandığında, standart gösterimde, dizinin ikinci üyesi tarafından sağlanan boş sonlandırıcı ile bu bayt kümesini üreten bir çift değer seçerek kodu gizlemişlerdir.

Bu kodun farklı bir endian temsili altında çalışmayacağını unutmayın. Ayrıca, çağrıya main()kesinlikle izin verilmez.


3
fGeri dönüşünüz neden bir int?
leftaroundabout

1
Ee, çünkü intsorudaki getiriyi kopyalarken beyinsizdim . Bunu düzeltmeme izin ver.
Jack Aidley

1

İlk olarak, çift duyarlıklı sayıların bellekte ikili biçimde saklandığını hatırlamalıyız:

(i) işaret için 1 bit

(ii) üs için 11 bit

(iii) Büyüklük için 52 bit

Bitlerin sırası (i) 'den (iii)' e düşer.

İlk olarak ondalık kesirli sayı, eşdeğer kesirli ikili sayıya dönüştürülür ve sonra ikili olarak büyüklük sırası olarak ifade edilir.

Böylece 7709179928849219.0 sayısı olur

(11011011000110111010101010011001010110010101101000011)base 2


=1.1011011000110111010101010011001010110010101101000011 * 2^52

Şimdi, büyüklük bitleri 1. dikkate alındığında, tüm büyüklük yöntemi 1 ile başlayacağı için ihmal edilir .

Böylece büyüklük kısmı:

1011011000110111010101010011001010110010101101000011 

Şimdi gücü 2 olduğu 52 , biz bu numarayı polarizasyon eklemek gerekir 2 ^ (üs için bit-1) -1 , yani 2 ^ (11 -1) -1 = 1023 eden üs hale gelir, böylece, 52 + 1023 = 1075

Şimdi kodumuz sayıyı 2 , 771 kez siler ve bu da üssü 771 arttırır.

Yani üssümüz (1075 + 771) = 1846 , ikili eşdeğeri (11100110110)

Şimdi sayımız pozitif, yani işaret bitimiz 0 .

Böylece değiştirilmiş numaramız:

işaret biti + üs + büyüklük (bitlerin basit birleşimi)

0111001101101011011000110111010101010011001010110010101101000011 

m karakter işaretçisine dönüştürüldüğünden, 8 parçasındaki bit desenini LSD'den ayırırız

01110011 01101011 01100011 01110101 01010011 00101011 00101011 01000011 

(Hex eşdeğeri olan :)

 0x73 0x6B 0x63 0x75 0x53 0x2B 0x2B 0x43 

ASCII ŞEMASI Karakter haritasından gösterildiği gibi:

s   k   c   u      S      +   +   C 

Şimdi bu yapıldıktan sonra m [1] 0'dır, bu da NULL karakter anlamına gelir

Şimdi bu programı küçük endian bir makinede çalıştırdığınızı varsayarsak (alt sıra biti alt adreste saklanır) böylece işaretçi m işaretçisi en alt adres bitine işaret eder ve daha sonra 8 chucks'taki bitleri alarak (char * ) ve printf (), son yığında 00000000 ile karşılaştığında durur ...

Ancak bu kod taşınabilir değildir.

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.