首先看一段必現的Crash程序。
char* do_strcpy(char* pDstStr, const char* pSrcStr)
{
char* pRet = pDstStr;
while ((*(pDstStr++) = *(pSrcStr++))!='\0'){;}
return pRet;
}
bool __strcpy(const char* pSrcStr)
{
char destStr[8] = {0};
do_strcpy(&destStr[-24], pSrcStr);
printf("ok\n");
printf(destStr);
return true;
}
int main()
{
printf("input a string: \n");
char strInput[256] = {0};
fgets(strInput, 256, stdin);
__strcpy(strInput);
getchar();
return 0;
}
這段代碼必然crash,下面分析下原因。輸入一段長字符串,比如50個1,執行到__strcpy中後,在棧上申請一個char[],長度爲8+1.然後調用do_strcpy並傳參,然後執行printf("ok\n").
函數的調用需要壓棧,在調用do_strcpy前,在棧上開闢了9個字節的空間,調用do_strcpy時,首先將返回後的下一個指令地址壓棧,即將printf("ok\n")的函數地址壓棧。然後穿參數,在vc上參數從右到左壓棧,最後傳入do_strcpy的函數地址。待函數執行完後依次出棧,最終得到printf("ok\n")並繼續執行。
do_strcpy傳入的第一個參數是棧上的數組destStr向前偏移24個字節。在do_strcpy中,由於執行了字符串拷貝,且目標字符串的其實地址是destStr[-24],所以拷貝後,數據從destStr[-24]一路覆蓋到destStr[0],再到destStr[7],當然,函數do_strcpy的入參,及返回後下一條指令地址也都被覆蓋,寫入的是什麼取決於Input a String時的輸入。所以,下一條指令地址出錯了,crash也就不奇怪了。
由此擴展到C/C++程序的內存分佈狀況。
從高地址到低地址分別爲棧、堆、未初始化的靜態和全局變量,初始化的靜態和全局變量,以及常量,代碼段。
其中,棧由高地址向低地址生長,棧頂在低地址。
堆由低地址向高地址生長。
抄一段網上的經典代碼:
int a = 0; //全局初始化區
char *p1; //全局未初始化區
main()
{
int b; //棧
char s[] = "abc"; //棧
char *p2; //棧
char *p3 = "123456"; //123456\0在常量區,p3在棧上。
static int c = 0; //全局(靜態)初始化區
p1 = (char *)malloc(10);
p2 = (char *)malloc(20);
//分配得來得10和20字節的區域就在堆區。
strcpy(p1, "123456");
//123456\0放在常量區,編譯器可能會將它與p3所指向的"123456"優化成一塊。
}
堆的使用更復雜一些,如下圖:
程序通過VirtualAlloc和FileMapping可以直接控制虛擬內存進行內存分配,通過HeapAlloc、LocalAlloc和new、malloc可以通過堆管理器動態使用內存,其中,new、malloc是標準庫的函數,在不同平臺下調用的系統api不同,需要經過crt。