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源代码增加到搜索路径。