转自:https://www.hahack.com/wiki/tools-makefile.html#

调试器的出现固然极大地改善了可怜的程序员们的生活水平,然而调试器也并不总是扮演救世主的角色,例如,在有复杂竞争条件的多线程程序或者分布式程序中,调试器所能起的作用通常都不大。另外,调试运行和正常运行的程序实际上是有一定的差异的,有些神奇的 bug,当你以正常方式运行程序时,它跑出来作威作福,可以当你以调试模式运行程序的时候,它就躲得无影无踪了。更为极端的情况是没有调试器可以用,如果 gdb 的开发人员需要用 gdb 来调试 gdb 都还可以接受的话,那么 Linux kernel 的开发人员就真的是悲剧了。

因此,很多时候,我们需要在没有调试器的情况下进行调试,幸运的是,在这样的情况下,也是有一些约定的方法可以遵循的。

core dump

在 Linux 下,程序如果出现段错误退出,会产生 core dump 文件,默认情况下被 ulimit 禁用了这个功能,运行下面这个命令:

1
ulimit -c 5000

将允许系统产生 5kB 以内的 core dump 文件,可以根据自己的需求调整大小,并写到 shell 的启动脚本里。系统生成的 core dump 文件通常就是叫做 core,包含了程序出错时的整个状态,用 gdb 加载 core 文件:

1
gdb program core

就可以进行一些事后分析,例如,可以通过 backtrace 命令查出出错时的调用栈,并查看一些变量的值等,通常对于定位 bug 有很大的帮助。

printf 调试

printf 调试泛指通过记录程序执行状态来做调试的方法。具体来说,通常我们对于程序的行为和状态都有一些期望的值,通过将程序运行时的实际值打印出来,与期望的情况进行对比,就可以逐渐找到问题的所在。然而这种方法操作起来却有一些相当繁琐的地方:

首先,由于不知道问题出在哪里,又不能在所有的地方都添加输出语句(输出信息太多的话,要找到问题就变得困难了),所以通常会在可疑的地方添加输出语句,!如果结果发现猜错了,就需要换一个地方或者扩大范围,修改代码,重新编译,运行,再查看新的输出结果。对于编译时间很长(例如,有很多模版代码的 C++程序)的情况,整个过程会变得相当痛苦,因为可能需要重复很多次,并且许多时间都是在做无聊的等待。 其次,如果找到了问题所在,是不是要删除那些状态输出语句呢?过多的输出是会影响程序运行性能的,特别是打印到终端上。这些输出语句可能遍布代码的各个角落,要全部清除也不容易,而且,万一以后遇到了类似的 bug 呢?可能还要再写一遍这些类似的输出语句。另一个选择就是把他们注释掉。但是,无论如何,代码会被改得越来越乱。

避免让代码变乱的解决方案是使用标准化的工具,例如,最简单的情况,可以使用下面这样的一个宏:

1
2
3
4
5
#ifdef DEBUG
#define LOG(args) printf args
#else
#define LOG(args) ((void) 0)
#endif /* DEBUG */

需要记录信息的时候,使用

1
LOG(("a = %d, b = %d\n", a, b));

就可以了(注意双重括号是必要的),需要调试的时候,只要定义 DEBUG,就可以得到调试输出, 而调试结束之后可以直接去掉 DEBUG 的定义, 这样 LOG 宏在编译的时候就会变成空语句,也就不会产生任何输出了。即使想要移除这些调试语句,由于它们都有统一的格式,因此也可以方便地进行自动化处理。

对于更为复杂的项目,可以使用一些第三方的成熟的日志库来满足更复杂的需求,实现更灵活的控制。

总的来说, printf 调试主要用在两种情况下:

  • 过于简单的情况:懒得启动调试器了。
  • 过于复杂的情况:调试器已经无能为力了,例如一些分布式的程序。

assert 断言

程序一般分为 Debug 版本和 Release 版本, Debug 版本用于内部调试, Release 版本发行给用户使用。

