LoginSignup
5
1

More than 3 years have passed since last update.

Rust勉強中 - その27 -> unsafe

Posted at

自己紹介

出田 守と申します。
しがないPythonプログラマです。
情報セキュリティに興味があり現在勉強中です。CTFやバグバウンティなどで腕を磨いています。主に低レイヤの技術が好きで、そっちばかり目が行きがちです。

Rustを勉強していくうえで、読んで学び、手を動かし、記録し、楽しく学んでいけたらと思います。

環境

新しい言語を学ぶということで、普段使わないWindowsとVimという新しい開発環境で行っています。
OS: Windows10 Home 64bit 1903
CPU: Intel Core i5-3470 @ 3.20GHz
Rust: 1.39.0
RAM: 8.00GB
Editor: Vim 8.1.1
Terminal: PowerShell

前回

前回は並列処理について学びました。
Rust勉強中 - その27

unsafe

Rustにはunsafeと呼ばれる仕組みがあります。これらのコードはプログラマにも安全の責任を負うことになります。

unsafeブロック

unsafeブロックを宣言するには以下のように単にブロックの前にunsafeと書きます。

unsafe {
    ...
}

unsafeブロック内では以下の追加機能を使うことができます。

  • unsafe関数の使用
  • rawポインタの参照解決
  • 可変スタティックへのアクセス
  • 他言語の関数へのアクセス

以下はベクタのunsafeメソッドを使用する例です。

fn main() {
    let mut v = vec![0, 1, 2];
    unsafe {
        println!("{:#x}", v.get_unchecked(2)); // 0x2
    }
}

get_uncheckedは境界を確認せずに要素にアクセスします。そのため、例えば以下のように、範囲外へのアクセスも許します。

fn main() {
    let mut v = vec![0, 1, 2];
    unsafe {
        println!("{:#x}", v.get_unchecked(4)); // read out of bound value, but not error
    }
}

なので私の環境ではたいてい0x0を出力しますが、5回に1回くらいは、0x0でない値も返します。おそらく、メモリに記憶された何らかの値なのかもしれません。

unsafe関数

unsafe関数は、通常の関数にunsafeと付けることで宣言します。

unsafe fn f(...) {
    ...
}

定義したunsafe関数はunsafeコード内でしか呼べません。
以下にunsafe関数の定義と使用例を示します。

unsafe fn unsafe_get_mut(v: &mut Vec<i32>, idx: usize) -> &mut i32 {
    v.get_unchecked_mut(idx)
}

fn main() {
    let mut v = vec![0, 1, 2];
    let i;
    let idx = 2;
    unsafe {
        ...
        i = unsafe_get_mut(&mut v, idx);
    }
    println!("{:#x}", i); // 0x2
}

注意しないといけないのは、未定義動作を引き起こす可能性があるのは、unsafeコード中だけとは限らず、unsafeコードを使っているコード全体で未定義動作を引き起こす可能性があるということです。これを確認するために先ほどのコードを少し変えてみます(意図通りか分かりませんが・・・)。

unsafe fn unsafe_get_mut(v: &mut Vec<i32>, idx: usize) -> &mut i32 {
    v.get_unchecked_mut(idx)
}

fn main() {
    let mut v = vec![0, 1, 2];
    let i;
    let idx = 2;
    unsafe {
        i = unsafe_get_mut(&mut v, idx);
    }
    println!("{:#x}", i); // 0x2
    *i = 0x0;    // value overwrite!
    unsafe {
        println!("{:#x}", v.get_unchecked(idx)); // get element at idx
    }
    println!("{:?}", v); // [0, 1, 0]
}

一つ目のunsafeブロックで変数iに変数idxで指定したベクタの要素2のアドレスを格納します。そして、変数iを参照解決し0x0を書き込みます。
すると、ベクタの要素2は当然0に書き換わります。
次に変数idxを範囲外の4として指定してみます。

unsafe fn unsafe_get_mut(v: &mut Vec<i32>, idx: usize) -> &mut i32 {
    v.get_unchecked_mut(idx)
}

fn main() {
    let mut v = vec![0, 1, 2];
    let i;
    let idx = 4;
    unsafe {
        i = unsafe_get_mut(&mut v, idx); // read out of bound value
    }
    println!("{:#x}", i);
    *i = 0x0;    // value overwrite!
    unsafe {
        println!("{:#x}", v.get_unchecked(idx)); // get element at idx
    }
    println!("{:?}", v);
}
0x45325a6e
0x0
[0, 1, 2]

一行目の出力は、変数iを上書きする前です。二行目は上書き後に再び範囲外アクセスした際の値です。0x0に書き換えられていることが分かります。このように、unsafeコード中でないところでアドレスの値を書き換えてしまうことがあります。今回の場合、変数idxはハードコードですが、何らかの処理をした結果であった場合知らず知らずのうちに範囲外のデータへアクセスしてしまっている可能性があります。
unsafeコードは未定義動作を引き起こさない安全なコードでなければいけません。

unsafeトレイト

トレイトにもunsafeを付けることができます。

unsafe trait TraitName {
    ...
}

