在C語言程式設計中,記憶體管理是核心也是最容易出錯的環節。本文所討論的「記憶體」主要指靜態資料區、堆疊區與堆區三大塊(詳細佈局請參考《Linux虛擬位址空間佈局》)。靜態資料區在編譯期分配,生命週期贯穿程式整個執行期間,典型代表為全域變數與static修飾的靜態變數。函式執行時在堆疊上分配區域自動變數空間,函式結束自動釋放。堆區記憶體又稱動態記憶體,由程式執行期呼叫malloc/calloc/realloc等函式申請,需由開發者顯式呼叫free釋放。堆記憶體容量較大、生存期可控、靈活度高,但也極易引發記憶體洩漏、越界存取、重複釋放等嚴重問題。
記憶體越界分為讀越界與寫越界。讀越界指讀取非屬於目標變數的資料(如讀取位元組數超過分配量),若位址無效程式立即崩潰,若有效則讀到隨機資料導致不可預期後果。寫越界即「緩衝區溢出」,寫入資料對目標位址而言同樣是隨機的,後果同樣不可控。其危險在於症狀與根因的隨機性,使除錯極度困難。
資料區越界主要發生在全域/靜態變數、陣列或結構體的讀寫超出合法範圍時。常見原因有二:
memset/memcpy/memmove等記憶體操作函式調用錯誤#define NAME_SIZE 5
#define NAME_LEN NAME_SIZE-1 /* 終結符 */
char gszName[NAME_SIZE] = "Mike";
char *pszName = "Jason";
int main(void) {
memset(gszName, 0, NAME_SIZE+1); // 越界1
gszName[NAME_SIZE] = 0; // 越界2
if (strlen(pszName) <= NAME_SIZE) // 越界3(注意 '=' 號)
strcpy(gszName, pszName);
int dwSrcLen = strlen(pszName);
if (dwSrcLen < NAME_SIZE)
memcpy(gszName, pszName, dwSrcLen); // 未複製結束符 '\0'
return 0;
}
使用陣列時,下標在for迴圈條件判斷或經由函式參數傳入、複雜運算後極易越界:
void ModifyNameChar(unsigned char ucCharIdx, char cModChar) {
gszName[ucCharIdx] = cModChar; // 寫越界
}
int main(void) {
ModifyNameChar(5, 'L');
unsigned char ucIdx = 0;
for (; ucIdx <= NAME_SIZE; ucIdx++) // '=' 號導致讀越界
printf("NameChar = %c\n", gszName[ucIdx]);
return 0;
}
對重要全域資料,可植入結構體並添加頭尾檢查碼:
#define CODE_SIZE 4
#if (1 == CODE_SIZE)
#define CODE_TYPE char
#define CHK_CODE 0xCC
#elif (2 == CODE_SIZE)
#define CODE_TYPE short
#define CHK_CODE 0xCDDC
#else
#define CODE_TYPE int
#define CHK_CODE 0xABCDDCBA
#endif
#define CHK_HEAD CODE_TYPE ChkHead;
#define CHK_TAIL CODE_TYPE ChkTail;
#define INIT_CHECK(ptChkMem) do{ \
(ptChkMem)->ChkHead = CHK_CODE; \
(ptChkMem)->ChkTail = CHK_CODE; \
}while(0)
#define CHK_OVERRUN(ptChkMem) do{ \
if ((ptChkMem)->ChkHead != CHK_CODE || (ptChkMem)->ChkTail != CHK_CODE) { \
printf("[%s(%d)<%s>]Memory Overrun(ChkHead:0x%X,ChkTail:0x%X)!\n", \
__FILE__, __LINE__, FUNC_NAME, \
(ptChkMem)->ChkHead, (ptChkMem)->ChkTail); \
} \
}while(0)
typedef struct {
CHK_HEAD;
char szName[NAME_SIZE];
CHK_TAIL;
} T_CHK_MEM;
T_CHK_MEM gtChkMem;
int main(void) {
memset(>ChkMem, 0, sizeof(T_CHK_MEM));
INIT_CHECK(>ChkMem);
memset(>ChkMem, 11, 6);
CHK_OVERRUN(>ChkMem);
strcpy(gtChkMem.szName, "Elizabeth");
CHK_OVERRUN(>ChkMem);
return 0;
}
執行結果顯示頭尾檢查碼均被破壞,確認發生越界。若模組提供全域資料存取函式,可將檢查置於存取函式內,並結合巨集控制編譯期開關。
memset/memcpy/memmove與陣列下標越界風險。StrCopy)。CHECK_ARRAY_INDEX巨集)。C語言連結器對符號處理規則:強符號(已初始化全域變數/函式)最多一個;若有一強多弱(未初始化全域變數),選強符號;全弱則任選一。不同檔案定義同名全域變數(即便類型不同)會共享同一記憶體。若均初始化會報multiple definition錯誤;若有未初始化者僅警告size of symbol changed,最壞情況編譯通過但讀寫相互干擾,極難排查。
static修飾(無強弱之分、不產生衝突),並封裝存取函式供外部呼叫。volatile 修飾符的正確使用volatile 告訴編譯器該變數值可能隨時被外部改變,禁止優化,每次存取必須從記憶體重新讀取。適用場景:
可與const共用(如唯讀硬體暫存器),指標也可被volatile修飾。誤用會導致邏輯錯誤,例如:
int CalcSquare(volatile int *pVal) {
return (*pVal) * (*pVal); // 缺陷:編譯器生成兩次讀取,中間值可能被改變
}
// 正確寫法
long CalcSquare(volatile int *pVal) {
int dwTemp = *pVal;
return dwTemp * dwTemp;
}
另例:#define READ(val, addr) (val = *(unsigned long *)addr) 若addr資料讀取頻繁,優化器會將值存入暫存器,併發時造成資料不一致,必須用volatile修飾。
未初始化的堆疊變數內容為隨機值,直接使用導致不可預期後果。指標未初始化(野指標)或無效初始化(空指標)極度危險。對策:定義時即初始化;編譯器警告可協助定位。
每個執行緒堆疊空間有限(如ulimit -s預設 8MB)。主要成因:
甚至呼叫系統庫函式(如printf需 2KB 堆疊)或直接使用介面模組定義的大型結構(如MAX_MACTABLE_SIZE定義 8000 條目)也會導致溢位。多執行緒下大量建立執行緒可能耗盡虛擬位址空間,應使用PTHREAD_CREATE_DETACHED或pthread_join/pthread_detach及時釋放資源。
對策:掌握平台資源限制,評估函式及被呼叫者堆疊用量;大型自動變數改用全域/靜態/堆記憶體;巢狀呼叫不超過三層。
因作用域與生命週期限制,堆疊越界較資料區易發現。典型案例:
char szWanName[] = "OAM_WAN_VOIP";
if (bIsUniCommBlv)
strcpy(szWanName, "OAM_WAN_MNGIP"); // 目的緩衝區過小導致越界
更糟的是char szWanName[] = "";僅能容納一個'\0',strcpy必然越界。函式傳遞void*參數時若未驗證類型與大小極易越界;指標位移運算錯誤(p+n 等於 (char*)p + n*sizeof(*p) 而非 (char*)p + n)也常導致越界。堆疊越界可能改寫函式返回位址,導致呼叫關係異常或段錯誤(SIGSEGV),特徵為發生在被呼叫函式即將返回處。
對策:同資料區,但更重視程式碼走查而非執行期檢測。
函式內區域變數隨函式返回被釋放,不應被外部引用。雖記憶體未真正回收,但安全性無保障。編譯器會警告function returns address of local variable。
char *ParseOmciMsgType(unsigned char ucMsgType) {
if (ucMsgType < ucMsgNameNum)
return paMsgNameMap[ucMsgType];
char szStrMsgType[sizeof("255")] = {0};
sprintf(szStrMsgType, "%u", ucMsgType);
return szStrMsgType; // 警告:返回區域變數位址
}
解決方案:
static區域變數(注意重入問題)void ParseOmciMsgType(..., char *pszMsgType)),不可嘗試修改指標指向(pszMsgType = ... 無效)。執行緒若將自身堆疊結構指標傳給pthread_exit,pthread_join取得時原堆疊可能已被重用。
malloc分配的動態記憶體初值未定義,存取得垃圾值。對策:malloc後memset清零,或直接用calloc。
原因:剩餘空間不足、記憶體碎片化、記憶體越界破壞管理資訊。申請 GB 級大塊記憶體易失敗。對策:考慮 64 位元定址或暫存硬碟;分配後必須檢查 NULL 並防錯處理(return/exit)。
主要原因:
pMem++ 後 free(pMem))複雜邏輯下難以追蹤釋放時機,多副本指標易導致雙重釋放。對策:釋放失敗會崩潰,故障明顯,可用靜態/動態檢測工具排查。參考 SAFE_FREE 巨集(釋放後置 NULL),但無法解決多副本問題。根本在於設計階段簡化資料結構與流程。
同樣是 free,除錯版/釋出版、單/多執行緒庫實現不同,連結錯誤庫導致跨管理器分配釋放。封裝介面(GetBuffer/FreeBuffer)若混用 malloc/free 也會出問題。對策:分配釋放介面與次數必須配對;同一管理者負責並提供專用介面。
除明顯讀寫越界,sizeof 計算錯誤極常見:
T_CHK_MEM *pMem = malloc(sizeof(pMem)); // 錯誤:sizeof 指標而非結構體 memset(pMem, 0, sizeof(T_CHK_MEM)); // 越界寫入 // 正確寫法 T_CHK_MEM *pMem = malloc(sizeof(*pMem)); // 或 T_CHK_MEM *pMem = malloc(sizeof(T_CHK_MEM));
此類越界也是釋放失敗的常見原因。