關於C/C++語言的部分BUG

scanf格式匹配引起的錯誤

  運行以下程序時,出現這類錯誤:*** stack smashing detected ***: ./test_global terminated。錯誤緣由多是由於scanf("%d%d", &row, &col)接收的是int型,可是我使用的是short int,長度是Int的一半。修改爲int後錯誤消失。linux

#include<stdio.h>

int main(){ 
    int row, col;
    
    scanf("%d%d", &row, &col);
    printf("%d %d", row, col);
    return 0;
}

  使用gcc編譯時出現的警告以下:
    
  出現的錯誤以下:
    ios

局部變量被釋放引起的bug

  運行以下程序時,會無終止地打印-1。緣由是變量p所指向的變量k在addr()函數執行後自行銷燬,k所使用的內存被分配給loop()中的變量i,從而致使p指向i。而此時對p的操做是減1,對i的操做是加1,致使i的值始終爲-1,沒法跳出循環。shell

#include<stdio.h>

void addr();
void loop();

long *p;
int main(){
    addr();
    loop();
}

void addr(){
    long k;
    k = 0;
    p = &k;
}

void loop(){
    long i, j;
    j = 0;
    for (i = 0; i<10;i++){
        (*p)--;
        j++;
        printf("%d\n", i);
    }
}

  程序運行輸出結果以下:
    
  程序調試結果以下:
    數組

數組寫入超出索引維度

  雖然運行下面代碼不會出錯,可是對數組a[10]的寫操做超出了維度,致使在地址爲a+10的地方也寫入了數據,可是容易引起潛在bug。函數

#include<stdio.h>

int main()
{                                                        
    int i;
    int a[10];
    for (i = 0; i <= 10; ++i)
    {    
        a[i] = 0;
        printf("%d\n", i);
    }
    exit(0);    
}

指針的指針引起的思考

  對於將指針做爲參數進行傳遞時,若是是將在子函數內賦值給一個新申請的空間,那麼就要注意在傳遞指針時,須要傳遞指針的地址,即指針的指針。錯誤程序以下:oop

#include<stdio.h>

void allocateInt(int * i, int m);
void main()
{
    int m = 5;
    int * i = &m;
    printf("i address: %x\n", &i);
    allocateInt(i, m);
    printf("*i = %d\n", *i);
}

void allocateInt(int * i, int m)
{
    printf("i address: %x\n", &i);
    i = (int *) malloc(sizeof(int));
    *i = 3;
}

指針的指針引起的思考——思考

  雖然對該問題的解釋通常是:在傳遞參數時,系統爲子函數的變量新申請一部分空間,所以在void allocateInt(int * i)中,i的地址和在void main()中的地址是不一樣的,而void allocateInt(int * i)中的i是局部變量,在子函數運行結束會被釋放掉,所以void main()中的i是沒法獲得malloc的地址的,更不可能獲得新的賦值。
  下面經過gdb調試以及反彙編來進行說明:性能

  1. 程序在運行至main函數中的allocateInt(i, m);語句時,變量i和m的內存地址以下圖所示,&i=0x7fffffffdaf0,&m=0x7fffffffdaec:
  2. 以後使用命令si對彙編語言進行單步調試,連續運行5次si命令後(主要是保留變量i和m的值),程序進入allocateInt函數。進入時,i=0x7ffff7ffe168, m=0,也就是說i和m還並無被傳遞賦值,結果以下所示:

    但此時,變量i和m的地址是不一樣的,&i=0x7fffffffdac8,&m=0x7fffffffdac4,以下圖所示:
  3. 再運行5次彙編指令後,纔將參數的完成傳遞賦值,程序的指針纔開始指向void allocateInt(int * i, int m)中的printf("i address: %x\n", &i);,以下圖所示:

    此時的i和m已經被賦值,i=(int *) 0x7fffffffdaec, m=5。
  4. 針對在第3點提到的4次彙編指令,這裏進一步說明。
    • 第1條指令是push %rbp,也就是把rbp寄存器入棧;
    • 第2條指令是mov %rsp,%rbp,其中rsp是堆棧指針。也就是把堆棧指針的值賦值給rbp寄存器;
    • 第3條指令是sub $0x10,%rsp,也就是把堆棧指針所指向的地址減小16個字節。這是由於變量i和m一共佔用了16個字節;
    • 第4條指令是mov %rdi,-0x8(%rbp),也就是把寄存器rdi的值(rdi=0x7fffffffdaec,以下圖所示)賦值給i。由於i的地址就是rbp-0x8;
    • 第5條指令是mov %esi,-0xc(%rbp),做用相似於第4條,將寄存器esi的值(esi=0x5,以下圖所示)賦值給m。
  5. 關於寄存器的相關知識、gdb的調試命令能夠參考下面的參考資料;
  6. 關於彙編指令中出現的lea命令能夠網上查找,主要就是一種更加有效的mov方法;
  7. 關於彙編指令中出現的callq 0x4004a0 <printf@plt>,意思是調用print函數。可是這裏並非直接調用print函數,而是調用相似於print函數在進程中的別名。由於這是公用庫中的函數,所以不一樣進程中都會調用,因此只在進程中存留一個函數地址或者別名就好。具體參見stackoverflow上的一篇文章What does @plt mean here?

