3 คะแนน โดย GN⁺ 2024-04-09 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • แม้จะเป็นเพียงบรรทัด Hello World ที่เขียนด้วย C ก็ต้องผ่านไฟล์ปฏิบัติการที่คอมไพล์แล้ว, C standard library, system call, เคอร์เนล และเทอร์มินัลตามลำดับ ก่อนจะแสดงบนหน้าจอ
  • ผลลัพธ์ที่สร้างด้วย gcc hello.c -o hello คือ ไฟล์ปฏิบัติการ ELF 64-bit x86-64 และโค้ด _start จะถูกรันก่อนที่จุดเริ่มต้น 0x1060 ใน ELF header
  • main() ที่ผู้ใช้เขียนไม่ได้เริ่มทำงานโดยตรง แต่ผ่าน _start และ __libc_start_main ก่อน และ printf("Hello World!\n") ก็ถูกปรับแต่งให้กลายเป็นการเรียก puts() ที่เรียบง่ายกว่า
  • สตริงถูกเก็บเป็นลำดับไบต์ไว้ที่ 0x2004 ใน .rodata และสตริงของ C ใช้ NULL terminator ในการบอกจุดสิ้นสุดแทนข้อมูลความยาว
  • เส้นทางการแสดงผลจริงต่อจากนั้นยังรวมถึง buffering และ locking ของ libc, system call แบบ write หรือ writev, เคอร์เนล Linux, pseudo-terminal และการเรนเดอร์ของ terminal emulator ซึ่งอาจแตกต่างกันไปตามสภาพแวดล้อมที่รัน

เริ่มจาก C Hello World

  • โปรแกรมตัวอย่างคือโค้ดต่อไปนี้ที่เขียนด้วย C
#include <stdio.h>

int main() {
    printf("Hello World!\n");
    return 0;
}
  • แม้จะให้ผลลัพธ์เหมือน print('Hello World!') ของ Python แต่โปรแกรม C ไม่สามารถรันผ่าน interpreter ได้ทันที และต้อง คอมไพล์ ก่อน
gcc hello.c -o hello
./hello
  • ผลลัพธ์การรันเป็นดังนี้
Hello World!
  • หากมีพื้นฐาน C หรือแอสเซมบลีอยู่บ้าง ก็จะตามลำดับการทำงานนี้ได้ไม่ยาก

ตัวตนของไฟล์ปฏิบัติการ

  • ประเด็นสำคัญจากผลลัพธ์ของ file hello คือ ELF executable, x86-64
    • ไฟล์ปฏิบัติการ ELF คือรูปแบบของโปรแกรมที่สามารถรันได้บน Linux
    • x86-64 หมายถึงโปรแกรมภาษาเครื่องสำหรับโปรเซสเซอร์ x86 แบบ 64 บิต
  • เมื่อดู ELF header ด้วย readelf -h hello จะเห็น Entry point address: 0x1060
    • ที่อยู่นี้คือจุดที่ CPU เริ่มรันหลังจากโหลดโปรแกรมเสร็จแล้ว

_start และจุดเข้าสู่ C library

  • เมื่อทำ disassemble ด้วย objdump -D hello จะพบ _start อยู่ที่ตำแหน่ง 0x1060
  • _start ไม่ใช่โค้ดที่ผู้ใช้เขียนเอง แต่เป็นโค้ดที่ compiler หรือให้แม่นยำกว่านั้นคือ linker ใส่เข้ามาให้อัตโนมัติ
  • โค้ดนี้จะทำการเริ่มต้นระบบเบื้องต้น แล้วจึงเรียกคำสั่งต่อไปนี้
call *0x2f53(%rip)        # 3fd8 <__libc_start_main@GLIBC_2.34>
  • ฟังก์ชันนี้ไม่ได้ถูกนิยามไว้โดยตรงในโปรแกรม แต่เป็นส่วนของ standard C library
  • ใน dynamic section ของ readelf -d hello จะเห็น dependency ของ libc.so.6
Shared library: [libc.so.6]
  • libc.so.6 คือ standard C library ของระบบ และไฟล์ .so บน Linux ก็เก็บโค้ดที่หลายโปรแกรมใช้ร่วมกันได้ คล้ายกับ .dll บน Windows
  • C library รับหน้าที่เริ่มต้นระบบ เช่น การจัดการ argument ของ command line และ environment variable จากนั้นจึงเรียก main() และปิดโปรแกรมด้วยค่าที่ฟังก์ชันนั้นคืนกลับมา

