1 คะแนน โดย GN⁺ 4 시간 전 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • ไบนารี Rust จะผ่าน ขั้นตอนเริ่มต้นรันไทม์ ก่อน fn main() และในช่วงนี้จะมีการทำงานอย่างการเตรียมการจัดการ panic·unwinding และการแปลงอาร์กิวเมนต์ของโปรแกรม
  • เมื่อ OS loader ส่งต่อการควบคุมไปยัง entry point แล้ว C runtime และ Rust runtime จะเรียกฟังก์ชันเริ่มต้นต่าง ๆ และสามารถวาง โค้ด pre-main ได้ผ่าน #[unsafe(link_section = "...")] และแนวทางแบบ constructor
  • ลิงเกอร์เซกชัน ช่วยรวบรวมข้อมูลที่หลาย crate ส่งเข้ามาไว้ในที่เดียวตอนสร้างไบนารี และ link-section ทำให้ใช้งานสิ่งนี้ได้เหมือน Rust slice
  • เมื่อใช้ ctor ร่วมกับ link-section ก็สามารถจัดรูปแบบอย่างการลงทะเบียน CLI subcommand หรือการจัดเรียง string interning pool ให้เสร็จก่อน main และหลังจากนั้นอ่านได้โดยไม่ต้องล็อก
  • วิธีนี้ให้ทั้งการรวมข้อมูลโดยไม่ต้อง allocate และ inversion of control แต่ควรเลือกใช้ด้วยความระมัดระวัง เพราะมีข้อจำกัดเรื่องการลบ dead code, ข้อจำกัดของ constructor, ความต่างระหว่างแพลตฟอร์ม และข้อจำกัดด้านความเข้ากันได้กับ Miri

ช่วงก่อน main ของไบนารี Rust

  • ไบนารี Rust ทุกตัวมี fn main() แต่ลำดับการทำงานจริงจะผ่าน OS loader และการเริ่มต้น runtime ก่อนจึงจะไปถึง main
  • ในภาษา C มี C runtime ที่มักรับรู้กันในชื่อ libc ส่วน Rust ก็มี runtime ของตัวเองผ่าน standard library และสร้าง abstraction ระดับสูงกว่าไว้บน C runtime
  • จุดประสงค์ของ runtime คือเชื่อมโค้ดของนักพัฒนาเข้ากับระบบปฏิบัติการของแพลตฟอร์ม
  • C runtime จะเตรียมบริการ runtime ต่าง ๆ ก่อน main เช่น allocation, การเข้าถึงไฟล์ และ thread-local storage
  • ในช่วงนี้ Rust จะเตรียมการจัดการ panic และ unwinding และแปลง program arguments แบบ C ไปเป็นอินเทอร์เฟซ std::env::args
  • ช่วง pre-main ทำงานก่อนโค้ดของผู้ใช้ เป็น single-threaded และมีลำดับที่คาดเดาได้ จึงเหมาะกับการเริ่มต้นแบบกำหนดแน่นอน

