5  调试 Zig 应用程序

对于任何想要使用任何语言进行严肃编程的程序员来说,能够调试应用程序都是至关重要的。因此,在本章中,我们将讨论调试用 Zig 编写的应用程序的可用策略和工具。

5.1打印调试

我们从经典且久经考验的_打印调试_策略开始。调试提供的关键优势是_可视性_。使用_打印语句,_您可以轻松查看应用程序生成的结果和对象。

_这是打印调试_的本质——使用打印表达式查看程序生成的值,从而更好地了解程序的行为方式。

许多程序员经常使用 Zig 中的打印功能,例如stdout.print()、 或std.debug.print(),来更好地理解他们的程序。这是一种众所周知的古老策略,非常简单有效,在编程社区中更广为人知的名称是_打印调试_。在 Zig 中,您可以将信息打印到系统的stdout或流中。stderr

让我们从 开始。首先,您需要通过调用Zig 标准库中的 方法stdout来访问。此方法返回一个_文件描述符_对象,您可以通过该对象读取/写入。我建议您通过查看 Zig 标准库官方参考中类型1 的页面,来查看此对象中可用的所有方法。stdout``getStdOut()``stdoutFile

就我们这里的目的而言,也就是向 写入一些内容stdout,尤其是为了调试我们的程序,我建议你使用writer()方法,它会返回一个_writer_对象。这个_writer_对象提供了一些辅助方法,可以将内容写入代表流的文件描述符对象stdout。具体来说,就是print()方法。

print()此_writer_对象中的方法是一个“打印格式化程序”类型的函数。换句话说,此方法的工作方式printf()与 C 语言或println!()Rust 语言中的函数完全相同。在函数的第一个参数中,指定一个模板字符串;在第二个参数中,提供要插入到模板消息中的值(或对象)列表。

理想情况下,第一个参数中的模板字符串应该包含一些格式说明符。每个格式说明符都与第二个参数中列出的值(或对象)匹配。因此,如果您在第二个参数中提供了 5 个不同的对象,那么模板字符串应该包含 5 个格式说明符,每个提供的对象对应一个。

每个格式说明符都由一个字母表示,并且需要用一对花括号括起来。因此,如果您想使用字符串说明符 ( s) 来格式化对象,则可以将文本插入{s}模板字符串中。以下是一些最常用的格式说明符的简要列表:

  • d:用于打印整数和浮点数。
  • c:用于打印字符。
  • s:用于打印字符串。
  • p:用于打印内存地址。
  • x:用于打印十六进制值。
  • any:使用任何兼容的格式说明符(即,它会自动为您选择格式说明符)。

print()下面的代码示例为您提供了使用格式说明符使用此方法的示例d

const std = @import("std");
const stdout = std.io.getStdOut().writer();
fn add(x: u8, y: u8) u8 {
    return x + y;
}

pub fn main() !void {
    const result = add(34, 16);
    try stdout.print("Result: {d}", .{result});
}
Result: 50

需要强调的是,stdout.print()正如您所期望的,该方法会将模板字符串打印到stdout系统流中。但是,stderr如果您愿意,也可以将模板字符串打印到流中。您只需将stdout.print()调用替换为函数即可std.debug.print()。如下所示:

const std = @import("std");
fn add(x: u8, y: u8) u8 {
    return x + y;
}

pub fn main() !void {
    const result = add(34, 16);
    std.debug.print("Result: {d}\n", .{result});
}
Result: 50

您还可以通过获取文件描述符对象stderr,然后创建_写入器_对象stderr,然后使用print()此_写入器_对象的方法来实现完全相同的结果,如下例所示:

const std = @import("std");
const stderr = std.io.getStdErr().writer();
// some more lines ...
try stderr.print("Result: {d}", .{result});

5.2通过调试器进行调试

虽然_打印调试_是一种有效且非常有用的策略,但大多数程序员更喜欢使用调试器来调试他们的程序。由于 Zig 是一种低级语言,因此您可以使用 GDB(GNU 调试器)或 LLDB(LLVM 项目调试器)作为调试器。

两种调试器都可以处理 Zig 代码,这取决于个人喜好。您可以选择自己喜欢的调试器,并使用它进行调试。在本书的示例中,我将使用 LLDB 作为调试器。

5.2.1在调试模式下编译源代码

为了通过调试器调试程序,您必须在Debug模式下编译源代码。因为当您在其他模式下(例如Release)编译源代码时,编译器通常会删除一些调试器用来读取和跟踪程序的基本信息,例如 PDB(程序数据库)文件。

通过以模式编译源代码Debug,可以确保调试器能够在程序中找到调试所需的信息。默认情况下,编译器会使用该Debug模式编译代码。考虑到这一点,当您使用命令(在1.2.4 节build-exe中描述)编译程序时,如果您未通过命令行2参数指定显式模式,那么编译器将以该模式编译代码。-ODebug

5.2.2让我们调试一个程序

作为示例,让我们使用 LLDB 来导航和调查以下 Zig 代码:

const std = @import("std");
const stdout = std.io.getStdOut().writer();

fn add_and_increment(a: u8, b: u8) u8 {
    const sum = a + b;
    const incremented = sum + 1;
    return incremented;
}

pub fn main() !void {
    var n = add_and_increment(2, 3);
    n = add_and_increment(n, n);
    try stdout.print("Result: {d}!\n", .{n});
}
Result: 13!

