Konsep di balik empat baris kode C rumit ini


384

Mengapa kode ini memberikan hasil C++Sucks? Apa konsep di baliknya?

#include <stdio.h>

double m[] = {7709179928849219.0, 771};

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

Uji di sini .


1
@BoBTFish teknis, ya, tapi itu berjalan semua sama di C99: ideone.com/IZOkql
nijansen

12
@nurettin saya punya pemikiran serupa. Tapi itu bukan kesalahan OP, itu adalah orang-orang yang memilih pengetahuan yang tidak berguna ini. Diakui, hal-hal yang membingungkan kode ini mungkin menarik tetapi ketik "kebingungan" di Google dan Anda mendapatkan banyak hasil dalam setiap bahasa formal yang dapat Anda pikirkan. Jangan salah paham, saya merasa OK untuk mengajukan pertanyaan seperti itu di sini. Itu hanya pertanyaan yang berlebihan karena tidak terlalu berguna.
TobiMcNamobi

6
@ detonator123 "Anda harus baru di sini" - jika Anda melihat alasan penutupan, Anda dapat mengetahui bahwa ini bukan masalahnya. Pemahaman minimal yang diperlukan jelas hilang dari pertanyaan Anda - "Saya tidak mengerti ini, jelaskan itu" bukanlah sesuatu yang diterima di Stack Overflow. Jika Anda telah mencoba sesuatu sendiri terlebih dahulu, apakah pertanyaannya belum ditutup. Ini sepele untuk google "representasi ganda C" atau sejenisnya.

42
Mesin PowerPC big-endian saya mencetak skcuS++C.
Adam Rosenfield

27
Kata saya, saya benci pertanyaan yang dibuat-buat seperti ini. Ini sedikit pola dalam memori yang kebetulan sama dengan beberapa string konyol. Ini tidak memiliki tujuan yang berguna bagi siapa pun, namun menghasilkan ratusan poin rep untuk penanya dan penjawab. Sementara itu, pertanyaan-pertanyaan sulit yang bisa bermanfaat bagi orang dapat menghasilkan beberapa poin, jika ada. Ini adalah semacam anak poster dari apa yang salah dengan SO.
Carey Gregory

Jawaban:


494

Nomor tersebut 7709179928849219.0memiliki representasi biner berikut sebagai 64-bit double:

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

+menunjukkan posisi tanda; ^dari eksponen, dan -mantissa (yaitu nilai tanpa eksponen).

Karena representasi menggunakan eksponen biner dan mantissa, menggandakan jumlah akan menambah eksponen dengan satu. Program Anda melakukannya tepat 771 kali, sehingga eksponen yang dimulai pada 1075 (representasi desimal 10000110011) menjadi 1075 + 771 = 1846 pada akhirnya; representasi biner tahun 1846 adalah 11100110110. Pola yang dihasilkan terlihat seperti ini:

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

Pola ini sesuai dengan string yang Anda lihat dicetak, hanya mundur. Pada saat yang sama, elemen kedua array menjadi nol, memberikan terminator nol, membuat string cocok untuk diteruskan printf().


22
Mengapa senar mundur?
Derek

95
@Derek x86 adalah little-endian
Angew tidak lagi bangga dengan SO

16
@ Derek Ini karena endianness platform-spesifik : byte dari representasi IEEE 754 abstrak disimpan dalam memori pada alamat yang menurun, sehingga string mencetak dengan benar. Pada perangkat keras dengan endianness besar, seseorang harus memulai dengan nomor yang berbeda.
dasblinkenlight

14
@ AlvinWong Anda benar, standar tidak memerlukan IEEE 754 atau format spesifik lainnya. Program ini hampir non-portable, atau sangat dekat :-)
dasblinkenlight

10
@GrijeshChauhan Saya menggunakan kalkulator IEEE754 presisi ganda : Saya menempelkan 7709179928849219nilainya, dan mendapatkan representasi biner kembali.
dasblinkenlight

223

Versi yang lebih mudah dibaca:

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

Itu secara rekursif memanggil main()771 kali.

Pada awalnya, m[0] = 7709179928849219.0yang berdiri untuk C++Suc;C. Dalam setiap panggilan, m[0]digandakan, untuk "memperbaiki" dua huruf terakhir. Dalam panggilan terakhir, m[0]berisi representasi karakter ASCII C++Sucksdan m[1]hanya berisi nol, sehingga memiliki terminator nol untuk C++Sucksstring. Semua dalam asumsi yang m[0]disimpan pada 8 byte, sehingga masing-masing karakter membutuhkan 1 byte.

Tanpa rekursi dan main()panggilan ilegal akan terlihat seperti ini:

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

8
Ini adalah penurunan postfix. Jadi itu akan dipanggil 771 kali.
Jack Aidley

106

Penafian: Jawaban ini diposting ke bentuk asli dari pertanyaan, yang hanya menyebutkan C ++ dan menyertakan header C ++. Konversi pertanyaan menjadi murni C dilakukan oleh komunitas, tanpa masukan dari penanya semula.