Entry point

  • ไบนารีเริ่มทำงานเมื่อ OS loader โหลดไบนารีเข้า memory จัดสภาพแวดล้อม แล้วส่งต่อการควบคุมให้
  • บน Linux ค่า entry point จะเก็บอยู่ในฟิลด์ e_entry ของ ELF header โดยปกติลิงเกอร์จะใส่ที่อยู่ของสัญลักษณ์ _start
  • บน Windows ก็มี hook ที่คล้ายกัน โดยไฟล์ executable จะเริ่มที่ฟังก์ชัน _WinMainCRTStartup
  • การ bootstrap runtime ในระยะแรกเป็นเพียงต้นไม้ของการเรียกฟังก์ชันแบบ static เช่น การเริ่มต้น file I/O และ allocator
  • เมื่อ runtime ซับซ้อนขึ้น ต้นไม้เรียก static initialization ก็ใหญ่ขึ้นด้วย และไบนารีก็เริ่มพ่วงฟีเจอร์ของ C runtime มากขึ้นทั้งที่อาจจำเป็นหรือไม่จำเป็น
  • เมื่อ linker สามารถตัดโค้ดที่ไม่ได้ใช้ออกได้ก่อนสร้างไบนารี จึงต้องมีวิธีใหม่มาแทนต้นไม้เรียก static initialization เดิม
  • วิธี __attribute__((constructor)) ของ GCC คือการวางลิสต์ฟังก์ชันพอยน์เตอร์สำหรับ initialization ไว้ในพื้นที่ต่อเนื่องของไบนารี แล้วให้ C runtime วนเรียกตอนเริ่มต้น
  • ต่อมาสามารถกำหนด priority ให้ constructor ได้ เช่น อาจต้อง initialize malloc ก่อน file I/O แบบมี buffering
  • glibc runtime รุ่นใหม่บน Linux จะเก็บ function pointer ไว้ใน .init_array และกำหนดลำดับการทำงานด้วย suffix ตัวเลข
  • ค่า priority ที่ต่ำกว่าหรือเท่ากับ 100 ถูกสงวนไว้ให้ runtime เอง ดังนั้นโค้ดที่ใช้ C runtime ควรใช้ 101 ขึ้นไป
  • ใน Rust สามารถวาง function pointer สำหรับ initialization ได้ด้วยแอตทริบิวต์อย่าง #[used] และ #[unsafe(link_section = ".init_array.101")]

linktime: ctor, link-section และอื่น ๆ

  • ตัวอย่างนี้ใช้ได้บน Linux และ BSD หลายตัว แต่ไม่ได้ออกแบบเป็นตัวอย่าง cross-platform โดยสมบูรณ์
  • macOS รองรับสัญลักษณ์ start และ stop แต่ใช้ชื่อไม่เหมือนกัน ส่วน Windows ไม่รองรับ start และ stop โดยตรง แต่มีหลักการจัดเรียง section ที่เทียบเท่ากันได้แทบทั้งหมด
  • ctor และ link-section เป็น crate ในโปรเจกต์ linktime ที่ทำ abstraction ความต่างรายแพลตฟอร์มและความซับซ้อนของงานฝั่ง linker
  • inventory และ linkme เป็น crate ที่นิยมใช้และสร้างบนหลักการเดียวกัน แต่มีข้อจำกัดสำหรับตัวอย่างนี้
  • crate ctor จัดการ boilerplate สำหรับลงทะเบียน constructor แบบ cross-platform
  • ฟังก์ชันที่ติดแอตทริบิวต์อย่าง #[ctor(unsafe, priority = 101)] จะถูก C runtime เรียกหลังจาก linker จัดทุกอย่างแล้ว แม้จะไม่ได้ถูกเรียกโดยตรงจากโค้ดก็ตาม

Section และ linker script

  • คอมไพเลอร์สามารถวางข้อมูลหรือโค้ดไว้ในตำแหน่งเฉพาะภายในไบนารี ซึ่งบนแพลตฟอร์มส่วนใหญ่เรียกว่า section
  • Rust ก็ใช้ความสามารถในการจัดโครงสร้างแบบเดียวกันนี้ได้ผ่านแอตทริบิวต์ link_section
  • ลิงเกอร์จำนวนมากเปิดให้นักพัฒนาส่ง linker script เข้าไปได้ โดยไฟล์ข้อความนี้จะบอกลิงเกอร์ว่า object file ต่าง ๆ ควรถูกประกอบเข้าด้วยกันอย่างไร
  • ด้วย linker script ไฟล์ C เพียงไฟล์เดียวอาจถูกสร้างเป็นไฟล์ executable บน Linux หรือเป็นบล็อก assembly ดิบที่วางลงใน boot sector ของฮาร์ดดิสก์ก็ได้
  • linker script สามารถนิยาม virtual symbol ที่ไม่มีอยู่ใน source file แต่ใช้เข้าถึง data pointer พื้นฐานของไบนารีที่โหลดจากโค้ด C ได้
  • ในตัวอย่าง linker script สัญลักษณ์ _TEXT_START_ และ _TEXT_END_ ถูกนิยามให้ชี้ไปยังจุดเริ่มและจุดสิ้นสุดของ section .text
  • จุด . ใน _TEXT_START_ = .; หมายถึง location counter ซึ่งตีความเป็นค่าที่ใกล้กับ output address ปัจจุบันของไบนารี