这个程序本身没有任何问题,但这对我们来说是一个好的开始。首先,我们需要使用zig build-exe命令编译这个程序。在本例中,假设我已经将上面的 Zig 代码编译成一个名为 的二进制可执行文件add_program

zig build-exe add_program.zig

现在,我们可以使用 启动 LLDB add_program,如下所示:

lldb add_program

从现在开始,LLDB 已启动,您可以通过查看前缀 知道我正在执行 LLDB 命令(lldb)。如果某个命令以 为前缀(lldb),那么您就知道它是一个 LLDB 命令。

我要做的第一件事,就是在main()函数中执行 ,设置一个断点b main。之后,我只需使用 即可开始执行程序run。您可以在下面的输出中看到,main()正如我们预期的那样,执行在函数的第一行停止了。

(lldb) b main
Breakpoint 1: where = debugging`debug1.main + 22
    at debug1.zig:11:30, address = 0x00000000010341a6
(lldb) run
Process 8654 launched: 'add_program' (x86_64)
Process 8654 stopped
* thread #1, name = 'add_program',
    stop reason = breakpoint 1.1 frame #0: 0x10341a6
    add_program`debug1.main at add_program.zig:11:30
   8    }
   9
   10   pub fn main() !void {
-> 11       var n = add_and_increment(2, 3);
   12       n = add_and_increment(n, n);
   13       try stdout.print("Result: {d}!\n", .{n});
   14   }

我可以开始浏览代码,并检查正在生成的对象。如果您不熟悉 LLDB 中可用的命令,我建议您阅读该项目的官方文档3。您还可以查找速查表,其中快速描述了所有可用的命令4

目前,我们位于main()函数的第一行。在这一行,我们n通过执行add_and_increment()函数来创建对象。要执行当前代码行并转到下一行,我们可以运行nLLDB 命令。让我们执行这个命令。

n执行此行后,我们还可以使用LLDB 命令查看此对象中存储的值p。此命令的语法为p <name-of-object>

如果我们查看n对象(p n)中存储的值,会发现它存储的是十六进制值0x06,即十进制数 6。我们还可以看到,该值的类型为,它是一个无符号 8 位整数。我们已经在1.8 节unsigned char中讨论过这一点,Zig 中的整数等同于 C 数据类型。u8``unsigned char

(lldb) n
Process 4798 stopped
* thread #1, name = 'debugging',
    stop reason = step over frame #0: 0x10341ae
    debugging`debug1.main at debug1.zig:12:26
   9
   10   pub fn main() !void {
   11       var n = add_and_increment(2, 3);
-> 12       n = add_and_increment(n, n);
   13       try stdout.print("Result: {d}!\n", .{n});
   14   }
(lldb) p n
(unsigned char) $1 = '\x06'

现在,在下一行代码中,我们add_and_increment()再次执行该函数。为什么不进入这个函数内部呢?可以吗?我们可以通过执行sLLDB 命令来实现。请注意,在下面的示例中,执行此命令后,我们进入了该函数的上下文add_and_increment()

还要注意,在下面的例子中,我在函数主体中又走了两行,然后,我执行frame variableLLDB 命令,立即查看在当前范围内创建的每个变量中存储的值。

您可以在下面的输出中看到,该对象sum存储了值\f,该值表示_换页_符。在 ASCII 表中,该字符对应的十六进制值0x0C,或者用十进制表示为数字 12。因此,这意味着在第 5 行执行的表达式的结果a + b为数字 12。

(lldb) s
Process 4798 stopped
* thread #1, name = 'debugging',
    stop reason = step in frame #0: 0x10342de
    debugging`debug1.add_and_increment(a='\x02', b='\x03')
    at debug1.zig:4:39
-> 4    fn add_and_increment(a: u8, b: u8) u8 {
   5        const sum = a + b;
   6        const incremented = sum + 1;
   7        return incremented;
(lldb) n
(lldb) n
(lldb) frame variable
(unsigned char) a = '\x06'
(unsigned char) b = '\x06'
(unsigned char) sum = '\f'
(unsigned char) incremented = '\x06'

5.3如何调查对象的数据类型

由于 Zig 是一种强类型语言,因此与对象关联的数据类型对于程序非常重要。因此,调试与对象关联的数据类型对于了解程序中的错误和错误可能很重要。

当您使用调试器运行程序时,只需使用 LLDBp命令将对象类型打印到控制台即可检查对象类型。此外,您还可以使用语言本身内置的替代方法来访问对象的数据类型。

在 Zig 中,您可以使用内置函数检索对象的数据类型@TypeOf()。只需将此函数应用于对象,即可访问该对象的数据类型。

const std = @import("std");
const stdout = std.io.getStdOut().writer();
const expect = std.testing.expect;

pub fn main() !void {
    const number: i32 = 5;
    try expect(@TypeOf(number) == i32);
    try stdout.print("{any}\n", .{@TypeOf(number)});
}
i32

该函数类似于type()Python的内置函数,或者typeofJavascript中的运算符。


  1. https://ziglang.org/documentation/master/std/#std.fs.File↩︎

  2. 参阅https://ziglang.org/documentation/master/#Debug。↩︎

  3. https://lldb.llvm.org/ ↩︎

  4. https://gist.github.com/ryanchang/a2f738f0c3cc6fbd71fa↩︎