banner
ekko

ekko's blog

时间不在于你拥有多少,而在于你怎样使用
github
xbox
email

zephyrユーザーモードの紹介

機能概要#

概要 — Zephyr プロジェクトドキュメント

[! 機械翻訳自公式サイト]

スレッドモデル#

ユーザーモードスレッドは Zephyr によって信頼されていないと見なされ、他のユーザーモードスレッドやカーネルと隔離されます。 欠陥のあるまたは悪意のあるユーザーモードスレッドは、他のスレッドやカーネルのプライベートデータ / リソースを漏洩または変更することができず、他のユーザーモードスレッドやカーネルを干渉または制御することができません。

Zephyr ユーザーモード機能の例:

  • カーネルは、多くの意図しないプログラミングエラーを防ぐことができ、これらのエラーは静かにまたは衝撃的にシステムを破壊する可能性があります。
  • カーネルは、複雑なデータパーサー(インタプリタ、ネットワークプロトコル、ファイルシステムなど)をサンドボックス化できるため、悪意のあるサードパーティのコードやデータがカーネルや他のスレッドに危害を加えることはありません。
  • カーネルは、複数の論理「アプリケーション」の概念をサポートでき、各アプリケーションは独自のスレッドグループとプライベートデータ構造を持ち、1 つのアプリケーションがクラッシュしたり他の危害を受けても、これらのアプリケーションは互いに隔離されます。

設計目標#

非特権 CPU 状態(以下「ユーザーモード」と呼ぶ)で実行されるスレッドに対して、私たちの目標は以下の状況を防ぐことです:

  • 特に許可されていないメモリへのアクセスや、ポリシーと互換性のないメモリへの誤ったアクセスを防ぎます。たとえば、読み取り専用領域への書き込みを試みることなどです。
    • スレッドスタックバッファへのアクセスはポリシーによって制御され、そのポリシーは部分的に基盤となるメモリ保護ハードウェアに依存します。
      • デフォルトでは、ユーザースレッドは自分のスタックバッファに対して読み取り / 書き込みアクセスができます。
      • ユーザースレッドはデフォルトで同じメモリドメインに属さないユーザースレッドスタックにアクセスできません。
      • デフォルトでは、ユーザースレッドは監視スレッドが所有するスレッドスタックや、システムコールの権限昇格、中断、または CPU 例外を処理するためのスレッドスタックにアクセスできません。
      • ユーザースレッドは、同じメモリドメイン内の他のユーザースレッドのスタックに対して読み取り / 書き込みアクセスができますが、これはハードウェアに依存します。
        • MPU システムでは、スレッドは自分のスタックバッファにしかアクセスできません。
        • MMU システムでは、スレッドは同じメモリドメイン内の任意のユーザースレッドスタックにアクセスできます。ポータブルコードはこれを前提にすべきではありません。
    • デフォルトでは、すべてのカーネル範囲内のスレッドはプログラムテキストと読み取り専用データに対して読み取り専用でアクセスできます。このポリシーは調整可能です。
    • 上記の内容を除き、ユーザースレッドはデフォルトでメモリにアクセスできません。
  • 特に許可されていないデバイスドライバやカーネルオブジェクトの使用を防ぐために、各オブジェクトまたは各ドライバインスタンスの権限粒度に基づいています。
  • エラーのあるパラメータを持つカーネルまたはドライバ API 呼び出しを検証し、そうでなければカーネルがクラッシュしたりカーネル専用データ構造が破損することになります。これには以下が含まれます:
    • 誤ったカーネルオブジェクトタイプの使用。
    • 適切な範囲を超えたまたは不合理な値のパラメータの使用。
    • API の意味に基づいて、呼び出しスレッドが読み取りまたは書き込みのための十分な権限を持たないメモリバッファを渡すこと。
    • 正しく初期化されていないカーネルオブジェクトの使用。
  • ユーザーモードスタックオーバーフローの検出と安全な処理を確保します。
  • カーネル構成の排除機能を呼び出すシステムコールを防ぎます。
  • カーネル定義およびハードウェアによって実行されるメモリ保護を無効にしたり改ざんしたりすることを防ぎます。
  • ユーザーモードから監視モードに再入することを防ぎます。カーネル定義のシステムコールおよび中断ハンドラを介してのみ可能です。
  • ユーザーモードスレッドが新しい実行可能コードを導入することを防ぎます。カーネルシステムコールのサポートを除きます。