Linker symbol

  • ลิงเกอร์ไม่ได้ตั้งค่าสัญลักษณ์เริ่มต้น·สิ้นสุดเป็นค่าพอยน์เตอร์โดยตรง แต่ตั้ง address ที่ static ชื่อเดียวกันจะถูกวางไว้
  • สัญลักษณ์เริ่มต้น·สิ้นสุดไม่ใช่พอยน์เตอร์ *const Type และไม่มีข้อมูลของตัวเอง มีเพียง address ที่มีความหมาย
  • section ประกอบด้วยข้อมูลในช่วงที่รวมสัญลักษณ์เริ่มต้น แต่ไม่รวมสัญลักษณ์สิ้นสุด
  • ลิงเกอร์หลายตัวมีความสามารถในการนิยามขอบเขตของทุก section ใน executable ให้อัตโนมัติ
  • ใน GNU toolchain หากมี section ชื่อ MY_SECTION จะมีการนิยามสัญลักษณ์ __start_MY_SECTION และ __stop_MY_SECTION ให้อัตโนมัติ
  • macOS มีรูปแบบคล้ายกันโดยสร้างสัญลักษณ์ section$start และ section$end สำหรับแต่ละ section
  • บน GNU linker section ที่ไม่ได้ระบุไว้ใน linker script เรียกว่า orphan section
  • ลิงเกอร์จะสร้างสัญลักษณ์ prefix แบบ _start·_stop ให้อัตโนมัติก็ต่อเมื่อชื่อ section เข้ากันได้กับชื่อสัญลักษณ์ของ C
  • our_strings ใช้ได้ แต่ our.strings หรือ .our_strings จะไม่ทำงานแบบเดียวกัน
  • เนื่องจาก boundary symbol ไม่มีข้อมูลและมีเพียง address ที่สำคัญ ตัวอย่างจึงใช้ MaybeUninit<()> แทน
  • ใน Stable Rust ยังไม่มี “opaque external type” ที่เหมาะสมที่สุด จึงใช้ MaybeUninit เป็นตัวแทนชั่วคราว
  • การสร้างพอยน์เตอร์ &raw const สำหรับ static item นั้นถูกต้องเสมอ จึงสามารถเอาเฉพาะ address ได้อย่างปลอดภัยโดยไม่ต้องอ่านค่า
  • link-section ทำ abstraction รายละเอียดของ linker section เหล่านี้และแปลงเป็น Rust slice ที่ใช้การทำงานมาตรฐานของ slice ได้
  • พลังของ link section อยู่ที่ crate ใดก็ตามที่ส่งโค้ดเข้าไบนารีสามารถส่งรายการเข้า section เดียวกันได้ และ linker จะรวบรวมทั้งหมดให้ก่อนสร้างไบนารีสุดท้าย

Dependency injection

  • รูปแบบการลงทะเบียนด้วย section ทำงานบนหลักการเดียวกับ dependency injection
  • เฟรมเวิร์กอย่าง Dagger และ Spring ก็อยู่บนหลักที่ว่าผู้ใช้ข้อมูลการลงทะเบียนไม่ควรถูกผูกติดกับผู้ให้ข้อมูล
  • ผู้ให้ข้อมูลจะลงทะเบียนข้อมูลตรงจุดที่นิยาม ส่วนผู้ใช้ข้อมูลจะอ่านจาก registry
  • ใน dependency injection แบบดั้งเดิม เฟรมเวิร์กมักต้องไล่กราฟของโมดูลหรือสแกนคลาสที่โหลดมาแล้วตอนเริ่มต้น เพื่อหาทั้งผู้ให้และผู้ใช้ข้อมูล
  • แต่ใน linker section นั้น linker จะรวบรวมข้อมูลจากผู้ให้ไว้ตอนสร้างไบนารี และทำให้ผู้ใช้ข้อมูลอ่านได้ง่าย
  • ตัวอย่างการลงทะเบียน CLI subcommand เป็นกรณีของรูปแบบนี้ โดยลงทะเบียน subcommand ผ่าน link_section::section
  • Turbopack ใช้รูปแบบนี้กับ string pool constant, กลไกลงทะเบียน serialization·deserialization และการลงทะเบียน ฟังก์ชัน incremental compilation ของ turbotask
  • แม้แต่เว็บเซิร์ฟเวอร์สมมติก็สามารถใช้รูปแบบนี้เพื่อรวบรวม route และ middleware แบบอัตโนมัติตอน build ได้