สิ่งที่เกิดขึ้นจริงใน main()

  • จากผลการ disassemble จะพบว่า main() อยู่ที่ 0x1149
  • ลำดับการทำงานของ main() มีดังนี้
    • ตั้งค่า stack frame
    • เตรียม argument สำหรับการเรียกฟังก์ชัน
    • เรียกฟังก์ชันสำหรับพิมพ์ Hello World
    • เก็บกวาด stack frame
    • คืนค่ารหัสจบการทำงานเป็น 0
  • จุดสำคัญคือส่วนที่เตรียมที่อยู่ของสตริงเป็น argument แล้วเรียก puts@plt
lea    0xeac(%rip),%rax
call   1050 <puts@plt>
  • แม้ใน source code จะใช้ printf() แต่ compiler ได้ปรับแต่งให้เป็น puts()
    • printf() เป็นฟังก์ชันที่ซับซ้อนสำหรับการแสดงผลแบบจัดรูปแบบ
    • ตัวอย่างนี้ไม่ได้ใช้ความสามารถอย่างการแทรกตัวแปรลงในรูปแบบข้อความ จึงถูกแทนที่ด้วย puts() ที่เรียบง่ายกว่า
    • puts() จะเติมอักขระขึ้นบรรทัดใหม่ต่อท้ายสตริงเอง ดังนั้น \n ในสตริงเดิมจึงถูกเอาออก

วิธีเก็บสตริง

  • สตริงอยู่ที่แอดเดรส 0x2004 ในเซกชัน .rodata
  • ไบต์ที่ตำแหน่งนั้นมีดังนี้
48 65 6c 6c 6f 20 57 6f 72 6c 64 21 00
  • ลำดับไบต์นี้ตีความได้เป็น "Hello World!" และ 0x00 ตัวสุดท้าย
  • 0x00 คือ NULL terminator ซึ่งใช้บอกจุดสิ้นสุดของสตริงในภาษา C
  • เนื่องจากสตริงของ C ไม่ได้เก็บข้อมูลความยาวไว้ด้วย ฟังก์ชันที่รับสตริงจึงต้องประมวลผลทีละไบต์จนกว่าจะพบ NULL terminator
  • หากไม่มี NULL terminator คั่นระหว่างสตริง ฟังก์ชันของ C อาจอ่านต่อไปหลายสตริงติดกัน หรืออ่านหน่วยความจำที่ไม่ควรเข้าถึงจนจบด้วย Segmentation Fault ได้

เส้นทางของ puts() ใน Glibc

  • puts@plt จะเชื่อมต่อไปยัง standard library ในที่สุด
  • ใน Glibc นั้น puts() จะไปต่อที่ _IO_puts
  • _IO_puts ทำงานดังนี้
    • หาความยาวของสตริง
    • ขอ lock สำหรับสตรีมเอาต์พุต stdout
    • ตรวจสอบเงื่อนไขแล้วเรียก _IO_sputn
    • พิมพ์อักขระขึ้นบรรทัดใหม่
    • ปลด lock และคืนจำนวนอักขระที่พิมพ์ออกไป
  • เนื่องจาก implementation ภายในของ Glibc มีขนาดใหญ่และซับซ้อน บทความจึงเปลี่ยนไปตามลำดับการทำงานของ musl libc ที่เล็กกว่าแทน

กระบวนการที่ผลลัพธ์ไหลลงไปใน musl libc

  • puts() ของ musl จะขอ lock ของ stdout แล้วเรียก fputs() และ putc_unlocked('\n', stdout) ก่อนจะปลด lock
  • fputs() จะหาความยาวของสตริงแล้วเรียก fwrite()
  • จากนั้น fwrite() จะขอ lock อีกครั้งและเรียก __fwritex()
  • __fwritex() จะตรวจสอบสถานะของบัฟเฟอร์ และหากจำเป็นก็จะเรียก function pointer write ของ output stream
  • stdout ถูกกำหนดให้เป็น fd = 1 และฟังก์ชัน write จะถูกตั้งค่าเริ่มต้นเป็น __stdout_write
  • __stdout_write() จะทำ TIOCGWINSZ ioctl แล้วจึงเรียก __stdio_write()
  • __stdio_write() จะทำ system call ผ่าน SYS_writev

