RustはHeartbleedを防げたのか?

はじめに

ja.wikipedia.org

2014年に、「OpenSSL」にHeartbleedという脆弱性が見つかり、話題になっていました。 この脆弱性は、典型的なメモリ操作に関するバグであったため、Rustであれば発生しなかったのではないか?という観点で調べていると、過去に同じ主張をしている文献をいくつか発見しました。

こういうのは、後からなら何とでも言える部分があるのですが、やはりRustなら発生しなかっただろうな、というのが私の見解です。 Rustでは、メモリ操作に関連する脆弱性になり得るコードの多くが、コンパイル時にチェックされます。 スライスやコレクションについては、実行時に境界チェックが入るため、最悪サービスが停止しますが、データが流出したり、バグったまま動き続けるようなことはありません。

なるべく、安全なプログラミング言語を使って、開発を進められるようにしていきたいですね。

RustでならHeartbleedを防げたとする文献

Rust would’ve prevented Heartbleed.

tonyarcieri.com

The Heartbleed bug would not have been possible if OpenSSL had been implemented in Rust.

blog.getreu.net

Embedded System Security with Rust: Case Study of Heartbleed

エストニアのセキュリティ研究者の論文?レポート?が公開されています。 非常に丁寧に解説されているので、こちらを詳細に見ていきます。

Abstract

組込みでは、速度向上やメモリレイアウトを制御するため、C/C++が使われてきました。 しかし、今日、組込みシステムでは、セキュリティがより重要になってきています。

Unfortunately, C/C++ supports some secure software design principles only rudimentary.

多くの脆弱性が、C/C++がメモリ安全でないことに関連しています。 例として、Heartbleedを取り上げて、技術的な詳細とその影響を論じます。

Introduction

組込みデバイスがネットワークに接続するようになり、組込みデバイスのセキュリティが重要になってきています。 IoTによりコンシューマデバイスのあらゆるものがネットワーク接続されるようになります。

Causes

Schneirは、組込みデバイスがセキュアでない主な理由は、ソフトウェアにパッチがあてられておらず、最新のものよりはるかに古いせいだと述べています。 SoCや組込みデバイスの製造元がfirmwareのアップデートを用意しても、ユーザーがそれを適用するのは容易ではありません。

状況は変わったのでしょうか?製造元の観点ではセキュリティはコストのかかるサービスです。 コストとセキュリティによる利益のバランスを取ることは、いつも簡単ではありません。 しかし、日に日にそのリスクに気づくユーザーが増えています。 デバイスがハッキングされると、ブランドに傷がつきます。 Deutsche-Telecomや他の会社は、既にパッチポリシーや脆弱性報告の交付を改善しています。

消費者がセキュリティにコストをかけるには至っていません。 技術に精通していないユーザーは、エラーのないソフトウェアが存在していないことを理解できません。 メーカーは、ソフトウェアの不具合を隠す代わりに、バグフィックスのインフラ整備を進めるべきです。

Embedded System Development

組込みで最も無視されていた属性は、機密性です。ネットワークインタフェースを使った通信を行うようになり、徐々に重要性が増してきています。

組込みシステムをより安全にするために、何ができるのでしょうか? 設計レベルのアプローチとして、BPNM, Secure Tropos, Misuse Cases, Mal-activity diagrams, UMLsec, SecureUML, Trust Trade-off Analysisなどがあります。

プログラミング側では、多くの近代プログラミング言語がメモリ安全性やデータ競合のないセキュリティ機能を提供しています。 しかし、組込みシステムでは、53%がC++を、52%がCを使っています。

CもC++もメモリ安全性を保証しません。 CやC++を使った低レイヤプログラミングでは、メモリ破壊のバグが古くからコンピュータセキュリティの問題でした。 このような安全性を欠いた言語は、攻撃者にプログラムの動作を変更したり、制御フローをハイジャックすることにより完全に制御を奪うことを許します。

CWE/SANS Top 25 of 2011

による脆弱性のランキングのうち、C/C++のメモリ安全性バグに関連するものを、示す。

f:id:tomo-wait-for-it-yuki:20190215210234p:plain
CWE in C/C++ that affect memory

メモリに関連する問題は、Cプログラムが変数やオブジェクトのポインタをメモリ配置やライフタイムの外側から、操作することです。 Javaのようなメモリ安全な言語は、直接ポインタを操作できません。Javaコンパイラは実行時のリソースコストとガベージコレクタによって、メモリ安全を達成します。 この追加のコストがあるため、リアルタイムの組込みアプリケーションでは、このような解決策を使えません。

長い間、プログラムの効率とメモリ安全とは、相反するもののようでした。 しかし、Rustと呼ばれる新しいプログラミング言語は、データの所有権というセマンティクスを導入することで、うまく両者のバランスを取っています。 新しいプログラミングパラダイムでは、コンパイル時にメモリ安全性を保証します。 実行時のコストは必要ありません。表に示したCWEは、Rustではほとんどコンパイル時に検出されます。 Rustのメモリ安全性は、未定義動作を引き起こさないことと、メモリリークが発生ないことを保証します。

The Heartbleed Vulnerability

Heartbleedは、C/C++のメモリ安全性に関連する脆弱性の典型的な例です。関連するCWEは、CWE-126: Buffer Over-readです。

The Bug of the Century

まるでSFのようですが、たった4行の誤ったプログラムが、インターネット暗号化インフラの少なくとも4分の1を危険にさらしました。 当時、信頼された認証局から証明書が発行されているインターネット上のWebサーバの17.5%(約50万台)で、この脆弱性が存在するHeartbeat拡張が有効になっていました。 これらのサーバーの秘密鍵や利用者のセッション・クッキーやパスワードを盗み出すことが出来る可能性がありました。

このバグは、2012年1月にSSL/TLSプロトコルの拡張であるHeartbeat機能によってもたらされました。

The Heartbeat Protocol Extension

Heartbleedというバグの名前は、HeartbeatというTLS/DTLSのプロトコル拡張から名づけられています。 SSL/TLSクライアントがリモート接続が生きているかどうかをテストできるようにします。 クライアントは、TLS1_HB_REQUESTを使い捨ての文字列とともに送ります。サーバーは届いたデータをエコーバックします。 クライアント側は、エコーバックで届いたデータを、自身が送ったデータと比較し、一致すると接続が有効であると判断します。

TLS1_HB_REQUESTは、payloadと呼ばれるフィールドを持っています。 payloadは、payload文字列の長さを表しており、サーバーがエコーバックする時に、memcpy()で何バイトコピーするかを決めるために使います。

f:id:tomo-wait-for-it-yuki:20190216044313p:plain
TLS Heartbeat protocol

How Does the Heartbleed-Exploit Work?

Heartbleed脆弱性は、実装の問題です。境界チェックがないため、不適切な入力検証によって発生します。 Heartbeatプロトコルの設計には不具合がないことが重要です。

Heartbleedのexploitは非常にシンプルです。 TLS1_HB_REQUESTに、実際のpayloadより長いpayload-lengthを設定します。 すると、サーバー側の、OpenSSLメモリ内部の64KiBのデータがクライアントに送り返されます。

f:id:tomo-wait-for-it-yuki:20190216045002p:plain
TLS Heartbeat with spoofed payload-length-field

unsigned char *p = &s->s3->rrec.data[0], *pl;
unsigned short hbtype;
unsigned int payload;
unsigned int padding = 16;

hbtype = *p++;
n2s(p, payload);  // TLSリクエストから`payload-length`を読み込む。実際のpayloadの長さと一致しているかのチェックをしていない。
pl = p;


if (hbtype == TLS1_HB_REQUEST)
        {
        unsigned char *buffer, *bp;
        int r;

        buffer = OPENSSL_malloc(1 + 2 + payload + padding);
        bp = buffer;

        *bp++ = TLS1_HB_RESPONSE;
        s2n(payload, bp);
        memcpy(bp, pl, payload);  // `payload-length`バイト分、コピーする。Buffer over-read脆弱性!

        r = ssl3_write_bytes(s, TLS1_RT_HEARTBEAT, buffer,
                3 + payload + padding); 

The Heartbleed-Patch

2つのif文を追加することで、Heartbleedのバグを修正できます。

// TLSリクエストが短すぎる場合、リクエストを破棄して、返答しない 
if (1 + 2 + 16 > s->s3->rrec.length)   
    return 0;   
hbtype = *p++;
n2s(p, payload);
// TLSリクエストのpayload文字列が、payload-length-fieldより短い場合、リクエストを破棄して、返答しない
if (1 + 2 + payload + 16 > s->s3->rrec.length)  
    return 0;   
pl = p;

What Can We Learn?

Heartbleedは、単純なプログラミングエラーであり、悪意があったわけではありません。 しかし、将来の同種の脆弱性について考えると、Heartbleedが意図的に導入された脆弱性に分類されることに留意することが重要です。

f:id:tomo-wait-for-it-yuki:20190216051939p:plain
Development Concepts

Heartbleedは、意図的に導入される可能性があったわけです。 忘れてはいけないことは、検証の欠落はよくあるミスであることです。 さらに、コードから欠落している行を見つけることは、コード内の不具合を見つけるより、難しいです。 これはコードレビュワーにとっても同じです。 脆弱性のあるコードがレビュープロセスを通過することは、驚くことではありません。

C/C++では、memcpy(bp, pl, payload)メモリの秘密鍵や認証情報を含んでいるpaylodをコピーするというようなことを明示的に示してくれるセマンティクスがありません。 Rustは、プログラムに明示的に書いたことだけを行い、それ以外を行わないプログラミング言語なのです。

Could Heartbleed Have Happened With Rust?

下のプログラムは、Rustで、Cのコードを再現したものです。

fn tls1_process_heartbeat (s: Ssl) -> Result<(), isize> {
    const PADDING: usize = 16;

    let p = s.s3.rrec;
    let hbtype:u8 = p[0];
    // payload-lengthを`TLS1_HB_REQUEST`から抽出する
    let payload:usize = ((p[1] as usize) << 8) + p[2] as usize; 

    let mut buffer: Vec<u8> = Vec::with_capacity(1+2+payload+PADDING);
    buffer.push(TLS1_HB_RESPONSE);
    // `payload`-lengthをTLS1_HB_RESPONSEにシリアライズする
    buffer.extend(p[1..1+2].iter().cloned());                   
    // payload文字列をコピーする。境界チェックをしていないため、Heartbleed脆弱性を引き起こすはず!
    buffer.extend(p[3..3+payload].iter().cloned());             

    let mut rng = rand::thread_rng();                           
    buffer.extend( (0..PADDING).map(|_|rng.gen::<u8>())
            .collect::<Vec<u8>>() );

    if hbtype == TLS1_HB_REQUEST {
        let r = ssl3_write_bytes(s, TLS1_RT_HEARTBEAT, &*buffer);
        return r
    }
    Ok(())
}

上記のRust実装は、Heartbleedのexploitに対して、どのように動作するのでしょうか? 22バイトのpayload文字列を持つHeartbeatに対して、payload-lengthを257に設定してexploitを実行します。

  buffer.extend(p[3..3+payload].iter().cloned());

3+payloadは、スライスインデックスの上限です。payload文字列は22要素しか持っていないため、257は境界外です。 元々のCコードでは、これはBuffer over-readによりHeartbleedを引き起こします。

一方、Rustコードを実行したときの結果は、次の通りです。 プログラムは、panicメッセージとともにアボートします。 攻撃者は、依然としてDoS攻撃が可能ですが、データリークは発生しません。

thread '<main>' panicked at 'assertion failed: index.end <= self.len()',
Process didn't exit successfully: `target/release/heartbeat` (exit code: 101)

exploitに使ったコードは下記の通りです。

    let s: Ssl = Ssl {
        s3 : Rrec{
                rrec:  &[TLS1_HB_REQUEST, 1, 1, 14, 15, 16, 17,
                         18, 19, 20, 21, 22, 23, 24, 25, 26, 27,
                         28, 29, 30, 31, 32]
        }
    };
    tls1_process_heartbeat(s).unwrap();
    }

