이 코드는 어떤 문제를 푸나요?
사용자 함수가 커널 함수를 일반 call로 직접 호출하는 것은 아닙니다. arm64의 SVC, x86의 SYSCALL, RISC-V의 ECALL이 권한 경계를 넘긴 뒤 번호를 검사해 서비스 함수를 고릅니다. 아래는 arm64 시스템 호출 번호를 테이블에 연결하고 반환값을 기록하는 함수입니다.
읽을 범위: v6.18.37 · arch/arm64/kernel/syscall.c · invoke_syscall 38–66행입니다. 아래에 이 범위의 원문과 각 줄의 설명을 실었습니다. 주제 전체의 흐름과 다른 경로는 기존 분석에서 함께 읽으실 수 있습니다.
먼저 알아둘 개념
시스템 호출 번호
arm64는 x8, x86-64는 rax, RISC-V는 a7을 사용하는 ABI를 구분해야 합니다. C 함수 인수 번호와 같은 개념이 아닙니다.
array_index_nospec
범위 검사 이후 추측 실행이 부적절한 인덱스로 테이블을 접근하지 않도록 인덱스를 제한하는 도구입니다.
반환 레지스터
커널 C 함수가 얻은 결과를 pt_regs에 기록하면 최종 사용자 복귀 때 해당 레지스터로 반영됩니다.
처음 읽을 때
번호 5가 테이블의 어느 항목을 선택하고 결과가 어느 저장 프레임으로 돌아가는지 따라가십시오.
더 깊이 살펴볼 때
seccomp·ptrace·restart는 이 함수 전후의 공통 경로와 함께 읽고, 스택 무작위화의 적용 시점도 구분하십시오.
그림으로 보는 변화
add_random_kstack_offset() 뒤 번호를 검사합니다. 범위 안은 테이블 함수를, 범위 밖은 do_ni_syscall()을 호출합니다. 두 결과를 공통으로 기록한 뒤 다음 offset을 고릅니다. 고정 원문 44–65행.1. 현재 offset 적용과 번호 검사
1. 현재 offset 적용과 번호 검사
add_random_kstack_offset()(44행)을 적용하고 scno < sc_nr(46행)로 갈라집니다.
2. 두 호출 경로
유효 번호만 array_index_nospec()으로 테이블 함수를 골라 __invoke_syscall()을 실행합니다(48–49행). 범위 밖은 do_ni_syscall()(51행)로 갑니다.
3. 결과 합류와 다음 호출 준비
어느 분기에서든 ret을 syscall_set_return_value()(54행)에 전달한 뒤 choose_random_kstack_offset()(65행)으로 다음 offset을 고릅니다.
invoke_syscall를 한 줄씩 읽기
줄 번호는 v6.18.37 원문 기준입니다. 주석·빈 줄을 포함한 함수 전체를 먼저 보고, 그 아래에서 각 줄을 설명합니다.
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 (likely(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);
/*
* This value will get limited by KSTACK_OFFSET_MAX(), which is 10
* bits. The actual entropy will be further reduced by the compiler
* when applying stack alignment constraints: the AAPCS mandates a
* 16-byte aligned SP at function boundaries, which will remove the
* 4 low bits from any entropy chosen here.
*
* The resulting 6 bits of entropy is seen in SP[9:4].
*/
choose_random_kstack_offset(get_random_u16());
}static void invoke_syscall(struct pt_regs *regs, unsigned int scno,저장 레지스터 프레임과 사용자 시스템 호출 번호를 받습니다.
unsigned int sc_nr,유효한 테이블 항목 수를 인수로 받습니다. 번호가 이 범위를 넘지 않는지 검사할 기준입니다.
const syscall_fn_t syscall_table[])현재 ABI에 대응하는 시스템 호출 함수 포인터 테이블을 받습니다. 호환 ABI에는 다른 표가 사용될 수 있습니다.
long ret;서비스 함수의 결과를 보관할 long 변수를 선언합니다.
add_random_kstack_offset();이번 진입에 사용할 커널 스택 offset 무작위화를 적용합니다. 사용자 인수 버퍼의 주소를 바꾸는 작업은 아닙니다.
if (likely(scno < sc_nr)) {번호가 테이블의 항목 수 미만인지 확인합니다. likely는 정상 호출이 흔하다는 컴파일러 힌트이지 검사를 생략하는 명령이 아닙니다.
syscall_fn_t syscall_fn;선택할 함수 포인터를 보관할 변수를 선언합니다.
syscall_fn = syscall_table[array_index_nospec(scno, sc_nr)];범위가 확인된 번호에도 array_index_nospec을 적용해 추측 실행의 테이블 범위 밖 접근을 제한합니다.
ret = __invoke_syscall(regs, syscall_fn);선택한 시스템 호출 wrapper에 regs를 전달하고 결과를 받습니다.
} else {번호가 유효한 범위를 벗어난 경우 처리 경로로 갈립니다.
ret = do_ni_syscall(regs, scno);미구현 또는 호환 처리 경로를 통해 해당 번호의 결과를 얻습니다. 임의 주소를 함수로 실행하지 않습니다.
syscall_set_return_value(current, regs, 0, ret);현재 task의 사용자 복귀 레지스터 상태에 결과를 기록합니다. 실제 사용자 레지스터 복원은 뒤의 진입 복귀 코드가 맡습니다.
choose_random_kstack_offset(get_random_u16());다음 호출에서 쓸 무작위 스택 offset을 선택합니다. 주석처럼 정렬 제약 때문에 난수 모든 비트가 유효한 위치 변화로 남는 것은 아닙니다.
함께 생각해 볼 질문
범위를 벗어난 번호를 호출하면 임의 함수가 실행됩니까?
이 경로는 범위를 검사하고 미구현 처리로 보냅니다.
시스템 호출의 C 반환값은 어디로 갑니까?
syscall_set_return_value가 저장 프레임에 반영하고 복귀 코드가 사용자 레지스터로 복원합니다.
SVC immediate가 Linux syscall 번호입니까?
일반 AArch64 Linux ABI에서는 x8이 번호이며 SVC immediate와 구분합니다.
출처와 읽은 범위
Linux stable v6.18.37 · arch/arm64/kernel/syscall.c
해당 버전 원본 파일 · 기존 코드 분석 · 설명 원고