- นิยาม initrd ว่าเป็น หน่วยโปรแกรมที่เคอร์เนลตีความและรันโดยตรง และตีความ Linux ใหม่ให้เป็น อินเทอร์พรีเตอร์ ชนิดหนึ่ง
- ใช้
kexec, base64, cpio เพื่อสร้าง ดิสโทร Linux แบบเรียกซ้ำที่รีบูตตัวเอง โดย initrd จะรันตัวเองอีกครั้ง
- หากทำให้สคริปต์
/init พิมพ์ cpio image ของตัวเองออกมา ก็จะเกิด initrd แบบจำลองตัวเองในรูปแบบ Quine
- อธิบาย โครงสร้างลำดับชั้นของอินเทอร์พรีเตอร์ที่ต่อเนื่องไปจนถึงเคอร์เนล ผ่านโครงสร้างการรัน ELF และ
ld.so, binfmt_misc
- หากใช้
kexec หรือ QEMU ก็สามารถ รัน Linux อีกตัวบน Linux แบบ tail-recursive ได้ ทำให้ทดลองขยายขอบเขตของเคอร์เนล เวอร์ชวลไลเซชัน และอินเทอร์พรีเตอร์ได้
การย้อนวิเคราะห์ rkx.gz และโครงสร้าง initrd แบบเรียกซ้ำตัวเอง
- คำสั่ง
curl https://astrid.tech/rkx.gz | gunzip | sudo sh จะดาวน์โหลดและรัน เชลล์สคริปต์ที่เข้ารหัสแบบ base64 ขนาด 20MB
- สคริปต์จะตรวจสอบสิทธิ์ root และตรวจเช็กว่ามี
kexec, base64, cpio อยู่หรือไม่
- ถอดรหัสข้อมูล base64 เพื่อสร้าง cpio archive ชื่อ
r และแตกไฟล์อิมเมจเคอร์เนลชื่อ k ออกมาจากข้างใน
- ใช้
kexec โหลด k เป็นเคอร์เนล และ r เป็น ramdisk จากนั้นจึงรัน
- ภายใน
r.cpio มีไฟล์ /bin, /init, k โดย k คือ อิมเมจเคอร์เนล Linux 6.18.18 และ /init อยู่ในรูปเชลล์สคริปต์
/init จะเมานต์ /proc แล้วรวมระบบไฟล์ปัจจุบันเป็น cpio ไปไว้ที่ /r ก่อนจะใช้ kexec รัน /k และ /r อีกครั้ง
- ผลลัพธ์คือกลายเป็น ดิสโทร Linux แบบเรียกซ้ำที่รีบูตตัวเองต่อเนื่อง
มุมมองที่มอง Linux kernel เป็นอินเทอร์พรีเตอร์
- initrd ไม่ใช่เพียง ramdisk สำหรับบูตเท่านั้น แต่สามารถมองได้ว่าเป็น โปรแกรมที่ Linux kernel ตีความและรัน
- เช่นเดียวกับ
curl | sh หรือ python3 script.py initrd ก็เป็นรูปแบบของโปรแกรมอินพุตที่ถูกเคอร์เนลรัน
- ดังนั้น Linux kernel จึงทำหน้าที่เป็นอินเทอร์พรีเตอร์ที่ตีความ initrd
- โครงสร้างนี้คล้ายกับ การปรับให้เหมาะสมแบบ tail-call optimization
kexec จะโหลดและรันในพื้นที่หน่วยความจำใหม่ โดยไม่เขียนทับเคอร์เนลก่อนหน้า
- แต่ละเคอร์เนลจะไม่คงสถานะเดิมไว้ และถูกแทนที่ด้วย “stack frame” ใหม่
Quine และการจำลองตัวเองของ initrd
- Quine หมายถึง โปรแกรมที่พิมพ์ตัวมันเองออกมา
- หากสคริปต์
/init ทำ cat /r ในตอนท้าย ก็จะพิมพ์ cpio ที่เหมือนกับตัวเองออกมา
- ในกรณีนี้จะเกิด Quine ของอินเทอร์พรีเตอร์ Linux initrd
- เนื่องจากทุกไฟล์อยู่บน
tmpfs ใน RAM จึงไม่มี disk I/O จริงเกิดขึ้น
ELF, ld.so และลำดับชั้นของอินเทอร์พรีเตอร์
- ไฟล์รันแบบ ELF มีพาธของ อินเทอร์พรีเตอร์ (
ld-linux-x86-64.so.2) อยู่ใน header
- ตอนรัน เคอร์เนลจะรัน
ld.so ก่อน แล้ว ld.so จะโหลดไลบรารีแบบไดนามิกของ ELF และรันโปรแกรม
- ดังนั้น ELF เองก็อาจมองได้ว่าเป็น ภาษาอินเทอร์พรีเตอร์ ชนิดหนึ่ง
/bin/sh ถูกตีความโดย ld.so และ ld.so ก็ถูกตีความโดยเคอร์เนลโดยตรง
ld.so เป็น ELF ที่ลิงก์แบบสแตติก จึงสามารถรันโดยเคอร์เนลได้โดยตรง
- สิ่งนี้ทำให้เกิด กรณีฐาน (base case) ของลำดับชั้นอินเทอร์พรีเตอร์
การรัน CPIO ผ่าน binfmt_misc
- หากใช้
binfmt_misc จะสามารถ รันไฟล์ที่มี magic byte เฉพาะด้วยอินเทอร์พรีเตอร์ที่กำหนดไว้ ได้
- สามารถลงทะเบียนสคริปต์ที่ใช้ QEMU รัน CPIO เป็น initrd ให้เป็นอินเทอร์พรีเตอร์ได้
- QEMU จะบูตเครื่องเสมือนด้วยเคอร์เนลและ initrd ที่กำหนด
- ผลลัพธ์คือ อินเทอร์พรีเตอร์ของไฟล์ CPIO ก็คือ Linux kernel ที่ QEMU บูตขึ้นมา
อินเทอร์พรีเตอร์แบบเรียกซ้ำและ “ลูปที่ประหลาดที่สุด”
- อินเทอร์พรีเตอร์ที่อิง QEMU จะสร้าง stack frame ของสภาพแวดล้อม Linux ใหม่
- เป็นโครงสร้างที่รัน Linux อีกตัวบน Linux และสามารถซ้อนกันได้จนกว่าจะถึงข้อจำกัดของหน่วยความจำ
- หากแทนที่ด้วยอินเทอร์พรีเตอร์ที่อิง
kexec ก็จะทำให้ การรัน Linux แบบเรียกซ้ำที่ปรับ tail call แล้ว เป็นไปได้
- หากตั้งค่าให้ลงทะเบียน binfmt_misc ใน
/init แล้วรัน /r
initrd ที่รันตัวเอง ก็จะสมบูรณ์
/r คือ init process ถัดไปในฟอร์แมต CPIO และเมื่อรันก็จะตีความตัวเองอีกครั้ง
บทสรุป
- initrd ไม่ได้เป็นแค่เครื่องมือบูต แต่เป็น หน่วยโปรแกรมที่ Linux kernel ตีความ
- เมื่อใช้
kexec และ binfmt_misc ก็จะสามารถ รัน Linux เองซ้ำแบบเรียกซ้ำได้ราวกับเป็นอินเทอร์พรีเตอร์
- โครงสร้างนี้เป็นแนวคิดเชิงทดลองที่ ลบเส้นแบ่งระหว่างเคอร์เนล เวอร์ชวลไลเซชัน อินเทอร์พรีเตอร์ และโปรแกรมจำลองตัวเอง
- ซอร์สโค้ดที่เกี่ยวข้องเผยแพร่อยู่ใน GitHub repository ifd3f/rekexec
2 ความคิดเห็น
ไม่รู้อะไรแต่กลับกล้าพูดจริงๆ.. อยากให้เลี่ยงบทความแบบนี้ครับ
ความคิดเห็นจาก Hacker News
อ่านบทความนี้แล้วทรมานมากเพราะมี ความเข้าใจผิด เยอะเกินไป
cpio archive ไม่ใช่ filesystem ผู้เขียนใช้ initramfs ซึ่งมีพื้นฐานอยู่บน tmpfs Linux สามารถแตก cpio ลง tmpfs ได้ archive ของไฟล์และไดเรกทอรีไม่ได้เป็นโปรแกรมในตัวมันเอง
แค่บางอย่างดูคล้ายกัน ไม่ได้แปลว่ามันเหมือนกัน โปรแกรมไบนารี ถูกรันบน CPU และถ้ามี interpreter อยู่ มันก็ซ่อนอยู่ในสภาพแวดล้อมฮาร์ดแวร์ นั่นอยู่นอกขอบเขตของเคอร์เนล
การรัน shell script ต้องมี shell ที่ใช้ตีความสคริปต์นั้น ผู้เขียนละส่วนนี้ไปและสับสนระหว่างเคอร์เนลกับโปรแกรม shell
Linux สามารถคอมไพล์ได้โดยไม่ต้องมี initramfs หรือ ramdisk และก็ยังสามารถรัน userland ของ filesystem ได้อยู่
คำว่า “Linux initrd interpreter” เป็นคำอธิบายที่ผิดมากจริงๆ
ระบบปฏิบัติการทุกตัวไม่ได้ทำหน้าที่เป็น interpreter ของ machine code ด้วยสิทธิ์ระดับเคอร์เนลหรอกหรือ?
บทความนี้โอเคถ้ามองเป็น mental model ว่า “Linux คือ interpreter” แต่ถ้ารับตามตัวอักษรก็ผิด
ถ้าไม่มองว่าเป็นการตีความในระดับคำสั่งของ CPU แต่เป็นบทบาทของเคอร์เนลในการ ประสานงาน รูปแบบการรันอย่าง ELF, shebang script และ initramfs ก็จะสมเหตุสมผลกว่า ความสับสนน่าจะมาจากการปนกันของความหมายสองแบบของคำว่า ‘interpreter’
ประเด็นสำคัญไม่ใช่ว่าอุปมาเปรียบเทียบนี้ถูกไหม แต่คือมันแสดงให้เห็นว่าแนวคิดเรื่อง ‘การรัน’ นั้นขึ้นกับสภาพแวดล้อมมากแค่ไหน
“ทุกอย่างคือ interpreter?”
Theta Combinator ของ Turing
ในบทความก่อนหน้าของซีรีส์นี้ ผู้เขียนบอกว่าไม่อยากใช้ object storage ของ Contabo เลยสร้างอิมเมจ VPS เอง
ฉันคิดว่ามี จุดสมดุล ระหว่างการใช้เวลา 50 ชั่วโมงเพื่อประหยัด 1.50 ดอลลาร์ต่อเดือน กับอีกสุดขั้วคือใช้เงิน 250,000 ดอลลาร์ไปกับ token
ถ้ารับภาระค่า infra ไม่ไหว ปัญหาอาจอยู่ที่ ปัจจัยทางสังคม มากกว่าความสามารถทางเทคนิค การหมกมุ่นกับการรัน Doom ผ่าน curl ดูไม่ค่อยก่อผลผลิตเท่าไร
ถ้าเปิด
man ld.soจะเห็นว่ามันระบุชัดว่า dynamic linker ที่เก็บอยู่ใน section.interpของ ELF จะถูกเรียกใช้งาน ชื่อ section เองก็น่าสนใจLinux มีประโยชน์มากในฐานะ อินเทอร์เฟซที่ตั้งโปรแกรมได้ Windows ก็ทำได้เหมือนกัน แต่รู้สึกว่า Linux เหมาะกว่า
ฉันคิดว่า GUI บน Windows ดีกว่า แต่ GNOME หรือ KDE ก็ใช้งานไม่สะดวกเหมือนกัน เลยใช้ fluxbox, icewm และบางครั้งก็ xfce หรือ mate-desktop ทุกวันนี้ฉันชอบสภาพแวดล้อมที่เรียบง่ายและเร็ว งานส่วนใหญ่ทำผ่าน command line และการแก้โค้ด