system call และเคอร์เนล

  • C library เพียงอย่างเดียวไม่สามารถสื่อสารกับฮาร์ดแวร์ได้โดยตรง และการเข้าถึงฮาร์ดแวร์เป็นหน้าที่ของเคอร์เนลระบบปฏิบัติการ
  • คำขอให้แสดงผลจะจบลงที่ system call ซึ่งเป็นการร้องขอให้ระบบปฏิบัติการเขียนข้อความลงใน output stream
  • โดยทั่วไปการแสดงผลจะทำผ่าน system call write ส่วน musl ใช้ writev ที่สามารถเขียนหลายบัฟเฟอร์เป็นอาร์เรย์ได้
  • implementation ของ system call สำหรับ x86-64 ใน musl แบ่งเป็น __syscall0 ถึง __syscall6 ตามจำนวน argument
  • แต่ละฟังก์ชันจะตั้งค่า argument ลงใน CPU register แล้วรันคำสั่ง syscall
    • การควบคุมจะถูกส่งต่อไปยังเคอร์เนล
    • เคอร์เนลจะอ่านพารามิเตอร์จาก register และทำ system call ตามที่ร้องขอ

หลังจากเคอร์เนลจนกระทั่งมองเห็นบนหน้าจอ

  • เคอร์เนล Linux รับ system call write แล้วเขียนข้อมูลลงในไฟล์หรือสตรีมที่เปิดอยู่
  • system call write รับ argument เป็น file descriptor, บัฟเฟอร์ที่จะเขียน และจำนวนไบต์ที่จะเขียน
  • ในสภาพแวดล้อมตัวอย่าง โปรแกรม hello ถูกรันใน GNOME terminal emulator และ stdout เชื่อมต่ออยู่กับ pseudo-terminal /dev/pts/0
  • เคอร์เนลจะเก็บข้อความ Hello World ไว้ในบัฟเฟอร์ จากนั้น terminal emulator จะอ่านข้อมูลนั้นแล้วแสดงบนหน้าจอ
  • terminal emulator จะเรนเดอร์ข้อความเป็นเฟรม แล้ว X server หรือ compositor จะนำไปผสมกับหน้าจอของแอปอื่น ก่อนให้เคอร์เนลแสดงผลบนดิสเพลย์
  • หลังจากนั้นเส้นทางอาจแตกต่างกันไปตามสภาพแวดล้อมการรัน
    • ในการล็อกอินระยะไกล เคอร์เนลจะส่งข้อความไปยัง sshd และ sshd จะห่อเป็นแพ็กเก็ตเข้ารหัสก่อนส่งกลับผ่านเคอร์เนลออกสู่อินเทอร์เน็ต
    • หากใช้ physical terminal และ serial-to-USB adapter เคอร์เนลจะส่งข้อความเป็นแพ็กเก็ต USB
    • ใน framebuffer console เคอร์เนลจะเรนเดอร์ข้อความเป็นเฟรมแล้วแสดงออกสู่ดิสเพลย์

ความซับซ้อนที่ซ่อนอยู่หลังเอาต์พุตเล็ก ๆ หนึ่งครั้ง

  • การส่งข้อความ Hello World เป็นเพียง system call หนึ่งครั้งที่เกิดจากโปรแกรมหนึ่งตัวเท่านั้น
  • ซอฟต์แวร์และฮาร์ดแวร์สมัยใหม่ประกอบด้วยชั้นการทำงานที่ซับซ้อนและละเอียดอ่อนมาก จนแม้แต่การทำงานเล็ก ๆ เพียงครั้งเดียวก็ยากจะไล่ตามได้ครบถ้วน
  • คำอธิบายนี้ละรายละเอียดปลีกย่อย ข้อยกเว้น และการทำงานภายในของเคอร์เนลไว้อีกมาก และติดตามเฉพาะลำดับหลักเท่านั้น

