전체 흐름
범위는 AArch64 사용자 태스크가 svc #0로 일으킨 EL0 동기 예외다. entry.S가 레지스터 문맥을 저장하고 entry-common.c가 ESR의 예외 종류를 판별한다. SVC인 경우에만 do_el0_svc()로 넘어간다. 데이터 중단·명령 중단·IRQ는 시스템 호출 디스패치가 아니다.
- EL0 · SVC인자는 x0–x5, native 호출 번호는 x8.
- entry.S · kernel_entry
pt_regs를 만들고 기본syscallno를NO_SYSCALL로 둔다. - entry-common.c · SVC64 판별
el0t_64_sync_handler()가 ESR을 분기해el0_svc()로 보낸다. - syscall.c · el0_svc_common번호·원래 x0를 기록하고 진입 추적·스킵 조건을 처리한다.
- invoke_syscall테이블 범위 안이면 핸들러를 호출하고 결과를
regs에 저장한다. - exit_to_user_mode → kernel_exit복귀 작업 뒤 문맥을 복원하고
eret으로 EL0에 돌아간다.
갈림길: SVC가 아닌 예외는 별도 핸들러로 간다. 추적·seccomp·ptrace가 호출을 건너뛰거나 비정상 번호가 미구현 경로로 갈 수도 있다.
v6.6의 el0_svc()는 fp_user_discard() 뒤 exit_to_user_mode()를 호출한다. 버전별 복귀 코드를 같은 것으로 간주하지 않는다.
소스 코드 위치
entry.S의 예외 프레임 저장·복원, entry-common.c의 SVC 판별, syscall.c의 번호 검사·테이블 선택, kernel/entry/common.c의 사용자 복귀 작업을 대조했다.
Linux v6.6 원본: arch/arm64/kernel/entry.S arch/arm64/kernel/entry-common.c arch/arm64/kernel/syscall.c kernel/entry/common.c include/uapi/asm-generic/unistd.h
| 파일 | 연결 |
|---|---|
entry.S | kernel_entry → C handler → ret_to_user → kernel_exit 0 |
entry-common.c | ESR_ELx_EC_SVC64일 때 el0_svc로 분기하고 복귀 준비를 호출한다. |
syscall.c | do_el0_svc, el0_svc_common, invoke_syscall이 번호와 결과를 처리한다. |
kernel/entry/common.c | 조건에 따라 사용자 복귀 전 작업을 처리한다. |
대표 코드
kernel_entry 끝 / kernel_exit 시작
alternative_else_nop_endif
#endif
stp x22, x23, [sp, #S_PC]
/* Not in a syscall by default (el0_svc overwrites for real syscall) */
.if \el == 0
mov w21, #NO_SYSCALL
str w21, [sp, #S_SYSCALLNO]
.endif
#ifdef CONFIG_ARM64_PSEUDO_NMI
alternative_if_not ARM64_HAS_GIC_PRIO_MASKING
b .Lskip_pmr_save\@
alternative_else_nop_endif
mrs_s x20, SYS_ICC_PMR_EL1
str x20, [sp, #S_PMR_SAVE]
mov x20, #GIC_PRIO_IRQON | GIC_PRIO_PSR_I_SET
msr_s SYS_ICC_PMR_EL1, x20
.Lskip_pmr_save\@:
#endif
/*
* Registers that may be useful after this macro is invoked:
*
* x20 - ICC_PMR_EL1
* x21 - aborted SP
* x22 - aborted PC
* x23 - aborted PSTATE
*/
.endm
.macro kernel_exit, el
.if \el != 0
disable_daif
.endif
#ifdef CONFIG_ARM64_PSEUDO_NMI읽는 법: 이 발췌 자체는 el0_svc() 함수가 아니다. NO_SYSCALL은 EL0 예외의 기본값이며 실제 SVC 번호는 이후 syscall.c에서 기록한다. 끝부분은 kernel_exit의 시작이므로 전체 반환 어셈블리는 포함하지 않는다.
invoke_syscall
#endif
return sys_ni_syscall();
}
static long __invoke_syscall(struct pt_regs *regs, syscall_fn_t syscall_fn)
{
return syscall_fn(regs);
}
static void invoke_syscall(struct pt_regs *regs, unsigned int scno,
unsigned int sc_nr,
const syscall_fn_t syscall_table[])
{
long ret;
add_random_kstack_offset();
if (scno < sc_nr) {
syscall_fn_t syscall_fn;
syscall_fn = syscall_table[array_index_nospec(scno, sc_nr)];
ret = __invoke_syscall(regs, syscall_fn);
} else {
ret = do_ni_syscall(regs, scno);
}
syscall_set_return_value(current, regs, 0, ret);
/*
* Ultimately, this value will get limited by KSTACK_OFFSET_MAX(),
* but not enough for arm64 stack utilization comfort. To keep
* reasonable stack head room, reduce the maximum offset to 9 bits.
*
* The actual entropy will be further reduced by the compiler when
* applying stack alignment constraints: the AAPCS mandates a
* 16-byte (i.e. 4-bit) aligned SP at function boundaries.
*
* The resulting 5 bits of entropy is seen in SP[8:4].
*/
choose_random_kstack_offset(get_random_u16() & 0x1FF);읽는 법: 앞 함수의 마지막 줄에서 시작하는 발췌다. 번호 범위를 검사하고 array_index_nospec으로 인덱스를 제한한 다음 함수 포인터를 호출한다. 범위 밖이면 do_ni_syscall()로 가며 결과는 사용자 복귀용 regs에 기록된다.
함수별 분석
예외 진입, 호출 선택, 사용자 복귀는 서로 다른 층이다. el0_svc()만으로 번호 검사와 복귀 작업 전체를 설명할 수 없다.
| 순서 | 함수·매크로 | 역할 |
|---|---|---|
| 1 | kernel_entry 0,64 | 사용자 레지스터·PC·PSTATE를 저장하고 syscallno=NO_SYSCALL로 둔다. |
| 2 | el0t_64_sync_handler | ESR exception class가 SVC64인 경우에만 el0_svc를 선택한다. |
| 3 | el0_svc → do_el0_svc | 사용자 모드 진입 상태를 전환하고 x8 번호를 넘긴다. fp_user_discard 처리가 이 주변에 있다. |
| 4 | el0_svc_common | orig_x0와 번호를 저장한다. MTE fault·syscall-work 플래그는 실제 호출 전 분기를 만든다. |
| 5 | invoke_syscall | 테이블 함수를 호출하거나 미구현 경로로 가고 반환값을 저장한다. |
| 6 | exit_to_user_mode → ret_to_user | 사용자 복귀 작업과 문맥 복원 뒤 eret한다. |
구조체와 필드
호출 인자와 반환값은 저장된 pt_regs와 선택된 테이블을 통해 해석된다.
| 값 | 생산·소비 | 주의 |
|---|---|---|
regs->regs[8] | native 호출 번호. do_el0_svc가 읽는다. | 일반 C 함수의 여덟 번째 인자가 아니다. |
regs->regs[0] / orig_x0 | 진입 x0를 보관하고 결과도 복귀용 x0에 쓴다. | 입력과 반환 시점을 구분한다. |
regs->syscallno | 진입 기본값은 NO_SYSCALL, SVC에서 번호 갱신. | 모든 EL0 예외가 시스템 호출은 아니다. |
sys_call_table[] | native 범위 검사 뒤 핸들러 선택. | 범위 밖 번호는 테이블을 인덱싱하지 않는다. |
compat_sys_call_table[] | CONFIG_COMPAT의 AArch32 SVC는 x7과 __NR_compat_syscalls를 사용. | native ABI와 크기를 섞지 않는다. |
| thread flags | 진입 추적·사용자 복귀 작업의 조건. | 핸들러 반환이 즉시 eret은 아니다. |
실행 단계와 분기
실제 핸들러를 실행하지 않는 경우도 pt_regs와 사용자 복귀 절차를 통과할 수 있다.
| 조건 | 분기 | 확인할 결과 |
|---|---|---|
ESR이 SVC64가 아님 | 다른 EL0 예외 핸들러 | 페이지 폴트 등을 invoke_syscall로 그리지 않는다. |
| 비동기 MTE fault 플래그 | 재시작용 반환값을 설정하고 디스패치를 미룸 | 복귀 쪽 시그널 전달을 살핀다. |
| ptrace·seccomp 등 syscall-work | 번호 변경 또는 NO_SYSCALL 스킵 가능 | x0와 종료 추적을 함께 본다. |
| 번호가 테이블 범위 밖 | do_ni_syscall 처리 | 미구현 결과 또는 compat 처리 여부. |
| 핸들러가 반환함 | 필요 시 추적 종료, 복귀 작업, 레지스터 복원 | 즉시 eret한다고 가정하지 않는다. |
불변 조건과 실패 사례
SVC64 판별 전에 EL0 동기 예외 전체를 호출로 취급하면 데이터 중단 경로를 잘못 설명한다.
범위 검사 뒤 함수 포인터를 선택한다. 추적 경로에서는 호출 번호가 바뀌거나 스킵될 수 있다.
orig_x0와 사용자에게 반환할 x0를 구분한다. 저장된 문맥은 복귀 어셈블리까지 이어진다.
시그널·재스케줄·추적은 사용자 복귀 준비에 포함될 수 있다. 핸들러 뒤 바로 eret으로 건너뛰는 그림은 틀리다.
계측과 검증
tracepoint 가용성은 빌드 설정에 따라 다르다. sys_enter/sys_exit만으로 ESR 분기, 스킵 이유, 모든 복귀 작업을 식별할 수 없으므로 코드 조건과 함께 읽는다.
| 관찰 | 알 수 있는 것 | 한계 |
|---|---|---|
raw_syscalls:sys_enter | 관측된 호출 번호·인자 | 모든 EL0 예외를 나타내지 않는다. |
raw_syscalls:sys_exit | 관측된 반환값 | 최종 eret 시점은 아니다. |
| 코드 경로 대조 | 스킵·범위 밖 조건 | 정적 함수의 ftrace 가능 여부는 빌드·최적화에 좌우된다. |
ls /sys/kernel/tracing/events/raw_syscalls/
cat /sys/kernel/tracing/events/raw_syscalls/sys_enter/format
cat /sys/kernel/tracing/events/raw_syscalls/sys_exit/format추가 확인 사항
- 같은 EL0 동기 예외라도
SVC64와DABT_LOW는 어디서 갈라지는가? - 추적이
NO_SYSCALL로 실행을 건너뛰면 사용자 x0와 종료 추적은 어떻게 처리되는가? - 핸들러 반환과 최종
eret사이에 어떤 사용자 복귀 작업이 가능한가? - 두 버전의 FP 상태 처리와 스택 오프셋 선택은 어떻게 달라졌는가?