私たちは特に以下の攻撃を防ぎません:

  • カーネル自体および管理モードで実行される任意のスレッドは信頼できると見なされます。
  • ビルドシステムで使用されるツールチェーンおよび任意の補助プログラムが信頼できると仮定します。
  • カーネルビルドが信頼できると仮定します。効果的なカーネルオブジェクトテーブルを作成し、システムコールを定義し、中断を構成するためのかなりのビルド時ロジックがあります。このプロセスで使用される.elf バイナリはすべて信頼できるコードと見なされます。
  • カーネルモードで完了したメモリドメイン構成のエラーを防ぐことはできず、これによりプライベートカーネルデータ構造がユーザースレッドに露出する可能性があります。カーネルオブジェクトの RAM は常に管理者専用に構成されるべきです。
  • ユーザーモードスレッドに対してトップレベルの宣言を行い、カーネルオブジェクトに権限を割り当てることができます。一般的に、zephyr.elf を生成するカーネルビルドの一部としてすべての C およびヘッダーファイルは信頼できると見なされます。
  • スレッド CPU 不足によって DDOS 攻撃を防ぐことはありません。 Zephyr にはスレッド優先度の老朽化がなく、特定の優先度のユーザースレッドは、時間分割が無効になっている場合、すべての低優先度スレッドを飢餓させることができ、同じ優先度の他のスレッドも飢餓させることができます。
  • 同時にアクティブなスレッドの数にはビルド時に定義された制限があり、それを超えると新しいユーザースレッドの作成は失敗します。
  • 管理モードで実行されるスレッドのスタックオーバーフローはキャッチされる可能性がありますが、システムの完全性は保証されません。

ポリシーの詳細#

概括的に言えば、これらのスレッドレベルのメモリ保護目標を達成するために、以下のメカニズムを使用します:

  • いかなるユーザースレッドもメモリのサブセットにしかアクセスできません:通常はそのスタック、プログラムテキスト、読み取り専用データ、およびそれが所属するメモリ保護設計で構成された任意のパーティションです。他のメモリへのアクセスは、スレッドの名義でシステムコールを介して行う必要があり、または監視スレッドがメモリドメイン API を使用して特別に付与する必要があります。新しく作成されたスレッドは親スレッドのメモリドメイン構成を継承します。スレッド間は、同じメモリドメインのメンバーシップを共有するか、カーネルオブジェクト(セマフォやパイプなど)を介して通信できます。
  • ユーザースレッドはカーネルオブジェクトに属するメモリに直接アクセスできません。カーネルオブジェクトのポインタはこれらのオブジェクトを参照するために使用できますが、カーネルオブジェクトの実際の操作はシステムコールインターフェースを介して行われます。デバイスドライバやスレッドスタックもカーネルオブジェクトと見なされます。これにより、カーネルオブジェクト内のカーネルプライベートデータが改ざんされることはありません。
  • ユーザースレッドはデフォルトで自分のスレッドオブジェクト以外のカーネルオブジェクトやドライバにアクセスする権限がありません。このアクセス権は、別の監視モードのスレッドによって付与される必要があります。または、そのスレッドが受信スレッドオブジェクトとアクセス権を付与されたカーネルオブジェクトの両方の権限を同時に持っている必要があります。新しいスレッドを作成する際に、親スレッドが付与したすべてのカーネルオブジェクトの権限を自動的に継承することを選択できますが、親スレッド自体は除外されます。
  • パフォーマンスとスペースの占有を考慮して、Zephyr は通常、カーネルオブジェクトやデバイスドライバ API に対してパラメータエラーチェックをほとんど行いません。ユーザーモードからのシステムコールアクセスは、アクセス権とオブジェクトタイプを厳密に検証し、境界チェックやその他の方法で他のパラメータの有効性を確認し、関連するメモリバッファへの正しい読み取り / 書き込みアクセスを検証する必要がある追加の処理関数レイヤーを伴います。
  • スレッドスタックの定義方法は、指定されたスタックスペースを超えるとハードウェア障害が発生することです。具体的な実装はアーキテクチャによって異なります。

注意#

** すべてのカーネルオブジェクト、スレッドスタック、およびデバイスドライバインスタンスをユーザーモードで使用するには、ビルド時に定義する必要があります。** カーネルオブジェクトの動的な使用例は、事前に定義された利用可能なオブジェクトプールを介って行う必要があります。

