แนวคิดเบื้องหลังรหัส C สี่สายที่ยุ่งยากเหล่านี้


384

ทำไมรหัสนี้ให้การส่งออกC++Sucks? แนวคิดเบื้องหลังมันคืออะไร?

#include <stdio.h>

double m[] = {7709179928849219.0, 771};

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

ทดสอบที่นี่


1
@BoBTFish ในทางเทคนิคแล้วใช่ แต่ทำงานเหมือนกันทั้งหมดใน C99: ideone.com/IZOkql
nijansen

12
@nurettin ฉันมีความคิดที่คล้ายกัน แต่มันไม่ใช่ความผิดของ OP แต่เป็นคนที่โหวตความรู้ที่ไร้ประโยชน์นี้ ยอมรับว่าสิ่งที่ทำให้งงงวยรหัสนี้อาจน่าสนใจ แต่พิมพ์ "obfuscation" ใน Google และคุณได้รับผลลัพธ์มากมายในทุกภาษาที่คุณคิด อย่าเข้าใจฉันผิดฉันคิดว่ามันโอเคที่จะถามคำถามแบบนี้ที่นี่ เป็นเพียงคำถามที่ overrated เพราะคำถามที่ไม่มีประโยชน์มาก
TobiMcNamobi

6
@ detonator123 "คุณต้องใหม่ที่นี่" - ถ้าคุณดูเหตุผลการปิดคุณจะพบว่าไม่ใช่กรณี ความเข้าใจที่น้อยที่สุดที่ต้องการนั้นหายไปจากคำถามของคุณอย่างชัดเจน - "ฉันไม่เข้าใจสิ่งนี้อธิบาย" ไม่ใช่สิ่งที่ยินดีต้อนรับใน Stack Overflow หากคุณลองทำเองก่อนคำถามจะไม่ถูกปิดหรือไม่ มันเป็นเรื่องธรรมดาที่ google "double double C" หรือสิ่งที่คล้ายกัน

42
เครื่อง PowerPC ของฉัน skcuS++Cbig-พิมพ์ออก
Adam Rosenfield

27
คำพูดของฉันฉันเกลียดคำถามที่วางแผนไว้เช่นนี้ มันเป็นรูปแบบบิตในหน่วยความจำที่เกิดขึ้นเหมือนกับสตริงโง่ ๆ มันไม่มีจุดประสงค์ที่เป็นประโยชน์สำหรับทุกคนและยังได้รับคะแนนนับร้อยสำหรับทั้งผู้ถามและผู้ตอบ ในขณะเดียวกันคำถามที่ยากซึ่งอาจเป็นประโยชน์กับผู้คนจะได้รับคะแนนไม่กี่คนถ้ามี นี่เป็นลูกของโปสเตอร์ว่ามีอะไรผิดปกติกับ SO
Carey Gregory

คำตอบ:


494

ตัวเลข7709179928849219.0มีการแทนค่าไบนารี่ต่อไปนี้เป็น 64- บิตdouble:

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

+แสดงตำแหน่งของเครื่องหมาย; ^ของเลขชี้กำลังและ-ของ mantissa (เช่นค่าที่ไม่มีเลขชี้กำลัง)

เนื่องจากการเป็นตัวแทนใช้เลขชี้กำลังเลขชี้กำลังและแมนทิสซาทำให้เพิ่มจำนวนทวีคูณของเลขยกกำลังสองเท่า โปรแกรมของคุณทำอย่างแม่นยำ 771 ครั้งดังนั้นเลขชี้กำลังที่เริ่มต้นที่ 1,075 (การแทนทศนิยม10000110011) จะกลายเป็น 1075 + 771 = 1846 ในตอนท้าย แทน binary 1846 11100110110คือ รูปแบบผลลัพธ์มีลักษณะดังนี้:

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

รูปแบบนี้สอดคล้องกับสตริงที่คุณเห็นพิมพ์ย้อนหลังเท่านั้น ในเวลาเดียวกัน, printf()องค์ประกอบที่สองของอาร์เรย์จะกลายเป็นศูนย์ให้เทอร์มินัลทำให้สตริงที่เหมาะสมสำหรับการผ่านไป


22
ทำไมสตริงจึงย้อนกลับ
ดีเร็ก