Results

OpenSSLがRustで実装されていれば、Heartbleedバグは起こり得なかったでしょう。 Rustでは、データ構造を超えた読み込みができないため、悲惨なデータリークは発生しません。

「The Challenge of Using C in Safety-Critical Applications」を雑に読む

The Challenge of Using C in Safety-Critical Applicationsというwhite paperを雑に読んでみます。

このwhite paperは、C言語でsafety-criticalなシステムを作ることが、いかに難しいかを述べています。

white paper内で参照しているコードは、下記のレポジトリで公開されています。

github.com

white paper内で静的解析ツールで検出できなかった4つのケースのうち、1つは、PCLintCoverityでは正しく検出できることがissue #2で報告されています。

Abstract

safety-criticalなシステムにおいて、未定義動作や予期せぬ停止をするようなプログラミング言語 (C言語) が使われ続けています。 プログラミング言語の欠陥を埋めるために、MISRA-Cのようなサブセットが定義されていますが、実行時の安全性を保証するには、結局のところ不十分です。

Introduction

safety-criticalなシステムを構築するにあたり、ソフトウェアエラーを開発プロセスの早い段階で検出する戦略が必要です。 そのような戦略のうち、設計ドキュメントと定期的なコード検査が、今日のソフトウェア開発プロセスでは一般的ですが、ソフトウェア実装と同様にヒューマンエラーという脆弱性の影響を受けます。 静的解析は、危険なコードパターンを自動で検出することで、この問題を緩和します。

Background

safety-criticalシステムでは、C言語があらゆる場所で使われています。 これは、性能やフットプリント、コンパイラサポートなど、多くの理由があります。

2010 ISO/IEC 9899:2011の草案では、C言語の200個近い未定義動作について詳述しています。 未定義動作を減らすために、CERT CやMISRA-Cのようなサブセットが定義されています。

このwhite paperでは、このようなサブセットがC言語にとって意味のあるものになっているかどうか検証するために、一般的な実行時エラーについて調査します。

Methods

MISRA-C基準とCの静的解析ツールの有効性を評価するため、C言語の4つの危険な動作を選択しています。

  • 複数の可変参照
  • 不変データの修正
  • 曖昧なパターンマッチ
  • データ競合

その後、MISRA-Cガイドラインといくつかの静的解析ツールを適用し、その有効性を評価します。 静的解析ツールには、以下のツールを利用します。

  • Cppcheck
  • Flawfinder
  • Flint++
  • Frama-C
  • OCLint
  • scan-build
  • splint
  • Vera++

訳注:全て、OSSの静的解析ツールです。製品使うと、色々問題があるのでしょう。

可変参照

複数の可変参照があると、気づかずに無効な参照を使うリスクが常にあります。 MISRA-Cでは、実行時のNULLポインタチェックを要求しています。静的解析ツールは、NULLポインタの参照外しに対しては、よく機能します。しかし、他のデータ破壊については、あまり検出しません。

MISRA-Cおよび静的解析で検出できない例の、簡略化バージョンです。

typedef struct {
    int32_t x;
    int32_t y;
    int32_t z;
} example_s;

/* Example data. */
example_s a = {
    .x = 1,
    .y = 2,
    .z = 3
};

/* Mutable reference. */
example_s * b = &a;
/* Mutable alias. */
example_s ** c = &b;
/* Reference corrupted. */
*c += 2048;
/* Use after corruption. */
b->y = 4;
b->x = 5;
b->z = 6;

破壊された参照の利用は、未定義動作を引き起こします。大抵の場合、セグメンテーションフォールトになります。

const外し

訳注:この例は、PCLintおよびCoverityでは検出できる、という報告がissueにあがっています

基本的には、上述の可変参照のケースと同じです。 今回は、constの契約を破ります。

/* Example data. */
example_s a = {
    .x = 1,
    .y = 2,
    .z = 3
};

/* Constant reference to
* example data.
*/
const example_s const * b = &a;
/* Constant alias to reference. */
const example_s * const * c = &b;
/* Cast away const to corrupt
* reference. */
*((example_s **)(c)) += 2048;
/* Use reference after
* corruption. */
int32_t sum = b->x + b->y + b->z;

信頼できないパターンマッチ

C言語では、列挙体の誤使用についての保護機能がありません。 C言語enumは、定数のintと区別ができないため、下の例では、関係のない値とマッチングしてしまいます。

/* Enumeration intended for use. */
typedef enum {
APPLY_BRAKE = 1,
APPLY_THROTTLE = 2
} action_e;
/* Ambiguous enumeration. */
enum {
SELF_DESTRUCT = 2,
};
/* Example data. */
example_s a = {
    .x = 1,
    .y = 2,
    .z = 3
};
/* Mutable reference. */
example_s * b = &a;
/* Mutable alias. */
example_s ** c = &b;
/* Intended use as an 'action_e'
* enum type.
*/
action_e t = APPLY_THROTTLE;
/* Match on integer instead. */
switch (t)
{
    /* Wrong pattern. */
    case SELF_DESTRUCT: {
        *c += 2048; break;
    }
    default: { break; }
}
/* Use after corruption. */
b->y = 4;
b->x = 5;
b->z = 6;

データ競合

bが共有リソースの場合、b->xへの書き込みは、破壊された参照への書き込みである可能性があります。

/* Arbitrary bound. */
while ((b != NULL) && (b->x < 10))
{
    /* Simulate some amount of
    * work.
    */
    (void)sleep(0);

    /* If another thread has
    * changed the shared resource.
    */
    if (b->a >= 10)
    {
        b += 2048;
    }
    else
    {
        /* Increment potentially
        * corrupted reference.
        */
        b->x += 1;
    }
}

結局のところ、共有の参照を破壊するコードが1箇所あると、C言語では未定義動作に繋がってしまいます。 これを、C言語の静的解析で検出することは、困難です。

別のアプローチ

安全性を保証するために、他の言語を調査しました。 このwhite paperでは、Rustを選びました。

ここまでのC言語と同様のコードを、Rustで書くとどうなるか、を見ていきます。

安全な参照

下の例はコンパイルできません。Rustの借用と所有権のコンセプトが守ってくれます。 所有権を持っていないデータを破壊することはできません。

struct Example {
    x: i32,
    y: i32,
    z: i32,
}
let mut a: Example = Example { x: 1, y: 2, z: 3 };
let mut b: Box<&Example> = Box::new(&mut a);
let _c: Box<&&Example> = Box::new(&mut b);
drop(**c);
//~^ ERROR cannot move out of borrowed content
b.x = 4;
//~^ ERROR cannot assign to `b.x` because it is borrowed
b.y = 5;
//~^ ERROR cannot assign to `b.y` because it is borrowed
b.z = 6;
//~^ ERROR cannot assign to `b.z` because it is borrowed

不変性の保証

下の例はコンパイルエラーになります。不変データを変更しようとするからです。 Rustでは、不変性を簡単には破ることができません。

let mut a: Example = Example { x: 1, y: 2, z: 3 };
let mut b: Box<&Example> = Box::new(&mut a);
let c: Box<&&Example> = Box::new(&mut b);
// Attempt to corrupt referenced data
drop(*(&mut(**c)));
//~^ ERROR cannot borrow immutable `Box` content `*c` as mutable
b.x = 4;
//~^ ERROR cannot assign to field `b.x` of immutable binding
b.y = 5;
//~^ ERROR cannot assign to field `b.y` of immutable binding
b.z = 6;
//~^ ERROR cannot assign to field `b.y` of immutable binding

信頼できるパターンマッチ

下のプログラムはコンパイルエラーになります。 Rustは曖昧なenumを許容していません。また、Rustはデフォルトでは他の型のパターンマッチを許しません。

// Enumeration intended for use.
enum Action {
    ApplyBrake = 1,
    ApplyThrottle = 2,
}
// Second enumeration.
enum Destruct {
    SelfDestruct = 1,
}
let pattern = Action::ApplyThrottle;
match pattern {
    Destruct::SelfDestruct =>
    //~^ ERROR mismatched types
    {
        panic!("Memory corrupted.")
    }
}

データ競合の保護

サンプルコードを2つ示します。 1つは、Cの例を、愚直に実装しようとしたものです。 下のコードは、Rustコンパイラによって多くのエラーが報告されます。 これはRustのムーブセマンティクスのコンセプトによるものです。

訳注:bはBoxのインスタンスで、複数のスレッド関数にbを渡そうとした、と考えて下さい。 ムーブセマンティクスにより、bの利用はエラーになります。

while b.x < 10 {
//~^ ERROR use of moved value: `b.x`
    thread::sleep(time::Duration::from_secs(0));
    if b.x >= 10 {
    //~^ ERROR use of moved value:
    // `b.x`
        drop(b);
        //~^ ERROR use of moved value: `b`
    }
    b.x = b.x + 1;
    //~^ ERROR use of moved value:
    // `*b`
}

次に、スレッド間でデータを共有する場合に、Rustコンパイラが要求することについて見ていきます。

let a: Example =
Example {
    x: 1, y: 2, z: 3
};
let b: Box<&mut Example> = Box::new(&mut a);
//~^ ERROR `a` does not live long enough
for _ in 0..thread_count {
    handles.push(Some(thread::spawn(|| { black_box(b)})));
    //~^ ERROR capture of moved value: `b`
}

spawn()関数は、Rustがライフタイムと呼ぶ制約を持っています。 spawn()の引数(クロージャ)は、「プログラム実行の間中、ライフタイムを持つ必要があります」。 ここでは、関数内のローカルデータを参照しているため、spawnしたスレッドが、関数スコープより長く存在した場合、ローカルデータへの参照は無効になります。

次に、可変参照は常に唯一である必要があります。 上記例の参照は、ループの1回目で、スレッドにムーブされる必要があります。 これまでに見た借用ではなく、所有権を完全に移譲する必要があります。 次のイテレーションでは、既に所有権を失っているため、渡せるものが何もありません。

最後に

一通り読んでみました。 やはり、強力な型システム、所有権、借用、ライフタイム、による安全性という観点に行きつくのだなぁ、という感想です。

nRF5 SDK for Meshで学ぶBluetooth mesh②~light_switch example概要~

www.nordicsemi.com

実装を見てみないと、どういうものなのかよくわからないので、NordicのSDKを解析しながら、どういうものか理解していきます。

今回は、SDKの大枠とlight_switch exampleの概要を理解していきます。

README

なにはともあれ、READMEです。

This GitHub repository contains all the files from the official release zip package.

SDKをダウンロードしましたが、普通にGitHubソースコードが公開されていました。NordicのSDKは大部分がOSSとして公開されているので、非常にありがたいです。

github.com

Mesh SDK自体のドキュメントはこちらにあります。

www.nordicsemi.com

repository structure

  • bin: 全サンプルのプリビルドバイナリ
  • CMake: CMakeの設定ファイルとユーティリティ
  • doc: SDKのドキュメントと、Doxygenで生成したドキュメント
  • examples: サンプルアプリケーション
  • external: SoftDeviceなどの外部プロジェクト
  • mesh: meshスタックのソースコードとテストコード
  • models: mesh modelのソースコード
  • scripts: meshスタックを使ったシリアルインタフェースとの通信スクリプトなど
  • tools: 開発に役立つツール

