功能綜述#
[! 機翻自官網]
執行緒模型#
使用者模式執行緒被 Zephyr 視為不信任,因此與其他使用者模式執行緒和核心隔離。 有缺陷或惡意的使用者模式執行緒不能洩漏或修改另一個執行緒或核心的私有資料 / 資源,並且不能干擾或控制另一個使用者模式執行緒或核心。
Zephyr 使用者模式功能示例:
- 核心可以防止許多無意的程式設計錯誤,否則這些錯誤可能會悄無聲息地或令人震驚地破壞系統。
- 核心可以對複雜的資料解析器(如解釋器、網路協議和檔案系統)進行沙箱處理,這樣惡意的第三方程式碼或資料就無法危害核心或其他執行緒。
- 核心可以支持多個邏輯 "應用程式" 的概念,每個應用程式都有自己的執行緒組和私有資料結構,如果其中一個應用程式崩潰或受到其他危害,這些應用程式就會相互隔離。
設計目標#
對於在非特權 CPU 狀態(以下簡稱 "使用者模式")下運行的執行緒,我們的目標是防止以下情況發生:
- 我們會防止訪問未特別授權的記憶體,或錯誤訪問與策略不兼容的記憶體,如嘗試寫入唯讀區域。
- 對執行緒堆疊緩衝區的訪問將受到策略控制,該策略部分取決於底層記憶體保護硬體。
- 默認情況下,使用者執行緒可以讀 / 寫訪問自己的堆疊緩衝區。
- 使用者執行緒默認永遠無法訪問不屬於同一記憶體域的使用者執行緒堆疊。
- 默認情況下,使用者執行緒永遠無法訪問監督執行緒擁有的執行緒堆疊,或用於處理系統調用權限提升、中斷或 CPU 異常的執行緒堆疊。
- 使用者執行緒可以讀 / 寫訪問同一記憶體域中其他使用者執行緒的堆疊,具體取決於硬體。
- 在 MPU 系統上,執行緒只能訪問自己的堆疊緩衝區。
- 在 MMU 系統中,執行緒可以訪問同一記憶體域中的任何使用者執行緒堆疊。可攜式程式碼不應假定這一點。
- 默認情況下,所有核心範圍內的執行緒都可以以唯讀方式訪問程式文本和唯讀資料。這一策略可以調整。
- 除上述內容外,使用者執行緒默認不允許訪問任何記憶體。
- 我們根據每個物件或每個驅動程式實例的權限粒度,防止使用未經特別授權的設備驅動程式或核心物件。
- 我們會驗證帶有錯誤參數的核心或驅動程式 API 調用,否則會導致核心崩潰或核心專用資料結構損壞。這包括:
- 使用錯誤的核心物件類型。
- 使用超出適當範圍或不合理值的參數。
- 根據 API 的語義,傳遞調用執行緒沒有足夠權限讀取或寫入的記憶體緩衝區。
- 使用未處於正確初始化狀態的核心物件。
- 我們確保檢測和安全處理使用者模式堆疊溢出。
- 防止調用核心配置排除功能的系統調用。
- 防止禁用或篡改核心定義和硬體執行的記憶體保護。
- 防止從使用者模式重新進入監管模式,除非通過核心定義的系統調用和中斷處理程序。
- 防止使用者模式執行緒引入新的可執行程式碼,核心系統調用支持的情況除外。
我們特別不防範以下攻擊:
- 核心本身以及在管理模式下執行的任何執行緒都被假定為可信的。
- 假設構建系統使用的工具鏈和任何補充程式是可信的。
- 假設核心構建是可信的。有相當多的構建時邏輯用於創建有效核心物件表、定義系統調用和配置中斷。在此過程中使用的 .elf 二進制檔案都被假定為可信程式碼。
- 我們無法防止在核心模式下完成的記憶體域配置中發生錯誤,從而將私有核心資料結構暴露給使用者執行緒。核心物件的 RAM 應始終配置為僅限管理程式。
- 可以對使用者模式執行緒進行頂層聲明,並將權限分配給核心物件。一般來說,作為生成 zephyr.elf 的核心構建的一部分的所有 C 和標頭檔案都被假定為可信的。
- 我們不會通過執行緒 CPU 不足來防止 DDOS 攻擊。 Zephyr 沒有執行緒優先級老化,並且特定優先級的使用者執行緒可以使所有較低優先級的執行緒挨餓,如果未啟用時間分片,則還可以使其他相同優先級的執行緒挨餓。
- 對於可以同時活動的執行緒數量存在構建時定義的限制,在此之後創建新使用者執行緒將失敗。
- 在管理模式下運行的執行緒的堆疊溢出可能會被捕獲,但無法保證系統的完整性。
策略細節#
概括地說,我們通過以下機制來實現這些執行緒級記憶體保護目標:
- 任何使用者執行緒都只能訪問記憶體的一個子集:通常是它的堆疊、程式文本、唯讀資料,以及它所屬的記憶體保護設計中配置的任何分區。對其他記憶體的訪問必須通過系統調用以執行緒的名義進行,或由監督執行緒使用記憶體域應用程式介面專門授予。新創建的執行緒繼承父執行緒的記憶體域配置。執行緒之間可以通過共享相同記憶體域的成員身份,或通過核心物件(如信號量和管道)進行通信。
- 使用者執行緒不能直接訪問屬於核心物件的記憶體。雖然核心物件的指標可用於引用這些物件,但對核心物件的實際操作是通過系統調用介面完成的。設備驅動程式和執行緒堆疊也被視為核心物件。這就確保了核心物件中任何屬於核心私有的資料都不會被篡改。
- 使用者執行緒默認情況下沒有權限訪問除自己執行緒物件外的任何核心物件或驅動程式。這種訪問權限必須由另一個處於監督模式的執行緒授予,或者該執行緒同時擁有接收執行緒物件和被授予訪問權限的核心物件的權限。創建新執行緒時,可以選擇自動繼承父執行緒授予的所有核心物件權限,但父執行緒本身除外。
- 出於性能和佔用空間的考慮,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
官方共有四個用例,測試 libc 增加了一個用例,各用例內容:
用例 | 功能 |
---|---|
hello_world_user | 基礎用例,在使用者模式執行緒打印資訊 |
prod_consumer | 生產者消費者用例,較完整的流程,包括了記憶體域配置、資源池配置、核心物件權限傳遞、系統調用等使用者模式基本操作,使用者模式開發可參考該例程 |
shared_mem | 共享記憶體用例,主要靠記憶體域配置實現多使用者執行緒的通信 |
syscall_perf | 系統調用用例,用於測試特權模式和使用者模式系統調用的性能差異,但使用的 riscv 的 csr(控制狀態)寄存器系統調用 |
malloc | 使用者模式使用 libc 中 malloc 示例 |
開發指南#
本節講解使用使用者模式需要哪些步驟,涉及到哪些操作,只做基本描述,詳細步驟請參考 prod_consumer 例程。
1. 工程配置#
需要在 zephyr 工程文件中開啟使用者空間
[!prj.conf]
CONFIG_USERSPACE=y
2. 使用者模式全局變數定義#
使用者模式對記憶體權限有要求,因此使用到的全局變數需要手動規劃到具體的分區:
K_APPMEM_PARTITION_DEFINE(app_a_partition);//定義 a app 使用到的分區
#define APP_A_DATA K_APP_DMEM(app_a_partition)//data 資料段,存放初始不為 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. 創建使用者執行緒#
和普通創建執行緒流程相同,差異點在 option 選擇 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 app 的分區列表
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 列表,參考資料為Memory Protection Design #thread-resource-pools— Zephyr Project Documentation,Kernel Objects — Zephyr Project Documentation
使用現狀#
基本前面的步驟中已經說明了當前可以使用的部分:
- libc 庫使用
- 授權分區的全局變數使用
- 核心物件授權使用
- 動態創建核心物件
優缺點分析#
優點#
前面的功能綜述中已經對使用者模式下的各種特性優勢做了說明,這部分就說一下在實際使用中可能的幾個場景:
- 使用者執行緒崩潰不會導致系統崩潰,可以使用監控執行緒監控各使用者執行緒運行情況,崩潰後進行重啟對應執行緒,保證操作系統本身不受影響
- 使用者執行緒權限有限,多團隊多使用者 APP 開發即使出錯不會影響到其他部分,沙盒性隔離性比較好
- 記憶體權限明晰,可以避免很多開發過程中的程式設計錯誤
劣勢#
- 流程較為複雜,門檻變高
- 權限受限,有些 posix 相關介面封裝了很多核心物件,目前還未找到好的方式來授權,只能使用 zephyr 定義好的核心物件進行授權操作,對 zephyr 核心物件使用依賴性變高
- 性能損失,前面提到的系統調用性能測試用例是 RSICV 平台的,以下輸出供參考,列出了時鐘耗費及指令數量的差異:
使用者執行緒: 18012 cycles 748 instructions
監督執行緒: 7 cycles 4 instructions
使用者執行緒: 20136 cycles 748 instructions
監督執行緒: 7 cycles 4 instructions
使用者執行緒: 18014 cycles 748 instructions
監督執行緒: 7 cycles 4 instructions
本文只是初步測試使用者模式後的一點總結,詳細資料請參考官網以及用例程式。使用者模式 — Zephyr 專案文件