通过gdb找出cpu飙升的代码行

前言

某日,运维的同学告诉你,php-fpm进程的cpu飚的有点高,是不是哪里的业务出问题了。这时候怎么办?有几个思路:

1)看日志。查看mysql慢查询日志或者在业务接口写日志记录代码运行时间。

2)借助xhprof。xhprof 是facebook开发的一个测试php性能的扩展,它可以精确分析每个方法执行的时间。不过自php5.6之后facebook就没有再维护了。

3) gdb工具。一款很强大的调试工具,它可以查看此时代码在内存中的调用栈,查看在运行的方法和参数。

本文重点介绍gdb调试工具。

简介

GDB(GNU debugger)是GNU开源组织发布的一个强大的UNIX下的程序调试工具。可以使用它通过命令行的方式调试程序。它使你能在程序运行时观察程序的内部结构和内存的使用情况。你也可以使用它分析程序崩溃前的发生了什么,从而找出程序崩溃的原因。相对于windows下的图形界面的VC等调试工具,它提供了更强大的功能。如果想在Windows下使用gdb,需要安装MinGW或者CygWin,并且需要配置环境变量才可以使用。

一般来说,gdb完成以下四个方面的工作:

1、启动你的程序,修改一些东西,从而影响程序运行的行为。

2、可以指定断点。程序执行到断点处会暂停执行。

3、当你的程序停掉时,你可以用它来观察发生了什么事情。

4、动态的改变你程序的执行环境,尝试修正bug。

安装

在安装gdb之前,先确定你的linux操作系统是否安装了gdb。你可以使用如下命令来确定是否安装了gdb。

如果已经安装了gdb,那么将会显示它能使用的所有参数。如果没有安装,我们可以通过以下几种方式来安装。

通过yum命令安装

通过yum安装的命令行如下:

通过rpm包方式安装

从http://rpmfind.net/linux/rpm2html/search.php?query=gdb上下载合适的rpm安装包。假如我们下载的安装名称为gdb-7.8.1.rpm。然后通过如下命令安装。

通过源码方式安装

安装gdb是很容易的。只要按照以下步骤一步步操作即可。

1、但是安装之前,必须保证它所依赖的环境没问题。下面是它依赖的依赖环境。

* c语言编译器。推荐使用gcc。

* 确保有不少于150M的磁盘空间。

2、然后打开这个网址 ftp://sourceware.org/pub/gdb/releases/,下载需要安装的gdb源码包。我们下载的源码包是gdb-7.8.1.tar.gz。

3、解压压缩包。压缩包解压结束后,进入解压后的目录。

4、然后一次执行如下命令,以便完成安装

基本使用

命令行格式

gdb    [-help] [-nx] [-q] [-batch] [-cd=dir] [-f] [-b bps] [-tty=dev] [-s symfile] [-e prog] [-se prog] [-c core] [-x

              cmds] [-d dir] [prog[core|procID]]

常用功能介绍

网上的一些教程基本上都是介绍使用gdb调试c或者c++语言编写的程序的。我们这节主要说明如何使用gdb调试php程序。我们的php脚本如下:
文件名为test.php,代码如下:

启动gdb

启动gdb可以使用如下几种方式:

第一种方式:

启动的时候指定要执行的脚本。

启动的时候指定php程序的路径。
Reading symbols from /home/fpm-php5.5.15/bin/php…done. 说明已经加载了php程序的符号表。
使用set args 命令指定php命令的参数。
使用r命令开始执行脚本。r即为run的简写形式。也可以使用run命令开始执行脚本。

第二种方式:
启动后通过file命令指定要调试的程序。当你使用gdb调试完一个程序,想调试另外一个程序时,就可以不退出gdb便能切换要调试的程序。具体操作步骤如下:

上面的例子中我们先使用gdb加载了程序exproxy进行调试。然后通过file命令加载了php程序,从而切换了要调试的程序。

获取帮助信息

gdb的子命令很多,可能有些你也不太熟悉。没关系,gdb提供了help子命令。通过这个help子命令,我们可以了解指定子命令的一些用法。如:

可见,通过help命令,我们可以了解命令的功能和使用方法。如果这个子命令还有一些子命令,那么它的所有子命令也会列出来。如上,set命令的set args等子命令也都列出来了。你还可以使用help命令来了解set args的更详尽的信息。

设置断点

为什么要设置断点呢?设置断点后,我们就可以指定程序执行到指定的点后停止。以便我们更详细的跟踪断点附近程序的执行情况。
设置断点的命令是break,缩写形式为b。
设置断点有很多方式。下面我们举例说明下常用的几种方式。
根据文件名和行号指定断点
如果你的程序是用c或者c++写的,那么你可以使用“文件名:行号”的形式设置断点。示例如下:

示例中的(gdb) b basic_functions.c:4439 是设置了断点。断点的位置是basic_functions.c文件的4439行。使用r命令执行脚本时,当运行到4439行时就会暂停。暂停的时候会把断点附近的代码给显示出来。可见,断点处是zif_sleep方法。这个zif_sleep方法就是我们php代码中sleep方法在php内核中的实现。根据规范,php内置提供的方法名前面加上zif_,就是这个方法在php内核或者扩展中实现时定义的方法名。