カーネル起動後に追加のアプリケーションバイナリデータをロードすると、いくつかの制限を受けます:

  • ロードされたオブジェクトコードは、カーネルが認識できるカーネルオブジェクトを定義することはできません。代わりに、これらのコードは API を使用してプールからカーネルオブジェクトを要求する必要があります。
  • 同様に、ロードされたターゲットコードはカーネルビルドプロセスの一部ではないため、どのモードで実行されても、これらのコードは中断ハンドラをインストールしたり、デバイスドライバをインスタンス化したり、システムコールを定義したりすることはできません。
  • ロードされたターゲットコードが検証されたソースコードからのものでない場合、CPU がユーザーモードにある場合に常に入力する必要があります。

サンプル説明#

ekko@work: ~/zephyrproject/zephyr/samples/userspace dev!
$ tree                                                                                                               
.
├── hello_world_user
   ├── CMakeLists.txt
   ├── README.rst
   ├── prj.conf
   ├── sample.yaml
   └── src
       └── main.c
├── malloc
   ├── CMakeLists.txt
   ├── prj.conf
   └── src
       └── main.c
├── prod_consumer
   ├── CMakeLists.txt
   ├── README.rst
   ├── prj.conf
   ├── sample.yaml
   └── src
       ├── app_a.c
       ├── app_a.h
       ├── app_b.c
       ├── app_b.h
       ├── app_shared.c
       ├── app_shared.h
       ├── app_syscall.c
       ├── app_syscall.h
       ├── main.c
       ├── sample_driver.h
       ├── sample_driver_foo.c
       └── sample_driver_handlers.c
├── shared_mem
   ├── CMakeLists.txt
   ├── README.rst
   ├── boards
   ├── nucleo_f746zg.overlay
   └── ok3568.conf
   ├── prj.conf
   ├── sample.yaml
   └── src
       ├── enc.c
       ├── enc.h
       ├── main.c
       └── main.h
└── syscall_perf
    ├── CMakeLists.txt
    ├── README.rst
    ├── prj.conf
    ├── sample.yaml
    └── src
        ├── main.c
        ├── test_supervisor.c
        ├── test_user.c
        └── thread_def.h

11 directories, 43 files

公式には 4 つのユースケースがあり、libc のテストで 1 つのユースケースが追加され、各ユースケースの内容は次のとおりです:

ユースケース機能
hello_world_user基本的なユースケースで、ユーザーモードスレッドが情報を印刷します。
prod_consumerプロデューサー - コンシューマーユースケースで、メモリドメイン構成、リソースプール構成、カーネルオブジェクト権限の伝達、システムコールなどのユーザーモード基本操作を含む、より完全なフローです。ユーザーモード開発の参考にできます。
shared_mem共有メモリユースケースで、主にメモリドメイン構成を利用して複数のユーザースレッド間の通信を実現します。
syscall_perfシステムコールユースケースで、特権モードとユーザーモードのシステムコールの性能差をテストしますが、使用されるのは RISC-V の CSR(制御状態)レジスタシステムコールです。
mallocユーザーモードで libc の malloc の例を使用します。

開発ガイド#

このセクションでは、ユーザーモードを使用するために必要な手順と関連する操作について基本的な説明を行います。詳細な手順については prod_consumer の例を参照してください。

1. プロジェクト設定#

Zephyr プロジェクトファイルでユーザースペースを有効にする必要があります。

[!prj.conf]
CONFIG_USERSPACE=y

2. ユーザーモードのグローバル変数定義#

ユーザーモードはメモリ権限に要求があるため、使用するグローバル変数は手動で特定のパーティションに計画する必要があります:

K_APPMEM_PARTITION_DEFINE(app_a_partition);//aアプリが使用するパーティションを定義

#define APP_A_DATA  K_APP_DMEM(app_a_partition)//データセクション、初期値が0でないグローバル/静的データを格納
#define APP_A_BSS   K_APP_BMEM(app_a_partition)//bssセクション、初期値が0のグローバル/静的データを格納

//以降のグローバル変数定義にはデータセクション修飾子を前に付ける必要があります
APP_A_BSS unsigned int count;
APP_A_DATA uint32_t baudrate = 115200;

3. ユーザースレッドの作成#

通常のスレッド作成プロセスと同じですが、オプションで K_USER を選択し、ユーザーモードを指定し、スレッドの実行遅延を無限に設定します。なぜなら、後でこのスレッドに対して他の操作があるからです。

