nRF52840 CryptoCell 310を使った乱数生成サンプルコードの解析

CryptoCell 310ソフトウェアスタックを理解する目的で、コードが簡単そうだった乱数生成サンプルを解析します。

CryptoCell 310

f:id:tomo-wait-for-it-yuki:20190202082613p:plain
cc310ブロック図

nRF52840のProduct Specificationによると、CryptoCell 310は、複数の暗号化アクセラレータを内包するペリフェラルバイスになっています。 このデバイスの機能を使う場合、firmwareとして提供されているCryptCell library経由で処理を依頼します。

ソフトウェアスタックは、大まかには次のようになっています。

f:id:tomo-wait-for-it-yuki:20190202082645p:plain
nrf_crypto_library

nrf_cryptoというフロントエンドAPIがあり、後は、複数のbackendから1つを選ぶことができます。 今回興味があるのは、cc310 backendです。

|> nRF5_SDK/components/libraries/cryptoが該当しそうです。

$ tree -L 2
.
├── backend
│   ├── cc310
│   ├── cc310_bl
│   ├── cifra
│   ├── mbedtls
│   ├── micro_ecc
│   ├── nrf_hw
│   ├── nrf_sw
│   └── oberon
├── nrf_crypto_aead_backend.h
├── nrf_crypto_aead.c
├── nrf_crypto_aead.h
├── nrf_crypto_aead_shared.h
├── nrf_crypto_aes_backend.h
├── nrf_crypto_aes.c
├── nrf_crypto_aes.h
├── nrf_crypto_aes_shared.c
├── nrf_crypto_aes_shared.h
├── nrf_crypto_ecc_backend.h
├── nrf_crypto_ecc.c
├── nrf_crypto_ecc.h
├── nrf_crypto_ecc_shared.h
├── nrf_crypto_ecdh_backend.h
...

トップレベルにある*.cファイルがフロントエンドに該当します。 さらに、backendディレクトリ配下に、それぞれのbackendの実装があります。

rng example

コードが短いので、手始めにnRF5_SDK/examples/crypto/nrf_crypto/rngを解析します。

まずは、includeしているファイルを確認します。今回、重要そうなのは、nrf_crypto.hくらいです。

#include <stdbool.h>
#include <stdint.h>
#include "boards.h"
#include "nrf_log_default_backends.h"
#include "nrf_log.h"
#include "nrf_log_ctrl.h"
#include "nrf_crypto.h"

コード全体は、以下の通りです。CryptoCellの処理だけを抽出して見ていきます。

int main(void)
{
    ret_code_t ret_val;

    log_init();

    NRF_LOG_INFO("RNG example started.");

    ret_val = nrf_crypto_init();
    APP_ERROR_CHECK(ret_val);

    // The RNG module is not explicitly initialized in this example, as
    // NRF_CRYPTO_RNG_AUTO_INIT_ENABLED and NRF_CRYPTO_RNG_STATIC_MEMORY_BUFFERS_ENABLED
    // are enabled in sdk_config.h.

    NRF_LOG_INFO("Generate %u random vectors of length %u:", ITERATIONS, VECTOR_LENGTH);
    for (int i = 0; i < ITERATIONS; i++)
    {
        ret_val = nrf_crypto_rng_vector_generate(m_random_vector, VECTOR_LENGTH);
        APP_ERROR_CHECK(ret_val);
        NRF_LOG_HEXDUMP_INFO(m_random_vector, VECTOR_LENGTH)
    }

// 乱数の範囲を指定するサンプルなので、省略します。
...

    NRF_LOG_INFO("RNG example executed successfully.");

    for (;;)
    {
    }
}

nrf_crypto_init

まずは初期化からです。ヘッダでの宣言とコメントから見ていきます。

|> components/libraries/crypto/nrf_crypto_init.h

/**@brief Function for initializing nrf_crypto and all registered backends.
 *
 * @details Must always be called before any other @ref nrf_crypto function.
 *
 * @retval  NRF_SUCCESS         The initialization was successful.
 * @retval  NRF_ERROR_INTERNAL  An internal error occured in the nrf_crypt backend init.
 */
ret_code_t nrf_crypto_init(void);

backendは複数登録できて、nrf_crypto_init()の中では、登録したbackendを全て初期化するようです。 backendを登録するマクロは、同ヘッダ内にあります。

/**@internal @brief  Macro for registering a nrf_crypto backend for initialization by using
 *                   nrf_section.
 *
 * @details     This macro places a variable in a section named "crypto_data", which
 *              is initialized by @ref nrf_crypto_init.
 *
 * @note    This macro is used internally based on sdk_config.h configurations for nrf_crypto
 */
#define CRYPTO_BACKEND_REGISTER(crypto_var) NRF_SECTION_ITEM_REGISTER(crypto_data, crypto_var)

cc310 backendの実装を見ると、CRYPTO_BACKEND_REGISTERマクロでinit_fnuninit_fnを登録しています。

|> components/libraries/crypto/backend/cc310/cc310_backend_init.c

CRYPTO_BACKEND_REGISTER(nrf_crypto_backend_info_t const cc310_backend) =
{
    .init_fn    = cc310_backend_init,
    .uninit_fn  = cc310_backend_uninit,
};

CRYPTO_BACKEND_REGISTERマクロのコメントから、この関数ポインタを、crypto_dataというsection内に配置するみたいですね。

だいぶ横道にそれましたが、nrf_crypto_init()の中身を見ていきます。

|> components/libraries/crypto/nrf_crypto_init.c

ret_code_t nrf_crypto_init(void)
{
    ret_code_t      ret_val;
    size_t const    num_backends = NRF_CRYPTO_BACKEND_SECTION_ITEM_COUNT;

    m_state = INITIALIZING;

    // Iterate through each backends to call the init function
    for (size_t i = 0; i < num_backends; i++)
    {
        nrf_crypto_backend_info_t const * p_backend = NRF_CRYPTO_BACKEND_SECTION_ITEM_GET(i);
        ret_val = p_backend->init_fn();
        if (ret_val != NRF_SUCCESS)
        {
            return ret_val;
        }
    }

    // Set nrf_crypto to initialized
    m_state = INITIALIZED;
    return NRF_SUCCESS;
}

ちょっとずつ見ていきます。

    size_t const    num_backends = NRF_CRYPTO_BACKEND_SECTION_ITEM_COUNT;

NRF_CRYPTO_BACKEND_SECTION_ITEM_COUNTは、crypto_dataセクションを解析して、backendがいくつ登録されているか、を計算します。

#define NRF_CRYPTO_BACKEND_SECTION_ITEM_COUNT       NRF_SECTION_ITEM_COUNT(crypto_data, nrf_crypto_backend_info_t)

次に、登録されているbackendの個数がわかると、backendの初期化を行います。

    // Iterate through each backends to call the init function
    for (size_t i = 0; i < num_backends; i++)
    {
        nrf_crypto_backend_info_t const * p_backend = NRF_CRYPTO_BACKEND_SECTION_ITEM_GET(i);
        ret_val = p_backend->init_fn();
        if (ret_val != NRF_SUCCESS)
        {
            return ret_val;
        }
    }

sectionから、NRF_CRYPTO_BACKEND_SECTION_ITEM_GET(i)を使って、nrf_crypto_backend_info_tを1つずつ取得します。

#define NRF_CRYPTO_BACKEND_SECTION_ITEM_GET(i)      NRF_SECTION_ITEM_GET(crypto_data, nrf_crypto_backend_info_t, (i))

nrf_crypto_backend_info_tには、init_fn()が登録されているので、各backendの初期化関数を呼び出します。 cc310 backendのinit_fn()実装を見てみましょう。

