4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Rust】意味不明に見えたSIGILLとその解消

Last updated at Posted at 2023-01-14

はじめに

みなさんは未定義動作 (以下、Undefined BehaviorからUBと略す) を見聞きしたり、悩まされたりしたことはありますか?そうです。CやC++でよく聞く「鼻から悪魔」とか「タイムトラベル」ってやつです。モダンな言語として愛されているRustにもUBがあり、私はunsafeを使ったばかりにその沼にハマってしまいました。

コード

とはいえ、実際のコードを提示しないことには話が始まらないのでコードを示します。

#[cfg(test)]
mod test {
    use core::num::NonZeroUsize;
    #[test]
    fn test() {
        pretty_size(0);
    }
    
    fn pretty_size(bytes: usize) -> String {
        let mut buf = [0u8; 5];
        let mut rest = bytes;
        let mut head_index: Option<NonZeroUsize> = None;
        write_numeric_char(bytes, 1000, &mut buf, 0, &mut rest, &mut head_index);
        write_numeric_char(bytes, 100, &mut buf, 1, &mut rest, &mut head_index);
        write_numeric_char(bytes, 10, &mut buf, 2, &mut rest, &mut head_index);
        write_numeric_char(bytes, 1, &mut buf, 3, &mut rest, &mut head_index);
        buf[4] = b'B';
        let head = unsafe { head_index.unwrap_unchecked() }.get() - 1;
        unsafe { std::str::from_utf8_unchecked(&buf[head..]).to_string() }
    }
    
    fn write_numeric_char(n: usize, pow: usize, bytes: &mut [u8], byte_index: usize, r: &mut usize, head: &mut Option<NonZeroUsize>) {
        if n >= pow {
            bytes[byte_index] = convert_to_numeric_char(*r / pow);
            *r -= *r / pow * pow;
            head.get_or_insert_with(|| (unsafe { NonZeroUsize::new_unchecked(byte_index + 1) }));
        }
    }

    fn convert_to_numeric_char(a: usize) -> u8 {
        match a {
            0 => b'0',
            1 => b'1',
            2 => b'2',
            3 => b'3',
            4 => b'4',
            5 => b'5',
            6 => b'6',
            7 => b'7',
            8 => b'8',
            9 => b'9',
            _ => unreachable!("{}", a)
        }
    }
}

どこがUBかすぐわかったあなたは鋭いです。

原因の特定

もしかしたらコンパイラのバグかもしれないとは思ったので、ひとまずminimizeすることを考えました。minimizeとは、再現できる最小ケースを探して確定することで、開発者の手間を大幅に減らすことになるプロセスです。一般的にはすることが望ましいとされ、rustcでもminimizeすることが望ましいとされています。

打ったコマンド

  • cargo check - 問題ない
  • cargo run - 問題ない
  • cargo test - 問題あり
  • cargo clean && cargo test - 問題あり

ということで、自分が書いたテスト自体に問題がありそうだということがわかりました。

二分探索-likeに関数をコメントアウト

まずは関数単位で絞り込んでいきます。このとき一つずつコメントアウトしていくと線形時間がかかってしまうので、二分探索のような方法1で半分ずつコメントアウトしていきました。そうして上記のコードにたどり着きます。
なんとなくunsafe fn2が原因な気がしたので、unsafeな関数3を、非unsafeな同等の関数の呼び出しで置き換えました。そうすると見事に

thread 'main' panicked at 'called `Option::unwrap()` on a `None` value'

です。原因は明らかとなりました。

miriで確認

念の為UBを確認することができるmiriで「自分がUBを引き起こした」という仮説を検証します。

rustup +nightly component add miri
cargo miri test

そうすると、やはりUBでした:

test pretty_size::test::main ... error: Undefined Behavior: entering unreachable code
   --> src/pretty_size.rs:134:29
    |
134 |         let head = unsafe { head_index.unwrap_unchecked() }.get() - 1;
    |                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ entering unreachable code
    |
    = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
    = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
    = note: BACKTRACE:
    = note: inside `pretty_size::pretty_size` at src/pretty_size.rs:134:29

解決

解決は至って簡単です。head_indexを正しい値で初期化するだけです。ついでにこういう事故をまた起こしたりしないようにSAFETYコメントも書き足しておきましょう。

-        write_numeric_char(bytes, 1, &mut buf, 3, &mut rest, &mut head_index);
+        buf[3] = convert_to_numeric_char(rest % 10);
+        // SAFETY: 4 != 0.
+        head_index.get_or_insert_with(|| (unsafe { NonZeroUsize::new_unchecked(3 + 1) }));
         buf[4] = b'B';
+        // SAFETY: we've just initialized head_index with some value.
         let head = unsafe { head_index.unwrap_unchecked() }.get() - 1;
+        // SAFETY: convert_to_numeric_char yields only b'0'..b'9', 
+        //         which is valid codepoint for UTF-8 strings.
         unsafe { std::str::from_utf8_unchecked(&buf[head..]).to_string() }

そしてcargo testcargo clean && cargo testをしてSIGILLが消えたことを確認します。
めでたしめでたし。

まとめ

unsafe fnが必要ではないときは使うな」とよく言われます[要出典]が、たしかにこれぐらいなら使わないほうが良いかもしれません。

おまけ

SIGILLの原因はud2命令を呼び出していたからでした4

  1. 厳密には、一つの関数が他の関数に依存している場合があったので、bit全探索と表現したほうが正確かもしれません

  2. Rustにおけるunsafe fnとは、「予め定められた条件があり、その条件は呼び出し側が守らなければならず、その条件はコンパイラによって強制できず、条件を破った場合の責任は呼び出し側が負う」というマーカーです。

  3. unwrap_uncheckedは「Noneに対して呼ぶとUBである」と明言されています。

  4. x86_64では無効な命令5です。

  5. IA-32 インテル® アーキテクチャ・ソフトウェア・デベロッパーズ・マニュアル p4-274

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?