断言 assert 是仅在 Debug 版本起作用的宏,它用于检查“不应该”发生的情况,为程序增加诊断功能。

1
void assert(int expression)

assert(expression)执行时,如果表达式的值为0,那么 assert 宏将在标准出错输出流 stderr 输出一条如下所示的信息:

1
Assertion failed: 表达式, file 文件名, line nnn

然后调用 abort 终止执行。其中的源文件名和行号来自于预处理程序宏 __FILE__ 和 __LINE__ 。

如果在头文件 assert.h 被包含时定义了宏 NDEBUG,那么宏 assert 将被忽略。

下例是一个内存复制函数。在运行过程中,如果 assert 的参数为假,那么程序就会中止(一般地还会出现提示对话,说明在什么地方引发了 assert)。

1
2
3
4
5
6
7
8
9
void *memcpy(void *pvTo, const void *pvFrom, size_t size)
{assert((pvTo != NULL) && (pvFrom != NULL)); // 使用断言byte *pbTo = (byte *) pvTo;  // 防止改变 pvTo 的地址byte *pbFrom = (byte *) pvFrom;    // 防止改变 pvFrom 的地址while(size -- > 0 )*pbTo ++ = *pbFrom ++ ;return pvTo;
}

在程序里许多地方插入断言也没有关系,断言在正常的时候并不会产生输出,而且在去掉调试选项之后,断言会编译为空语句,不会影响最终程序的性能。另外,断言通常是对程序状态的一个客观描述,还可以起到注释的作用。因此在代码中保留合适的断言是比较推荐的做法。

断言出错之后立即退出,而 printf 则需要事后再去分析和寻找问题。然而太过于暴力也算是断言的一个缺点,因为 bug 有大小疾缓,有时候让程序能持续稳定地运行也是很重要的,因此除非特别严重的时候,人们通常会倾向于使用更加温和的记录日志的方式来记录下潜在的 bug,而不是直接结束程序。

使用断言的规则:

  1. 使用断言捕捉不应该发生的非法情况。不要混淆非法情况与错误情况之间的区别,后者是必然存在的并且是一定要作出处理的。
  2. 在函数的入口处,使用断言检查参数的有效性(合法性)。
  3. 在编写函数时,要进行反复的考查,并且自问:“我打算做哪些假定?”一旦确定了的假定,就要使用断言对假定进行检查。
  4. 一般教科书都鼓励程序员们进行防错设计,但要记住这种编程风格可能会隐瞒错误。当进行防错设计时,如果“不可能发生”的事情的确发生了,则要使用断言进行报警。

func 变量

在打印调试信息时除了文件名和行号之外还可以打印出当前函数名,C99引入一个特殊的标识符__func__支持这一功能。这个标识符应该是一个变量名而不是宏定义,不属于预处理的范畴,但它的作用和__FILE____LINE__类似,所以放在一起讲。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>void myfunc(void)
{printf("%s\n", __func__);
}int main(void)
{myfunc();printf("%s\n", __func__);return 0;
}

输出:

1
2
3
4
$ gcc main.c
$ ./a.out
myfunc
main

调试宏

Mongrel 的作者 Zed A. Shaw 编写了一个更为实用的调试宏,内容只有如下短短几行:

#ifndef __dbg_h__
#define __dbg_h__#include <stdio.h>
#include <errno.h>
#include <string.h>#ifdef NDEBUG
#define debug(M, ...)
#else
#define debug(M, ...) fprintf(stderr, "DEBUG %s:%d: " M "\n", __FILE__, __LINE__, ##__VA_ARGS__)
#endif#define clean_errno() (errno == 0 ? "None" : strerror(errno))#define log_err(M, ...) fprintf(stderr, "[ERROR] (%s:%d: errno: %s) " M "\n", __FILE__, __LINE__, clean_errno(), ##__VA_ARGS__)#define log_warn(M, ...) fprintf(stderr, "[WARN] (%s:%d: errno: %s) " M "\n", __FILE__, __LINE__, clean_errno(), ##__VA_ARGS__)#define log_info(M, ...) fprintf(stderr, "[INFO] (%s:%d) " M "\n", __FILE__, __LINE__, ##__VA_ARGS__)#define check(A, M, ...) if(!(A)) { log_err(M, ##__VA_ARGS__); errno=0; goto error; }#define sentinel(M, ...)  { log_err(M, ##__VA_ARGS__); errno=0; goto error; }#define check_mem(A) check((A), "Out of memory.")#define check_debug(A, M, ...) if(!(A)) { debug(M, ##__VA_ARGS__); errno=0; goto error; }#endif

