前置内容:练习1-编译调试teamTalkcmake入门笔记,对 makefile 有一定了解。
参考:nginx依赖库Windows下编译Nginxgdb远程调试,《深入剖析Nginx》,chatgpt 。

前言

Nginx 是一款高性能的开源Web服务器和反向代理服务器,其特点是占用内存少、并发能力强、处理静态文件快、可扩展性好、稳定性高、配置简单等。毫无疑问,花时间学习它将是一笔划算的投资,那么第一步便是编译、运行并调试它。本文给出以下几种途径来编译调试 Nginx:

  • 原生 gdb 调试
  • CLion 编译数据库组织项目 + 远程 gdb 调试
  • CLion 原生支持 CMake 管理项目并调试
  • (转载)VS 编译调试 Nginx

在上节练习1-编译调试teamTalk中,我们熟悉了 Windows 的 Visual Studio 开发环境,本节我们则重点讲述 Linux 下的 CLion 开发环境,本机 Linux 环境为 Ubuntu 20.4.6 。

原生 gdb 调试

此方式直接对包含调试信息的可执行程序 nginx 进行 gdb 调试,这种方式当然是最简单的,也当然是最痛苦的,没人希望在黑洞洞的狭窄界面中调试跟踪几万行的大型项目。下面先说明如何编译并配置 Nginx,此内容也是后续方法的前提步骤

1.安装依赖库

  • pcre 库,以支持正则表达式。如果我们在配置文件 nginx.conf 中使用了正则表达式,那么必须链接此库。

    1
    sudo apt-get install libpcre3 libpcre3-dev  
  • zlib 库,用于对 HTTP 包的内容做 gzip 格式的压缩。

    1
    sudo apt-get install zlib1g-dev
  • OpenSSL 库,用于支持 SSL 协议。

    1
    sudo apt-get install openssl libssl-dev 

2.安装 Nginx

  1. 官方下载源码,考虑到《深入剖析Nginx》这本书采用的是 1.2.0 版本进行讲解,为了方便后续对照学习,笔者也下载的此版本。

  2. 在你喜欢的地方解压并进入该目录,使用以下配置生成 makefile:

    1
    ./configure --with-debug --prefix="/home/jyx/CLionProjects/nginx" --with-cc-opt="-Wno-error"
    • --with-debug :添加调试信息,这是我们后续调试的前提!
    • --prefix="/home/jyx/CLionProjects/nginx" :1)指定安装目录,如果不指定,则默认安装在 usr/local/nginx 下。2)每次启动 nginx 时,程序会首先从该位置的 conf 目录下读取 nginx.conf 配置文件。
      为了方便后续统一在 CLion 下编辑整个项目,我们不打算 make install,所以将路径设置为该目录本身,这样读取配置时就直接从该目录下的 conf 目录读取配置文件。注意,不能有中文路径!
    • --with-cc-opt="-Wno-error" :指定编译选项以忽略下面的错误:

    输入 ./configure --help 以查看其他选项。

  3. 执行 make ,你可能会遇到报错:

    这是版本问题,不用慌,注释掉这行代码即可

  4. 使用 sudo 权限运行 objs/nginx,报错:

    小问题,我们在 nginx 目录下创建 logs 目录即可,重新运行,成功:
    成功访问本地服务器

    1
    2
    3
    4
    5
    $ sudo ps -aux | grep nginx
    jyx 1916 0.0 0.1 175184 8988 ? Sl May18 0:00 /usr/libexec/ibus-engine-simple
    root 15773 0.0 0.0 7324 1184 ? Ss 00:38 0:00 nginx: master process /home/jyx/Downloads/nginx-1.2.0/objs/nginx
    nobody 15774 0.0 0.0 7960 3104 ? S 00:38 0:00 nginx: worker process
    jyx 18578 0.0 0.0 17672 720 pts/1 S+ 02:01 0:00 grep --color=auto ngi

    可见 Nginx 开启了多个进程。注意,nginx 以后台方式运行,后续重启调试 nginx 时需要先关闭所有相关进程,否则会提示 bind 函数调用失败。关闭方法如下:

    1
    $ sudo killall nginx

3.调试 nginx

