《深入剖析Nginx》一2.1 利用gdb调试

2.1 利用gdb调试

深入剖析Nginx
gdb是Linux下调试程序的常用工具,任何Linux开发工程师初学程序调试时第一个接触到的工具应该就是gdb。关于gdb本身的详细用法,我们不多详述,读者可以参考gdb官网手册1,而在这里,我们将重点介绍一些与Nginx相关的注意点与调试技巧。

2.1.1 绑定Nginx到gdb

利用gdb调式Nginx,首先得在生成Nginx程序时把-g编译选项打开。当然,这并不是说不打开-g选项就无法用gdb调试它,只是会因为缺少相应的符号信息导致调试不便,而此时可能也将获得“No symbol table is loaded. Use the "file" command.”的提示。上一章已经介绍了如何编译Nginx,在执行./configure 命令生成对应的objs/Makefile文件后,检查该文件里的CFLAGS变量是否已带上了-g选项2,没有则加上即可。另一个值得关注的编译选项是-O0,如果在gdb内打印变量时提示“< value optimized out>”或gdb显示的当前正执行的代码行与源码匹配不上而让人感觉莫名其妙,那么,这多半是因为gcc的优化导致,我们可以加上-O0选项来强制禁用gcc的编译优化。除了可以通过编辑objs/Makefile文件,把这两个选项直接加在CFLAGS变量里以外,还有另外几种方法也可以达到同样的效果。

1. 在进行configure配置时,按如下方式执行。

[root@localhost nginx-1.2.0]# ./configure--with-cc-opt='-g –00'

上面是利用configure所提出的选项3来做的,属于比较推荐的方法,但也可使用如下方法。

[root@localhost nginx-1.2.0]# CFLAGS="-g -O0" ./configure
2. 在执行make时,按如下方式执行。

[root@localhost nginx-1.2.0]# make CFLAGS="-g -O0"
直接修改objs/Makefile文件和上面提到的第2种方法是在我们已经执行configure之后进行的,如果之前已经执行过make,那么在进行第二次make时,需带上强制重新编译 2选项-B或--aluays- make。也可以通过刷新所有源文件的时间戳,间接达到重新编译出一个新Nginx可执行程序的目的。

[root@localhost nginx-1.2.0]# find . -name "*.c" | xargs touch
不直接使用make clean是因为执行它会把objs整个目录都删除,当然这也包括我们修改过的objs/Makefile文件。获得正常编译后的Nginx二进制可执行程序后,我们可以利用gdb调试它,不过这首先需要把Nginx运行起来。在默认情况下,Nginx会有多个进程,所以需通过如下类似命令正确找到我们要调试的进程。

[root@localhost ~]# ps -efH | grep nginx
root    3971 24701  0 12:20 pts/4   00:00:00   grep nginx [root@localhost nginx-1.2.0]# make -B
root     3905     1 0 12:16 ?     00:00:00   nginx: master process ./nginx
nobody 3906  3905 0 12:16 ?     00:00:00   nginx: worker process
nobody 3907  3905 0 12:16 ?     00:00:00   nginx: worker process

源码实现已经给Nginx进程加上了title,所以根据标题很容易区分出哪个是监控进程,哪些个是工作进程。如要对如上所示的工作进程3906进行gdb调试,那么可以利用gdb的-p命令行参数。

[root@localhost ~]# gdb -p 3906

或者执行gdb命令进入gdb后执行。

(gdb) attach 3906

这两种方法都可以。

如果是要调试Nginx对客户端发过来请求的处理过程,那么要注意请求是否被交付给另外一个工作进程处理而导致绑定到gdb的这个工作进程实际没有动作。此时可以考虑开两个终端,运行两个gdb分别attach到两个工作进程上或干脆修改配置项worker_processes的值为1,从而使得Nginx只运行一个工作进程。

worker_processes  1;