Overview

nRF5 SDK for Mesh architecture

f:id:tomo-wait-for-it-yuki:20190212082833p:plain
mesh stack of nRF5 SDK for Mesh

component description
Models Bluetooth meshモデル。デバイスの動作とデータ形式を実装
Access バイスが受信したメッセージを適切なモデルへ転送
Device State Manager 暗号鍵とアドレスを保持。
Mesh Core ネットワーク層トランスポート層
Provisioning プロビジョナーと、ネットワークに参加する側のデバイス、両方のプロセスを実装
Bearer 低レベルの無線コントローラで、上位レイヤへの送受信インタフェースを提供
DFU (Device Firmware Upgrade) ブートローダとともに利用することでファームウェアを更新する機能を提供。Nordicのプロプライエタリ機能で一般的なmeshデバイスで使えるものではない
Mesh Stack meshの初期化など、トップレベルの機能を提供する薄いラッパ
Serial アプリケーションレベルのシリアル通信API

Resource usage

バイナリサイズを小さくするコンパイルオプションを付けると、Flashを100kB, RAMを10~13kBほど使用するようです。 Flashメモリの寿命計算方法などもあって面白いです。

Getting started

Quick start guide: Running a first example

light switch examplesを動かすための手順です。 nRF52840 DKの交換待ちなので、今は動かせないです。ぐぬぬ

このサンプルは、3つのサブアプリケーションから構成されています。

  • Generic OnOff server model
  • Generic OnOff client model
  • Provisioner

Hardware requirements

  • One nRF5 development board for the client.
  • One nRF5 development board for the provisioner.
  • One or more nRF5 development boards for the servers (maximum up to 30 boards).

あれ、ということは、nRF52840が3台必要…? サンプルのREADMEを見ると、static provisionerを使わない場合、最低2台あれば動かせるようです。安心。

デモの内容は、下記のように、clientのボタンを押すと、server側LEDのOn/Offが切り替える、というものです。

f:id:tomo-wait-for-it-yuki:20190212094621p:plain
mesh network demo

light_switchサンプル

nRF5 SDK for Meshのexamples/light_switchを見ていきます。掲載するソースコードは、主観で重要な部分のみを抽出しています。

nRF Meshアプリケーションがあれば、Provisionerとして使えるようです。

ディレクトリ構成

$ tree -L 1
.
├── client
├── CMakeLists.txt
├── img
├── include
├── provisioner
├── README.md
└── server

CMakeLists.txt

トップレベルのCMakeはシンプルです。

add_subdirectory("provisioner")

# Proxy only supported on nRF52 series devices due to nRF5 SDK dependency:
if (${${PLATFORM}_FAMILY} STREQUAL "NRF52" AND NOT PLATFORM MATCHES "nrf52810")
    add_subdirectory("client")
    add_subdirectory("server")
endif()

clientのCMakeを見てみます。プロトコルスタックを明示的に書いてあるので、長いですが、普通です。

|> examples/light_switch/client/CMakeLists.txt

set(target "light_switch_client_${PLATFORM}_${SOFTDEVICE}")

add_executable(${target}
    "${CMAKE_CURRENT_SOURCE_DIR}/src/main.c"
    "${CMAKE_SOURCE_DIR}/mesh/stack/src/mesh_stack.c"
    "${CMAKE_SOURCE_DIR}/examples/common/src/mesh_provisionee.c"
    "${MBTLE_SOURCE_DIR}/examples/common/src/rtt_input.c"
    "${CMAKE_SOURCE_DIR}/examples/common/src/simple_hal.c"
    "${CMAKE_SOURCE_DIR}/examples/common/src/mesh_app_utils.c"
    ${BLE_SOFTDEVICE_SUPPORT_SOURCE_FILES}
# meshプロトコルスタックとボードのソースファイル
    ${${nRF5_SDK_VERSION}_SOURCE_FILES})

target_include_directories(${target} PUBLIC
    "${CMAKE_CURRENT_SOURCE_DIR}/include"
    "${CMAKE_CURRENT_SOURCE_DIR}/../include"
    "${CMAKE_SOURCE_DIR}/examples/common/include"
    "${CMAKE_SOURCE_DIR}/external/rtt/include"
    ${BLE_SOFTDEVICE_SUPPORT_INCLUDE_DIRS}
# meshプロトコルスタックとボードのヘッダファイル
    ${${nRF5_SDK_VERSION}_INCLUDE_DIRS})

# example内にあるリンカスクリプトを利用
set_target_link_options(${target}
    ${CMAKE_CURRENT_SOURCE_DIR}/linker/${PLATFORM}_${SOFTDEVICE})

target_compile_options(${target} PUBLIC
    ${${ARCH}_DEFINES})

target_compile_definitions(${target} PUBLIC
    ${USER_DEFINITIONS}
    -DUSE_APP_CONFIG
    -DCONFIG_APP_IN_CORE
    ${${PLATFORM}_DEFINES}
    ${${SOFTDEVICE}_DEFINES}
    ${${BOARD}_DEFINES})

target_link_libraries(${target}
    rtt_${PLATFORM}
    uECC_${PLATFORM})

# hexファイルを作成
create_hex(${target})
add_flash_target(${target})

get_property(target_include_dirs TARGET ${target} PROPERTY INCLUDE_DIRECTORIES)
add_pc_lint(${target}
    "${CMAKE_CURRENT_SOURCE_DIR}/src/main.c"
    "${target_include_dirs}"
    "${${PLATFORM}_DEFINES};${${SOFTDEVICE}_DEFINES};${${BOARD}_DEFINES}")

add_ses_project(${target})

main.c

アプリケーションコードは、main.cのみで、client側は350行、server側は300行ほどです。 server側を見てみます。

main

main関数は、これでもか、というくらいシンプルです。

int main(void)
{
    initialize();
    start();

    for (;;)
    {
        (void)sd_app_evt_wait();
    }
}

initialize

static void initialize(void)
{
    // GPIOを有効化し、出力に設定します
    // 定義は、`exmaples/common/src/simple_hal.c`
    hal_leds_init();

    // SoftDeviceのAPIを呼び出して、BLEスタックを有効かします
    // 定義は、`exmaples/common/src/ble_softdevice_support.c`
    ble_stack_init();

    mesh_init();
}

mesh_init()は次の実装になっています。パラメータ (コールバックハンドラ含む) を用意して、mesh_stack_init()を呼び出します。

static void mesh_init(void)
{
    mesh_stack_init_params_t init_params =
    {
        .core.irq_priority       = NRF_MESH_IRQ_PRIORITY_LOWEST,
        .core.lfclksrc           = DEV_BOARD_LF_CLK_CFG,
        .core.p_uuid             = NULL,
        .models.models_init_cb   = models_init_cb,
        .models.config_server_cb = config_server_evt_cb
    };
    ERROR_CHECK(mesh_stack_init(&init_params, &m_device_provisioned));
}

mesh_stack_init()SDK側で提供されているトップレベルのAPIです。初期化処理を抜粋します。 今回は、これより下は追いかけませんが、nRF Meshアーキテクチャで見たDevice State ManagerとAccessを初期設定しているようです。

|> mesh/stack/src/mesh_stack.c

uint32_t mesh_stack_init(const mesh_stack_init_params_t * p_init_params,
                         bool * p_device_provisioned)
{
    uint32_t status;

    /* Initialize the mesh stack */
    status = nrf_mesh_init(&p_init_params->core);

    /* Initialize the access layer */
    dsm_init();
    access_init();

    /* Initialize the configuration server */
    status = config_server_init(p_init_params->models.config_server_cb);

    /* Initialize the health server for the primary element */
    status = health_server_init(&m_health_server, 0, DEVICE_COMPANY_ID,
                                p_init_params->models.health_server_attention_cb,
                                p_init_params->models.p_health_server_selftest_array,
                                p_init_params->models.health_server_num_selftests);

    /* Load configuration, and check if the device has already been provisioned */
    mesh_config_load();

    (void) dsm_flash_config_load();
    (void) access_flash_config_load();

    bool is_provisioned = mesh_stack_is_device_provisioned();

    return NRF_SUCCESS;
}

start

プロビジョニング用のパラメータを用意して、プロビジョニングを開始します。 その後、mesh_stack_start()プロトコルスタックを起動しています。

static void start(void)
{
    if (!m_device_provisioned)
    {
        static const uint8_t static_auth_data[NRF_MESH_KEY_SIZE] = STATIC_AUTH_DATA;
        mesh_provisionee_start_params_t prov_start_params =
        {
            .p_static_data    = static_auth_data,
            .prov_complete_cb = provisioning_complete_cb,
            .prov_device_identification_start_cb = device_identification_start_cb,
            .prov_device_identification_stop_cb = NULL,
            .prov_abort_cb = provisioning_aborted_cb,
            .p_device_uri = EX_URI_LS_CLIENT
        };
        ERROR_CHECK(mesh_provisionee_prov_start(&prov_start_params));
    }

    mesh_app_uuid_print(nrf_mesh_configure_device_uuid_get());

    ERROR_CHECK(mesh_stack_start());

    hal_led_mask_set(LEDS_MASK, LED_MASK_STATE_OFF);
    hal_led_blink_ms(LEDS_MASK, LED_BLINK_INTERVAL_MS, LED_BLINK_CNT_START);
}

プロトコルスタックを起動してから、プロビジョニング、ではないんですね? プロビジョニングのlistenを開始してから、プロトコルスタックの動的な部分が動き始めれば良い、ということでしょう。多分。

mesh_stack_start()の中では、ネットワーク層トランスポート層を有効化し、ベアラ層からのイベント通知を開始しています。

最後に、LEDをOFF状態にして、点滅の期間を設定しています。

start()が終わって、main()に戻ると、sd_app_evt_wait()を無限ループしています。省電力化のための割り込み待ちですね。

int main(void)
{
    initialize();
    start();

    for (;;)
    {
        (void)sd_app_evt_wait();
    }
}

これで後は、基本的にイベントに対するハンドラで動作します。

nRF5 SDK for Meshで学ぶBluetooth mesh①~プロトコル概要~

www.nordicsemi.com

実装を見てみないと、どういうものなのかよくわからないので、NordicのSDKを解析しながら、どういうものか理解していきます。

はじめに

Bluetooth meshは、2017年夏に採用された新しい規格です。Bluetooth Low Energyの技術を応用して、メッシュネットワークを構築します。 早い話が、BLEデバイス同士が通信して、より遠くへ通信できるようになります。

今回はソースコードに手を付ける前に、Bluetooth SIGのBluetooth meshネットワークを読んで、Bluetooth meshの要点をまとめていきます。

Bluetooth meshの基本

概念と用語

meshとポイントツーポイント

BLEは、P2Pの1対1通信を行い、これを「ピコネット」と呼んでいます。 一方、meshネットワークは多対多接続になるため、mesh内の各デバイスはどれでも互いに通信できます。 メッセージは中継転送できるため、エンドツーエンドの通信距離が各ノードの通信領域をはるかに超えます。

バイスとノード

meshネットワーク内のデバイスノード、そうでないデバイスプロビジョニングされていないデバイスと呼びます。 デバイスをノードに変えるプロセスをプロビジョニングと呼びます。

プロビジョニングは、プロビジョニングされていないデバイスに一連の暗号化キーを渡し、プロビジョナーバイススマートフォンタブレット)に認識させるセキュリティ手順です。

暗号化キーには、NetKeyと呼ばれるものがあります。このキーを所有することでノードとして認められます。

エレメント(要素)