16
@Derek นี่เป็นเพราะendiannessเฉพาะของแพลตฟอร์ม: ไบต์ของการแทนค่า IEEE 754 ที่เป็นนามธรรมจะถูกเก็บไว้ในหน่วยความจำเมื่อมีการลดจำนวนแอดเดรสดังนั้นสตริงจะพิมพ์อย่างถูกต้อง สำหรับฮาร์ดแวร์ที่มีความเพียรอันยิ่งใหญ่เราจะต้องเริ่มด้วยจำนวนที่แตกต่างกัน
dasblinkenlight

14
@AlvinWong คุณถูกต้องมาตรฐานไม่จำเป็นต้องใช้ IEEE 754 หรือรูปแบบเฉพาะอื่น ๆ โปรแกรมนี้เกี่ยวกับการไม่พกพาตามที่ได้รับหรือใกล้เคียงกับมันมาก :-)
dasblinkenlight

10
@GrijeshChauhan ฉันใช้เครื่องคิดเลข IEEE754 ที่มีความแม่นยำสองเท่า : ฉันวาง7709179928849219ค่าและได้รับการนำเสนอแบบไบนารีกลับมา
dasblinkenlight

223

รุ่นที่อ่านเพิ่มเติมได้:

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

มันโทรซ้ำmain()771 ครั้ง

ในการเริ่มต้นm[0] = 7709179928849219.0ซึ่งยืนC++Suc;Cสำหรับ ในทุกการโทรm[0]รับสองเท่าเพื่อ "ซ่อมแซม" ตัวอักษรสองตัวสุดท้าย ในการโทรครั้งล่าสุดm[0]มีตัวแทน ASCII ถ่านC++Sucksและm[1]มีเพียงศูนย์ดังนั้นมันจึงมีตัวสิ้นสุดโมฆะสำหรับC++Sucksสตริง ทั้งหมดภายใต้สมมติฐานที่m[0]เก็บไว้ใน 8 ไบต์ดังนั้นแต่ละถ่านใช้เวลา 1 ไบต์

หากไม่มีการเรียกซ้ำและการmain()โทรที่ผิดกฎหมายจะมีลักษณะเช่นนี้:

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

8
มันลดลง postfix ดังนั้นมันจะถูกเรียกว่า 771 ครั้ง
Jack Aidley

106

คำเตือน:คำตอบนี้ถูกโพสต์ในรูปแบบดั้งเดิมของคำถามซึ่งกล่าวถึงเพียง C ++ และรวมส่วนหัว C ++ การแปลงคำถามเป็น Pure C เกิดขึ้นโดยชุมชนโดยไม่มีการป้อนข้อมูลจากผู้ถามดั้งเดิม


การพูดอย่างเป็นทางการเป็นไปไม่ได้ที่จะให้เหตุผลเกี่ยวกับโปรแกรมนี้เนื่องจากรูปแบบไม่เหมาะสม (เช่นไม่ใช่ภาษา C ++) มันละเมิด C ++ 11 [basic.start.main] p3:

ฟังก์ชั่นหลักจะต้องไม่ถูกใช้ภายในโปรแกรม

สิ่งนี้มันขึ้นอยู่กับข้อเท็จจริงที่ว่าคอมพิวเตอร์แบบทั่วไปมีความdoubleยาว 8 ไบต์และใช้การเป็นตัวแทนภายในที่รู้จักกันดี ค่าเริ่มต้นของอาร์เรย์จะคำนวณเพื่อที่ว่าเมื่อ "อัลกอริทึม" จะดำเนินการค่าสุดท้ายในครั้งแรกdoubleจะเป็นเช่นนั้นการแสดงภายใน (8 bytes) จะเป็นรหัส ASCII ของ 8 C++Sucksตัวอักษร องค์ประกอบที่สองในอาเรย์นั้นจะ0.0มีไบต์แรกอยู่0ในการเป็นตัวแทนภายในทำให้นี่เป็นสตริงรูปแบบ C ที่ถูกต้อง printf()นี้จะถูกส่งไปเพื่อส่งออกโดยใช้

