← Architecture 비교

Linux 6.18.37 LTS · Architecture comparison 11/21

System call entry: SVC, SYSCALL과 ECALL

user ABI register에서 syscall number와 argument를 꺼내 seccomp, ptrace, audit를 거쳐 handler를 호출하고 user mode로 돌아가는 경로를 비교합니다.

비교 대상
arm64 / x86-64 / RISC-V
실제 원본
파일 3개 · 원문 193줄
기준 태그
Linux v6.18.37
분석 축
상태 · 접근 순서 · 객체 수명 · 지연 시간

01 · QUESTION

무엇을 확인할 것인가

libc wrapper가 설정한 register는 언제 kernel pt_regs에 저장되고, 각 아키텍처는 syscall 번호를 어떻게 검사해 handler를 선택하며, restart와 signal은 return value를 어떻게 다시 씁니까?

entry가 user register를 pt_regs로 저장한다. arm64는 필요할 때 trace/entry work를 거쳐 table 범위를 확인하고, x86-64·RISC-V는 syscall_enter_from_user_mode() 뒤 arch dispatch에서 번호 범위를 확인한다. 실제 handler 반환 뒤 exit work가 signal, tracing, audit, reschedule을 처리한다.

user/kernel privilege, user address 접근, instrumentation 금지 entry 영역과 일반 C code 경계를 구분한다. seccomp가 argument를 관찰하는 시점에는 완전한 pt_regs가 있어야 한다.

지연 시간 관점최소 entry/exit instruction 비용에 KPTI, speculation mitigation, seccomp BPF, audit, ptrace, signal과 reschedule 비용이 선택적으로 더해진다.

02 · CONTRACT

공통 규약과 아키텍처별 구현

architecture핵심 mechanism실패 형태확인할 상태
arm64SVC #0, x8 syscall number와 x0-x5 argumentx8 범위 검사 누락, compat AArch32 table 혼동 또는 restart 시 original x0 손실은 엉뚱한 syscall과 argument로 이어진다.ESR.EC=SVC64, regs[0..8], syscallno, thread flags, seccomp result와 final x0를 추적한다.
x86-64SYSCALL, RAX number와 RDI/RSI/RDX/R10/R8/R9 argumentSYSRET의 사용자 RIP 상한·RCX/R11·CS/SS·RF/TF 조건을 놓치거나 R10을 RCX argument로 오인하면 return fault 또는 네 번째 argument corruption이 생긴다.MSR_LSTAR/STAR/FMASK, orig_ax, ax, ip, sp, cs/ss와 exit path가 SYSRET인지 IRET인지 확인한다.
RISC-VECALL from U-mode, a7 number와 a0-a5 argumentSEPC를 무조건 증가시킨 뒤 restart 보정을 놓치거나 compressed instruction처럼 2 bytes로 오인하면 잘못된 PC에서 복귀한다.SCAUSE=U_ECALL, SEPC, a0-a7, orig_a0, thread flags와 syscall table index를 본다.

03 · DIAGRAMS

세 그림으로 먼저 읽기

그림 1. 같은 기능을 구현하는 서로 다른 방식각 명령어 집합(ISA)에서 추적할 상태와 확인할 항목을 나란히 정리했습니다.

arm64

mechanism
SVC #0, x8 syscall number와 x0-x5 argument
state
SVC64 exception이 vector를 거쳐 el0_svc()와 invoke_syscall()로 들어간다. x8을 table index로 사용하고 x0에 return value를 쓰며 orig_x0와 syscallno를 pt_regs에 보존한다. native restart는 필요할 때 원래 x0·PC를 복원하고 restart syscall의 번호를 x8에 둔다.
checkpoint
ESR.EC=SVC64, regs[0..8], syscallno, thread flags, seccomp result와 final x0를 추적한다.

x86-64

mechanism
SYSCALL, RAX number와 RDI/RSI/RDX/R10/R8/R9 argument
state
hardware가 RCX에 user RIP, R11에 RFLAGS를 남기고 STAR/LSTAR 설정으로 kernel RIP/CS를 선택하지만 RSP는 자동 교체하지 않는다. entry assembly가 per-CPU 변수 cpu_current_top_of_stack에서 현재 task의 kernel stack 상단을 읽어 RSP를 바꾸고 pt_regs를 만든 뒤 do_syscall_64()에서 x64/x32 번호 검사와 switch 기반 handler dispatch가 이어진다.
checkpoint
MSR_LSTAR/STAR/FMASK, orig_ax, ax, ip, sp, cs/ss와 exit path가 SYSRET인지 IRET인지 확인한다.

RISC-V