ノードには、複数の部分から構成されていて、各部分を個別に制御できるものがります。このような構成部分をエレメントと呼びます。

メッセージ

他のノードにステータスを問い合わせたり、他のノードを制御する場合、該当するタイプのメッセージを送信します。 メッセージには大きく肯定応答要求肯定応答不要の2種類に分類できます。

アドレス

メッセージはアドレス間で送受信されます。 Bluetooth meshには3種類のアドレスが定義されています。

  • ユニキャストアドレス:個々のエレメントを一意に識別するアドレスです。
  • グループアドレス:1つまたは複数のエレメントを示すマルチキャストアドレスです。Bluetooth SIGが定義したSIG Fixed Group Addressと動的に割り当てられたアドレスがあります。
  • 仮想アドレス:1つまたは複数のノード内の1つまたは複数のエレメントに割り当てることができます。エレメントを関連付けることのできるUUIDで、ラベルのようなものです。

パブリッシュ / サブスクライブ

メッセージを送信することをパブリッシュ、特定アドレスに送られたメッセージを選んで処理することをサブスクライブと呼びます。

メッセージはグループアドレスか仮想アドレス宛に送信されます。ドキュメント内の図を参照すると理解しやすいです。

f:id:tomo-wait-for-it-yuki:20190211112345p:plain
パブリッシュ/サブスクライブ

ステートとプロパティ

エレメントが持つ状態をステートという概念で表します。ステートは、利用できるコンテキストが限られており、例えば、On / Offといった状態を定義します。

プロパティはBLEのキャラクタリスティック(特性)相当のものです。プロパティは、エレメントに関連した値であり、例えば、センサの現在の温度といったものを表現します。 プロパティは、読み取り専用のManufacturerと、読み書き可能なAdminの2種類があります。

メッセージ、ステート、プロパティ

meshでは、操作はメッセージによって開始します。メッセージは、GET / SET / STATUSの3つのタイプに分けられます。

  • GET: ノードに対して指定したステートの値を要求します。応答はSTATUSメッセージとして送信されます。
  • SET: ノードの指定したステート値を変更します。肯定応答要求のSETメッセージに対しては、STATUSメッセージが返されます。
  • STATUS: GET/SETに対する応答、または、タイマなどにより独立して送信されます。

ステートの指定は、メッセージ内のオペコードを使います。プロパティの指定は、16ビットのプロパティIDを使います。

ステートの遷移、バインドされたステート

ステートの遷移は、瞬時に起きることも、遷移時間と呼ばれる時間を経て起きることもあります。 1つのステートの変更が、別ステートの変更を引き起こすような関係を、ステートがバインドされていると言います。

モデル

まだ、いまいちしっくり来ていません。

モデルは以上の概念をひとまとめにしたものです。モデルには3つのカテゴリがあります。

  • サーバーモデル
  • クライアントモデル
  • コントロールモデル

モデルはイミュータブルです。

汎用項目

異なる種類のデバイス間でも、同じ意味合いを持つステートがあります。例えば、オンオフです。 Bluetooth meshのモデルの仕様で、Generic OnOffや、Generic Levelのように再利用可能なステートが、各種定義されています。

このようなGenericステートに対するGenericメッセージも定義されています。

シーン

シーンは複数のステートを保存したもので、特別なタイプのメッセージを受信したときや、指定された時間に呼び出すことができます。 16ビットのシーンナンバーで識別されます。

シーンを使うことで、予め用意されたステートを複数のノードに一度に設定できます。

プロビジョニング

バイスがノードになるプロセスです。5つのステップを経て進められます。

  1. ビーコニング
  2. インビテーション
  3. パブリックキーの交換
  4. 認証
  5. プロビジョニングデータの配布
ビーコニング

<<meshビーコン>>ADタイプを使用したアドバタイジングパケットを送信します。 新しいデバイスのアドバタイジングは、ユーザー側でボタンを押すなどの操作をして、開始する必要があるかもしれません。

インビテーション

プロビジョナーがデバイスに、Provisioning Invite PDUでインビテーションを送ります。 デバイスは、これに応答して、Provisioning Capabilities PDUで自身の情報を返します。

パブリックキーの交換

プロビジョナーとデバイスはお互いのパブリックキーを交換します。

認証

バイスは、LEDを点滅されるなど、デバイスの機能を使って、1桁以上のランダムな数値をユーザーに示します。 ユーザーはデバイスから示された数値をプロビジョナーに入力します。 プロビジョナーとデバイスとの間で、その数値に関係した暗号化通信が行われ、それを通してお互いの認証が完了します。

プロビジョニングデータの配布

認証が済むと、デバイスのプライベートキーと、交換されたお互いのパブリックキーを使ってセッションキーが生成されます。 セッションキーを使って、セキュアなプロビジョニングプロセス完了のデータ配布を行います。このデータには、NetKeyも含まれます。

プロビジョニングが完了すると、デバイスは、ネットワークのNetKey、IVIndexと呼ばれるセキュリティパラメータ、プロビジョナーから割り当てられたユニキャストアドレスを持つことになり、デバイスはノードになります。

機能

ノードに、4つのオプション機能を持たせることができます。

  • Relay
  • Proxy
  • Friend
  • Low Power

Relayノード

Relayノードは、受信したメッセージを転送できます。 このリレー機能によって、メッセージはノード間をホップし、meshネットワーク全体を横断して宛先に届きます。

Low PowerノードとFriendノード

電力が限られていて、可能な限り節電するようなノードはLow Powerノード (LPN)に指定します。LPNは、電力制約のないFriendノードと連携して機能します。 Friendノードは、LPNノードに対して送信されたメッセージを保管し、LPNからポーリングされると、保管したメッセージを送信します。

LPNとFriendノードの関係がフレンドシップです。

Proxyノード

Bluetooth meshのプロトコルスタックを備えていないBLEデバイスが、meshネットワークと通信できるようにするノードをProxyノードと呼びます。 Proxyノードは、GATTインタフェースを提供し、プロキシプロトコルに対応します。

meshのシステムアーキテクチャ

Bluetooth meshのアーキテクチャについて見ていきます。

f:id:tomo-wait-for-it-yuki:20190211123027p:plain
Bluetooth meshのアーキテクチャ

meshアーキテクチャの最下層は、Bluetooth LEです。これは、BLEのスタック全体であり、meshアーキテクチャに必要な基本的な無線通信能力を提供します。

ベアラ層

meshのメッセージ送受信をするためのベースとなる通信システムです。 アドバタイジングベアラとGATTベアラが定義されています。

アドバタイジングベアラは、Bluetooth LEのGAPのアドバタイジング機能とスキャニング機能を使って、meshのPDUを送受信します。

GATTベアラは、アドバタイジングベアラに対応していないデバイスが、間接的にmeshネットワークのノードと通信できるようにします。 プロキシプロトコルは、特別に定義されたGATT特性を利用して、通信します。

ネットワーク層

メッセージアドレスタイプとネットワークメッセージ形式を定義します。 複数のベアラに対応でき、それぞれのベアラには、同じノード内のエレメント間通信に使用されるインタフェースを含む、複数のネットワークインタフェースを対応させます。

ネットワーク層では、どのネットワークインタフェースを介してメッセージを出力するかを決めます。ベアラ層から届いたメッセージには入力フィルタが適用され、ネットワーク層で処理する必要があるか判定されます。出力もフィルタが適用され、ドロップするかどうかの判定が行われます。

リレー機能とプロキシ機能は、ネットワーク層で実装されます。

下位トランスポート層

必要であればPDUのセグメント化と再組立てを行います。

上位トランスポート層

アクセス層とやりとりするアプリケーションデータの暗号化、複号、認証を行います。 また、トランスポートコントロールメッセージも扱います。 これは、フレンドシップやハートビートに関連するメッセージがあります。

アクセス層

アプリケーションが上位トランスポート層をどのように利用できるかを定義します。

  • アプリケーションデータの形式の定義
  • 暗号化、複号プロセスの定義と制御
  • 認証

基盤モデル層

meshネットワークの構成と管理に関連するモデルの実装を扱います。

モデル層

モデル層はモデルの実装、すなわち、1つまたは複数のモデル仕様で定義された動作、メッセージ、ステート、ステートのバインド等の実装を扱います。

セキュリティ

Bluetooth LEでは、セキュリティはオプションとして存在します。

Bluetooth meshでは、セキュリティは必須です。

meshセキュリティの基本

  1. meshの全メッセージは暗号化され、認証されなくてはならない
  2. ネットワーク、アプリケーション、デバイスのセキュリティはそれぞれ別に扱う
  3. セキュリティキーは、meshネットワークができた後も、Key Refreshを通して変更できる
  4. メッセージを難読化すると、ネットワーク内で送られたメッセージを追跡することが難しくなり、ノードの追跡を困難にするプライバシーの仕組みとなる
  5. meshのセキュリティはネットワークをリプレイ攻撃から守る
  6. バイスをmeshネットワークに追加してノードにするプロセスのものが安全なプロセスである
  7. trashcan攻撃を防ぐ方法としてノードはネットワークから安全に外すことができる

懸念の分離とmeshのセキュリティキー

3つのセキュリティーを組み合わせて使用することで、meshの異なる側面のセキュリティを保護します。 このことにより、懸念の分離というmeshセキュリティにおいて重要な点を実現します。

meshネットワークの全ノードは、ネットワークキー (NetKey) を持っています。この共通のキーを持っていることで、ノードはネットワークの一員になります。 NetKeyを持つノードは、データを複号してネットワーク層までの認証を行い、中継などのネットワーク機能を実行できます。しかし、アプリケーションデータの複合はできません。 ネットワークはサブネットに分割できます。そのサブネット内では、メンバーのみに与えられるNetKeyを使います。

特定のアプリケーションデータを複号できるのは、適切なアプリケーションキー (AppKey) を持ったノードだけです。AppKeyは、そのアプリケーションに関わることができるタイプのノードのみが所有できます。例えば、照明は照明アプリケーションのAppKeyを持ちますが、暖房システムのAppKeyは持ちません。 AppKeyは、上位トランスポート層がメッセージをアクセス層に引き渡す前に、複号、認証するために使用されます。

最後の1つは、デバイスキー (DevKey) です。これは特別なアプリケーションキーです。各ノードは、プロビジョナーのみに知られている一意のDevKeyを持ちます。 DevKeyはプロビジョニングのプロセスでセキュリティを確保するために使用されます。

ノードの削除、キーリフレッシュ、ごみ箱攻撃

ノードを破棄する場合や、ノードを売却する場合、ノードのデバイスと中のキーが元のネットワークへの攻撃に使用できないようにすることが重要です。

プロビジョナーのアプリケーションがノードをブラックリストに追加することで、Key Refresh Procedureと呼ばれる手順が開始し、ノードをネットワークから削除します。 Key Refresh Procedureにより、ブラックリストに載っているノード以外のネットワーク内の全ノードに新しい、ネットワークキーやアプリケーションキーが支給されます。

プライバシー

NetKeyから生成されるプライバシーキーは、送信元アドレスなどのネットワークPDUのヘッダー値を難読化するために使用されます。 そのため、たまたま傍受されることがあっても、デバイスなどを追跡するには使用できないことが保証できます。 トラフィック分析に基づいた攻撃も難しくします。

リプレイ攻撃

リプレイ攻撃とは、通信を傍受した者が、後でメッセージを再送信することによってなりすます攻撃方法です。