未定義賦值的變量引起的bug

  運行以下代碼時,本意是用g_logger.WriteLog()將"in A()"寫入文本文件中,可是結果倒是將"in A()"打印在了shell裏。ui

// file: main.cc
#include <iostream>
#include "CLLogger.h"

using namespace std;

extern CLLogger g_logger;

class A
{
public:
    A()
    {
    CLStatus s = g_logger.WriteLog("in A()", 0);
    if(!s.IsSuccess())
        cout << "g_logger.WriteLog error" << endl;
    }
};

A g_a;

CLLogger g_logger;

int main()
{
    return 0;
}

// file: CLLogger.h
#include "CLStatus.h"

class CLLogger
{
public:
    CLLogger();
    virtual ~CLLogger();

    CLStatus WriteLog(const char *pstrMsg, long lErrorCode);

private:
    CLLogger(const CLLogger&);
    CLLogger& operator=(const CLLogger&);

private:
    int m_Fd;
};

// file: CLStatus.h
class CLStatus
{
public:
    CLStatus(long lReturnCode, long lErrorCode);
    CLStatus(const CLStatus& s);
    virtual ~CLStatus();

public:
    bool IsSuccess();

public:
    const long& m_clReturnCode;
    const long& m_clErrorCode;

private:
    long m_lReturnCode;
    long m_lErrorCode;
};

  緣由是g_a是定義在g_logger以前,所以在運行到語句CLStatus s = g_logger.WriteLog("in A()", 0);時,g_logger仍未定義。但因爲在文件開頭聲明瞭extern CLLogger g_logger;,所以編譯器不會報錯,而此時默認將聲明爲外部變量的g_logger中的文件操做符m_Fd賦值爲0,以下:
    spa

題外話

  • 在編寫時注意局部性原理,提升性能。通常cache會把某次訪問的內存地址附近區域的內容都加載進去。若是在編寫程序時相鄰語句訪問的數據是在內存中連續的,那麼就會調高cache的命中率。
  • 在編寫時注意分支預測致使的性能問題。在向下跳轉的狀況下,優先將最有可能執行的語句放在if分支下,減小分支預測時的開銷(向下跳轉在靜態分支預測中通常默認不跳轉;向上跳轉在靜態分支預測中通常默認跳轉),例如:
int a = -5;
int b = 0; 
................................................
if(a > 0){                 if(a <= 0){
    b = 1;                        b = 2;
}                             }
else{                      else{
    b = 2;                    b=1;
}                            }

  關於分支預測的一些預測方式能夠參考一篇博客C++性能榨汁機之分支預測器

參考資料
Visual Studio文檔:寄存器使用
探究Linux下參數傳遞及查看和修改方法
gdb 調試入門,大牛寫的高質量指南
GDB的調試命令
What does @plt mean here?
C++性能榨汁機之分支預測器

相關文章
相關標籤/搜索