- xmas.c ผลงานชนะเลิศจาก International Obfuscated C Code Contest ปี 1988 เป็นโค้ด C ที่ดูเหมือนการพิมพ์มั่ว ๆ และพิมพ์เนื้อเพลง The Twelve Days of Christmas ออกมา
- มันเก็บสตริงที่เข้ารหัสไว้ในโค้ดซึ่งมีขนาดเล็กกว่าผลลัพธ์ที่พิมพ์ออกมา แล้วถอดรหัสคำและวลีด้วย substitution cipher และการเรียกซ้ำแบบ recursive
- เมื่อลองคลี่ ternary operator ออกเป็นบล็อก
if-then-elseและตั้งชื่อให้words,shiftก็จะเห็นโครงสร้างที่ค่าtใช้เปลี่ยนทิศทางของการไหลแบบ recursive shiftจับคู่ตัวอักษรด้านหน้ากับตัวอักษรที่อยู่ถัดไป 31 ตำแหน่ง และwordsเก็บ ชิ้นส่วนเนื้อเพลงที่เข้ารหัสไว้ โดยคั่นด้วยเครื่องหมายทับ (/)- แม้จะเป็นเพียงโปรแกรมพิมพ์เนื้อเพลงธรรมดา แต่การซ้อนกันของ substitution cipher, recursion สองทิศทาง, โค้ดที่ไม่จำเป็น และอาร์กิวเมนต์ที่ไม่ได้ใช้ ทำให้มันยังคงเป็นตัวอย่างการทำให้โค้ด C อ่านยากอย่างสร้างสรรค์
สิ่งที่ xmas.c พิมพ์ออกมา
- xmas.c เป็นโปรแกรมภาษา C ที่ชนะ International Obfuscated C Code Contest ในปี 1988
- ผู้วิเคราะห์พบโปรแกรมนี้ครั้งแรกประมาณปี 2000 และแยกโครงสร้างโค้ดเพื่อทำความเข้าใจการทำงานในเดือนพฤศจิกายน 2008
- เมื่อคอมไพล์และรันโดยไม่ส่งอาร์กิวเมนต์ มันจะพิมพ์เนื้อเพลงคริสต์มาสแครอล The Twelve Days of Christmas ตั้งแต่วันแรกจนถึงวันที่สิบสอง
- ในคอมเมนต์ของโค้ดต้นฉบับมีข้อความว่าโปรแกรมมีขนาดเล็กกว่ารูปแบบที่ “บีบอัด” ของผลลัพธ์เสียอีก และกรรมการคิดว่ามันดูเหมือน “ผลลัพธ์จากการเคาะเครื่องพิมพ์ดีดเก่าแบบสุ่มเป็นเวลานาน”
โครงสร้างภายในเมื่อคลี่ให้อ่านง่าย
- ขั้นแรกของการวิเคราะห์คือเปลี่ยนทุกโครงสร้าง
a ? b : cให้เป็นบล็อก if-then-else แบบชัดเจน - สตริงสองชุดที่เดาความหมายได้ยากถูกตั้งชื่อใหม่ตามหน้าที่
words: ชุดคำและวลีที่เข้ารหัสไว้เพื่อประกอบเป็นเนื้อเพลงคริสต์มาสแครอลshift: สตริงสำหรับแทนที่อักขระที่เข้ารหัสให้กลายเป็นอักขระจริงที่ต้องพิมพ์
main()เริ่มด้วยxmas(1, 0, '\0')และหลังจากนั้นฟังก์ชันxmas()เพียงตัวเดียวจะจัดการการพิมพ์ทั้งหมดแบบ recursive- ตัวแปร
tคือค่าหลักที่ควบคุมทิศทางของ recursion และพฤติกรรมการแตกแขนง
substitution cipher และข้อมูลเนื้อเพลง
- สตริง shift ทำงานราวกับเป็นการนำสองสตริงมาต่อกัน
- อักขระที่พบในครึ่งแรกจะถูกถอดรหัสเป็นอักขระที่อยู่ถัดไป 31 ตำแหน่ง
- ตัวอย่างเช่น อักขระแรกของสตริงคือ
!จะตรงกับอักขระขึ้นบรรทัดใหม่ที่อยู่ถัดไป 31 ตำแหน่ง
- ตัวอย่างเช่น อักขระแรกของสตริงคือ
- กิ่ง
t < -50จะเลื่อนผ่านสตริงaทีละตัวอักษรจนกว่าจะพบอักขระอินพุต_อยู่ในshift- เมื่อพบอักขระที่ตรงกัน มันจะพิมพ์
a[31]แล้วคืนค่า
- เมื่อพบอักขระที่ตรงกัน มันจะพิมพ์
- สตริง
wordsคือ ข้อมูลเนื้อเพลงที่เข้ารหัสไว้ ซึ่งจะถูกคลี่ด้วย substitution cipher- รูปคำลำดับที่และชิ้นส่วนเนื้อเพลงของแต่ละท่อนถูกคั่นด้วยเครื่องหมายทับ (
/)
- รูปคำลำดับที่และชิ้นส่วนเนื้อเพลงของแต่ละท่อนถูกคั่นด้วยเครื่องหมายทับ (
หน้าที่ของแต่ละกิ่ง recursive
- กิ่ง
t < -72จะสลับอาร์กิวเมนต์สองตัวแรก แล้วเรียกซ้ำอีกครั้งโดยใส่wordsเป็นอาร์กิวเมนต์ตัวที่สาม- จุดประสงค์หลักคือทำให้สับสน และเปิดทางให้เกิด recursion ซ้อนที่ไม่สนใจอาร์กิวเมนต์ตัวที่สาม
- กิ่ง
t < 0จะค้นหาเครื่องหมายทับ (/) ลำดับที่|t|ภายในสตริง แล้วส่งต่อสตริงที่เริ่มจากตัวอักษรถัดไป - กิ่ง
t == 0จะถอดรหัสและพิมพ์สตริงออกมาจนกว่าจะเจอเครื่องหมายทับถัดไป แล้วคืนค่า1 - กิ่ง
t == 1ถูกเรียกเพียงครั้งเดียวตอนเริ่มต้น เพื่อเริ่ม recursion จริงด้วยxmas(2, 2, "%s") - กิ่ง
t == 2จะพิมพ์บรรทัดแรกในรูปแบบ"On the [ordinal] day of Christmas my true love gave to me\n" - บล็อกเงื่อนไขสองส่วนสุดท้ายทำหน้าที่คง recursion ไว้สองทิศทาง
- ไล่ลงจากวันที่ปัจจุบันเพื่อพิมพ์เนื้อเพลงของท่อนนั้นในลำดับย้อนกลับ
- เพิ่มวันไปเรื่อย ๆ จนถึงวันที่สิบสองเพื่อวนพิมพ์ครบทุกท่อน
ลำดับการทำงานเมื่อทำให้เรียบง่าย
- เมื่อเข้าใจการทำงานแล้ว ก็สามารถเขียนใหม่เป็นโค้ดที่ง่ายขึ้นด้วยลูปและรูทีนไลบรารีสตริงของ C
- แม้ในเวอร์ชันที่ลดรูปแล้ว ข้อมูลหลักอย่าง
wordsและshiftก็ยังคงเดิม - กิ่ง
t < 0ใช้index(a, '/')เพื่อหาเครื่องหมายทับที่ใช้คั่น แล้วเลื่อนไปยังตำแหน่งของชิ้นส่วนเนื้อเพลงที่ต้องการ - กิ่ง
t == 0ใช้index(shift, *a++)[31]เพื่อถอดรหัสอักขระแล้วพิมพ์ออกมา - กิ่ง
t == 2จะพิมพ์ช่วงต้นของแต่ละท่อนตามลำดับต่อไปนี้"On the "- คำลำดับของวันนั้น
" my true love gave to me\n"
ทำไมความอ่านยากนี้จึงน่าสนใจ
- หากลดรูปจนสุด โปรแกรมนี้สามารถสรุปได้ว่าเป็นโค้ดสำหรับพิมพ์เนื้อเพลง
- แต่ต้นฉบับใช้ substitution cipher ร่วมกับ recursion เพื่อสร้างโครงสร้างที่ซับซ้อนกว่าการพิมพ์ธรรมดามาก
- โค้ดส่วนเกินขนาดเล็กและอาร์กิวเมนต์สุ่มที่จริง ๆ แล้วไม่ถูกใช้งาน ยิ่งทำให้ความเข้าใจโค้ดยากขึ้น
- การเข้าใจโค้ดกับการเขียนโค้ดแบบนี้ได้เองเป็นคนละเรื่องกัน และ xmas.c ก็ยังถูกมองว่าเป็นตัวอย่างโค้ด C ที่สร้างสรรค์
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
ฝั่ง TeX ก็มีตัวอย่างคล้าย ๆ กันคือ
xii.texถ้าใส่เนื้อหานี้ลงในไฟล์
.texแล้วรันpdftexจากนั้นดู PDF ที่ได้ จะออกมาแบบนี้: https://shreevatsa.net/post/xii/ตอนที่เผยแพร่ครั้งแรกผมดาวน์โหลดเก็บไว้ แต่ต่างจากชื่อไฟล์ในบทความนี้ ไฟล์ของผมชื่อ
carol.cลองคอมไพล์และรันบนระบบสมัยใหม่แล้ว ที่
gcc -o carol carol.cมีคำเตือนอย่างreturn type defaults to ‘int’,type of ‘t’ defaults to ‘int’,type of ‘_’ defaults to ‘int’intอีกแล้ว: https://fedoraproject.org/wiki/Changes/PortingToModernC#Remo...mainมีการเรียกxmas()ก่อนที่จะนิยามถ้าคอมไพล์ด้วย GCC บน macOS จะเกิดข้อผิดพลาด
ISO C99 and later do not support implicit function declarationsและถ้าย้ายmain()ลงไปข้างล่าง ก็จะคอมไพล์ได้ถูกต้องและให้เอาต์พุตที่ถูกต้องเห็นสิ่งนี้แล้วนึกถึง Kolmogorov complexity
โปรแกรมนี้ดูเหมือนพูดจาไม่เป็นเรื่อง แต่ก็สร้างเอาต์พุตที่ต้องการได้ เลยสงสัยว่าจะมีโปรแกรมที่สั้นกว่าและดูไร้เหตุผลยิ่งกว่านี้ แต่ให้เอาต์พุตเดียวกันได้ไหม
แล้วจะหาโปรแกรมแบบนั้นได้อย่างไร?
แต่การค้นหาแบบ brute force ไม่มีประสิทธิภาพอย่างยิ่ง คำตอบที่ทำได้จริงจึงใกล้เคียงกับคำว่า “ทำอย่างชาญฉลาด” ในเชิงคณิตศาสตร์
โดยทั่วไป Kolmogorov complexity เป็นสิ่งที่คำนวณไม่ได้ ดังนั้นจึงไม่มีโปรแกรมที่รับสตริงใด ๆ แล้วส่งคืนโปรแกรมที่สั้นที่สุดซึ่งคำนวณสตริงนั้นได้
อย่างไรก็ตาม โดยหลักการแล้วเป็นไปได้ที่ใครสักคนจะพิสูจน์ว่า Kolmogorov complexity ของสตริงเฉพาะบางตัวมีค่า X
เพราะอย่างนั้นมันจึงเหมาะกับการแข่งขันระยะยาว และด้วยเส้นโค้งการเติบโตแบบลอการิทึม ก็มักมีการค้นพบที่น่าสนใจตรงช่วงท้าย ๆ
ตอนนี้ผมกำลังจัดมินิคอมเพทิชันแข่ง LLM ที่ท่องจำ ตัวเลขของค่า pi ได้มากที่สุดจนถึงเดือนมีนาคมปีหน้า เงินรางวัลปัจจุบันคือ 100 ดอลลาร์ และจะแบ่งตามสัดส่วนการมีส่วนร่วมในสเกล log
ค่า pi ในเชิงทฤษฎีบีบอัดได้ค่อนข้างมาก ดังนั้นน่าจะน่าสนใจหากดูว่าโมเดลสามารถเรียนรู้ชุดน้ำหนักที่ใกล้กับ minimum description length (MDL) ซึ่งกู้คืนอัลกอริทึมการบีบอัดสูงจากข้อมูลได้หรือไม่
แต่ยังไม่ชัดเจนว่าโมเดลสำเร็จรูปทำได้หรือเปล่า ตอนนี้เลยตั้งใจปล่อยไว้เป็นการแข่งขันท่องจำตัวเลขแล้วรอดูไปก่อน
คำอธิบายดี และ IOCCC ก็ดูเหมือนยังมีชีวิตอยู่ต่อในปี 2023: https://www.ioccc.org/years.html
แต่หน้าแรกมีอัปเดตเมื่อเดือนพฤษภาคม 2023 ว่า “มีแผนจะจัด IOCCC ครั้งที่ 28”
มีอะไรบางอย่างที่คุ้มค่ากับการรอ เหมือนกับรีลีสของ Nethack
ช่วงหลังมานี้ผมเพิ่งรู้เรื่องน่าสนใจเกี่ยวกับ The Twelve Days of Christmas คือของขวัญทั้งหมดเป็นนกชนิดหนึ่ง
แม้แต่บรรดาหญิงสาวที่กระโดดและเหล่าขุนนางก็ว่ากันว่าเป็นนกทั้งหมด
ตาม Wikipedia สิ่งพิมพ์เนื้อเพลงที่เก่าแก่ที่สุดเท่าที่รู้จักคือหนังสือเด็กมีภาพประกอบชื่อ Mirth Without Mischief ที่ตีพิมพ์ในลอนดอนเมื่อปี 1780: https://en.wikipedia.org/wiki/The_Twelve_Days_of_Christmas_(...
เว็บไซต์นี้พยายามเชื่อมโยงทุกอย่างเข้ากับนก https://www.birdspot.co.uk/culture/the-birds-of-the-twelve-d... แต่โดยเฉพาะตรง Five Gold Rings นั้นฝืนมาก
ใน Mirth and Mischief มีภาพประกอบที่วาดแหวนไว้อย่างชัดเจนว่าเป็นเครื่องประดับ และ Archive.org ก็มีสแกนไว้ด้วย: https://archive.org/details/mirth_without_mischief/page/n7/m...
มีสิ่งที่ผมเคยค้นคว้าเองเมื่อกว่า 20 ปีก่อนด้วย: http://michaeldnahas.com/xmassong/index.html
ถ้าปิดคำเตือน ก็ยังทำงานได้แม้บน trunk: https://compiler-explorer.com/z/hGvs1e9jo
ทำให้นึกถึงความทรงจำดี ๆ ตอนปี 2022 ซึ่งเป็นสองเทอมสุดท้ายในมหาวิทยาลัย อาจารย์เอาโค้ดชิ้นนี้มาให้ดูทันทีที่เริ่มคาบ
แยกไม่ออกว่าเขาพูดจริงหรือเป็นตลกแบบสุขุม
ตอนเรียนมหาวิทยาลัย อาจารย์ใส่มันไว้ในเอกสารประกอบการสอนภาษา C แบบพิมพ์ออกมา จำได้ว่าครั้งหนึ่งเคยพิมพ์ตามด้วยมือเอง
ใน Rosetta Code ก็มีโจทย์คล้ายกัน
เป็นโปรแกรมที่พิมพ์เพลง Old Lady Swallowed a Fly ซึ่งเป็นเพลงที่ท่อนร้องยาวขึ้นเรื่อย ๆ: https://rosettacode.org/wiki/Old_lady_swallowed_a_fly
puts [zlib inflate [binary decode base64 "7VRNa8MwDL3nV2(...)"]]https://rosettacode.org/wiki/Old_lady_swallowed_a_fly#Tcl
Python, Nim, Julia ฯลฯ ก็น่าจะมีเวอร์ชันคล้าย ๆ กัน