第 20 章 调试器 (ocamldebug)

本章介绍 OCaml 源代码级回放调试器 ocamldebug.

Unix:  调试器在提供 BSD 套接字的 Unix 系统上可用。
Windows:  调试器在 OCaml 的 Cygwin 移植版下可用,但在本机 Win32 移植版下不可用。

1 为调试编译

在使用调试器之前,必须使用 -g 选项编译并链接程序:所有程序中包含的 .cmo.cma 文件都应该使用 ocamlc -g 创建,并且必须使用 ocamlc -g 将它们链接在一起。

使用 -g 编译不会对程序的运行时间造成任何损失:目标文件和字节码可执行文件更大,生成时间也更长,但可执行文件运行速度与未使用 -g 编译时完全相同。

2 调用

2.1 启动调试器

通过运行程序 ocamldebug 并将字节码可执行文件名作为第一个参数调用 OCaml 调试器。

        ocamldebug [options] program [arguments]

紧跟 program 后的参数是可选的,并作为命令行参数传递给正在调试的程序。(另请参见 set arguments 命令。)

识别以下命令行选项

-c count
将同时存在的活动检查点的最大数量设置为 count
-cd dir
从工作目录 dir 运行调试器程序,而不是当前目录。(另请参见 cd 命令。)
-emacs
告诉调试器它是在 Emacs 下执行的。(有关如何在 Emacs 下运行调试器的信息,请参见第 ‍20.10 节。)
-I directory
directory 添加到用于搜索源文件和已编译文件的目录列表中。(另请参见 directory 命令。)
-s socket
使用 socket 与正在调试的程序进行通信。有关 socket 的格式,请参见命令 set socket 的说明(第 ‍20.8.8 节)。
-version
打印版本字符串并退出。
-vnum
打印简短的版本号并退出。
-help--help
显示简短的使用摘要并退出。

2.2 初始化文件

启动时,调试器将在将控制权交给用户之前从初始化文件中读取命令。如果存在,默认文件为当前目录中的 .ocamldebug,否则为用户主目录中的 .ocamldebug

2.3 退出调试器

命令 quit 退出调试器。您也可以通过键入文件结束符(通常为 ctrl-D)来退出调试器。

键入中断符(通常为 ctrl-C)不会退出调试器,但会终止任何正在进行的调试器命令的操作并返回到调试器命令级别。

3 命令

调试器命令是单行输入。它以命令名称开头,后面跟着取决于此名称的参数。示例

        run
        goto 1000
        set arguments arg1 arg2

只要没有歧义,命令名称就可以截断。例如,go 1000 被理解为 goto 1000,因为没有其他命令的名称以 go 开头。对于最常用的命令,允许使用有歧义的缩写。例如,r 代表 run,尽管还有其他命令以 r 开头。您可以使用 help 命令测试缩写的有效性。

如果前一个命令已成功执行,则空行(仅键入 RET)将重复它。

3.1 获取帮助

OCaml 调试器具有一个简单的在线帮助系统,它提供每个命令和变量的简要说明。

help
打印命令列表。
help command
提供有关命令 command 的帮助。
help set variable, help show variable
提供有关变量 variable 的帮助。可以使用 help set 获取所有调试器变量的列表。
help info topic
提供有关 topic 的帮助。使用 help info 获取已知主题的列表。

3.2 访问调试器状态

set variable value
将调试器变量 variable 设置为值 value
show variable
打印调试器变量 variable 的值。
info subject
提供有关给定主题的信息。例如,info breakpoints 将打印所有断点的列表。

4 执行程序

4.1 事件

事件是源代码中的“有趣”位置,对应于“有趣”子表达式的求值开始或结束。事件是单步执行的单位(单步执行到程序执行中遇到的下一个或上一个事件)。此外,断点只能在事件处设置。因此,事件在传统语言的调试器中扮演着行号的角色。

在程序执行期间,在遇到的每个事件处都会递增一个计数器。此计数器的值被称为当前时间。借助于逆向执行,可以跳转到执行过程中的任何时间点。

以下是在源代码中调试器事件(用 ⋈ 表示)的位置

异常:函数应用后跟函数返回被编译器替换为跳转(尾调用优化)。在这种情况下,在函数应用之后不会放置事件。

4.2 启动正在调试的程序

调试器仅在需要时才开始执行正在调试的程序。这允许在执行开始之前设置断点或分配调试器变量。启动执行有几种方法

run
运行程序,直到遇到断点或程序终止。
goto 0
加载程序并在第一个事件处停止。
goto time
加载程序并执行到给定时间。当您已经知道问题大约在什么时间出现时很有用。也可以用来在时间 0 尚未计算的函数值上设置断点(参见第 20.5 节)。

