요약·해설과 원문, 전문 번역을 서로 분리했습니다. API 이름, symbol, source path는 원문 표기를 사용합니다.
1. 요약·해설
원문의 핵심 논리와 kernel programming 관점의 보충 설명입니다. 아래의 전문 번역과는 별도로 작성했습니다.
2. 영어 원문 전체
번역 기준이 된 Linux v6.18.37 원문입니다. 줄 번호는 이 버전의 파일 좌표입니다.
원문 전체 펼치기
============================
Kernel-provided User Helpers
============================
These are segment of kernel provided user code reachable from user space
at a fixed address in kernel memory. This is used to provide user space
with some operations which require kernel help because of unimplemented
native feature and/or instructions in many ARM CPUs. The idea is for this
code to be executed directly in user mode for best efficiency but which is
too intimate with the kernel counter part to be left to user libraries.
In fact this code might even differ from one CPU to another depending on
the available instruction set, or whether it is a SMP systems. In other
words, the kernel reserves the right to change this code as needed without
warning. Only the entry points and their results as documented here are
guaranteed to be stable.
This is different from (but doesn't preclude) a full blown VDSO
implementation, however a VDSO would prevent some assembly tricks with
constants that allows for efficient branching to those code segments. And
since those code segments only use a few cycles before returning to user
code, the overhead of a VDSO indirect far call would add a measurable
overhead to such minimalistic operations.
User space is expected to bypass those helpers and implement those things
inline (either in the code emitted directly by the compiler, or part of
the implementation of a library call) when optimizing for a recent enough
processor that has the necessary native support, but only if resulting
binaries are already to be incompatible with earlier ARM processors due to
usage of similar native instructions for other things. In other words
don't make binaries unable to run on earlier processors just for the sake
of not using these kernel helpers if your compiled code is not going to
use new instructions for other purpose.
New helpers may be added over time, so an older kernel may be missing some
helpers present in a newer kernel. For this reason, programs must check
the value of __kuser_helper_version (see below) before assuming that it is
safe to call any particular helper. This check should ideally be
performed only once at process startup time, and execution aborted early
if the required helpers are not provided by the kernel version that
process is running on.
kuser_helper_version
--------------------
Location: 0xffff0ffc
Reference declaration::
extern int32_t __kuser_helper_version;
Definition:
This field contains the number of helpers being implemented by the
running kernel. User space may read this to determine the availability
of a particular helper.
Usage example::
#define __kuser_helper_version (*(int32_t *)0xffff0ffc)
void check_kuser_version(void)
{
if (__kuser_helper_version < 2) {
fprintf(stderr, "can't do atomic operations, kernel too old\n");
abort();
}
}
Notes:
User space may assume that the value of this field never changes
during the lifetime of any single process. This means that this
field can be read once during the initialisation of a library or
startup phase of a program.
kuser_get_tls
-------------
Location: 0xffff0fe0
Reference prototype::
void * __kuser_get_tls(void);
Input:
lr = return address
Output:
r0 = TLS value
Clobbered registers:
none
Definition:
Get the TLS value as previously set via the __ARM_NR_set_tls syscall.
Usage example::
typedef void * (__kuser_get_tls_t)(void);
#define __kuser_get_tls (*(__kuser_get_tls_t *)0xffff0fe0)
void foo()
{
void *tls = __kuser_get_tls();
printf("TLS = %p\n", tls);
}
Notes:
- Valid only if __kuser_helper_version >= 1 (from kernel version 2.6.12).
kuser_cmpxchg
-------------
Location: 0xffff0fc0
Reference prototype::
int __kuser_cmpxchg(int32_t oldval, int32_t newval, volatile int32_t *ptr);
Input:
r0 = oldval
r1 = newval
r2 = ptr
lr = return address
Output:
r0 = success code (zero or non-zero)
C flag = set if r0 == 0, clear if r0 != 0
Clobbered registers:
r3, ip, flags
Definition:
Atomically store newval in `*ptr` only if `*ptr` is equal to oldval.
Return zero if `*ptr` was changed or non-zero if no exchange happened.
The C flag is also set if `*ptr` was changed to allow for assembly
optimization in the calling code.
Usage example::
typedef int (__kuser_cmpxchg_t)(int oldval, int newval, volatile int *ptr);
#define __kuser_cmpxchg (*(__kuser_cmpxchg_t *)0xffff0fc0)
int atomic_add(volatile int *ptr, int val)
{
int old, new;
do {
old = *ptr;
new = old + val;
} while(__kuser_cmpxchg(old, new, ptr));
return new;
}
Notes:
- This routine already includes memory barriers as needed.
- Valid only if __kuser_helper_version >= 2 (from kernel version 2.6.12).
kuser_memory_barrier
--------------------
Location: 0xffff0fa0
Reference prototype::
void __kuser_memory_barrier(void);
Input:
lr = return address
Output:
none
Clobbered registers:
none
Definition:
Apply any needed memory barrier to preserve consistency with data modified
manually and __kuser_cmpxchg usage.
Usage example::
typedef void (__kuser_dmb_t)(void);
#define __kuser_dmb (*(__kuser_dmb_t *)0xffff0fa0)
Notes:
- Valid only if __kuser_helper_version >= 3 (from kernel version 2.6.15).
kuser_cmpxchg64
---------------
Location: 0xffff0f60
Reference prototype::
int __kuser_cmpxchg64(const int64_t *oldval,
const int64_t *newval,
volatile int64_t *ptr);
Input:
r0 = pointer to oldval
r1 = pointer to newval
r2 = pointer to target value
lr = return address
Output:
r0 = success code (zero or non-zero)
C flag = set if r0 == 0, clear if r0 != 0
Clobbered registers:
r3, lr, flags
Definition:
Atomically store the 64-bit value pointed by `*newval` in `*ptr` only if `*ptr`
is equal to the 64-bit value pointed by `*oldval`. Return zero if `*ptr` was
changed or non-zero if no exchange happened.
The C flag is also set if `*ptr` was changed to allow for assembly
optimization in the calling code.
Usage example::
typedef int (__kuser_cmpxchg64_t)(const int64_t *oldval,
const int64_t *newval,
volatile int64_t *ptr);
#define __kuser_cmpxchg64 (*(__kuser_cmpxchg64_t *)0xffff0f60)
int64_t atomic_add64(volatile int64_t *ptr, int64_t val)
{
int64_t old, new;
do {
old = *ptr;
new = old + val;
} while(__kuser_cmpxchg64(&old, &new, ptr));
return new;
}
Notes:
- This routine already includes memory barriers as needed.
- Due to the length of this sequence, this spans 2 conventional kuser
"slots", therefore 0xffff0f80 is not used as a valid entry point.
- Valid only if __kuser_helper_version >= 5 (from kernel version 3.1).
3. 한국어 전문 번역
영어 원문의 문단 순서와 의미를 유지한 전체 번역입니다. 코드, 함수명, symbol과 URL은 원문 표기를 유지합니다.
Kernel-provided User Helpers
1-41kernel memory의 고정 주소에 놓이고 userspace에서 접근할 수 있는 kernel 제공 user code segment입니다. 많은 ARM CPU에 native feature나 instruction이 없어 kernel 도움이 필요한 operation을 효율적으로 제공합니다.
이 code는 user mode에서 직접 실행되지만 kernel counterpart와 밀접해 user library에 맡기기 어렵습니다. CPU instruction set이나 SMP 여부에 따라 구현이 달라질 수 있으며 kernel은 예고 없이 code를 바꿀 수 있습니다. 여기 문서화된 entry point와 결과만 stable ABI로 보장합니다.
full VDSO 구현과는 다른 방식이며 VDSO를 배제하지도 않습니다. VDSO는 constant를 이용한 assembly branch 최적화를 막을 수 있고, helper 자체가 몇 cycle 만에 돌아오므로 indirect far call overhead도 측정 가능할 정도로 큽니다.
충분히 최신 processor를 target으로 하고 다른 native instruction 때문에 binary가 이미 구형 ARM과 호환되지 않는다면 compiler inline code나 library call 구현으로 helper를 우회할 수 있습니다. helper 하나를 피하려는 이유만으로 기존 processor 호환성을 버려서는 안 됩니다.
시간이 지나며 새 helper가 추가되므로 old kernel에는 helper가 없을 수 있습니다. program은 helper를 호출하기 전에 `__kuser_helper_version`을 확인해야 합니다. process startup에서 한 번 검사하고 필요한 helper가 없으면 일찍 종료하는 것이 좋습니다.
kuser_helper_version
42-75| 항목 | 값 |
|---|---|
| location | `0xffff0ffc` |
| symbol | `__kuser_helper_version` |
| 의미 | 실행 중인 kernel이 구현한 helper 수 |
extern int32_t __kuser_helper_version;
userspace는 값을 읽어 특정 helper의 availability를 판단합니다. 다음 예는 version이 2보다 작으면 atomic operation을 제공하지 못하는 오래된 kernel로 보고 종료합니다.
#define __kuser_helper_version (*(int32_t *)0xffff0ffc)
void check_kuser_version(void)
{
if (__kuser_helper_version < 2) {
fprintf(stderr, "can't do atomic operations, kernel too old\n");
abort();
}
}
한 process의 lifetime 동안 값은 바뀌지 않는다고 가정할 수 있으므로 library initialization 또는 program startup에서 한 번만 읽어도 됩니다.
kuser_get_tls
76-115| 항목 | 값 |
|---|---|
| location | `0xffff0fe0` |
| prototype | `void * __kuser_get_tls(void)` |
| input | `lr` = return address |
| output | `r0` = TLS value |
| clobbered | 없음 |
| version | `__kuser_helper_version >= 1`, kernel 2.6.12부터 |
void * __kuser_get_tls(void);
`__ARM_NR_set_tls` syscall로 앞서 설정한 TLS value를 반환합니다. 고정 주소를 function pointer로 선언해 직접 호출할 수 있습니다.
typedef void * (__kuser_get_tls_t)(void);
#define __kuser_get_tls (*(__kuser_get_tls_t *)0xffff0fe0)
void foo()
{
void *tls = __kuser_get_tls();
printf("TLS = %p\n", tls);
}
kuser_cmpxchg
116-170| 항목 | 값 |
|---|---|
| location | `0xffff0fc0` |
| prototype | `int __kuser_cmpxchg(int32_t oldval, int32_t newval, volatile int32_t *ptr)` |
| input | `r0=oldval`, `r1=newval`, `r2=ptr`, `lr=return address` |
| output | `r0` success code, 성공이면 0. C flag는 `r0 == 0`이면 set |
| clobbered | `r3`, `ip`, flags |
| version | `__kuser_helper_version >= 2`, kernel 2.6.12부터 |
int __kuser_cmpxchg(int32_t oldval, int32_t newval, volatile int32_t *ptr);
`*ptr`이 `oldval`과 같을 때만 `newval`을 atomic하게 저장합니다. 교환했으면 0, 하지 않았으면 non-zero를 반환합니다. assembly caller 최적화를 위해 성공 시 C flag도 set합니다.
아래 `atomic_add()` 예는 현재 값을 읽고 새 값을 계산한 뒤 `__kuser_cmpxchg()`가 성공할 때까지 반복합니다.
typedef int (__kuser_cmpxchg_t)(int oldval, int newval, volatile int *ptr);
#define __kuser_cmpxchg (*(__kuser_cmpxchg_t *)0xffff0fc0)
int atomic_add(volatile int *ptr, int val)
{
int old, new;
do {
old = *ptr;
new = old + val;
} while(__kuser_cmpxchg(old, new, ptr));
return new;
}
필요한 memory barriers는 routine 자체에 이미 포함되어 있습니다.
kuser_memory_barrier
171-205| 항목 | 값 |
|---|---|
| location | `0xffff0fa0` |
| prototype | `void __kuser_memory_barrier(void)` |
| input | `lr` = return address |
| output | 없음 |
| clobbered | 없음 |
| version | `__kuser_helper_version >= 3`, kernel 2.6.15부터 |
void __kuser_memory_barrier(void);
직접 수정한 data와 `__kuser_cmpxchg` 사용 사이의 consistency를 보존하는 데 필요한 memory barrier를 적용합니다.
typedef void (__kuser_dmb_t)(void);
#define __kuser_dmb (*(__kuser_dmb_t *)0xffff0fa0)
kuser_cmpxchg64
206-268| 항목 | 값 |
|---|---|
| location | `0xffff0f60` |
| prototype | `int __kuser_cmpxchg64(const int64_t *oldval, const int64_t *newval, volatile int64_t *ptr)` |
| input | `r0` = oldval pointer, `r1` = newval pointer, `r2` = target pointer, `lr` = return address |
| output | `r0` success code, 성공이면 0. C flag는 `r0 == 0`이면 set |
| clobbered | `r3`, `lr`, flags |
| version | `__kuser_helper_version >= 5`, kernel 3.1부터 |
int __kuser_cmpxchg64(const int64_t *oldval,
const int64_t *newval,
volatile int64_t *ptr);
`*ptr`이 `*oldval`이 가리키는 64-bit value와 같을 때만 `*newval`의 64-bit value를 atomic하게 저장합니다. 바뀌었으면 0, 교환이 없으면 non-zero이며 성공 시 C flag도 set합니다.
`atomic_add64()` 예는 old/new 64-bit value의 주소를 전달해 성공할 때까지 compare-exchange를 반복합니다.
typedef int (__kuser_cmpxchg64_t)(const int64_t *oldval,
const int64_t *newval,
volatile int64_t *ptr);
#define __kuser_cmpxchg64 (*(__kuser_cmpxchg64_t *)0xffff0f60)
int64_t atomic_add64(volatile int64_t *ptr, int64_t val)
{
int64_t old, new;
do {
old = *ptr;
new = old + val;
} while(__kuser_cmpxchg64(&old, &new, ptr));
return new;
}
routine은 필요한 memory barriers를 포함합니다. sequence가 길어 conventional kuser slot 두 개를 차지하므로 `0xffff0f80`은 유효한 entry point로 쓰지 않습니다.
요약과 해설
kernel_user_helpers.rst:1-268helper 구현 code는 kernel과 CPU에 따라 달라질 수 있지만 고정 entry address와 결과는 ABI입니다. process startup에서 version을 한 번 확인한 뒤 필요한 helper만 호출해야 합니다.
주소는 높은 helper version word에서 낮은 64-bit compare-exchange routine 순으로 배치됩니다.