将它保存为 dbg.h 就可以在需要调试的地方引入该文件然后调用预定义的几个调试函数:

  • debug:当没有预定义 NDEBUG 宏时,调用形如 debug("format", arg1, arg2) 的语句将可以像 fprintf 一样输出内容到 stderr。如果预定义了 NDEBUG ,则调用 debug 函数将不会产生任何输出;
  • clean_errno:获得一个更安全且可读的 errno 版本。通常作为其他几个调试函数的参数;
  • log_errlog_warnlog_info:产生日志输出。和 debug 函数类似,但是不能通过设置 NDEBUG 来跳过执行;
  • check:非常有用的宏,可以检查条件 A 是否成立。如果不成立,将错误 M 输出到日志(利用 log_err 宏),并跳转到函数的错误处理部分(使用 error: 标号标记的语句段)。
  • sentinel:另一个实用的宏。用于放到一个不该被执行的函数里面。如果该函数被执行,则会打印一个错误信息,并跳转到函数的错误处理部分 error: 。常用的用法是将它放到 if 语句或 switch 语句中不该执行的边界条件里,例如 default: 语句段中;
  • check_mem:确保一个指针是有效的指针(不是空指针),如果该指针为空,则提示“Out of memory.”错误信息;
  • check_debug:和 check 类似,但是底层执行的是 debug 宏而非 log_err 宏,因此可以通过设置 NDEBUG 来禁用这些输出,但仍然会进行错误检查和处理。

总结

  1. 在绝大多数的情况下,使用调试宏来诊断和修复跟逻辑语句相关的错误。
  2. 使用 Valgrind 来捕捉所有跟内存相关的错误;
  3. 对于上面两个工具无法解决的诡异问题,或者在某些紧急的场合被逼尽可能多的获取错误相关信息的时候,才使用 gdb 。

