本文档收集了有关调试 Apache httpd 及其模块的工具和技术的笔记。
有更多技巧吗?请发送到 docs@httpd.apache.org。谢谢!
如果您使用 gcc 编译器,那么您的系统上最好的调试器可能是 gdb。这只是关于如何在 Apache 上运行 gdb 的简要总结 - 您应该查看 gdb 的 info 和 man 文件,以获取有关 gdb 命令和常见调试技术的更多信息。在运行 gdb 之前,请确保服务器在 CFLAGS 中使用 -g 选项编译,以便在目标文件中包含符号信息。
在 Apache 上运行 gdb 的唯一棘手部分是强制服务器进入单进程模式,以便调试的父进程执行请求处理工作,而不是派生子进程。为此,我们提供了 -X 选项,它在大多数情况下都能正常工作。但是,某些模块不喜欢使用 -X 启动,但如果您强制只运行一个子进程(使用“MaxClients 1”),它们会很高兴;然后您可以使用 gdb 的 attach 命令来调试子服务器。
以下示例,其中用户输入以绿色显示,显示了在当前工作目录中运行的服务器可执行文件 (httpd) 上运行 gdb 的输出,并使用 /usr/local/apache 作为服务器根目录
% gdb httpd
GDB is free software and you are welcome to distribute copies of it
under certain conditions; type "show copying" to see the conditions.
There is absolutely no warranty for GDB; type "show warranty" for
details.
GDB 4.16.gnat.1.13 (sparc-sun-solaris2.5),
Copyright 1996 Free Software Foundation, Inc...
(gdb) b ap_process_request
Breakpoint 1 at 0x49fb4: file http_request.c, line 1164.
(gdb) run -X -d /usr/local/apache
Starting program: /usr/local/apache/src/httpd -X -d /usr/local/apache
[ at this point I make a request from another window ]
Breakpoint 1, ap_process_request (r=0x95250) at http_request.c:1164
1164 if (ap_extended_status)
(gdb) s
1165
ap_time_process_request(r->connection->child_num,...
(gdb) n
1167 process_request_internal(r);
(gdb) s
process_request_internal (r=0x95250) at http_request.c:1028
1028 if (!r->proxyreq && r->parsed_uri.path) {
(gdb) s
1029 access_status = ap_unescape_url(r->parsed_uri.path);
(gdb) n
1030 if (access_status) {
(gdb) s
1036 ap_getparents(r->uri); /* OK...
(gdb) n
1038 if ((access_status = location_walk(r))) {
(gdb) n
1043 if ((access_status = ap_translate_name(r))) {
(gdb) n
1048 if (!r->proxyreq) {
(gdb) n
1053 if (r->method_number == M_TRACE) {
(gdb) n
1062 if (r->proto_num > HTTP_VERSION(1,0) &&
ap_...
(gdb) n
1071 if ((access_status = directory_walk(r))) {
(gdb) s
directory_walk (r=0x95250) at http_request.c:288
288 core_server_config *sconf = ap_get_module_...
(gdb) b ap_send_error_response
Breakpoint 2 at 0x47dcc: file http_protocol.c, line 2090.
(gdb) c
Continuing.
Breakpoint 2, ap_send_error_response (r=0x95250, recursive_error=0)
at http_protocol.c:2090
2090 BUFF *fd = r->connection->client;
(gdb) where
#0 ap_send_error_response (r=0x95250, recursive_error=0)
at http_protocol.c:2090
#1 0x49b10 in ap_die (type=403, r=0x95250) at http_request.c:989
#2 0x49b60 in decl_die (status=403, phase=0x62db8 "check access",
r=0x95250)
at http_request.c:1000
#3 0x49f68 in process_request_internal (r=0x95250) at
http_request.c:1141
#4 0x49fe0 in ap_process_request (r=0x95250) at http_request.c:1167
#5 0x439d8 in child_main (child_num_arg=550608) at http_main.c:3826
#6 0x43b5c in make_child (s=0x7c3e8, slot=0, now=907958743)
at http_main.c:3898
#7 0x43ca8 in startup_children (number_to_start=6) at http_main.c:3972
#8 0x44260 in standalone_main (argc=392552, argv=0x75800) at
http_main.c:4250
#9 0x449fc in main (argc=4, argv=0xefffee8c) at http_main.c:4534
(gdb) s
2091 int status = r->status;
(gdb) p status
$1 = 403
(gdb)
关于上面的示例,需要注意几点
“gdb httpd”命令不包含任何 httpd 的命令行选项:这些选项是在 gdb 中执行“run”命令时提供的;
我在启动运行之前设置了一个断点,以便执行会在 ap_process_request() 的顶部停止;
“s”命令单步执行代码并进入调用的过程,而“n”(下一步)命令单步执行代码但不进入调用的过程。
可以使用“b”命令设置其他断点,并使用“c”命令继续运行。
使用“where”命令(又名“bt”)查看堆栈跟踪,该跟踪显示调用的过程的顺序及其参数值。
使用“p”命令打印变量的值。
根目录中的一个名为 .gdbinit 的文件提供了有用的宏,用于打印 httpd 的各种内部结构,如表 (dump_table)、旅 (dump_brigade) 和过滤器链 (dump_filters)。
如果您正在调试可重复的崩溃,只需按上述方式运行 gdb 并发出请求 - gdb 应该捕获崩溃并提供一个提示,提示崩溃发生的位置。
如果您正在调试明显的无限循环,只需按上述方式运行 gdb 并键入 Control-C - gdb 将中断进程并提供一个提示,提示它停止的位置。
如果您正在调试系统崩溃,并且您有崩溃时的核心文件,那么请执行以下操作
% gdb httpd -c core
(gdb) where
它将(希望)打印出核心转储在处理过程中发生时的堆栈跟踪。
堆栈跟踪将让您了解为到达进程中的特定点而调用的过程的层次结构。在某些平台上,您可以获取任何进程的实时堆栈跟踪。
对于基于 SVR4 的 Unix 变体,可以使用 proc 的 pstack 命令来显示实时堆栈跟踪。例如,在 Solaris 上,它看起来像
% /usr/proc/bin/pstack 10623
10623: httpd -d /usr/local/apache
ef5b68d8 poll (efffcd08, 0, 3e8)
ef5d21e0 select (0, ef612c28, 0, 0, 3e8, efffcd08) + 288
00042574 wait_or_timeout (0, 75000, 75000, 7c3e8, 60f40, 52c00) + 78
00044310 standalone_main (5fd68, 75800, 75c00, 75000, 2, 64) + 240
000449f4 main (3, efffeee4, efffeef4, 75fe4, 1, 0) + 374
000162fc _start (0, 0, 0, 0, 0, 0) + 5c
另一种技术是使用 gdb 附加到正在运行的进程,然后使用“thread apply all bt”打印堆栈跟踪,例如
sudo gdb /path/to/bin/httpd 10623 --batch --eval-command "where" --eval-command "thread apply all bt" --eval-command "detach" --eval-command "quit" | tee gdb-output.txt
或交互式地
% gdb httpd 10623
GDB is free software and you are welcome to distribute copies of it
under certain conditions; type "show copying" to see the conditions.
There is absolutely no warranty for GDB; type "show warranty" for
details.
GDB 4.16.gnat.1.13 (sparc-sun-solaris2.5),
Copyright 1996 Free Software Foundation, Inc...
/usr/local/apache/src/10623: No such file or directory.
Attaching to program `/usr/local/apache/src/httpd', process 10623
Reading symbols from /usr/lib/libsocket.so.1...done.
...
0xef5b68d8 in ()
(gdb) where
#0 0xef5b68d8 in ()
#1 0xef5d21e8 in select ()
#2 0x4257c in wait_or_timeout (status=0x0) at http_main.c:2357
#3 0x44318 in standalone_main (argc=392552, argv=0x75800) at...
#4 0x449fc in main (argc=3, argv=0xefffeee4) at http_main.c:4534
(gdb) thread apply all bt
(gdb) detach
(gdb> quit
在 Apache2 根目录树中解压缩 -symbols.zip 文件(从 Apache 下载站点获取)(通常是 bin、htdocs、modules\ 等所在的位置)。这些 .pdb 文件应该与它们表示的 .exe、.dll、.so 二进制文件一起解压缩,例如,mod_usertrack.pdb 将与 mod_usertrack.so 一起解压缩。
可选:从 Sysinternals 获取 procdump。
调用 drwtsn32 并确保您正在创建崩溃转储文件,您正在转储所有线程上下文,您的日志和崩溃转储路径有意义,并且(取决于错误的性质)您选择适当的崩溃转储类型。(完整转储非常大,但有时对于程序员来说,需要将您的崩溃转储加载到调试器中并开始精确地解开发生了什么。迷你转储足以进行第一次尝试。)
您可以在此阶段使用 procdump 或 procdump -ma。
请注意,如果您之前安装了其他调试软件,然后又卸载了,您可能需要调用 drwtsn32 -i 以将 Dr Watson 设置为默认的崩溃转储工具。这将替换“向 MS 报告问题”对话框。(如果您在机器上安装了完整的调试器,例如 Visual Studio 或 windbg,请不要这样做,除非您备份 HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug 注册表树下 Debugger 的注册表值。使用多个工具的开发人员可能希望保留其不同工具 Debugger 条目的副本,以便快速切换。)
调用任务管理器,选择“显示所有用户的进程”,并将“视图 -> 选择列”修改为至少包含 PID 和 线程数。您只需更改一次,任务管理器应该保留您的首选项。
现在,找到有问题的 Apache 进程,它正在挂起。父进程大约有三个线程,我们不关心那个。我们想要的子工作进程有更多线程(比您使用 ThreadsPerChild 指令配置的线程多几个)。进程名称是 Apache(对于 1.3 和 2.0)或 httpd(对于 2.2 和 2.4)。记下子工作进程的 PID。
使用您上面记下的 {pid} 号,调用以下命令
drwtsn32 -p {pid}或procdump {pid}
瞧,您将在您的“日志文件路径”中找到一个 drwtsn32.log 文件,如果您选择“追加到现有日志文件”,请跳过“App:”部分,直到找到您刚刚杀死的进程的“App:”部分。现在,您可以识别“堆栈跟踪”指向的大致位置,以帮助识别服务器正在做什么。
您会注意到,许多线程看起来相同,几乎所有线程都在轮询下一个连接,我们不关心这些线程。您需要查看在您杀死它们时处于请求深处的线程,以及这些线程的堆栈跟踪条目。这可以让人们了解该请求在哪里挂起,哪个处理程序模块接管了该请求,以及它可能卡在哪个过滤器中。
对于子进程间歇性崩溃的情况,必须配置和启动服务器,以便它生成核心转储,以便进一步分析。
为了确保核心转储被写入子进程运行的用户(例如 apache)可写的目录,必须将 指令添加到 httpd.conf 中;例如
CoreDumpDirectory /tmp
在启动服务器之前,必须解除对核心转储文件大小的任何进程限制;例如
# ulimit -c unlimited
# apachectl start
在某些平台上,可能需要采取进一步的步骤来启用核心转储 - 请参阅下面的 Solaris 和核心转储。
当子进程崩溃时,类似以下内容的消息将被记录到 error_log 中
[Mon Sep 05 13:35:39 2005] [notice] child pid 2027 exit signal Segmentation
fault (11), possible coredump in /tmp
如果错误行中没有出现“可能的核心转储在 /tmp 中”的文本,请检查 ulimit 是否设置正确,配置的 CoreDumpDirectory 上的权限是否合适,以及是否已完成平台特定的步骤(Solaris 和核心转储)。
要分析核心转储,请在 gdb 命令行上传递核心转储文件名,并在 gdb 提示符下输入命令 thread apply all bt full
% gdb /usr/local/apache2/bin/httpd /tmp/core.2027
...
Core was generated by `/usr/local/apache2/bin/httpd -k start'
...
(gdb) thread apply all bt full
大多数基于 Unix 的系统至少有一个命令,用于显示正在运行的进程访问系统调用和信号的跟踪。此命令在大多数基于 SVR4 的系统上称为 truss,在许多其他系统上称为 trace 或 strace。
在 Solaris 上使用 truss 命令的一个有用技巧是 -f 选项(通常也适用于 strace);它告诉 truss 跟踪并继续跟踪主进程派生的任何子进程。获取服务器完整跟踪的最简单方法是执行以下操作
% truss -f httpd -d /usr/local/apache >& outfile
% egrep '^10698:' outfile
以查看仅进程 ID 10698 的跟踪。
如果尝试跟踪线程服务器,例如使用 worker MPM 时,truss 选项 -l 非常有用,因为它还在进程 ID 后打印 LWP ID。您可以使用以下命令
% egrep '^10698/1:' outfile
以查看仅进程 ID 10698 和 LWP ID 1 的跟踪。
truss 的其他有用选项是
-a 打印用于此可执行文件的所有命令行参数。
-e 打印用于此可执行文件的所有环境变量。
-d 打印时间戳。
奇怪的是,有时您实际上希望强制服务器崩溃,以便您可以查看一些奇怪的行为。通常,这可以通过简单地使用 gcore 命令来完成。但是,出于安全原因,大多数 Unix 系统不允许 setuid 进程转储核心,因为文件内容可能会泄露内存中应该受到保护的内容。
以下是一种从 Solaris 上的 setuid Apache httpd 进程获取核心转储文件的方法,而无需知道哪个 httpd 子进程可能会崩溃 [注意:使用上面第一部分中的 MaxClients 技巧可能更容易]。
# for pid in `ps -eaf | fgrep httpd | cut -d' ' -f4`
do
truss -f -l -t\!all -S SIGSEGV -p $pid 2>&1 | egrep SIGSEGV
&
done
truss 的 未公开的 '-S' 标志 会在收到特定信号(本例中为 SIGSEGV)时立即停止进程。此时,您可以使用
# gcore PID
然后查看上面讨论的 gdb 的回溯。
在 Solaris 上,使用 使 setuid() 进程实际转储核心。默认情况下,setuid() 进程不会转储核心。这就是为什么以 root 身份启动的 httpd 服务器,其子进程以不同的用户(例如 apache)身份运行,即使 CoreDumpDirectory 指令已设置为适当的可写目录,并且 ulimit -c 具有足够的大小,也不会转储核心。另请参见上面讨论的 调试间歇性崩溃。
示例
-bash-3.00# coreadm
global core file pattern: /var/core/core.%f.%p.u%u
global core file content: default
init core file pattern: core
init core file content: default
global core dumps: disabled
per-process core dumps: enabled
global setid core dumps: enabled
per-process setid core dumps: enabled
global core dump logging: disabled
这个主题太深奥,无法在本文档中完全描述。以下是一些指向有用讨论和工具的指针