未定義動作 (Undefined Behavior; UB) とは、その状態に陥るとプログラムがどのような挙動になるかわからないことを意味します。未定義動作の結果、プログラムがクラッシュすることもあれば、不正なメモリアクセスを繰り返しながら意図せぬ動作をする場合もあります。
Zenにはいくつかの未定義動作があります。C言語の未定義動作をご存知の方は悪夢の再来のように感じるかもしれませんが、Zenの未定義動作は安全性保護付き未定義動作です。安全性保護が意味するところは、未定義動作を起こす振る舞いがチェックされ、未定義動作が発見された場合には、Zenのパニック機構が働くということです。Zenのパニック機構は、定義された動作 (プロセスを終了するなど) を提供するため、プログラム全体としては動作が定義された状態であると言えます。
未定義動作に対する安全性保護を無効にすることも可能です。ReleaseFast
モードでのビルドは全ての安全性保護を無効にします。また、組込み関数の@setRuntimeSafety
により、ブロック単位で安全性保護を有効にするか無効にするかを制御できます。
未定義動作がコンパイル時に発見された場合は、コンパイルエラーになります。しかし、多くの未定義動作はコンパイル時に検出することができません。実行時に未定義動作が発生した場合、デフォルトではスタックトレースを出力し、プロセスが停止します。
まず、コンパイル時に発見されてコンパイルエラーになる例です。comptime_ub.zen
を次の内容で作成して下さい。後ほど説明しますが、unreachable
に到達することは未定義動作を引き起こします。次のコードは、incurUndefinedBehavior()
関数がコンパイル時に呼び出され、unreachable
に到達します。
comptime {
incurUndefinedBehavior();
}
fn incurUndefinedBehavior() void {
unreachable;
}
このcomptime_ub.zen
を次のコマンドでビルドします。
$ zen build-exe comptime_ub.zen
すると、次の通りコンパイルエラーが発生します。
comptime_ub.zen:6:5: error[E04000]: unable to evaluate constant expression
unreachable;
~
comptime_ub.zen:2:27: note[E00029]: called from here
incurUndefinedBehavior();
~
comptime_ub.zen:1:10: note[E00029]: called from here
comptime {
~
コンパイルエラー発生時、バックトレースが出力されるため、どのような経路で未定義動作に到達したか、がすぐに分かるようになっています。
続いて、実行時に未定義動作が発生し、パニックでプログラムが終了する例です。runtime_ub.zen
を次の内容で作成します。
pub fn main() void {
incurUndefinedBehavior();
}
fn incurUndefinedBehavior() void {
unreachable;
}
runtime_un.zen
のビルドは成功します。
$ zen build-exe runtime_ub.zen
しかし、実行ファイルを実行すると、未定義動作によりパニックが発生し、プログラムが終了します。
$ ./runtime_ub
reached unreachable code
examples/ch11-advanced/ub/src/runtime_ub.zen:6:5: 0x227a36 in incurUndefinedBehavior (runtime_ub)
unreachable;
^
examples/ch11-advanced/ub/src/runtime_ub.zen:2:27: 0x227958 in main (runtime_ub)
incurUndefinedBehavior();
^
zen/std/special/start.zen:129:22: 0x226a50 in std.special.posixCallMainAndExit (runtime_ub)
root.main();
^
zen/std/special/start.zen:56:5: 0x22695f in std.special._start (runtime_ub)
@noInlineCall(posixCallMainAndExit);
^
Aborted (core dumped)
結果的にプログラムはクラッシュしますが、この動作は定義された動作です。Zenの標準ライブラリ内にはデフォルトのパニックハンドラ (std/special/panic.zen
) が実装されています。OS上で動作するプログラムは、そのパニックハンドラからstd.debug.panicExtra
関数を呼び出し、その中でスタックトレースを出力し、std.os.abort
関数でプログラムを終了しています。パニックハンドラをカスタマイズすることも可能ですが、ここでは説明しません。
それでは、Zenに存在する未定義動作について、個々に説明します。コードの例として、目にする機会が多い、実行時に未定義動作が発生するものを多く紹介しています。コンパイル時に未定義動作が発生した場合は、エラーメッセージが異なるので注意して下さい。
プログラマが実行されることを想定していないコードを実行した時に発生する未定義動作です。
unreachable
は、到達しない (評価されない) コードを表すキーワードです。unreachable
は式として評価され、コンパイル時もしくは実行時に評価されると、未定義動作となります。
アサーションで利用するstd.debug.assert
関数は次の通り実装されており、引数がfalse
であればunreachable
に到達し、プログラムが停止するようになっています。
pub fn assert(ok: bool) void {
if (!ok) unreachable; // assertion failure
}
オプション型にnull
が格納されている状態で、.?
演算子でアンラップすると発生する未定義動作です。
pub fn main() void {
var optional: ?u32 = null;
var value = optional.?;
}
このコードを実行すると、実行時に未定義動作が発生し、プログラムが停止します。
$ zen run src/unwrap_null.zen
attempt to unwrap null
examples/ch11-advanced/ub/src/unwrap_null.zen:3:25: 0x22797c in main (run)
var value = optional.?;
^
# 中略
Aborted (core dumped)
この未定義動作を防止するには、if
を使ってnull
かどうかをチェックします。
var optional: ?u32 = null;
if (optional) |value| {
// use value here
} else {
// do something
}
不正なメモリアクセスを行った際に発生する未定義動作です。
配列やスライスに対して、範囲外の要素アクセスを行った場合、未定義動作となります。
コンパイル時に未定義動作になる例です。
test "comptime out of bounds" {
var array: [5]u8 = undefined;
array[10] = 0;
}
$ zen test src/index_out_of_bounds.zen --test-filter "comptime out of bounds"
index_out_of_bounds.zen:3:10: error[E04028]: index is out of bounds
array[10] = 0;
~
実行時に未定義動作になるコードです。
fn index(i: usize) u32 {
var a: [5]u32 = undefined;
return a[i];
}
test "runtime out of bounds" {
var i = @to(usize, 10);
_ = index(i);
}
$ zen test src/index_out_of_bounds.zen --test-filter "runtime out of bounds"
1/1 test "runtime out of bounds"...index out of bounds
examples/ch11-advanced/ub/src/index_out_of_bounds.zen:8:13: 0x20578a in index (test)
return a[i];
^
examples/ch11-advanced/ub/src/index_out_of_bounds.zen:13:14: 0x2055a8 in test "runtime out of bounds" (test)
_ = index(i);
^
# 中略
Tests failed. Use the following command to reproduce the failure:
算術演算やビット演算を行った際に発生する未定義動作です。
整数オーバーフローが発生する可能性がある演算には、次のものがあります。演算子の+
/ -
(二項) / -
(単項) / *
/ /
、を使う演算と、組込み関数の@divTrunc
/ @divFloor
/ @divExact
を使う演算です。
オーバーフローが発生する加算の例を示します。
pub fn main() void {
var byte: u8 = 255;
byte += 1;
}
このコードを実行すると、実行時に未定義動作が発生し、プログラムが停止します。
$ zen run src/integer_overflow.zen
integer overflow
examples/ch11-advanced/ub/src/integer_overflow.zen:3:10: 0x227980 in main (run)
byte += 1;
^
# 中略
Aborted (core dumped)
この未定義動作を防ぐ方法は3つあります。
std.math
には、次の関数があり、それぞれオーバーフローの発生をエラー (error.Overflow
) として返します。
add
関数のシグネチャはpub fn add(comptime T: type, a: T, b: T) (error{Overflow}!T)
となっています。引数はターゲットとする整数型、演算する2つの整数値で、戻り値はオーバーフローを表すエラーか演算結果が格納されるエラー共用体です。negate
を除く他の関数のシグネチャはadd
と同様です。negate
は整数値1つだけを引数に取ります。
test "standard library math functions" {
const math = std.math;
const byte: u8 = 255;
const result = math.add(u8, byte, 1);
err(error.Overflow, result);
}
これらの標準ライブラリの関数を使うと、次のようなコードでオーバーフロー発生時のエラーを処理することができます。
const math = std.math;
const byte: u8 = 255;
// エラーが発生していなければ、`byte`に`result`が格納される
byte = if (math.add(u8, byte, 1)) |result| result else |_| {
// ここでエラーを処理する
};
組込み関数には、オーバーフロー発生時にブール型を返す整数演算があります。
加算の例に取ると、関数シグネチャは@addWithOverflow(comptime T: type, a: T, b: T, result: *mut T) bool
です。引数に整数型、演算する2つの整数値に加えて、結果を格納するためのポインタを取ります。オーバーフローが発生した場合、この結果には、オーバーフローした後の値が格納されます。戻り値は、オーバーフローが発生した場合はtrue
が、それ以外ではfalse
が返ってきます。
test "builtin overflow functions" {
var byte: u8 = 0xFF; // 255
const overflow = @addWithOverflow(u8, byte, 5, &mut byte);
ok(overflow == true);
// 0xFF + 0x05 = 0x104だが、`u8`に格納できない`0x100`はクリアされている
ok(byte == 4);
}
これらの組込み関数を使うと、次のようなコードでオーバーフロー発生時のエラーを処理することができます。
var byte: u8 = 0xFF; // 255
if (@addWithOverflow(u8, byte, 5, &mut byte)) {
// ここでエラー処理をする
}
次の演算子を使用すると、結果がラップアラウンドすることが保証されます。
これらのラップアラウンド演算子を使うと、オーバーフローは発生しません。
test "wrapping operators" {
var byte: u8 = 255;
byte +%= 1;
ok(byte == 0);
}
/
で除算する際、もしくは%
で剰余を求める際、除数 (右辺オペランド) が0
の場合に発生する未定義動作です。
pub fn main() void {
var a: u32 = 0;
const result = 1 / a;
}
このコードを実行すると、実行時に未定義動作が発生し、プログラムが停止します。
$ zen run src/division_by_zero.zen
division by zero
examples/ch11-advanced/ub/src/division_by_zero.zen:3:22: 0x22797c in main (run)
const result = 1 / a;
^
# 中略
Aborted (core dumped)
この未定義動作を防ぐには、除数が0
でないかどうか、if
でチェックします。
if (a == 0) {
// ここでエラーを処理する
}
整除演算の結果が余りを持つ場合に発生する未定義動作です。
ノート: 整除とは、整数
x
が整数y
を余りがでないように割り切ることです。
組込み関数の@divExact
は、整除を計算します。@divExact
を呼ぶ場合、除数が0
でない、かつ、被除数が除数で割り切れることを保証しなければなりません。
pub fn main() void {
var a: u32 = 2;
const result = @divExact(5, a);
}
このコードを実行すると、実行時に未定義動作が発生し、プログラムが停止します。
$ zen run src/exact_divition_remainder.zen
exact division produced remainder
examples/ch11-advanced/ub/src/exact_divition_remainder.zen:3:20: 0x2279b5 in main (run)
const result = @divExact(5, a);
^
std.math.divExact
を使用すると、整除が余りを持つ場合、error.UnexpectedRemainder
を返します。
test "exact division remainder" {
const math = std.math;
var a: u32 = 2;
const result = math.divExact(u32, 5, a);
err(error.UnexpectedRemainder, result);
}
組込み関数の@shlExact
と@shrExact
とで、1
の立っているビットがシフトにより消えてしまう場合に発生する未定義動作です。
pub fn main() void {
var a: u8 = 0b1111_0000;
const result = @shlExact(a, 1);
}
このコードを実行すると、実行時に未定義動作が発生し、プログラムが停止します。
$ zen run src/shift_overflow.zen
left shift overflowed bits
examples/ch11-advanced/ub/src/shift_overflow.zen:3:20: 0x22798e in main (run)
const result = @shlExact(a, 1);
^
Aborted (core dumped)
組込み関数には、オーバーフローが発生したかどうかをブール型で返すシフト演算関数があります。
左シフトを例に取ると、関数シグネチャは@shlWithOverflow(comptime T: type, a: T, shift_amt: Log2T, result: *mut T) bool
です。引数に整数型、演算する2つの整数値に加えて、結果を格納するためのポインタを取ります。オーバーフローが発生した場合、この結果には、オーバーフローした後の値が格納されます。戻り値は、オーバーフローが発生した場合はtrue
が、それ以外ではfalse
が返ってきます。
test "exact shift overflow" {
var a: u8 = 0b1111_0000;
const overflow = @shlWithOverflow(u8, a, 1, &mut a);
ok(overflow == true);
ok(a == 0b1110_0000);
}
型変換を実行する際に発生する未定義動作です。
負数を符号なし整数へ変換しようとすると発生する未定義動作です。
pub fn main() void {
var negative: i32 = -1;
const unsigned: u32 = @intCast(u32, negative);
}
このコードを実行すると、実行時に未定義動作が発生し、プログラムが停止します。
$ zen run src/negative_to_uint.zen
attempt to cast negative value to unsigned integer
examples/ch11-advanced/ub/src/negative_to_uint.zen:3:27: 0x227988 in main (run)
const unsigned: u32 = @intCast(u32, negative);
^
# 中略
Aborted (core dumped)
同じビットの並びを符号なし整数型に格納したい場合、@bitCast
を使用します。
test "cast negative number to unsgined integer" {
// 内部のビット表現は`0xFFFF_FFFF`になる
var negative: i32 = -1;
const unsigned: u32 = @bitCast(u32, negative);
ok(unsigned == 0xFFFF_FFFF);
}
ターゲットとする整数型の範囲に収まらない整数値を型変換すると発生する未定義動作です。
pub fn main() void {
var x: u32 = 0x1234_5678;
const y = @intCast(u16, x);
}
このコードを実行すると、実行時に未定義動作が発生し、プログラムが停止します。
$ zen run src/truncate.zen
integer cast truncated bits
examples/ch11-advanced/ub/src/truncate.zen:3:15: 0x2206f0 in main (run)
const y = @intCast(u16, x);
^
# 中略
Aborted (core dumped)
組込み関数の@truncate
を使用することで、未定義動作を起こさず、常に上位ビットが切り捨てられた整数値を得ることができます。
test "truncate" {
var x: u32 = 0x1234_5678;
const result = @truncate(u16, x);
ok(result == 0x5678);
}
ターゲットとする整数型の範囲に収まらない浮動小数点値を型変換すると発生する未定義動作です。
pub fn main() void {
var x: f64 = 256.0;
const y: u8 = @floatToInt(u8, x);
}
このコードを実行すると、実行時に未定義動作が発生し、プログラムが停止します。
$ zen run src/float_to_int.zen
integer part of floating point value out of bounds
examples/ch11-advanced/ub/src/float_to_int.zen:3:19: 0x2279c8 in main (run)
const y: u8 = @floatToInt(u8, x);
^
# 中略
Aborted (core dumped)
浮動小数点値が取り得る値に対して、十分大きな整数型をターゲットとするか、std.math.intMax
を使用して変換可能かどうかをチェックして下さい。
test "out of bounds float to integer" {
const math = @import("std").math;
var x: f64 = 256.0;
if (x > @intToFloat(f64, math.maxInt(u8))) {
// ここでエラーを処理する
} else {
const y: u8 = @floatToInt(u8, x);
}
}
0
番地を指しているポインタを、0
番地が指せないポインタに型変換すると発生する未定義動作です。
ノート: Zenの普通のポインタは
0
番地を指すことができません。0
番地を指すことが許されるポインタは、オプション型に包まれたポインタ、allowzero
修飾子がついたポインタ、Cポインタ、の3つのみです。オプション型に包まれたポインタのnull
は、内部データとして0
を格納しています。
pub fn main() void {
var c_ptr: [*c]i32 = 0;
const ptr: *i32 = @ptrCast(*i32, c_ptr);
}
このコードを実行すると、実行時に未定義動作が発生し、プログラムが停止します。
$ zen run src/null_pointer_cast.zen
cast causes pointer to be null
examples/ch11-advanced/ub/src/null_pointer_cast.zen:3:23: 0x227980 in main (run)
const ptr: *i32 = @ptrCast(*i32, c_ptr);
^
# 中略
Aborted (core dumped)
ほとんど全ての場合、このような型変換は不要です。オプション型に包まれたポインタの場合、if
でnull
かどうかをチェックしてからポインタを利用しましょう。Cポインタやallowzero
ポインタを利用する機会は非常に限られていますが、これらのポインタをキャストする場合には、オプション型に包まれたポインタに変換するか、0
番地を指していないかのチェック (nullチェック) を行うようにしましょう。
組込み関数@alignCast
でポインタの型変換を実施する際に、ポインタのアドレスがアライメントを満たさない場合に発生する未定義動作です。
次のコードサンプルでは、ptr
は4
番地を指すポインタで、4
バイト境界でアライメントされています。このptr
を8
バイト境界でアライメントしようとすると未定義動作となります。
pub fn main() void {
var ptr = @intToPtr(*i32, 0x4);
const aligned = @alignCast(8, ptr);
}
このコードを実行すると、実行時に未定義動作が発生し、プログラムが停止します。
$ zen run src/pointer_alignment.zen
incorrect alignment
examples/ch11-advanced/ub/src/pointer_alignment.zen:3:35: 0x227996 in main (run)
const aligned = @alignCast(8, ptr);
^
# 中略
Aborted (core dumped)
std.mem.isAligned
関数を使用することで、ポインタがアライメントを満たすかどうかチェックすることができます。
test "pointer alignment check" {
const mem = std.mem;
var ptr = @intToPtr(*i32, 0x4);
const result = mem.isAligned(@ptrToInt(ptr), 8);
ok(result == false);
}
次のようにif
でチェックすることで、未定義動作を防ぐことができます。
if (!mem.isAligned(@ptrToInt(ptr), 8)) {
// ここでエラーを処理する
} else {
const aligned = @alignCast(8, ptr);
}
組込み関数@bytesToSlice
でバイト列をスライスに変換する時に、バイト列のサイズ % スライスの要素サイズ == 0
を満たさない場合に発生する未定義動作です。
pub fn main() void {
var bytes = [5]u8{ 0, 1, 2, 3, 4 };
const slice = @bytesToSlice(u32, bytes[0..]);
}
このコードを実行すると、実行時に未定義動作が発生し、プログラムが停止します。
$ zen run src/slice_widen_remainder.zen
slice widening size mismatch
examples/ch11-advanced/ub/src/slice_widen_remainder.zen:3:19: 0x2279bd in main (run)
const slice = @bytesToSlice(u32, bytes[0..]);
^
# 中略
Aborted (core dumped)
バイト列のサイズ % スライスの要素サイズ == 0
が満たされるかどうかチェックすることで、この未定義動作を防ぐことができます。
test "slice widen remainder" {
var bytes = [5]u8{ 0, 1, 2, 3, 4 };
if ((bytes[0..].len % @sizeOf(u32)) != 0) {
// ここでエラーを処理する
} else {
const slice = @bytesToSlice(u32, bytes[0..]);
}
}
組込み関数の@intToEnum
で整数型を列挙型に変換する際に、ヴァリアントに対応する整数値がない場合に発生する未定義動作です。
const Value = enum(u32) {
U32 = 0,
F64 = 1,
String = 2,
};
pub fn main() void {
var x: u32 = 5;
const result = @intToEnum(Value, x);
}
このコードを実行すると、実行時に未定義動作が発生し、プログラムが停止します。
$ zen run src/invalid_enum.zen
invalid enum value
examples/ch11-advanced/ub/src/invalid_enum.zen:9:20: 0x227983 in main (run)
const result = @intToEnum(Value, x);
^
# 中略
Aborted (core dumped)
この未定義動作を防ぐには、switch
で整数値が列挙型のヴァリアントに対応しているかどうかをチェックします。
test "enum cast" {
var x: u32 = 5;
const result = switch (x) {
@enumToInt(Value.U32),
@enumToInt(Value.F64),
@enumToInt(Value.String),
=> @intToEnum(Value, x),
else => error.InvalidEnumValue,
};
err(error.InvalidEnumValue, result);
}
このような変換処理を列挙型のメソッドとして実装しておくと良いでしょう。
組込み関数の@intToError
で整数型をエラー型に変換する際に、エラー種別に対応する整数値がない場合に発生する未定義動作です。
pub fn main() void {
var err = error.AnError;
var err_int = @errorToInt(err);
const invalid_error = @intToError(err_int + 1);
}
このコードを実行すると、実行時に未定義動作が発生し、プログラムが停止します。
zen run src/invalid_error.zen
invalid error code
examples/ch11-advanced/ub/src/invalid_error.zen:4:27: 0x2279c8 in main (run)
const invalid_error = @intToError(err_int + 1);
^
# 中略
Aborted (core dumped)
組込み関数の@errorToInt
で取得した整数値に対して、演算を行うこと自体が適切ではありません。この未定義動作を防ぐ方法は、@errorToInt
で取得した整数値に対して演算を行わないことです。
組込み関数@errSetCast
を使用して、あるエラー型から別のエラー型に変換する際、変換先にないエラー種別を変換した時に発生する未定義動作です。
const AnErrorSet = error {
A,
B,
};
const AnotherErrorSet = error {
A,
C,
};
pub fn main() void {
var err = AnErrorSet.B;
const invalid_err = @errSetCast(AnotherErrorSet, err);
}
このコードを実行すると、実行時に未定義動作が発生し、プログラムが停止します。
$ zen run src/invalid_error_set.zen
invalid error code
examples/ch11-advanced/ub/src/invalid_error_set.zen:13:25: 0x2279a5 in main (run)
const invalid_err = @errSetCast(AnotherErrorSet, err);
^
# 中略
Aborted (core dumped)
@errSetCast
はスーパーセットからサブセットへのエラー型変換にだけ用いるべきです。
共用体のデータを利用する際、誤ったヴァリアントにアクセスした場合に発生する未定義動作です。
const Value = union {
U32: u32,
F64: f64,
};
pub fn main() void {
var x = Value{ .U32 = 42 };
const y = x.F64;
}
このコードを実行すると、実行時に未定義動作が発生し、プログラムが停止します。
$ zen run src/wrong_union.zen
access of inactive union field
examples/ch11-advanced/ub/src/wrong_union.zen:8:6: 0x227996 in main (run)
const y = x.F64;
^
# 中略
Aborted (core dumped)
この未定義動作を防ぐには、タグ付き共用体を利用し、ヴァリアントをチェックしてからアクセスすると良いでしょう。
const TaggedValue = union(enum) {
U32: u32,
F64: f64,
};
test "variant access" {
var x = TaggedValue{ .U32 = 42 };
const result = if (x == TaggedValue.F64) x.U32 else error.NotF64;
err(error.NotF64, result);
}
☰ 人の生きた証は永遠に残るよう ☰
Copyright © 2018-2020 connectFree Corporation. All rights reserved. | 特定商取引法に基づく表示
Zen, the Zen three-circles logo and The Zen Programming Language are trademarks of connectFree corporation in Japan and other countries.