static ret_code_t cc310_backend_init(void)
{
    uint32_t    ret_val;
    CRYSError_t crys_error;

    cc310_backend_mutex_init();

    // Enable the CC310 HW.
    NRF_CRYPTOCELL->ENABLE = 1;

    // Initialize the CC310 run-time library
    crys_error = SaSi_LibInit();

    // Shut down CC310 after initialization.
    NRF_CRYPTOCELL->ENABLE = 0;

    ret_val = init_result_get(crys_error);
    VERIFY_SUCCESS(ret_val);

#if defined(NRF_CRYPTO_RNG_AUTO_INIT_ENABLED) && (NRF_CRYPTO_RNG_AUTO_INIT_ENABLED == 1)

    ret_val = nrf_crypto_rng_init(NULL, NULL);
    VERIFY_SUCCESS(ret_val);

#elif defined(NRF_CRYPTO_RNG_AUTO_INIT_ENABLED) && (NRF_CRYPTO_RNG_AUTO_INIT_ENABLED == 0)
...
#endif // NRF_CRYPTO_RNG_AUTO_INIT_ENABLED

    return ret_val;
}

マクロでメモリマップドレジスタを操作しています。ENABLEを1にすることで、cc310のfirmware APIが使えるようになります。

CRYPTOCELL subsystem enabled When enabled the CRYPTOCELL subsystem can be initialized and controlled through the CryptoCell firmware API

    // Enable the CC310 HW.
    NRF_CRYPTOCELL->ENABLE = 1;

Nordicのドキュメントによると、CryptoCellのベースアドレスは0x5002A000で、ENABLEのオフセットは0x500です。

|> modules/nrfx/mdk/nrf52840.h内に定義がありました。

#define NRF_CRYPTOCELL_BASE         0x5002A000UL
#define NRF_CRYPTOCELL              ((NRF_CRYPTOCELL_Type*)    NRF_CRYPTOCELL_BASE)

このrun-time libraryというのが、firmwareなのでしょう。

    // Initialize the CC310 run-time library
    crys_error = SaSi_LibInit();

|> external/nrf_cc310/include/sns_silib.hに宣言があります。

/*!
@brief This function Perform global initialization of the ARM CryptoCell 3xx runtime library;
it must be called once per ARM CryptoCell for 3xx cold boot cycle.

\note The Mutexes, if used, are initialized by this API. Therefore, unlike the other APIs in the library,
this API is not thread-safe.
@return SA_SILIB_RET_OK on success.
@return A non-zero value in case of failure.
*/
SA_SilibRetCode_t SaSi_LibInit(void);

実体は、バイナリ配布されているライブラリ内にありそうです。

初期化の後、cc310をdisableしています。最小限の期間だけ有効化しながら使う、ということでしょうかね?

    // Shut down CC310 after initialization.
    NRF_CRYPTOCELL->ENABLE = 0;

乱数生成器を初期化します。処理の流れは、cc310の初期化と同じです。

#if defined(NRF_CRYPTO_RNG_AUTO_INIT_ENABLED) && (NRF_CRYPTO_RNG_AUTO_INIT_ENABLED == 1)

    ret_val = nrf_crypto_rng_init(NULL, NULL);

|> components/libraries/crypto/backend/cc310/cc310_backend_rng.c