1 ความคิดเห็น

 
GN⁺ 2024-04-09
ความคิดเห็นจาก Hacker News
  • ผมลองทำอะไรคล้าย ๆ กันด้วย Rust บน macOS เพราะว่าง ๆ โดยใช้ #![no_std], #![no_main] และเรียก system call WRITE/EXIT เองโดยตรง แต่ “Hello, world!” ที่ได้พอเปิดดูด้วย Ghidra แล้ว ไม่ว่าจะทำอย่างไรก็มีขนาดเอาต์พุตอยู่ที่ ประมาณ 16KB
    น่าจะยังเล่น code golf ลดลงได้อีก แต่ดูมีโอกาสสูงว่ามีคนเคยลองและเขียนเอกสารไว้แล้ว

    • บน Windows โปรแกรมคล้ายกันมีขนาด 3072 ไบต์ และคอมไพล์ด้วย rustc hello.rs -C panic=abort -C opt-level=3 -C link-arg=/entry:main
      เรียก ExitProcess, GetStdHandle, WriteFile ของ kernel32 โดยตรง และเพราะเป็น hello world เลยปล่อย panic handler ไว้แบบคร่าว ๆ ในไฟล์ executable ยังมี padding อยู่ค่อนข้างมาก จึงยังใส่อะไรเพิ่มได้โดยไม่ทำให้ขนาดใหญ่ขึ้น และอาจลดได้ด้วยวิธีที่ “อาชญากรรม” กว่านี้ แต่ดูไม่ค่อยมีความหมายเท่าไร
      อย่างไรก็ดี ฐานข้อมูลดีบัก PDB ที่เกี่ยวข้องมีขนาด 208,896 ไบต์
    • ถ้าจะทำให้เล็กที่สุด ต้องทิ้ง main ไปเลยแล้วใช้ _start และต้องส่ง linker flag เพื่อไม่ให้จัดแนว section ด้วย
      ดู https://darkcoding.net/software/a-very-small-rust-binary-ind... วิธีนี้ทำให้ลงไปใกล้ 500 ไบต์ได้ง่าย ๆ
    • code golf ก็สนุกดี แต่ควรดูด้วยว่า page size ทุกวันนี้เป็นเท่าไร
      ถ้าเป็นภาษาที่มี stack สุดท้ายไฟล์ executable ก็น่าจะถูกโหลดขึ้นมาอย่างน้อยราวสอง page คือ page แบบอ่านอย่างเดียว/อ่าน-เขียน
    • โปรเจกต์ min-sized-rust รวบรวม optimization จำนวนมากสำหรับลดขนาดไบนารีของ Rust ไว้
      ถ้าจำไม่ผิด เมื่อใช้ optimization ทั้งหมดแล้ว hello world สุดท้ายอยู่ที่ประมาณ 8KB: https://github.com/johnthagen/min-sized-rust
    • XNU ไม่โหลด Mach-O ที่เล็กกว่าหนึ่ง page ดังนั้นบนแพลตฟอร์มนั้นจึงน่าเสียดายที่ไม่ค่อยมีพื้นที่ให้เล่นกับไบนารีขนาดเล็กมากนัก
  • ยังมี rabbit hole อีกเรื่องที่ Musl ข้ามไป บน Linux การเรียกฟังก์ชันระบบไม่ได้มีแค่การใช้ syscall โดยตรง
    วิธีที่ “สุภาพ” กว่าคือการเรียก vDSO ซึ่งเป็นไลบรารีเล็ก ๆ แบบมหัศจรรย์ที่ kernel map เข้ามาใน address space ให้อัตโนมัติ ทำให้ kernel สามารถจัดเตรียมโค้ดที่เหมาะที่สุดสำหรับการทำ system call ได้
    system call บางตัวอาจรันใน user space จนไม่ต้องใช้ syscall เองเลยด้วยซ้ำ และในอดีต vDSO ยังเคยเลือกหนึ่งในกลไกเรียก kernel อย่าง int 0x80 หรือ sysenter ให้ด้วย
    https://man7.org/linux/man-pages/man7/vdso.7.html

    • มีเฉพาะบน x86 32 บิต เท่านั้นที่ vDSO มีชิมสำหรับ system call แบบเร็วทั่วไป
      บน x86-64 วิธีมาตรฐานของ system call คือคำสั่ง SYSCALL และใน vDSO มีเพียงฟังก์ชันเกี่ยวกับเวลา กับฟังก์ชันเกี่ยวกับ SGX อีกไม่กี่ตัว
  • บทความที่เปรียบเทียบ overhead ของโปรแกรม “Hello World” ในแต่ละภาษาก็น่าอ่าน: https://drewdevault.com/2020/01/04/Slow.html
    บทความต่อเนื่อง: https://drewdevault.com/2020/01/08/Re-Slow.html
    ยังมีบทความระดับตำนานที่ทำโปรแกรมเล็กที่สุดบน Linux ด้วย โปรแกรมนั้นแค่จบการทำงานด้วย status code 42: https://www.muppetlabs.com/~breadbox/software/tiny/teensy.ht...
    ในเว็บเดียวกันยังหาโปรแกรม “Hello World” ที่เล็กที่สุดได้ด้วย

  • บทความนี้แทบจะข้ามบทบาทของ dynamic linker ซึ่งอาจมองได้ว่าเป็น entry point ตัวจริงของโปรแกรมไป
    ถ้าสนใจมุมมองนี้ ดูได้ที่ https://gist.github.com/kenballus/c7eff5db56aa8e4810d39021b2...

  • ถ้าคุณเป็นคนรัก DOS, “hello, world” ที่เขียนด้วย assembly/machine code บน DOS เคยทำให้เล็กได้ถึง 23 ไบต์: https://github.com/susam/hello
    ใน 23 ไบต์นี้ 15 ไบต์เป็นตัวสตริงที่ลงท้ายด้วยเครื่องหมายดอลลาร์เอง ดังนั้นโค้ด machine code จริง ๆ มีเพียง 8 ไบต์จากคำสั่ง x86 สี่คำสั่งเท่านั้น

  • บทความดี แต่ผมอยากให้ทำเพิ่มอีกสองอย่าง ควรปิด optimization และ inlining ที่ทำให้ printf ถูกเปลี่ยนเป็น puts หรือไม่ก็เขียน hello world ที่ใช้ puts โดยตรงตั้งแต่แรกจะดีกว่า
    อีกอย่างคือ ถ้าแบ่งขั้นตอนคอมไพล์เป็นสี่ขั้นคือ preprocessing, compiling, assembling, linking หรือใส่ --save-temps ให้ cc แล้วอธิบายไฟล์ที่ถูกสร้างขึ้น ก็น่าจะดี พอเห็น pipeline ด้วยตัวเอง ส่วนที่ดูเหมือนเวทมนตร์จะลดลงไปมาก

  • ทำให้นึกถึงการบ้านที่เคยชอบในวิชาการเขียนโปรแกรมระบบที่มหาวิทยาลัย: เป็นโจทย์ประมาณว่า “ให้โค้ด C++ hello world มาหนึ่งชิ้น แล้วส่งไบนารีที่คอมไพล์แล้วให้มีขนาดเล็กที่สุดเท่าที่เป็นไปได้”
    ยังจำได้ว่าใช้เครื่องมืออย่าง readelf กับ objdump ส่องดูโปรแกรม แล้วค่อย ๆ ลอกเลเยอร์กับการปรับแต่งของคอมไพเลอร์ออก จนลดเหลือไบนารีที่เล็กที่สุดซึ่งยังพิมพ์ “hello world” ได้
    แน่นอนว่าพอลองค้นดูก็พบว่ามีคนทำได้ดีกว่านักศึกษาเยอะมาก: https://www.muppetlabs.com/%7Ebreadbox/software/tiny/teensy....

    • สงสัยเหมือนกันว่าการที่ชิ้นนั้นเป็น C++ มีความหมายอะไรไหม
      แค่สร้างไบนารีที่เล็กที่สุดซึ่งพิมพ์ hello world แล้วอ้างได้ว่าเทียบเท่ากันในเชิงความหมายก็น่าจะพอไม่ใช่หรือ แม้รวมข้อมูลสตริงด้วย ก็น่าจะใช้คำสั่ง x86 แค่ราว ๆ สิบกว่าคำสั่ง
    • ถ้านี่เป็นโจทย์ที่คนชอบ ก็สงสัยว่าทำไมถึงไม่ค่อยเห็นคนทำ ไบนารีที่เล็กที่สุดเท่าที่เป็นไปได้ สำหรับโปรแกรมที่ไม่ใช่ “hello world” มากกว่านี้
      ส่วนตัวแล้วผมชอบประหยัดพื้นที่ในคอมพิวเตอร์ของตัวเอง เลยรู้สึกว่าน่าสนุก แต่ทุกวันนี้มีโปรแกรมจำนวนมากที่ถูกเขียนออกมาขนาดเกิน 10MiB, 20MiB, 50MiB, 100MiB บางตัวทำขึ้นในสภาพแวดล้อมเชิงพาณิชย์เพื่อวัตถุประสงค์เชิงพาณิชย์ก็จริง แต่ก็มีหลายโปรแกรมที่บอกว่าเขียนขึ้นเพื่อความสนุกล้วน ๆ ไม่มีความสุขจากการใช้โปรแกรมขนาดเล็กบ้างหรือ
  • การจบแบบ “เลยเที่ยงคืนแล้ว คงต้องไปนอน” กลับเป็น บทสรุปที่สมบูรณ์แบบ สำหรับบทความนี้

  • น่าเสียดายที่เหมือนกับการเจาะลึก “hello world” หลาย ๆ ครั้ง บทความนี้ก็หยุดอยู่ที่ system call write แล้วข้ามส่วนที่เหลือไปแบบคร่าว ๆ
    ก่อนถึง system call โดยแก่นแล้วก็เป็นแค่ลูกโซ่ของการเรียกฟังก์ชันที่ printf เรียก puts, puts เรียก write, ส่ง char const* ต่อกันไปพร้อมทำบัญชีเล็กน้อย ซึ่งโดยส่วนตัวไม่ใช่ส่วนที่น่าสนใจที่สุด
    ส่วนที่น่าสนใจและซับซ้อนจริง ๆ เริ่มหลัง system call ต่างหาก เคอร์เนลเชื่อม stdout ของโปรเซสเข้ากับอินพุตของ terminal emulator และเทอร์มินัลก็เตรียม framebuffer ผ่านไลบรารีเรนเดอร์ฟอนต์กับไดรเวอร์ GPU อ่านเส้นขอบตัวอักษรของฟอนต์ที่ตรงกับไบต์อักขระจากดิสก์ ปรับให้เข้ากับ viewport แล้วใช้การปรับขนาด·kerning·font metrics จากนั้น GPU จึงทำ rasterization และ anti-aliasing
    ต่อจากนั้น window manager จะ composite กรอบหน้าต่างเทอร์มินัลกับเดสก์ท็อป และถ้ามีความโปร่งใสหรือเอฟเฟกต์กระจกฝ้า ก็ประมวลผลด้วย shader framebuffer ผลลัพธ์จะถูกทำเป็นแพ็กเก็ตเป็นสัญญาณ HDMI หรือ DisplayPort ให้ตรงกับความละเอียดและความลึกสีของมอนิเตอร์ ผ่านสายเคเบิลและวงจรอินพุตของจอ แล้วเปลี่ยนเป็นสัญญาณระบุตำแหน่งพิกเซล วิธีรีเฟรชจะแตกต่างกันไปตาม LCD, OLED, พลาสมา, CRT เช่น WRGB OLED ขนาด 3840×2400 ต้องจัดการซับพิกเซลประมาณ 36.86 ล้านจุด
    กระบวนการทั้งหมดนี้เกิดขึ้นภายใน 16.67ms ซึ่งเป็นเวลาหนึ่งเฟรมที่ 60Hz

    • คำอธิบายดีนะ แต่สุดท้ายก็หยุดอยู่ที่ ระบบการมองเห็นของมนุษย์ และตรงนั้นก็เป็นอีกส่วนที่น่าสนใจและซับซ้อนจริง ๆ :)
      https://en.wikipedia.org/wiki/Visual_system
    • ถ้าชอบการเจาะลึกระดับนี้ น่าจะชอบบทความของ Gynvael Coldwind ที่ขุดลงไปว่า Python hello world ทำงานบน Windows อย่างไร
      ครอบคลุมภายในของ CPython, Windows conhost, การ rasterize ฟอนต์, การเรนเดอร์ด้วย GPU ฯลฯ: https://gynvael.coldwind.pl/?id=754
    • ส่วนใหญ่ทั้งหมดนี้ไม่เกี่ยวกับตัวโปรแกรมเอง เช่น ถ้า pipe เอาต์พุตไปที่ /dev/null เรื่องพวกนี้ก็จะไม่เกิดขึ้น
    • สิ่งที่เกิดขึ้นก่อน _start ก็ยังขาดไปด้วย เช่น วิธีที่โปรเซสถือกำเนิดบน Linux โดยเฉพาะ execve ที่ค่อนข้างแปลก, กระบวนการโหลดโปรแกรมเข้าสู่หน่วยความจำ, binfmt_* และ binfmt_misc อันทรงพลัง, relocation, exception handling frame, section, ตัวโหลด ELF โดยรวม, รวมถึงการจัดสรรทรัพยากรของระบบปฏิบัติการอย่าง malloc ที่จำเป็น เป็นต้น
  • ประโยคที่ว่า “ต่างจาก Python เราเรียก interpreter เพื่อรันโปรแกรมนี้ไม่ได้” ก็ไม่ได้ถูกต้องเสียทีเดียว
    ใช้ tcc -run hello.c ก็ทำได้ พูดให้เคร่งครัดแล้วมันใกล้เคียงกับ คอมไพเลอร์ในหน่วยความจำ มากกว่า interpreter ก็เถอะ
    ถ้าอยากได้คะแนนความเนิร์ดเพิ่ม ก็ทำให้โปรแกรมพูดว่า “Hellorld” แทน “Hello world” ก็ได้