程序的执行会受到调试器启动它时接收到的某些信息的影响,例如程序的命令行参数及其工作目录。调试器提供命令来指定这些信息(set argumentscd)。这些命令必须在程序执行开始之前使用。如果您尝试在启动程序后更改参数或工作目录,调试器将终止程序(在征求确认后)。

4.3 运行程序

以下命令从当前时间开始向前或向后执行程序。执行将停止,要么由命令指定,要么遇到断点时停止。

run
从当前时间向前执行程序。在下一个断点或程序终止时停止。
reverse
从当前时间向后执行程序。主要用于转到当前时间之前遇到的最后一个断点。
step [count]
运行程序并在下一个事件处停止。带参数时,执行 count 次。如果 count 为 0,则运行程序直到程序终止或遇到断点。
backstep [count]
向后运行程序并在上一个事件处停止。带参数时,执行 count 次。
next [count]
运行程序并在下一个事件处停止,跳过函数调用。带参数时,执行 count 次。
previous [count]
向后运行程序并在上一个事件处停止,跳过函数调用。带参数时,执行 count 次。
finish
运行程序直到当前函数返回。
start
向后运行程序并在当前函数调用之前的第一个事件处停止。

4.4 时间旅行

您可以使用 goto 命令直接跳转到给定时间,而不会在断点处停止。

当您遍历程序时,调试器会维护一个您停止时的连续时间的历史记录。可以使用 last 命令重新访问这些时间:每个 last 命令会沿着历史记录向后移动一步。这主要用于撤消诸如 stepnext 之类的命令。

goto time
跳转到给定时间。
last [count]
返回执行历史记录中记录的最新时间。带参数时,执行 count 次。
set history size
设置执行历史记录的大小。

4.5 终止程序

kill
终止正在执行的程序。此命令主要用于在不退出调试器的情况下重新编译程序。

5 断点

断点会导致程序在达到程序中的特定点时停止。可以使用 break 命令通过多种方式设置断点。断点在设置时会被分配数字,以便于以后引用。设置断点的最方便方法是通过 Emacs 界面(参见第 20.10 节)。

break
在程序执行的当前位置设置断点。当前位置必须在事件上(即,既不在程序的开头,也不在程序的结尾)。
break function
function 的开头设置断点。这仅在标识符 function 的函数值已经计算并分配给标识符时才有效。因此,此命令不能在程序执行的最开始使用,此时所有标识符都尚未定义;使用 goto time 来推进执行,直到函数值可用。
break @ [module] line
在模块 module(如果没有给出,则在当前模块中)的第 line 行的第一个事件处设置断点。
break @ [module] line column
在模块 module(如果没有给出,则在当前模块中)的第 line 行、第 column 列最接近的事件处设置断点。
break @ [module] # character
在模块 module 中最接近字符号 character 的事件处设置断点。
break frag:pc, break pc
在代码地址 frag:pc 处设置断点。整数 frag 是代码片段的标识符,即一组模块,这些模块已同时加载,要么最初加载,要么使用 Dynlink 模块加载。整数 pc 是此代码片段中的指令计数器。如果省略 frag,则默认为 0,即最初加载的程序的代码片段。
delete [breakpoint-numbers]
删除指定的断点。如果没有参数,则删除所有断点(在征求确认后)。
info breakpoints
打印所有断点的列表。

6 调用栈

每次程序执行函数应用时,它都会将应用的位置(返回地址)保存在一个称为栈帧的数据块中。该帧还包含调用者的局部变量。所有帧都分配在一个称为调用栈的内存区域中。命令 backtrace(或 bt)显示调用栈的部分内容。

在任何时候,调试器都会“选择”其中一个栈帧;几个调试器命令会隐式地引用所选的帧。特别是,当您要求调试器给出局部变量的值时,该值会在所选的帧中找到。命令 frameupdown 会选择您感兴趣的任何帧。

当程序停止时,调试器会自动选择当前正在执行的帧,并简要地描述它,就像 frame 命令所做的那样。

frame
描述当前选择的栈帧。
frame frame-number
按编号选择一个栈帧并描述它。程序停止时正在执行的帧编号为 0;其调用者编号为 1;依此类推,直到调用栈的顶端。
backtrace [count], bt [count]
打印调用栈。这有助于查看导致当前执行的帧的函数调用序列。带正参数时,只打印最里面的 count 个帧。带负参数时,只打印最外面的 -count 个帧。
up [count]
选择并显示“位于”所选帧之上的栈帧,即调用所选帧的帧。参数表示向上移动多少个帧。
down [count]
选择并显示“位于”所选帧之下的栈帧,即被所选帧调用的帧。参数表示向下移动多少个帧。

7 检查变量值

调试器可以打印简单表达式的当前值。表达式可以包含程序变量:在所选程序点范围内所有标识符都可以访问。