ret_code_t nrf_crypto_rng_backend_init(void * const p_context,
                                       void * const p_temp_buffer)
{
...
    mutex_locked = cc310_backend_mutex_trylock();
    VERIFY_TRUE(mutex_locked, NRF_ERROR_CRYPTO_BUSY);

    cc310_backend_enable();

    err_code = CRYS_RndInit(&p_ctx->crys_rnd_state, p_work_buffer);
    ret_val = result_get(err_code);

    cc310_backend_disable();

    cc310_backend_mutex_unlock();
...

cc310_backend_enable()は、cc310を有効化して、割り込みを許可します。

|> components/libraries/crypto/backend/cc310/cc310_backend_shared.c

void cc310_backend_enable(void)
{
    m_use_count++;

    if (m_use_count == 1)
    {
        // Enable the CryptoCell hardware
        NRF_CRYPTOCELL->ENABLE = 1;

        // Enable the CryptoCell IRQ
        NVIC_EnableIRQ(CRYPTOCELL_IRQn);
    }
}

CRYS_RndInitは、やはりバイナリ配布されているライブラリ内にありそうです。

nrf_crypto_rng_vector_generate

さて、乱数を生成する関数を見ていきます。

|> components/libraries/crypto/nrf_crypto_rng.c

ret_code_t nrf_crypto_rng_vector_generate(uint8_t * const p_target, size_t size)
{
    ret_code_t ret_code;

    ret_code = generate(p_target, size, true);

    return ret_code;
}

generate()を呼ぶだけです。generate()は、同ファイル内で定義されています。

static ret_code_t generate(uint8_t * const p_target, size_t size, bool use_mutex)
{
    ret_code_t ret_code;

...
    ret_code = nrf_crypto_rng_backend_vector_generate(mp_context, p_target, size, use_mutex);
...

    return ret_code;
}

nrf_crypto_rng_backend_vector_generate()でbackendの関数を呼び出します。

|> components/libraries/crypto/backend/cc310/cc310_backend_rng.c

ret_code_t nrf_crypto_rng_backend_vector_generate(void      * const p_context,
                                                  uint8_t   * const p_target,
                                                  size_t            size,
                                                  bool              use_mutex)
{
    bool                mutex_locked;
    CRYSError_t         err_code;
    ret_code_t          ret_val;
    CRYS_RND_State_t  * p_crys_rnd_state =
        &((nrf_crypto_backend_rng_context_t *)p_context)->crys_rnd_state;

    if (use_mutex)
    {
        mutex_locked = cc310_backend_mutex_trylock();
        VERIFY_TRUE(mutex_locked, NRF_ERROR_CRYPTO_BUSY);
    }

    cc310_backend_enable();

    err_code = CRYS_RND_GenerateVector(p_crys_rnd_state, size, p_target);

    cc310_backend_disable();

    ret_val = result_get(err_code);

    if (use_mutex)
    {
        cc310_backend_mutex_unlock();
    }

    return ret_val;
}

CRYS_RND_GenerateVector()は、バイナリ配布されているライブラリにありそうです。

これで生成された8バイトの乱数がp_targetに格納されます。

今後

割り込みを使うサンプルがあれば、中身を解析します。

後は、cc310 firmwareとlibraryさえ移植できれば、ZephyrなどのRTOSからでも簡単に使えそうなので、インテグレーション方法も調査していきます。

参考

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

Rustの本質は、プログラムを理解する苦労を、未来から現在に移すことにある。

プログラミングRust p.120より

はじめに

www.rust-lang.org

Rustは、とても良い言語です。 Rustを使う理由は、性能、信頼性、生産性を高めたいから、に尽きると考えています。

Rustを学ぶうちに、組込み開発でRustを使うと、多くの恩恵を受けられるのではないか、と考えるようになりました。

また、Rustを学ぶことは、プログラマとしてのスキルを向上させてくれます。 プログラミングRustで一番お気に入りの文章を引用します。

Rustを使い始めてわかったのは、C/C++では長い間かけてゆっくり学んでいたような「良い書き方」を学ばないことには、 Rustではプログラムをコンパイルすることすらできない、ということだ。 Rustは2,3日でまずは手早く覚えて、後から難しい点、技術的な点、良い書き方などを学べるような言語ではないことを強調したい。 Rustでは厳密な安全性についてただちに学ばなければならず、最初のうちはあまり快適には感じないだろう。 しかし、このことは、プログラムをコンパイルするということが本来どういうことだったかを思い出させてくれたように思う。

Mitchell Nordine

原文

p.69 4章 所有権より

残念ながら、組込みプログラマには(にも?)プログラミングや設計スキルが高くない方が多く見られます。 組込みRust普及のため、組込みプログラマ全体のレベルアップのため、Rustが組込みでどう良いのか、を言語化してみます。

長くなりすぎて疲れたので、分割して投稿します。

この記事が目指すもの

一般的なRustの利点と、The Embedded Rust Bookを合わせた要約を作りたいです。 興味のある方はぜひ、The Embedded Rust Bookにも目を通して欲しいのですが、端的に言って長いです。 この記事も長いです。

組込み開発でRustを使いたい人が、周りを説得する材料として、使える部品を作り上げていく意図があります。 1発でうまく作れる気がしないので、フィードバックをもらいながら改版していこう、と考えています。

最終的な成果物としては、GitHubにまとまった1つの資料ができれば良いかな、と思っています。 分かるように説明しておじさん用にプレゼンテーション資料も用意できれば、と考えていますが、確約はできません。

注意事項

比較対象の言語について

Rustの比較対象となる言語は、CおよびC++です(2つの言語を明確に区別します)。 特に今回は組込み開発を対象とするため、特にC言語との比較について言及します。 残念なことに、観測範囲内では、modernなC++を最低限使える組込みプログラマは少ないです。 ということで、C++との比較はこの記事内ではあまり重要視しません(modernなC++を使っている人なら、そのうちRustに興味を持つでしょう)。

この記事内で、CおよびC++に言及する箇所があります。 私としては、この両言語を貶める意図は一切ありません。 (ただ、Cは危ない、C++は難しすぎる、というのが個人の見解です)

また、Rustが両言語を完全に置き換える、ということも現実的でないです。 既存アプリケーションの修正では、依然として、両言語を使う必要があるでしょう。 当面のRustの利用方法としては、新規アプリケーション、または、CおよびC++をラッピングできる状態での使用、となるでしょう。

著者のバックボーン

組込み、というのは曖昧な言葉で、非常に幅広い範囲をカバーしています。 そこで、私自身のバックボーンを明確にしておきます。

私は業務では、主に組込みLinuxを取り扱っています。ハードウェアも比較的、リッチな環境を用いることが多いです。ARMであればAシリーズです。 リソースが制限されているボード上で開発経験は、ごく一部の業務と趣味でのプログラミングとに限定されます。 上記のバックボーンのため、マイコンボードガチ勢の方や、プロプライエタリRTOS上で開発しているプログラマとは、少し見解が違う可能性があります。

対象とする組込み開発

プロプライエタリなツールチェインが必須な環境での開発は、対象外です。 「Rustが良いって言われても、使えないんだけど!」と言われても、ごめんなさい、としか言えません。ごめんなさい。

RTOSでのRust利用方法も、現時点では、確立されていません。 コミュニティでは議題になっています。

Develop resources for Rust integration with RTOSs

組込みLinuxについても少し触れますが、基本は、新規のユーザランドアプリケーションならRust、Linux device driverはC言語で書いて下さい、が主な主張です。 GUI作るなら、Qtとか使った方が良いです。

トピック

細かな話に移る前に、大まかなトピックをまとめます。

今、ざっと思いつく限りで、下の3つが大まかなトピックとして浮かびました。

  • コンパイル時検証による安全性向上
  • 言語仕様、エコシステムによる生産性向上
  • 設計、プログラム品質の向上

本記事では、1つ目のコンパイル時検証による安全性向上と組込みLinuxでの状況について書きます。

コンパイル時検証による安全性向上

コンパイル時、すなわち、実行時のオーバーヘッドなしに、安全なプログラミングをすることができます。 これは、Rustの次の言語仕様によってもたらされます。

  • 型システム
  • 所有権、借用、ライフタイム

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

Rustは習得が難しい言語と言われており、その通りだと思います。 C言語GCCの組み合わせで開発を続けて来たプログラマにとって、効率的にRustを書けるようになるまでのコストは大きいでしょう。 ただ、一度学習を終えると、Rustの言語仕様+エコシステムによってもたらされるものの恩恵がいかに大きいか実感できると思います。 下のトピックを扱います。

設計、プログラム品質の向上

Rustでの開発を続けていて感じることは、より優れた設計考えることであったり、ライブラリを作る思考になったり、ドキュメントを書くことが増えた、ということです。 なぜそうなるのでしょうか?Rustでは、その方がうまく行くからです。 これは、次のようなRustの言語仕様やエコシステムに由来するものです。

  • 所有権、借用、ライフタイム
  • Result型によるエラー処理
  • パッケージマネジメント
  • テストフレームワーク
  • rustdoc

問題点と障害

組込みRustの問題点や普及の障害について書きます。 あくまでも、私の観測範囲ですが、下記の要因が複合して、うーんどうしよう状態です。下に行くほど深刻です。抗う仲間募集中です!

  • 単純にRustが難しいという問題
  • ドキュメントと実績不足
  • プログラミングがめっちゃ好き、という人が少ない。新しい言語に自ら手を出す人は希少種。関数型?聞いたことない。
  • 大手メーカが顧客で、プログラミング音痴だが、こちらが技術選定できる立場にない
  • 分かるように説明しておじさん

細かく書くと憂鬱な気分になるので、この話題はここまでにします。

Why Rust?

Rust公式のRust for Embedded devicesに、組込みデバイスでRustを使う理由として、次の6点が挙げられていますので、紹介しておきます。

  • 強力な静的解析 (Powerful static analysis)
  • 柔軟なメモリ管理 (Flexible memory management)
  • 恐れる必要のない並行性 (Fearless concurrency)
  • 相互運用性 (Interoperability)
  • 移植性 (Portability)
  • コミュニティ駆動 (Community driven)

コンパイル時検証による安全性向上

コンパイル時、すなわち、実行時のオーバーヘッドなしに、安全なプログラミングをすることができます。

型安全

まず、Rustは型安全なプログラミング言語です。コンパイル可能なRustのコードは、未定義動作を引き起こしません。 ただし、unsafeブロックという抜け道があり、unsafeブロックの中では容易に未定義動作を起こすことができます。例えば、生ポインタを扱うことはunsafeブロックの中でしかできません。 組込みでは、メモリマップドレジスタを扱うために生ポインタを使う必要があり、unsafeを使わずにプログラミングすることは難しいでしょう。 プログラマは、このunsafeブロックの中で未定義動作を引き起こさないことを保証する必要があります。

逆に言えば、unsafeブロックを安全に保つ、というコストだけで、プログラムが完全に定義されている状態になる、とも言えます。 unsafeブロックの影響を局所化するプラクティスが多く考えられており、ベアメタルな組込みプログラミングであっても、既存のcrateを使うことで、安全にプログラミングすることができます。

品質の良いcrateでは、unsafeブロックが実際には安全である理由がコメントとして書かれていることが多いです。 Rustを使った仕事でのプログラミングでは、当然unsafeブロックを使う理由の説明が求められるでしょう。 しかし、C言語でプログラミングしていて、生ポインタを操作しているからと言って、いちいち槍玉に挙げることはしないでしょう(しないよね?でも危ないんだよ?)。

このことは、unsafeブロックを減らし、未定義動作を引き起こす危険な場所を局所化するインセンティブになります。 例えば、安全なAPIを用意したにも関わらず、そのAPIを使わない人は、unsafeブロックを追加せざるを得ないでしょう。そのことをレビューで指摘することは、従来よりずっと簡単なはずです。 Rustでコードを書くことは、より安全な方向へプログラマを誘導します。

型状態プログラミング

例えば、GPIOを制御することを考えます。あるレジスタの特定ビットを操作することで、GPIOの有効/無効、入力/出力、入力時のプルアップ/プルダウン/ハイインピーダンス、出力時のハイ/ローを制御できるとします。 GPIOの出力をハイレベルにする場合、C言語では次のように書くと思います。

    uint8_t temp = gpio0.read();
    temp |= GPIO0_OUTPUT_HIGH;
    gpio0.write(temp);

このコードでは、gpio0が有効になっているかおよび出力モードになっているかどうか、はチェックしていません。そのため、この操作が本当に意図通り作用するかわかりません。入力モードになっている場合は不正な操作と言えるでしょう。 実行時チェックを追加すると、有効かつ出力モードの時だけ、意図した書き込みを行うようにできます。もちろん、実行時にコストがかかります。

    uint8_t temp = gpio0.read();
    // bit演算を最適化しても本質的なコストは同じです
    if (enable_bit_is_set(temp) && output_bit_is_set(temp)) {
        temp |= GPIO_OUTPUT_HIGH;
        gpio0.write(temp);
    }

Rustでは型状態を使って、不正な操作をコンパイル不可能にすることができます。次の例のような実装を考えます。

struct GpioConfig<ENABLED, DIRECTION, MODE> {
    periph: GPIO_CONFIG,
    enabled: ENABLED,
    direction: DIRECTION,
    mode: MODE,
}

// GpioConfigのMODEのための型状態
struct Disabled;
struct Enabled;
struct Output;
struct Input;
...

/// これらの関数はどのGPIOピンにも使えます。型パラメータは任意の型を取れます。
impl<EN, DIR, IN_MODE> GpioConfig<EN, DIR, IN_MODE> {
...
    // GpioConfigは、`Enabled`かつ`Output`な型状態になります。
    pub fn into_enabled_output(self) -> GpioConfig<Enabled, Output, DontCare> {
        self.periph.modify(|_r, w| {
            w.enable.enabled()
             .direction.output()
             .input_mode.set_high()
        });
        GpioConfig {
            periph: self.periph,
            enabled: Enabled,
            direction: Output,
            mode: DontCare,
        }
    }
}

/// この関数は`Enabled`かつ`Output`な型状態のピンにのみ使用できます。
impl GpioConfig<Enabled, Output, DontCare> {
    pub fn set_bit(&mut self, set_high: bool) {
        self.periph.modify(|_r, w| w.output_mode.set_bit(set_high));
    }
}

これを使うアプリケーションは以下のようになります。

// これで未初期化のGPIOピンが取得できるとします。
let pin: GpioConfig<Disabled, _, _> = get_gpio();

// これはコンパイルエラーになります。`Enabled`かつ`Output`な型状態を持つピンしか出力モードは設定できません。
// pin.set_bit(true);

// 今度は大丈夫です。まず`Enable`かつ`Output`な型状態にします。
let output_pin = pin.into_enabled_output();
output_pin.set_bit(true);

struct Disabled;のような型状態を定義していますが、サイズが0のstructはコンパイラの最適化によって消えます。 上記のアプリケーションコードは、実行時には、メモリマップドレジスタに値を書くだけの機械語になります。

Rustでは、静的に安全性を保証するAPI設計が可能です。

@nodamushi さんが分かりやすい図を作ってくれました。

f:id:tomo-wait-for-it-yuki:20190209054345j:plain
型状態プログラミングによるGPIO制御

所有権、借用、ライフタイム

プログラムの複数の部分が、共有リソース(グローバル変数、ハードウェアレジスタなど)にアクセスすると、プログラムやハードウェアが意図せぬ状態に陥る可能性があります。 そのため、共有リソースの扱いは慎重に検討すべきです。 Rustでは、共有リソースの扱いを慎重に検討しないと、至るところにunsafeブロックが出現したり、コンパイルすることすらできません。

まず、Rustでは、ミュータブルなグローバル変数への読み書きは、常にunsafeです。unsafeブロックを使うのであれば、安全である理由の説明が必要です!

static mut COUNTER: u32 = 0;

fn main() -> ! {
    // 危険!なぜ安全と言えるのか説明して下さい!
    unsafe { COUNTER += 1 };
    loop {}
}

上記の例では、COUNTERの操作中に、COUNTERの値を変更するような割り込みがあると、データ競合が発生し、実行結果はどうなるかわかりません。 例えば、アトミックなアクセスが保証されていれば良いのであれば、そのように書くことができます。

use core::sync::atomic::{AtomicUsize, Ordering};

static COUNTER: AtomicUsize = AtomicUsize::new(0);

fn main() -> ! {
    // アトミック操作命令の`fetch_add()`を使います
    COUNTER.fetch_add(1, Ordering::Relaxed);
    loop {}
}

Rustでは、ガベージコレクションなしにメモリ安全性を保証するため、プログラマに所有権、借用、ライフタイム、というルールを課します。 このルールは、多くのメモリ管理バグや並行性バグを排除できるため、プログラマがこのルールに従うことは、理にかなったものだと思います。

Rustでは、全ての値には唯一の所有者が存在します。これは、その所有者がライフタイムを終えたとき、所有していた値も解放(ドロップ)されることを意味します。 C++をご存知の方はスマートポインタを想像して下さい。

fn main() {
    // Rustでは、`Box<T>型`を使うことで、ヒープ領域を割り当てます。
    let x = Box::new(5);
}  // `x`のライフタイムはここで終了し、所有しているヒープ領域もここで解放(ドロップ)される。

ムーブすることで、所有権を譲渡することが可能です。

fn main() {
    let x = Box::new(5);
    let y = x;  // 所有権を`y`に移動します。

    println!("{}", y);
    // println!("{}", x);  // コンパイルエラー!所有権は`y`に移っています。
}

このように、値は唯一の所有者を持ちますが、例外があります。それが参照の借用です。

fn main() {
    let x = Box::new(5);
    let y = &x;  // `参照`を`借用`する。

    println!("{}", y);
    println!("{}", x);  // 今度はOK!`y`に参照を借用しただけです。
}

参照の借用には、次の2つのルールがあります。

  • ミュータブル参照は同時に1つだけ存在します
  • イミュータブル参照は同時に複数存在することができます

例え、所有権の所有者であっても、ミュータブル参照を誰かに借用すると、その値にはアクセスできません。 このルールによって、Rustの借用チェッカは、データ競合が発生しないことをコンパイル時に検出します。

せっかくの仕組みも、台無しにするのは簡単です。 同一ハードウェアにアクセスするインタンスを複数作ってしまえば、借用チェッカの目を逃れることができます。

0xE000_E010番地にマッピングされているSystemTimerを考えます。

pub struct SystemTimer {
    p: &'static mut RegisterBlock
}

#[repr(C)]
struct RegisterBlock {
    pub csr: RW<u32>,
    pub rvr: RW<u32>,
    pub cvr: RW<u32>,
    pub calib: RO<u32>,
}

impl SystemTimer {
    pub fn new() -> SystemTimer {
        SystemTimer {
            p: unsafe { &mut *(0xE000_E010 as *mut RegisterBlock) }
        }
    }

    pub fn set_reload(&mut self, reload_value: u32) {
        unsafe { self.p.rvr.write(reload_value) }
    }
}

残念ながら、次のコードはコンパイルが通ってしまいます。借用チェッカは、同一ハードウェアにアクセスするインタンスが2つあって安全が脅かされることを検知できません。

fn thread1() {
    let mut st = SystemTimer::new();
    st.set_reload(2000);
}

fn thread2() {
    let mut st = SystemTimer::new();
    st.set_reload(1000);
}

シングルトンは、唯一のインスタンスが存在するように保証するパターンです。乱用すべきでないパターンではありますが、今回の使い方は適切です。

下の例でもグローバル変数を使いますが、単純なグローバル変数ではありません。ペリフェラルアクセス用の唯一のインスタンスを取得するためだけのグローバル変数です。

struct Peripherals {
    serial: Option<SerialPort>,
}

impl Peripherals {
    fn take_serial(&mut self) -> SerialPort {
        let p = replace(&mut self.serial, None);
        p.unwrap()
    }
}

static mut PERIPHERALS: Peripherals = Peripherals {
    serial: Some(SerialPort),
};

使い方は、次のようになります。

fn main() {
    let serial_1 = unsafe { PERIPHERALS.take_serial() };
    // This panics!
    // let serial_2 = unsafe { PERIPHERALS.take_serial() };
}

2回以上take_serial()を呼び出すと、Noneをunwrap()しようとするため、パニックが発生します。 ペリフェラルへのアクセスインスタンスが唯一であることが保証できるので、後はRustの借用チェッカが正しいプログラムであることを保証してくれます。

組込みLinux

組込みLinuxでRustを使うことについて簡単に書きます。

user space application

特に理由がなければ、新規アプリケーションはRustでの開発をお勧めします、と言いたいところですが、欠点もあります。

  • (未来永劫変わらないかもしれませんが)少なくとも現時点では、GUIアプリケーションを作ると苦労するでしょう。
  • Yoctoでフルビルドする場合、LLVMがフルビルドされます。

YoctoとRustでの組込み開発を紹介するブログがあります。

Embedded development with Yocto and Rust

YoctoでSDKを作成してあげると、sysrootもターゲットの環境を使えます。 Runnerをユーザモードエミュレーションのqemuに設定すると、ホスト上でユニットテストが動きます。 ということで、Cargoや言語組込みのユニットテスト機能など、普通のRustの開発サイクルを回すことが可能です。 この環境を一度構築すると、めちゃくちゃ快適に開発できます。

また、CargoのプロジェクトからYoctoのレシピを生成するcargo bitbakeも存在します。 ただ、meta-rustのレイヤを追加すると、LLVMがフルビルドされ、ビルド時間が大幅に増加します。また、人権のないマシンでビルドしている場合、LLVMのリンク時にメモリ不足でビルドが失敗します。 運用上、フルビルドの機会が少ない、夜間ビルドで回している、という場合は影響が少ないと思います。

C言語のライブラリを利用するのは、かなり簡単です。 C言語のヘッダファイルからバインディングを自動生成するbindgenがあります。 C言語のヘッダファイルからバインディングを自動生成して、クレートをビルドする、というプロセスも自動化することができます。

Linux device driver

Rustで書く方法がないわけではないですが、素直にC言語で書くのをお勧めします。

参考ですが、Rustでkernel moduleを書くプロジェクトとして、kernel-rouletteがあります。 Linux kernel moduleからRustを呼ぶkernel-rouletteを解析に解説記事も書いているため、興味がある方は覗いてみて下さい。

kmallocをラッピングして、グローバルアロケータとして登録することで、Vecのようなコレクションを使えるようにしている点が、非常におもしろいです。

続きはこちら。

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

RustでZephyrのアプリケーションを書く

github.com

最近、ZephyrというRTOSで遊んでいます。 新しい環境が手に入ると、Rustと結びつけたくなりますよね!

ということで、ZephyrのアプリケーションをRustで書いてみます。 ズルしましたけど、hello worldは動きました。

知見として、no_stdだけど完全なfree standing環境じゃない場合、一工夫必要そうな感触でした。

基本戦略

Zephyrをqemu_cortex_m3ターゲットでビルドして、hello worldをします。 ベースとするのは、Zephyrのサンプルアプリです。

|> zephyr/samples/hello_worldを使います。中身は下のような感じです。

main.c

#include <zephyr.h>
#include <misc/printk.h>

void main(void)
{
    printk("Hello World! %s\n", CONFIG_BOARD);
}

Zephyrのプロジェクトでは、アプリケーションコードはライブラリ(libapp.a)としてビルドされ、最後にZephyr本体とリンクされます。。

|> <build>/app/libapp.a

Rustのプロジェクトをライブラリ(libapp.a)として作って、C言語で書かれたアプリケーションの代わりにリンクしてあげれば良いはずです。

Let's Hello World...できない!

ライブラリとしてプロジェクトを新規作成します。

cargo new --lib hello

プロジェクトの設定をします。

$ tail -n 3 Cargo.toml
[lib]
name = "app"
crate-type = ["staticlib"]

Cortex-M3をデフォルトのビルドターゲットにします。

$ cat .cargo/config
[build]
target = "thumbv7m-none-eabi"

|> src/lib.rs

#![no_std]

#[no_mangle]
pub extern "C" fn main() -> ! {
    loop{}
}

use core::panic::PanicInfo;
#[panic_handler]
#[no_mangle]
pub fn panic(_info: &PanicInfo) -> ! {
    loop{}
}

とりあえず無限ループでQEMUが起動することを確認します。Zephyrでは、アプリケーションのエントリポイントはmainという名前の関数なので、Rust側もそれに合わせます。

では、Rust側をビルドしましょう。

$ cargo build
$ ls target/thumbv7m-none-eabi/debug/libapp.a
target/thumbv7m-none-eabi/debug/libapp.a

無事、libapp.aができています。

Zephyr側も一度ビルドします。 そして、Zephyrのlibapp.aを、Rustで作ったlibapp.aに置き換えます。 これで、再度Zephyrをビルドすれば、Zephyr本体とRustで書いたライブラリがリンクされてめでたしめでたし、のはずです!

ninja
arm-none-eabi/bin/ld: zephyr/lib/libc/minimal/liblib__libc__minimal.a(string.c.obj): in function `memcmp':
zephyr/lib/libc/minimal/source/string/string.c:179: multiple definition of `memcmp'; app/libapp.a(compiler_builtins-f49b1fd2880b372d.compiler_builtins.9n9954vw-cgu.0.rcgu.o):/rustc/9fda7c2237db910e41d6a712e9a2139b352e558b//src/rustc/compiler_builtins_shim/../../libcompiler_builtins/src/mem.rs:55: first defined here
...
# 以下、memmove, memcpy, memsetに関する同様のエラー
collect2: error: ld returned 1 exit status
%
ninja: build stopped: subcommand failed.

ということで、memcmp, memmove, memcpy, memsetの多重定義エラーが発生しました。忍者もしょんぼりです。

リンクエラーの調査

ZephyrはしっかりとしたOSなので、ユーザアプリケーションを書くために、最低限のlibcを提供しています。この中には、memcmpやmemmoveが含まれています。

一方、no_stdなRustでは、core libraryの機能を提供するために、compiler_builtinsというクレートを利用しています。 この中に、問題になっているmemcmpやmemmoveといった関数が、C言語のインタフェースで定義されています。

下記からソースコードを一部抜粋します。

compiler-builtins/mem.rs at master · rust-lang-nursery/compiler-builtins · GitHub

#[cfg_attr(all(feature = "mem", not(feature = "mangled-names")), no_mangle)]
pub unsafe extern "C" fn memcpy(dest: *mut u8,
                                src: *const u8,
                                n: usize)
                                -> *mut u8 {
    let mut i = 0;
    while i < n {
        *dest.offset(i as isize) = *src.offset(i as isize);
        i += 1;
    }
    dest
}

これらのシンボルがリンク時に衝突するため、リンクエラーになっていまいます。

応急処置

ということで、応急処置をして無理矢理動かします。 具体的には、Rust側のmemcmpやmemmoveのシンボルをweakに書き換えます。

シンボルをweakにすれば、通常の定義があれば、weakな方は無視されます。

arm-none-eabi-objcopy --weaken-symbol=memmove --weaken-symbol=memcpy --weaken-symbol=memset --weaken-symbol=memcmp libapp.a

これでRust側のlibapp.a内にある邪魔なmemcmpなどのシンボルはweakになります。

$ ninja run
[3/7] Linking C executable zephyr/zephyr_prebuilt.elf
...
[6/7] Linking C executable zephyr/zephyr.elf
...
[7/7] To exit from QEMU enter: 'CTRL+a, x'[QEMU] CPU: cortex-m3
***** Booting Zephyr OS zephyr-v1.13.0-3321-g7f956a9 *****

リンクが通って、Zephyrが起動しました!

無理矢理Hello World

Zephyrのprintk()は次のように宣言されています。

void printk(const char *fmt, ...);

C言語のフォーマット形式をRustから呼び出すのは面倒くさそうなので、ラッパー関数を作ります。

void puts_c(const char *str) {
    printk("%s", str);
}

面倒くさいので、Zephyr内(zephyr/misc/printk.c)に直接突っ込んでいます。良い子は真似しちゃダメだぞ★

後はRust側からこのputs_c()を呼び出すだけです。

extern "C" {
    fn puts_c(c_str: *const u8);
}

#[no_mangle]
pub extern "C" fn main() -> ! {
    const HELLO: &[u8] = b"Hello from Rust.\0";
    unsafe { puts_c(HELLO.as_ptr()) };

    loop{}
}

改めて、Rustのプロジェクトをビルドして、libapp.aを置き換えて、シンボルをweakにして、

$ ninja run
***** Booting Zephyr OS zephyr-v1.13.0-3321-g7f956a9 *****
Hello from Rust.

いぇーい!

Zephyrのリンカスクリプトを修正する

www.mouser.jp

nRF52840 Dongle (pca10059)というNordicのボードに、Zephyrを乗せて遊んでいます。

Zephyrのリンカスクリプトがおかしいらしく、うまく動作しなかったのですが、修正方法まで含めて動かす方法が判明したので、まとめます。

Zephyr blinky sample

Zephyrは、samples下に多くのサンプルがあります。 zephyr/samples/basic/blinkyは、pca10059でも動作する様子だったので、このサンプルから動かすことにしました。 このサンプルを動かすと、ボード上の緑のLEDがチカチカします。

今回は説明しませんが、Zephyrをビルドできる環境、ARMクロスコンパイラ、nRFのFlash書き込みツール(nrfutil)が必要です。 この条件がそろえば、下のコマンドでビルドすることが可能です。

# at blinky directory
mkdir build && cd build
cmake -GNinja -DBOARD=nrf52840_pca10059 ..
ninja
nrfutil  pkg generate --hw-version 52 --sd-req=0x00 --application zephyr/zephyr.hex --application-version 1 pkg.zip
nrfutil dfu usb-serial -pkg pkg.zip -p /dev/ttyACM0

ただ、この手順でできたバイナリでは、動作しません。

リンカスクリプト

上記コマンドのビルドで使用されているリンカスクリプトを見てみます。

|> build/zephyr/linker.cmd

$ head -n 10 zephyr/linker.cmd
 OUTPUT_FORMAT("elf32-littlearm")
_region_min_align = 32;
MEMORY
    {
    FLASH (rx) : ORIGIN = (0x0 + 0), LENGTH = (1024*1K - 0)
    SRAM (wx) : ORIGIN = 0x20000000, LENGTH = (256 * 1K)
    IDT_LIST (wx) : ORIGIN = (0x20000000 + (256 * 1K)), LENGTH = 2K
    }
ENTRY("__start")
SECTIONS

FLASHのORIGINが0x0番地から始まっていますが、pca10059プログラミングチュートリアルなどを参照すると、0x0 - 0xFFFの先頭4KBはMBR (Master Boot Record)のエリアとなっており、ここには書き込みしてはいけない、とあります。 試しに、手動で、リンカスクリプトを次のように編集して、ビルドすると、うまく動きます。

$ head -n 10 zephyr/linker.cmd
 OUTPUT_FORMAT("elf32-littlearm")
_region_min_align = 32;
MEMORY
    {
    FLASH (rx) : ORIGIN = (0x0 + 0x1000), LENGTH = (1024*1K - 0x1000)
    SRAM (wx) : ORIGIN = 0x20000000, LENGTH = (256 * 1K)
    IDT_LIST (wx) : ORIGIN = (0x20000000 + (256 * 1K)), LENGTH = 2K
    }
ENTRY("__start")
SECTIONS

先頭を後ろに4KBずらした分、FLASHの容量を4KB減らしています。

これで手動ではうまくいきますが、毎回リンカスクリプトを編集するのは面倒です。 そこで、Zephyrのビルドプロセスを解析して、ビルドすると自動で上記リンカスクリプトが生成されるようにします。

nRF52リンカスクリプト

nRF52から始まるSoCのリンカスクリプトは、下記にあります。

|> zephyr/soc/arm/nordic_nrf/nrf52/linker.ld

/* linker.ld - Linker command/script file */

/*
 * Copyright (c) 2014 Wind River Systems, Inc.
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#include <arch/arm/cortex_m/scripts/linker.ld>

中身を見ると、cortex_mのリンカスクリプトをincludeしているだけです。そこで、cortex_mのリンカスクリプトを見てみます。

|> zephyr/include/arch/arm/cortex_m/scripts/linker.ld

MEMORY
    {
    FLASH                 (rx) : ORIGIN = ROM_ADDR, LENGTH = ROM_SIZE
...
    SRAM                  (wx) : ORIGIN = RAM_ADDR, LENGTH = RAM_SIZE

    /* Used by and documented in include/linker/intlist.ld */
    IDT_LIST  (wx)      : ORIGIN = (RAM_ADDR + RAM_SIZE), LENGTH = 2K
    }