通过上面这种方法只能调试Nginx运行起来之后的流程,对于启动过程中的逻辑,比如进程创建、配置解析等,因为已经执行完毕而无法调试,要调试这部分逻辑必须在Nginx启动的开始就把gdb绑定上,也就是在gdb里启动Nginx。这有几点需要注意,首先是Nginx默认以daemon形式运行,即它会调用fork()创建子进程并且把父进程直接exit(0)丢弃,因此在启动Nginx前,我们需设定

set follow-fork-mode child

也就是让gdb跟踪fork()之后的子进程,而gdb默认将跟踪fork()之后的父进程,不做此设定则将导致跟踪丢失。即便做了这样的设置,仍然比较麻烦,因为Nginx创建工作进程也用的是fork()函数,所以如果要调试监控进程则还需要做另外的灵活处理。我们可以修改Nginx配置文件。

daemon off;

这样Nginx就不再以daemon形象执行,利用gdb可以从Nginx的main()函数开始调试,默认情况下调试的当然就是监控进程的流程,如果要调试工作进程的流程需要在进入gdb后执行set follow-fork-mode child,在刚才已经提到了该条gdb命令的作用。另外更简单的方法就是直接设置:

master_process off;

将监控进程逻辑和工作进程逻辑全部合在一个进程里。不管怎样做,我们都必须让gdb attach到想要调试的对应进程上,比如如果必须要经过多次fork()后才能达到的代码位置(像函数ngx_cache_manager_process_cycle()),那么就要在多处恰当位置下断点,然后在执行到该断点时根据需要切换follow-fork-mode标记。这些变通设置对于调试像配置信息解析流程、文件缓存等这一类初始相关逻辑是非常重要的,因为Nginx的这些逻辑是在Nginx启动时进行的。如果你发现gdb跟丢了进程或当前调试的代码不是你预想的流程,那么请仔细做这些确认与检查工作。

最后,因为执行Nginx需指定配置文件路径,如何在gdb里带参数运行Nginx是必须知道的。这有很多种方法,比如在Shell里执行:

gdb --args ./objs/nginx -c /usr/local/nginx/conf/nginx.conf

进入到gdb后在执行r命令即可;或者在Shell里执行:

gdb ./objs/nginx

进入到gdb后执行r -c /usr/local/nginx/conf/nginx.conf或在gdb内先执行命令

set args -c /usr/local/nginx/conf/nginx.conf

再执行r命令。

2.1.2 gdb的watch指令

将Nginx特定进程绑定到gdb后,剩余的跟踪与调试操作无非就是gdb的使用,这可以参考官方手册。手册内容很多,因为gdb提供的功能非常丰富,但平常我们使用的功能却很少。其实gdb的某些功能是相当有利用价值的,像Break conditions、Watchpoints等。这里仅以Watchpoints(监视点)为例看看它的实际使用效果。Watchpoints可以帮助我们监视某个变量在什么时候被修改,这对于我们了解Nginx程序的执行逻辑非常有帮助。比如在理解Nginx的共享内存逻辑时,看到ngx_shared_memory_add()函数内初始化的shm_zone->init回调为空。