mechanism
ECALL from U-mode, a7 number와 a0-a5 argument
state
ECALL exception은 SEPC가 trapping instruction을 가리키므로 return 전에 4 bytes를 더한다. do_trap_ecall_u()가 a7을 syscall number로 사용하고 a0에 반환값을 기록한다.
checkpoint
SCAUSE=U_ECALL, SEPC, a0-a7, orig_a0, thread flags와 syscall table index를 본다.
그림 2. 공통 커널 코드와 아키텍처별 함수의 역할공통 커널 코드와 아키텍처별 코드가 각각 담당하는 일을 구분했습니다.
리눅스 공통 규약entry가 user register를 pt_regs로 저장한다. arm64는 필요할 때 trace/entry work를 거쳐 table 범위를 확인하고, x86-64·RISC-V는 syscall_enter_from_user_mode() 뒤 arch dispatch에서 번호 범위를 확인한다. 실제 handler 반환 뒤 exit work가 signal, tracing, audit, reschedule을 처리한다.
arm64SVC #0, x8 syscall number와 x0-x5 argumentPAN과 user access 상태를 차단한 뒤 syscall work를 실행하고 exit-to-user ordering을 거쳐야 한다.
x86-64SYSCALL, RAX number와 RDI/RSI/RDX/R10/R8/R9 argumentSWAPGS, CR3/KPTI, stack switch와 speculation barrier가 일반 C 진입 전에 완료되어야 한다. SYSRET 가능 조건이 아니면 IRET로 돌아간다.
RISC-VECALL from U-mode, a7 number와 a0-a5 argumentSSTATUS.SPP/SUM, SEPC 증가와 syscall-enter work 순서를 지켜 restart가 필요할 때 원래 ECALL을 다시 실행할 수 있게 한다.
객체 수명 관리pt_regs와 syscall argument는 현재 task kernel stack에 있고 syscall이 끝날 때까지 유효하다. user pointer가 가리키는 memory는 별도 copy_from_user와 fault/revalidation 규칙을 따른다.
그림 3. syscall 진입 work와 dispatch의 실제 순서화살표는 정상 dispatch의 코드·상태 순서를 뜻하며 공통 하드웨어 메모리 배리어가 아닙니다. 재시작은 별도 exit-to-user 작업에서 오류값·signal 조건에 따라 처리됩니다.
arm64SVC64 vector가 EL0 register를 pt_regs에 저장→el0_svc_common(): orig_x0·syscallno 저장, thread flag에 따라 trace entry→invoke_syscall(): table 검사·handler→x0 결과. 호출자 el0_svc_common()이 조건에 따라 syscall_trace_exit() 실행. 이후 별도 arch_do_signal_or_restart()에서 재시작 결정
x86-64SYSCALL 진입에서 kernel stack·pt_regs 준비→syscall_enter_from_user_mode(): tracing·seccomp 등 entry work→x64/x32 번호 검사·handler→exit work; 조건에 따라 SYSRET 또는 IRET
RISC-VU-mode ECALL: epc += 4, orig_a0 보존→syscall_enter_from_user_mode(): entry work→번호 검사·handler→exit work. 별도 signal/restart 작업에서 필요 시 a0 복원·epc 되감기

04 · SOURCE

Linux 6.18.37 원본 코드와 주요 구문 해설

소스 위치를 고정된 숫자로 복사하지 않고 Linux v6.18.37 tree에서 함수 선언을 다시 찾아 발췌했습니다. 원문 전체 발췌를 보존하고, 개별 해설은 근거가 있는 구문에만 붙였습니다.

arm64 · Linux 6.18.37

SVC #0, x8 syscall number와 x0-x5 argument

SVC64 exception이 vector를 거쳐 el0_svc()와 invoke_syscall()로 들어간다. x8을 table index로 사용하고 x0에 return value를 쓰며 orig_x0와 syscallno를 pt_regs에 보존한다. native restart는 필요할 때 원래 x0·PC를 복원하고 restart syscall의 번호를 x8에 둔다.

원본 코드: arch/arm64/kernel/syscall.c:30-92