ROM_ADDRROM_SIZEはマクロになっており、これらのマクロは、同ファイルで下記のように定義されています。

#define ROM_ADDR (CONFIG_FLASH_BASE_ADDRESS + CONFIG_FLASH_LOAD_OFFSET)
#ifdef CONFIG_TI_CCFG_PRESENT
...
#else
#if CONFIG_FLASH_LOAD_SIZE > 0
  #define ROM_SIZE CONFIG_FLASH_LOAD_SIZE
#else
  #define ROM_SIZE (CONFIG_FLASH_SIZE*1K - CONFIG_FLASH_LOAD_OFFSET)
#endif
#endif

ここで、CONFIG_FLASH_LOAD_OFFSETマクロを4Kに設定できれば、最終的なリンカスクリプトが望むものになることが分かりました。 Zephyrのソースを検索すると、Kconfig.zephyrで次のようになっていました。

if !HAS_DTS
config FLASH_LOAD_OFFSET
    hex "Kernel load offset"
    default 0
    depends on HAS_FLASH_LOAD_OFFSET
    help
      This option specifies the byte offset from the beginning of flash that
      the kernel should be loaded into. Changing this value from zero will
      affect the Zephyr image's link, and will decrease the total amount of
      flash available for use by application code.

      If unsure, leave at the default value 0.

