目录
杂言
对它简单处理,它就简单;
对它复杂处理,它就复杂;
复杂难以把控,无法预测;
可预测的程度,关系成败;
综述
总结个人以下几种方式学习和常用的调试和跟踪要点。以下有可以很好的对程序做hotfix动作的机制,可以有效降低程序的故障等。也可以提高我们学习和理解代码的效率
- 打印日志
- gdb
- systemtap
- strace
1.gdb
1.1 说明
gdb是我们理解和调试程序的重要工具,也是我们解决线上服务出现bug的重要手段
官方使用文档是:https://sourceware.org/gdb/current/onlinedocs/gdb.pdf
文档里面有很多详细的说明,可以有时间的时候慢慢学习下。
官方说明支持的语言有:
C, C++, D, Go, Objective-C, Fortran, OpenCL C, Pascal, Rust, assembly, Modula-2, and Ada
这里不会罗列所有的用法,总结了下最近使用比较重要的几个点, 我个人实际中比较常用的
- 启动gdb
- 条件和数据断点
- frame层查看
- 内存数据查看
- struct成员查看
- 内存数据分析
- 代码地址转换
- 无中断执行
- 汇编分析
1.2 启动gdb
- gdb program #直接启动程序
- gdb program 1234 #绑定到运行的进程
- gdb -p 1234 #绑定到运行的进程
- gdb attach 1234 #绑定到运行的进程 一般调试线上程序的时候基本用这个方式
例如: gdb hello_test
1.3 条件和数据断点
断点一般分为:一般断点,条件断点,数据断点,捕获断点。 结合下面 程序1 代码来说明
条件断点:
break 8 if i == 50 #通过行号来设置,在第8行设置断点
如果设置成这个 break test_func if i == 50 会不成功,在函数那个开始地址上没有 i 变量的上下文
条件断点对于多次循环和具体的分支调试非常有用
数据断点:
如果直接设置 watch watch_b_test, 很有可能没有上下文而失败
一般设置数据断点通过地址方式或者明确有上下文的时候设置,地址方式如下:
先通过一般函数或者行断点找到变量的地址,之后设置断点:
(gdb) p &watch_b_test
$2 = (int *) 0x7fffffffe2bc
(gdb) watch *(int *) 0x7fffffffe2bc
Hardware watchpoint 2: *(int *) 0x7fffffffe2bc #设置成功
watch_b_test = 1 在这条语句的时候会断下。
数据断点在内存访问异常,非预期的行为调试非常有用。对于全局变量等数据断点一般是可以比较方便的直接使用
#include <stdio.h>
void test_func()
{
int i = 0;
for (; i < 100; i++) {
if (i == 50) { //第8行 测试条件断点
printf("test\n");
}
}
}
void main()
{
int watch_b_test = 0;
char str_test[] = "str_test";
char str_test1[] = "str_test1";
char *p;
test_func();
/*测试数据断点*/
watch_b_test = 1;
/*测试内存访问*/
p = str_test1;
printf("prev test %s\n", str_test);
p[18] = 'o';
printf("after test %s\n", str_test);
}
程序1
1.4 frame层查看
打印出来调用栈后,有很多层的函数,每一层都有上下文,独自的上下文只有在当前层才能看到。如下图举例:在frame 0 层没法看到 watch_b_test 这个变量,需要切换到frame 1
1.5 内存数据查看
查看内存里面的数据,可以16进制查看
命令格式 : x /nfu addr
n :要显示的数量;
f :显示的格式 比如16进制;(‘x’, ‘d’, ‘u’, ‘o’, ‘t’, ‘a’, ‘c’, ‘f’, ‘s’), ‘i’ (for machine instructions)
u:显示的宽度;b 一个字节,h 两个字节, w 四个字节, g 八个字节
例子:
1.6 struct成员查看
两种情况 一种是查看标准结构, 一种是强制转换
代码段
typedef struct _s_test {
int s_t_a;
int s_t_b;
} s_test;s_test st;
char *ps;st.s_t_a = 1;
st.s_t_b = 2;
ps = (char *)&st;第一种 :直接查看
gdb) p st.s_t_a
第二种:地址转换
gdb)p ((s_test *)ps)->s_t_a
1.7 内存数据分析
有时候在程序异常,特别是内存问题的时候需要分析内存的情况,比如是否越界,是否有非法内存使用等,个人经常会先分析附近内存的是否有使用不当导致出现非法使用。
参考前面的 程序1, 在str_test1操作的时候,影响了str_test
上面输出结果:
prev test str_test
after test sto_test
str_test 被非法改成了 sto_test ,如何去发现呢,有一个方法是用 x 命令来查看下内存的使用情况。分析:
char str_test[] = "str_test"
char str_test1[] = "str_test1"
在内存中地址如下 ,先定义的在前面地址,并且是占位了16个地址:
(gdb) p &str_test[0]
$1 = 0x7fffffffe290 "str_test"
(gdb) p &str_test1[0]
$2 = 0x7fffffffe280 "str_test1"用 x 命令查看str_test附近的内存, 观察str_test附近有 str_test1,可以猜测是否有关联是操作str_test1造成了str_test被非法修改 :
(gdb) x /32sb 0x7fffffffe280
0x7fffffffe280: "str_test1"
0x7fffffffe28a: "@"
0x7fffffffe28c: ""
0x7fffffffe28d: ""
0x7fffffffe28e: ""
0x7fffffffe28f: ""
0x7fffffffe290: "str_test"
0x7fffffffe299: ""
0x7fffffffe29a: ""
1.8 代码地址转换
在代码运行的时候出现bug或者段出错的时候,linux会打印出来一个代码地址,通过这个代码地址可以查看对应的源代码,通过 addr2line 来实现。这个对于比较清晰的bug很容易定位出来
如这个代码, 会引起执行错误
*(int *)0 = 1;
dmesg的时候会打印
test[7588]: segfault at 0 ip 00000000004005e4 sp 00007ffc22c3ab20 error 6 in test[400000+1000]
通过addr2line来算出源文件的代码段
执行命令:addr2line -e test 0x4005e4
---> ./test/test.c:28直接算出来出问题的程序是在test.c:28行
1.9 无中断执行
一般在线上服务的程序出问题的时候,很多时候不能直接gdb,这样会影响服务,特别是对于大型的服务器上面有成千上万的tcp连接,如果中断程序影响会比较大。在这种情况下为了收集一些信息和定位问题,不中断程序变的很重要了。gdb提供了脚本来实现这个功能
define commandname #自定义名字
print $arg0 + $arg1 + $arg2 #参数传递
statement #自定义语句
......
end
可以在gdb里面直接运行上面脚本,也可以在gdb里面通过source命令来执行一个脚本文件例子 定义一个脚本文件 test.gdb ,收集调用栈立即退出gdb,从而达到减少影响服务的目的:
define myprint_backtrace
run #启动程序
break test_func #定义断点
bt #断点后打印调用栈
c #断点后继续运行
quit #调用栈打印完后退出gdb
end
脚本运行:
(gdb)source test.gdb
有空的时候再研究下是否可以整个过程都是可以脚本化
脚本里面还可以进一步编程用if,while等语句匹配自己想要的信息define matcharg0
if $arg0 == 0
printf "arg0 is 0"
end
end
1.10 汇编分析
通过编译工具 比如objdump等来生成汇编和源文件的关系,从而分析一些问题。汇编这个比较复杂,后面好好学习下专门写个专题
2.systemtap
systemtap是linux系统的一个调试跟踪程序的一个开源软件。个人觉得它是一个功能强大的热补丁工具hotfix,可以在运行的程序插入自己的代码和更改数据结构,可以很方便的跟踪程序执行情况。
官方文档
SystemTaphttps://sourceware.org/systemtap/documentation.html个人常用的功能:
- 打印执行日志
- 更改内核和应用程序的数据结构与流程,修复线上bug,标准hotfix。比如对nginx的更改
- 理解程序执行的内部逻辑
2.1 如何使用
先看如下程序2: 基本功能是给结构体赋值,每3秒打印一次global_test ok
#include <stdio.h>
typedef struct _s_test {
int a;
int b;
} s_test;
int global_test = 1;
s_test global_s_t;
void test_func()
{
if (global_test) {
printf("global_test ok");
}
global_s_t.a = 1;
global_s_t.b = 2;
}
void main()
{
while(1) {
test_func();
sleep(3);
}
}
systemtap最简单的理解就是在函数执行前和执行后加入hook,从而控制和跟踪运行的程序
centos安装和测试
sudo yum install systemtap
stap -ve 'probe begin { log("hello systap!") exit() }' #测试是否可以成功使用
sudo yum install kernel-devel-3.10.0-1160.el7.x86_64 -y #需要安装对应的内核开发包
函数执行前hook脚本pre_func.stap如下
#!/usr/bin/stap
probe begin
{
print("begin to probe")
}
#在函数test_func开始调用的时候做如下动作probe process("/home/chenyinghong/test/stap_test").function("test_func")
{
#print_backtrace(); 打印调用栈
print("global_test is ", $global_test) #显示变量global_test的内容
}probe end
{
print("end to probe")
}执行命令
sudo stap -gv ./pre_func.stap
函数执行后hook脚本只要改下,加下return
probe process("/home/chenyinghong/test/stap_test").function("test_func").return
2.2 更改数据结构和流程
对于干预程序的运行非常重要,如以下例子:
probe process("/home/chenyinghong/test/stap_test").function("test_func")
{
#print_backtrace(); 打印调用栈
print("global_test is ", $global_test) #显示变量global_test的内容$global_test = 0 #更改变量的内容,以控制流程等
}
2.3 总结
systemtap相关的功能非常强大,具体可以参考文文档。有时候灵活使用往往可以降低我们程序故障的发生
3.strace
个人主要用在系统调用的性能分析,异常分析,进程hung分析
优点是无语言限制,只要是liunx跑的基本都可以