Secara formal, tidak mungkin untuk beralasan tentang program ini karena programnya tidak lengkap (artinya, ini bukan C ++ legal) Itu melanggar C ++ 11 [basic.start.main] p3:

Fungsi utama tidak boleh digunakan dalam suatu program.

Selain itu, ini bergantung pada fakta bahwa pada komputer konsumen tipikal, doublepanjangnya 8 byte, dan menggunakan representasi internal tertentu yang terkenal. Nilai awal array dihitung sehingga ketika "algoritma" dilakukan, nilai akhir yang pertama doubleakan sedemikian rupa sehingga representasi internal (8 byte) akan menjadi kode ASCII dari 8 karakter C++Sucks. Elemen kedua dalam array adalah 0.0byte pertama yang berada 0dalam representasi internal, menjadikannya string gaya C yang valid. Ini kemudian dikirim ke keluaran menggunakan printf().

Menjalankan ini di HW di mana beberapa hal di atas tidak berlaku akan menghasilkan teks sampah (atau bahkan mungkin akses di luar batas) sebagai gantinya.


25
Saya harus menambahkan bahwa ini bukan penemuan C ++ 11 - C ++ 03 juga memiliki basic.start.main3.6.1 / 3 dengan kata-kata yang sama.
sharptooth

1
Inti dari contoh kecil ini adalah untuk menggambarkan apa yang dapat dilakukan dengan C ++. Sampel ajaib menggunakan trik UB atau paket perangkat lunak besar dengan kode "klasik".
SChepurin

1
@sharptooth Terima kasih telah menambahkan ini. Saya tidak bermaksud mengatakan sebaliknya, saya hanya mengutip standar yang saya gunakan.
Angew tidak lagi bangga dengan SO

@ Angew: Yeap, saya mengerti itu, hanya ingin mengatakan bahwa kata-katanya sudah cukup tua.
sharptooth

1
@JimBalter Perhatikan Saya berkata "secara formal, tidak mungkin untuk alasan," bukan "tidak mungkin untuk alasan secara formal." Anda benar bahwa ada kemungkinan untuk beralasan tentang program ini, tetapi Anda perlu mengetahui detail dari kompiler yang digunakan untuk melakukan itu. Ini akan sepenuhnya dalam hak kompiler untuk hanya menghilangkan panggilan main(), atau menggantinya dengan panggilan API untuk memformat harddisk, atau apa pun.
Angew tidak lagi bangga dengan SO

57

Mungkin cara termudah untuk memahami kode adalah bekerja melalui hal-hal secara terbalik. Kami akan mulai dengan string untuk mencetak - untuk keseimbangan, kami akan menggunakan "C ++ Rocks". Poin penting: sama seperti aslinya, panjangnya persis delapan karakter. Karena kita akan melakukan (kira-kira) seperti aslinya, dan mencetaknya dalam urutan terbalik, kita akan mulai dengan meletakkannya dalam urutan terbalik. Untuk langkah pertama kami, kami hanya akan melihat pola bit itu sebagai double, dan mencetak hasilnya:

#include <stdio.h>

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

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

Ini menghasilkan 3823728713643449.5. Jadi, kami ingin memanipulasi itu dengan cara yang tidak jelas, tetapi mudah untuk dibalik. Saya akan semi-sewenang-wenang memilih perkalian dengan 256, yang memberi kita 978874550692723072. Sekarang, kita hanya perlu menulis beberapa kode yang dikaburkan untuk dibagi dengan 256, lalu mencetak masing-masing byte dengan urutan terbalik:

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

Sekarang kami memiliki banyak casting, memberikan argumen kepada (rekursif) mainyang sepenuhnya diabaikan (tetapi evaluasi untuk mendapatkan kenaikan dan penurunan sangat penting), dan tentu saja angka yang benar-benar sewenang-wenang mencari untuk menutupi fakta bahwa apa yang kita lakukan benar-benar sangat mudah.

Tentu saja, karena intinya adalah kebingungan, jika kita merasa kita dapat mengambil lebih banyak langkah juga. Sebagai contoh, kita dapat mengambil keuntungan dari evaluasi hubung singkat, untuk mengubah ifpernyataan kita menjadi satu ekspresi, sehingga badan utama terlihat seperti ini:

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

Bagi siapa pun yang tidak terbiasa dengan kode yang dikaburkan (dan / atau kode golf) ini mulai terlihat sangat aneh - menghitung dan membuang logika andbeberapa angka floating point yang tidak berarti dan nilai pengembalian dari main, yang bahkan tidak mengembalikan nilai. Lebih buruk lagi, tanpa menyadari (dan berpikir tentang) bagaimana evaluasi hubung singkat bekerja, bahkan mungkin tidak segera jelas bagaimana hal itu menghindari rekursi tak terbatas.

Langkah kami berikutnya mungkin akan memisahkan mencetak setiap karakter dari menemukan karakter itu. Kita dapat melakukannya dengan cukup mudah dengan menghasilkan karakter yang tepat sebagai nilai pengembalian main, dan mencetak apa yang maindikembalikan:

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