Bluetooth meshには、リプレイ攻撃への防御機能が備わっています。防御機能のベースは、Sequence Number (SEQ) とIV Indexと呼ばれるネットワークPDUの2つのフィールドです。 このSEQとIV Indexが、前回の有効なメッセージから受け取ったSEQとIV Index以下であった場合、リプレイ攻撃に関連している可能性が高いため、メッセージを破棄します。

Bluetooth meshの仕組み

Bluetooth meshはmanaged floodingと呼ばれる手法を使ってメッセージを配信します。 メッセージはブロードキャスト配信され、無線範囲内にある全ノードがメッセージを受け取り、中継機能が設定されていれば、メッセージを中継します。

managed floodingにより、メッセージは、ネットワーク内の複数の経路を通って宛先に届きます。これは、ネットワークの信頼性を非常に高めます。

Managed Flooding

Bluetooth meshネットワークでは、floodingの強みを活用しながら、最適化を図ることで、信頼性と効率性を高めています。

ハートビート

ノードはハートビートメッセージを定期的に送り、ノードがアクティブであることを知らせます。

TTL (Time to Live)

TTL (有効生存時間) は、Bluetooth meshのすべてのPDUに含まれるフィールドで、メッセージを中継するホップ数の上限を指定します。 メッセージが必要以上に中継されるのを避け、中継と省電力をコントロールします。

メッセージキャッシュ

全ノードに実装されなければならないものです。 最近受け取ったメッセージが保存され、同じメッセージが届いた場合、すぐに破棄されます。

フレンドシップ

Bluetooth meshネットワークで最も重要な最適化の仕組みと言えます。 Friendノードと連携することで、Low Powerノードはかなりの電力を節約できます。

次回から、nRF5 SDK for Meshを見ていきます。

Zephyrのecho_serverサンプルを理解する

echo_server

github.com

Zephyrのecho_serverサンプルを解析し、Zephyrでのネットワークアプリケーション作成方法の理解を目指します。

一通り見てみると、割と普通のTCPサーバアプリケーションでした。

configs

重要そうなkernel configを確認します。

|> prf.conf

# Generic networking options
# ネットワーク機能を有効化します
CONFIG_NETWORKING=y
CONFIG_NET_UDP=y
CONFIG_NET_TCP=y
CONFIG_NET_IPV6=y
...

# Logging
# ログと統計情報出力を有効化します
CONFIG_NET_LOG=y
CONFIG_NET_STATISTICS=y
...

# Network shell
# Zephyrではsubsystemごとにshellを持ちます。
# ネットワークのshellを有効化しています。
CONFIG_NET_SHELL=y

今回は、enc28j60 Ethernet MACコントローラを使用します。 このコントローラ用のkernel configを確認します。

|> overlay-enc28j60

# L2 Ethernetはここで有効化しています。
CONFIG_NET_L2_ETHERNET=y

# ENC28J60 Ethernet Device
# driverを組み込みます
CONFIG_ETH_ENC28J60=y
# インスタンスを1つ作成します。
CONFIG_ETH_ENC28J60_0=y
# device treeで公開されるSPIのソフトウェアインタフェースを指定します。
CONFIG_ETH_ENC28J60_0_SPI_PORT_NAME="SPI_1"

Zephyrのおもしろいところは、device treeでハードウェアを定義して、kernel configでソフトウェアを定義する、という使い分けをしている点です。

SPI_1という名前のSPIポートを使用する設定をしています。このSPI_1は、device treeにおいて、実際のGPIOピンなどを利用したハードウェア定義に対してつけたラベルです。 これがZephyrにおけるソフトウェアインタフェースとして、kernel configで利用されます。

source files

いくつかのファイルに分割されています。トップは、echo-server.cです。

$ ls src/
common.h  echo-server.c  tcp.c  test_certs.h  udp.c  vlan.c

本記事では、TCPの動作を追っていきます。 udp.cvlan.cは解析対象外にします。

include header

まずは、includeしているheaderを確認します。

#include <zephyr.h>
#include <linker/sections.h>
#include <errno.h>

#include <net/net_pkt.h>
#include <net/net_core.h>
#include <net/net_context.h>
#include <net/net_app.h>

#include "common.h"

linker/sections.hが興味深いですが、どこで使われているのか、よくわかりませんでした…。

common.hは、関数が宣言されています。 後は、2つのマクロ定義があります。

#define MY_PORT 4242

#define MAX_DBG_PRINT 64

mainの開始と終了

echo_serverのmain()を見ていきます。

void main(void)
{
    init_app();

    if (IS_ENABLED(CONFIG_NET_TCP)) {
        start_tcp();
    }
...
    k_sem_take(&quit_lock, K_FOREVER);

    NET_INFO("Stopping...");

    if (IS_ENABLED(CONFIG_NET_TCP)) {
        stop_tcp();
    }
...
}

init_app()を見ます。

static struct k_sem quit_lock;
...

static inline int init_app(void)
{
    k_sem_init(&quit_lock, 0, UINT_MAX);

    // 今回は対象外です。
    init_vlan();

    return 0;
}

セマフォを使っています。

Zephyr doc Semaphoresを見る限りでは普通ですね。

k_sem_init()の第3引数では、セマフォカウントの初期値を0、リミットをUINT_MAXに設定しています。

main()に戻って、start_tcp()を一旦飛ばして、k_sem_take()が何をしているかを説明します。

 if (IS_ENABLED(CONFIG_NET_TCP)) {
        start_tcp();
    }
...
    k_sem_take(&quit_lock, K_FOREVER);

    NET_INFO("Stopping...");

    if (IS_ENABLED(CONFIG_NET_TCP)) {
        stop_tcp();
    }

k_sem_take(&quit_lock, K_FOREVER);では、第2引数においてミリ秒単位で指定した期間でセマフォが獲得できない場合、タイムアウトになります。 ここでは、K_FOREVERを指定しており、これはセマフォが取得できるまで、制限なく待ち続けることを意味します。

このアプリケーションは、セマフォが獲得できると、TCPを停止して、アプリケーションを終了します。では、セマフォはどうなると獲得できるのでしょうか? k_sem_init()セマフォの初期値は0になっているため、そのままではセマフォを取得できません。

答えは、下の関数にあります。

void quit(void)
{
    k_sem_give(&quit_lock);
}

ここで、セマフォを1つ解放しています。このquit()が呼ばれると、セマフォが1つインクリメントされるため、結果として、アプリケーションが終了します。

このquit()を呼ぶのは、TCPパケットの受信処理ハンドラ内です。 エコーに失敗すると、quit()を呼び出し、アプリケーションが終了する仕組みのようです。

