【GDB调试-2】内存检查(AddressSanitizer的使用)

一、内存错误类型

在程序中有如下几种常见的错误类型:

  • 内存泄漏
  • 栈溢出
  • 堆溢出
  • 全局数据段溢出
  • 已经释放的内存继续被使用

二、内存泄漏检查和调试

2.1 AddressSanitizer

AddressSanitizer是一个C/C++内存错误探测器,它可以用于找出如下错误:

  • 内存释放后继续被使用
  • 堆溢出
  • 栈溢出
  • 全局数据区溢出
  • 函数返回后继续被使用
  • 访问超出范围后继续被使用
  • 初始化顺序的错误
  • 内存泄漏

它是一个快速的内存错误检测工具,它非常快,只拖慢程序两倍左右,从gcc 4.8开始,AddressSanitizer成为gcc的一部分,如果在链接的时候提示找不到/usr/lib64/libasan.so,可以通过安装包管理工具进行安装,例如Ubuntu:sudo apt-get install libasan

下面是从官方获取到的支持的操作系统和芯片架构:在这里插入图片描述
为了使用 AddressSanitizer,需要使用带有 -fsanitize=address 的选项编译和链接您的程序。要获得合理的性能,请添加 -O1 或更高。要在错误消息中获得更好的堆栈跟踪,请添加 -fno-omit-frame-pointer。

2.2 堆溢出

堆是一种数据结构,在C语言程序的数据可以被保存在栈区和堆区以及全局数据区,堆和栈同属于缓冲区,当程序开始运行后,堆和栈的大小就被确定了,在程序运行时申请的动态内存空间存放在堆中,水满则溢,当数据的大小超过了程序运行时申请的空间(访问越界),就会造成溢出。

下面是一段堆内存泄漏的代码,使用new申请了一个字节的空间,但是却向对应的内存中写入了14个字节。此时按照上述说法,已经造成了堆溢出。

#include <iostream>
#include <cstring>

char* memoryOverflowExample()
{
    char* ret = new char;
    
    strcpy(ret, "Hello World\r\n");

    return ret;
}

int main()
{
    char* ret = memoryOverflowExample();
    std::cout << ret << std::endl;
    delete ret;
}

将代码编译然后运行,程序并没有产生异常,结果也完全正确,这也充分说明了这类错误的隐蔽性。

./a.out

Hello World

为了检测出这个错误,在这里可以借助Address Sanitizer,使用方法如下(添加-g选项可以直接得到对应代码位置,否则只会显示代码地址信息):

g++ -fsanitize=address -g main.cpp

下面是重新编译后的运行结果:
在这里插入图片描述
运行之后报告了错误类型为heap-buffer-overflow,直接显示出了问题代码的位置,#0~#3就是对应的调用栈信息,下面还列出了错误地址周围的影子字节,一个影子字节代表应用程序中8个字节,对应字节的含义也在下面进行了说明。

这里需要给大家解释下图中用箭头指出的那一行的意思,所在行出现一个[01],按照下面的说明,是一个可以被部分访问的区域,能访问的长度就是1字节,中括号中就是代表可以访问的长度,如果我把上面的程序申请的内存改成8字节,那么这个位置会变成00,在他之后的位置会变成[fa](溢出位置),我们能访问这里的8字节,但是实际写入的是14字节,按照图中所示,它的下一个字节是标识为0xfa的,代表的是堆空间剩余可用的部分,当前处于空闲状态,所以没有产生错误,但是实际的程序往往比这个复杂的多,所以长时间运行肯定会出现,但是不容易重现。

2.3 内存泄漏

内存泄漏是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,由于一个进程的堆空间始终是有限的,32位的程序最多可以使用的内存不会超过2GB,如果代码中存在内存泄漏,短时间内程序不会崩溃,但是长时间运行直到没有内存可分配时,程序会崩溃。

LeakSanitizer 是集成到 AddressSanitizer 中的内存泄漏检测器。该工具在 x86_64 Linux 和 OS X 上受支持。

LeakSanitizer 在 x86_64 Linux 的 ASan 版本中默认启用,在 x86_64 OS X 上通过环境变量ASAN_OPTIONS=detect_leaks=1 启用。 LSan 处于休眠状态,直到运行结束,此时有一个额外的泄漏检测阶段。在性能关键的场景中,LSan 也可以在没有 ASan 检测的情况下使用。

如果您只需要泄漏检测,并且不想承受 ASan 的减速,您可以使用 -fsanitize=leak 而不是 -fsanitize=address 进行构建。这会将您的程序链接到一个运行时库,该库只包含 LeakSanitizer 工作基本所需,需要注意的是,与在 ASan 之上运行 LSan 相比,独立模式的测试效果较差。

下面是一段堆内存泄漏的代码,使用new申请了段空间,没有进行释放。

#include <iostream>
#include <cstring>
#include <cstdlib>

char* memoryLeakExample()
{
    char* ret = new char[10000];
    return ret;
}

int main()
{
    char* ret = memoryLeakExample();
}

编译命令:

g++ -g -fsanitize=address main.cpp

运行结果如下:
在这里插入图片描述
运行结果中侦测出内存错误,显示了调用栈信息和溢出数量。

2.4 栈溢出

栈也是一种数据结构,遵循LIFO的原则,在 x86架构和ARM架构中,进行push操作后栈地址会从高地址向低地址增长,函数调用时保存上下文的状态,参数传递,局部变量,函数返回地址,都会进行入栈,如果函数调用深度过大,特别是在一些递归算法中,可能会超过栈界限,造成栈溢出。