30	return sys_ni_syscall();
31}
32 
33static long __invoke_syscall(struct pt_regs *regs, syscall_fn_t syscall_fn)
34{
35	return syscall_fn(regs);
36}
37 
38static void invoke_syscall(struct pt_regs *regs, unsigned int scno,
39			   unsigned int sc_nr,
40			   const syscall_fn_t syscall_table[])
41{
42	long ret;
43 
44	add_random_kstack_offset();
45 
46	if (likely(scno < sc_nr)) {
47		syscall_fn_t syscall_fn;
48		syscall_fn = syscall_table[array_index_nospec(scno, sc_nr)];
49		ret = __invoke_syscall(regs, syscall_fn);
50	} else {
51		ret = do_ni_syscall(regs, scno);
52	}
53 
54	syscall_set_return_value(current, regs, 0, ret);
55 
56	/*
57	 * This value will get limited by KSTACK_OFFSET_MAX(), which is 10
58	 * bits. The actual entropy will be further reduced by the compiler
59	 * when applying stack alignment constraints: the AAPCS mandates a
60	 * 16-byte aligned SP at function boundaries, which will remove the
61	 * 4 low bits from any entropy chosen here.
62	 *
63	 * The resulting 6 bits of entropy is seen in SP[9:4].
64	 */
65	choose_random_kstack_offset(get_random_u16());
66}
67 
68static inline bool has_syscall_work(unsigned long flags)
69{
70	return unlikely(flags & _TIF_SYSCALL_WORK);
71}
72 
73static void el0_svc_common(struct pt_regs *regs, int scno, int sc_nr,
74			   const syscall_fn_t syscall_table[])
75{
76	unsigned long flags = read_thread_flags();
77 
78	regs->orig_x0 = regs->regs[0];
79	regs->syscallno = scno;
80 
81	/*
82	 * BTI note:
83	 * The architecture does not guarantee that SPSR.BTYPE is zero
84	 * on taking an SVC, so we could return to userspace with a
85	 * non-zero BTYPE after the syscall.
86	 *
87	 * This shouldn't matter except when userspace is explicitly
88	 * doing something stupid, such as setting PROT_BTI on a page
89	 * that lacks conforming BTI/PACIxSP instructions, falling
90	 * through from one executable page to another with differing
91	 * PROT_BTI, or messing with BTYPE via ptrace: in such cases,
92	 * userspace should not be surprised if a SIGILL occurs on

주요 구문 해설

원본 63줄은 그대로 표시하며, 검토한 23개 구문에 설명을 붙였습니다.

L30 return sys_ni_syscall();

호환 전용 시스템 콜까지 처리할 수 없는 번호이므로 공통 미구현 결과 -ENOSYS를 반환합니다.

L33static long __invoke_syscall(struct pt_regs *regs, syscall_fn_t syscall_fn)

선택된 시스템 콜 함수 포인터를 호출하는 작은 래퍼입니다. 레지스터 묶음을 인자로 넘기고 long 결과를 그대로 돌려줍니다.

L35 return syscall_fn(regs);

레지스터 저장 영역에서 인자를 해석하는 시스템 콜 래퍼를 실행합니다. 반환된 성공값 또는 음수 오류는 상위 invoke_syscall이 사용자 결과 레지스터에 기록합니다.

L38static void invoke_syscall(struct pt_regs *regs, unsigned int scno,

시스템 콜 번호 scno를 검사하고 해당 처리기를 호출하는 함수입니다. 스택 무작위화와 반환 레지스터 기록도 이 범위에서 수행합니다.

L39 unsigned int sc_nr,

이 ABI의 시스템 콜 테이블 항목 수를 sc_nr로 받습니다. 허용되는 번호는 0부터 sc_nr-1까지입니다.

L40 const syscall_fn_t syscall_table[])

현재 사용자 ABI에 맞는 시스템 콜 함수 포인터 배열을 받습니다. 번호로 고른 항목에 저장 레지스터를 전달합니다.

L42 long ret;

시스템 호출 처리 함수의 결과를 담을 long 변수 ret를 선언합니다. 처리 함수가 돌려준 정상 결과 또는 음수 오류를 나중에 사용자 반환 레지스터에 넣기 위한 값입니다.

L44 add_random_kstack_offset();

시스템 콜 처리 중 사용할 커널 스택 위치에 미리 정한 무작위 오프셋을 반영합니다. 같은 진입점에서도 스택상의 자료 위치를 예측하기 어렵게 하며, 해당 무작위화 기능을 켠 구성에서 효과가 있습니다.

L46 if (likely(scno < sc_nr)) {

부호 없는 scno가 테이블 크기보다 작은지 검사합니다. 유효한 번호면 테이블을 사용하고 범위 밖이면 미구현·호환 전용 처리로 보냅니다.

L47 syscall_fn_t syscall_fn;

테이블에서 선택할 실제 시스템 콜 처리기 포인터입니다. regs를 받아 long 결과를 돌려주는 동일한 함수 형식을 사용합니다.

L48 syscall_fn = syscall_table[array_index_nospec(scno, sc_nr)];

speculative out-of-bounds 접근을 막으며 handler pointer를 선택한다.

L49 ret = __invoke_syscall(regs, syscall_fn);

검증된 시스템 호출 번호로 선택한 syscall_fn에 저장 레지스터 regs를 전달하여 호출합니다. 해당 함수가 인자를 해석하고 수행한 결과를 ret에 받습니다.

L50 } else {

번호가 일반 테이블 범위를 벗어난 경우입니다. 호환 태스크의 ARM 전용 번호를 확인한 뒤 처리할 수 없으면 -ENOSYS를 돌려줍니다.

L51 ret = do_ni_syscall(regs, scno);

번호가 일반 시스템 호출 표 범위 밖이면 미구현 호출 처리로 넘깁니다. 32비트 호환 태스크의 ARM 전용 호출을 먼저 시도하고, 지원하지 않으면 -ENOSYS를 반환합니다.

L54 syscall_set_return_value(current, regs, 0, ret);

ret를 사용자에게 돌려줄 저장 레지스터 x0에 기록합니다. 별도 error 인자는 0이므로 ret를 그대로 사용하고, 32비트 호환 태스크이면 하위 32비트로 제한합니다. 실제 사용자 모드 복귀는 이후 진입·복귀 코드가 수행합니다.

L65 choose_random_kstack_offset(get_random_u16());

16비트 난수로 다음 시스템 콜에 사용할 스택 오프셋을 갱신합니다. 실제 오프셋은 최대 10비트로 제한되고 16바이트 정렬로 하위 4비트가 사라져, 이 경로에서는 SP[9:4]의 6비트가 변합니다.

L68static inline bool has_syscall_work(unsigned long flags)

현재 thread flags에 시스템 콜 진입·종료 시 추가 작업이 있는지 판단하는 함수입니다.

L70 return unlikely(flags & _TIF_SYSCALL_WORK);

추적·감사 등 _TIF_SYSCALL_WORK에 해당하는 비트가 하나라도 있으면 참입니다. unlikely는 드문 경로라는 최적화 힌트이며 검사 결과를 바꾸지 않습니다.

L73static void el0_svc_common(struct pt_regs *regs, int scno, int sc_nr,

EL0의 SVC 요청을 공통으로 처리합니다. 저장 레지스터와 사용자 ABI의 번호·테이블 크기를 받아 추적 및 디스패치 흐름을 연결합니다.

L74 const syscall_fn_t syscall_table[])

호출한 ABI에 맞는 시스템 콜 테이블을 전달받습니다. 공통 진입 코드가 AArch64와 호환 ABI의 서로 다른 함수 배열을 재사용하도록 합니다.

L76 unsigned long flags = read_thread_flags();

현재 태스크의 thread flags를 읽어 진입 시 필요한 추가 처리를 판단할 자료로 보관합니다. 아래에서 MTE 비동기 오류와 시스템 콜 추적·감사 같은 작업의 존재 여부를 검사할 때 사용합니다.

L78 regs->orig_x0 = regs->regs[0];

첫 번째 인자였던 원래 x0를 별도 필드에 보존합니다. x0가 반환값으로 덮여도 추적이나 시스템 콜 재시작에서 최초 인자를 찾을 수 있습니다.

L79 regs->syscallno = scno;

들어온 시스템 콜 번호를 regs에 남깁니다. 추적·신호·재시작 코드가 이 예외가 어떤 시스템 콜 문맥인지 식별할 때 사용합니다.

x86-64 · Linux 6.18.37

SYSCALL, RAX number와 RDI/RSI/RDX/R10/R8/R9 argument

hardware가 RCX에 user RIP, R11에 RFLAGS를 남기고 STAR/LSTAR 설정으로 kernel RIP/CS를 선택하지만 RSP는 자동 교체하지 않는다. entry assembly가 per-CPU 변수 cpu_current_top_of_stack에서 현재 task의 kernel stack 상단을 읽어 RSP를 바꾸고 pt_regs를 만든 뒤 do_syscall_64()에서 x64/x32 번호 검사와 switch 기반 handler dispatch가 이어진다. sys_call_table[]은 syscall 호출에는 쓰이지 않고 tracing이 syscall 주소를 찾을 때 사용된다.

dispatch 근거: arch/x86/entry/syscall_64.c:23-84 · 원본 코드: arch/x86/entry/syscall_64.c:79-141

79		xnr = array_index_nospec(xnr, X32_NR_syscalls);
80		regs->ax = x32_sys_call(regs, xnr);
81		return true;
82	}
83	return false;
84}
85 
86/* Returns true to return using SYSRET, or false to use IRET */
87__visible noinstr bool do_syscall_64(struct pt_regs *regs, int nr)
88{
89	add_random_kstack_offset();
90	nr = syscall_enter_from_user_mode(regs, nr);
91 
92	instrumentation_begin();
93 
94	if (!do_syscall_x64(regs, nr) && !do_syscall_x32(regs, nr) && nr != -1) {
95		/* Invalid system call, but still a system call. */
96		regs->ax = __x64_sys_ni_syscall(regs);
97	}
98 
99	instrumentation_end();
100	syscall_exit_to_user_mode(regs);
101 
102	/*
103	 * Check that the register state is valid for using SYSRET to exit
104	 * to userspace.  Otherwise use the slower but fully capable IRET
105	 * exit path.
106	 */
107 
108	/* XEN PV guests always use the IRET path */
109	if (cpu_feature_enabled(X86_FEATURE_XENPV))
110		return false;
111 
112	/* SYSRET requires RCX == RIP and R11 == EFLAGS */
113	if (unlikely(regs->cx != regs->ip || regs->r11 != regs->flags))
114		return false;
115 
116	/* CS and SS must match the values set in MSR_STAR */
117	if (unlikely(regs->cs != __USER_CS || regs->ss != __USER_DS))
118		return false;
119 
120	/*
121	 * On Intel CPUs, SYSRET with non-canonical RCX/RIP will #GP
122	 * in kernel space.  This essentially lets the user take over
123	 * the kernel, since userspace controls RSP.
124	 *
125	 * TASK_SIZE_MAX covers all user-accessible addresses other than
126	 * the deprecated vsyscall page.
127	 */
128	if (unlikely(regs->ip >= TASK_SIZE_MAX))
129		return false;
130 
131	/*
132	 * SYSRET cannot restore RF.  It can restore TF, but unlike IRET,
133	 * restoring TF results in a trap from userspace immediately after
134	 * SYSRET.
135	 */
136	if (unlikely(regs->flags & (X86_EFLAGS_RF | X86_EFLAGS_TF)))
137		return false;
138 
139	/* Use SYSRET to exit to userspace */
140	return true;
141}

주요 구문 해설

원본 63줄은 그대로 표시하며, 검토한 23개 구문에 설명을 붙였습니다.

L79 xnr = array_index_nospec(xnr, X32_NR_syscalls);

범위 검사를 통과한 x32 시스템 콜 번호를 추측 실행에도 안전한 인덱스로 제한합니다. CPU가 분기 결과를 잘못 예측하더라도 범위 밖 번호가 디스패치에 그대로 쓰이지 않도록 합니다.

L80 regs->ax = x32_sys_call(regs, xnr);

x32 ABI의 시스템 콜 디스패처에 레지스터 저장 영역과 보정된 번호를 넘깁니다. 반환값은 regs->ax에 기록하여 사용자 모드로 돌아갈 때 RAX 결과가 되게 합니다.

L81 return true;

x32_sys_call()로 x32 번호의 호출을 처리했음을 참으로 반환합니다. 시스템 콜 자체의 결과는 이미 regs->ax에 기록되어 있어 이 bool과 구분됩니다.

L83 return false;

x32 ABI가 꺼져 있거나 번호가 범위 밖이라 이 디스패처가 처리하지 않았음을 반환합니다.

L87__visible noinstr bool do_syscall_64(struct pt_regs *regs, int nr)

64비트 시스템 콜 진입·종료를 처리하고 복귀 명령 선택을 반환합니다. true는 SYSRET, false는 IRET이며 noinstr는 민감한 진입 구간의 자동 계측을 제한합니다.

L89 add_random_kstack_offset();

설정이 활성화된 경우 이번 시스템 콜의 커널 스택 사용 위치를 무작위 오프셋만큼 바꿉니다. 공격자가 시스템 콜 내부의 스택 배치를 일정한 위치로 예측하기 어렵게 하는 진입 처리입니다.

L90 nr = syscall_enter_from_user_mode(regs, nr);

ptrace, seccomp, audit 등 syscall-entry work를 수행하고 skip 또는 변경된 syscall number를 돌려준다.

L92 instrumentation_begin();

noinstr 진입 함수 안에서 계측이 허용되는 구간의 시작을 objtool에 표시합니다. 사용자 모드 이탈 처리가 끝난 뒤 일반 시스템 콜 함수를 호출해도 되는 경계이며, 추적 장치를 켜는 실행 함수가 아닙니다.

L94 if (!do_syscall_x64(regs, nr) && !do_syscall_x32(regs, nr) && nr != -1) {

native x64와 x32에서 모두 처리하지 못했고 의도적 생략 번호 -1도 아닌지 검사합니다. 그때만 미구현 결과를 만들어, 추적기가 생략시킨 호출의 결과를 덮지 않습니다.

L96 regs->ax = __x64_sys_ni_syscall(regs);

x64와 x32 어느 쪽에도 없는 시스템 콜 번호에 대해 미구현 처리기를 실행하고 -ENOSYS 결과를 regs->ax에 넣습니다. nr == -1인 실행 생략 요청은 위 조건에서 제외됩니다.

L99 instrumentation_end();

일반 시스템 콜 디스패치 구간의 계측 허용 표시를 끝냅니다. 이 뒤에는 사용자 복귀 처리를 하는 noinstr 문맥으로 넘어가므로, 검증기가 그 경계를 구분할 수 있게 합니다.

L100 syscall_exit_to_user_mode(regs);

signal, reschedule, audit와 user return 준비를 반복 처리한다.

L109 if (cpu_feature_enabled(X86_FEATURE_XENPV))

Xen PV 게스트인지 확인합니다. 이 환경은 아래의 일반 SYSRET 조건과 별개로 IRET 복귀 경로를 사용합니다.

L110 return false;

Xen PV에서 IRET로 돌아가도록 false를 반환합니다. 시스템 콜 처리 실패를 뜻하는 값은 아닙니다.

L113 if (unlikely(regs->cx != regs->ip || regs->r11 != regs->flags))

SYSRET이 사용할 RCX·R11 값이 저장된 RIP·RFLAGS와 같은지 검사합니다. 다르면 SYSRET만으로 원하는 레지스터 상태를 복원할 수 없습니다.

L114 return false;

RCX/RIP 또는 R11/RFLAGS가 다르므로 모든 저장 상태를 복원할 수 있는 IRET를 선택합니다.

L117 if (unlikely(regs->cs != __USER_CS || regs->ss != __USER_DS))

복귀 CS·SS가 MSR_STAR로 설정한 표준 사용자 세그먼트와 같은지 검사합니다. SYSRET은 임의의 세그먼트 선택자를 복원할 수 없습니다.

L118 return false;

CS·SS가 SYSRET 규약과 맞지 않으므로 IRET로 복귀하도록 알립니다.

L128 if (unlikely(regs->ip >= TASK_SIZE_MAX))

복귀 IP가 허용되는 일반 사용자 주소 상한 이상인지 검사합니다. 비정규 주소에 대한 SYSRET의 커널 모드 예외를 피하려는 검사입니다.

L129 return false;

SYSRET에 안전한 사용자 IP 범위가 아니므로 IRET 경로를 선택합니다.

L136 if (unlikely(regs->flags & (X86_EFLAGS_RF | X86_EFLAGS_TF)))

Resume Flag 또는 Trap Flag가 설정됐는지 검사합니다. SYSRET은 RF를 복원하지 못하고 TF의 예외 시점도 IRET와 달라 이 상태에는 부적합합니다.

L137 return false;

디버그 플래그의 복귀 의미를 지키기 위해 IRET를 선택합니다.

L140 return true;

모든 SYSRET 사용 조건이 맞으므로 빠른 SYSRET 복귀를 선택합니다. 시스템 콜의 성공값은 별도로 regs->ax에 들어 있습니다.

RISC-V · Linux 6.18.37

ECALL from U-mode, a7 number와 a0-a5 argument

ECALL exception은 SEPC가 trapping instruction을 가리키므로 return 전에 4 bytes를 더한다. do_trap_ecall_u()가 a7을 syscall number로 사용하고 a0에 반환값을 기록한다.

원본 코드: arch/riscv/kernel/traps.c:319-385

319 
320		handle_break(regs);
321 
322		irqentry_nmi_exit(regs, state);
323	}
324}
325 
326asmlinkage __visible __trap_section  __no_stack_protector
327void do_trap_ecall_u(struct pt_regs *regs)
328{
329	if (user_mode(regs)) {
330		long syscall = regs->a7;
331 
332		regs->epc += 4;
333		regs->orig_a0 = regs->a0;
334		regs->a0 = -ENOSYS;
335 
336		riscv_v_vstate_discard(regs);
337 
338		syscall = syscall_enter_from_user_mode(regs, syscall);
339 
340		add_random_kstack_offset();
341 
342		if (syscall >= 0 && syscall < NR_syscalls) {
343			syscall = array_index_nospec(syscall, NR_syscalls);
344			syscall_handler(regs, syscall);
345		}
346 
347		/*
348		 * Ultimately, this value will get limited by KSTACK_OFFSET_MAX(),
349		 * so the maximum stack offset is 1k bytes (10 bits).
350		 *
351		 * The actual entropy will be further reduced by the compiler when
352		 * applying stack alignment constraints: 16-byte (i.e. 4-bit) aligned
353		 * for RV32I or RV64I.
354		 *
355		 * The resulting 6 bits of entropy is seen in SP[9:4].
356		 */
357		choose_random_kstack_offset(get_random_u16());
358 
359		syscall_exit_to_user_mode(regs);
360	} else {
361		irqentry_state_t state = irqentry_nmi_enter(regs);
362 
363		do_trap_error(regs, SIGILL, ILL_ILLTRP, regs->epc,
364			"Oops - environment call from U-mode");
365 
366		irqentry_nmi_exit(regs, state);
367	}
368 
369}
370 
371#ifdef CONFIG_MMU
372asmlinkage __visible noinstr void do_page_fault(struct pt_regs *regs)
373{
374	irqentry_state_t state = irqentry_enter(regs);
375 
376	handle_page_fault(regs);
377 
378	local_irq_disable();
379 
380	irqentry_exit(regs, state);
381}
382#endif
383 
384static void noinstr handle_riscv_irq(struct pt_regs *regs)
385{

주요 구문 해설

원본 67줄은 그대로 표시하며, 검토한 30개 구문에 설명을 붙였습니다.

L320 handle_break(regs);

커널 모드에서 발생한 breakpoint를 분류·처리합니다. kprobe나 디버거가 처리할 수 있는 중단점인지 확인하고, WARN·BUG 등 커널 중단 명령의 의미에 맞춰 다음 동작을 결정합니다.

L322 irqentry_nmi_exit(regs, state);

커널 breakpoint를 처리하기 전에 시작한 NMI 방식의 예외 문맥을 종료합니다. 진입 때 보관한 state를 사용해 RCU·추적 상태를 원래 문맥에 맞게 되돌립니다.

L326asmlinkage __visible __trap_section __no_stack_protector

어셈블리 진입 코드에서 참조하는 예외 함수임을 표시하고 trap 코드 섹션에 놓습니다. __no_stack_protector는 이 함수에 자동 스택 canary 검사를 삽입하지 않게 합니다.

L327void do_trap_ecall_u(struct pt_regs *regs)

사용자 ECALL 예외로 들어온 저장 레지스터를 받아 시스템 콜을 처리하는 함수입니다.

L329 if (user_mode(regs)) {

저장된 권한 상태가 사용자 모드인지 확인합니다. 정상 ECALL은 디스패치하고, 커널 모드로 표시된 비정상 진입은 오류 경로로 처리합니다.

L330 long syscall = regs->a7;

RISC-V 시스템 콜 규약에 따라 a7에서 호출 번호를 읽습니다. 진입 추적 처리에서 이 번호가 바뀌거나 생략될 수 있어 별도 변수로 사용합니다.

L332 regs->epc += 4;

ECALL은 항상 32-bit instruction이므로 정상 return이 다음 instruction으로 가도록 SEPC를 이동한다.

L333 regs->orig_a0 = regs->a0;

첫 인자 a0의 원래 값을 보존합니다. a0는 곧 결과 레지스터로 쓰이므로 재시작·추적에 필요한 입력을 따로 남깁니다.

L334 regs->a0 = -ENOSYS;

처리할 번호가 없을 때의 기본 결과를 -ENOSYS로 둡니다. 유효한 시스템 콜을 실행하면 그 처리 결과로 덮어씁니다.

L336 riscv_v_vstate_discard(regs);

시스템 콜을 건너 유지할 필요가 없는 사용자 벡터 상태를 폐기하고 변경 상태로 표시합니다. 벡터를 사용 중이었다면 실제 벡터 레지스터를 정리하며, 원래 값을 보존하는 저장 함수로 해석하면 안 됩니다.

L338 syscall = syscall_enter_from_user_mode(regs, syscall);

공통 ptrace/seccomp/audit entry work가 syscall number를 유지, 변경 또는 취소할 수 있다.

L340 add_random_kstack_offset();

활성화된 커널 스택 무작위화 설정에 따라 이번 시스템 콜의 스택 사용 위치를 이동합니다. 번호 검사와 실제 호출 전에 적용하여 처리 함수 내부의 스택 위치가 고정되지 않게 합니다.

L342 if (syscall >= 0 && syscall < NR_syscalls) {

추적 처리 후 번호가 음수가 아니고 테이블 크기 미만인지 검사합니다. 음수 생략 요청이나 범위 밖 번호는 실제 처리기를 호출하지 않습니다.

L343 syscall = array_index_nospec(syscall, NR_syscalls);

0 이상 NR_syscalls 미만이라는 검사와 함께 추측 실행에서도 유효한 인덱스만 사용하도록 번호를 제한합니다. 분기 오예측으로 시스템 콜 테이블 밖을 참조하는 문제를 막는 보완입니다.

L344 syscall_handler(regs, syscall);

선택한 시스템 콜 번호와 저장 레지스터를 실제 처리기에 전달합니다. CONFIG_COMPAT이면 사용자 레지스터 폭에 맞는 테이블을 고르고, 처리 결과를 regs->a0에 기록합니다.

L357 choose_random_kstack_offset(get_random_u16());

다음 진입에 사용할 커널 스택 오프셋을 새 난수로 갱신합니다. 최대 10비트 범위와 16바이트 스택 정렬을 적용하므로 실제 위치 변화에 쓰이는 비트는 SP[9:4]입니다.

L359 syscall_exit_to_user_mode(regs);

시스템 콜 종료 추적과 사용자 복귀 전에 필요한 작업을 처리합니다. 미처리 신호·재스케줄 요청 등의 상태를 확인하고, 사용자 문맥으로 돌아갈 준비를 마칩니다.

L360 } else {

사용자 ECALL 처리기에 커널 모드 상태가 들어온 비정상 경로입니다. 일반 시스템 콜처럼 실행하지 않고 NMI 방식의 오류 문맥으로 처리합니다.

L361 irqentry_state_t state = irqentry_nmi_enter(regs);

사용자 ECALL 처리 경로에 커널 모드 레지스터가 들어온 비정상 상황을 NMI 방식의 예외 문맥으로 감쌉니다. 이어지는 오류 보고 중 RCU·추적 상태가 올바르게 유지되도록 진입 상태를 보관합니다.

L363 do_trap_error(regs, SIGILL, ILL_ILLTRP, regs->epc,

잘못된 ECALL 예외를 SIGILL·ILL_ILLTRP 유형과 발생 PC로 보고합니다. 커널 문맥이라면 오류 복구 또는 Oops 처리 여부를 공통 예외 코드가 판단합니다.

L364 "Oops - environment call from U-mode");

앞의 오류 보고에 붙일 진단 문자열입니다. 보고 대상은 사용자 ECALL 예외인데 저장 상태가 사용자 모드가 아닌 불일치입니다.

L366 irqentry_nmi_exit(regs, state);

잘못된 모드의 ECALL에 대한 오류 처리를 마친 뒤 NMI 방식으로 시작한 예외 문맥을 종료합니다. state를 넘겨 진입 전 문맥에 맞는 상태로 되돌립니다.

L371#ifdef CONFIG_MMU

MMU를 사용하는 빌드에만 실제 페이지 오류 처리 함수를 포함합니다. 예외 진입 상태를 관리하며 handle_page_fault로 주소 공간·페이지 표 처리를 넘기는 경로이고, MMU가 없으면 entry.S에서 이 예외를 unknown trap으로 연결합니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.

L372asmlinkage __visible noinstr void do_page_fault(struct pt_regs *regs)

MMU 페이지 폴트의 C 진입점입니다. noinstr 구간에서 공통 예외 진입·종료를 감싸고 실제 폴트 처리를 handle_page_fault에 맡깁니다.

L374 irqentry_state_t state = irqentry_enter(regs);

페이지 폴트 처리에 들어가기 전에 사용자·커널 기원을 반영하여 공통 예외 진입 상태를 설정합니다. 반환된 state는 폴트 처리를 마칠 때 어떤 상태로 복구할지 기록합니다.

L376 handle_page_fault(regs);

fault 주소와 저장된 레지스터를 바탕으로 페이지 폴트 원인을 처리합니다. 정상적인 페이지 할당·매핑 보완이 가능한지 확인하고, 접근 권한 위반이나 복구 불가능한 커널 접근은 오류 경로로 보냅니다.

L378 local_irq_disable();

페이지 폴트 처리 중 켜졌을 수 있는 로컬 인터럽트를 다시 막습니다. 공통 예외 종료 함수가 IRQ가 꺼진 상태에서 복귀 작업을 수행하도록 맞추는 단계입니다.

L380 irqentry_exit(regs, state);

페이지 폴트 진입 때 저장한 state를 사용해 공통 예외 종료 처리를 합니다. 사용자 모드로 돌아갈 경우 필요한 복귀 작업을 처리하고, 커널에서 발생한 경우에는 해당 진입 문맥을 복원합니다.

L382#endif

371행에서 시작한 선택 구간을 마칩니다. MMU를 사용하는 빌드에만 실제 페이지 오류 처리 함수를 포함합니다. 예외 진입 상태를 관리하며 handle_page_fault로 주소 공간·페이지 표 처리를 넘기는 경로이고, MMU가 없으면 entry.S에서 이 예외를 unknown trap으로 연결합니다.

L384static void noinstr handle_riscv_irq(struct pt_regs *regs)

하드웨어 IRQ의 본 처리를 감싸는 내부 함수입니다. IRQ 문맥 계산을 시작하고 현재 레지스터 포인터를 바꾼 뒤 아키텍처 IRQ 처리기를 호출하여 원래 상태로 복구합니다.

05 · WORKED EXAMPLE

숫자로 검산하기

01

write(fd, buf, 12) register와 return 흐름

64-bit native ABI에서 fd=1, buf=0x7fff1000, count=12인 호출을 가정한다.

  1. user ABIarm64는 x8=__NR_write, x0=1, x1=buf, x2=12. x86-64는 rax, rdi, rsi, rdx. RISC-V는 a7, a0, a1, a2를 사용한다.
  2. entry각 exception instruction이 privilege를 바꾸고 kernel stack의 pt_regs에 원래 argument를 고정한다.
  3. dispatchentry work 뒤 아키텍처별 dispatch가 선택한 handler가 user buffer 12 bytes를 검증하고 file descriptor 1의 file operation으로 보낸다.
  4. return성공 시 12, 실패 시 음수 errno가 x0/rax/a0에 들어가고 libc가 -1과 errno로 변환할 수 있다.

결론같은 C prototype이라도 네 번째 이후 argument register와 trap PC 규칙은 architecture ABI에서 확인해야 한다.

06 · DEEP DIVE

주요 동작 상세 분석

01

공통 커널 코드와 아키텍처별 함수의 역할 구분

entry가 user register를 pt_regs로 저장한다. arm64는 필요할 때 trace/entry work를 거쳐 table 범위를 확인하고, x86-64·RISC-V는 syscall_enter_from_user_mode() 뒤 arch dispatch에서 번호 범위를 확인한다. 실제 handler 반환 뒤 exit work가 signal, tracing, audit, reschedule을 처리한다.

user/kernel privilege, user address 접근, instrumentation 금지 entry 영역과 일반 C code 경계를 구분한다. seccomp가 argument를 관찰하는 시점에는 완전한 pt_regs가 있어야 한다.

02

arm64: SVC #0, x8 syscall number와 x0-x5 argument

SVC64 exception이 vector를 거쳐 el0_svc()와 invoke_syscall()로 들어간다. x8을 table index로 사용하고 x0에 return value를 쓰며 orig_x0와 syscallno를 pt_regs에 보존한다. native restart는 필요할 때 원래 x0·PC를 복원하고 restart syscall의 번호를 x8에 둔다.

PAN과 user access 상태를 차단한 뒤 syscall work를 실행하고 exit-to-user ordering을 거쳐야 한다. 디버깅할 때는 ESR.EC=SVC64, regs[0..8], syscallno, thread flags, seccomp result와 final x0를 추적한다.

03

x86-64: SYSCALL, RAX number와 RDI/RSI/RDX/R10/R8/R9 argument

hardware가 RCX에 user RIP, R11에 RFLAGS를 남기고 STAR/LSTAR 설정으로 kernel RIP/CS를 선택하지만 RSP는 자동 교체하지 않는다. entry assembly가 per-CPU 변수 cpu_current_top_of_stack에서 현재 task의 kernel stack 상단을 읽어 RSP를 바꾸고 pt_regs를 만든 뒤 do_syscall_64()에서 x64/x32 번호 검사와 switch 기반 handler dispatch가 이어진다.

SWAPGS, CR3/KPTI, stack switch와 speculation barrier가 일반 C 진입 전에 완료되어야 한다. SYSRET 가능 조건이 아니면 IRET로 돌아간다. 디버깅할 때는 MSR_LSTAR/STAR/FMASK, orig_ax, ax, ip, sp, cs/ss와 exit path가 SYSRET인지 IRET인지 확인한다.

04

RISC-V: ECALL from U-mode, a7 number와 a0-a5 argument

ECALL exception은 SEPC가 trapping instruction을 가리키므로 return 전에 4 bytes를 더한다. do_trap_ecall_u()가 a7을 syscall number로 사용하고 a0에 반환값을 기록한다.

SSTATUS.SPP/SUM, SEPC 증가와 syscall-enter work 순서를 지켜 restart가 필요할 때 원래 ECALL을 다시 실행할 수 있게 한다. 디버깅할 때는 SCAUSE=U_ECALL, SEPC, a0-a7, orig_a0, thread flags와 syscall table index를 본다.

05

객체를 사용하고 해제하는 주체와 시점을 확인한다

pt_regs와 syscall argument는 현재 task kernel stack에 있고 syscall이 끝날 때까지 유효하다. user pointer가 가리키는 memory는 별도 copy_from_user와 fault/revalidation 규칙을 따른다.

주소나 레지스터 값만 확인하면 갱신되지 않은 상태를 놓칠 수 있다. 상태를 만드는 코드, 다른 코드에 전달하는 시점, 사용하는 코드, 해제하는 시점을 함께 기록한다.

06

최대 지연 시간은 명령 하나의 실행 시간만으로 결정되지 않는다

최소 entry/exit instruction 비용에 KPTI, speculation mitigation, seccomp BPF, audit, ptrace, signal과 reschedule 비용이 선택적으로 더해진다.

평균값뿐 아니라 인터럽트를 비활성화한 시간, 다른 CPU의 응답 시간, 펌웨어 호출 시간, 재시도 횟수를 따로 확인해야 최대 지연의 원인을 찾을 수 있다.

07 · FAILURE

실패를 어떤 증거로 나눌 것인가

분류관찰되는 결과첫 확인값
arm64x8 범위 검사 누락, compat AArch32 table 혼동 또는 restart 시 original x0 손실은 엉뚱한 syscall과 argument로 이어진다.ESR.EC=SVC64, regs[0..8], syscallno, thread flags, seccomp result와 final x0를 추적한다.
x86-64SYSRET의 사용자 RIP 상한·RCX/R11·CS/SS·RF/TF 조건을 놓치거나 R10을 RCX argument로 오인하면 return fault 또는 네 번째 argument corruption이 생긴다.MSR_LSTAR/STAR/FMASK, orig_ax, ax, ip, sp, cs/ss와 exit path가 SYSRET인지 IRET인지 확인한다.
RISC-VSEPC를 무조건 증가시킨 뒤 restart 보정을 놓치거나 compressed instruction처럼 2 bytes로 오인하면 잘못된 PC에서 복귀한다.SCAUSE=U_ECALL, SEPC, a0-a7, orig_a0, thread flags와 syscall table index를 본다.

08 · LAB

재현과 계측 절차

  1. 직접 assembly syscall wrapper를 작성해 모든 argument register와 pt_regs를 kprobe로 대조한다.
  2. seccomp, ptrace, pending signal을 하나씩 켜 entry/exit latency와 return path 변화를 측정한다.
  3. 동일한 workload에서 세 architecture의 tracepoint 이름, CPU 번호, PC, stack pointer와 address-space identifier를 같은 열로 기록한다.
  4. 소스만 읽고 끝내지 않고 최종 vmlinux의 objdump -dr, readelf -SW 결과로 선택된 alternative와 section 배치를 확인한다.

09 · REFERENCES

원문 위치

Linux kernel source: GPL-2.0-only. 이 글의 코드 발췌는 Linux v6.18.37 원문을 기준으로 하며, 분석 문장은 해당 코드의 실행 조건과 상태 경계를 설명합니다.