RISC-V Linux 的併發修改和指令執行 (CMODX)

CMODX 是一種程式設計技術,其中程式執行由程式本身修改的指令。RISC-V 硬體不保證指令儲存和指令快取 (icache) 同步。因此,程式必須使用非特權 fence.i 指令強制執行其自己的同步。

核心空間中的 CMODX

動態 ftrace

本質上,動態 ftrace 透過在每個可修補的函式入口處插入函式呼叫來指導控制流,並在執行時動態地修補它以啟用或停用重定向。對於 RISC-V,需要 2 條指令 AUIPC + JALR 來組成一個函式呼叫。但是,不可能修補 2 條指令並期望併發讀取方在沒有競爭條件的情況下執行它們。該系列使 RISC-V ftrace 中可以進行原子程式碼修補。核心搶佔使情況變得更糟,因為它允許舊狀態在帶有 stop_machine() 的修補過程中持續存在。

為了擺脫 stop_machine() 並執行具有完全核心搶佔的動態 ftrace,我們在啟動時部分初始化每個可修補的函式入口,將第一條指令設定為 AUIPC,第二條指令設定為 NOP。現在,原子修補是可能的,因為核心只需要更新一條指令。根據 Ziccif,只要指令是自然對齊的,ISA 就可以保證原子更新。

透過固定第一條指令 AUIPC,由於 RISC-V 中缺少立即編碼空間,ftrace trampoline 的範圍被限制在距預定目標 ftrace_caller 的 +-2K 範圍內。為了解決這個問題,我們引入了 CALL_OPS,其中在每個可修補函式的前面添加了一個 8B 自然對齊的元資料。該元資料在第一個 trampoline 中解析,然後執行可以重定向到另一個自定義 trampoline。

使用者空間中的 CMODX

雖然 fence.i 是一條非特權指令,但預設的 Linux ABI 禁止在使用者空間應用程式中使用 fence.i。在任何時候,排程程式都可能將任務遷移到新的 hart 上。如果在使用者空間使用 fence.i 同步了 icache 和指令儲存後發生遷移,則新 hart 上的 icache 將不再是乾淨的。這是由於 fence.i 的行為只會影響呼叫它的 hart。因此,任務已遷移到的 hart 可能沒有同步的指令儲存和 icache。

有兩種方法可以解決這個問題:使用 riscv_flush_icache() 系統呼叫,或者使用 PR_RISCV_SET_ICACHE_FLUSH_CTX prctl() 並在使用者空間發出 fence.i。系統呼叫執行一次性的 icache 重新整理操作。prctl 更改 Linux ABI 以允許使用者空間發出 icache 重新整理操作。

順便說一句,有時可以在核心中觸發“延遲” icache 重新整理。在編寫本文時,這僅在 riscv_flush_icache() 系統呼叫和核心使用 copy_to_user_page() 時發生。這些延遲重新整理僅在 hart 使用的記憶體對映發生更改時才會發生。如果 prctl() 上下文導致了 icache 重新整理,則將跳過此延遲的 icache 重新整理,因為它會多餘。因此,在使用 prctl() 上下文中的 riscv_flush_icache() 系統呼叫時,不會有額外的重新整理。

prctl() 介面

使用 PR_RISCV_SET_ICACHE_FLUSH_CTX 作為第一個引數呼叫 prctl()。其餘引數將委託給下面詳細介紹的 riscv_set_icache_flush_ctx 函式。

int riscv_set_icache_flush_ctx(unsigned long ctx, unsigned long scope)

啟用/停用使用者空間中的 icache 重新整理指令。

引數

unsigned long ctx

設定使用者空間中允許/禁止的 icache 重新整理指令的型別。支援的值如下所述。

unsigned long scope

設定允許發出 icache 重新整理指令的範圍。支援的值如下所述。

描述

ctx 的支援值

  • PR_RISCV_CTX_SW_FENCEI_ON: 允許在使用者空間中使用 fence.i。

  • PR_RISCV_CTX_SW_FENCEI_OFF: 禁止在使用者空間中使用 fence.i。當 scope == PR_RISCV_SCOPE_PER_PROCESS 時,程序中的所有執行緒都將受到影響。因此,必須謹慎;僅當您可以保證程序中沒有執行緒從此以後發出 fence.i 時才使用此標誌。

scope 的支援值

  • PR_RISCV_SCOPE_PER_PROCESS: 確保此程序中任何執行緒的 icache

    在遷移時與指令儲存一致。

  • PR_RISCV_SCOPE_PER_THREAD: 確保當前執行緒的 icache

    在遷移時與指令儲存一致。

scope == PR_RISCV_SCOPE_PER_PROCESS 時,允許程序中的所有執行緒發出 icache 重新整理指令。每當程序中的任何執行緒被遷移時,將保證相應 hart 的 icache 與指令儲存一致。這不強制執行遷移之外的任何保證。如果一個執行緒修改了另一個執行緒可能嘗試執行的指令,則另一個執行緒必須在嘗試執行可能已修改的指令之前發出 icache 重新整理指令。這必須由使用者空間程式執行。

在按執行緒上下文(例如 scope == PR_RISCV_SCOPE_PER_THREAD)中,僅允許呼叫此函式的執行緒發出 icache 重新整理指令。當執行緒被遷移時,將保證相應 hart 的 icache 與指令儲存一致。

在未配置 SMP 的核心上,此函式是一個空操作,因為不會發生跨 hart 的遷移。

用法示例

以下檔案旨在相互編譯和連結。modify_instruction() 函式用加一的指令替換加零的指令,導致 get_value() 中的指令序列從返回零變為返回一。

cmodx.c

#include <stdio.h>
#include <sys/prctl.h>

extern int get_value();
extern void modify_instruction();

int main()
{
        int value = get_value();
        printf("Value before cmodx: %d\n", value);

        // Call prctl before first fence.i is called inside modify_instruction
        prctl(PR_RISCV_SET_ICACHE_FLUSH_CTX, PR_RISCV_CTX_SW_FENCEI_ON, PR_RISCV_SCOPE_PER_PROCESS);
        modify_instruction();
        // Call prctl after final fence.i is called in process
        prctl(PR_RISCV_SET_ICACHE_FLUSH_CTX, PR_RISCV_CTX_SW_FENCEI_OFF, PR_RISCV_SCOPE_PER_PROCESS);

        value = get_value();
        printf("Value after cmodx: %d\n", value);
        return 0;
}

cmodx.S

.option norvc

.text
.global modify_instruction
modify_instruction:
lw a0, new_insn
lui a5,%hi(old_insn)
sw  a0,%lo(old_insn)(a5)
fence.i
ret

.section modifiable, "awx"
.global get_value
get_value:
li a0, 0
old_insn:
addi a0, a0, 0
ret

.data
new_insn:
addi a0, a0, 1