可以打印的表达式是 OCaml 表达式的一个子集,由以下语法描述

simple-expr::= lowercase-ident
  { capitalized-ident. } lowercase-ident
 *
 $integer
 simple-expr.lowercase-ident
 simple-expr.(integer)
 simple-expr.[integer]
 !simple-expr
 (simple-expr)

前两种情况指的是一个值标识符,可以是未限定的,也可以由定义它的结构的路径限定。* 指的是刚刚计算的结果(通常是函数应用的值),并且仅在选定事件是“after”事件(通常是函数应用)时有效。 $ integer 指的是之前打印的值。剩下的四种形式选择表达式的一部分:分别是记录字段、数组元素、字符串元素以及引用的当前内容。

print variables
打印给定变量的值。 print 可以缩写为 p
display 变量
print 相同,但将打印深度限制为 1。这对于浏览大型数据结构而不完全打印它们很有用。 display 可以缩写为 d

当打印一个复杂的表达式时,一个形如 $整数 的名称会自动分配给它的值。这种名称也会分配给由于超过最大打印深度而无法打印的值的一部分。命名值可以在稍后使用 p $整数d $整数 命令打印。命名值仅在程序停止时有效。一旦程序恢复执行,它们就会被遗忘。

set print_depth d
将值的打印限制为最大深度 d
set print_length l
将值的打印限制为最多打印 l 个节点。

8 控制调试器

8.1 设置程序名称和参数

set program 文件
将程序名称设置为 文件
set arguments 参数
参数 作为程序的命令行参数提供。

一个 shell 用于将参数传递给被调试的程序。因此,您可以在参数中使用通配符、shell 变量和文件重定向。为了调试从标准输入读取的程序,建议将它们从文件中重定向输入(使用 set arguments < input-file),否则程序的输入和调试器的输入不会被正确地分隔,并且输入在反向运行程序时不会被正确地重播。

8.2 程序的加载方式

loadingmode 变量控制程序的执行方式。

set loadingmode direct
程序由调试器直接运行。这是默认模式。
set loadingmode runtime
调试器在程序上执行 OCaml 运行时 ocamlrun。很少有用;此外,它会阻止调试在“自定义运行时”模式下编译的程序。
set loadingmode manual
用户在调试器的提示下手动启动程序。允许远程调试(参见第 ‍20.8.8 节)。

8.3 文件的搜索路径

调试器在目录列表(搜索路径)中搜索源文件和编译后的接口文件。搜索路径最初包含当前目录 . 和标准库目录。 directory 命令将目录添加到路径中。

每当搜索路径被修改时,调试器将清除它可能缓存的关于文件的任何信息。

directory 目录名
将给定的目录添加到搜索路径中。这些目录被添加到前面,因此将首先搜索它们。
directory 目录名 for 模块名
directory 目录名 相同,但给定的目录只会在查找已被打包到 模块名 中的模块的源文件时被搜索。
directory
重置搜索路径。这需要确认。

8.4 工作目录

每次在调试器中启动程序时,它都会从调试器的当前工作目录继承它的工作目录。这个工作目录最初是它从它的父进程(通常是 shell)继承的任何目录,但是您可以在调试器中使用 cd 命令或 -cd 命令行选项指定一个新的工作目录。

cd 目录
ocamldebug 的工作目录设置为 目录
pwd
打印 ocamldebug 的工作目录。

8.5 打开和关闭反向执行

在某些情况下,您可能希望关闭反向执行。这将加速程序执行,并且在某些情况下对交互式程序也很有用。

通常,调试器会不时地对程序状态进行检查点。也就是说,它会对程序的当前状态进行复制(使用 Unix 系统调用 fork)。如果变量 checkpoints 设置为 off,调试器将不会进行任何检查点。

set checkpoints on/off
选择调试器是否进行检查点。

8.6 调试器对 fork 的行为

当程序发出对 fork 的调用时,调试器可以跟踪子进程或父进程。默认情况下,调试器跟踪父进程。变量 follow_fork_mode 控制此行为

set follow_fork_mode child/parent
选择在调用 fork 时是跟踪子进程还是父进程。

8.7 在加载新代码时停止执行

调试器与 Dynlink 模块兼容。但是,当外部模块尚未加载时,无法在它的代码中设置断点。为了便于在动态加载的代码中设置断点,调试器会在每次加载新模块时停止程序。可以使用 break_on_load 变量禁用此行为

set break_on_load on/off
选择是否在加载新代码后停止。

8.8 调试器与程序之间的通信

调试器通过一个 Unix 套接字与被调试的程序进行通信。您可能需要更改套接字名称,例如如果您需要在机器上运行调试器,而您的程序在另一台机器上运行。

set socket 套接字
使用 套接字 与程序进行通信。 套接字 可以是文件名,也可以是 Internet 端口规范 主机:端口,其中 主机 是主机名或点符号表示的 Internet 地址,而 端口 是主机上的端口号。

