安全實戰(zhàn)指南)
1. 從一次HardFault說起為什么C庫運行時值得單獨拎出來講很多人寫單片機代碼注意力幾乎全放在寄存器配置、外設(shè)驅(qū)動、通信協(xié)議上覺得C庫就是#include string.h之后隨便調(diào)調(diào)memcpy、strlen、sprintf的事。我早年也是這么想的直到有一次在Cortex-M3上跑一個帶FreeRTOS的多任務(wù)采集系統(tǒng)任務(wù)里調(diào)了一次malloc運行了大概四十分鐘之后系統(tǒng)直接進了HardFault?;厮葜赶虻牡刂仿湓诹艘粋€我從來沒見過的段里。查了兩天才反應(yīng)過來問題出在C庫的堆管理和多任務(wù)搶占的交互上。這件事之后我才認真去翻C庫運行時的實現(xiàn)包括newlib、newlib-nano、ARM Compiler自帶的microlib以及各家IDE默認鏈接進去的那套啟動代碼。越看越覺得C庫運行時C Runtime Library在單片機上是一個被嚴重低估的領(lǐng)域。它不像外設(shè)驅(qū)動那樣有直觀的寄存器可以看也不像RTOS那樣有明確的API文檔但它幾乎滲透在你寫的每一行C代碼里——從main函數(shù)被誰調(diào)用、全局變量誰清零、堆棧指針誰設(shè)置到printf重定向、malloc的線程安全、中斷里能不能調(diào)memcpy全都歸它管。這篇內(nèi)容我打算把單片機C庫運行時這條線從頭到尾捋一遍。核心圍繞幾個問題展開libspace到底是什么、C庫啟動流程在單片機上是怎樣一個順序、多任務(wù)環(huán)境下C庫的哪些函數(shù)是雷區(qū)、中斷安全到底該怎么保證。適合已經(jīng)能寫裸機程序、正在往RTOS或多任務(wù)方向走的開發(fā)者也適合那些被printf卡死、被malloc坑過、被HardFault折磨過的朋友。我會盡量把原理講透同時給出可以直接抄的配置和代碼讓你少走我當年走過的彎路。2. libspace與C庫運行時的真實邊界2.1 libspace不是某個具體庫而是一類運行時支撐空間先澄清一個概念。很多人在論壇上看到libspace這個詞以為是某個具體的庫文件或者某個廠商的專有組件。實際上在單片機語境下libspace通常指的是C庫運行時所占用的那一塊代碼空間和數(shù)據(jù)空間的總和包括啟動代碼startup、C庫函數(shù)本體、庫內(nèi)部使用的靜態(tài)數(shù)據(jù)區(qū)、堆區(qū)heap以及庫初始化時依賴的各類段section。你可以把它理解成編譯器把你的main函數(shù)和它依賴的所有C庫函數(shù)打包在一起再加上一段在main之前運行的啟動代碼這一整套東西在Flash和RAM里占據(jù)的空間就是libspace。它不是一個.a文件的名字而是一個空間概念。理解這一點很重要因為后面講的所有優(yōu)化、裁剪、多任務(wù)安全本質(zhì)上都是在管理這塊空間。在ARM GCC工具鏈下libspace的構(gòu)成大致是這樣的組成部分典型來源占用位置啟動代碼startup_*.s/crt0.oFlashC庫函數(shù)本體newlib / newlib-nanoFlash庫靜態(tài)數(shù)據(jù).data/.bss中的庫變量RAM堆區(qū)_sbrk管理的區(qū)域RAM棧區(qū)MSP/PSP指向的區(qū)域RAM初始化表.init_array/.fini_arrayFlash這里有個容易被忽略的點C庫函數(shù)本體在Flash里的占用往往比你想象的大得多。一個完整的printf加上浮點格式化支持在newlib下能吃掉十幾KB甚至二十幾KB的Flash。對于STM32F103C8這種只有64KB Flash的芯片來說這是致命的。所以后面我會專門講怎么裁剪。2.2 啟動代碼到底替你做了哪些事很多人寫單片機程序從來沒看過startup文件里到底寫了什么。但C庫運行時的第一站就是啟動代碼不理解它后面全是空中樓閣。以Cortex-M為例上電復(fù)位后硬件做的第一件事是從向量表的前兩個字取出初始MSP和Reset_Handler地址。Reset_Handler里通常按這個順序執(zhí)行設(shè)置棧指針其實硬件已經(jīng)幫你從向量表加載了MSP但有些啟動代碼會再確認一次。調(diào)用SystemInit配置時鐘、向量表偏移等這個函數(shù)由廠商提供。調(diào)用__libc_init_array這是C庫運行時的關(guān)鍵入口它會遍歷.init_array段執(zhí)行所有標注了__attribute__((constructor))的函數(shù)以及C的全局構(gòu)造函數(shù)。清零.bss段把所有未初始化的全局變量和靜態(tài)變量置零。搬運.data段把Flash里的初值拷貝到RAM。調(diào)用main終于輪到你的代碼。這里有個非常關(guān)鍵的細節(jié).bss清零和.data搬運的順序以及它們和__libc_init_array的先后關(guān)系不同工具鏈是不一樣的。ARM Compiler的__main會先做段初始化再調(diào)__rt_entry而GCC的crt0順序又不同。如果你在全局對象的構(gòu)造函數(shù)里訪問了另一個全局變量而那個變量還沒被初始化就會讀到垃圾值。這種bug極難排查因為它在單步調(diào)試時可能表現(xiàn)正常一上電跑就出問題。提示如果你在C環(huán)境下寫單片機全局對象的構(gòu)造函數(shù)里絕對不要依賴其他全局對象的狀態(tài)。所有跨對象的初始化依賴都應(yīng)該顯式地放到main里按順序執(zhí)行。2.3 堆和棧在libspace里的位置關(guān)系棧和堆是libspace里最容易出問題的兩塊。棧由鏈接腳本里的_estack符號定義頂部向下增長堆由_end符號定義起始向上增長在_sbrk實現(xiàn)里。兩者相向而行中間的空隙就是可用空間。很多默認的鏈接腳本只給了一個_Min_Heap_Size和_Min_Stack_Size實際運行時如果堆??偤统^RAM就會發(fā)生堆棧碰撞。這種碰撞不會立刻報錯而是表現(xiàn)為某個全局變量莫名其妙被改寫、某個函數(shù)返回地址被覆蓋、系統(tǒng)跑一段時間后隨機死機。我實測過一個案例STM32F407有192KB RAM鏈接腳本里棧給了1KB堆給了512字節(jié)看起來綽綽有余。但系統(tǒng)里有個任務(wù)遞歸調(diào)用了深度較大的函數(shù)棧實際用了1.8KB直接沖進了堆區(qū)把malloc的元數(shù)據(jù)踩爛了。后來把棧調(diào)到4KB問題消失。判斷堆棧是否夠用的方法我常用這兩個棧水位檢測啟動時把棧區(qū)域全部填成0xDEADBEEF運行一段時間后掃描看最深用到哪里。堆使用統(tǒng)計在_sbrk里加計數(shù)器記錄歷史最大堆用量。這兩個手段配合使用基本能摸清libspace在RAM里的真實占用。3. 多任務(wù)環(huán)境下C庫函數(shù)的雷區(qū)清單3.1 為什么malloc在RTOS里是頭號危險分子裸機程序里malloc用起來很爽因為沒有并發(fā)堆的管理是串行的。但一旦上了RTOS多個任務(wù)可能同時調(diào)用malloc而標準C庫的malloc實現(xiàn)無論是newlib還是microlib默認不是線程安全的。newlib的malloc內(nèi)部維護一個空閑鏈表_sbrk負責向系統(tǒng)要內(nèi)存。當任務(wù)A正在遍歷空閑鏈表準備分配時如果被任務(wù)B搶占任務(wù)B也來遍歷同一個鏈表兩個任務(wù)就可能拿到同一塊內(nèi)存或者把鏈表指針改亂。這種問題的可怕之處在于它不一定立刻崩潰而是讓堆慢慢碎片化、元數(shù)據(jù)錯亂最后在某個不相干的地方爆出來。解決方案有三條路給malloc加互斥鎖重寫_sbrk或者在malloc外層包一層用RTOS的mutex保護。但要注意malloc本身可能被中斷服務(wù)程序調(diào)用而mutex不能在中斷里用所以這條路有局限。使用RTOS自帶的內(nèi)存管理FreeRTOS的pvPortMalloc、RT-Thread的rt_malloc都是線程安全的且針對嵌入式做了優(yōu)化。這是我最推薦的做法——在RTOS項目里徹底禁用標準malloc把所有動態(tài)分配換成RTOS的接口。干脆不用動態(tài)內(nèi)存所有緩沖區(qū)靜態(tài)分配。這是最穩(wěn)的方案代價是靈活性下降。我個人的經(jīng)驗是在RAM小于64KB的單片機上動態(tài)內(nèi)存能不用就不用。靜態(tài)分配雖然浪費一點空間但換來的是確定性這在嵌入式里比什么都值錢。3.2printf家族的重入問題printf、sprintf、snprintf這些函數(shù)在多任務(wù)下的問題比malloc更隱蔽。它們內(nèi)部通常有一個靜態(tài)的緩沖區(qū)或者文件流狀態(tài)FILE結(jié)構(gòu)體多個任務(wù)同時調(diào)用時輸出會交錯、錯亂甚至因為內(nèi)部指針被改而崩潰。更麻煩的是很多人為了調(diào)試方便在中斷服務(wù)程序里也調(diào)printf。中斷可能打斷正在執(zhí)行printf的任務(wù)導(dǎo)致庫內(nèi)部的靜態(tài)狀態(tài)被破壞。這種問題在調(diào)試階段可能看不出來因為中斷觸發(fā)頻率低但產(chǎn)品上線后中斷頻繁觸發(fā)就會隨機死機。我的處理原則很明確中斷里絕對不調(diào)printf。需要輸出調(diào)試信息時往一個環(huán)形緩沖區(qū)里寫由低優(yōu)先級任務(wù)負責搬運。多任務(wù)環(huán)境下用snprintf而不是sprintf并且每個任務(wù)用自己的局部緩沖區(qū)避免共享。如果必須共享一個輸出通道用RTOS的mutex把整個格式化加輸出過程保護起來。這里有個細節(jié)snprintf本身在newlib里也不是完全可重入的因為它可能調(diào)用locale相關(guān)的函數(shù)。但在單片機環(huán)境下locale通常被裁剪掉了所以實際風險可控。真正危險的是printf直接操作stdout的那部分。3.3strtok、rand這些隱藏的不可重入函數(shù)除了malloc和printfC庫還有一批函數(shù)是明確不可重入的因為它們內(nèi)部使用了靜態(tài)變量函數(shù)不可重入原因替代方案strtok內(nèi)部靜態(tài)指針保存上次位置strtok_rrand/srand內(nèi)部靜態(tài)種子自己實現(xiàn)或加鎖asctime/ctime返回靜態(tài)緩沖區(qū)指針asctime_r/ctime_rgmtime/localtime返回靜態(tài)結(jié)構(gòu)體gmtime_r/localtime_rgetenv可能操作共享環(huán)境區(qū)避免在任務(wù)中調(diào)用這些函數(shù)在裸機下用沒問題但在多任務(wù)下就是定時炸彈。我見過一個項目兩個任務(wù)同時調(diào)strtok解析不同的字符串結(jié)果解析結(jié)果互相串臺查了一周才發(fā)現(xiàn)是庫函數(shù)的問題。注意帶_r后綴的可重入版本在newlib里是有的但會額外占用Flash。如果你的項目對空間敏感可以自己實現(xiàn)簡化版比如strtok_r的邏輯其實很簡單二十行代碼就能寫出來。4. 中斷安全從原理到可落地的防護策略4.1 中斷安全的核心是可重入和原子性中斷安全這個詞聽起來很玄拆開來看就兩件事可重入一個函數(shù)在執(zhí)行過程中被中斷打斷中斷里又調(diào)用了同一個函數(shù)等中斷返回后原來的執(zhí)行能正確繼續(xù)。原子性對共享數(shù)據(jù)的讀-改-寫操作不能被中斷打斷否則會出現(xiàn)丟失更新。C庫函數(shù)里大部分純計算函數(shù)如memcpy、strlen、abs是可重入的因為它們只用寄存器和棧上的局部變量。但涉及全局狀態(tài)、靜態(tài)緩沖、堆管理的函數(shù)就不可重入。判斷一個函數(shù)是否中斷安全我的經(jīng)驗法則是看它有沒有訪問函數(shù)作用域之外的、可寫的數(shù)據(jù)。如果有就要警惕。4.2 中斷里調(diào)用C庫函數(shù)的實際風險等級我把常見C庫函數(shù)在中斷里的風險分成三檔低風險可以直接用memcpy、memset、memcmp、memmovestrlen、strcmp、strcpy前提是目標緩沖區(qū)不被其他上下文共享abs、labs、簡單的數(shù)學(xué)運算中風險需要評估snprintf如果只往局部緩沖區(qū)寫且不涉及浮點通常安全。但newlib的浮點格式化會用到較大的棧空間中斷??赡懿粔颉in、cos、sqrt數(shù)學(xué)庫函數(shù)通常是可重入的但浮點運算在中斷里可能觸發(fā)FPU上下文保存問題如果RTOS沒做懶加載。高風險絕對不要在中斷里用malloc、free、calloc、reallocprintf、fprintf、putsstrtok、rand、localtime任何涉及文件流操作的函數(shù)4.3 用臨界區(qū)保護共享的C庫狀態(tài)如果你確實需要在多個上下文任務(wù)和中斷之間共享某些C庫功能就必須用臨界區(qū)保護。在Cortex-M上最常用的方法是關(guān)中斷// 進入臨界區(qū)保存當前中斷狀態(tài) __attribute__((always_inline)) static inline uint32_t enter_critical(void) { uint32_t primask __get_PRIMASK(); __disable_irq(); return primask; } // 退出臨界區(qū)恢復(fù)之前的中斷狀態(tài) __attribute__((always_inline)) static inline void exit_critical(uint32_t primask) { __set_PRIMASK(primask); }使用方式uint32_t state enter_critical(); // 這里操作共享的C庫狀態(tài)比如調(diào)用一個非線程安全的函數(shù) exit_critical(state);注意這里保存和恢復(fù)PRIMASK而不是簡單地開中斷是因為臨界區(qū)可能嵌套直接__enable_irq()會破壞外層臨界區(qū)的狀態(tài)。在FreeRTOS里更推薦用taskENTER_CRITICAL()和taskEXIT_CRITICAL()它們內(nèi)部處理了嵌套計數(shù)。但要注意臨界區(qū)里不能調(diào)用任何可能引起任務(wù)切換的函數(shù)否則會觸發(fā)斷言。4.4 中斷棧和任務(wù)棧的分離配置Cortex-M的MSP和PSP分離機制是保證中斷安全的重要硬件基礎(chǔ)。通常的配置是MSP給中斷服務(wù)程序和啟動代碼用大小要能容納最深的ISR調(diào)用鏈。PSP給任務(wù)用每個任務(wù)有自己的棧。如果中斷里調(diào)用了C庫函數(shù)比如snprintf消耗的是MSP。很多人只關(guān)注任務(wù)棧大小忽略了MSP結(jié)果中斷里一調(diào)庫函數(shù)就棧溢出。我的配置習慣是MSP至少給1KB如果中斷里要用浮點格式化給2KB。任務(wù)棧根據(jù)實際水位檢測來定一般1KB起步。在FreeRTOS的FreeRTOSConfig.h里configISR_STACK_SIZE_WORDS或者鏈接腳本里的_Min_Stack_Size控制的就是MSP。這個值不能省。5. 裁剪與優(yōu)化讓libspace瘦下來的實操手段5.1 從newlib切換到newlib-nano的收益與代價ARM GCC工具鏈默認鏈接的是完整版newlib功能全但體積大。切換到newlib-nano只需要在鏈接選項里加--specsnano.specsarm-none-eabi-gcc --specsnano.specs -o firmware.elf ...實測數(shù)據(jù)STM32F103-Os優(yōu)化配置Flash占用RAM占用printf浮點支持完整newlib約28KB約4KB支持newlib-nano約12KB約2KB默認不支持newlib-nano 浮點printf約18KB約3KB支持newlib-nano的代價主要是printf默認不支持浮點、部分函數(shù)的實現(xiàn)被簡化、malloc的行為略有不同。如果你的項目需要打印浮點數(shù)要額外加-u _printf_float鏈接選項這會增加約6KB Flash。5.2 用-u _printf_float之外的辦法處理浮點輸出如果只是偶爾需要打印浮點加6KB Flash有點虧。我的做法是自己寫一個定點格式化函數(shù)把浮點轉(zhuǎn)成整數(shù)和小數(shù)兩部分分別打印void print_float(float value, int decimals) { int int_part (int)value; float frac value - int_part; if (frac 0) { frac -frac; int_part -int_part; } char buf[16]; int i 0; // 打印整數(shù)部分 // ... 省略整數(shù)轉(zhuǎn)字符串邏輯 // 打印小數(shù)點 buf[i] .; // 打印小數(shù)部分 for (int d 0; d decimals; d) { frac * 10; int digit (int)frac; buf[i] 0 digit; frac - digit; } buf[i] \0; uart_send_string(buf); }這個函數(shù)只占幾百字節(jié)Flash比鏈接整個浮點printf劃算得多。缺點是精度和舍入行為要自己控制但對于調(diào)試輸出足夠了。5.3 重定向_write和_sbrk的正確姿勢在GCC newlib環(huán)境下printf最終會調(diào)用_writemalloc最終會調(diào)用_sbrk。這兩個函數(shù)需要你自己實現(xiàn)#include sys/stat.h #include errno.h // 重定向printf到串口 int _write(int file, char *ptr, int len) { (void)file; for (int i 0; i len; i) { while (!(USART1-SR USART_SR_TXE)); USART1-DR ptr[i]; } return len; } // 堆管理 extern char _end; // 鏈接腳本定義的堆起始 extern char _estack; // 棧頂 static char *heap_ptr _end; void *_sbrk(int incr) { char *prev heap_ptr; if (heap_ptr incr _estack - 1024) { // 留1KB給棧 errno ENOMEM; return (void *)-1; } heap_ptr incr; return prev; }這里有個坑_sbrk的返回值必須是之前的位置而不是新的位置。很多人寫反了導(dǎo)致malloc拿到的指針錯位堆直接亂掉。另外_write里如果用了while等待發(fā)送完成在中斷上下文里調(diào)用printf就會死等。所以再次強調(diào)中斷里不要調(diào)printf。5.4 用鏈接腳本精確控制libspace布局鏈接腳本是控制libspace的最終手段。一個典型的STM32鏈接腳本關(guān)鍵部分MEMORY { FLASH (rx) : ORIGIN 0x08000000, LENGTH 64K RAM (rwx) : ORIGIN 0x20000000, LENGTH 20K } SECTIONS { .text : { *(.isr_vector) *(.text*) *(.rodata*) *(.init_array*) } FLASH .data : { _sdata .; *(.data*) _edata .; } RAM AT FLASH .bss : { _sbss .; *(.bss*) *(COMMON) _ebss .; } RAM ._user_heap_stack : { . ALIGN(8); _end .; . . _Min_Heap_Size; . . _Min_Stack_Size; . ALIGN(8); } RAM }關(guān)鍵點是_end符號的位置它標記了堆的起點。_sbrk里用的就是這個符號。如果你改了鏈接腳本一定要確認_end還在正確的位置。6. 實戰(zhàn)案例一個多任務(wù)采集系統(tǒng)的C庫安全改造6.1 問題現(xiàn)象與初步定位回到開頭那個HardFault的案例。系統(tǒng)配置是STM32F103 FreeRTOS三個任務(wù)采集任務(wù)100Hz、處理任務(wù)10Hz、通信任務(wù)事件驅(qū)動。采集任務(wù)里用malloc分配臨時緩沖區(qū)處理任務(wù)里用printf輸出調(diào)試信息。運行約四十分鐘后死機HardFault。用調(diào)試器看出錯地址在_malloc_r內(nèi)部。初步判斷是堆被破壞。6.2 逐步排查的完整鏈路第一步檢查棧溢出。把MSP和任務(wù)棧都填成0xDEADBEEF跑十分鐘后掃描發(fā)現(xiàn)采集任務(wù)的棧用了約800字節(jié)配置的是1KB沒溢出。MSP用了約400字節(jié)配置1KB也沒問題。第二步檢查堆棧碰撞。計算_end到_estack的距離減去棧配置剩余約8KB給堆。采集任務(wù)每100ms分配一次每次128字節(jié)理論上不會耗盡。但malloc的元數(shù)據(jù)有開銷實際每塊占用約144字節(jié)。跑四十分鐘約24000次分配如果free沒正確執(zhí)行堆早就爆了。第三步加日志。在_sbrk里記錄每次調(diào)用的incr和返回地址發(fā)現(xiàn)堆指針一直在增長沒有回落。說明**free沒有真正釋放內(nèi)存**。第四步查free的調(diào)用。采集任務(wù)里確實調(diào)了free但處理任務(wù)里也調(diào)了malloc和free。兩個任務(wù)并發(fā)操作堆導(dǎo)致空閑鏈表被破壞free時無法正確合并相鄰塊堆碎片化越來越嚴重最終_sbrk返回失敗malloc返回NULL而采集任務(wù)沒檢查NULL就直接寫踩了空指針。6.3 修復(fù)方案與驗證修復(fù)分三步禁用標準malloc在FreeRTOSConfig.h里把configUSE_NEWLIB_REENTRANT設(shè)為0所有動態(tài)分配改用pvPortMalloc和vPortFree。printf加互斥創(chuàng)建一個mutex所有printf調(diào)用前先獲取mutex輸出完釋放。中斷里改用環(huán)形緩沖區(qū)串口中斷收到數(shù)據(jù)后寫入環(huán)形緩沖區(qū)通信任務(wù)負責讀取和解析。改造后連續(xù)運行72小時無故障。堆使用量穩(wěn)定在2KB以內(nèi)沒有增長趨勢。6.4 這個案例暴露的通用問題這個案例的核心教訓(xùn)是C庫的線程安全性不會因為你用了RTOS就自動獲得。RTOS只提供調(diào)度和同步原語C庫的狀態(tài)保護需要你自己做。另外malloc返回NULL的檢查經(jīng)常被忽略。在裸機下malloc幾乎不會失敗但在多任務(wù)下堆被耗盡是很常見的。所有malloc調(diào)用后都必須檢查返回值。7. 幾個容易被忽略的細節(jié)與個人經(jīng)驗7.1errno在多任務(wù)下的表現(xiàn)errno在標準C里是一個全局變量但在多任務(wù)環(huán)境下每個任務(wù)應(yīng)該有自己獨立的errno。newlib通過_impure_ptr和struct _reent來實現(xiàn)這一點但需要RTOS配合。FreeRTOS提供了configUSE_NEWLIB_REENTRANT選項開啟后每個任務(wù)有自己的_reent結(jié)構(gòu)。開啟這個選項的代價是每個任務(wù)多占用約100字節(jié)RAM。如果你的任務(wù)多、RAM緊張要權(quán)衡。不開的話errno就是全局的一個任務(wù)設(shè)置的值可能被另一個任務(wù)覆蓋導(dǎo)致錯誤判斷。7.2atexit和全局析構(gòu)在單片機上的行為atexit注冊的函數(shù)在main返回后執(zhí)行。但單片機的main通常是個死循環(huán)永遠不會返回所以atexit注冊的函數(shù)永遠不會被調(diào)用。同理C的全局析構(gòu)函數(shù)也不會執(zhí)行。這意味著不要依賴析構(gòu)函數(shù)來釋放資源。所有清理邏輯要么顯式調(diào)用要么放在看門狗復(fù)位前的鉤子里。7.3 浮點運算與FPU上下文如果芯片有FPU如Cortex-M4F中斷里使用浮點運算時硬件會自動保存FPU寄存器到棧上。但如果RTOS配置了懶加載lazy stacking而中斷里又調(diào)用了C庫的浮點函數(shù)可能出現(xiàn)上下文保存不完整的問題。我的建議是如果中斷里要用浮點關(guān)閉懶加載。在FreeRTOS里configENABLE_FPU和configENABLE_LAZY_STACKING要配合設(shè)置。關(guān)閉懶加載會增加中斷延遲但換來的是確定性。7.4 一個實用的調(diào)試技巧當你懷疑C庫運行時出問題時可以在鏈接選項里加-Wl,-Mapoutput.map生成map文件。map文件里能看到每個C庫函數(shù)占用的Flash地址和大小以及堆棧符號的位置。配合arm-none-eabi-nm和arm-none-eabi-objdump可以精確定位問題。我常用的命令組合# 查看符號地址和大小 arm-none-eabi-nm -S --size-sort firmware.elf | tail -30 # 反匯編特定函數(shù) arm-none-eabi-objdump -d firmware.elf | grep -A 50 _malloc_r # 查看段布局 arm-none-eabi-readelf -S firmware.elf這幾個命令配合使用基本能摸清libspace的全貌。7.5 關(guān)于libspace裁剪的取舍最后說一個心態(tài)問題。很多人為了省Flash把C庫裁到極致結(jié)果調(diào)試時連printf都用不了開發(fā)效率大幅下降。我的建議是開發(fā)階段用完整庫發(fā)布階段再裁剪。用條件編譯或者不同的鏈接配置來切換不要為了省幾KB Flash犧牲整個開發(fā)體驗。Flash便宜時間貴。這個賬要算清楚。