การเรียกใช้สิ่งนี้บน HW โดยที่บางส่วนที่ไม่ได้กล่าวถึงข้างต้นอาจส่งผลให้ข้อความขยะ (หรืออาจเป็นการเข้าถึงที่ไม่เหมาะสม) แทน


25
ฉันต้องเพิ่มว่านี่ไม่ใช่สิ่งประดิษฐ์ของ C ++ 11 - C ++ 03 ก็มีbasic.start.main3.6.1 / 3 ด้วยถ้อยคำเดียวกัน
sharptooth

1
จุดตัวอย่างเล็ก ๆ นี้เพื่อแสดงให้เห็นถึงสิ่งที่สามารถทำได้ด้วย C ++ ตัวอย่างมหัศจรรย์โดยใช้เทคนิค UB หรือชุดซอฟต์แวร์ขนาดใหญ่ของรหัส "คลาสสิค"
SChepurin

1
@sharptooth ขอบคุณที่เพิ่มสิ่งนี้ ฉันไม่ได้ตั้งใจจะบอกเป็นอย่างอื่นฉันแค่อ้างถึงมาตรฐานที่ฉันใช้
Angew ไม่ภูมิใจใน SO SO

@ อังกูล: ใช่ฉันเข้าใจว่าแค่อยากจะบอกว่าถ้อยคำนั้นค่อนข้างเก่า
sharptooth

1
@JimBalter สังเกตเห็นฉันพูดว่า "พูดอย่างเป็นทางการเป็นไปไม่ได้ที่จะให้เหตุผล" ไม่ใช่ "เป็นไปไม่ได้ที่จะให้เหตุผลอย่างเป็นทางการ" คุณถูกต้องที่เป็นไปได้ที่จะให้เหตุผลเกี่ยวกับโปรแกรม แต่คุณจำเป็นต้องรู้รายละเอียดของคอมไพเลอร์ที่เคยทำ มันจะอยู่ในสิทธิ์ของคอมไพเลอร์อย่างสมบูรณ์ในการกำจัดการเรียกmain()หรือแทนที่ด้วยการเรียก API เพื่อจัดรูปแบบฮาร์ดไดรฟ์หรืออะไรก็ตาม
Angew ไม่ภูมิใจใน SO SO

57

บางทีวิธีที่ง่ายที่สุดในการเข้าใจรหัสคือการทำงานในสิ่งที่ตรงกันข้าม เราจะเริ่มต้นด้วยสตริงที่จะพิมพ์ออกมา - เพื่อความสมดุลเราจะใช้ "C ++ Rocks" จุดสำคัญ: เช่นเดียวกับต้นฉบับมันมีความยาวแปดตัวอักษร เนื่องจากเราจะทำ (ประมาณ) เหมือนต้นฉบับและพิมพ์ออกมาในลำดับย้อนกลับเราจะเริ่มต้นด้วยการเรียงลำดับกลับกัน สำหรับขั้นตอนแรกของเราเราเพียงแค่ดูรูปแบบบิตนั้นเป็นdoubleและพิมพ์ผล:

#include <stdio.h>

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

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

3823728713643449.5นี้ผลิต ดังนั้นเราต้องการจัดการในบางวิธีที่ไม่ชัดเจน แต่กลับง่าย ผมกึ่งพลจะเลือกโดยคูณ 256 978874550692723072ซึ่งจะช่วยให้เรา ตอนนี้เราแค่ต้องเขียนโค้ดที่ยุ่งเหยิงเพื่อหารด้วย 256 จากนั้นพิมพ์แต่ละไบต์ของสิ่งนั้นในลำดับย้อนกลับ:

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

ตอนนี้เรามีการคัดเลือกนักแสดงมากมายการส่งผ่านข้อโต้แย้งไปยัง (เรียกซ้ำ) mainที่ไม่สนใจอย่างสมบูรณ์ (แต่การประเมินเพื่อให้ได้จำนวนที่เพิ่มขึ้นและลดลงมีความสำคัญอย่างยิ่ง) และแน่นอนว่าจำนวนที่ต้องการค้นหาโดยพลการ ค่อนข้างตรงไปตรงมา

