ทำไมรหัสนี้ให้การส่งออกC++Sucks
? แนวคิดเบื้องหลังมันคืออะไร?
#include <stdio.h>
double m[] = {7709179928849219.0, 771};
int main() {
m[1]--?m[0]*=2,main():printf((char*)m);
}
skcuS++C
big-พิมพ์ออก
ทำไมรหัสนี้ให้การส่งออกC++Sucks
? แนวคิดเบื้องหลังมันคืออะไร?
#include <stdio.h>
double m[] = {7709179928849219.0, 771};
int main() {
m[1]--?m[0]*=2,main():printf((char*)m);
}
skcuS++C
big-พิมพ์ออก
คำตอบ:
ตัวเลข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()
องค์ประกอบที่สองของอาร์เรย์จะกลายเป็นศูนย์ให้เทอร์มินัลทำให้สตริงที่เหมาะสมสำหรับการผ่านไป
7709179928849219
ค่าและได้รับการนำเสนอแบบไบนารีกลับมา
รุ่นที่อ่านเพิ่มเติมได้:
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);
คำเตือน:คำตอบนี้ถูกโพสต์ในรูปแบบดั้งเดิมของคำถามซึ่งกล่าวถึงเพียง 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 โดยที่บางส่วนที่ไม่ได้กล่าวถึงข้างต้นอาจส่งผลให้ข้อความขยะ (หรืออาจเป็นการเข้าถึงที่ไม่เหมาะสม) แทน
basic.start.main
3.6.1 / 3 ด้วยถ้อยคำเดียวกัน
main()
หรือแทนที่ด้วยการเรียก API เพื่อจัดรูปแบบฮาร์ดไดรฟ์หรืออะไรก็ตาม
บางทีวิธีที่ง่ายที่สุดในการเข้าใจรหัสคือการทำงานในสิ่งที่ตรงกันข้าม เราจะเริ่มต้นด้วยสตริงที่จะพิมพ์ออกมา - เพื่อความสมดุลเราจะใช้ "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;
อย่างน้อยสำหรับฉันที่ดูเหมือนจะสับสนมากพอดังนั้นฉันจะทิ้งมันไว้
มันเป็นเพียงการสร้างอาร์เรย์คู่ (16 ไบต์) ซึ่ง - ถ้าตีความว่าเป็นอาร์เรย์ถ่าน - สร้างรหัส ASCII สำหรับสตริง "C ++ Sucks"
อย่างไรก็ตามรหัสไม่ทำงานในแต่ละระบบมันขึ้นอยู่กับข้อเท็จจริงบางอย่างที่ไม่ได้กำหนดดังต่อไปนี้:
รหัสต่อไปนี้จะพิมพ์C++Suc;C
ดังนั้นการคูณทั้งหมดจะใช้สำหรับตัวอักษรสองตัวสุดท้ายเท่านั้น
double m[] = {7709179928849219.0, 0};
printf("%s\n", (char *)m);
คนอื่น ๆ ได้อธิบายคำถามอย่างละเอียดถี่ถ้วนฉันต้องการเพิ่มบันทึกว่านี่เป็นพฤติกรรมที่ไม่ได้กำหนดตามมาตรฐาน
C ++ 11 3.6.1 / 3 ฟังก์ชั่นหลัก
ฟังก์ชั่นหลักจะต้องไม่ถูกใช้ภายในโปรแกรม การเชื่อมโยง (3.5) ของหลักคือกำหนดการดำเนินงาน โปรแกรมที่กำหนด main ว่าถูกลบหรือที่บอกว่า main เป็น inline, static หรือ constexpr นั้นเกิดรูปแบบไม่ดี ชื่อหลักไม่ได้สงวนไว้เป็นอย่างอื่น [ตัวอย่าง: ฟังก์ชันสมาชิกคลาสและการแจกแจงสามารถเรียกว่า main ได้เช่นเดียวกับเอนทิตีในเนมสเปซอื่น - ส่งตัวอย่าง]
รหัสสามารถเขียนซ้ำได้เช่นนี้:
void f()
{
if (m[1]-- != 0)
{
m[0] *= 2;
f();
} else {
printf((char*)m);
}
}
สิ่งที่มันทำคือการสร้างชุดของไบต์ในdouble
อาเรย์m
ที่เกิดขึ้นเพื่อให้สอดคล้องกับตัวละคร 'C ++ Sucks' ตามด้วย null-terminator พวกเขาทำให้โค้ดยุ่งเหยิงโดยการเลือกค่าสองเท่าซึ่งเมื่อเพิ่มเป็น 771 เท่าในการเป็นตัวแทนมาตรฐานชุดของไบต์นั้นพร้อมด้วยตัวปิดเทอร์มินัลที่จัดทำโดยสมาชิกตัวที่สองของอาร์เรย์
โปรดทราบว่ารหัสนี้จะไม่ทำงานภายใต้การเป็นตัวแทน endian อื่น นอกจากนี้การโทรmain()
ไม่ได้รับอนุญาตอย่างเคร่งครัด
f
กลับมาint
?
int
ผลตอบแทนในคำถาม ขอผมแก้ไขหน่อย
ครั้งแรกที่เราควรจำไว้ว่าตัวเลขความแม่นยำสองเท่าจะถูกเก็บไว้ในหน่วยความจำในรูปแบบไบนารีดังนี้
(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
ซึ่งจากแผนที่อักขระตามที่แสดงคือ:
s k c u S + + C
ทีนี้เมื่อสิ่งนี้ถูกทำให้ m [1] เป็น 0 ซึ่งหมายถึงอักขระ NULL
ทีนี้สมมติว่าคุณรันโปรแกรมนี้บนเครื่องเล็ก ๆ น้อย ๆ (บิตลำดับล่างถูกเก็บไว้ในที่อยู่ต่ำกว่า) ดังนั้นตัวชี้ m ไปยังบิตที่อยู่ต่ำสุดแล้วดำเนินการโดยใช้บิตเป็น chucks ที่ 8 (ตามประเภท cast to char * ) และ printf () หยุดเมื่อพบ 00000000 ในช่องสุดท้าย ...
รหัสนี้ไม่สามารถพกพาได้