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

## はじめに
みなさんは未定義動作 (以下、Undefined Behaviorから**UB**と略す) を見聞きしたり、悩まされたりしたことはありますか？そうです。CやC++でよく聞く「[鼻から悪魔](https://qiita.com/tkmtSo/items/de3148dd1dcb70f38d6a)」とか「[タイムトラベル](https://cpplover.blogspot.com/2014/06/old-new-thing.html)」ってやつです。モダンな言語として愛されているRustにもUBがあり、私は`unsafe`を使ったばかりにその沼にハマってしまいました。

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

```rust
#[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 fn`[^2]が原因な気がしたので、`unsafe`な関数[^3]を、非`unsafe`な同等の関数の呼び出しで置き換えました。そうすると見事に

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

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

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

```sh
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`コメント](https://std-dev-guide.rust-lang.org/documentation/safety-comments.html#inside-safe-elements)も書き足しておきましょう。

```diff
-        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 test`と`cargo clean && cargo test`をしてSIGILLが消えたことを確認します。
めでたしめでたし。

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

## おまけ
SIGILLの原因は`ud2`命令を呼び出していたからでした[^4]。

[^1]: 厳密には、一つの関数が他の関数に依存している場合があったので、bit全探索と表現したほうが正確かもしれません
[^2]: Rustにおける`unsafe fn`とは、「予め定められた条件があり、その条件は呼び出し側が守らなければならず、その条件はコンパイラによって強制できず、条件を破った場合の責任は呼び出し側が負う」というマーカーです。
[^3]: `unwrap_unchecked`は「`None`に対して呼ぶとUBである」と明言されています。
[^4]: x86\_64では無効な命令[^5]です。
[^5]: [IA-32 インテル® アーキテクチャ・ソフトウェア・デベロッパーズ・マニュアル](https://www.intel.co.jp/content/dam/www/public/ijkk/jp/ja/documents/developer/IA32_Arh_Dev_Man_Vol2B_i.pdf) p4-274