説明は省略します。
ただ一点、unsafeトレイトを誤って実装してしまうと、その型を指定した関数が未定義動作を引き起こす可能性があることには注意です。

rawポインタ

rawポインタは、通常のポインタに比べて制約が無いポインタです。rawポインタを使えば通常のポインタでは実現しにくいグラフ構造などを作ることができます。

*const Tと*mut T

rawポインタには2種類あります。

rawポインタ モード
*const T 読込み
*mut T 読書き

それぞれ以下のように参照から変換して作ります。

*const
&T as *const T;
*mut
&mut T as *mut T;

以下に例を示します。

fn main() {
    ...
    // raw pointer
    // *const T
    let x  = 10_i32;
    let rx = &x as *const i32;
    unsafe {
        println!("{:?} {} {}", rx, *rx, get_type(rx));
    }
    // *mut T
    let mut y = 20_i32;
    let ry = &mut y as *mut i32;
    unsafe {
        println!("{:?} {} {}", ry, *ry, get_type(ry));
        *ry = *rx;
        println!("{:?} {} {}", ry, *ry, get_type(ry));
    }
}
0xf73af1f6cc 10 *const i32
0xf73af1f77c 20 *mut i32
0xf73af1f77c 10 *mut i32

rawポインタの参照解決はunsafe内でしか行えません。また、参照解決は明示的に行う必要があります。
rawポインタ同士の比較は、アドレス同士の比較となります。

std::ptr::null

rawポインタはnullを持つこともできます。
nullの定義は以下のようになっています。

pub const fn null<T>() -> *const T { 0 as *const T }
fn main() {
    ...
    // null
    let rnull: *const i32 = std::ptr::null();
    println!("{:?}", rnull); // 0x0
}

FFI(Foreign Function Interface)

Rustは他言語の関数を呼び出すことができます。また、他言語からRustを呼び出すことができます。
今回はCCクレートを使って、C言語で書かれた関数をRustで呼び出してみます。

Cのプログラム

hello.c
#include <stdio.h>

void hello() {
    printf("Hello, C!\n");
}

このhello関数をRustから呼び出します。

externブロック

externブロックは、Rustにリンクされるライブラリ中の関数や変数を宣言するために使います。

extern {
    ...
}

例えば、Rustのプログラムでは必ずCの標準ライブラリがリンクされているので、その関数をRustで使えるように宣言してあげることもできます。

use std::os::raw::c_int;
extern {
    fn abs(n: c_int) -> c_int;
}
fn main() {
    unsafe {
        println!("{}", abs(-1)); // 1
    }
}

ここで、std::os::rawは、Cに対応するRustの型が定義されています。上記はabs関数を宣言しました。他言語の関数はunsafeコード中でのみ呼び出せます。

externブロックの上に#[link(...)]属性を付けることで、特定のライブラリの関数を宣言することができます。今回はhelloライブラリのhello関数を呼び出したいので以下のようにします。

#[link(name="hello")]
extern {
    fn hello();
}

name="hello"はライブラリの名前です。

ccクレート

CCクレートを使うことで簡単に他言語のライブラリをコンパイルすることができます。
(というより余談ですが、Windows環境なのでcl.exeとlib.exeでコンパイルして試したのですが、エラーを吐いてしまいました。エラー内容を確認したいのですが、powershellのせいか何なのか分かりませんが、すべて文字化けされてしまいうまくいきませんでした。どうもスットコドッコイな状況だったので今回はccクレートに頼ることにしました。)

build.rs

Cargo.toml
[package]
...
build = "build.rs"

[build-dependencies]
cc = "1.0"

build = "build.rs"はCargoがビルドする際に用いるビルドスクリプトを指定しています。ビルドスクリプトはCargo.tomlと同じディレクトリに配置します。いろんなことができるらしいですが(下の参照を参照!)、今回はccクレートを使ってコンパイルする処理を書きます。

参照:
document - Build Scripts

build.rs
extern crate cc;

fn main() {
    cc::Build::new()
        .file("hello.c")
        .compile("hello")
}

これで準備は整ったのでhello関数を呼び出してみます。

#[link(name="hello")]
extern {
    fn hello();
}

fn main() {
    unsafe {
        hello();
    }
}
$ cargo run
    Compiling ffi v0.1.0 (C:\Users\deta\hack\rust\ffi)
     Finished dev [unoptimized + debuginfo] target(s) in 1.12s
      Running `target\debug\ffi.exe`
Hello, C!

ここでひとまず・・・

今回でRust勉強中シリーズは終わりです。
環境構築からここまではRustのほんの一部にすぎないと思います。ただただ基本的なRustが使えるようになっただけです。ここから、Rustを使って作りたいものをガンガン作っていけます!
最初の方にも書きましたが、Rustを勉強していると安全なプログラムを作るための考え方なども同時に学べるため、Rustだけでなくほかの言語を使う際も応用できますよね。あと、やってて楽しいですね。使っていない筋肉が刺激されるようで。笑
これからは、Rustを一つの主武器として使っていこうと思います。Rustさん、これからもよろしくお願いします。

5
1
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
5
1