GDB调试----基本用法

示例代码下载地址

第一章 gcc的编译过程

1。编译过程

  • gcc -E hello.c -o hello.i:1.预处理,生成预编译文件
  • gcc -S hello.i -o hello.s:2.编译,生成汇编文件
  • gcc -c hello.s -o hello.o:3.汇编生成对象文件,可以用objdump查看
    • as hello.s -o hello.o
  • gcc main.o hello.o4.链接

2。热身准备:

  • 1.复现之前的心得
    • 配置环境
      • 挂载、内核模块参数、选项
      • 网络通信对方的硬件(网卡)
      • 磁盘的硬件制造商
      • 配置文件的内容
    • 听取:有疑问或需要更多信息应总结之后一起问,此外,对方可能不会告诉你重要消息
    • 重新审视配置:确认网线、配置文件内容、命令确认
  • 2.复现之后的心得
    • 确认现象:有些现象很像但bug不是真的复现了
    • 确认复现率和时间
  • 3.分析时候的心得
    • 目视确认现象
      • 某种操作后重启的例子,应当确认,是执行特定操作后出现panic就重启,还是几十秒后有watchdog重启,重启前能否操作、ping是否畅通
      • 控制台消息无法显示到画面上,请执行 echo 7 > /proc/sys/kernel/printk
    • 尽量缩小范围,缩短复现时间、提高复现概率
    • 根据内核配置、内核启动参数划分问题
      • 内核bug,可修改启动参数、内核配置方法。启动参数加上nosmp,确认是否为SMP环境下发生bug,e1000驱动bug,禁用NAPI也许能做出判断
    • 根据版本划分问题:低版本时候发生
    • 其他途径
      • 如网络问题,不仅查ifconfig、还需考虑ip命令、route命令、/proc/net信息
    • 根据事实做出判断
  • 4.问题不明时心得
    • 怀疑硬件问题
    • 找找以前改正的同类错误:git或Bugzilla中关键字搜索
    • 无法复现、原因不明时:加调试信息
    • 为bug做准备:输出日志,定期获取内存、网络、IO、CPU使用率,sar、top、free、/proc/meminfo、/proc/slainfo等日志
    • 跟同事讨论
    • 咨询社区

二章 调试前的必会知识

4. 获取内核的进程转储