ใช้ section สำหรับการลงทะเบียน

  • ข้อดีของการทำงานก่อน main คือจะยังไม่มี thread ใดทำงานอยู่ เว้นแต่จะเริ่มมันขึ้นมาอย่างชัดเจน
  • ในสภาพแวดล้อมแบบนี้ หลายกรณีสามารถหลีกเลี่ยงความซับซ้อนของ lock หรือ synchronization primitive ได้
  • สามารถแบ่งวงจรชีวิตของข้อมูลได้ชัดเจนเป็นช่วงเขียนได้ก่อน main และช่วง immutable หลัง main
  • หากเลี่ยงการ lock และ unlock เวลาเข้าถึงข้อมูลระหว่างโปรแกรมทำงานอยู่ โครงสร้างก็อาจง่ายขึ้นและมีประสิทธิภาพมากขึ้น
  • ตัวอย่างนี้ใช้ struct CliSubcommand, ฟังก์ชัน constructor แบบ const และ #[section] เพื่อรวบรวม subcommand
  • subcommand อย่าง list, add, help สามารถอยู่ตรงไหนก็ได้ในโค้ด
  • ฟังก์ชัน main สามารถ dispatch แบบไดนามิกได้ตราบใดที่เห็นเพียงนิยามของ section CLI_SUBCOMMANDS โดยไม่ต้องรู้ชื่อหรือที่อยู่ของ subcommand ที่ลงทะเบียนไว้ล่วงหน้า
  • หากไม่มี subcommand ที่ลงทะเบียนไว้ ก็จะกลับไปใช้ subcommand ค่าเริ่มต้น ซึ่งในตัวอย่าง help ทำหน้าที่เป็นค่าเริ่มต้น

มากกว่าข้อมูล immutable

  • ตัวอย่างก่อนหน้านี้สมมติว่าข้อมูลที่เชื่อมลิงก์เข้ามาเป็น immutable แต่การจัดโครงสร้างข้อมูลด้วย linker ก็ใช้กับข้อมูล mutable ได้เช่นกัน
  • ความเป็น mutable ของ global static data เป็นปัญหาที่พบได้บ่อยใน Rust และแก้ได้ด้วยเครื่องมือ internal mutability เช่น mutex หรือ atomic type
  • mutex และ atomic type อาจไม่แพงมากเมื่อไม่มีการแย่งกันใช้ แต่ก็ไม่ได้ฟรีเสมอไป
  • หากต้องการแก้ไขข้อมูลอย่างปลอดภัยใน Rust การเปลี่ยนแปลงต้องเกิดขึ้นอย่าง thread-safe และหากมี mutable reference อยู่ ก็ต้องไม่มี reference อื่นมายังข้อมูลเดียวกัน
  • สภาพแวดล้อม pre-main เป็น single-threaded ตราบใดที่ไม่ได้เริ่ม thread อย่างชัดเจน จึงไม่ต้องพึ่งการทำงานแบบ atomic
  • ในสภาพแวดล้อมแบบ single-threaded ความสัมพันธ์แบบ happens-before ที่ว่าการเขียนต้องเกิดก่อนการอ่านภายหลังจะเกิดขึ้นโดยอัตโนมัติ
  • การแก้ไขข้อมูลใน link section ก่อน main ทำให้หลังจากนั้น thread ใดก็เข้าถึงได้อย่างปลอดภัยโดยไม่ต้องล็อก
  • หากสร้าง mutable reference เฉพาะก่อน main แล้วปิดมันลง ก็จะเป็นไปตามเงื่อนไขที่ว่าในช่วงมี mutable reference ต้องไม่มี reference อื่นอยู่
  • slice ของ link section เป็น alias ของ static item ภายใน section ดังนั้นทั้ง slice และ static item ต่างก็อยู่ภายใต้กฎเรื่อง alias
  • หากต้องการแก้ไขผ่าน slice อย่างปลอดภัย จะต้องวาง static item ไว้ภายใน UnsafeCell เท่านั้น
  • หาก static item ไม่ได้ถูกห่อด้วย UnsafeCell แล้ว LLVM อาจ cache ค่า, reorder การทำงาน หรือสมมติเกี่ยวกับข้อมูลนั้นได้
  • UnsafeCell เองไม่ใช่ Sync จึงต้องมี wrapper type เพิ่มเติม
  • ตัวอย่างนี้ใช้ SyncUnsafeCell และ MaybeUninit<SyncUnsafeCell<...>> เพื่อสร้างทั้ง boundary symbol และรายการข้อมูล
  • ตัวอย่าง string interning pool ที่จัดเรียงได้ จะนิยาม string pool ตอน link time จากนั้นจึง sort slice ในช่วงต้นของ runtime เพื่อให้ภายหลังค้นหาสตริงด้วย binary search ได้
  • การทำเองทั้งหมดมี boilerplate มาก แต่เมื่อใช้ ctor และ link-section ก็สามารถสร้างโครงสร้างเดียวกันได้อย่างกระชับด้วย TypedMutableSection และ constructor
  • รายการของ TypedMutableSection ต้องเป็น const เพราะภายในใช้โค้ดลักษณะใกล้เคียงกับตัวอย่างที่เขียนเอง

