ใน Rust มีโค้ดที่ทำงานได้ตั้งแต่ก่อนถึง main
(grack.com)- ไบนารี 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 glibcruntime รุ่นใหม่บน 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 ความต่างรายแพลตฟอร์มและความซับซ้อนของงานฝั่ง linkerinventoryและ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สำหรับstaticitem นั้นถูกต้องเสมอ จึงสามารถเอาเฉพาะ 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 แบบไดนามิกได้ตราบใดที่เห็นเพียงนิยามของ sectionCLI_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 ของ
staticitem ภายใน section ดังนั้นทั้ง slice และstaticitem ต่างก็อยู่ภายใต้กฎเรื่อง alias - หากต้องการแก้ไขผ่าน slice อย่างปลอดภัย จะต้องวาง
staticitem ไว้ภายในUnsafeCellเท่านั้น - หาก
staticitem ไม่ได้ถูกห่อด้วย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 timeScattered*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 ความคิดเห็น
ความคิดเห็นจาก Lobste.rs
Apple ใช้ libSystem.dylib เป็นขอบเขตความเสถียรของ ABI สำหรับ system call ส่วน Windows ตระกูล NT ใช้
ntdll.dllเป็นขอบเขตความเสถียรของ ABI ไม่ใช่ตัว system call เอง: not syscallsบน OpenBSD ดูเหมือนว่า Go เคยตั้งค่า metadata flag ในลักษณะปิดการบังคับใช้บิต NX เพื่อหลีกเลี่ยงนโยบายที่เคอร์เนลจะสั่งยุติหากพยายามทำ system call นอก
libcmapping แบบอ่านอย่างเดียวที่ตัว loader ตั้งไว้อย่างไรก็ตาม libSystem.dylib contains the functionality which would normally be
libc.soplus 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 fielde_entryที่ offset0x18จะเก็บ address ที่ loader ต้องกระโดดไปหลังจัดเตรียมหน่วยความจำเสร็จ_startเป็นธรรมเนียมของ GCC สำหรับระบุเป้าหมายที่e_entryจะชี้ไปเมื่อไม่ได้ใช้ entry point ที่ libc จัดให้ และเท่าที่จำได้เครื่องมืออย่าง NASM ก็ทำตามแนวทางนี้ด้วยส่วน
_WinMainCRTStartupบน Windows นั้น loader จะหาได้จากAddressOfEntryPointใน PE header ซึ่งอยู่ที่ Offset0x0028โดยนับจากจุดเริ่มต้นของ 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 ที่น่าอ่านเช่นกัน
_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()จะสร้าง heapmainใน Rust และคิดว่าน่าจะดีถ้ามีบทความสักชิ้นสรุปว่ามันคืออะไรและมีประโยชน์อย่างไรผมยังมีไอเดียบทความต่อยอด เช่น วิธีใช้ linker aggregation เพื่อสร้าง collection ที่เร็วขึ้น แต่ก่อนอื่นอยากฟังความเห็นต่อหัวข้อ เชิงปูพื้น นี้ก่อน
no_stdและบางครั้งไม่มีแม้แต่allocนั้นmainก็เป็นเพียงอีกฟังก์ชันหนึ่ง และการเริ่มต้นระบบก็มักเป็นหน้าที่ของผู้พัฒนาเองใน codebase ก็มีโค้ดซ้ำ ๆ ที่ทำขึ้นเองสำหรับจุดประสงค์คล้ายกันอยู่พอสมควร จึงสงสัยว่า crate เหล่านี้จะทำงานเชื่อมกับ สภาพแวดล้อมแบบ embedded อย่างไร