未定义操作
zig 本身有许多未定义行为,它们可以很方便地帮助开发者找出错误。
如果在编译期就检测到了未定义的行为,那么 zig 会发出编译错误并停止继续编译,大多数编译时无法检测到的未定义行为均会在运行时被检测到。这就是 zig 的安全检查!
WARNING
注意:本章节并没有 CI 检查,故可能存在内容过期的情况,具体可参考 官方手册。
安全检查会在debug、ReleaseSafe 模式下开启,但可以使用 @setRuntimeSafety
来强制指定在单独的块中是否开启安全检查(这将忽略构建模式)。
当出现安全检查失败时,zig 会编译失败并触发堆栈跟踪:
test "safety check" {
unreachable;
}
$ zig test test_undefined_behavior.zig
1/1 test.safety check... thread 892159 panic: reached unreachable code
/home/ci/actions-runner/_work/zig-bootstrap/zig/docgen_tmp/test_undefined_behavior.zig:2:5: 0x222c65 in test.safety check (test)
unreachable;
^
/home/ci/actions-runner/_work/zig-bootstrap/out/host/lib/zig/test_runner.zig:181:28: 0x22da7d in mainTerminal (test)
} else test_fn.func();
^
/home/ci/actions-runner/_work/zig-bootstrap/out/host/lib/zig/test_runner.zig:36:28: 0x223c8a in main (test)
return mainTerminal();
^
/home/ci/actions-runner/_work/zig-bootstrap/out/host/lib/zig/std/start.zig:575:22: 0x22319c in posixCallMainAndExit (test)
root.main();
^
/home/ci/actions-runner/_work/zig-bootstrap/out/host/lib/zig/std/start.zig:253:5: 0x222cf1 in _start (test)
asm volatile (switch (native_arch) {
^
???:?:?: 0x0 in ??? (???)
error: the following test command crashed:
/home/ci/actions-runner/_work/zig-bootstrap/out/zig-local-cache/o/4558e134302b78f1a543202d094b3e06/test
以下说明各种未定义行为。
不可达代码
即 unreachable
,如果程序执行到它,那么会触发完整的堆栈跟踪!
索引越界访问
无论是数组还是切片,发生越界访问会发生错误导致程序终止进而触发堆栈跟踪!
负数转换为无符号整数
这本身就是非法行为,故会直接出现报错,如果仅仅是想要将负数当作无符号整数看待,可以使用 @bitCast
。
如果想要获取到无符号整数的最大值,可以使用 std.math.maxInt
。
数据截断
注意我们这里指的是数据类型的范围变小了,不足以容纳数据的值,例如:
const spartan_count: u16 = 300;
const byte: u8 = @intCast(spartan_count);
上面这段代码毫无疑问会失败,因为 u8
类型无法容纳下 300 这个数。
除非,我们显式强制截断位,使用 @truncate
。
整数溢出
常规的运算可能导致溢出,如加 +
减 -
乘 *
除 /
取反 -
运算可能出现溢出。
还有 @divTrunc
、@divFloor
、@divExact
,可能造成溢出。
标准库提供的函数可能存在溢出:
@import("std").math.add
@import("std").math.sub
@import("std").math.mul
@import("std").math.divTrunc
@import("std").math.divFloor
@import("std").math.divExact
@import("std").math.shl
为了处理这些情况,zig 提供了几个溢出检测函数来处理溢出问题:
以上这些内建函数会返回一个元组,包含计算的结果和是否发生溢出的判断位。
const print = @import("std").debug.print;
pub fn main() void {
const byte: u8 = 255;
const ov = @addWithOverflow(byte, 10);
if (ov[1] != 0) {
print("overflowed result: {}\n", .{ov[0]});
} else {
print("result: {}\n", .{ov[0]});
}
}
除此以外,我们还可以使用环绕(Wrapping)操作来处理计算:
+%
加法环绕-%
减法环绕-%
取否环绕*%
乘法环绕
它们会取计算后溢出的值!
移位溢出
进行左移操作时,可能导致结果溢出,此时程序或者编译器会停止并发出警告!
除零操作
很显然,除零是非法操作,故会引起程序或者编译器报错!
当然,还包括求余运算,除数为零是也是非法的!
精确除法溢出
精确除法使用的是 @divExact
,它需要保证被除数可以整除除数,否则会触发编译器错误!
尝试解开可选类型 Null
可选类型值是 null
时,如果直接使用 variable.?
语法来解开可选,那么会导致出现错误!
正确的处理方案是使用 if
语法来解开可选类型。
尝试解开错误联合类型 Error
错误联合类型如果是 error
时,直接使用它会导致程序或者编译器停止运行!
正确的处理方案是使用 if
语法来解开可选类型。
无效错误码
使用 @errorFromInt
获取错误时,如果没有对应整数的错误,那么会导致程序或编译器报错!
无效枚举转换
当使用 @enumFromInt
来获取枚举时,如果没有对应整数的枚举,那么会导致程序或者编译器报告错误!
无效错误集合转换
两个不相关的错误集不可以相互转换,如果强制使用 @errorCast
转换两个不相关的错误集,那么会导致程序或者编译器报告错误!
指针对齐错误
指针对齐转换可能发生错误,如:
const ptr: *align(1) i32 = @ptrFromInt(0x1);
const aligned: *align(4) i32 = @alignCast(ptr);
0x1
地址很明显是不符合 4 字节对齐,会导致编译器错误。
联合类型字段访问错误
如果访问的联合类型字段并非是它当前的有效字段,那么会触发非法行为!
可以通过重新分配来更改联合类型的有效字段:
const Foo = union {
float: f32,
int: u32,
};
var f = Foo{ .int = 42 };
f = Foo{ .float = 12.34 };
🅿️ 提示
注意:packed 和 extern 标记的联合类型并没有这种安全监测!
浮点转换整数发生越界
当将浮点数转换为整数时,如果浮点数的值超出了整数类型的范围,就会发生非法越界,例如:
const float: f32 = 4294967296;
const int: i32 = @intFromFloat(float);
指针强制转换为 Null
将允许地址为 0 的指针转换为地址不可能为 0 的指针,这会触发非法行为。
C 指针、可选指针、allowzero
标记的指针,这些都是允许地址为 0,但普通指针是不允许的。