device treeがない場合は、このKconfigでCONFIG_FLASH_LOAD_OFFSETを設定することが可能です。 今回は、device treeを使用しているため、device treeで設定する方法を調べます。

Zephyr Doc Device Tree Flash PartitionsLinking Zephyr Within a Partitionを見ると、

If the chosen node has no zephyr,code-partition property, the application image link uses the entire flash device. If a zephyr,code-partition property is defined, the application link will be restricted to that partition.

とあり、device tree内でFLASHパーティションを作成し、そのパーティションchosen内でcode-partitionプロパティに設定してあげると良さそうなことが分かりました。 pca10059のdevice treeを見てみます。

|> zephyr/boards/arm/nrf52840_pca10059/nrf52840_pca10059.dts

&flash0 {
    /*
     * For more information, see:
     * http://docs.zephyrproject.org/latest/devices/dts/flash_partitions.html
     */
    partitions {
        compatible = "fixed-partitions";
        #address-cells = <1>;
        #size-cells = <1>;

        boot_partition: partition@e0000 {
            label = "mcuboot";
            reg = <0x0000e0000 0x0001a000>;
        };
        slot0_partition: partition@1000 {
            label = "image-0";
            reg = <0x00001000 0x000060000>;
        };
        slot1_partition: partition@61000 {
            label = "image-1";
            reg = <0x0061000 0x000060000>;
        };
        scratch_partition: partition@c1000 {
            label = "image-scratch";
            reg = <0x000c1000 0x0001f000>;
        };
        storage_partition: partition@fa000 {
            label = "storage";
            reg = <0x000fa000 0x00004000>;
        };
    };
};