ข้อดีของรูปแบบ link section

  • รูปแบบนี้ช่วยรวมรายการที่ถูกติดแท็กไว้ได้อย่างรับประกัน และวางข้อมูลทั้งหมดลงในหน่วยความจำต่อเนื่องที่จัดเตรียมไว้ล่วงหน้า
  • สามารถกระจายตำแหน่งที่ลงทะเบียนไว้ที่ใดก็ได้ในโค้ด
  • สามารถทราบจำนวนรายการใน section ได้อย่างแน่นอน
  • link section ไม่ต้องมีการ allocate เพิ่ม
  • หากสร้างโครงสร้างแบบเดียวกันโดยไม่ใช้ link section ก็อาจต้อง allocate HashMap, Vec หรือโครงสร้างข้อมูลอื่น และอาจต้อง resize หลายรอบระหว่างรวบรวมรายการ
  • ในรูปแบบการรวบรวมแบบดั้งเดิม dependency ระหว่างโมดูลชนิดร่วม, โมดูลผู้ส่งข้อมูล, และโมดูลผู้รวบรวม มักพันกันลึก
  • เมื่อใช้ link section ตัวผู้รวบรวมสามารถอยู่ที่ใดก็ได้ และไม่จำเป็นต้องสนใจว่าโมดูลใดเป็นผู้ส่งข้อมูล
  • scattered-collect มีโครงสร้างข้อมูลคล้ายหลายชนิดที่รองรับการทำงานระดับ link time
    • Scattered*Slice เป็นโครงสร้างคล้าย Vec หลายแบบที่ให้ slice และรองรับการ sort แบบเลือกได้
    • ScatteredMap และ ScatteredSet เป็นโครงสร้างคล้าย HashMap·HashSet ที่ให้การค้นหา key-value แบบใช้ hash พร้อมการ initialize ก่อน main เพียงเล็กน้อย