static void tcp_received(struct net_app_ctx *ctx,
...
{
...
    ret = net_app_send_pkt(ctx, reply_pkt, NULL, 0, K_NO_WAIT,
                   UINT_TO_POINTER(net_pkt_get_len(reply_pkt)));
    if (ret < 0) {
        NET_ERR("Cannot send data to peer (%d)", ret);
        net_pkt_unref(reply_pkt);

        quit();
    }
}

TCPエコー

start_tcp()で何をしているか解析していきます。

 if (IS_ENABLED(CONFIG_NET_TCP)) {
        start_tcp();
    }

Zephyr Network Application API

本アプリケーションでは、Zephyr doc Network Application APIを主に利用します。

ドキュメントによると、シンプルなTCP serverアプリケーションは次ステップで作ることができます。

  1. net_app_init_tcp_server()IPアドレスTCPポートを設定する
  2. net_app_set_cb()イベントハンドラを登録する
  3. オプションで、net_app_server_tls()TLSを有効にする
  4. net_app_listen()でクライアントからの接続を待つ

すごく普通のTCPサーバアプリケーションですね。

start_tcp

まずは、関数全体を掲載します。

|> tcp.c

static struct net_app_ctx tcp;
...
void start_tcp(void)
{
    int ret;
    // `#define MY_PORT 4242` in common.h
    ret = net_app_init_tcp_server(&tcp, NULL, MY_PORT, NULL);
    if (ret < 0) {
        NET_ERR("Cannot init TCP service at port %d", MY_PORT);
        return;
    }

#if defined(CONFIG_NET_CONTEXT_NET_PKT_POOL)
    net_app_set_net_pkt_pool(&tcp, tx_tcp_slab, data_tcp_pool);
#endif

    ret = net_app_set_cb(&tcp, NULL, tcp_received, NULL, NULL);
    if (ret < 0) {
        NET_ERR("Cannot set callbacks (%d)", ret);
        net_app_release(&tcp);
        return;
    }

#if defined(CONFIG_NET_APP_TLS)
...
#endif

    net_app_server_enable(&tcp);

    ret = net_app_listen(&tcp);
    if (ret < 0) {
        NET_ERR("Cannot wait connection (%d)", ret);
        net_app_release(&tcp);
        return;
    }
}

メモリの設定をします。 これでアプリケーション専用のメモリプールが用意されます。

#if defined(CONFIG_NET_CONTEXT_NET_PKT_POOL)
    net_app_set_net_pkt_pool(&tcp, tx_tcp_slab, data_tcp_pool);
#endif

イベントハンドラを登録します。 connect, receive, send, closeのハンドラが登録できます。 ここでは、receiveだけ登録しています。

 ret = net_app_set_cb(&tcp, NULL, tcp_received, NULL, NULL);

後は、サーバは有効化して、listenするだけです。

 // サーバを有効化します
    net_app_server_enable(&tcp);

    // コネクションを待つ状態になります。
    ret = net_app_listen(&tcp);

これで、コネクションが確立して、データを受信すると、tcp_receivedのコールバックが呼ばれます。

tcp_received

パケット受信時の処理を見ていきます。受信したパケットからreply_pktを作成して、net_app_send_pkt()でpeerに送信しているだけです。

static void tcp_received(struct net_app_ctx *ctx,
             struct net_pkt *pkt,
             int status,
             void *user_data)
{
    struct net_pkt *reply_pkt;
...
    reply_pkt = build_reply_pkt(dbg, ctx, pkt, NET_TCPH_LEN);

    net_pkt_unref(pkt);
...
    ret = net_app_send_pkt(ctx, reply_pkt, NULL, 0, K_NO_WAIT,
                   UINT_TO_POINTER(net_pkt_get_len(reply_pkt)));
    if (ret < 0) {
        NET_ERR("Cannot send data to peer (%d)", ret);
        net_pkt_unref(reply_pkt);

        quit();
    }
}

build_reply_pkt()はアプリケーション内で定義している関数です。こちらは後で内容を見ます。 受信したパケットから、エコーバックするパケットを作成した後、net_pkt_unref(pkt)しています。

Zephyr doc Networking API / Network and application librariesによると、コールバックで渡されるpktの解放は、コールバック側がやらないといけないようです。

net_app_send_pkt()では、送信成功時はnet_app_send_pkt()reply_pktが消費されて、失敗時は自分で解放しています。 APIドキュメントを見ると、これが正しい使い方です。

If the function return < 0, then it is caller responsibility to unref the pkt.

C言語は、こういうところきついですね…。

build_reply_pkt()を見ていきます。受信したpktからデータをコピーして、reply_pktを返しています。

struct net_pkt *build_reply_pkt(const char *name,
                struct net_app_ctx *ctx,
                struct net_pkt *pkt,
                u8_t proto_len)
{
    struct net_pkt *reply_pkt;
    struct net_buf *frag;
...
    reply_pkt = net_app_get_net_pkt(ctx, net_pkt_family(pkt), BUF_TIMEOUT);
...
    net_pkt_set_appdatalen(reply_pkt, net_pkt_appdatalen(pkt));

    frag = net_pkt_copy_all(pkt, 0, BUF_TIMEOUT);
...
    net_pkt_frag_add(reply_pkt, frag);

    return reply_pkt;
}

大体、アプリケーションとしての使い方は理解できました。

組込みRust考察②~効率良く安全な組込み開発をしたい~

はじめに

tomo-wait-for-it-yuki.hatenablog.com

前回の続きです。 今回は、Rustを使うことによる生産性について、考察します。

言語仕様、エコシステム、コミュニティによる生産性向上

生産性は可視化が難しいです。 そのため、本トピックではわかりやすく数値を比較するようなことは、できません。

Rustは厳格なルールをプログラマに課しますが、Rustの厳格なルールはバグを減らすことで、生産性の向上に寄与するでしょう。 Rustは、とりあえず動くものを作る時には、堅苦しい言語かもしれません。しかし、正しく動くものを作る時は、絶大な力を発揮します。

C言語プログラマに最大限の自由を与え、その自由による責任をプログラマに負わせます。 Rustは、プログラマに義務に負わせ、その分保証を与えます。

unsafeブロックを含まないRustの関数は、コンパイルできた時点でスレッドセーフです。 Cでは、実行時の挙動からしか発見できないバグの多くが、Rustではコンパイラによって暴かれます。 Rustにおいて、コンパイルエラーと格闘することは、Rustのシンタックスを守るというより、正しい設計を考えることなのです。

また、プログラマが生産性を高めるための機能やエコシステムが整備されています。 次のトピックについて、紹介します。

  • トレイト / ジェネクリクス
  • 代数的データ型 / パターンマッチ
  • Cargo
  • LLVM
  • Rust Embedded WG

トレイト / ジェネリクス

他のプログラミング言語と同様に、Rustでもポリモーフィズムが利用できます。 ポリモーフィズムを適切に利用することで、再利用性の高いコードを記述することができます。

Rustでは、トレイトとジェネリクス、という機能を使います。

トレイト

トレイトは、C++の抽象基底クラス、Javaのインタフェースに似たものです。 例として、SPIデバイスにデータを書き込むためのトレイトを考えてみます。

世の中には様々なSPIデバイス(ここではSPI Masterを意味します)があります。 ただ、SPIをインタフェースとして使うデバイスドライバやアプリケーションとしては、それを意識せず、汎用的に書きたいと思うのが普通です。

例えば、8-bit単位でレジスタにデータを書き込みできるSPIデバイスに書き込む関数は、次のように書くことができるでしょう。

/// ある8-bit単位で書き込めるSPIデバイスを表すstruct
struct MySpiU8 {
    ...
}

impl MySpiU8 {
    /// 書き込み関数。エラー処理は今は、考えません。
    fn write(&mut self, byte: u8) {
        // Do something for MySpiU8
        // 生ポインタを使って、メモリマップドデバイスに書き込みます。
        unsafe { &self.addr as *const _ as *mut u8 = word };
    }
}

このMySpiU8を使うアプリケーションは、MySpiU8専用のアプリケーションになってしまいます。

fn spi_write(spi: &mut MySpiU8, byte: u8) {
    spi.write(byte);
}

バイスが例えばAnotherSpiU8に変わると、MySpiU8としている部分を書き直す必要があります。 SPIデバイスが変更になっても、アプリケーションを変更しなくて良いようにしたいです。

そこでトレイトの出番です。 例えば、SPIデバイスの書き込み処理を行うトレイトを次のように定義します。これはあくまでも、インタフェースの定義です。

pub trait SpiWrite {
    fn write(&mut self, word: u8);
}

このトレイトを、MySpiU8AnotherSpiU8に対して実装します。

impl SpiWrite for MySpiU8 {
    fn write(&mut self, word :u8) {
        // Do something for MySpiU8
        unsafe { unsafe { &self.addr as *const _ as *mut u8 = word };
    }
}

impl SpiWrite for AnotherSpiU8 {
    fn write(&mut self, word :u8) {
        // Do something for AnotherSpiU8
        unsafe { unsafe { &self.addr as *const _ as *mut u8 = word };
    }
}

このようにSpiWriteトレイトを実装している両者は、次のように同じコードで扱うことができます。

fn spi_write(spi: &mut SpiWrite, byte: u8) {
    spi.write(byte);
}

これでアプリケーションコードの再利用性を高めることができます。

ジェネリクス

Rustでポリモーフィズムを行うための、もう1つの方法です。これは、C++のテンプレートに相当します。

トレイトで使用したSPIの例(下記に再掲載)では、データが8-bit固定でした。しかし、SPI Masterによっては、16-bit単位でレジスタにデータを転送できるかもしれません。

pub trait SpiWrite {
    fn write(&mut self, word: u8);
}

今のトレイトでは、転送のデータ幅が変更になると、結局アプリケーションを変更しなければなりません。 そこで、SpiWriteトレイトをジェネリック関数を使用するように修正します。

pub trait SpiWrite<W> {
    fn write(&mut self, word: W);
}

ここで、Wは任意の型であることを意味します。 この修正により、write()関数は、任意の型Wに対して適用できるようになります。

MySpiU8AnotherSpiU16があったとして、次のように定義します。任意の型Wに対して、u8u16という具体的な型を与えます。

impl SpiWrite<u8> for MySpi {
    fn write(&mut self, word :u8) {
        // Do something for MySpiU8
        unsafe { unsafe { &self.addr as *const _ as *mut u8 = word };
    }
}

impl SpiWrite<u16> for AnotherSpi {
    fn write(&mut self, word :u16) {
        // Do something for AnotherSpiU8
        unsafe { unsafe { &self.addr as *const _ as *mut u16 = word };
    }
}

これらの書き込みデータの幅が異なる2つの実装に対して、下記の関数は共通して使用することができます。

fn spi_write<W: Clone>(spi: &mut SpiWrite<W>, data: W) {
    // `clone()`は明示的なコピーを行う関数で、`Clone`トレイトによって実装されます。
    spi.write(data.clone());
}

ここで、W: Cloneは、Cloneトレイトを実装する任意の型を意味します。この: Cloneの部分は、trait boundと呼ばれ、型Wに対して満たすべき制約を与えます。 C++をご存知の方は、次こそ入るであろうConceptのことだと考えて下さい。

上記関数内で、dataclone()関数を持っていなければなりません。これを保証するのがCloneトレイトです。 trait boundにより、ジェネリクスが求めている型が、どのような特性を満足しなければならないか、を明示することができます。

トレイトオブジェクトとジェネリクス

さて、下記のspi_write()では、第一引数は&mut SpiWrite<W>を、第二引数は<W: Clone>を受け取ります。

fn spi_write<W: Clone>(spi: &mut SpiWrite<W>, data: W) {
    // `clone()`は明示的なコピーを行う関数で、`Clone`トレイトによって実装されます。
    spi.write(data.clone());
}

実は、この2つの使い方は、似て非なるものです。

前者の&mut SpiWrite<W>はトレイトオブジェクトと呼ばれます。後者の<W: Clone>ジェネリクスです。 重要な違いは、トレイトオブジェクトが動的なポリモーフィズムを実現しているのに対して、ジェネリクスは静的なポリモーフィズムを実現している点です。

一般的に、ジェネリクスの方がより高速に動作します。ジェネリクスに対しては、コンパイラがコードを自動生成します。 大雑把に言うと、今回の場合、spi_write()は次のような2つの関数に展開されます。

fn spi_write_u8(spi: &mut SpiWrite<u8>, data: u8) {
    spi.write(data.clone());
}

fn spi_write_u16(spi: &mut SpiWrite<u16>, data: u16) {
    spi.write(data.clone());
}

Rustでは、第一の選択肢はジェネリクスです。 ただし、Vecのようなコレクションに複数の型を格納したい場合は、ジェネリクスは使えません。

トレイトオブジェクトとジェネリクスを使い分けることで、高速かつ抽象度の高いコードを記述することができます。

代数的データ型 / パターンマッチ

代数的データ型とパターンマッチの組み合わせを使うことで、直感的で安全な場合分け処理が記述できます。 代数的データ型はenumとも呼ばれますが、想定読者が知っているC/C++enumよりはるかに強力です。

代数的データ型

Rustのenumデータを持つことができます。これは、どちらかというと共用体 (union) に似たものです。 しかし、Rustのenumは安全です。

enc28j60 driverのコードから、enumを利用している箇所を引用します。

enum Register {
    Bank0(bank0::Register),
    Bank1(bank1::Register),
    Bank2(bank2::Register),
    Bank3(bank3::Register),
    Common(common::Register),
}

enumの要素である、Bank0Bank1はそれぞれ別の型を持っています。 構造体を要素として持つこともできます。

もちろん、Cのenumと同様の使い方も可能です。

pub enum Register {
    ERDPTL = 0x00,
    ERDPTH = 0x01,
    EWRPTL = 0x02,
    EWRPTH = 0x03,
    ETXSTL = 0x04,
    ETXSTH = 0x05,
    ETXNDL = 0x06,
...
}

パターンマッチ

enumから値を取り出す時は、パターンマッチを使います。

    fn addr(&self) -> u8 {
        match *self {
            Register::Bank0(r) => r.addr(),
            Register::Bank1(r) => r.addr(),
            Register::Bank2(r) => r.addr(),
            Register::Bank3(r) => r.addr(),
            Register::Common(r) => r.addr(),
        }
    }

ここで重要なことは、Bank0型を格納すると、Bank0型としてしか取り出すことができない点です。 Cのunionと異なり、異なる型として処理することは、できません。

enumによるエラー定義

Rustのenumは、エラーを定義するときも便利です。例えば、異なるレイヤで発生したエラーを便利に定義することができます。 enc28j60は、SPIを使用したEthernet MACバイスであるため、SPIレイヤのエラーと、MACレイヤのエラーが発生します。 MACレイヤでのエラーはより上位レイヤでのエラーと考えることができます。

SPIレイヤでは複数のエラーが、MACレイヤではLate Collisionのエラーが発生するとします。

pub enum Error<E> {
    /// Late collision
    LateCollision,
    /// SPI error
    Spi(E),
}

上記のようにError enumを定義すると、SPIレイヤのエラーは、さらに任意のE型のデータを持つことができます。 まず、LateCollisionSpiどっちのエラーなのかをパターンマッチで処理し、Spiの場合はさらに、E型に対する処理を行うことができます。

このように異なるデータを持つenumに対して、switch caseのノリで処理することができます。

    match error {
        LateCollision => { /* error handling for `LateCollision` */ }
        Spi(e) => {
            match e {
                /* error handling for each of `E` type. */
            }
        }
    }

enumを使ったエラーハンドリング

Rustのエラーハンドリングでは、Result<T, E>を使用します。処理が成功した時には、T型を、エラー発生時にはEを保持します。 これは、ジェネリック (TEが任意の型) な代数的データ型です。

例えば、SPIデバイスに8-bitのデータを書き込む関数を考えます。

Rustのembedded_halには、blocking::spi::Writeというtraitがあります。 このtraitのwrite()関数は、SPIスレーブにW型のビット数単位で、データを送信します。 一般的なSPIデバイスの送信動作です。受信はしないため、成功時に受け取るデータはありません。そのため、成功時には空のタプル()が返ってきます。

pub trait Write<W> {
    // 任意のエラー型を関連型`Error`として使うことができます
    type Error;
    fn write(&mut self, words: &[W]) -> Result<(), Self::Error>;
}

このwrite()の実装例は次のようになります。わかりやすいように、具体的な型を書きます。

/// SPIデバイスを表すstructです。
pub struct Spi {
    ...
}

/// SPIのエラー定義です。
#[derive(Debug)]
pub enum SpiError {
    /// オーバーラン
    Overrun,
    /// モードフォルト
    ModeFault
}

/// `Write` traitをSpi structに実装します。
impl Write<u8> for Spi {
    // Self::Errorは、上で定義している`SpiError`になります。
    type Error = SpiError;

    fn write(&mut self, bytes: &[u8]) -> Result<(), SpiError> {
        // ステータスレジスタを読み込みます
        let status = self.sr.read();

        // SPIデバイスがエラー状態であれば、エラーを返します
        if sr.ovr().bit_is_set() {
            return Error(SpiError::Overrun)
        } else if sr.modf().bit_is_set() {
            return Error(SpiError::ModeFault)
        }

        // エラー状態でなれば、データを送信します
        for byte in bytes.iter() {
            unsafe { ptr::write_volatile(&self.spi.addr as *const _ as *mut u8, byte) }
        }
        Ok(())
    }
}

このSpiを使用するアプリケーションは、次のようにエラーハンドリングできます。

match spi.write(&data) {
    Ok(_) => { /* 成功時の処理 */ }
    Error(e) => {
        match e {
            SpiError::Overrun => { /* オーバーランのエラー処理 */ }
            SpiError::ModeFault => { /* モードフォールドのエラー処理 */ }
        }
    }
}

この例では愚直に書いていますが、エラーを上位に伝播する方法や、他のエラーと合成する方法など、様々なエラー処理がうまく書けるようになっています。

このように、代数的データ型とパターンマッチを使うことで、C言語よりはるかに強力で安全な場合分け処理を実現することができます。

Cargo

Cargoは、Rustのビルドシステム兼パッケージマネージャです。 何でも卒なくこなしてくれる頼もしい貨物です。 Rustによる快適開発ライフの3分の1くらいを担っていると思います。

package manager

まず、パッケージマネージャとして、こなして欲しい仕事を全て面倒見てくれます。

  • セマンティックバージョニングで依存関係を管理
  • 依存関係を、crates.io, git, pathの形で指定可能
  • Cargo.lockでビルドの再現性を確保
  • srcディレクトリ以下のファイルを自動で解析してビルド

crates.ioは、Rustのcrate (パッケージ) を登録する場所です。 ここにあるcrateであれば、cargoの設定ファイルCargo.tomlに次のように書くだけで、依存を解決してくれます。

[dependencies]
time = "0.1.12"

ローカルにtime crateがない場合、ソースコードを取得して、最終バイナリまで一気にビルドしてくれます。 crateのバージョンは、完全に指定することもできますし、セマンティックバージョニングで互換性が崩れない範囲で最新のものを取得することもできます。

gitで取得可能なcrateや、自身のプロジェクトの中で小さなcrateを複数使っても、簡単に管理できます。そのためRustでは小さなcrateを多く作ることが多いです。そうしても管理コストはあまり増えませんし、再利用できる部品が増えます。

[dependencies]
rand = { git = "https://github.com/rust-lang-nursery/rand" }
my_utils = { path = "../my_utils" }

srcディレクトリ下のファイルで、トップ (main.rslib.rs) から辿って利用しているソースファイルは自動的にビルドします。 面倒な設定は必要ありません。

project configuration

.cargo/configに様々な設定を記述することができます。組込みRustでは非常にお世話になります。

例えば、RISC-V向けのクロスコンパイラを使用し、カスタムリンカスクリプトを指定、実行時にはqemu-riscvを立ち上げるようにします。 これは、.cargo/configに次のように記述するだけです (ツールのインストールは別途必要です)。

# ビルドターゲットがRISC-Vの時の設定です
[target.riscv32imac-unknown-none-elf]
# ランナーとしてqemu-riscvを使います
runner = "qemu-system-riscv32 -nographic -machine sifive_u -kernel"
# 自作のリンカスクリプトでリンクします
rustflags = [
  "-C", "link-arg=-Tlinker.ld",
]

# デフォルトでRISC-Vをビルドターゲットにします
[build]
target = "riscv32imac-unknown-none-elf"

このように設定ファイルを用意すると、

cargo run

だけで、RISC-Vのバイナリが生成され、qemuでバイナリを実行します。

build script

ビルド時にRustで記述したビルドスクリプトを実行することができます。 例えば、依存するC言語のライブラリをビルドしたり、アセンブリで書いたブートストラップをビルドした上で、Rustのコードとリンクすることができます。

cc crateを使うと、非常に容易です。

extern crate cc;

fn main() -> Result<(), Box<Error>> {
    Build::new()
        .file("boot.s")
        .flag("-mabi=ilp32")
        .compile("asm");

    Ok(())
}

これで、boot.sコンパイルして、Rustのコードとリンクしてくれます。 ただし、クロスコンパイラを使う場合は、環境変数の設定が必要です。

コマンドラインでやる場合、次のようになります。

$ env CC=riscv32-unknown-linux-gnu-gcc  cargo run

external tools

binutils

cargoはプラグインシステムがあるため、サブコマンドを自由に作ることができます。 組込みで一番有用なのは間違いなくbinutilsでしょう。

次のコマンドでプラグインをインストールするだけで、cargoのサブコマンドとしてbinutilsが利用可能です。

$ rustup component add llvm-tools-preview
$ cargo install cargo-binutils --vers 0.1.4

下のような感じで使うことができます。

$ cargo objdump --bin app -- -d -no-show-raw-insn

フォーマッタ、lintツール

Rustのフォーマッタや、ソースコードを静的解析するlintツールもCargoのサブコマンドとして使うことができます。

cargo fmt
cargo clippy

LLVM

RustはLLVMをバックエンドに持つプログラミング言語であるため、クロスコンパイルがお手軽です。 ARMv7のクロスコンパイラをインストールするのは、コマンド1つです。

$ rustup target add thumbv7m-none-eabi

Rust Embedded WG

Rustの開発はコミュニティが行っています。 注力分野として組込みも挙げられており、Embedded WGがあります。

このEmbedded WGは、組込み開発で使用するcrateを開発したり、学習用ドキュメントを整備しています。 また、Rust本体に組込みで欲しい機能を要望するブリッジとしての役割も担っています。

下のようなターゲットに、注力しています。

また、SVD (System View Description)ファイルから、マイコンペリフェラルにアクセスするRustコードを自動生成する、svd2rustのようなツール開発も行われています。

Rustで普通にプログラミングするだけでMISRA-Cのルールを90%満足できる

はじめに

2019/2/10追記

記事を書いてから気づいたのですが、正確には、Rustのアトリビュートをいくつか設定すれば、MISRA-Cのルールを90%満足できるでした。

私はMISRA-Cのコーディング規約でプログラミングしたことがないため、内容に誤りがありかもしれません。間違っている点があれば、ご指摘いただけるとありがたいです。

後、いつも通りですが、C言語を貶める意図は一切ありません。

昨日からtwitterで、Rustが組込みのセキュリティが重要な分野で広まると良いなぁ、という議論がありました。 その中で、車載では、やはりMISRA基準との関係が明確になること、ということが1つの基準になりそうでした。

github.com

@hashaskell さんから、MISRAコーディングルールのうち、Rustコンパイラアトリビュートの設定を含めて、保証するルールのリストを作成しているレポジトリを教えて頂きました。

結果のサマリを集計したところ、次のような結果でした。

status ルール数
デフォルトでコンパイラエラー 89
C言語有機能のためのルールで、Rustに該当機能がないものが 54
デフォルトでコンパイルエラーにならない 35

つまり、Rustでコンパイルが通った時点で、MISRAコーディング基準の80%を満たしていることになります。

Rustを使い始めてわかったのは、C/C++では長い間かけてゆっくり学んでいたような「良い書き方」を学ばないことには、 Rustではプログラムをコンパイルすることすらできない、ということだ。

プログラミングRustより

残り35個のルールが気になったため、1つずつその内容を確認しました。

6個は、Rustコンパイラまたはclippyの警告で検出可能です。 11個は、Rustの言語仕様上、問題にならないと考えられます(私がそう考える、というだけですが)。

ということで、Rustのアトリビュートを設定した上で、コンパイルでき、コンパイラの警告とclippyの警告を修正すると、MISRAコーディング基準の90%は満たすことになります。 コンパイラの警告とclippyの警告を修正するのは、割と普通のRustプログラミングの範囲です。 どうしても黙らせたい時もありますが

と、いうことで、Rustやろうぜ!

本記事の趣旨は以上です。

以下、コンパイルエラーにできない35個のルールを1個ずつ見ていった結果です。 興味のある方はご覧ください。

MISRA-Rust

MISRA-Rust

デフォルトでコンパイルエラーにならない例

MISRA-Rules

MISRAのルールは下から引用しています。

MISRA-C

コメント

3.1 「/*」や「//」という文字の並びをコメント内で使用してはならない

どうでも良さそうですね。

fn main() {
    /* /* Nested comment */ // Nested comment */
    //~^ ERROR nested comments
}

識別子

MISRAの基本方針が、「識別子を再利用しない」なので、Rustがこのあたりのルールに準拠しないのは当然です。 Rustはシャドーイングを許す言語ですが、これは次のような利点があります。

  • 可変性の制御
  • 中間変数

可変性の制御では、ある変数が変更不要になったことをコンパイラに教えることができます。

    let mut x = 10;
    /* We can modify x here. */

    let x = x;
    /* We no longer modify x! */

また、処理が連鎖するような場合、シャドーイングにより、誤って中間結果を使うことがなくなります。

    let data = 0;

    let data = do_something(data);
    let data = do_another(data);

    let data = data.unwrap();

シャドーイングする度に、その前のdataにはアクセスできなくなります。 これは、下手に一時変数を作るより安全ではないでしょうか。

5.1 外部識別子は異なったものにしなければならない

テストコードがおかしい気がします。外部に公開する識別子が一意になれば良い気がしますが…?

const ABC: i32 = 0;

fn main() {
    let abc: i32 = 1;
    //~^ ERROR Non-compliant - variable name shadows ABC
    let _ = abc + ABC;
}

5.2 同じスコープと名前空間で宣言された識別子は異なったものにしなければならない

当然シャドーイングが発生します。

fn main() {
    let engine_exhaust_gas_temperature_raw: i32 = 0;
    let engine_exhaust_gas_temperature_raw: i32 = 1;
    //~^ ERROR Non-compliant - variable name shadows engine_exhaust_gas_temperature_raw
}

5.3 内側のスコープで宣言された識別子は、外側のスコープで宣言された識別子を隠してはならない

はい。

fn main() {
    let i: i16 = 1;
    if true {
        let i: i16 = 0; //~ ERROR Non-compliant - `i` shadows outer scope.
        let _ = i;
    }
    let _ = i;
}

しかし、staticconstシャドーイングは、コンパイルエラーになります。

    static abc: i32 = 0;
    {
        let abc: i32 = 1;
    }
error[E0530]: let bindings cannot shadow statics
  |
4 |     static abc: i32 = 0;
  |     -------------------- the static `abc` is defined here
5 |     {
6 |         let abc: i32 = 1;
  |             ^^^ cannot be named the same as a static

そのため、グローバル領域に宣言したstatic変数や定数については、シャドーイングするとコンパイルエラーになります。 関数ローカルでしかシャドーイング起きない(?)ので、けっこう安全な気がします。

5.4 マクロ識別子は異なったものにしなければならない

通るんですね。意外。ちゃんとunused macro definitionの警告は出ます。

macro_rules! engine_exhaust_gas_temperature_raw {
    () => {
        3;
    };
}

macro_rules! engine_exhaust_gas_temperature_raw {
    //~^ ERROR Non-compliant - variable name shadows engine_exhaust_gas_temperature_raw
    () => {
        4;
    };
}

fn main() {
    let _ = engine_exhaust_gas_temperature_raw!();
    let _ = engine_exhaust_gas_temperature_raw!();
}

5.6 typedef名は一意の識別子でなければならない

ルール的には、関連型とか全部ダメですよね…。

fn main() {
    type U8 = bool;
    {
        type U8 = u8;
        //~^ ERROR Non-compliant - type name shadows U8
    }
}

リテラルと定数

7.2 符号なしの型で表現されているすべての整数定数には「u」または「U」接尾語を適用しなければならない

fn main() {
    let compliant_unsigned: u32 = 1u32;
    let unsigned: u32 = 1;
    //~^ ERROR Non-compliant - suffix specifying unsigned type required on integer constants
}

C言語と違って、下はちゃんとエラーになります。

    let unsigned: u32 = -1;
error[E0600]: cannot apply unary operator `-` to type `u32`
  |
2 |     let unsigned: u32 = -1;
  |                         ^^ cannot apply unary operator `-`
  |
  = note: unsigned values cannot be negated

C言語は、下のコードはコンパイルが通っちゃうので大変です。

    uint32_t u = -1;

7.4 オブジェクトの型が「const修飾文字へのポインタ」でない限り、文字列リテラルをオブジェクトに代入してはならない

普通にできちゃいますわな。

fn main() {
    let mut _l = "string literal";
    //~^ ERROR Non-compliant - string literal not const-qualified.
}

宣言と定義

8.1 型は明示的に指定されなければならない

まぁ、型推論ありますからね…。ただ、危ない暗黙の型変換が起きないため、明示的に型を書くメリットがほとんどありません。

fn main() {
    let x;
    //~^ ERROR Non-compliant - type not explicitly specified
    x = 1;

8.7 翻訳単位内での参照がただ1つである場合、関数やオブジェクトは外部リンケージを使用して定義してはいけない

これは、clippyさんでも警告が出なかったです。

pub const LIBRARY_GLOBAL: u32 = 0;
//~^ ERROR Non-compliant - public but not used

8.9 識別子が単一の関数内にのみ出現する場合、そのオブジェクトはブロックスコープで定義されなければならない

これも、clippyさんでも警告出ないですね。

const GLOBAL: u32 = 0;
fn main() {
    let _x = GLOBAL + 1;
    //~^ ERROR Non-compliant - global only used at block scope
}

8.13 ポインタは可能な限りconst修飾型を指さなければならない

これは、残りのコードで使っていなければ、mut外せるよ、の警告が出ます。

    let mut x: Box<u8> = Box::new(8);
    //~^ ERROR Non-compliant - "pointer" is not const-qualified.

実質的な型モデル

10.1 オペランドが不適切な実質的な型であってはならない

テストコードが変な気がします。MISRAは暗黙変換のことを言っていると思うのですが…。

    let x: i32 = 0xFF;
    let y = x << 2;
    //~^ ERROR Non-compliant - inappropriate essential type

CERT Cの似た項目にある、下がちゃんと動けば良いのでは? C言語では、0x0aが期待値ですが、最初に暗黙変換されてint32_tあたりになる結果、0xfaになります。

    uint8_t port = 0x5a;
    uint8_t result_8 = ( ~port ) >> 4;

Rustでは、これはちゃんと0x0aになります。4u8型推論されるため(?)ですかね。

    let x: u8 = 0x5a;
    let y = ( !x ) >> 4;

10.8 複合式の値は異なる実質的な型分類やより広い実質的な型にキャストしてはならない

これもテストケースがおかしい気がしますね…。複合式の中で最終結果より大きい型にキャストされてはいけない、というルールっぽいのですが。

    let x: u16 = 1;
    let y: u16 = 2;
    let _: u32 = (x + y) as u32;
    //~^ ERROR Non-compliant - composite cast to wider type

下のようなコードが通らなければ良い?下はコンパイルエラーになります。

    let x: u8 = 255;
    let y: u8 = (x + 1u16) as u8;

12.1 式の中の演算子の優先順位は明白でなければならない

下はコンパイルエラーにはなりません。clippyさんは()を付けなさい、と言ってくれます。

    let x: usize = 1;
    if x >= 2 << 2 + 1 as usize {
        //~^ ERROR Non-compliant - operator precedence can trip the unwary
    }

12.4 定数式の評価は、符号なし整数のラップアラウンドを引き起こしてはならない

へー、これ、コンパイル時はダメなんですね。

    let u8a: u8 = 0;
    let _x = u8a - 10;
    //~^ERROR Non-compliant - attempt to subtract with overflow

皆さんご存知の通り、実行時は定義されたパニックが発生します。

thread 'main' panicked at 'attempt to subtract with overflow', src/main.rs:3:14

副作用

13.2 式の値とその永続的な副作用は、すべての許可された評価順で同じでなければならない

Rustは式の評価順決まっていそうですが…? 明示的に書いてあるドキュメントは見つかっていませんが、Document function argument evaluation order (or lack thereof)などを見ても、左から右へ評価される模様です。

そうでないと、ボローチェッカがうまくチェックできない気がします。

/// This function has a side effect.
fn increment(x: &mut u8) -> &mut u8 {
    *x += 1;
    x
}

/// This function does not have a side effect.
fn add(x: u8, y: u8) -> u8 {
    x + y
}

fn main() {
    let mut x: u8 = 0;
    let _ = add(*increment(&mut x), x);
    //~ ERROR Non-compliant - evaluation order effects result
}

13.5 &&や||の論理演算子の右側のオペランドは、永続的な副作用を含んではならない

&&や||は、短絡評価されるので、これはいけません。clippyさんも叱ってくれないですね。

/// This function has a side effect.
fn not(x: &mut bool) -> &mut bool {
    *x = !*x;
    x
}

fn main() {
    let mut x: bool = true;

    if *not(&mut x) || *not(&mut x) {
        //~^ ERROR Non-compliant - right hand operand contains persistent side-effects
    }
}

制御文の式

14.1 ループカウンタは、実質的に浮動小数点型を持ってはいけない

ループカウンタ扱い...なのかな?

    let mut f: f64 = 0_f64;

    while f < 1.0 {
        f += 0.001_f64;
        //^ ERROR Non-compliant - loop counter with floating point type
    }

14.2 forループは適正に定義されなければならない

コンパイルエラーにはなりませんが、この書き方の場合、ループの回数自体は変化しません。100回きっちり周ります。

    let mut bound: u32 = 100;

    for i in 0..bound {
        bound -= i;
        //~^ ERROR Non-compliant - attempt to mutate range bound within loop
    }

clippyさんも以下の苦言を呈してくれます。

warning: attempt to mutate range bound within loop; note that the range of the loop is unchanged
  |
6 |         bound -= i;
  |         ^^^^^^^^^^

ただし、boundが途中でoverflowするので、実行時に定義されたパニックが発生します。

thread 'main' panicked at 'attempt to subtract with overflow', src/main.rs:7:9

14.3 制御式は不変であってはならない

意外とこれ叱ってもらえないんですね…。

    let a: i32 = 0;
    if (a < 10) && (a > 20) {
        //~^ ERROR Non-compliant - always true
    }

制御フロー

15.5 関数は、その最後に1つだけの出口を持たなけらばならない

まぁ、これはね…。必須ルールではありませんし。リソース解放忘れが通常は発生しないので、Rustではガンガン早期リターンすれば良いと思います。

    let x = 1;

    if x > 1 {
        return;
    }

    if x < 1 {
        return;
        //~^ ERROR Non-compliant - more than one exit point from function
    }

15.7 すべてのif ... else if構文は、else文で終了しなければならない

clippyさんも反応なし。

fn main() {
    let x = 1;
    if x == 2 {
        let _ = 1;
    } else if x == 3 {
        let _ = 2;
    }
    //~^ ERROR Non-compliant - no terminating `else`
}

switch文

16.6 すべてのswitch文は、少なくとも2つのスイッチ節を持たなければならない

clippyさんも反応なし。

    let i = 1;
    match i {
        _ => {} //~ ERROR Non-compliant - less that two clauses
    }

16.7 スイッチ式は実質的にブール型を持ってはいけない

clippyさんから、if/else式にしなよ、とお叱りを受けます。

    let i = true;

    match i as bool {
        //~^ ERROR Non-compliant - match on a boolean expression
        false => {
            let _ = 1;
        }
        _ => {
            let _ = 2;
        }
    }

関数

17.2 関数は、直接的または間接的に、自分自身を呼び出しはいけない

そうだった。再帰呼出し禁止なんですよね…。

17.7 void以外の戻り値の型を持つ関数が返す値は使用されなければならない

_で捨てるコードを書けば良いだけなのですが、それに意味があるか、という話ではありますね。 Result型は使わないと警告出ます。

fn func(para1: u16) -> u16 {
    para1
}

fn discarded(para2: u16) {
    func(para2);
    //~^ ERROR Non-compliant - `func` return discarded
}

fn main() {
    discarded(1);
}

17.8 関数パラメータを変更してはいけない

Rustでこんなコード書く人居るのかな…という気持ちになりながらも、コンパイルエラーにはなりません。

fn paramod(mut para: u16) -> u16 {
    para += 1; //~ ERROR parameter modified without persistent effect
    let _ = para;
    1
}

fn main() {
    paramod(1);
}

ポインタと配列

18.5 2段階を超える入れ子のポインタを宣言してはいけない

これ、clippyさんの警告が面白いです。

fn nesting(p: &&&&&&[u8; 10]) {
    let _ = ****p;
}

fn main() {
    let a = [5; 10];
    nesting(&&&&&&a);
    //~^ ERROR Non-compliant - reference nesting exceeds maximum allowed
}
warning: this argument is passed by reference, but would be more efficient if passed by value
  |
1 | fn nesting(p: &&&&&&[u8; 10]) {
  |               ^^^^^^^^^^^^^^ help: consider passing by value instead: `&&&&&[u8; 10]`

1個減らせ、とのことです。ちなみに&が1個になるまで、永遠に1個減らせ、と言われます。ウケる。

重なり合う記憶域

19.2 unionキーワードを使用してはならない

使えますが、unsafeです。

union UnionA {
    f1: i16,
    f2: i32,
}

fn main() {
    let mut u = UnionA { f2: 0 };
    unsafe { u.f1 = u.f2 as i16 };
    //~^ ERROR mismatched types
}

前処理指令

20.1 #include指令に対しては、他の前処理指令やコメントのみが先行しうる

#includeと違って、useは順番に依存しないので、ちょっと話が別な気がします。

struct MyStruct {
    a: u32,
}

fn func(_: MyStruct) {}

use std::fmt;
//~ ERROR Non-compliant - `include` directive preceeded by something other than macros or comments

impl fmt::Display for MyStruct {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "MyStruct{{ {} }}", self.a)
    }
}

fn main() {
    let s = MyStruct { a: 10 };
    func(s);
}

20.2 「'」、「"」または「\」文字、「/*」または「//」文字列がヘッダファイル名に存在してはならない

これ処理できるんですね。そもそもヘッダファイルがないので…。

include!("../../include/_'_.rs");
//~ ERROR invalid character `'` in crate name: `Rule_20_2_'`

fn main() {}

標準ライブラリ

21.1 予約済み識別子や予約済みマクロ名に対して#defineや#undefを使用してはならない

printlnって予約済みマクロなんでしょうか…?

macro_rules! println {
    //~ ERROR Non-compliant - redefinition of macro name
    () => {
        3;
    };
}

fn main() {
    println!();
}

21.2 予約済み識別子またはマクロ名を宣言してはならない

21.1と同じです。

参考

ESCR Ver. 2.0