แน่นอนเนื่องจากประเด็นทั้งหมดคือความงงงวยถ้าเรารู้สึกว่ามันเราสามารถทำตามขั้นตอนมากขึ้นเช่นกัน ตัวอย่างเช่นเราสามารถใช้ประโยชน์จากการประเมินการลัดวงจรเพื่อเปลี่ยนifคำพูดของเราให้เป็นนิพจน์เดียวดังนั้นเนื้อหาหลักที่มีลักษณะเช่นนี้:

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

สำหรับผู้ที่ไม่คุ้นเคยกับรหัสที่สับสน (และ / หรือรหัสกอล์ฟ) สิ่งนี้เริ่มที่จะดูแปลก ๆ อย่างแน่นอน - การคำนวณและการทิ้งตรรกะandของจำนวนจุดลอยตัวที่ไม่มีความหมายและค่าส่งคืนจากmainซึ่งไม่ได้ส่งคืน ราคา. ที่เลวร้ายยิ่งกว่านั้นหากไม่มีการตระหนักถึง (และคิดถึง) การประเมินการลัดวงจรมันอาจไม่ชัดเจนในทันทีว่าจะหลีกเลี่ยงการเรียกซ้ำที่ไม่สิ้นสุด

ขั้นตอนต่อไปของเราอาจเป็นการแยกการพิมพ์อักขระแต่ละตัวออกจากการค้นหาตัวละครนั้น เราสามารถทำได้อย่างง่ายดายโดยการสร้างตัวละครที่ถูกต้องเป็นค่าตอบแทนจากmainนั้นพิมพ์สิ่งที่mainส่งกลับ:

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

อย่างน้อยสำหรับฉันที่ดูเหมือนจะสับสนมากพอดังนั้นฉันจะทิ้งมันไว้


1
ชอบวิธีการทางนิติวิทยาศาสตร์
ryyker

24

มันเป็นเพียงการสร้างอาร์เรย์คู่ (16 ไบต์) ซึ่ง - ถ้าตีความว่าเป็นอาร์เรย์ถ่าน - สร้างรหัส ASCII สำหรับสตริง "C ++ Sucks"

อย่างไรก็ตามรหัสไม่ทำงานในแต่ละระบบมันขึ้นอยู่กับข้อเท็จจริงบางอย่างที่ไม่ได้กำหนดดังต่อไปนี้:

  • double มีขนาด 8 ไบต์
  • endianness

12

รหัสต่อไปนี้จะพิมพ์C++Suc;Cดังนั้นการคูณทั้งหมดจะใช้สำหรับตัวอักษรสองตัวสุดท้ายเท่านั้น

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

11

คนอื่น ๆ ได้อธิบายคำถามอย่างละเอียดถี่ถ้วนฉันต้องการเพิ่มบันทึกว่านี่เป็นพฤติกรรมที่ไม่ได้กำหนดตามมาตรฐาน

C ++ 11 3.6.1 / 3 ฟังก์ชั่นหลัก

ฟังก์ชั่นหลักจะต้องไม่ถูกใช้ภายในโปรแกรม การเชื่อมโยง (3.5) ของหลักคือกำหนดการดำเนินงาน โปรแกรมที่กำหนด main ว่าถูกลบหรือที่บอกว่า main เป็น inline, static หรือ constexpr นั้นเกิดรูปแบบไม่ดี ชื่อหลักไม่ได้สงวนไว้เป็นอย่างอื่น [ตัวอย่าง: ฟังก์ชันสมาชิกคลาสและการแจกแจงสามารถเรียกว่า main ได้เช่นเดียวกับเอนทิตีในเนมสเปซอื่น - ส่งตัวอย่าง]


1
ฉันจะบอกว่ามันเป็นรูปแบบไม่ดี (เหมือนที่ฉันทำในคำตอบของฉัน) - มันละเมิด "จะ"
Angew ไม่ได้ภูมิใจใน SO

9

รหัสสามารถเขียนซ้ำได้เช่นนี้:

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

สิ่งที่มันทำคือการสร้างชุดของไบต์ในdoubleอาเรย์mที่เกิดขึ้นเพื่อให้สอดคล้องกับตัวละคร 'C ++ Sucks' ตามด้วย null-terminator พวกเขาทำให้โค้ดยุ่งเหยิงโดยการเลือกค่าสองเท่าซึ่งเมื่อเพิ่มเป็น 771 เท่าในการเป็นตัวแทนมาตรฐานชุดของไบต์นั้นพร้อมด้วยตัวปิดเทอร์มินัลที่จัดทำโดยสมาชิกตัวที่สองของอาร์เรย์

