这两天遇到一个程序挂的问题,关键是原因还不好找,虽然core了产生了core文件,但是通过gdb调试的时候很遗憾,关键的core线程,堆栈显示为问号,连哪个线程都看不知道,程序已经通过g++ -g编译,其他的线程是可以看到堆栈信息的,看不到应该是堆栈的内存应该是被程序破坏了。
先说解决办法,就是查看寄存器的信息,根据寄存器里面保存的信息来得到更多的信息。
一 函数调用过程
1.1 栈基本介绍
说到函数调用就不得不提到栈,栈是OS分配的一块内存区域,在函数调用过程中,为了让调用能够顺序返回显然要保存函数的返回地址,还有些函数的局部变量,寄存器信息都是通过栈来保存的。函数返回的时候再弹出,函数调用肯定要一层一层调用,也同样要一层一层返回,后调用的先返回,先调用的后返回,所以用栈这种数据结构可以很好的表达。
栈的申请和释放由系统来负责,特别方便,经典的OS中,栈是向下增长的。栈长的就如下图:
说明下:
- 栈包括栈底和栈顶,上面只是画了一个栈帧 即statck frame,在gdb中可以通过frame命令来查看。
- 栈一般是向下增长的,增长的时候esp指向的地址是减少的,弹出的时候刚好相反,esp减少是弹出。
- 图上有些地方标的由问题,栈里面的值其实是栈的地址,为了明确显示,栈顶的地址是小地址,栈底的地址是大地址。
- ebp和esp之间的栈帧即是一次调用的。
特别注意64位系统ebp已经改成了RBP,esp改成了RSP。
以上就是栈的基本情况,那么栈里面的内容是什么,一般是如何分布的那?
1.2 栈内容
栈的内容主要包括局部变量,寄存器,参数和返回地址,具体内容排列大致如下:
这样看比较虚,看点世界代码:
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 void g() {
5 int *p = 0;
6 long a = 0x1234;
7 printf("%p %x\n", &a, a);
8 printf("%p %x\n", &p, p);
9 *p = 1;
10 }
11
12 void b(int argc, char **argv) {
13 printf("%p %p\n", &argc, &argv);
14 g();
15 }
16
17 int main(int argc, char **argv) {
18 b(argc, argv);
19 return 0;
20 }
21
编译调试如下:
[root@localhost testcode]# g++ -g testsp.c -o test
(gdb) b g
Breakpoint 1 at 0x4005f5: file testsp.c, line 5.
(gdb) r
Starting program: /home/miaohq/testcode/test
0x7fffffffe00c 0x7fffffffe000
Breakpoint 1, g () at testsp.c:5
5 int *p = 0;
Missing separate debuginfos, use: debuginfo-install glibc-2.17-196.el7_4.2.x86_64 libgcc-4.8.5-39.el7.x86_64 libstdc++-4.8.5-39.el7.x86_64
(gdb)
函数调用是:
我们来看看堆栈信息:
(gdb) p $rsp
$14 = (void *) 0x7fffffffdfe0
(gdb) p $rbp
$15 = (void *) 0x7fffffffdff0
(gdb) p &p
$16 = (int **) 0x7fffffffdfe8
(gdb) x/8ag $rsp
0x7fffffffdfe0: 0x0 0x7ffff7ffe150
0x7fffffffdff0: 0x7fffffffe010 0x400673 <b(int, char**)+46>
0x7fffffffe000: 0x7fffffffe118 0x100000000
0x7fffffffe010: 0x7fffffffe030 0x400695 <main(int, char**)+32>
(gdb) p &a
$21 = (long *) 0x7fffffffdfe0
(gdb) p &p
$22 = (int **) 0x7fffffffdfe8
(gdb) x/1ag 0x7fffffffdfe8
0x7fffffffdfe8: 0x7ffff7ffe150
(gdb) x/1ag 0x7fffffffdff0
0x7fffffffdff0: 0x7fffffffe010
(gdb) x /1ag 0x7fffffffe000
0x7fffffffe000: 0x7fffffffe118
(gdb) x /1ag 0x7fffffffe010
0x7fffffffe010: 0x7fffffffe030
(gdb) x/1ag 0x7fffffffdff8
0x7fffffffdff8: 0x400673 <b(int, char**)+46>
(gdb) x /1ag 0x7fffffffe018
0x7fffffffe018: 0x400695 <main(int, char**)+32>
x/8ag 表示按十六进制格式显示变量方式,显示8个8个字节的内存内容。
gdb 的x命令,很少用,却很重要,格式:x /<n/f/u>
n是一个正整数,需要显示的内存单元个数,一个内存单元大小由u来决定,f标识显示格式。
u包括:b:1 byte h:2 bytes w:4 bytes g:8 bytes
如:x/3uh 0x54320 表示从0x54320地址开始,以h双字节位内存单位,u是16进制的无符号整数显示。
二 函数调用过程
根据上面的打印情况,我们画出栈帧,如下图,我们看到g函数的栈帧为16个字节,包含两个局部变量,我们可以反汇编看下g函数部分内容:
(gdb) disassemble g
Dump of assembler code for function g():
0x00000000004005ed <+0>: push %rbp
0x00000000004005ee <+1>: mov %rsp,%rbp
0x00000000004005f1 <+4>: sub $0x10,%rsp
=> 0x00000000004005f5 <+8>: movq $0x0,-0x8(%rbp)
0x00000000004005fd <+16>: movq $0x1234,-0x10(%rbp)
0x0000000000400605 <+24>: mov -0x10(%rbp),%rdx
0x0000000000400609 <+28>: lea -0x10(%rbp),%rax
调用开始:
- 把
push %rbp
即把老的rbp压入到栈中,这个可以在返回的时候恢复。 sub $0x10,%rsp
rsp 下移即分配栈空间为16个字节和我们预期相符和。- 分配局部变量的空间,且赋值。
再看看g函数结尾:
0x000000000040062f <+66>: mov $0x0,%eax
0x0000000000400634 <+71>: callq 0x4004d0 <printf@plt>
0x0000000000400639 <+76>: mov -0x8(%rbp),%rax
0x000000000040063d <+80>: movl $0x1,(%rax)
0x0000000000400643 <+86>: leaveq
0x0000000000400644 <+87>: retq
其中:leaveq
相当于:
movq %rbp, %rsp
popq %rbp
所做的动作:
- 将%rbp的值赋值给%rsp即弹出这个函数的调用栈帧。
- 弹出栈顶现在位置的值给%rbp 在g函数中这个地址为:b:rbp位置即地址:0x7fffffffe010
三 怎么破解问号
其实通过上面过程我们了解到函数调用就是通过这种压栈和出栈调用的,其实还是蛮简单的。
对于我们调试来说,如果因为程序的错误破坏了栈,但是有个地方数据不会破坏那就是寄存器的值,所以我们可以通过 打印rbp和rsp的值得到了最后调用栈的栈底和栈顶,但是这个这两个地址间的数据可能被破坏了,怎么办那,那就是以此地址为基准,继续对上找,就会找到其他函数的栈信息。
使用到的命令如下:
(gdb) p $rbp
$40 = (void *) 0x7fffffffdff0
(gdb) p $rsp
$41 = (void *) 0x7fffffffdfe0
(gdb) x /8ag $rbp
0x7fffffffdff0: 0x7fffffffe010 0x400673 <b(int, char**)+46>
0x7fffffffe000: 0x7fffffffe118 0x100000000
0x7fffffffe010: 0x7fffffffe030 0x400695 <main(int, char**)+32>
0x7fffffffe020: 0x7fffffffe118 0x100000000
(gdb)
这样我们知道了,前面的调用栈原来是通过main
函数调用到b
函数,接着再调用到我们core的函数,从而定位core的大概位置。
四 诗词鉴赏
水调歌头·昵昵儿女语
[宋] [苏轼]
欧阳文忠公尝问余:“琴诗何者最善?答以退之听颖师琴诗最善。公曰:此诗最奇丽,然非听琴,乃听琵琶也。余深然之。建安章质夫家善琵琶者,乞为歌词。余久不作,特取退之词,稍加隐括,使就声律,以遗之云。
昵昵儿女语,灯火夜微明。
恩怨尔汝来去,弹指泪和声。
忽变轩昂勇士,一鼓填然作气,千里不留行。
回首暮云远,飞絮搅青冥。
众禽里,真彩凤,独不鸣。
跻攀寸步千险,一落百寻轻。
烦子指间风雨,置我肠中冰炭,起坐不能平。
推手从归去,无泪与君倾。
花了3个多小时写的文章,希望可以给大家带来点启发。这两天遇到一个程序挂的问题,关键是原因还不好找,虽然core了产生了core文件,但是通过gdb调试的时候很遗憾,关键的core线程,堆栈显示为问号,连哪个线程都看不知道,程序已经通过g++ -g编译,其他的线程是可以看到堆栈信息的,看不到应该是堆栈的内存应该是被程序破坏了。
先说解决办法,就是查看寄存器的信息,根据寄存器里面保存的信息来得到更多的信息。
一 函数调用过程
1.1 栈基本介绍
说到函数调用就不得不提到栈,栈是OS分配的一块内存区域,在函数调用过程中,为了让调用能够顺序返回显然要保存函数的返回地址,还有些函数的局部变量,寄存器信息都是通过栈来保存的。函数返回的时候再弹出,函数调用肯定要一层一层调用,也同样要一层一层返回,后调用的先返回,先调用的后返回,所以用栈这种数据结构可以很好的表达。
栈的申请和释放由系统来负责,特别方便,经典的OS中,栈是向下增长的。栈长的就如下图:
说明下:
- 栈包括栈底和栈顶,上面只是画了一个栈帧 即statck frame,在gdb中可以通过frame命令来查看。
- 栈一般是向下增长的,增长的时候esp指向的地址是减少的,弹出的时候刚好相反,esp减少是弹出。
- 图上有些地方标的由问题,栈里面的值其实是栈的地址,为了明确显示,栈顶的地址是小地址,栈底的地址是大地址。
- ebp和esp之间的栈帧即是一次调用的。
特别注意64位系统ebp已经改成了RBP,esp改成了RSP。
以上就是栈的基本情况,那么栈里面的内容是什么,一般是如何分布的那?
1.2 栈内容
栈的内容主要包括局部变量,寄存器,参数和返回地址,具体内容排列大致如下:
这样看比较虚,看点世界代码:
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 void g() {
5 int *p = 0;
6 long a = 0x1234;
7 printf("%p %x\n", &a, a);
8 printf("%p %x\n", &p, p);
9 *p = 1;
10 }
11
12 void b(int argc, char **argv) {
13 printf("%p %p\n", &argc, &argv);
14 g();
15 }
16
17 int main(int argc, char **argv) {
18 b(argc, argv);
19 return 0;
20 }
21
编译调试如下:
[root@localhost testcode]# g++ -g testsp.c -o test
(gdb) b g
Breakpoint 1 at 0x4005f5: file testsp.c, line 5.
(gdb) r
Starting program: /home/miaohq/testcode/test
0x7fffffffe00c 0x7fffffffe000
Breakpoint 1, g () at testsp.c:5
5 int *p = 0;
Missing separate debuginfos, use: debuginfo-install glibc-2.17-196.el7_4.2.x86_64 libgcc-4.8.5-39.el7.x86_64 libstdc++-4.8.5-39.el7.x86_64
(gdb)
函数调用是:
我们来看看堆栈信息:
(gdb) p $rsp
$14 = (void *) 0x7fffffffdfe0
(gdb) p $rbp
$15 = (void *) 0x7fffffffdff0
(gdb) p &p
$16 = (int **) 0x7fffffffdfe8
(gdb) x/8ag $rsp
0x7fffffffdfe0: 0x0 0x7ffff7ffe150
0x7fffffffdff0: 0x7fffffffe010 0x400673 <b(int, char**)+46>
0x7fffffffe000: 0x7fffffffe118 0x100000000
0x7fffffffe010: 0x7fffffffe030 0x400695 <main(int, char**)+32>
(gdb) p &a
$21 = (long *) 0x7fffffffdfe0
(gdb) p &p
$22 = (int **) 0x7fffffffdfe8
(gdb) x/1ag 0x7fffffffdfe8
0x7fffffffdfe8: 0x7ffff7ffe150
(gdb) x/1ag 0x7fffffffdff0
0x7fffffffdff0: 0x7fffffffe010
(gdb) x /1ag 0x7fffffffe000
0x7fffffffe000: 0x7fffffffe118
(gdb) x /1ag 0x7fffffffe010
0x7fffffffe010: 0x7fffffffe030
(gdb) x/1ag 0x7fffffffdff8
0x7fffffffdff8: 0x400673 <b(int, char**)+46>
(gdb) x /1ag 0x7fffffffe018
0x7fffffffe018: 0x400695 <main(int, char**)+32>
x/8ag 表示按十六进制格式显示变量方式,显示8个8个字节的内存内容。
gdb 的x命令,很少用,却很重要,格式:x /<n/f/u>
n是一个正整数,需要显示的内存单元个数,一个内存单元大小由u来决定,f标识显示格式。
u包括:b:1 byte h:2 bytes w:4 bytes g:8 bytes
如:x/3uh 0x54320 表示从0x54320地址开始,以h双字节位内存单位,u是16进制的无符号整数显示。
二 函数调用过程
根据上面的打印情况,我们画出栈帧,如下图,我们看到g函数的栈帧为16个字节,包含两个局部变量,我们可以反汇编看下g函数部分内容:
(gdb) disassemble g
Dump of assembler code for function g():
0x00000000004005ed <+0>: push %rbp
0x00000000004005ee <+1>: mov %rsp,%rbp
0x00000000004005f1 <+4>: sub $0x10,%rsp
=> 0x00000000004005f5 <+8>: movq $0x0,-0x8(%rbp)
0x00000000004005fd <+16>: movq $0x1234,-0x10(%rbp)
0x0000000000400605 <+24>: mov -0x10(%rbp),%rdx
0x0000000000400609 <+28>: lea -0x10(%rbp),%rax
调用开始:
- 把
push %rbp
即把老的rbp压入到栈中,这个可以在返回的时候恢复。 sub $0x10,%rsp
rsp 下移即分配栈空间为16个字节和我们预期相符和。- 分配局部变量的空间,且赋值。
再看看g函数结尾:
0x000000000040062f <+66>: mov $0x0,%eax
0x0000000000400634 <+71>: callq 0x4004d0 <printf@plt>
0x0000000000400639 <+76>: mov -0x8(%rbp),%rax
0x000000000040063d <+80>: movl $0x1,(%rax)
0x0000000000400643 <+86>: leaveq
0x0000000000400644 <+87>: retq
其中:leaveq
相当于:
movq %rbp, %rsp
popq %rbp
所做的动作:
- 将%rbp的值赋值给%rsp即弹出这个函数的调用栈帧。
- 弹出栈顶现在位置的值给%rbp 在g函数中这个地址为:b:rbp位置即地址:0x7fffffffe010
三 怎么破解问号
其实通过上面过程我们了解到函数调用就是通过这种压栈和出栈调用的,其实还是蛮简单的。
对于我们调试来说,如果因为程序的错误破坏了栈,但是有个地方数据不会破坏那就是寄存器的值,所以我们可以通过 打印rbp和rsp的值得到了最后调用栈的栈底和栈顶,但是这个这两个地址间的数据可能被破坏了,怎么办那,那就是以此地址为基准,继续对上找,就会找到其他函数的栈信息。
使用到的命令如下:
(gdb) p $rbp
$40 = (void *) 0x7fffffffdff0
(gdb) p $rsp
$41 = (void *) 0x7fffffffdfe0
(gdb) x /8ag $rbp
0x7fffffffdff0: 0x7fffffffe010 0x400673 <b(int, char**)+46>
0x7fffffffe000: 0x7fffffffe118 0x100000000
0x7fffffffe010: 0x7fffffffe030 0x400695 <main(int, char**)+32>
0x7fffffffe020: 0x7fffffffe118 0x100000000
(gdb)
这样我们知道了,前面的调用栈原来是通过main
函数调用到b
函数,接着再调用到我们core的函数,从而定位core的大概位置。
四 诗词鉴赏
水调歌头·昵昵儿女语
[宋] [苏轼]
欧阳文忠公尝问余:“琴诗何者最善?答以退之听颖师琴诗最善。公曰:此诗最奇丽,然非听琴,乃听琵琶也。余深然之。建安章质夫家善琵琶者,乞为歌词。余久不作,特取退之词,稍加隐括,使就声律,以遗之云。
昵昵儿女语,灯火夜微明。
恩怨尔汝来去,弹指泪和声。
忽变轩昂勇士,一鼓填然作气,千里不留行。
回首暮云远,飞絮搅青冥。
众禽里,真彩凤,独不鸣。
跻攀寸步千险,一落百寻轻。
烦子指间风雨,置我肠中冰炭,起坐不能平。
推手从归去,无泪与君倾。
花了3个多小时写的文章,希望可以给大家带来点启发。