4.1 举例
  • ulimit -c:查看当前内核转储是否有效
  • ulimit -c unlimited:不限制内核转储文件大小
  • ulimit -c 1073741824:设置内核转储文件上限
  • 打开/etc/sysctl.conf设置core文件路径
    # %p-进程ID,%s-引发转储的信号编号,%t-转储时刻,%e-可执行文件名
    kernel.core_pattern=/mnt/f/core/core.%t-%e-%p
    kernel.core_uses_pid=0
    
  • sysctl -p
  • 新建测试文件
    #include <stdio.h>
    int main(void){
       int *a=NULL;
       *a=0x1;
       return 0;
    }
    
  • 编译:gcc 14_a.c -g#注意加上-g选项保存调试信息
  • ./a.out#运行,,正常应该报错
  • file *core:在当前目录下查看生成的内核转储文件
  • gdb -c core ./a.out:用GDB调试生成的内核转储文件,出现如下信息
    [New LWP 3096]
    Core was generated by `./a.out'.
    Program terminated with signal SIGSEGV, Segmentation fault.
    #0  0x00000000004004e6 in main () at 14_a.c:4
    4	   *a=0x1;
    
    • 指示第7行在访问a地址的时候出现错误
4.2 启用整个系统的内核转储功能
  • 修改会添加 /etc/profie:
    ulimit -S -c unlimited > /dev/null 2>&1
    DAEMON_COREFILEFILE='unlimited'
  • 在文件/etc/sysctl.conf中添加:fs.suid_dumpable=1
  • 重启系统
4.3 利用内核转储掩码排除共享内存

5. 调试器(GDB)的基本使用方法(之一)

1.带着调试选项编译
  • gcc -wall -02 -werror -g 源文件
    • -g:保存调试信息
    • -werror:在警告发生时当做错误来处理
  • CFLAGS = -Wall -O2 -g
  • ./configure CFLAGS="-Wall -O2 -g"
2.启动调试器
  • 运行gdb
    • gdb a.out
    • ./a.out ;ls *core#运行并查看内核转储文件
    • gdb -c core ./a.out:用GDB调试生成的内核转储文件,出现如下信息
  • 设置断点
    • break 函数名/行号/文件名:行号/文件名:函数名/+偏移量/-偏移量/*地址
      • b main:在main函数出设置断点
      • b test.cpp:4:在main函数出设置断点
      • b +3:现在暂停位置往后3行
    • info braek:查看断点
  • 运行
    • run -a:运行程序,不加参数会执行到设置了断点的位置
    • start:同上
  • 显示栈帧
    • backtrace full -N:在端点处显示最后的N个栈帧,简写bt
    • bt 3:显示前3个栈帧
    • bt full -3:从外向内显示3个栈帧及局部变量
  • 显示变量
    • print:显示局部变量
      • p *argv:打印argv指针指向的值
  • 显示寄存器
    • info reg/registers:显示寄存器值
    • p/c $eax:ASCII显示寄存器的值
      • u:无符号十进制
      • o:八进制
      • t:二进制
      • d:十进制
      • f/s/c/a/i:浮点/字符串/ASCII/地址/机器语言
  • 显示内存内容
    • x/NFU addr
      • n为重复次数,F为格式,U:
        • b字节,h半字,w字,g双字
        • x/10i $pc:显示地址从pc开始的10条汇编代码
  • 反汇编
    • disassemble:反汇编当前的整个函数
    • disassemble 程序计数器:反汇编程序计数器所在函数的整个函数
    • disassemble 开始地址 结束地址:反汇编从开始地址到结束地址之间的部分
      • disas $pc:反汇编程序计数器所在函数的整个函数
      • disas $pc $pc+50:从开始到结束地址之前的部分
  • 单步执行
    • next:执行下一步
    • step:执行到函数内部,简写为 p
    • nexti/stepi:逐条执行汇编指令
  • 继续运行
    • continue:继续执行,加数字如 c 5表遇到5次断点不停止
    • continue 5:忽略5次断点
  • 监视点
    • watch <表达式>:常量或变量发生变化时暂停运行
    • awatch <表达式>:被访问、改变时暂停访问
    • rwatch <表达式>:被访问时暂停运行
      • awatch short_output:short_output被访问时暂停运行
  • 删除断点或监视点
    • 先通过info b查看断点编号
    • delete 2:删除断点或监视点编号3
  • 改变变量的值
    • set variable <变量>=<表达式>:如set variable a=1,把a的值改为1
  • 生成内核转储文件
    • generate-core-file:生成内核转储文件
    • gcore 'pidof emacs':在命令行直接生成内核转储文件,无须停止程序

6. 调试器(GDB)的基本使用方法(之二)

  • attach到进程
    • attach pid:attach到进程ID为pid的进程上
    • detach:和进程分离
    • info proc:显示进程信息
  • 条件断点
    • break 断点 if 条件:条件断点
      • b iseq_compile if node==0
    • condition 断点编号 条件
      • 给指定断点添加或删除触发条件
  • 反复执行
    • ignore 断点编号 次数:反复执行
    • step/stepi/next/netxi/continue 次数:指定命令执行的次数
  • 删除断点和禁用断点
    • clear:清空已定义的断点
      • clear/clear 函数名/行号…
    • disable/enable:禁用/启用断点
      • disable display:禁用display命令定义的自动显示
      • disable men:禁用men命令定义的内存区域
      • enable [breakpoints] once 断点编号:启用一次断点编号
  • 断点命令:在断点中继后自动执行命令
    commands 断点编号
       命令
       ...
    end
    
    • 例:
      (gdb) command 2
      Type commands for when breakpoint 2 is hit,one per line.
      End whth a line saying just "end".
      >p *iseq
      >end
      
  • 其他命令
    • directory:dir,插入目录
    • down/up:在当前栈帧中选择要显示的栈帧
    • edit:编辑文件或函数
    • frame:选择要显示的栈帧
    • forward-search:向前搜索
    • list:l,显示函数或行
    • print-object:po,显示目标信息
    • sharedlibrary:share,加载共享库的符号
    • finish:执行完当前函数后暂停
    • until <地址>:执行完当前代码块后跳出循环,常用于跳出循环

7. 调试器(GDB)的基本使用方法(之三)

  • p $:显示最后一次print输出的值
  • show walue:显示历史中print的最后十条值

8. Intel架构基础知识(之三)

  • 32位环境中的寄存器
    • 1个 32bit EIP寄存器
    • 1个 32bit EFFLAGS寄存器
    • 6个 16bit 段寄存器
      • CS: 代码段
      • DS: 数据段
      • SS: 堆栈段
      • ES: 数据段
      • FS: 数据段
      • GS: 数据段
    • 8个 32bit 通用寄存器
      • ESP: 栈指针寄存器,指针指向顶部栈顶
      • EBP: 基址指针寄存器,指针指向顶部栈底
      • EAX: 操作数的运算、结果
      • EBX: 指向DS段中数据的指针
      • EXC: 字符串操作或循环计数器
      • EDX: 输入输出指针
      • ESI: …
      • EDI: …
  • 64位环境中的寄存器

第四章 应用程序调试实践

1. SIGSEGV:栈溢出导致segmentation fault

  • 引发SIGSEGV信号的非法内存操作包括:
    • NULL指针访问
    • 指针被破坏导致非法地址访问
    • 栈溢出导致访问超出已分配的地址空间
  • 示例代码(递归太多导致的栈溢出):
static int recursion(int num) {
    if(num <= 0)return 2;
    return recursion(num - 1)%7;
}
int main(int argc,char *argv[]){
    int num = 10000000;
    printf("递归深度 = %d\n",num);
    recursion(num);
    return 0;
}
  • 运行如下:
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zaB8xdsx-1624958900032)(./picture/segv1.png)]
    • 仔细观察发现 0x0001050c 地址的函数被多次调用,可以怀疑是递归调用导致的栈溢出
      • 注:本文的示例代码不具参考价值(一眼看出递归导致),原文中用到了的ruby源码中一个函数,是需要展开宏才能看到递归的逻辑的
    • info signal可查看gdb能够处理的信号
  • 处理方法

栈溢出导致的 SIGSEGV 发生时,栈空间已经溢出,无法保证启动信号捕获处理程序运行所需要的栈空间。一次,需要使用备用栈 – sigaltstack()

  • 示例:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <execinfo.h>

#include <bits/types/stack_t.h>

//cc -rdynamic prog.c -o prog
#define BT_BUF_SIZE 10
void showBacktrace(void)
{
    int j, nptrs;
    void *buffer[BT_BUF_SIZE];
    char **strings;

    nptrs = backtrace(buffer, BT_BUF_SIZE);
    printf("backtrace() returned %d addresses\n", nptrs);

    /* The call backtrace_symbols_fd(buffer, nptrs, STDOUT_FILENO)
              would produce similar output to the following: */

    strings = backtrace_symbols(buffer, nptrs);
    if (strings == NULL)
    {
        perror("backtrace_symbols");
        exit(EXIT_FAILURE);
    }

    for (j = 0; j < nptrs; j++)
        printf("%s\n", strings[j]);
    free(strings);
}

/** 信号处理函数 */
void sig_handler(int sig)
{
    switch (sig)
    {
    case SIGSEGV:
        showBacktrace();
        break;
    default:
        ;
        break;
    }
    exit(-1);
}
/** 信号处理函数 */
static sighandler_t ruby_signal(int signum, sighandler_t handler)
{
    static struct sigaction sigact, old;
    if (signum == SIGSEGV)
        sigact.sa_flags |= SA_ONSTACK;

    sigact.sa_handler = handler; /* Address of a signal handler */
    if (sigaction(signum, &sigact, &old) < 0)
        perror("in ruby_signal() sigaction()");

    return old.sa_handler;
}
/** 栈溢出测试 */
unsigned stackOver(unsigned x){return stackOver(x) + 1;}
/** 备用栈注册 */
void register_sigaltstack()
{
    static int is_altstack_defined = 0;
    stack_t newSS, oldSS;
    if (is_altstack_defined)
        return;

    newSS.ss_sp = malloc(SIGSTKSZ);
    if (!newSS.ss_sp)
    {
        perror("in register_sigaltstack() malloc():");
        return;
    }
    newSS.ss_size = SIGSTKSZ;
    newSS.ss_flags = 0;
    if (sigaltstack(&newSS, &oldSS) == -1)
    {
        perror("in register_sigaltstack()  sigaltstack():");
        return;
    }
    is_altstack_defined = 1;
}
int main(int argc,char *argv[])
{
    register_sigaltstack();
    ruby_signal(SIGSEGV, sig_handler);
    stackOver(0);
    return 0;
}
  • 运行效果对比如下:
    • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R7xw3FAh-1624958900033)(./picture/segvCatch.png)]

    总结:段错误总是要结束的,但是与其什么信息也不给,不如输出异常结束的提示信息,有助于应用程序的调试

    • 注意:后文其实也会说明,对于多线程的环境,该方法是不太适用的,因为信号捕获函数打印的栈帧信息是该函数所在栈帧的,而发出该信号的线程不一定是同一个线程,《unix 环境高级编程》里提到过同一个进程里的线程对信号是同享的,哪个线程捕获到是不一定的,如果你没处理过的话。

2. backtrace 无法正确显示

程序运行时生成的core执行backtrace,却完全看不出什么函数被调用

  • 什么时 backtrace
    • backtrace 是根据栈里保存的函数的返回地址来显示的,根据栈空间上返回地址和调试信息得出栈使用量,一次求出调用者函数,即调试器的 backtrace 地址来自进程的栈上
      • 信赖 backtrace 只有在栈没有被破坏的前提下才能成立,认为调试器的 backtrace 信息绝对正确是十分危险的
  • 查看寄存器和栈
    • info reg
      • 比如若已知 rip 的值是 0x3b4869ac80
        • 那可以通过x/i 0x3b4869ac80查看下一步要执行什么指令
      • 比如已知 rsp 的值是 0x4162f0c8
        • 那可以通过x/i 0x4162f0c8查看下一步要返回的地址

3. 数组非法访问导致内存破坏

典型bug之一就是缓冲区溢出,就是向已分配内存空间之外写入数据,如果发生在栈上就可能引发安全漏洞,因

  • 有许多典型的防御措施:
    • 指定缓冲区大小
    • 源代码检查工具
    • 编译器报警
  • 下图是一种栈被破坏后的backtrace
    • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NSBI4X93-1624958900033)(./picture/arrOver.png)]
    • 前一节讲过这种栈帧信息是不可信的,代码突然跳转或调用了错误的 0x20656c62 导致段错误的发生
  • 栈帧的#0、#1 显示的地址很难放置程序、共享内存等,大多数的 i386 架构的linux发行版本中的地址如下:
    • 0x08000000–>程序
    • 0xb0000000–>共享库
  • X86_64 中:
    • 程序–> 0x400000 或 0x06000000附近
    • 共享库–> 0x3000000000、0x2aaaaaaa0000 附近
  • 因此,记住调试对象环境中,程序和共享库被定位的位置,在阅读backtrace会很方便

3.1 运行地址的改变
  • 1.直接指定地址并调用
    • 栈较难被破坏,地址一般保存在只读空间
  • 2.指定内存区域保存了跳转地址(GOT/PLT)
  • 3.执行ret命令(函数返回)
    • 栈较容易被破坏,地址位于GOT或栈等可写空间
3.2 确定破坏跳转地址值的位置(栈破坏)
  • 将错误的地址当数据
    • 需要怀疑是否是字符串的一部分,原文中查看栈前后空间的内存,可以看到很多字符串(我在本地环境未能复现),原文截图如下:
    • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7uPnxio8-1624958900034)(./picture/arrOver2.png)]
    • 原文中结合代码得出了"ble "对应0x20656c62当作返回地址使用导致段错误的结论,我在本地试过多种方法打印相关栈附近的内容,都没有发现很多字符的情况,不知道作者怎么搞的,或许用了特殊的编译参数,或许试书出版以后编译器做了很多的优化,唉,谁知道呢。
    • 原文还给了一个栈空间的示意图(我怀疑试作者推测的,到今天可能不再适用了)
      • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a7eXpRVz-1624958900034)(./picture/arrOver3.png)]
3.3 确定破坏跳转地址值的位置(GOT破坏)
  • 访问空间中静态分配的数组时,也会有类似的现象
  • 原文中给的例子core的backtrace如下图所示:
    • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A6V7lq9W-1624958900035)(./picture/arrOver_GOT.png)]
    • 随后对地址0x080483ca 进行了反汇编
  • 书中的反汇编:
    • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uZ8RI9F1-1624958900036)(./picture/arrOver_GOT_disas.png)]
  • 书中怀疑是从0x080483c5地址的call指令执行到返回0x080483ca之间发生了问题,所以调查了地址0x080482b0:
    • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6cIaKQNQ-1624958900037)(./picture/arrOver_GOT_trace.png)]
  • 经过一通分析,我们知道了哪里导致了段错误,现在的问题是哪里向地址0x080495a8写入了0x8呢,在探索这个问题前,你是否有疑问,为什么会跳转到这个地址的值表示的地址,如果你不是对这个疑问不屑一顾,说明你跟我一样还不太了解什么GOT sectionPLT section, 详细解释可自行google点这里, 我做下简要解释吧:
    • GOT 保存了函数要调用的函数地址,运行一开始其表项为空,会在运行时实时更新表项,一个符号调用在第一次时会解析出绝对地址更新到GOT中,第二次直接找到GOT 表项所存储的函数地址直接调用了
      • GOT 全称 Global Offset Table, 即全局偏移表,位于.got.plt section,用于记录在ELF文件中所用到的共享库中符号的绝对地址
    • PLT:全称 Procedure Linkage Table, 即过程链接表,位于.plt section,作用是将位置无关的符号转移到绝对地址,当一个外部符号调用时,PLT 去引用 GOT 中的其符号对应的绝对地址,然后转入并执行
    • linux中ELF的详细参看文档 ELF_Format
  • 不用我说你也应该也猜到了,地址0x080495a8位于GOT区域,很可能就是将0x8当作错误的地址写入了GOT区域,弄错地址的情况是多种多样的,很难一概而论,可以假设应当写入的地址就位于附近,像下面这样调查该地址附近的整体结构
    • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bC6s0fWO-1624958900037)(./picture/arrOver_objdump.png)]
  • 书的作者猜测0x8本来是要写入数据段的(已初始化的全局/静态存储区), 然后查看代码后一眼就找到了bug, 这在现实中当然不太可能,所以下一章介绍了另一种方法

  • 按照原文的例子(本地测试代码),我尝试着本地复现一下:
    #include <stdio.h>
    #include <string.h>
    #include <cstdlib>
    int my_data[2] = {1,2};
    int calc_index(void){return -7;/* 故意返回一个错误值 */}
    int main(){
       int idx = calc_index();
       my_data[idx] = 0x0a;
       my_data[idx+1] = 0x08;
       printf("this is a message\n");
       return EXIT_SUCCESS;
    }
    
  • core:
    [New LWP 4318]
    Core was generated by `./errArray'.
    Program terminated with signal SIGSEGV, Segmentation fault.
    (gdb) bt
    #0  0x0000000a in ?? ()
    #1  0x00010764 in main () at errArray.cpp:15
    (gdb) p main
    $2 = {int (void)} 0x10724 <main()>
    (gdb) p calc_index
    $3 = {int (void)} 0x10708 <calc_index()>
    (gdb)
    
  • 本地反汇编 0x00010764 附近代码:disas 0x00010764

    -->后是我的注释,没有注释说明我看不懂

    (gdb) disas 0x00010764
    Dump of assembler code for function main():
       0x00010724 <+0>:     push    {r11, lr}
       0x00010728 <+4>:     add     r11, sp, #4
       0x0001072c <+8>:     sub     sp, sp, #8
       0x00010730 <+12>:    bl      0x10708 <calc_index()>
       0x00010734 <+16>:    str     r0, [r11, #-8]
       0x00010738 <+20>:    ldr     r2, [pc, #52]   ; 0x10774 <main()+80>
       0x0001073c <+24>:    ldr     r3, [r11, #-8]
       0x00010740 <+28>:    mov     r1, #10
       0x00010744 <+32>:    str     r1, [r2, r3, lsl #2]
       0x00010748 <+36>:    ldr     r3, [r11, #-8]
       0x0001074c <+40>:    add     r3, r3, #1
       0x00010750 <+44>:    ldr     r2, [pc, #28]   ; 0x10774 <main()+80>
       0x00010754 <+48>:    mov     r1, #8
       0x00010758 <+52>:    str     r1, [r2, r3, lsl #2]
       0x0001075c <+56>:    ldr     r0, [pc, #20]   ; 0x10778 <main()+84>
       0x00010760 <+60>:    bl      0x105e8 <puts@plt>
       0x00010764 <+64>:    mov     r3, #0
       0x00010768 <+68>:    mov     r0, r3
       0x0001076c <+72>:    sub     sp, r11, #4
       0x00010770 <+76>:    pop     {r11, pc}
       0x00010774 <+80>:    andeq   r1, r2, r8, lsr #32
       0x00010778 <+84>:    andeq   r0, r1, r12, ror #15
    End of assembler dump.
    
  • 发现和书中有点不太一样,由于本人离作者水平相差太远,不能一眼看出哪条汇编指令导致了段错误,所以决定单步执行看看
(gdb) b main
Note: breakpoint 1 also set at pc 0x10730.
Breakpoint 2 at 0x10730: file errArray.cpp, line 12.
(gdb) r
Starting program: /home/pi/debug/errArray

Breakpoint 1, main () at errArray.cpp:12
12          int idx = calc_index();
(gdb) display/i $pc
1: x/i $pc
=> 0x10730 <main()+12>: bl      0x10708 <calc_index()>
(gdb) disas $pc
Dump of assembler code for function main():
   0x00010724 <+0>:     push    {r11, lr}
   0x00010728 <+4>:     add     r11, sp, #4
   0x0001072c <+8>:     sub     sp, sp, #8
=> 0x00010730 <+12>:    bl      0x10708 <calc_index()>
   0x00010734 <+16>:    str     r0, [r11, #-8]
   0x00010738 <+20>:    ldr     r2, [pc, #52]   ; 0x10774 <main()+80>
   0x0001073c <+24>:    ldr     r3, [r11, #-8]
   0x00010740 <+28>:    mov     r1, #10
   0x00010744 <+32>:    str     r1, [r2, r3, lsl #2]
   0x00010748 <+36>:    ldr     r3, [r11, #-8]
   0x0001074c <+40>:    add     r3, r3, #1
   0x00010750 <+44>:    ldr     r2, [pc, #28]   ; 0x10774 <main()+80>
   0x00010754 <+48>:    mov     r1, #8
   0x00010758 <+52>:    str     r1, [r2, r3, lsl #2]
   0x0001075c <+56>:    ldr     r0, [pc, #20]   ; 0x10778 <main()+84>
   0x00010760 <+60>:    bl      0x105e8 <puts@plt>
   0x00010764 <+64>:    mov     r3, #0
   0x00010768 <+68>:    mov     r0, r3
   0x0001076c <+72>:    sub     sp, r11, #4
   0x00010770 <+76>:    pop     {r11, pc}
   0x00010774 <+80>:    andeq   r1, r2, r8, lsr #32
   0x00010778 <+84>:    andeq   r0, r1, r12, ror #15
End of assembler dump.
(gdb) ni
0x00010734      12          int idx = calc_index();
1: x/i $pc
=> 0x10734 <main()+16>: str     r0, [r11, #-8]
......-->省略n步
(gdb) ni
15          printf("this is a message\n");
1: x/i $pc
=> 0x1075c <main()+56>: ldr     r0, [pc, #20]   ; 0x10778 <main()+84>
(gdb) ni
0x00010760      15          printf("this is a message\n");
1: x/i $pc
=> 0x10760 <main()+60>: bl      0x105e8 <puts@plt>
(gdb) ni

Program received signal SIGSEGV, Segmentation fault.
0x0000000a in ?? ()
1: x/i $pc
=> 0xa: <error: Cannot access memory at address 0xa>
(gdb)
  • 发现是在执行汇编指令0x10760 <main()+60>: bl 0x105e8 <puts@plt>时出错了,也就是调用打印指令时出错了,看下这个地址执行了哪些命令:
(gdb) disas 0x105e8
Dump of assembler code for function puts@plt:
   0x000105e8 <+0>:     add     r12, pc, #0, 12
   0x000105ec <+4>:     add     r12, r12, #16, 20       ; 0x10000
   0x000105f0 <+8>:     ldr     pc, [r12, #2588]!       ; 0xa1c
End of assembler dump.
  • 打印 [r12, #2588]的值:
(gdb) x $r12
0x205f0:        0xe5bcfa1c
(gdb) x $r12+2588
0x2100c <puts@got.plt>: 0x0000000a
  • 就是说在访问地址0x2100c时出错了,同理我们看下0x2100c是在二进制文件的哪个部分:
pi@raspberrypi:~/debug $ objdump  -s errArray
errArray:     文件格式 elf32-littlearm
...
Contents of section .dynamic:
 20f00 01000000 01000000 01000000 55000000  ............U...
 20f10 01000000 5f000000 01000000 84000000  ...._...........
 20f20 0c000000 c8050100 0d000000 e0070100  ................
 20f30 19000000 f80e0200 1b000000 04000000  ................
 20f40 1a000000 fc0e0200 1c000000 04000000  ................
 20f50 f5feff6f b4010100 05000000 e4030100  ...o............
 20f60 06000000 64020100 0a000000 4b010000  ....d.......K...
 20f70 0b000000 10000000 15000000 00000000  ................
 20f80 03000000 00100200 02000000 20000000  ............ ...
 20f90 14000000 11000000 17000000 a8050100  ................
 20fa0 11000000 a0050100 12000000 08000000  ................
 20fb0 13000000 08000000 feffff6f 60050100  ...........o`...
 20fc0 ffffff6f 02000000 f0ffff6f 30050100  ...o.......o0...
 20fd0 00000000 00000000 00000000 00000000  ................
 20fe0 00000000 00000000 00000000 00000000  ................
 20ff0 00000000 00000000 00000000 00000000  ................
Contents of section .got:
 21000 000f0200 00000000 00000000 d4050100  ................
 21010 d4050100 d4050100 d4050100 00000000  ................
Contents of section .data:
 21020 00000000 00000000 01000000 02000000  ................
Contents of section .comment:
 0000 4743433a 20285261 73706269 616e2038  GCC: (Raspbian 8
 0010 2e332e30 2d362b72 70693129 20382e33  .3.0-6+rpi1) 8.3
 0020 2e3000  
...
  • 与原文一致,0x2100c也是在.got Contents,且位于数据段附近,因此我们得出了跟原文一样得猜测,数据段越界访问导致.got被意外修改,导致执行时跳转错误的地址而导致段错误

4. 利用监视点检测非法内存访问

对于 上一节介绍的情况,如果调试对象程序就在手头,且能立即复现bug,就能利用监测点高效确定bug所在

  • 上一小节中,我们已经知道了导致错误的根本原因是向地址0x2100c写入了0xa,所以我们可以直接监测地址0x2100c,看看什么时候,哪条指令改变了该值
Breakpoint 1, main () at errArray.cpp:12
12          int idx = calc_index();    --->我打的断点
1: x/i $pc
=> 0x10730 <main()+12>: bl      0x10708 <calc_index()>
(gdb) c
Continuing.

Hardware watchpoint 4: *0x2100c

Old value = 67028                      --->原来的值是 67028
New value = 10                         --->现在的值是 10,找到错误的写入了
main () at errArray.cpp:14
14          my_data[idx+1] = 0x08;     --->监视点是在访问数据后发生的,所以指向的是源码中的下一行
1: x/i $pc
=> 0x10748 <main()+36>: ldr     r3, [r11, #-8]
(gdb) c                                -->为了验证,我继续执行,如预期发生了段错误
Continuing.
Program received signal SIGSEGV, Segmentation fault.
  • 原因似乎已经知道是源码中13行引入的,看下代码:
(gdb) list *0x10748
0x10748 is in main() (errArray.cpp:14).
9       }
10      int main()
11      {
12          int idx = calc_index();
13          my_data[idx] = 0x0a;
14          my_data[idx+1] = 0x08;
15          printf("this is a message\n");
16          return EXIT_SUCCESS;
17      }
18
(gdb)
  • 但是作者觉得进一步分析似乎应该阅读汇编代码
(gdb) disas 0x10748
Dump of assembler code for function main():
   0x00010724 <+0>:     push    {r11, lr}
   0x00010728 <+4>:     add     r11, sp, #4
   0x0001072c <+8>:     sub     sp, sp, #8
   0x00010730 <+12>:    bl      0x10708 <calc_index()>
   0x00010734 <+16>:    str     r0, [r11, #-8]
   0x00010738 <+20>:    ldr     r2, [pc, #52]   ; 0x10774 <main()+80>
   0x0001073c <+24>:    ldr     r3, [r11, #-8]
   0x00010740 <+28>:    mov     r1, #10               -->r1寄存器赋值0xa
   0x00010744 <+32>:    str     r1, [r2, r3, lsl #2]  -->r1的值写入后面的地址
   0x00010748 <+36>:    ldr     r3, [r11, #-8]
   ...... -->已知引发bug的位置这这条语句上面,所以后面的代码我就省略了.
End of assembler dump.
(gdb) b *0x10748
Breakpoint 2 at 0x10748: file errArray.cpp, line 14.
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/pi/debug/errArray

Hardware watchpoint 1: *0x2100c

Old value = 67028
New value = 10

Breakpoint 2, main () at errArray.cpp:14
14          my_data[idx+1] = 0x08;
1: x/i $pc
=> 0x10748 <main()+36>: ldr     r3, [r11, #-8]
  • 到这里为止,我们已经从汇编代码确定了bug引入的地址
   0x00010744 <+32>:    str     r1, [r2, r3, lsl #2]  -->r1的值写入后面的地址 r2+r3=0x21028-7*4 = `0x2100c`
   ...
(gdb) p/x $r2
$6 = 0x21028
(gdb) p/d $r3
$4 = -7
(gdb) p/x $r1
$7 = 0xa

5. malloc()、free()发生故障

特别是C语言应用程序的 bug 中,最常见的就是内存相关库函数的错误引发的bug,如内存的双重释放、访问空间之外的内存等

  • 1.双重释放
    • code
    #include <stdlib.h>
    #include <assert.h>
    #include <stdio.h>
    void memBugTest()
    {
       char *p = (char*)malloc(1000);
       assert(p);
       free(p);
       free(p);
    }
    int main()
    {
       memBugTest();
       return 0;
    }
    
    • 运行
    pi@raspberrypi:~/debug $ ./membug
    double free or corruption (top)
    已放弃 (核心已转储)
    
    • 运行时使用环境变量 MALLOC_CHECK_ 检查(跟书中结果有点不一样)
    pi@raspberrypi:~/debug $ env MALLOC_CHECK_=1 ./membug
    free(): invalid pointer
    已放弃 (核心已转储)
    
    • core文件
    pi@raspberrypi:~/debug $ gdb membug core.1610279046-membug-12466
    GNU gdb (Raspbian 8.2.1-2) 8.2.1
    ......()
    [New LWP 12466]
    Core was generated by `./membug'.
    Program terminated with signal SIGABRT, Aborted.
    #0  __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
    50      ../sysdeps/unix/sysv/linux/raise.c: 没有那个文件或目录.
    (gdb) bt
    #0  __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
    #1  0xb6c63230 in __GI_abort () at abort.c:79
    #2  0xb6cb351c in __libc_message (action=action@entry=do_abort, fmt=<optimized out>) at ../sysdeps/posix/libc_fatal.c:181
    #3  0xb6cba044 in malloc_printerr (str=<optimized out>) at malloc.c:5341
    #4  0xb6cbe7f4 in free_check (mem=0x15f3f10, caller=<optimized out>) at hooks.c:254
    #5  0x00010750 in memBugTest () at membug.cpp:11
    #6  0x00010774 in main () at membug.cpp:15
    
  • 上文例举的代码很容易看出问题,直接导致SIGSEGV的情况是运气很好的情况,实际应用中可能出现内存受到破坏但程序而继续运行,产生错误的结果,这是最危险的。
  • 内存双重释放导致的bug与内存泄漏并列,是很难发现正真原因的bug之一

6. 应用程序停止响应(死锁篇)

  • 死锁在实际工作过程中大家肯定碰到过,相信大家应该也有自己的一套调试方法,这里介绍《debug hack》提到的一种
  • 示例代码
#include <stdlib.h>
#include <assert.h>
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int cnt = 0;
void cnt_reset(void)
{
    pthread_mutex_lock(&mutex);
    cnt = 0;
    pthread_mutex_unlock(&mutex);
}
void *thr(void *arg)
{
    while (1)
    {
        pthread_mutex_lock(&mutex);
        if (cnt > 2)
        {
            cnt_reset();
        }
        else
        {
            cnt++;
        }
        pthread_mutex_unlock(&mutex);
        printf("%d\n", cnt);
        sleep(1);
    }
}
int main()
{
    pthread_t tid;
    pthread_create(&tid, NULL, thr, NULL);
    pthread_join(tid, NULL);
    return EXIT_SUCCESS;
}
  • 运行效果:
pi@raspberrypi:~/debug $ make
g++   .//astall.o -o astall  -Wall -Werror -g -rdynamic -pthread -Wall -Werror -g -rdynamic -pthread -lm         -I ./lib/include
pi@raspberrypi:~/debug $ ./astall
1
2
3
<程序已知停在这>
  • 查看程序状态, ps ax -L |grep astall
    • ps 的 a选项:解除bsd风格的“只有你自己”限制,当使用某些bsd风格(不带“-”)选项或ps人格设置类似bsd时,这将强加于所有进程的集合上。以这种方式选择的流程集是其他方式选择的流程集的补充。另一种描述是,这个选项导致ps用终端(tty)列出所有进程,或者与x选项一起使用时列出所有进程。
    • ps 的 x选项:取消bsd风格的“必须有一个tty”限制,当使用某些bsd风格(不带“-”)选项时,或者当ps的个性设置类似bsd时,所有进程集合都将受到这种限制。以这种方式选择的流程集是其他方式选择的流程集的补充。另一种描述是,该选项导致ps列出您拥有的所有进程(与ps相同的EUID),或者在与a选项一起使用时列出所有进程。
    • ps 的 -L选项:显示线程,可能带有LWP和NLWP列。
    • STAT: 该行程的状态:
      • D: 无法中断的休眠状态 (通常 IO 的进程)
      • R: 正在执行中
      • S: 静止、睡眠状态
      • T: 暂停执行
      • l:是多线程的(使用CLONE_THREAD,像NPTL pthreads那样)
      • +:表示前台进程组
      • …(其他请查看man手册)
pi@raspberrypi:~/debug $ ps ax -L |grep astall
15461 15461 pts/1    Sl+    0:00 ./astall
15461 15462 pts/1    Sl+    0:00 ./astall
15488 15488 pts/2    S+     0:00 grep --color=auto astall
  • GDB 调试
    • 调试在运行的进程(显示的是最初启动的线程的bt):gdb -p 'pidof astall'
pi@raspberrypi:~/debug $ gdb -p `pidof astall`
......(省略了GDB的版本信息)
Attaching to process 15461
[New LWP 15462]
#0  0xb6d64a3c in __GI___pthread_timedjoin_ex (threadid=3066094672, thread_return=0x0, abstime=<optimized out>,
    block=<optimized out>) at pthread_join_common.c:89
#1  0x00010978 in main () at astall.cpp:37
  • 查看线程信息:info thread
(gdb) info thread
Id   Target Id                              Frame
* 1    Thread 0xb6fd5010 (LWP 15461) "astall" 0xb6d64a3c in __GI___pthread_timedjoin_ex (threadid=3066094672,
   thread_return=0x0, abstime=<optimized out>, block=<optimized out>) at pthread_join_common.c:89
2    Thread 0xb6c0e450 (LWP 15462) "astall" __lll_lock_wait (futex=futex@entry=0x21040 <mutex>, private=0)
   at lowlevellock.c:46
(gdb)
  • 由于只有两个线程,主线程号是15461,且出于__GI___pthread_timedjoin_ex,即主函数,我们切换到线程2:thread 2
(gdb) thread 2
[Switching to thread 2 (Thread 0xb6c0e450 (LWP 15462))]
#0  __lll_lock_wait (futex=futex@entry=0x21040 <mutex>, private=0) at lowlevellock.c:46
46      lowlevellock.c: 没有那个文件或目录.
(gdb) bt
#0  __lll_lock_wait (futex=futex@entry=0x21040 <mutex>, private=0) at lowlevellock.c:46
#1  0xb6d65f44 in __GI___pthread_mutex_lock (mutex=0x21040 <mutex>) at pthread_mutex_lock.c:80
#2  0x000108ac in cnt_reset () at astall.cpp:11
#3  0x000108fc in thr (arg=0x0) at astall.cpp:22
#4  0xb6d63494 in start_thread (arg=0xb6c0e450) at pthread_create.c:486
#5  0xb6ce6578 in ?? () at ../sysdeps/unix/sysv/linux/arm/clone.S:73 from /lib/arm-linux-gnueabihf/libc.so.6
Backtrace stopped: previous frame identical to this frame (corrupt stack?)
(gdb)
  • 其实到这里可以很容易的看出是卡在程序的第11行,但是实际编程中往往比这复杂得多,因此作者还给了其他一个方法,使用GDB命令文件,记录死锁发生之前得操作,该命令会在调用pthread_mutex_lockpthread_mutex_unlock 时显示backtrace,并将内容记录到debug.log中:
set pagination off
set logging file debug.log
set logging overwrite
set logging on
set $addr1 = pthread_mutex_lock
set $addr2 = pthread_mutex_unlock
b *$addr1
b *$addr2
start
while 1
    c
    bt
    if $pc != $addr1 && $pc != $addr2
        quit
    end
end
  • 运行(gdb astall -x debug.cmd):
  • 查看文件内容:
pi@raspberrypi:~/debug $ cat debug.log |grep -A1 "^#0.*pthread_mutex_"|sed s/from\ .*$// |sed s/.*\ in\ //
pthread_mutex_lock@plt ()
thr (arg=0x0) at astall.cpp:19
--
pthread_mutex_unlock@plt ()
thr (arg=0x0) at astall.cpp:28
--
pthread_mutex_lock@plt ()
thr (arg=0x0) at astall.cpp:19
--
pthread_mutex_unlock@plt ()
thr (arg=0x0) at astall.cpp:28
--
pthread_mutex_lock@plt ()
thr (arg=0x0) at astall.cpp:19
--
pthread_mutex_unlock@plt ()
thr (arg=0x0) at astall.cpp:28
--
pthread_mutex_lock@plt ()
thr (arg=0x0) at astall.cpp:19
--
pthread_mutex_lock@plt ()
cnt_reset () at astall.cpp:11
  • 由过滤后的文件很容易看出时在19行加锁后又在第11行加锁导致
6.1 使用多个mutex时的调试方法
  • 上问中调试单个mutex时,在加锁和解锁时显示backtrace, 多个mutex的环境并不适用, 参考书中HACK#10 函数调用时的参数传递方法(x86_64)HACK#11 函数调用时的参数传递方法(i386篇),可在bt命令前后加上(实测会出错,期待大佬解答问什么)
    • printf "## addr: %08x\n", *(int*)($esp+4)
    • 或者之家尝试i r
      (gdb) p &mutex
      $5 = (pthread_mutex_t *) 0x21040 <mutex>
      (gdb) i r
      r0             0x21040             135232
      r1             0xb6c33450          3066246224
      r2             0xb6c32e60          3066244704
      r3             0x108d0             67792
      r4             0xb6ffe968          3070224744
      r5             0xb6c33450          3066246224
      r6             0xb6ff74d0          3070194896
      r7             0x152               338
      r8             0xbefff49a          3204445338
      r9             0xb6c33450          3066246224
      r10            0x0                 0
      r11            0xb6c32e54          3066244692
      r12            0xb6c32ec8          3066244808
      sp             0xb6c32e48          0xb6c32e48
      lr             0x108e8             67816
      pc             0x10740             0x10740 <pthread_mutex_lock@plt>
      cpsr           0x60000010          1610612752
      fpscr          0x0                 0
      

6. 应用程序停止响应(死循环篇)

  • 考虑到工作量与头发数量,不再编译书中提到的tcpdump源码,总结下大概的排查方法:
    • 1.确定大概范围,在适当地方设置断点
    • 2.使用top、vmstat命令确认CPU使用情况
  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值