在默认情况下, Nginx 会有多个进程,即一个主进程和多个工作进程(由 nginx.conf 决定)。如果要调试 Nginx 对客户端发过来请求的处理过程, 那么要注意请求是否被交付给另外一个工作进程处理而导致绑定到 gdb 的这个工作进程实际没有动作,为了方便,我们直接指定工作进程的数量为 1 :

1
2
#nginx.conf文件
worker_processes 1;

同时,Nginx 默认以 daemon 形式运行, 即它会调用 fork 创建子进程并且把父进程直接 exit(0)丢弃, 而 gdb 默认将跟踪 fork() 之后的父进程, 这将导致跟踪丢失:

所以我们关闭 daemon 模式:

1
2
#nginx.conf文件
daemon off;

这样设置后,我们调试的是监控进程(master)的流程。工作进程(worker)由 master 进程 fork 而来,如果要调试 worker 进程,则需要在进入 gdb 后执行如下命令:

1
set follow-fbrk-mode child

也就是让 gdb 跟踪 fork 之后的子进程。如果我们想要同时跟踪 master 和 worker,则直接将它们合并到一个进程中 ,配置如下:

1
2
#nginx.conf文件
master_process off;

这样,我们就将监控进程逻辑和工作进程逻辑全部合在了一个进程里。通过这三处配置,调试难度就大大降低了。

接下来我们使用 gdb 调试 objs 目录下的 nginx 程序即可,注意使用 sudo 运行

run起来后,程序被阻塞在某处,我们中断它(ctrl+c),可以看到上图,程序执行流阻塞在epoll中

接下来就开始你的调试之路吧。


CLion 编译数据库+gdb远程调试

Nginx 使用 makefile 来构建项目,而 CLion 本身使用 cmake 来组织管理项目,对 makefile 的支持还不够到位,因此无法直接接管 Nginx 项目。所幸的是,CLion 支持编译数据库(compile_commands.json),我们能够通过它来组织管理项目。读者朋友们可能不太熟悉编译数据库,下面是 chatgpt 对它的简单介绍:

compile_commands.json 是一个 JSON 格式的文件,它包含了编译器生成的编译命令以及相关的源文件和编译选项。这个文件通常用于辅助代码分析工具,如代码编辑器、代码静态分析工具等,以便这些工具能够理解代码的编译过程,从而提供更好的代码提示、错误检查和重构等功能。
注意,它只能用来组织项目以便提供代码分析功能(比如代码跳转、代码提示等),其本身不是类似于 cmake 和 makefile 一类的构建文件,无法用来构建或生成项目

  1. 这里我们使用 compiledb 工具来生成 compile_commands.json,首先下载它:
1
2
#通过pip安装compiledb 
pip install compiledb
  1. 如之前所述,仍然先生成 makefile:
1
./configure --with-debug --prefix="/home/jyx/CLionProjects/nginx" --with-cc-opt="-Wno-error"
  1. 生成编译数据库 compile_commands.json :
1
2
$ compiledb -nf make
#-n选项表示执行make命令时不实际执行编译过程,只输出编译命令;-f选项指定Makefile文件的路径,如果不指定则默认使用当前目录下的Makefile文件.该命令利用make输出的编译信息来生成数据库.
  1. 通过 CLion 打开该编译数据库,open as projecct :

这样我们就能利用 CLion 分析整个 Nginx 项目了。你可以试试点击头文件跳转的功能(CTRL+点击),如果不使用编译数据库,是无法跳转的(除非该头文件和目前文件在同一目录)。

此时我们仅获得了代码分析功能,还无法调试该项目,还需要借助 gdb 的远程调试功能。我们先使用 make 编译该项目生成 objs/nginx 可执行文件,然后开启 gdbserver:

然后回到 CLion 添加配置选项:

点击 OK,接下来见证奇迹,点击右上方的 debug 虫子图标,开始调试:

程序在断点处停了下来,调试成功。另外,如果我们想让程序中断下来,则需要在 gdbserver 处执行 CTRL+C,在 CLion 中的 gdb 窗口执行 CTRL+C 将没有任何反应。

这里调试时依然别忘了我们之前的三处配置。


CLion + CMake 管理项目并调试

