Linux v6.6 · 개념과 코드 읽기

시스템 호출 번호를 검증한 뒤 함수로 연결합니다

이 코드는 어떤 문제를 푸나요?

사용자 프로그램은 커널 함수의 주소를 직접 골라 호출하지 않습니다. 시스템 호출 번호를 전달하고 커널은 범위를 확인해 테이블의 함수를 선택합니다. invoke_syscall은 이미 보존된 pt_regs를 받아 호출과 반환값 기록을 연결하는 부분입니다.

읽을 범위: v6.6 · arch/arm64/kernel/syscall.c · invoke_syscall 40–70행입니다. 아래에 이 범위의 원문과 각 줄의 설명을 실었습니다. 주제 전체의 흐름과 다른 경로는 기존 분석에서 함께 읽으실 수 있습니다.

먼저 알아둘 개념

pt_regs

예외 진입 시 저장한 사용자 레지스터와 복귀 상태입니다.

번호 검증과 nospec

정상 제어 흐름의 범위 검사와 추측 실행 중 잘못된 인덱스를 사용하지 않게 하는 처리는 함께 고려합니다.

처음 읽을 때

scno는 번호, sc_nr는 테이블 크기, syscall_fn은 선택한 함수 주소입니다. 이 세 값을 구분한 다음 결과 ret가 pt_regs를 통해 사용자 복귀에 전달되는 과정을 따라가세요.

더 깊이 살펴볼 때

이 함수는 syscall 진입 전체가 아니라 유효 범위 검사·함수 선택·결과 기록 구간입니다. 인자 수집 wrapper, 추적·필터, 예외 복귀와 스택 무작위화 매크로의 실제 제한은 연결된 소스에서 따로 확인하셔야 합니다.

그림으로 보는 변화

시스템 호출 번호를 검증한 뒤 함수로 연결합니다의 단계별 개념 그림
각 단계에 화살표 의미와 생략 범위를 표시했습니다. 주소·숫자 예제는 실제 장치 값을 뜻하지 않습니다.
1단계 설명

1단계 고정

GIF 원본 열기

1. 호출 번호 확인

scno가 syscall 테이블 크기 안에 있는지 검사합니다.

조건 분기의 두 경로 중 하나만 실행됩니다.

2. 함수 실행

유효 번호는 테이블 함수로, 그 외는 미구현 처리로 보냅니다.

함수 선택과 사용자 포인터 검증 전체는 같은 일이 아닙니다.

3. 결과 기록

반환값을 저장된 사용자 레지스터에 넣습니다.

이후 예외 복귀 코드가 그 값을 사용자에게 돌려줍니다.

invoke_syscall를 한 줄씩 읽기

줄 번호는 v6.6 원문 기준입니다. 주석·빈 줄을 포함한 함수 전체를 먼저 보고, 그 아래에서 각 줄을 설명합니다.

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);
}
static void invoke_syscall(struct pt_regs *regs, unsigned int scno,

저장된 사용자 레지스터 문맥 regs와 시스템 호출 번호 scno를 받습니다. 반환형은 void이며 호출 결과는 아래에서 regs에 기록합니다.

			   unsigned int sc_nr,

선택한 시스템 호출 테이블의 유효 항목 수를 받는 함수 인자입니다. 함수 안에서 새로 만드는 지역변수가 아닙니다.

			   const syscall_fn_t syscall_table[])

호출 함수 포인터들의 테이블을 받는 인자입니다. native·compat 중 어떤 테이블을 넘길지는 상위 경로에서 결정합니다.

	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);

선택한 함수에 저장된 레지스터 문맥을 전달하여 실행하고 반환값을 ret에 받습니다. syscall wrapper가 이 문맥에서 실제 인자를 꺼내는 경로로 이어집니다.

	} else {

앞 조건이 성립하지 않은 경로를 선택합니다. 앞쪽 처리와 아래 처리를 모두 수행하는 것이 아닙니다.

		ret = do_ni_syscall(regs, scno);

테이블 범위를 벗어난 시스템 호출 번호에 대해 미구현 또는 호환 처리를 수행합니다.

	syscall_set_return_value(current, regs, 0, ret);

호출 결과를 사용자 복귀에 사용할 레지스터 문맥에 기록합니다. 별도의 error 인자는 0이고 ret에 결과 또는 음수 오류값이 이미 들어 있습니다.

	choose_random_kstack_offset(get_random_u16() & 0x1FF);

v6.6의 이 줄은 난수의 하위 9비트를 남겨 다음 스택 오프셋 선택에 사용합니다. 실제 스택 조정에서는 정렬 등 별도 제약도 적용됩니다.

함께 생각해 볼 질문

범위 검사만 있으면 nospec은 필요 없나요?

정상 실행과 추측 실행의 위험을 구분하기 위한 보완입니다.

이 함수 안의 ret가 바로 ERET인가요?

여기서는 C 함수 호출의 결과입니다. 실제 예외 복귀는 별도 어셈블리 경로에서 수행합니다.

없는 syscall 번호는 어떻게 되나요?

정상 테이블 호출 대신 do_ni_syscall 경로로 보냅니다.

출처와 읽은 범위

Linux stable v6.6 · arch/arm64/kernel/syscall.c

해당 버전 원본 파일 · 기존 코드 분석 · 설명 원고

맨 위로 ↑