すでにパーティションが作られており、slot0_partitionが0x1000から始まっています。

chosenでzephyr,code-partitionをこのパーティションにしてあげます。

 chosen {
        zephyr,console = &uart0;
        zephyr,uart-mcumgr = &uart0;
        zephyr,sram = &sram0;
        zephyr,flash = &flash0;
        zephyr,code-partition = &slot0_partition;  // 追加
    };

もう一度ビルドすると、無事、リンカスクリプトが望み通り作られています。

 OUTPUT_FORMAT("elf32-littlearm")
_region_min_align = 32;
MEMORY
    {
    FLASH (rx) : ORIGIN = (0x0 + 4096), LENGTH = 393216
    SRAM (wx) : ORIGIN = 0x20000000, LENGTH = (256 * 1K)
    IDT_LIST (wx) : ORIGIN = (0x20000000 + (256 * 1K)), LENGTH = 2K
    }
...

これで、リンカスクリプトを手動で修正しなくて済むようになりました。

The Embedded Rust Book翻訳の気づきメモ③~CI環境構築~

The Embedded Rust Bookを翻訳しています。下記から見ることができます。 Getting startedのQEMUまで、翻訳完了しています。

導入 - The Embedded Rust Book

お仕事でもRustをやりたいな、と考えて、組込みでRustを広めるために翻訳を行っています。 翻訳を進める中で、技術的にも英語的にも、学びがあるので、それを綴っていきます。