Setidaknya bagi saya, itu tampaknya cukup membingungkan, jadi saya akan berhenti di situ.


1
Cinta pendekatan forensik.
ryyker

24

Itu hanya membangun array ganda (16 byte) yang - jika diartikan sebagai array char - membangun kode ASCII untuk string "C ++ Sucks"

Namun, kode ini tidak berfungsi pada setiap sistem, itu bergantung pada beberapa fakta tidak terdefinisi berikut:


12

Kode berikut dicetak C++Suc;C, jadi seluruh perkalian hanya untuk dua huruf terakhir

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

11

Yang lain telah menjelaskan pertanyaan dengan cukup teliti, saya ingin menambahkan catatan bahwa ini adalah perilaku yang tidak terdefinisi menurut standar.

C ++ 11 3.6.1 / 3 Fungsi utama

Fungsi utama tidak boleh digunakan dalam suatu program. Linkage (3.5) dari main ditentukan oleh implementasi. Program yang mendefinisikan main sebagai dihapus atau yang menyatakan main sebagai inline, static, atau constexpr adalah salah bentuk. Nama utama tidak dinyatakan dilindungi undang-undang. [Contoh: fungsi anggota, kelas, dan enumerasi dapat disebut main, seperti halnya entitas di ruang nama lain. —Kirim contoh]


1
Saya akan mengatakan itu bahkan salah bentuk (seperti yang saya lakukan dalam jawaban saya) - itu melanggar "harus".
Angew tidak lagi bangga dengan SO

9

Kode dapat ditulis ulang seperti ini:

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

Apa yang dilakukannya adalah menghasilkan satu set byte dalam doublearray myang sesuai dengan karakter 'C ++ Sucks' diikuti oleh null-terminator. Mereka mengaburkan kode dengan memilih nilai ganda yang ketika digandakan 771 kali menghasilkan, dalam representasi standar, set byte dengan terminator nol yang disediakan oleh anggota kedua array.

Perhatikan bahwa kode ini tidak akan berfungsi di bawah representasi endian yang berbeda. Selain itu, panggilan main()tidak diijinkan.


3
Mengapa fpengembalian Anda int?
leftaroundabout

1
Eh, karena aku tidak berani menyalin intkembalinya dalam pertanyaan. Biarkan saya memperbaikinya.
Jack Aidley

1

Pertama-tama kita harus ingat bahwa angka presisi ganda disimpan dalam memori dalam format biner sebagai berikut:

(i) 1 bit untuk tanda

(ii) 11 bit untuk eksponen

(iii) 52 bit untuk besarnya

Urutan bit menurun dari (i) ke (iii).

Pertama bilangan pecahan desimal dikonversi menjadi bilangan biner pecahan ekivalen dan kemudian dinyatakan sebagai urutan besarnya dalam biner.

Jadi angka 7709179928849219.0 menjadi

(11011011000110111010101010011001010110010101101000011)base 2


=1.1011011000110111010101010011001010110010101101000011 * 2^52

Sekarang sambil mempertimbangkan bit magnitudo 1. diabaikan karena semua urutan metode magnitudo akan dimulai dengan 1.

Jadi bagian besarnya menjadi:

1011011000110111010101010011001010110010101101000011 

Sekarang kekuatan 2 adalah 52 , kita perlu menambahkan angka bias sebagai 2 ^ (bit untuk eksponen -1) -1 yaitu 2 ^ (11 -1) -1 = 1023 , sehingga eksponen kita menjadi 52 + 1023 = 1075

Sekarang kode kita memutipkan angka dengan 2 , 771 kali yang membuat eksponen meningkat sebesar 771

Jadi eksponen kami adalah (1075 + 771) = 1846 yang setara binernya adalah (11100110110)

Sekarang angka kita positif sehingga bit tanda kita adalah 0 .

Jadi nomor kami yang diubah menjadi:

bit tanda + eksponen + magnitudo (penggabungan bit-bit sederhana)

0111001101101011011000110111010101010011001010110010101101000011 

karena m dikonversi menjadi pointer char kita akan membagi pola bit dalam potongan 8 dari LSD

01110011 01101011 01100011 01110101 01010011 00101011 00101011 01000011 

(yang setara Hex adalah :)

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

ASCII BAGAN Yang dari peta karakter seperti yang ditunjukkan adalah:

s   k   c   u      S      +   +   C 

Sekarang setelah ini dibuat m [1] adalah 0 yang berarti karakter NULL

Sekarang dengan asumsi bahwa Anda menjalankan program ini pada mesin little-endian (bit urutan lebih rendah disimpan di alamat yang lebih rendah) jadi pointer m pointer ke bit alamat terendah dan kemudian melanjutkan dengan mengambil bit dalam chuck 8 (seperti tipe yang dicor ke char * ) dan printf () berhenti ketika menemukan 00000000 di chunck terakhir ...

Namun kode ini tidak portabel.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.