เมื่อไม่ควรใช้วิธีนี้

  • การคำนวณระดับ link time นั้นทรงพลัง แต่ไม่ได้เป็นเครื่องมือที่เหมาะเสมอไป
  • แทนที่จะใช้แนวทางระดับ link time ก็สามารถรวบรวมข้อมูลด้วยมือใน crate ที่มองเห็นทุก crate ที่ต้องการส่งข้อมูลเข้ามาได้
  • การรวบรวมด้วยมืออาจไม่สะดวก และต้องมี crate สำหรับรวบรวมที่อ้างอิง crate จำนวนมาก แทนที่ผู้ส่งข้อมูลจะดูเพียงจุดส่งข้อมูลจุดเดียวใน core crate
  • การตัด dead code จะยากขึ้น
  • link-section และ linkme ติด #[used] ให้รายการ จึงทำให้ linker ไม่สามารถลบข้อมูลที่ไม่ได้ใช้ออกได้
  • สำหรับข้อมูลขนาดเล็กอย่าง interned string atom อาจไม่ใช่ปัญหา แต่ถ้า intern ข้อมูลอย่างชิ้นส่วน JSON·JavaScript ดิบหรือโครงสร้างข้อมูลขนาดใหญ่ ก็อาจสะสม dead code จำนวนมากที่มองหาสาเหตุได้ยาก
  • ฟังก์ชัน constructor ก่อน main ก็มีข้อจำกัด
  • ฟังก์ชัน constructor ห้าม panic และ Rust ก็ไม่ได้รับประกันว่าใช้ฟังก์ชันทั้งหมดใน standard library ได้
  • ลำดับเรียกฟังก์ชันเริ่มต้นที่มี priority เท่ากันไม่ถูกรับประกัน และขึ้นกับแพลตฟอร์มอย่างมาก
  • แม้จะหลบข้อจำกัดนี้ได้ด้วยการออกแบบอย่างระมัดระวัง แต่วิธี pre-main ก็อาจผิดพลาดได้จากเหตุผลที่ละเอียดอ่อนและ debug ยาก
  • Miri ยังไม่รองรับ constructor และการจัดโครงสร้าง link section แบบ pre-main ได้ครบถ้วน
  • ปัจจุบัน Miri มองการทำงาน pre-main ได้เพียงระดับพื้นฐานมาก และไม่ได้จำลอง link section
  • สำหรับการทดสอบ undefined behavior แนะนำให้ใช้ LLVM sanitizer อย่าง ASan, TSan เป็นต้น
  • รูปแบบ inversion of control อาจทำให้ audit ทุกจุดที่ส่งข้อมูลเข้า link section ทำได้ยากขึ้น
  • โปรแกรม Rust จำนวนมากที่เผยแพร่กว้างและถูกใช้งานมาก ก็พึ่งพาฟีเจอร์ pre-main อย่าง ctor, link-section, inventory, linkme อยู่แล้ว

สรุปสั้น ๆ เกี่ยวกับ WASM

  • WASM ไม่รองรับ linker section แบบ native เนื่องจากผลจากการตัดสินใจในอดีต
  • คำอธิบาย #[link_section] ไม่สามารถวางรายการลงใน code section จริงได้ แต่จะไปอยู่ใน WASM custom section ซึ่งตัวโค้ด WASM เองเข้าถึงไม่ได้
  • crate linktime รองรับ WASM และมีแนวทาง emulation workaround ที่ทำให้วิธีนี้ยังใช้ได้ใน WASM binary
  • อาจมีข้อเสนอในอนาคตเพื่อเพิ่มการรองรับ WASM ที่เหมาะสมยิ่งขึ้น