使用 gdb 远程调试总感觉有些别扭,而且中断程序时也不方便,所以下面我们尝试用 CLion 原生支持的 cmake 来管理项目。首先我们需要将 makefile 转换为 CMakeLists.txt ,这里有几点说明:

  • cmake 比 makefile 更抽象,具备一些 makefile 没有的东西,所以不存在一种统一(或者说官方)的工具来将 makefile 转为 CMakeLists.txt 。
  • 一般来说,采用 makefile 管理的项目中,其 makefile 都比较简单(不像 cmake 生成的 makefile,那可不是人看的),完全可以手动将其转换为 CMakeLists.txt 。

就 objs 目录下的 makefile 来说(执行 configure 后),其表达的功能就三个:

  1. 将指定源文件生成目标文件,如:

    1
    2
    3
    4
    $(CC) -c $(CFLAGS) $(CORE_INCS) 
    -o objs/src/core/nginx.o \
    src/core/nginx.c
    #nginx.c --> nginx.o
  2. 将所有目标文件和库文件链接成可执行文件(第 208 行):

    1
    2
    $(LINK) -o objs/nginx 所有目标文件 \
    -lpthread -lcrypt -lpcre -lcrypto -lcrypto -lz #链接库
  3. 至于第 1068 行之后的就不用管了,它们是用来处理文档和安装任务的。

所以转 CMakeLists.txt 是很轻松的,当然,前提是你需要有一定的 makefile 和 cmake 基础(头秃),下面给出笔者转换的 CMakeLists.txt :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
cmake_minimum_required(VERSION 3.16.3)
project(nginx_cmake)
set( SRC
src/core/nginx.c
src/core/ngx_log.c
src/core/ngx_palloc.c
src/core/ngx_array.c
src/core/ngx_list.c
src/core/ngx_hash.c
src/core/ngx_buf.c
src/core/ngx_queue.c
src/core/ngx_output_chain.c
src/core/ngx_string.c
src/core/ngx_parse.c
src/core/ngx_inet.c
src/core/ngx_file.c
src/core/ngx_crc32.c
src/core/ngx_murmurhash.c
src/core/ngx_md5.c
src/core/ngx_rbtree.c
src/core/ngx_radix_tree.c
src/core/ngx_slab.c
src/core/ngx_times.c
src/core/ngx_shmtx.c
src/core/ngx_connection.c
src/core/ngx_cycle.c
src/core/ngx_spinlock.c
src/core/ngx_cpuinfo.c
src/core/ngx_conf_file.c
src/core/ngx_resolver.c
src/core/ngx_open_file_cache.c
src/core/ngx_crypt.c
src/event/ngx_event.c
src/event/ngx_event_timer.c
src/event/ngx_event_posted.c
src/event/ngx_event_busy_lock.c
src/event/ngx_event_accept.c
src/event/ngx_event_connect.c
src/event/ngx_event_pipe.c
src/os/unix/ngx_time.c
src/os/unix/ngx_errno.c
src/os/unix/ngx_alloc.c
src/os/unix/ngx_files.c
src/os/unix/ngx_socket.c
src/os/unix/ngx_recv.c
src/os/unix/ngx_readv_chain.c
src/os/unix/ngx_udp_recv.c
src/os/unix/ngx_send.c
src/os/unix/ngx_writev_chain.c
src/os/unix/ngx_channel.c
src/os/unix/ngx_shmem.c
src/os/unix/ngx_process.c
src/os/unix/ngx_daemon.c
src/os/unix/ngx_setaffinity.c
src/os/unix/ngx_setproctitle.c
src/os/unix/ngx_posix_init.c
src/os/unix/ngx_user.c
src/os/unix/ngx_process_cycle.c
src/os/unix/ngx_linux_init.c
src/event/modules/ngx_epoll_module.c
src/os/unix/ngx_linux_sendfile_chain.c
src/core/ngx_regex.c
src/http/ngx_http.c
src/http/ngx_http_core_module.c
src/http/ngx_http_special_response.c
src/http/ngx_http_request.c
src/http/ngx_http_parse.c
src/http/ngx_http_header_filter_module.c
src/http/ngx_http_write_filter_module.c
src/http/ngx_http_copy_filter_module.c
src/http/modules/ngx_http_log_module.c
src/http/ngx_http_request_body.c
src/http/ngx_http_variables.c
src/http/ngx_http_script.c
src/http/ngx_http_upstream.c
src/http/ngx_http_upstream_round_robin.c
src/http/ngx_http_parse_time.c
src/http/modules/ngx_http_static_module.c
src/http/modules/ngx_http_index_module.c
src/http/modules/ngx_http_chunked_filter_module.c
src/http/modules/ngx_http_range_filter_module.c
src/http/modules/ngx_http_headers_filter_module.c
src/http/modules/ngx_http_not_modified_filter_module.c
src/http/ngx_http_busy_lock.c
src/http/ngx_http_file_cache.c
src/http/modules/ngx_http_gzip_filter_module.c
src/http/ngx_http_postpone_filter_module.c
src/http/modules/ngx_http_ssi_filter_module.c
src/http/modules/ngx_http_charset_filter_module.c
src/http/modules/ngx_http_userid_filter_module.c
src/http/modules/ngx_http_autoindex_module.c
src/http/modules/ngx_http_auth_basic_module.c
src/http/modules/ngx_http_access_module.c
src/http/modules/ngx_http_limit_conn_module.c
src/http/modules/ngx_http_limit_req_module.c
src/http/modules/ngx_http_geo_module.c
src/http/modules/ngx_http_map_module.c
src/http/modules/ngx_http_split_clients_module.c
src/http/modules/ngx_http_referer_module.c
src/http/modules/ngx_http_rewrite_module.c
src/http/modules/ngx_http_proxy_module.c
src/http/modules/ngx_http_fastcgi_module.c
src/http/modules/ngx_http_uwsgi_module.c
src/http/modules/ngx_http_scgi_module.c
src/http/modules/ngx_http_memcached_module.c
src/http/modules/ngx_http_empty_gif_module.c
src/http/modules/ngx_http_browser_module.c
src/http/modules/ngx_http_upstream_ip_hash_module.c
src/http/modules/ngx_http_upstream_keepalive_module.c
objs/ngx_modules.c
)