下面是测试代码:

#include <iostream>
#include <cstring>
#include <cstdlib>

char* stackOverflowExample()
{
    char buf[8];
    char *p = buf;
    strcpy(p, "Hello World\r\n");
    return p;
}

int main()
{
    char* ret = stackOverflowExample();
}

编译命令:

g++ -g -fsanitize=address main.cpp

运行结果:
在这里插入图片描述
结果显示了错误类型,调用栈信息和错误内存周围的影子字节。
[f3]为出错所在位置,在它前面由于申请了8个字节,所以那8个字节是可以访问的,所以显示00。

2.5 全局数据段溢出

全局数据区中使用指针和数组进行访问时,一旦超过变量本身的范围,也会造成溢出。

测试代码如下:

#include <iostream>
#include <cstring>
#include <cstdlib>

char g_buf[8];

char* globalOverflowExample()
{
    char *p = g_buf;
    strcpy(p, "Hello World\r\n");
    return p;
}

int main()
{
    char* ret = globalOverflowExample();
}

编译命令:

g++ -g -fsanitize=address main.cpp

运行结果:
在这里插入图片描述
结果显示了错误类型,调用栈信息和错误内存周围的影子字节。
[f9]为出错所在位置,在它前面那8个字节是可以访问的,所以显示00。

2.6 已经释放的内存继续被使用

在C语言中使用堆空间需要及时释放,否则就会造成内存泄漏,但是还有一种情况就是,内存已经回收,但是指向这片内存的指针没有重置为0,设置为0的好处就是可以通过处理器异常及时发现错误,如果没有设置为0继续被使用,也会造成内存错误访问。

测试代码:

#include <iostream>
#include <cstring>
#include <cstdlib>

char* useAfterDeleteExample()
{
    char* buf = new char[100];
    strcpy(buf, "Hello World\r\n");
    delete buf;
    return buf;
}

int main()
{
    char* ret = useAfterDeleteExample();
    std::cout << ret << std::endl;
}

编译命令:

g++ -g -fsanitize=address main.cpp

结果:
在这里插入图片描述

三、总结

为大家总结了一下避免踩坑的方法:

  • 在C++使用string代替char指针,原生指针容易访问越界,不安全。
  • 任何需要动态内存的东西都应该隐藏在一个RAII对象中,RAII在构造函数中分配内存并在析构函数中释放内存,这样当变量离开当前范围时,内存就可以被释放。
  • 除非要用旧的lib接口,否则不要使用原始指针。
  • malloc/new和free/delete成对使用,并且尽可能在同一个作用域下使用
  • 在C++中可以使用智能指针进行内存管理,但是只能是对象和变量,不能是数组。
  • delete之后的指针记得将其指向null,这样可以利用指令异常发现错误,部分情况可能因为上下文的关系导致释放了继续使用。
  • 代码编写完成后进行单元测试,及时发现其中的问题.
  • 在嵌入式操作系统中由于内存资源有限,线程的栈空间更加有限,在修改代码的过程中要记得及时调整栈空间,否则很容易因为代码的增加而导致栈溢出,加上调试环境比较复杂,更难发现其中的问题。
  • 使用现代化的工具进行问题分析。

养成良好的习惯可以帮助我们避免越过很多的坑,内存错误非常隐蔽,不容易被发现,借助专业的工具可以使我们更加便捷的进行调试,对程序进行优化。

  • 6
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
在 u-boot 中使用 gdb 进行调试,需要进行以下几个步骤: 1. 在配置文件中开启调试信息选项。在 u-boot 的配置文件(比如 `include/configs/board.h`)中添加以下选项: ``` #define CONFIG_DEBUG_UART 1 #define CONFIG_DEBUG_UART_BOARD_DETECT #define CONFIG_SYS_DEBUG 1 #define CONFIG_SYS_DEBUG_UART CONFIG_DEBUG_UART #define CONFIG_DEBUG_LL #define CONFIG_GDB_PORT 6666 ``` 其中,`CONFIG_GDB_PORT` 指定了 gdb 调试器连接的端口号。 2. 编译 u-boot。在编译 u-boot 时需要开启调试信息选项,可以使用以下命令: ``` make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- <board>_defconfig make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- all ``` 其中 `<board>` 为开发板的名称。 3. 烧录 u-boot 到开发板。将编译好的 u-boot 烧录到开发板中,可以使用 JTAG 调试器或者通过串口进行烧录。 4. 连接开发板和 host 机。通过串口连接开发板和 host 机,并使用以下命令启动 gdbserver: ``` arm-linux-gnueabihf-gdbserver :6666 ./u-boot ``` 其中 `./u-boot` 为编译好的 u-boot 的可执行文件。 5. 连接 gdb 调试器。在 host 机上打开一个新的终端窗口,使用以下命令连接到 gdbserver: ``` arm-linux-gnueabihf-gdb u-boot (gdb) target remote :6666 ``` 其中 `u-boot` 为编译好的 u-boot 的可执行文件。 6. 开始调试。使用 gdb 调试命令进行调试,比如设置断点、单步执行等。例如,设置断点可以使用以下命令: ``` (gdb) b main ``` 然后使用以下命令运行程序: ``` (gdb) c ``` 程序会在 `main` 函数处停下来,等待 gdb 命令。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

咕咚.萌西

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值