บทสรุป

  • ก่อนถึง main ยังมีงานอีกมากที่ทำได้ และในบางกรณีก็ให้ข้อดีอย่างมีนัยสำคัญ
  • สภาพแวดล้อม pre-main มีลำดับที่ควบคุมได้สูง ทำให้ทำงานหลายอย่างได้อย่างมั่นใจมากขึ้นโดยไม่ต้องใช้ lock, atomic type หรือ synchronization primitive อื่น
  • link section ช่วยให้สามารถรวบรวมข้อมูลที่เกี่ยวข้องจากทั้งไบนารีได้อย่างอิสระและวางไว้ร่วมกัน พร้อมหลีกเลี่ยงลำดับ dependency ระหว่าง crate ที่ดูไม่เป็นธรรมชาติ
  • ในหลายกรณีสามารถหลีกเลี่ยงการ allocate ได้ทั้งหมด จึงลดโอกาสเจอปัญหาจาก allocator เช่น fragmentation ที่เกิดจากการ allocate ซ้ำ ๆ
  • crate ที่เกี่ยวข้องได้แก่ ctor, dtor, link-section, scattered-collect

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

 
GN⁺ 4 시간 전
ความคิดเห็นจาก Lobste.rs
  • Go ถือว่าเป็นกรณียกเว้นตรงที่หลีกเลี่ยง C runtime บนแพลตฟอร์มส่วนใหญ่ แต่ Apple กำหนดให้ต้องใช้ C runtime ในการเข้าถึง system call
    Apple ใช้ libSystem.dylib เป็นขอบเขตความเสถียรของ ABI สำหรับ system call ส่วน Windows ตระกูล NT ใช้ ntdll.dll เป็นขอบเขตความเสถียรของ ABI ไม่ใช่ตัว system call เอง: not syscalls
    บน OpenBSD ดูเหมือนว่า Go เคยตั้งค่า metadata flag ในลักษณะปิดการบังคับใช้บิต NX เพื่อหลีกเลี่ยงนโยบายที่เคอร์เนลจะสั่งยุติหากพยายามทำ system call นอก libc mapping แบบอ่านอย่างเดียวที่ตัว loader ตั้งไว้
    อย่างไรก็ตาม libSystem.dylib contains the functionality which would normally be libc.so plus other things ดังนั้นในแง่นั้นก็เทียบได้กับแนวทางของ BSD ที่ถือว่า “libc คือขอบเขตความเสถียร”
    อีกทั้ง As of Go 1.16 เป็นต้นมา Go ก็ใช้ libc เพื่อปฏิบัติตามนโยบาย system call ของ OpenBSD
    Linux ค่อนข้างเป็นกรณีที่พบไม่บ่อยเพราะมีหมายเลข system call ที่เสถียร ไม่ได้มีโครงสร้างแบบ “ชิ้นส่วนของเคอร์เนลที่ถูกโหลดเป็นไลบรารีแบบไดนามิกใน address space ของโปรเซส แล้วแชร์นิยาม enum ของ system call ที่ไม่เสถียรกับโค้ดใน kernel mode” เหมือน OS อื่น ๆ และ Linux กับ glibc ก็ไม่ได้พัฒนาร่วมกันใน repository เดียวเหมือนบางที่
    บน Windows นั้น C runtime ยังรับหน้าที่แปลง command string แบบ CP/M ที่สืบทอดมาจาก MS-DOS และต่อเนื่องมายัง API การสร้างโปรเซสลูกของ Windows ให้กลายเป็นอาร์เรย์ argv แบบ POSIX
    ด้วยเหตุนี้เอกสาร Python subprocess จึงมีหัวข้อ Converting an argument sequence to a string on Windows ซึ่งอธิบายวิธีแปลงอาร์เรย์ argv ให้เป็นสตริงตามกฎการใส่เครื่องหมายอัญประกาศที่ฝังอยู่ใน MS C runtime โดยตัว parser ของโปรเซสลูกที่ถูกเรียกสามารถเลือกทำงานต่างจากกฎนี้ได้หากต้องการ
    แม้แต่ _start ของ Linux ก็ไม่ได้หมายความอย่างเคร่งครัดว่า linker จะใส่ symbol ชื่อนี้ลงในไบนารีโดยอัตโนมัติ หากไบนารีรูปแบบ ELF เป็น executable ไม่ใช่ไลบรารี ตัว header field e_entry ที่ offset 0x18 จะเก็บ address ที่ loader ต้องกระโดดไปหลังจัดเตรียมหน่วยความจำเสร็จ
    _start เป็นธรรมเนียมของ GCC สำหรับระบุเป้าหมายที่ e_entry จะชี้ไปเมื่อไม่ได้ใช้ entry point ที่ libc จัดให้ และเท่าที่จำได้เครื่องมืออย่าง NASM ก็ทำตามแนวทางนี้ด้วย
    ส่วน _WinMainCRTStartup บน Windows นั้น loader จะหาได้จาก AddressOfEntryPoint ใน PE header ซึ่งอยู่ที่ Offset 0x0028 โดยนับจากจุดเริ่มต้นของ PE header และ PE header นี้จะอยู่ถัดจาก MZ (DOS EXE) header กับ DOS Stub
    ถ้าอยากเรียนรู้รายละเอียดของ PE header แนะนำ Making the smallest Windows application และ Tiny PE โดย Tiny PE ถึงขั้นละเมิดสเปก PE ในแบบที่ Windows ยังยอมรับได้ เช่น ซ้อนทับส่วนที่ OS จะไม่อ่านเข้าด้วยกัน หรือใส่โค้ดลงใน header field ที่ไม่ได้ใช้งาน พอไปถึงระดับนี้ ขนาดไฟล์ขั้นต่ำที่ Windows ยอมรับได้ก็จะต่างกันไปตามเวอร์ชันของ Windows ที่ใช้รัน
    สำหรับ ELF executable ขนาดจิ๋วมากบน Linux ก็มี A Whirlwind Tutorial on Creating Really Teensy ELF Executables for Linux ที่น่าอ่านเช่นกัน
    • system call ของ FreeBSD และ NetBSD มี ความเสถียรของ ABI เช่นเดียวกับไลบรารีระบบ
    • ในเรื่อง _start บนระบบ a.out นั้น entry point ที่เคอร์เนลใช้เข้าสู่ executable ตามธรรมเนียมคือ start ที่ประกาศไว้ใน csu/crt0 ตัวอย่างเช่น 7th edition, VAX BSD
      ในยุคนั้น C compiler จะเติม _ ไว้หน้าสัญลักษณ์ global ดังนั้นใน V7 จึงประกาศ _main และใน BSD จะเห็นว่ามีการประกาศชื่อ assembly ของ start() ในภาษา C เป็น start แบบไม่มีการตกแต่งชื่อ
      ในเวลานั้นโปรแกรมเริ่มทำงานจากต้นไฟล์ และการเรียก linker ของ cc จะจัดให้ crt0 อยู่ลำดับแรกสุด โดย csu หมายถึง C startup code และ crt0 หมายถึงออบเจ็กต์สนับสนุน C runtime ลำดับที่ 0
      สำหรับ System V ที่มี ELF แล้ว การทำงานที่แน่ชัดหาได้ยากกว่า แต่ start หรือ _start ก็ยังถูกใช้ต่อไปเป็น entry point ของโปรแกรมที่ประกาศใน csu/crt0
      ผมไม่เคยทำความเข้าใจอย่างถ่องแท้ว่าการจัดการคำนำหน้า _ เปลี่ยนไปอย่างไรใน ELF แต่เดาว่าอาจมีการเพิ่มอีกชั้นแบบขำ ๆ จน start กลายเป็น _start ด้วยเหตุผลบางอย่าง
      คู่กันที่เห็นได้ชัดคือ ELF ดูเหมือนจะเพิ่ม _end เข้ามาด้วย ซึ่งสอดคล้องกับด้านบนของ BSS และเป็นตำแหน่งที่ sbrk(0) จะคืนค่าก่อนที่ malloc() จะสร้าง heap
  • ผมสนใจเรื่องชีวิตก่อน main ใน Rust และคิดว่าน่าจะดีถ้ามีบทความสักชิ้นสรุปว่ามันคืออะไรและมีประโยชน์อย่างไร
    ผมยังมีไอเดียบทความต่อยอด เช่น วิธีใช้ linker aggregation เพื่อสร้าง collection ที่เร็วขึ้น แต่ก่อนอื่นอยากฟังความเห็นต่อหัวข้อ เชิงปูพื้น นี้ก่อน
    • ผมทำ Embedded Rust มาเยอะ ดังนั้นในสภาพแวดล้อมแบบ no_std และบางครั้งไม่มีแม้แต่ alloc นั้น main ก็เป็นเพียงอีกฟังก์ชันหนึ่ง และการเริ่มต้นระบบก็มักเป็นหน้าที่ของผู้พัฒนาเอง
      ใน codebase ก็มีโค้ดซ้ำ ๆ ที่ทำขึ้นเองสำหรับจุดประสงค์คล้ายกันอยู่พอสมควร จึงสงสัยว่า crate เหล่านี้จะทำงานเชื่อมกับ สภาพแวดล้อมแบบ embedded อย่างไร