根据文件名和方法名指定断点
有些时候,手头没有源代码,不知道要打断点的具体位置。但是我们根据php的命名规范知道方法名。如,我们知道php程序中调用的sleep方法,在php内核中实现时的方法名是zif_sleep。这时,我们就可以通过”文件名:方法名”的方式打断点。示例如下:

另外,如果你不知道文件名的话,你也可以只指定方法名。命令示例如下:

设置条件断点
如果按上面的方法设置断点后,每次执行到断点位置都会暂停。有时候非常讨厌。我们只想在指定条件下才暂停。这时候根据条件设置断点就有了用武之地。设置条件断点的形式,就是在设置断点的基本形式后面增加 if条件。示例如下:

查看断点
可以使用info breakpoint查看断点的情况。包含都设置了那些断点,断点被命中的次数等信息。示例如下:

删除断点
对于无用的断点我们可以删除。删除的命令格式为 delete breakpoint 断点编号。info breakpoint命令显示结果中的num列就是编号。删除断点的示例如下:

上面的例子中我们删除了编号为9的断点。

查看代码

断点设置完后,当程序运行到断点处就会暂停。暂停的时候,我们可以查看断点附近的代码。查看代码的子命令是list,缩写形式为l。显示的代码行数为10行,基本上以断点处为中心,向上向下各显示几行代码。示例代码如下:

另外,你可以可以通过指定行号或者方法名来查看相关代码。
指定行号查看代码示例:

指定方法名查看代码示例:

单步执行

断点附近的代码你了解后,这时候你就可以使用单步执行一条一条语句的去执行。可以随时查看执行后的结果。单步执行有两个命令,分别是step和next。这两个命令的区别在于:
step 一条语句一条语句的执行。它有一个别名,s。
next 和step类似。只是当遇到被调用的方法时,不会进入到被调用方法中一条一条语句执行。它有一个别名n。
可能你对这两个命令还有些迷惑。下面我们用两个例子来给你演示下。
step命令示例:

可见,step命令进入到了被调用函数中zend_parse_parameters。使用step命令也会在这个方法中一行一行的单步执行。

next命令示例:

可见,使用next命令只会在本方法中单步执行。

继续执行

run命令是从头开始执行,如果我们只是想继续执行就可以使用continue命令。它的作用就是从暂停处继续执行。命令的简写形式为c。继续执行过程中遇到断点或者观察点变化依然会暂停。示例代码如下:

查看变量

现在你已经会设置断点,查看断点附近的代码,并可以单步执行和继续执行。接下来你可能会想知道程序运行的一些情况,如查看变量的值。print命令正好满足了你的需求。使用它打印出变量的值。print命令的简写形式为p。示例代码如下:

打印出的num的值为10,正好是我们在php代码中调用sleep方法传的值。另外可以使用“print/x my var” 的形式可以以十六进制形式查看变量值。

设置变量

使用print命令查看了变量的值,如果感觉这个值不符合预期,想修改下这个值,再看下执行效果。这种情况下,我们该怎么办呢?通常情况下,我们会修改代码,再重新执行代码。使用gdb的set命令,一切将变得更简单。set命令可以直接修改变量的值。示例代码如下:

上面的代码中我们是把sleep函数传入的10改为了2。即,sleep 2秒。注意,我们示例中修改的变量num是局部变量,只能对本次函数调用有效。下次再调用zif_sleep方法时,又会被设置为10。

设置观察点

设置观察点的作用就是,当被观察的变量发生变化后,程序就会暂停执行,并把变量的原值和新值都会显示出来。设置观察点的命令是watch。示例代码如下:

上例中num值从1变成了10时,程序暂停了。需要注意的是,你的程序中可能有多个同名的变量。那么使用watch命令会观察那个变量呢?这个要依赖于变量的作用域。即,在使用watch设置观察点时,可以直接访问的变量就是被观察的变量。

其他有用的命令

backtrace 简写形式为bt。查看程序执行的堆栈信息。

finish 执行到当前函数的结束。

.gdbinit工具

如果觉得gdb太麻烦,php官方有推荐的.gdbinit工具.

假设对于如下的脚本gdb.php:

使用php cli模式运行脚本

  • 1

查看php命令对应的进程pid

然后使用gdb调试php进程

使用后台运行以后, PHP5.4会sleep在e函数的sleep中, 这时, 如果我们使用gdb attach上去,

  1. gdb pid= xxx //使用ps获得后台运行脚本的pid

然后, source PHP源代码下面的.gdbinit:

然后, 让我们尝试调用下zbacktrace, 看看什么结果:

恩, 对于array和object, 因为我们为了保持不要乱屏, 所以没有展开, 不过, 如果我们要查看这个array具体是什么元素, 可以这样做, 注意到上面的:array(3)[0x2a95de7db0]:

类似的, 对于object, 注意到上面的: object[0x2a95de7840]

要注意的一点是, 对于object, 如果你是在调式Core文件, 而不是attach到一个运行的进程上, 那么上面的尝试会得到一个错误:

不过, 即使这样, 我们还是有办法, 只不过就比较麻烦了.在NTS下面:

参考:

“http://www.laruence.com/2011/12/06/3206.html”

 

发表评论

电子邮件地址不会被公开。 必填项已用*标注