k_tid_t user_tid = k_thread_create(&user_thread, user_stack, STACKSIZE,
			thread_entry, NULL, NULL, NULL,
			-1, K_USER,
			K_FOREVER);

4. メモリドメインの設定#

ユーザーモードのスレッドはメモリドメイン内のパーティションを操作する権限しかありません。このステップでは、前述のグローバル変数のパーティション権限をユーザースレッドに付与します。メモリドメインは複数のパーティションを設定でき、動的に変更することもできます。複数のユーザースレッドは、全員が権限を持つパーティションを介して共有メモリ通信とデータ交換を実現できます。

struct k_mem_domain user_domain;//メモリドメインを定義

//aアプリのパーティションリスト
struct k_mem_partition *app_a_parts[] = {
		&user_hello,
		&z_libc_partition,//libcグローバル変数パーティション
		&z_malloc_partition//libc mallocパーティション
	};
//メモリドメインを初期化
k_mem_domain_init(&user_domain, ARRAY_SIZE(app_a_parts), app_a_parts);

//ユーザースレッドをメモリドメインに追加
k_mem_domain_add_thread(&user_domain, user_tid);

5. リソースプールの設定#

リソースプールは k_heap のポインタであり、メモリを要求できるヒープです。いくつかの API 関数では、このリソースプールを使用して操作を行う必要があります。作成されたスレッドはデフォルトでリソースプールが設定されていないため、特権モードのスレッドはシステム自体のリソースプールを使用できますが、ユーザーモードのスレッドは手動でリソースプールをバインドする必要があります:

//グローバル変数定義リソースプール
K_HEAP_DEFINE(app_a_resource_pool, 256 * 5 + 128);
//リソースプールをスレッドにバインド
k_thread_heap_assign(user_tid, &app_a_resource_pool);

6. カーネルオブジェクトの権限付与#

カーネルオブジェクトの定義にはシステムヒープが使用されており、ユーザーモードはこの操作を行う権限がありません。したがって、グローバル変数の作成(機能概要の最後の注意に記載)を行い、その後権限付与の方法でユーザースレッドに渡します:

//メッセージキュー
K_MSGQ_DEFINE(mqueue, SAMPLE_DRIVER_MSG_SIZE, MAX_MSGS, 4);
//キュー
K_QUEUE_DEFINE(queue);
//ドライバ
APP_A_BSS const struct device *sample_device;
sample_device = device_get_binding(SAMPLE_DRIVER_NAME_0);
//カーネルオブジェクトの権限付与リスト
k_thread_access_grant(user_tid, &mqueue, &queue, sample_device);

7. ユーザースレッドの実行#

ついに最後のステップです。この後、このスレッドはユーザーモードで実行されます。

k_thread_start(user_tid);

8. カーネルオブジェクトの動的作成#

ユーザースレッドが、権限付与されたカーネルオブジェクトを介してグローバルカーネルオブジェクトを取得するのではなく、内部で使用するカーネルオブジェクト(セマフォ、キュー、ロックなど)を作成する必要がある場合は、以下の手順に従って作成します:

1. プロジェクト設定#

まず、動的作成オブジェクト機能を有効にする必要があります。この機能はユーザーモードに特有のもので、プロジェクト設定ファイルに以下のオプションを追加します:

CONFIG_DYNAMIC_OBJECTS=y

2. リソースプールが設定されていることを確認#

これは前述の第 5 ステップ、リソースプールの設定です。リソースプールは、権限付与されたカーネルオブジェクトの一部 API の使用に加え、動的にカーネルオブジェクトを作成するためにも使用できます。

3. カーネルオブジェクトを使用して作成#