โปรดทราบว่ารหัสนี้จะไม่ทำงานภายใต้การเป็นตัวแทน endian อื่น นอกจากนี้การโทรmain()ไม่ได้รับอนุญาตอย่างเคร่งครัด


3
ทำไมคุณถึงfกลับมาint?
leftaroundabout

1
เอ่อ 'เพราะฉันงี่เง่าคัดลอกintผลตอบแทนในคำถาม ขอผมแก้ไขหน่อย
Jack Aidley

1

ครั้งแรกที่เราควรจำไว้ว่าตัวเลขความแม่นยำสองเท่าจะถูกเก็บไว้ในหน่วยความจำในรูปแบบไบนารีดังนี้

(i) 1 บิตสำหรับเครื่องหมาย

(ii) 11 บิตสำหรับเลขชี้กำลัง

(iii) 52 บิตสำหรับขนาด

ลำดับของบิตลดลงจาก (i) ถึง (iii)

อันดับแรกตัวเลขทศนิยมแบบทศนิยมจะถูกแปลงเป็นเลขฐานสองแบบเศษส่วนเท่ากันจากนั้นจะแสดงเป็นลำดับของรูปแบบขนาดในไบนารี

ดังนั้นหมายเลข7709179928849219.0จึงกลายเป็น

(11011011000110111010101010011001010110010101101000011)base 2


=1.1011011000110111010101010011001010110010101101000011 * 2^52

ตอนนี้เมื่อพิจารณาขนาดบิตที่1จะถูกละเลยเนื่องจากวิธีลำดับความสำคัญทั้งหมดจะเริ่มต้นด้วย1

ดังนั้นขนาดจะกลายเป็น:

1011011000110111010101010011001010110010101101000011 

ตอนนี้กำลังของ2คือ52เราต้องเพิ่มหมายเลขการให้น้ำหนักมันเป็น2 ^ (บิตสำหรับเลขชี้กำลัง -1) -1 คือ2 ^ (11 -1) -1 = 1,023ดังนั้นเลขชี้กำลังของเรากลายเป็น52 + 1023 = 1,075

ตอนนี้โค้ดของเรารวบรวมตัวเลขด้วย2 , 771เท่าซึ่งทำให้เลขชี้กำลังเพิ่มขึ้น771

เลขชี้กำลังของเราคือ(1075 + 771) = 1846ซึ่งเทียบเท่าเลขฐานสองคือ(11100110110)

ขณะนี้จำนวนของเราเป็นบวกดังนั้นบิตเครื่องหมายของเราคือ0

ดังนั้นหมายเลขที่เราแก้ไขจะกลายเป็น:

sign bit + exponent + magnitude (การต่อข้อมูลอย่างง่ายของ bits)

0111001101101011011000110111010101010011001010110010101101000011 

ตั้งแต่ m ถูกแปลงเป็นตัวชี้ถ่านเราจะแยกรูปแบบบิตเป็นชิ้นจำนวน 8 จาก LSD

01110011 01101011 01100011 01110101 01010011 00101011 00101011 01000011 

(ซึ่งเทียบเท่า Hex คือ :)

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

แผนภูมิ ASCII ซึ่งจากแผนที่อักขระตามที่แสดงคือ:

s   k   c   u      S      +   +   C 

ทีนี้เมื่อสิ่งนี้ถูกทำให้ m [1] เป็น 0 ซึ่งหมายถึงอักขระ NULL

ทีนี้สมมติว่าคุณรันโปรแกรมนี้บนเครื่องเล็ก ๆ น้อย ๆ (บิตลำดับล่างถูกเก็บไว้ในที่อยู่ต่ำกว่า) ดังนั้นตัวชี้ m ไปยังบิตที่อยู่ต่ำสุดแล้วดำเนินการโดยใช้บิตเป็น chucks ที่ 8 (ตามประเภท cast to char * ) และ printf () หยุดเมื่อพบ 00000000 ในช่องสุดท้าย ...

รหัสนี้ไม่สามารถพกพาได้

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.