嵌入式與裸機

啟動程式碼(startup code / crt0)

/ see-AR-tee-zero /

舞台劇開演前,總有人先布置場景:擺好道具、調暗燈光、讓每個人就定位。之後主角才登場。在微控制器上,那份布景工作就是啟動程式碼——一小段在重置後立即執行的碼,它把世界準備好,好讓你的 main() 能在 C 的正常規則已經成立的前提下執行。main() 是主角;啟動程式碼是後台人員。

為什麼需要準備?因為 C 程式設計師把某些事視為理所當然,但硬體自己「不會」提供。有兩件核心工作。第一是 .bss 區段:在 C 裡,你沒有初始化(或設為零)的全域與靜態變數,保證一開始就是零。但 RAM 在上電時裝的是隨機垃圾。所以啟動程式碼必須走過 RAM 裡的整個 .bss 區段,把它全部寫成零。第二是 .data 區段:「有」非零初始值的全域與靜態變數(例如 int speed = 5;)。初始值必須在斷電後依然存在,所以存放在快閃裡,但變數住在 RAM(因為它可寫)。於是啟動程式碼把初始值從快閃複製到它們的 RAM 位置。連結器指令稿提供這兩個區段的起訖位址。在把 .bss 清零並複製完 .data 之後——有時還會設好堆疊、時脈或 C 函式庫——啟動程式碼最後才呼叫 main()。在有主機的系統上,這東西的經典名稱是 crt0(C runtime, object 0)。

它之所以重要,是因為若這份準備被略過或做錯,你的程式看起來正確卻會以令人費解的方式出錯:你「以為」是零的全域裝著垃圾,或一個已初始化的常數值錯了。誠實的細節:零初始化的全域與「明確」設為零的全域都歸到 .bss(所以它們不佔快閃空間,只多一次清零);而非零初始值要佔快閃來存那份副本。而若某個全域需要在 main 執行前就有值,唯有啟動程式碼的複製步驟能讓這成真——裸機上沒有作業系統載入器替你做。

// Reset_Handler 做什麼,用淺白步驟說明: void Reset_Handler(void) { // 1. 把 .data 初始值從快閃複製到 RAM uint32_t *src = &_sidata, *dst = &_sdata; while (dst < &_edata) *dst++ = *src++; // 2. 把 RAM 裡的 .bss 區段清零 for (uint32_t *p = &_sbss; p < &_ebss; p++) *p = 0; // 3. (選擇性)初始化時脈、呼叫 C 函式庫初始化 // 4. 把控制權交給你的程式 main(); for (;;) { } // main 不應回傳 }

清零 .bss、把 .data 從快閃複製過來,然後呼叫 main。_sdata/_sbss 等符號是連結器指令稿定義的位址。

若你看到某個「應該是零」的未初始化全域裝著垃圾,先懷疑啟動程式碼:裸機上除非你的 crt0 明確去做,否則沒有任何東西會清零 .bss 或複製 .data——這裡沒有作業系統載入器可依賴。

又称
crt0C runtime startupReset_HandlerbootstrapC 執行期啟動碼啟動檔