• 关于C/C++语言的部分BUG


    scanf格式匹配引发的错误

      运行如下程序时,出现这类错误:*** stack smashing detected ***: ./test_global terminated。错误原因可能是因为scanf("%d%d", &row, &col)接收的是int型,但是我使用的是short int,长度是Int的一半。修改成int后错误消失。

    #include<stdio.h>
    
    int main(){	
        int row, col;
        
        scanf("%d%d", &row, &col);
        printf("%d %d", row, col);
        return 0;
    }
    

      使用gcc编译时出现的警告如下:
        
      出现的错误如下:
        

    局部变量被释放引发的bug

      运行如下程序时,会无终止地打印-1。原因是变量p所指向的变量k在addr()函数执行后自行销毁,k所使用的内存被分配给loop()中的变量i,从而导致p指向i。而此时对p的操作是减1,对i的操作是加1,导致i的值始终为-1,无法跳出循环。

    #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
    ", 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
    ", i);
    	}
    	exit(0);    
    }
    

    指针的指针引发的思考

      对于将指针作为参数进行传递时,如果是将在子函数内赋值给一个新申请的空间,那么就要注意在传递指针时,需要传递指针的地址,即指针的指针。错误程序如下:

    #include<stdio.h>
    
    void allocateInt(int * i, int m);
    void main()
    {
    	int m = 5;
    	int * i = &m;
    	printf("i address: %x
    ", &i);
    	allocateInt(i, m);
    	printf("*i = %d
    ", *i);
    }
    
    void allocateInt(int * i, int m)
    {
    	printf("i address: %x
    ", &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 ", &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里。

    // 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,如下:
        

    题外话

    • 在编写时注意局部性原理,提高性能。一般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++性能榨汁机之分支预测器

  • 相关阅读:
    在IE浏览器中url传参长度问题
    Linq语句的认识
    关于选择表达式以及判断语句的书写,可以让代码更加的清晰。
    C#/对线程的认识
    Js/如何修改easyui修饰的input的val值
    Java Lambda表达式中的this
    MySQL USING关键词/USING()函数的使用
    复杂SQL查询
    Java 修饰符
    Git:idea中将当前分支修改的内容提交到其他分支上
  • 原文地址:https://www.cnblogs.com/wFrancow/p/9850684.html
Copyright © 2020-2023  润新知