フィードバックやコントリビューションをお待ちしております。

github.com

CI環境構築

構築、と言ってもTravis CIでレポジトリをactivateして、GitHub tokenを登録するだけでした。fork元レポジトリがCI環境を構築してくれていたおかげです。

今後、もう少し運用して、pull requestのときにちゃんとCIが走るか、ということを確認していきます。

travis.yml

travis.ymlは、下記の内容でした。

language: rust

install:
  - bash ci/install.sh

script:
  - bash ci/script.sh

after_success:
  - bash ci/after-success.sh
...

rustをインストールしています。 The Embedded Rust Bookでは、mdbookを使っています。

rust-lang-nursery.github.io

mdbookはRust製のドキュメントビルダーです。markdownから、上記のmdbookのページのようなページを生成してくれます。 cargoを使うので、rust環境をインストールします。

その後は、installscriptafter_successでそれぞれスクリプトを実行します。

ci/install.sh

mdbookと、補助ツールであるlinkcheckerをインストールしています。linkcheckerはページ内にリンク切れがないか、をチェックしてくれるツールのようです。

set -euxo pipefail

main() {
    local tag=$(git ls-remote --tags --refs --exit-code https://github.com/rust-lang-nursery/mdbook \
                    | cut -d/ -f3 \
                    | grep -E '^v[0.1.0-9.]+$' \
                    | sort --version-sort \
                    | tail -n1)
    # Temporarily use older version until packages are available for 0.2.2 (or newer)
    local tag="v0.2.1"
    curl -LSfs https://japaric.github.io/trust/install.sh | \
        sh -s -- --git rust-lang-nursery/mdbook --tag $tag

    pip install linkchecker --user
}

main

ci/script.sh

mdbookをビルドし、linkchekerでリンク切れをチェックします。

set -euxo pipefail

main() {
    mdbook build

    # FIXME(rust-lang-nursery/mdbook#789) remove `--ignore-url` when that bug is fixed
    linkchecker --ignore-url "print.html" book

    # now check this as a directory of the bookshelf
    rm -rf shelf
    mkdir shelf
    mv book shelf
    # FIXME(rust-lang-nursery/mdbook#789) remove `--ignore-url` when that bug is fixed
    linkchecker --ignore-url "print.html" shelf

    mv shelf/book .
    rmdir shelf
}

main

後半のshelfディレクトリを操作しているあたりが、イマイチ何をやっているのかわかっていません。

ci/after-success.sh

mdbookのビルド成功後は、GitHub pagesを更新します。 ghp-importは、対象ディレクトリを、gh-pagesブランチにコピーするスクリプトです。

github.com

set -euxo pipefail

main() {
    mkdir ghp-import
    curl -Ls https://github.com/davisp/ghp-import/archive/master.tar.gz |
        tar --strip-components 1 -C ghp-import -xz

    ./ghp-import/ghp_import.py book

    # NOTE(+x) don't print $GH_TOKEN to the console!
    set +x
    git push -fq https://$GH_TOKEN@github.com/$TRAVIS_REPO_SLUG.git gh-pages && echo OK
}

if [ $TRAVIS_BRANCH = master ] && [ $TRAVIS_PULL_REQUEST = false ]; then
    main
fi

手元の環境にcloneして、手動で手順を追ってみると、./ghp-import/ghp_import.py book実行後、ローカルのbranchにgh-pagesが追加されます。

$ git branch
* gh-pages
  master

このbranchをcheckoutすると、ビルドしたmdbookのファイルのみが置かれていました。

後は、このgh-pages branchを-fコマンド付きでpushすれば、更新完了です。

ということで、少なくともmasterへのpush時と、dailyのビルドはうまく走るはず、です!

The Embedded Rust Book翻訳の気づきメモ②

Getting started/QEMUページの翻訳が完了しました。下記から見ることができます。

導入 - The Embedded Rust Book

お仕事でもRustをやりたいな、と考えて、組込みでRustを広めるために翻訳を行っています。 翻訳を進める中で、技術的にも英語的にも、学びがあるので、それを綴っていきます。

フィードバックやコントリビューションをお待ちしております。

github.com

To Do

  • CI構築
  • 協力者/レビュワーの募集

英語

embeddedは組込みと訳しています

直接英語と関係ないのですが、embeddedの日本語訳は、組込みにしています。 組み込みなのか、組込なのか、組込みなのか、正直よくわかりません。

ただ、IPAのウェブサイト表記を見ると、組込みが多いようです。

www.ipa.go.jp

ということで、翻訳の中では、組込みを使うようにしています。

割込みも、割込み、と表記することにしています。 個人的には、割り込み、な気がしますが、組込みの表記に合わせると、割込みの方が統一感がある気がします。 後、適当に手元にあったLinuxのブートプロセスをみる、を見ても、割込みベクタテーブルといった表記であったので、良いかな、と思っています。

Rust

特にありませんでした。

その他

sizeコマンド

普段、binutilssizeコマンドは使わないのですが、デバッグ情報を抜いたバイナリサイズを表示してくれるようです。へー!。

size command man page - binutils | ManKier

GitHub pagesの整備

Webページとしてちゃんと見えるにしたいです。

qiita.com

GitHub pagesは、プロジェクト用のページは、gh-pagesというブランチを作るだけで良いみたいです。