【转】无调试器调试--使用调试宏相关推荐

  1. Visual Studio调试器指南---自动启动调试器

    visual studio 启动调试器,等待 app 连接 Visual Studio调试器指南---自动启动调试器 Visual Studio调试器指南---自动启动调试器 - 走看看 有时,可能需 ...

  2. 【瑞萨RA4系列】硬件调试器烧录和调试指南

    [瑞萨RA4系列]硬件调试器烧录和调试指南 文章目录 [瑞萨RA4系列]硬件调试器烧录和调试指南 一.背景简介 二.连接调试器 三.设置Keil项目 四.烧录和调试 4.1 Keil中烧录 4.2 K ...

  3. 【Windows 逆向】OD 调试器工具 ( OD 调试数据时硬件断点对应的关键代码 | 删除硬件端点恢复运行 )

    文章目录 前言 一.OD 调试数据时硬件断点对应的关键代码 二.删除硬件端点恢复运行 前言 在 [Windows 逆向]OD 调试器工具 ( CE 中获取子弹动态地址前置操作 | OD 中调试指定地址 ...

  4. 【Android 逆向】代码调试器开发 ( 代码调试器功能简介 | 设置断点 | 读写内存 | 读写寄存器 | 恢复运行 | Attach 进程 )

    文章目录 一.代码调试器功能简介 二.Attach 进程 一.代码调试器功能简介 代码调试器功能 : 设置断点 : 无论什么类型的调试器 , 都必须可以设置断点 , 运行到断点处 , 挂起被调试进程 ...

  5. Visual Studio 调试器---Visual Studio 调试器

    Visual Studio 调试器 启用内存泄漏检测 本主题适用于: Visual Studio 版本 Visual Basic C# C++ J# 速成版 否 否 本机 否 标准版 否 否 本机 否 ...

  6. 调试器原理_调试器的工作原理

    调试器原理 调试器是大多数(如果不是每种)开发人员在软件工程生涯中至少使用一次的软件之一,但是你们当中有多少人知道它们的实际工作原理? 在悉尼举行的linux.conf.au 2018上的演讲中,我将 ...

  7. python调试器原理_调试器工作原理——基础篇

    本文是一系列探究调试器工作原理的文章的第一篇.我还不确定这个系列需要包括多少篇文章以及它们所涵盖的主题,但我打算从基础知识开始说起. 关于本文 我打算在这篇文章中介绍关于Linux下的调试器实现的主要 ...

  8. 跨平台PHP调试器设计及使用方法——高阶封装

    在<跨平台PHP调试器设计及使用方法--协议解析>一文中介绍了如何将pydbgp返回的数据转换成我们需要的数据.我们使用该问中的接口已经可以构建一个简单的调试器.但是由于pydbgp存在的 ...

  9. 开源项目-基于Intel VT技术的Linux内核调试器

    本开源项目将硬件虚拟化技术应用在内核调试器上,使内核调试器成为VMM,将操作系统置于虚拟机中运行,即操作系统成为GuestOS,以这样的一种形式进行调试,最主要的好处就是调试器对操作系统完全透明.如下 ...

  10. 【Android 逆向】代码调试器开发 ( 等待进程状态改变 | detach 脱离进程调试 PTRACE_DETACH | 调试中继续运行程序 PTRACE_CONT )

    文章目录 一.等待进程状态改变 二.detach 脱离进程调试 PTRACE_DETACH 三.调试中继续运行程序 PTRACE_CONT 一.等待进程状态改变 上一篇博客 [Android 逆向]代 ...

最新文章

  1. 使用具体的例子来讲解如何使用Esper
  2. 使用netsh命令来管理IP安全策略(详细介绍)
  3. 四十四、深入Java 的序列化和反序列化
  4. boost::histogram::axis::circular用法的测试程序
  5. python开发cs软件_python cs架构实现简单文件传输
  6. linux安装python3教程_linux下安装python3和对应的pip环境教程详解
  7. mysql中数据库覆盖导入的几种方式
  8. LiveVideoStackCon 2017 Day 1 专场回顾 —— 多媒体与浏览器专场
  9. Java EE 7中的资源和依赖注入
  10. Python学习之路:多态实例
  11. caffe模型文件解析_Caffe ImageData神经网络基本示例无法解析模型文件
  12. 新申请了一个博客,这个博客主要用来记录编程学习笔记
  13. 表格闪退怎么解决_Excel中出现表格打开闪退的处理技巧
  14. matlab半峰宽计算公式,半峰宽单位换算(峰宽与半峰宽转换公式)
  15. 量化—神话、黑箱与真谛
  16. 看懂Oracle执行计划
  17. 物联网设计之智慧幼儿园(一)
  18. 论文笔记:OverFeat
  19. Java实现部标JTT1078实时音视频传输指令——视频流负载包(RTP)传输
  20. 带你深入了解 DNS 解析原理-递归与迭代

热门文章

  1. 软件需求分析作业提交测试
  2. be idle sometimes to_适合做签名的优质句子,简单干净,让你的朋友圈充满文艺气息...
  3. Android LOG系统原理剖析
  4. 在ubuntu下删除mysql数据库
  5. MyBatis-Plus 插件篇 >分页插件
  6. unbuntu14.04安装mxnet遇到的一些问题(未整理)
  7. 简单的电子词典 因手机问题未图
  8. CPU知识:主频、核心、线程、缓存、架构
  9. 看大佬总结的EMC知识,看完感觉太简单了!
  10. 云服务器选ssd还是hdd_选择物理服务器还是云服务器,你会怎么选呢?