요약·해설과 원문, 전문 번역을 서로 분리했습니다. API 이름, symbol, source path는 원문 표기를 사용합니다.
1. 요약·해설
원문의 핵심 논리와 kernel programming 관점의 보충 설명입니다. 아래의 전문 번역과는 별도로 작성했습니다.
2. 영어 원문 전체
번역 기준이 된 Linux v6.18.37 원문입니다. 줄 번호는 이 버전의 파일 좌표입니다.
원문 전체 펼치기
.. SPDX-License-Identifier: GPL-2.0
==============================================================================
Concurrent Modification and Execution of Instructions (CMODX) for RISC-V Linux
==============================================================================
CMODX is a programming technique where a program executes instructions that were
modified by the program itself. Instruction storage and the instruction cache
(icache) are not guaranteed to be synchronized on RISC-V hardware. Therefore, the
program must enforce its own synchronization with the unprivileged fence.i
instruction.
CMODX in the Kernel Space
-------------------------
Dynamic ftrace
---------------------
Essentially, dynamic ftrace directs the control flow by inserting a function
call at each patchable function entry, and patches it dynamically at runtime to
enable or disable the redirection. In the case of RISC-V, 2 instructions,
AUIPC + JALR, are required to compose a function call. However, it is impossible
to patch 2 instructions and expect that a concurrent read-side executes them
without a race condition. This series makes atmoic code patching possible in
RISC-V ftrace. Kernel preemption makes things even worse as it allows the old
state to persist across the patching process with stop_machine().
In order to get rid of stop_machine() and run dynamic ftrace with full kernel
preemption, we partially initialize each patchable function entry at boot-time,
setting the first instruction to AUIPC, and the second to NOP. Now, atmoic
patching is possible because the kernel only has to update one instruction.
According to Ziccif, as long as an instruction is naturally aligned, the ISA
guarantee an atomic update.
By fixing down the first instruction, AUIPC, the range of the ftrace trampoline
is limited to +-2K from the predetermined target, ftrace_caller, due to the lack
of immediate encoding space in RISC-V. To address the issue, we introduce
CALL_OPS, where an 8B naturally align metadata is added in front of each
pacthable function. The metadata is resolved at the first trampoline, then the
execution can be derect to another custom trampoline.
CMODX in the User Space
-----------------------
Though fence.i is an unprivileged instruction, the default Linux ABI prohibits
the use of fence.i in userspace applications. At any point the scheduler may
migrate a task onto a new hart. If migration occurs after the userspace
synchronized the icache and instruction storage with fence.i, the icache on the
new hart will no longer be clean. This is due to the behavior of fence.i only
affecting the hart that it is called on. Thus, the hart that the task has been
migrated to may not have synchronized instruction storage and icache.
There are two ways to solve this problem: use the riscv_flush_icache() syscall,
or use the ``PR_RISCV_SET_ICACHE_FLUSH_CTX`` prctl() and emit fence.i in
userspace. The syscall performs a one-off icache flushing operation. The prctl
changes the Linux ABI to allow userspace to emit icache flushing operations.
As an aside, "deferred" icache flushes can sometimes be triggered in the kernel.
At the time of writing, this only occurs during the riscv_flush_icache() syscall
and when the kernel uses copy_to_user_page(). These deferred flushes happen only
when the memory map being used by a hart changes. If the prctl() context caused
an icache flush, this deferred icache flush will be skipped as it is redundant.
Therefore, there will be no additional flush when using the riscv_flush_icache()
syscall inside of the prctl() context.
prctl() Interface
---------------------
Call prctl() with ``PR_RISCV_SET_ICACHE_FLUSH_CTX`` as the first argument. The
remaining arguments will be delegated to the riscv_set_icache_flush_ctx
function detailed below.
.. kernel-doc:: arch/riscv/mm/cacheflush.c
:identifiers: riscv_set_icache_flush_ctx
Example usage:
The following files are meant to be compiled and linked with each other. The
modify_instruction() function replaces an add with 0 with an add with one,
causing the instruction sequence in get_value() to change from returning a zero
to returning a one.
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
3. 한국어 전문 번역
영어 원문의 문단 순서와 의미를 유지한 전체 번역입니다. 코드, 함수명, symbol과 URL은 원문 표기를 유지합니다.
RISC-V CMODX
1-12CMODX(Concurrent Modification and Execution of Instructions)는 program이 스스로 수정한 instruction을 실행하는 기법입니다. RISC-V hardware에서는 instruction storage와 instruction cache(icache)의 동기화가 보장되지 않으므로 program이 unprivileged `fence.i` instruction으로 직접 동기화해야 합니다.
Kernel space의 dynamic ftrace
13-40Dynamic ftrace는 patch 가능한 function entry마다 function call을 넣고 runtime에 patch하여 control-flow redirection을 켜거나 끕니다. RISC-V function call은 `AUIPC + JALR` 두 instruction이 필요하므로 concurrent read-side와 race 없이 두 instruction을 patch할 수 없습니다. Kernel preemption은 `stop_machine()`을 쓰는 patching 도중에도 old state가 남게 해 문제를 더 어렵게 합니다.
`stop_machine()`을 제거하고 full kernel preemption에서 dynamic ftrace를 실행하기 위해 boot-time에 각 patchable function entry를 일부 초기화합니다. 첫 instruction을 `AUIPC`, 둘째를 `NOP`으로 고정하면 kernel은 한 instruction만 update하면 됩니다. `Ziccif`는 instruction이 natural alignment를 만족하는 한 atomic update를 보장합니다.
첫 instruction을 `AUIPC`로 고정하면 RISC-V immediate encoding 공간 때문에 ftrace trampoline range가 미리 정한 target `ftrace_caller`의 `+-2K`로 제한됩니다. 이를 해결하는 `CALL_OPS`는 각 patchable function 앞에 natural alignment를 만족하는 8B metadata를 추가합니다. 첫 trampoline이 metadata를 resolve한 뒤 다른 custom trampoline으로 execution을 보낼 수 있습니다.
Boot-time의 2-instruction slot을 runtime의 single-instruction update로 바꿉니다.
User space의 icache 동기화
41-64`fence.i`는 unprivileged instruction이지만 기본 Linux ABI는 userspace application에서 사용하지 못하게 합니다. Scheduler가 task를 새 hart로 migrate할 수 있고, `fence.i`는 호출된 hart에만 영향을 주므로 migration 뒤 새 hart의 instruction storage와 icache는 동기화되지 않을 수 있기 때문입니다.
해결책은 `riscv_flush_icache()` syscall로 일회성 icache flush를 수행하거나, `PR_RISCV_SET_ICACHE_FLUSH_CTX`를 `prctl()`에 전달해 Linux ABI context를 바꾸고 userspace에서 `fence.i`를 실행하는 것입니다.
Kernel의 deferred icache flush는 현재 `riscv_flush_icache()` syscall과 `copy_to_user_page()`에서만 발생하며 hart가 사용하는 memory map이 바뀔 때 실행됩니다. `prctl()` context가 이미 icache를 flush했다면 중복 deferred flush는 생략되므로 그 context 안에서 syscall을 사용해도 추가 flush는 없습니다.
일회성 syscall과 process context 기반 `fence.i` 허용 경로입니다.
prctl() interface
65-75`prctl()`의 첫 argument로 `PR_RISCV_SET_ICACHE_FLUSH_CTX`를 전달합니다. 나머지 argument는 `arch/riscv/mm/cacheflush.c`의 `riscv_set_icache_flush_ctx` function에 위임됩니다.
C example
76-106다음 두 file은 함께 compile하고 link합니다. `modify_instruction()`은 0을 더하는 instruction을 1을 더하도록 바꿔 `get_value()`의 return value를 0에서 1로 변경합니다.
#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;
}
Assembly example
107-130Assembly는 `old_insn`의 `addi a0, a0, 0`을 `new_insn`의 `addi a0, a0, 1`로 교체하고 `fence.i`로 instruction storage와 icache를 동기화합니다.
.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
요약과 해설
cmodx.rst:1-130Kernel ftrace는 두 instruction call slot을 boot-time에 준비해 runtime patch를 한 instruction으로 줄입니다. Userspace는 hart-local `fence.i`의 migration 문제 때문에 syscall 또는 명시적인 `prctl()` context를 사용해야 합니다.