とりあえず、手元のドキュメントをmdbookで整形して、表示してみるところから始めます。

mdbookをインストールします。

$ curl -LSfs https://japaric.github.io/trust/install.sh | \
        sh -s -- --git rust-lang-nursery/mdbook --tag "v0.2.1"
install.sh: GitHub repository: https://github.com/rust-lang-nursery/mdbook
install.sh: Crate: mdbook
install.sh: Tag: v0.2.1
install.sh: Target: x86_64-unknown-linux-gnu
install.sh: Installing to: /home/tomoyuki/.cargo/bin

レポジトリのトップでmdbook buildを実行します。

$ mdbook build
2019-01-06 14:19:05 [INFO] (mdbook::book): Book building has started
2019-01-06 14:19:05 [INFO] (mdbook::book): Running the html backend

f:id:tomo-wait-for-it-yuki:20190106142235p:plain
mdbook生成結果

きれいにページができました。

後は、gh-pagesブランチにpushすれば、とりあえず見られそうです。

とりあえず手動でpushしてみます。

導入 - The Embedded Rust Book

無事?見られようになりました。

後は、CIの設定をしたいですね。

エミュレータ開発日記 in Rust~ELFローダ編①~

ここ半年ほど、RustでCPUエミュレータを書いています。 今更ですが、これも記録に残していこうと思います。

誰かの役に立つかもしれないですし。

今は、ELFローダを作っています。 これまでは、objcopyコマンドでraw binaryを作成し、特定アドレスにマッピングする、という実装で動いていました。 エミュレータのテストで、ELFファイルを直接動かした方が楽な状況になったので、ELFローダを作ることにしました。

ELFを解析するcrateは既に存在しますが、ELFを解析するプログラムを作ったことがないため、自作します。 何かを理解したい時には、フルスクラッチで作る、そして、文書を書く、これが一番です。

当面の目標

readelfで得られるヘッダ情報をエミュレータ内で扱えるようにバイナリをパースしていきます。 特にエントリポイントと、プログラムヘッダ情報を読み出すあたりが重要です。

ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           RISC-V
  Version:                           0x1
  Entry point address:               0x80000000
  Start of program headers:          52 (bytes into file)
  Start of section headers:          8692 (bytes into file)
  Flags:                             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         2
  Size of section headers:           40 (bytes)
  Number of section headers:         6
  Section header string table index: 5

はじめの一歩

ELF Identification

ELF形式のファイルの先頭には、ELF Identificationという16バイトのデータが存在します。 先ほど示したヘッダ内の先頭にある部分です。

  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0

OS/ABIがUNIX - System Vとなっています。OS/ABIのフィールドが0x00の場合、ELFOSABI_NONEまたはELFOSABI_SYSVです。 今回は、ベアメタルで動作するバイナリを作成しているため、ELFOSABI_NONEと解釈すると良さそうです。

ということで、とりあえず使用する定数を定義しておきます。

/// 0x7f 'E' 'L' 'F'
const HEADER_MAGIC: [u8; 4] = [0x7f, 0x45, 0x4c, 0x46];
const SIZE_ELF_IDENT: usize = 16;

const ELF_CLASS_32: u8 = 1;
const ELF_DATA_LSB: u8 = 1;
const EV_CURRENT: u8 = 1;
const ELF_OS_ABI_NONE: u8 = 0;

ELF magic

ELF形式のファイルは、`0x7f454c46'というマジックナンバーから始まります。 適当なELFバイナリ(test-elfとしています)を用意して、マジックナンバーが読み込めるテストからスタートします。

とりあえず、ファイル名を引数に、ElfLoaderのオブジェクトを作成し、ELF形式のファイルかどうか、を確認できるような作りで考えてみます。

#[cfg(test)]
mod test {
    use super::*;
    #[test]
    fn is_elf() {
        let loader = ElfLoader::try_new("test-elf").unwrap();

        assert!(loader.is_elf(), "target file is not elf binary");
    }
}

ElfLoaderは次のようにしました。 try_newでは、ファイルを開いて、mmapしています。 newではなく、try_newとしているのは、clippyさんに怒られるからです。 clippyさん的には、newでResultを返すのはお気に召さないようです。

warning: methods called `new` usually return `Self`

is_elfでは、mmapした領域の先頭4バイトのデータが、0x7f454c45になっているか、を確認します。

use std::fs::File;
use memmap::Mmap;

// 0x7f 'E' 'L' 'F'
const HEADER_MAGIC: [u8; 4] = [0x7f, 0x45, 0x4c, 0x46];

pub struct ElfLoader {
    mapped_file: Mmap,
}

impl ElfLoader {
    pub fn try_new(file_path: &str) -> std::io::Result<ElfLoader> {
        let file = File::open(&file_path)?;
        Ok(ElfLoader {
            mapped_file: unsafe { Mmap::map(&file)? },
        })
    }

    fn is_elf(&self) -> bool {
        self.mapped_file[0..4] == elf::HEADER_MAGIC
    }
}

memmap crateのMmap::map()はunsafeな関数です。 リテラシの高いRustプログラマを目指す私としては、unsafeがなぜunsafeなのか、はたまたsafeなのか、をきちんと書きたいところです。 unsafeな理由は、下記のsafetyに記述されています。

github.com

/// ## Safety
///
/// All file-backed memory map constructors are marked `unsafe` because of the potential for
/// *Undefined Behavior* (UB) using the map if the underlying file is subsequently modified, in or
/// out of process. Applications must consider the risk and take appropriate precautions when
/// using file-backed maps. Solutions such as file permissions, locks or process-private (e.g.
/// unlinked) files exist but are platform specific and limited.

プロセス内外でファイルを書き換えられると未定義動作となる、とあります。 しっかりと作るのであれば、ファイルをロックするオプションを指定して、mmapする必要がありそうですね。

後、適当なELF形式以外のファイルを用意して、同じテストが失敗することを確認しておきました。 これで、ELF magicを読みだすところまで作ることができました。

ElfIdentification struct

次に、ELF Identificationをstructで管理するようにします。 愚直に行きます。

/// File identification in elf header.
struct ElfIdentification {
    magic: [u8; 4],
    class: u8,
    endianess: u8,
    version: u8,
    os_abi: u8,
    os_abi_version: u8,
    reserved: [u8; 7], // zero filled.
}

impl ElfIdentification {
    // assumption: `binary` has enough length to read elf identification.
    fn new(binary: &[u8]) -> ElfIdentification {
        let mut magic: [u8; 4] = [0; 4];
        for (i, b) in binary[0..4].iter().enumerate() {
            magic[i] = *b;
        }
        ElfIdentification {
            magic,
            class: binary[4],
            endianess: binary[5],
            version: binary[6],
            os_abi: binary[7],
            os_abi_version: binary[8],
            reserved: [0; 7],
        }
    }
}

ちゃんと読み込めているかどうか、テストしておきましょう。

    // Check the ELF identification is as bellow:
    //   Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
    //   Class:                             ELF32
    //   Data:                              2's complement, little endian
    //   Version:                           1 (current)
    //   OS/ABI:                            UNIX - System V
    #[test]
    fn elf_identification() {
        let file = File::open("test-elf").unwrap();
        let mapped_file = unsafe { Mmap::map(&file).unwrap() };
        let identification = ElfIdentification::new(&mapped_file);

        assert_eq!(ELF_CLASS_32, identification.class);
        assert_eq!(ELF_DATA_LSB, identification.endianess);
        assert_eq!(EV_CURRENT, identification.version);
        assert_eq!(ELF_OS_ABI_NONE, identification.os_abi);
    }

このテストは通りました。これで、ELF Identificationの読み込みが完成です!

参考