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