/*
K_OBJ_MEM_SLAB,
K_OBJ_MSGQ,
K_OBJ_MUTEX,
K_OBJ_PIPE,
K_OBJ_QUEUE,
K_OBJ_POLL_SIGNAL,
K_OBJ_SEM,
K_OBJ_STACK,
K_OBJ_THREAD,
K_OBJ_TIMER,
K_OBJ_THREAD_STACK_ELEMENT,などのタイプ
*/
	//キュー
	struct k_queue *queue = k_object_alloc(K_OBJ_QUEUE);
	printf("queue : %p\n",queue);
	k_queue_init(queue);
	printf("k_queue_init finish\n");
	int test = 10;
	

	k_queue_alloc_append(queue,&test);
	printf("k_queue_alloc_append finish\n");

	int *get = k_queue_get(queue,K_NO_WAIT);
	printf("get %p : %d\n",get,*get);

	//ロック
	struct k_mutex *mutex = k_object_alloc(K_OBJ_MUTEX);
	printf("mutex : %p\n",mutex);

	k_mutex_init(mutex);
	printf("k_mutex_init finish\n");

	k_mutex_lock(mutex,K_FOREVER);
	printf("k_mutex_lock finish\n");

	k_mutex_unlock(mutex);
	printf("k_mutex_unlock finish\n");

	//メッセージキュー
	struct k_msgq *msgq = k_object_alloc(K_OBJ_MSGQ);
	printf("msgq : %p\n",msgq);

	k_msgq_alloc_init(msgq,1,5);
	printf("k_msgq_alloc_init finish\n");

	k_msgq_put(msgq,&test,K_NO_WAIT);
	printf("k_msgq_put finish\n");
	int recv = 0;
	k_msgq_get(msgq,&recv,K_NO_WAIT);
	printf("recv : %d\n",recv);
	//セマフォ
	struct k_sem *sem = k_object_alloc(K_OBJ_SEM);
	printf("sem : %p\n",sem);

	k_sem_init(sem,0,10);
	printf("k_sem_init finish\n");

	k_sem_give(sem);
	printf("k_sem_give finish\n");

	k_sem_take(sem,K_NO_WAIT);
	printf("k_sem_take finish\n");

	//スタック
	struct k_stack *stack = k_object_alloc(K_OBJ_STACK);
	printf("stack : %p\n",stack);

	k_stack_alloc_init(stack,10);
	printf("k_sem_take finish\n");

	k_stack_push(stack,1);
	printf("k_sem_take finish\n");

	stack_data_t pop;
	k_stack_pop(stack,&pop,K_NO_WAIT);
	printf("pop : %ld\n",pop);

4. 注意事項#

すべての API が正常に使用できるわけではありません。上記の例では正常に使用できる API が示されていますが、ユーザーモードで動的に作成できるオブジェクトの完全な API リストは公式文書に見つかりませんでした。参考資料はメモリ保護設計 #thread-resource-pools— Zephyr プロジェクトドキュメントカーネルオブジェクト — Zephyr プロジェクトドキュメントです。

使用状況#

基本的に前述の手順で現在使用可能な部分が説明されています:

  1. libc ライブラリの使用
  2. 権限付与されたパーティションのグローバル変数の使用
  3. カーネルオブジェクトの権限付与使用
  4. カーネルオブジェクトの動的作成

長所と短所の分析#

長所#

前述の機能概要でユーザーモードのさまざまな特性の利点について説明しました。この部分では、実際の使用におけるいくつかのシナリオについて説明します:

  1. ユーザースレッドのクラッシュはシステムのクラッシュを引き起こさず、監視スレッドを使用して各ユーザースレッドの実行状況を監視し、クラッシュ後に対応するスレッドを再起動することで、オペレーティングシステム自体に影響を与えないようにできます。
  2. ユーザースレッドの権限は制限されており、複数のチームやユーザーによるアプリ開発でエラーが発生しても他の部分に影響を与えず、サンドボックス性の隔離性が良好です。
  3. メモリ権限が明確であり、開発プロセス中の多くのプログラミングエラーを回避できます。

短所#

  1. プロセスが複雑で、ハードルが高くなります。
  2. 権限が制限されており、一部の POSIX 関連インターフェースは多くのカーネルオブジェクトをラップしており、現在のところ権限を付与する良い方法が見つかっていないため、Zephyr が定義したカーネルオブジェクトを使用して権限付与操作を行う必要があります。Zephyr カーネルオブジェクトへの依存性が高くなります。
  3. パフォーマンスの損失。前述のシステムコール性能テストユースケースは RISC-V プラットフォームのもので、以下の出力はクロック消費と命令数の違いを示しています:
    ユーザースレッド: 18012 サイクル 748 命令
    監視スレッド: 7 サイクル 4 命令
    ユーザースレッド: 20136 サイクル 748 命令
    監視スレッド: 7 サイクル 4 命令
    ユーザースレッド: 18014 サイクル 748 命令
    監視スレッド: 7 サイクル 4 命令

この記事はユーザーモードの初期テスト後の簡単なまとめに過ぎません。詳細な資料については公式サイトやサンプルプログラムを参照してください。ユーザーモード — Zephyr プロジェクトドキュメント

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。