include_directories(
src/core
src/event
src/event/modules
src/os/unix
objs
src/http
src/http/modules
src/mail
src/core
)
add_executable(nginx ${SRC})
target_link_libraries(nginx pthread crypt pcre crypto z)

是不是感觉太简洁了?没错,就是这么简单。将这个 txt 放在项目的根目录下。

接下来执行 configure,还是之前的选项(注意修改路径),然后进入 objs 目录,执行 cmake .. 生成 makefile,最后 make 生成可执行文件,一切顺利!
此时我们用 CLion 打开该项目(如果之前已经打开过,记得删除项目根目录下的 .idea 文件,避免项目缓存而检测不到 CMakeLists.txt),可以发现此时 CLion 就是使用 cmake 来管理项目的:

然后点击右上角的运行图标,报错如下:
image-20230519195155896

这是提示权限不够,我们使用管理员权限来运行,修改步骤如下:
image-20230519195504657

然后运行,成功!

再试试调试,效果不错!
image-20230519195719650

而且我们可以直接在 Console 中断程序来看目前执行流阻塞在哪,下图指出程序阻塞在 epoll_wait 中:

笔者认为这种方式是最优雅的方式,读者朋友喜欢哪一种呢?


VS调试Nginx

参考Windows编译NginxVS调试Nginx


注意事项

有人发现 CLion 能够直接为外部导入的项目生成 CMakeLists.txt,如下:

自动创建 CMakeLists.txt 会将项目中的所有 .h 和 .c 文件都包含进来,这是不应该的! 我们应该根据执行 ./configure 后产生的 makefile 来按需引入 .c 和 .h 文件,即根据配置来引入,这是因为可能你明明指定了不需要某个功能,但却引入了相关的文件,这会在编译时产生大量报错,令人头疼不已!


总结

  • 查看 README,了解项目功能、选项、依赖。
  • 运行 ./configure --help 来查看编译选项。
  • 项目编译出错时,看看配置是否对的上,比如你没有配置某个功能,却引入了相关的源文件。
  • 一些莫名奇妙的问题多问问 google 。