gdb调试秘籍(寄存器、栈)
GDB的常用调试命令大家可以查阅gdb手册就可以快速的上手了,在这儿就不给大家分享了,需要的可以到GDB的官网去下载手册。这里重点分享下GDB调试中的一些寄存器和栈的相关知识用于解决下列gdb调试时的问题:
- 优化的代码在printf或其它glibc函数处core
- 没有检查返回值的函数调用异常导致的异常
- 优化的代码的计算异常的中间过程分析
- 栈溢出导致的core
- 局部变量越界导致栈异常的core
寄存器
通常调试的代码基本上都是在未开启优化的情况下,各个变量都可以直接查看,因此造成很多人调试时基本上不会看寄存器,但是对于线上的生产环境,可能会因为性能的因素,需要打开代码优化,此时出现异常需要调试时就通常需要查看寄存器了,下列是gdb调试中需要了解的寄存器。
- $rip 指令寄存器,指向当前执行的代码位置
- $rsp 栈指针寄存器,指向当前栈顶
- $rax,$rbx,$rcx,$rdx,$rsi,$rdi,$rbp,$r8,$r9,$r10,$r11,$r12,$r13,$r14,$r15 通用寄存器
函数入参
一般linux下会优先将参数压到寄存器中,只有当寄存器不够所有的参数时,才会将入参压到栈上,一般入参的压栈顺序为$rdi、$rsi、$rdx、$rcx、$r8、$r9,如下
arg_int入参int arg_int(int a, int b, int c, int d, int e, int f, int g, int h, int i) { return (a + b + c + d + e + f + g + h + i); }
第7、8、9个入参将会被压到栈上,具体的栈上的位置信息大家可以自己研究。Breakpoint 1, arg_int (a=1, b=2, c=3, d=4, e=5, f=6, g=7, h=8, i=9) at ./reg.cpp:7 ...... (gdb) i r rax 0x7ffff7639f60 140737343889248 rbx 0x14 20 rcx 0x4 4 rdx 0x3 3 rsi 0x2 2 rdi 0x1 1 rbp 0xa 0xa rsp 0x7fffffffe4e8 0x7fffffffe4e8 r8 0x5 5 r9 0x6 6 r10 0x7fffffffe3b0 140737488348080 r11 0x7ffff72cbbe0 140737340292064 r12 0x1e 30 r13 0x28 40 r14 0x32 50 r15 0x3c 60 rip 0x4005e4 0x4005e4 <arg_int(int, int, int, int, int, int, int, int, int)>
了解了入参时的寄存器知识,也该学以致用了,最典型的需要使用寄存器查看函数入参的场景是glibc的函数,在此以printf函数作为示例。printf最一类典型的core场景,通常是因为指定的格式和实际的类型不符合造成的,但是如果没有源码的话,即使断点抓住了也不知道具体是哪个参数的格式不对,但是通过查看寄存器就可以调试此类问题了
断点printf时寄存器信息int along[9] = { 10, 20, 30, 40, 50, 60, 70, 80, 90 }; printf("arg_long = %ld\n", arg_long(along[0], along[1], along[2], along[3], along[4], along[5], along[6], along[7], along[8]));
大家可以根据上面的知识来查看寄存器知道具体的输出信息,$rdi是format,先查看format后就可以按顺序来查看后面的参数是否正确了Breakpoint 3, 0x00007ffff72fb990 in printf () from /lib64/libc.so.6 (gdb) i r rax 0x0 0 rbx 0x7fffffffe648 140737488348744 rcx 0x4 4 rdx 0x15 21 rsi 0x1ef 495 rdi 0x400858 4196440 rbp 0x0 0x0 rsp 0x7fffffffe538 0x7fffffffe538 r8 0x5 5 r9 0x6 6 r10 0x7fffffffe2c0 140737488347840 r11 0x7ffff72fb990 140737340488080 r12 0x400500 4195584 r13 0x7fffffffe640 140737488348736 r14 0x0 0 r15 0x0 0 rip 0x7ffff72fb990 0x7ffff72fb990 <printf>
(gdb) p/s (char*)$rdi $1 = 0x400858 "arg_long = %ld\n" (gdb) p/d $rsi $2 = 495
函数返回值
函数返回值由$rax保存返回,单步执行上面的用例,printf的返回值如下:
打印的内容为”arg_long = 495\n”,一共15个字节arg_long = 495 ...... (gdb) p/d $rax $1 = 15
返回值查看的具体使用场景比较广,例如glibc、外部库等各种看不到源码的函数调用的结果都可以通过$rax查看返回值,这儿就不举例了,大家可以自己验证下
函数运行中
对于开启了优化的场景下,局部变量往往会仅在寄存器中存储,如果需要查看被优化了的局部变量的值,如下:
执行N次后的断点信息如下:for (int i = 0 ; i < end ; ++i) { total += i; }
直接打印i、total都显示被优化了,因此无法直接打印其值,通过打印当前寄存器信息得到了寄存器中的值,然后通过查看汇编和源码对比(gdb) 34 total += i; (gdb) p i $3 = <value optimized out> (gdb) p total $4 = <value optimized out>
通过汇编可以知道,i对应为寄存器$rdx,total对应为寄存器$rsi,end对应为寄存器$rax0x0000000000400735 <+120>: jle 0x400740 <main(int, char**)+131> => 0x0000000000400737 <+122>: add %edx,%esi 0x0000000000400739 <+124>: add $0x1,%edx 0x000000000040073c <+127>: cmp %eax,%edx 0x000000000040073e <+129>: jl 0x400737 <main(int, char**)+122>
也就是此时i为2、total为1,total += i;执行完后$rsi应该会变为3,这点大家可以尝试验证下。(gdb) i r rax 0x5 5 rbx 0x7fffffffe648 140737488348744 rcx 0x5 5 rdx 0x2 2 rsi 0x1 1 rdi 0x7fffffffe8a6 140737488349350 rbp 0x0 0x0 rsp 0x7fffffffe540 0x7fffffffe540 r8 0x7ffff7637580 140737343878528 r9 0x7ffff73eb9e0 140737341471200 r10 0x5 5 r11 0x1999999999999999 1844674407370955161 r12 0x400500 4195584 r13 0x7fffffffe640 140737488348736 r14 0x0 0 r15 0x0 0 rip 0x400737 0x400737 <main(int, char**)+122>
栈
在gdb调试栈错误前,你需要了解下列的栈知识
- 函数调用跳转时在新帧的栈首8Bytes存放上一帧的指令地址
- 通常函数的起始操作为push $rbp,将上帧的栈底地址8Bytes压入栈中
- 在保存完指令地址和栈底地址后,会进行一次sub xxx,$rsp,为当前函数内所有在栈上的局部变量都申请好需要的栈空间
- 函数调用前将需要保存的寄存器值和超过6个的参数都压入栈中
栈异常导致的core是线上最常见的core原因之一,常见原因有:
递归调用或大变量消耗栈空间,导致栈溢出
这类问题往往通过gdb查看栈基本信息就可以定位解决,话不多说,直接上实战
Stack frame at xxxx的意义为当前帧的用户栈起始地址(gdb) bt #0 0x00007ffff72fb990 in printf () from /lib64/libc.so.6 #1 0x000000000040069f in f1 (ac1=0x7fffffffe360, ac2=0x7fffffffe230) at ./stack.cpp:9 #2 0x00000000004006f9 in f2 (ac=0x7fffffffe360) at ./stack.cpp:18 #3 0x000000000040074a in main (argc=1, argv=0x7fffffffe658) at ./stack.cpp:27 (gdb) info frame 0 Stack frame at 0x7fffffffe1d0: Locals at 0x7fffffffe1c0, Previous frame's sp is 0x7fffffffe1d0 (gdb) info frame 1 Stack frame at 0x7fffffffe220: (gdb) info frame 2 Stack frame at 0x7fffffffe350: (gdb) info frame 3 Stack frame at 0x7fffffffe580:
frame 0的当前栈使用为:0x7fffffffe1d0 - 0x7fffffffe1c0 = 16 Bytes
frame 1的当前栈使用为:0x7fffffffe220 - 0x7fffffffe1d0 = 80 Bytes
frame 2的当前栈使用为:0x7fffffffe350 - 0x7fffffffe220 = 304 Bytes
frame 3的当前栈使用为:0x7fffffffe580 - 0x7fffffffe350 = 560 Bytes
当前栈使用为:0x7fffffffe580 - 0x7fffffffe1c0
由此类推,在遇到怀疑栈溢出的时候就可以根据整体栈使用和各帧的栈使用来快速定位出具体是哪层栈造成的溢出。
栈上变量的越界访问导致栈内的数据被改写
这类问题往往gdb查看时会得到一个错乱的栈信息
这种情况下通常是栈上前面帧的指令地址被修改导致的,可能刚好被修改的是上一帧的指令地址,也可能修改的是几帧前的指令地址。通常这类问题定位起来非常麻烦,根据$rsp不一定能还原栈信息,定位起来时往往需要大量的猜测和验证。Program received signal SIGSEGV, Segmentation fault. 0x00000000000b0000 in ?? () (gdb) bt #0 0x00000000000b0000 in ?? () #1 0x00000000000c0000 in ?? () #2 0x00000000000d0000 in ?? () #3 0x00000000000e0000 in ?? () #4 0x00000000000f0000 in ?? () #5 0x0000000000000002 in ?? () #6 0x0000000000000003 in ?? () ......
下面的示例是在踩了栈后没有其它的函数调用重新写栈信息### gdb 查找宏 define find set $ptr = $arg0 set $cnt = 0 while ( ($ptr<=$arg1) && ($cnt<$arg3) ) if ( *(unsigned long *)$ptr == $arg2 ) x/gx $ptr set $cnt = $cnt + 1 end set $ptr = $ptr + 8 end end
(gdb) p $rsp $2 = (void *) 0x7fffffffe4d0 (gdb) find 0x7fffffffe000 0x7fffffffe4d0 0x7fffffffe4c0 3 0x7fffffffe370: 0x00007fffffffe4c0 0x7fffffffe450: 0x00007fffffffe4c0 (gdb) x/2gx 0x7fffffffe370 0x7fffffffe370: 0x00007fffffffe4c0 0x00007ffff72fba2a (gdb) x/2gx 0x7fffffffe450 0x7fffffffe450: 0x00007fffffffe4c0 0x000000000040066c
示例中的find为定义的gdb宏,用于在栈上查找指定的地址值,$rsp为0x7fffffffe4d0,则该帧对应的$rpb为$rsp-16=0x7fffffffe4c0,在栈上查找该值后得到2个地址,分别查看按该地址为帧头来测试,找到了怀疑的附近地址/data/lambygao/test/./stack.cpp:20addr2line -e ./stack 0x000000000040066c /data/lambygao/test/./stack.cpp:20
当然如果是必现的core的话,也可以提前抓好栈的信息,然后设置好watch,这点大家可以自己尝试下。
补充:
find 0x7fffffffe000 0x7fffffffe4d0 0x7fffffffe4c0 3
其中查找范围的起始地址,和查找的值这2个值可能不太理解为什么是这2个值,因此补充下面的这张解释图说明下:
因为是在f1内越界访问写的f2内的数组,破坏的是f2帧头的main帧的返回后的执行代码地址和栈底。因此只有在f2函数执行完后出栈后才会core。刚好这个例子是f2调用了f1后只调用了printf,f1帧的栈的面貌没有被其它的调用清理掉,所以尝试找f1帧的栈头信息时刚好可以找到,否则的话找到的会是f2中最后一个调用函数调用入栈写入的$rbp值。
$rsp - 16是f1 frame中保存的f2 frame的栈底地址;
按页对齐0x7fffffffe4d0的页起始值是0x7fffffffe000,先搜索的本页地址也就是从0x7fffffffe000到0x7fffffffe4d0,之所以按页来查找也是因为前面的地址页是否存在,实际中可能需要逐渐搜索多页