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

简介:

本节书摘来自异步社区《深入剖析Nginx》一书中的第2章,第2.1节,作者: 高群凯 更多章节内容可以访问云栖社区“异步社区”公众号查看。

第2章  跟踪与调试

跟踪与调试,不仅是我们解决程序Bug的有力途径,也是帮助我们理解现有代码的有效方法。通过跟踪程序执行的过程,我们可以清楚地了解程序的内部逻辑,对于不明就里的实现细节,调试查看程序内部变量也能更好地帮助我们做出正确的理解。本章将介绍一些跟踪与调试程序的方法,除了最基本的 gdb 调试,我还将结合个人经验,介绍一些相对高级的应用技巧。

2.1 利用gdb调试

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


<a href=https://yqfile.alicdn.com/4c3a2d089c8abc4fdb28e5c9051e169f71fc8b71.png" >
相关实践学习
阿里云图数据库GDB入门与应用
图数据库(Graph Database,简称GDB)是一种支持Property Graph图模型、用于处理高度连接数据查询与存储的实时、可靠的在线数据库服务。它支持Apache TinkerPop Gremlin查询语言,可以帮您快速构建基于高度连接的数据集的应用程序。GDB非常适合社交网络、欺诈检测、推荐引擎、实时图谱、网络/IT运营这类高度互连数据集的场景。 GDB由阿里云自主研发,具备如下优势: 标准图查询语言:支持属性图,高度兼容Gremlin图查询语言。 高度优化的自研引擎:高度优化的自研图计算层和存储层,云盘多副本保障数据超高可靠,支持ACID事务。 服务高可用:支持高可用实例,节点故障迅速转移,保障业务连续性。 易运维:提供备份恢复、自动升级、监控告警、故障切换等丰富的运维功能,大幅降低运维成本。 产品主页:https://www.aliyun.com/product/gdb
相关文章
|
2月前
|
NoSQL 搜索推荐 openCL
【C/C++ 调试 GDB指南 】gdb调试基本操作
【C/C++ 调试 GDB指南 】gdb调试基本操作
71 2
|
4月前
|
NoSQL Linux 开发工具
【深入解析git和gdb:版本控制与调试利器的终极指南】(下)
【深入解析git和gdb:版本控制与调试利器的终极指南】
|
19天前
|
NoSQL Ubuntu 测试技术
【GDB自定义指令】core analyzer结合gdb的调试及自定义gdb指令详情
【GDB自定义指令】core analyzer结合gdb的调试及自定义gdb指令详情
12 1
|
19天前
|
NoSQL 编译器 C语言
【GDB调试技巧】提高gdb的调试效率
【GDB调试技巧】提高gdb的调试效率
11 1
|
19天前
|
NoSQL Ubuntu 开发工具
【gdb调试】在ubuntu环境使用gdb调试一棵四层二叉树的数据结构详解
【gdb调试】在ubuntu环境使用gdb调试一棵四层二叉树的数据结构详解
9 1
|
2月前
|
NoSQL C++ 开发者
【C/C++ 调试 GDB指南 】GDB中的‘info’命令:一次全面的探索
【C/C++ 调试 GDB指南 】GDB中的‘info’命令:一次全面的探索
45 0
|
2月前
|
NoSQL Shell 程序员
【C/C++ 调试 GDB指南 】GDB调试工具介绍:从基础到高级
【C/C++ 调试 GDB指南 】GDB调试工具介绍:从基础到高级
68 0
|
2月前
|
NoSQL 算法 Shell
【C/C++ 调试 GDB指南 】详解 gdb 断点的设置方式
【C/C++ 调试 GDB指南 】详解 gdb 断点的设置方式
21 2
|
4月前
|
NoSQL Unix 开发工具
【深入解析git和gdb:版本控制与调试利器的终极指南】(上)
【深入解析git和gdb:版本控制与调试利器的终极指南】
|
4月前
|
NoSQL Linux
Linux系统中调试GDB调试方法入门分享
Linux系统中调试GDB调试方法入门分享
85 0