在被调试的程序端,套接字名称通过 CAML_DEBUG_SOCKET 环境变量传递。

8.9 微调调试器

几个变量可以对调试器进行微调。提供了合理的默认值,您通常不需要更改它们。

set processcount 计数
将最大检查点数设置为 计数。更多的检查点便于回溯更久的时间,但会使用更多内存并创建更多 Unix 进程。

由于检查点相当昂贵,因此不能太频繁地执行。另一方面,当检查点更频繁地进行时,向后执行速度更快。特别是,当在当前时间之前进行了许多检查点时,向后单步执行更具响应性。为了微调检查点策略,调试器不会对长位移(例如 run)和短位移(例如 step)以相同的频率进行检查点。两个变量 bigstepsmallstep 包含这两种情况下两次检查点之间发生的事件数。

set bigstep 计数
为长位移设置两次检查点之间发生的事件数。
set smallstep 计数
为短位移设置两次检查点之间发生的事件数。

以下命令显示有关检查点和事件的信息

info checkpoints
打印检查点列表。
info events [模块]
打印给定模块中的事件列表(默认情况下是当前模块)。

8.10 用户定义的打印器

就像在顶层系统中一样(第 ‍14.2 节),用户可以注册函数来打印某些类型的值。由于技术原因,调试器无法调用位于被调试程序中的打印函数。因此,打印函数的代码必须在调试器中显式加载。

load_printer "文件名"
在调试器中加载指示的 .cmo.cma 对象文件。该文件在仅包含 OCaml 标准库和使用 load_printer 加载的对象文件提供的定义的环境中加载。如果此文件依赖于尚未加载的其他对象文件,则调试器会自动加载它们,如果它能够在搜索路径中找到它们。加载的文件无法直接访问被调试程序的模块。
install_printer 打印器名称
将名为 打印器名称(一个值路径)的函数注册为打印器,用于打印类型与函数参数类型匹配的对象。也就是说,当调试器需要打印这样的对象时,它将调用 打印器名称。打印函数 打印器名称 必须使用 Format 库模块来生成输出,否则它的输出将不会被正确地定位到顶层循环打印的值中。

值路径 打印器名称 必须引用使用 load_printer 加载的对象文件定义的函数之一。它不能引用被调试程序的函数。

remove_printer 打印器名称
从值打印器表中删除命名函数。

9 其他命令

list [模块] [开始] [结束]
列出模块 module 的源代码,从行号 beginning 到行号 end。默认情况下,会显示当前模块的 20 行代码,从当前位置的前 10 行开始。
source filename
从脚本 filename 中读取调试器命令。

10 在 Emacs 下运行调试器

使用调试器的最友好方式是在 Emacs 下运行它,使用通过 MELPA 获得的 OCaml 模式,也可以在 https://github.com/ocaml/caml-mode 获取。

在 Emacs 下,通过命令 M-x camldebug 启动 OCaml 调试器,参数为要调试的可执行文件 progname 的名称。与调试器的通信发生在名为 *camldebug-progname* 的 Emacs 缓冲区中。Shell 模式的编辑和历史功能可用于与调试器交互。

此外,Emacs 会显示包含当前事件(程序执行中的当前位置)的源文件,并突出显示事件的位置。此显示与调试器操作同步更新。

以下绑定用于最常用的调试器命令,可在 *camldebug-progname* 缓冲区中使用

C-c C-s
(命令 step): 将程序向前执行一步。
C-c C-k
(命令 backstep): 将程序向后执行一步。
C-c C-n
(命令 next): 将程序向前执行一步,跳过函数调用。
鼠标中键
(命令 display): 显示命名值。鼠标光标下的 $n (支持对大型数据结构的增量浏览)。
C-c C-p
(命令 print): 打印当前位置标识符的值。
C-c C-d
(命令 display): 显示当前位置标识符的值。
C-c C-r
(命令 run): 将程序向前执行到下一个断点。
C-c C-v
(命令 reverse): 将程序向后执行到最近的断点。
C-c C-l
(命令 last): 在命令历史记录中后退一步。
C-c C-t
(命令 backtrace): 显示函数调用的回溯。
C-c C-f
(命令 finish): 向前运行,直到当前函数返回。
C-c <
(命令 up): 选择当前帧下面的堆栈帧。
C-c >
(命令 down): 选择当前帧上面的堆栈帧。

在 OCaml 编辑模式下的所有缓冲区中,以下调试器命令也可用

C-x C-a C-b
(命令 break): 在最接近当前位置的事件处设置断点
C-x C-a C-p
(命令 print): 打印当前位置标识符的值
C-x C-a C-d
(命令 display): 显示当前位置标识符的值