大家好,我是雜燴君。
C 語言內存問題,難在于定位,定位到了就好解決了。
這篇筆記我們來聊聊踩內存。踩內存,通過字面理解即可。本來是操作這一塊內存,因為設計失誤操作到了相鄰內存,篡改了相鄰內存的數據。
踩內存,輕則導致功能異常,重則導致程序崩潰死機。
內存,粗略地分:
- 靜態存儲區
- 動態存儲區
存儲于相同存儲區的變量才有互踩內存的可能。
靜態存儲區踩內存
分享一個之前在實際項目中遇到的問題。
在Linux中,一個進程默認可以打開的文件數為1024個,fd的范圍為0~1023。
項目中使用了串口,串口fd為static全局變量,某次這個fd突然變為一個超范圍得值,顯然被踩了。
出問題的代碼如:
float arr[5];
int count = 8;
for (size_t i = 0; i < count; i++)
{
arr[i] = xxx;
}
操作同屬于靜態存儲區的arr數組出現了數組越界操作,踩了后面幾個連續變量,fd也踩了。
實際中,純靠log打印調試很難定位fd的相鄰變量,需要花比較多的時間。
在Linux中,這個問題我們可以通過生成生成map文件來查看,在CMakeLists.txt中生成map文件的代碼如:
set(CMAKE_EXE_LINKER_FLAGS "-Wl,-Map=output.map") # 生成map文件
set(CMAKE_C_FLAGS "-fdata-sections") # 把static變量地址輸出到map文件
set(CMAKE_CXX_FLAGS "-fdata-sections")
動態存儲區踩內存
動態堆內存踩內存典型例子:malloc與strcpy搭配使用不當導致緩沖區溢出。
#include < stdio.h >
#include < stdlib.h >
#include < unistd.h >
#include < string.h >
int main (void)
{
char *str = "hello";
int str_len = strlen(str);
///< 此時str_len = 5
printf("str_len = %d\\n", str_len);
///< 申請5字節的堆內存
char *ptr = (char*)malloc(str_len);
if (NULL == ptr)
{
printf("malloc error\\n");
exit(EXIT_FAILURE);
}
///< 定義一個指針p_a指向ptr向后偏移5字節的地址, 并在這個地址里寫入整數20
char *p_a = ptr + 5;
*p_a = 20;
printf("*p_a = %d\\n", *p_a);
///< 拷貝字符串str到ptr指向的地址
strcpy(ptr, str);
///< 打印結果:a指向的地方被踩了
printf("ptr = %s\\n", ptr);
printf("*p_a = %d\\n", *p_a);
///< 釋放對應內存
if (ptr)
{
free(ptr);
ptr = NULL;
}
return 0;
}
運行結果:
顯然,經過strcpy操作之后,數據a的值被篡改了。
原因:忽略了strcpy操作會把字符串結束符一同拷貝到目的緩沖區。
如果相鄰的空間里沒有存放其它業務數據,那么踩了也不會出現問題,如果正好存放了重要數據,這時候可能會出現大bug,而且可能是偶現的,不好復現定位。
針對這種情況,我們可以借助一些工具來定位問題,比如:
- dmalloc
- valgrind
valgrind的簡單使用可閱讀往期筆記:工具 | Valgrind仿真調試工具的使用
當然,我們也可以在我們的代碼里進行一些嘗試。針對這類問題,分享一個檢測思路:
我們在申請內存時,在申請內存的前后增加兩塊標識區(紅區),里面寫入固定數據。申請、釋放內存的時候去檢測這兩塊標識區有沒有被破壞(檢測操作堆內存時是否踩到高壓紅區)。
為了能定位到后面的標識區,在增加一塊len區用來存儲實際申請的空間的長度。
此處,我們定義:
- 前紅區(before_ red_area):4字節。寫入固定數據0x11223344。
- 后紅區(after_ red_area):4字節。寫入固定數據0x55667788。
- 長度區(len_area):4字節。存儲數據存儲區的長度。
自定義申請內存函數
除了數據存儲區之外,多申請12個字節。自定義申請內存的函數自然是要兼容malloc的使用方法。malloc原型:
void *malloc(size_t __size);
自定義申請內存的函數:
void *Malloc(size_t __size);
返回值自然要返回數據存儲區的地址。具體實現:
#define BEFORE_RED_AREA_LEN (4) ///< 前紅區長度
#define AFTER_RED_AREA_LEN (4) ///< 后紅區長度
#define LEN_AREA_LEN (4) ///< 長度區長度
#define BEFORE_RED_AREA_DATA (0x11223344u) ///< 前紅區數據
#define AFTER_RED_AREA_DATA (0x55667788u) ///< 后紅區數據
void *Malloc(size_t __size)
{
///< 申請內存:4 + 4 + __size + 4
void *ptr = malloc(BEFORE_RED_AREA_LEN + AFTER_RED_AREA_LEN + __size + LEN_AREA_LEN);
if (NULL == ptr)
{
printf("[%s]malloc error\\n", __FUNCTION__);
return NULL;
}
///< 往前紅區地址寫入固定值
*((unsigned int*)(ptr)) = BEFORE_RED_AREA_DATA;
///< 往長度區地址寫入長度
*((unsigned int*)(ptr + BEFORE_RED_AREA_LEN)) = __size;
///< 往后紅區地址寫入固定值
*((unsigned int*)(ptr + BEFORE_RED_AREA_LEN + LEN_AREA_LEN + __size)) = AFTER_RED_AREA_DATA;
///< 返回數據區地址
void *data_area_ptr = (ptr + BEFORE_RED_AREA_LEN + LEN_AREA_LEN);
return data_area_ptr;
}
自定義檢測內存函數
申請完內存并往內存里寫入數據后,檢測本該寫入到數據存儲區的數據有沒有寫到紅區。這種內存檢測方法我們是用在開發調試階段的,所以檢測內存,我們可以使用斷言,一旦觸發斷言,直接終止程序報錯。
檢測前后紅區里的數據有沒有被踩:
void CheckMem(void *ptr, size_t __size)
{
void *data_area_ptr = ptr;
///< 檢測是否踩了前紅區
printf("[%s]before_red_area_data = 0x%x\\n", __FUNCTION__, *((unsigned int*)(data_area_ptr - LEN_AREA_LEN - BEFORE_RED_AREA_LEN)));
assert(*((unsigned int*)(data_area_ptr - LEN_AREA_LEN - BEFORE_RED_AREA_LEN)) == BEFORE_RED_AREA_DATA);
///< 檢測是否踩了長度區
printf("[%s]len_area_data = 0x%x\\n", __FUNCTION__, *((unsigned int*)(data_area_ptr - LEN_AREA_LEN)));
assert(*((unsigned int*)(data_area_ptr - LEN_AREA_LEN)) == __size);
///< 檢測是否踩了后紅區
printf("[%s]after_red_area_data = 0x%x\\n", __FUNCTION__, *((unsigned int*)(data_area_ptr + __size)));
assert(*((unsigned int*)(data_area_ptr + __size)) == AFTER_RED_AREA_DATA);
}
自定義釋放內存函數
要釋放所有前面申請內存。釋放前同樣要進行檢測:
void Free(void *ptr)
{
void *all_area_ptr = ptr - LEN_AREA_LEN - BEFORE_RED_AREA_LEN;
///< 檢測是否踩了前紅區
printf("[%s]before_red_area_data = 0x%x\\n", __FUNCTION__, *((unsigned int*)(all_area_ptr)));
assert(*((unsigned int*)(all_area_ptr)) == BEFORE_RED_AREA_DATA);
///< 讀取長度區內容
size_t __size = *((unsigned int*)(all_area_ptr + BEFORE_RED_AREA_LEN));
///< 檢測是否踩了后紅區
printf("[%s]before_red_area_data = 0x%x\\n", __FUNCTION__, *((unsigned int*)(all_area_ptr + BEFORE_RED_AREA_LEN + LEN_AREA_LEN + __size)));
assert(*((unsigned int*)(all_area_ptr + BEFORE_RED_AREA_LEN + LEN_AREA_LEN + __size)) == AFTER_RED_AREA_DATA);
///< 釋放所有區域內存
free(all_area_ptr);
}
我們使用這種方法檢測上面的 malloc與strcpy搭配使用不當導致緩沖區溢出
的例子:
可以看到,這個例子踩了后紅區,把后紅區數據修改為了 0x55667700
,觸發斷言程序終止。
測試代碼:
// 公眾號:嵌入式大雜燴
#include < stdio.h >
#include < stdlib.h >
#include < unistd.h >
#include < string.h >
#include < assert.h >
#define BEFORE_RED_AREA_LEN (4) ///< 前紅區長度
#define AFTER_RED_AREA_LEN (4) ///< 后紅區長度
#define LEN_AREA_LEN (4) ///< 長度區長度
#define BEFORE_RED_AREA_DATA (0x11223344u) ///< 前紅區數據
#define AFTER_RED_AREA_DATA (0x55667788u) ///< 后紅區數據
void *Malloc(size_t __size)
{
///< 申請內存:4 + 4 + __size + 4
void *ptr = malloc(BEFORE_RED_AREA_LEN + AFTER_RED_AREA_LEN + __size + LEN_AREA_LEN);
if (NULL == ptr)
{
printf("[%s]malloc error\\n", __FUNCTION__);
return NULL;
}
///< 往前紅區地址寫入固定值
*((unsigned int*)(ptr)) = BEFORE_RED_AREA_DATA;
///< 往長度區地址寫入長度
*((unsigned int*)(ptr + BEFORE_RED_AREA_LEN)) = __size;
///< 往后紅區地址寫入固定值
*((unsigned int*)(ptr + BEFORE_RED_AREA_LEN + LEN_AREA_LEN + __size)) = AFTER_RED_AREA_DATA;
///< 返回數據區地址
void *data_area_ptr = (ptr + BEFORE_RED_AREA_LEN + LEN_AREA_LEN);
return data_area_ptr;
}
void CheckMem(void *ptr, size_t __size)
{
void *data_area_ptr = ptr;
///< 檢測是否踩了前紅區
printf("[%s]before_red_area_data = 0x%x\\n", __FUNCTION__, *((unsigned int*)(data_area_ptr - LEN_AREA_LEN - BEFORE_RED_AREA_LEN)));
assert(*((unsigned int*)(data_area_ptr - LEN_AREA_LEN - BEFORE_RED_AREA_LEN)) == BEFORE_RED_AREA_DATA);
///< 檢測是否踩了長度區
printf("[%s]len_area_data = 0x%x\\n", __FUNCTION__, *((unsigned int*)(data_area_ptr - LEN_AREA_LEN)));
assert(*((unsigned int*)(data_area_ptr - LEN_AREA_LEN)) == __size);
///< 檢測是否踩了后紅區
printf("[%s]after_red_area_data = 0x%x\\n", __FUNCTION__, *((unsigned int*)(data_area_ptr + __size)));
assert(*((unsigned int*)(data_area_ptr + __size)) == AFTER_RED_AREA_DATA);
}
void Free(void *ptr)
{
void *all_area_ptr = ptr - LEN_AREA_LEN - BEFORE_RED_AREA_LEN;
///< 檢測是否踩了前紅區
printf("[%s]before_red_area_data = 0x%x\\n", __FUNCTION__, *((unsigned int*)(all_area_ptr)));
assert(*((unsigned int*)(all_area_ptr)) == BEFORE_RED_AREA_DATA);
///< 讀取長度區內容
size_t __size = *((unsigned int*)(all_area_ptr + BEFORE_RED_AREA_LEN));
///< 檢測是否踩了后紅區
printf("[%s]before_red_area_data = 0x%x\\n", __FUNCTION__, *((unsigned int*)(all_area_ptr + BEFORE_RED_AREA_LEN + LEN_AREA_LEN + __size)));
assert(*((unsigned int*)(all_area_ptr + BEFORE_RED_AREA_LEN + LEN_AREA_LEN + __size)) == AFTER_RED_AREA_DATA);
///< 釋放所有區域內存
free(all_area_ptr);
}
int main (void)
{
char *str = "hello";
int