1256: 代码片段2.1.2-1,文件名: ngx_cycle.c
1257: ngx_shm_zone_t *
1258: ngx_shared_memory_add(ngx_conf_t cf, ngx_str_t name, size_t size, void *tag)
1259: {
1260: …
1318:     shm_zone->init = NULL;

而在ngx_init_cycle()函数里对该回调函数却是直接执行而并没有做前置判空处理。

41: 代码片段2.1.2-2,文件名: ngx_cycle.c
42: ngx_cycle_t *
43: ngx_init_cycle(ngx_cycle_t *old_cycle)
44: {
45: …
475:         if (shm_zone[i].init(&shm_zone[i], NULL) != NGX_OK) {
476:             goto failed;
477:         }

这说明这个函数指针一定是在其他某处被再次赋值,但具体是在哪里呢?搜索Nginx全部源代码可能一下子没找到对应的代码行,那么,此时就可利用gdb的Watchpoints功能进行快速定位。

(gdb) b ngx_cycle.c:1318
Breakpoint 1 at 0x805d7ce: file src/core/ngx_cycle.c, line 1318.
(gdb) r
Starting program: /home/gqk/nginx-1.2.0/objs/nginx -c /usr/local/nginx/conf/ nginx.conf. upstream.sharedmem
[Thread debugging using libthread_db enabled]
Breakpoint 1, ngx_shared_memory_add (cf=0xbffff39c, name=0xbfffeed8, size=134217728, tag= 0x80dbd80) at src/core/ngx_cycle.c:1318
1318     shm_zone->init = NULL;
Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.47.el6.i686 nss-softokn- freebl-3.12.9-11.el6.i686 openssl-1.0.0-20.el6.i686 pcre-7.8-3.1.el6. i686 zlib-1.2.3-27.el6.i686
(gdb) p &shm_zone->init
$1 = (ngx_shm_zone_init_pt *) 0x80eba68
(gdb) watch (ngx_shm_zone_init_pt ) 0x80eba68
Hardware watchpoint 2: (ngx_shm_zone_init_pt ) 0x80eba68
(gdb) c
Continuing.
Hardware watchpoint 2: (ngx_shm_zone_init_pt ) 0x80eba68

Old value = (ngx_shm_zone_init_pt) 0
New value = (ngx_shm_zone_init_pt) 0x809d9c7 <ngx_http_file_cache_init>
ngx_http_file_cache_set_slot (cf=0xbffff39c, cmd=0x80dc0d8, conf=0x0) at src/http/ngx_http_ file_cache.c:1807
1807     cache->shm_zone->data = cache;

先在shm_zone->init = NULL;代码所对应的第1318行先下一个Breakpoint,执行Nginx后将在此处暂停程序,通过 p 指令打印获取shm_zone->init的地址值,然后直接给shm_zone->init对应的地址下个Watchpoint进行监视。这样即便是跑出shm_zone->init变量所在的作用域也没有关系,执行c命令继续执行Nginx,一旦shm_zone->init被修改,那么就停止在进行修改的代码的下一行,修改之前的值Old value和修改之后的值New value也将都被gdb抓取出来。如上示例中,可以看到修改逻辑在第1806行(我这里是以proxy_cache所用的共享内存作为实例,而在其他实例情况下,可能将与此不同)。

1084: 代码片段2.1.3-1,文件名: ngx_http_file_cache.c
1085: …
1086:     cache->shm_zone->init = ngx_http_file_cache_init;
1087:     cache->shm_zone->data = cache;

从上面的简单示例里可以看到gdb watch命令的强大作用,除了利用该命令监控指定变量的写操作以外,还可以利用另外两个同类命令rwatch和awatch分别监控指定变量的读操作和读/写操作。当然,关于这方面的更多内容,在gdb手册上有详细介绍4。

2.1.3 Nginx对gdb的支持

Nginx本身对于gdb也有相关辅助支持,这表现在配置指令debug_points上,对于该配置项的配置值可以是stop或abort。当Nginx遇到严重错误时,比如内存超限或其他不可预料的逻辑错误,就会调用ngx_debug_point()函数(这类似于assert()一样的断言函数,只是函数ngx_debug_point()本身不带判断),该函数根据debug_points配置指令的设置做出相应的处理。如果将debug_points设置为stop,那么ngx_debug_point()函数的调用将使得Nginx进程进入到暂停状态,以便我们可通过gdb接入到该进程查看相关上下文信息。

[root@localhost ~]# ps aux | grep nginx
root      4614  0.0  0.0  24044   592 ?        Ts   12:48   0:00 ./nginx
root      4780  0.0  0.1 103152   800 pts/4    S+   13:00   0:00 grep nginx

注意上面的./nginx状态为Ts(s代表Nginx进程为一个会话首进程session leader),其中T就代表Nginx进程处在TASK_STOPPED状态,此时我们用gdb连上去即可查看问题所在(我这里只是一个测试,在main函数里主动调用ngx_debug_point()而已,所以下面看到的bt堆栈很简单,实际使用时,我们当然要把该函数放在需要观察的代码点,比如非正常逻辑点)。

[root@localhost ~]# gdb -q -p 4614
Attaching to process 4614
Reading symbols from /usr/local/nginx/sbin/nginx...done.
...
openssl-1.0.0-4.el6.x86_64 pcre-7.8-3.1.el6.x86_64 zlib-1.2.3-25.el6.x86_64
(gdb) bt
#0  0x0000003a9ea0f38b in raise () from /lib64/libpthread.so.0
#1  0x0000000000431a8a in ngx_debug_point () at src/os/unix/ngx_process.c:603
#2  0x00000000004035d9 in main (argc=1, argv=0x7fffbd0a0c08) at src/core/ nginx.c:406
(gdb) c
Continuing.

Program received signal SIGTERM, Terminated.
执行c命令,Nginx即自动退出。

如果将debug_points设置为abort,那么Nginx调用ngx_debug_point()函数时直接将程序abort崩溃掉,如果对操作系统做了恰当的设置,则将获得对应的core文件,这就大大方便我们进行事后的慢慢调试,延用上面的直接在main函数里主动调用ngx_debug_point()的例子。

[root@localhost nginx]# ulimit -c
0
[root@localhost nginx]# ulimit -c unlimited
[root@localhost nginx]# ulimit -c
unlimited
[root@localhost nginx]# ./sbin/nginx
[root@localhost nginx]# ls
client_body_temp  core.5242     html  proxy_temp  scgi_temp
conf              fastcgi_temp  logs  sbin        uwsgi_temp
生成了名为core.5242的core文件,利用gdb调试该core文件。

[root@localhost nginx]# gdb sbin/nginx core.5242 -q
Reading symbols from /usr/local/nginx/sbin/nginx...done.
[New Thread 5242]
...
(gdb) bt
#0  0x0000003a9de329a5 in raise () from /lib64/libc.so.6
#1  0x0000003a9de34185 in abort () from /lib64/libc.so.6
#2  0x0000000000431a92 in ngx_debug_point () at src/os/unix/ngx_process.c:607
#3  0x00000000004035d9 in main (argc=1, argv=0x7fffd5625f18) at src/core/ nginx.c:406
(gdb) up 3
#3  0x00000000004035d9 in main (argc=1, argv=0x7fffd5625f18) at src/core/nginx.c:406
406  ngx_debug_point();
(gdb) list
401         }
402     }
403
404     ngx_use_stderr = 0;
405
406  ngx_debug_point();
407
408     if (ngx_process == NGX_PROCESS_SINGLE) {
409            ngx_single_process_cycle(cycle);
410

2.1.4 宏

Nginx里有大量的宏。如果不事先做一下处理,在gdb里将无法查看这些宏的定义以及展开形式,也就会获得如下提示信息。

(gdb) info macro NGX_OK
The symbol 'NGX_OK' has no definition as a C/C++ preprocessor macro
at <user-defined>:-1
(gdb) p NGX_OK
No symbol "NGX_OK" in current context.

如果我们将编译选项-g改为-ggdb3,虽然这样编译得到的二进制文件会比较大,但是因为它包含了所有与宏相关的信息(当然也包含了很多其他信息),所以我们就可以在gdb里使用类似命令。

(gdb) info macro NGX_OK
Defined at src/core/ngx_core.h:30
  included at src/core/nginx.c:9
#define NGX_OK 0
(gdb) macro expand NGX_OK
expands to: 0

来查看指定宏的定义与展开形式,而gdb命令里也可以直接使用这些宏,比如执行打印指令p。

(gdb) p NGX_OK
$1 = 0

当然,这些操作需要在当前上下文里有对应的NGX_OK宏定义,否则同样无法查看。这很容易理解,毕竟宏也有对应的“作用域”,也就是说同一个宏名在不同的代码处可能有不同的展开,所以gdb是利用当前代码列表作为选择“作用域”的参考点。

如果当前应用程序在执行当中,比如在main()函数处下断点,然后执行r命令后被断了下来,那么当前代码列表就是以main函数里的第一行作为参考点,宏展开也就以当前执行行作为参考点。如果应用程序当前未处于执行状态,并且也没有使用list命令指定当前代码行,那么宏可能无法显示或显示不正确。比如我在Nginx的main()函数处查看EPOLLIN宏,结果如下。

(gdb) info macro EPOLLIN
The symbol `EPOLLIN' has no definition as a C/C++ preprocessor macro
at <user-defined>:-1

结果表明没有找到EPOLLIN宏,但如果我使用list命令列表,会使用到EPOLLIN宏的源文件,那么对应的情况如下。

(gdb) list ngx_epoll_module.c:0
1
2 /*
3  * Copyright (C) Igor Sysoev
4  * Copyright (C) Nginx, Inc.
5  */
6
7
8 #include <ngx_config.h>
9 #include <ngx_core.h>
10 #include <ngx_event.h>
(gdb) info macro EPOLLIN
Defined at /usr/include/sys/epoll.h:47
  included at src/os/unix/ngx_linux_config.h:86
  included at src/core/ngx_config.h:26
  included at src/event/modules/ngx_epoll_module.c:8
#define EPOLLIN EPOLLIN

可以看到第二次info macro就能正确找到并显示EPOLLIN宏了。关于这方面的更多实例,请参考这里5。

2.1.5 cgdb

cgdb6是我想推荐给大家使用的一个封装gdb的开源调试工具。相比Windows下的Visual Studio等图形调试工具而言,它的可视化功能显得十分轻量级,但它的最大好处在于能在终端里运行并且原生具备gdb的强大调试功能。关于cgdb的详细使用可以参考官方手册7或这里8。

cgdb在远程ssh里执行的界面如图2-1所示,如果上面类vi窗口没有显示对应的源代码或下面gdb窗口提示No such file or directory.,那么需要利用directory命令把Nginx源代码增加到搜索路径。

时间: 2024-10-28 19:21:21

《深入剖析Nginx》一2.1 利用gdb调试的相关文章

《深入剖析Nginx》——第2章  跟踪与调试2.1 利用gdb调试

第2章 跟踪与调试 跟踪与调试,不仅是我们解决程序Bug的有力途径,也是帮助我们理解现有代码的有效方法.通过跟踪程序执行的过程,我们可以清楚地了解程序的内部逻辑,对于不明就里的实现细节,调试查看程序内部变量也能更好地帮助我们做出正确的理解.本章将介绍一些跟踪与调试程序的方法,除了最基本的 gdb 调试,我还将结合个人经验,介绍一些相对高级的应用技巧. 2.1 利用gdb调试 gdb是Linux下调试程序的常用工具,任何Linux开发工程师初学程序调试时第一个接触到的工具应该就是gdb.关于gdb

《深入剖析Nginx》——2.3 利用strace/pstack调试Nginx

2.3 利用strace/pstack调试Nginx Linux下有两个命令strace1和ltrace2可以分别用来查看一个应用程序在运行过程中所发起的系统函数调用和动态库函数调用,这对作为标准应用程序的Nginx自然同样可用.由于这两个命令大同小异,下面就仅以strace为例做简单介绍,大致了解一些它能帮助我们获取哪些有用的调试信息.关于strace/ltrace以及后面介绍的pstack更多的用法请参考对应的Man手册. 从strace的Man手册可以看到几个有用的选项. p pid:通过

《深入剖析Nginx》——2.2 利用日志信息跟踪Nginx

2.2 利用日志信息跟踪Nginx 优秀的程序都会带有自己的日志输出接口,并且一般还会给出不同等级的输出级别,以便于重次信息的过滤,比如 Linux 内核的日志输出标准接口为 printk,并且给出了KERN_EMERG.KERN_ALERT.KERN_DEBUG等这样的输出等级.Nginx 与此类似,下面具体来看. 为了获取最丰富的日志信息,我们在进行configure配置时,需要把--with-debug选项加上,这样能生成一个名为NGX_DEBUG的宏,而在Nginx源码内,该宏被用作控制

《深入剖析Nginx》一2.4 获得Nginx程序完整执行流程

2.4 获得Nginx程序完整执行流程 深入剖析Nginx 利用strace命令能帮助我们获取到Nginx在运行过程中所发起的所有系统调用,但是不能看到Nginx内部各个函数的调用情况.利用gdb调试Nginx能让我们很清晰地获得Nginx每一步的执行流程,但是单步调试毕竟是非常麻烦的,有没有更为方便的方法一次性获得Nginx程序执行的整个流程呢?答案是肯定的,而且方法还非常多1.虽然相比直接使用某些强大工具(如System Tap2)而言,下面要介绍的方法比较笨,但它的确可行,而且从这个过程中

《深入剖析Nginx》一导读

前 言 深入剖析Nginx慕名对Nginx源码进行学习与研究是早在2008年的事情.当时正在为职业规划与未来发展困惑不已,一筹莫展之际不知从哪里得知高性能服务器是一个很有"前途"的努力方向,几经搜索又机缘偶合地得识Lighttpd与Nginx.在逐步了解和熟悉它们的源码后,我开始感到自己的无知与浅薄,发现原来代码也可以写得如此优雅. 我已编著过一本<Lighttpd源码分析>.先解析Lighttpd源码并没有什么特别的原因,只是因为在当时Lighttpd比Nginx要火,应

《深入剖析Nginx》——导读

前言 慕名对Nginx源码进行学习与研究是早在2008年的事情.当时正在为职业规划与未来发展困惑不已,一筹莫展之际不知从哪里得知高性能服务器是一个很有"前途"的努力方向,几经搜索又机缘偶合地得识Lighttpd与Nginx.在逐步了解和熟悉它们的源码后,我开始感到自己的无知与浅薄,发现原来代码也可以写得如此优雅. 我已编著过一本<Lighttpd源码分析>.先解析Lighttpd源码并没有什么特别的原因,只是因为在当时Lighttpd比Nginx要火,应用得也较广:而近几年

《深入剖析Nginx》——2.4 获得Nginx程序完整执行流程

2.4 获得Nginx程序完整执行流程 利用strace命令能帮助我们获取到Nginx在运行过程中所发起的所有系统调用,但是不能看到Nginx内部各个函数的调用情况.利用gdb调试Nginx能让我们很清晰地获得Nginx每一步的执行流程,但是单步调试毕竟是非常麻烦的,有没有更为方便的方法一次性获得Nginx程序执行的整个流程呢?答案是肯定的,而且方法还非常多1.虽然相比直接使用某些强大工具(如System Tap2)而言,下面要介绍的方法比较笨,但它的确可行,而且从这个过程中也许能学到一些额外的

《深入剖析Nginx》一2.3 利用strace/pstack调试Nginx

2.3 利用strace/pstack调试Nginx 深入剖析NginxLinux下有两个命令strace1和ltrace2可以分别用来查看一个应用程序在运行过程中所发起的系统函数调用和动态库函数调用,这对作为标准应用程序的Nginx自然同样可用.由于这两个命令大同小异,下面就仅以strace为例做简单介绍,大致了解一些它能帮助我们获取哪些有用的调试信息.关于strace/ltrace以及后面介绍的pstack更多的用法请参考对应的Man手册. 从strace的Man手册可以看到几个有用的选项.

《深入剖析Nginx》一2.2 利用日志信息跟踪Nginx

2.2 利用日志信息跟踪Nginx 深入剖析Nginx 优秀的程序都会带有自己的日志输出接口,并且一般还会给出不同等级的输出级别,以便于重次信息的过滤,比如 Linux 内核的日志输出标准接口为 printk,并且给出了KERN_EMERG.KERN_ALERT.KERN_DEBUG等这样的输出等级.Nginx 与此类似,下面具体来看. 为了获取最丰富的日志信息,我们在进行configure配置时,需要把--with-debug选项加上,这样能生成一个名为NGX_DEBUG的宏,而在Nginx源