System Call 분석

2026. 4. 13. 11:41·Linux/start_analyse()

시스템 콜 번호 정의는 어디에 되어있는가?

include/uapi/asm-generic/unistd.h 에 시스템 콜 번호가 저장되어있다. 만약, fork() 시스템콜이 몇번인지 확인하고자 한다면?

#define __NR_clone 220
__SYSCALL(__NR_clone, sys_clone)

__SYSCALL은 _arm64##sym로 define 되어있다. 따라서 unistd.h는 이렇게 구성이 될 것이다.

#define __NR_clone 220
__arm64_sys_clone

그러면 나중에 unistd.h만 include를 하면, __arm64_sys_clone() 함수를 사용할 수 있다는 뜻이다.


시스템 콜의 실제 정의

시스템 콜의 실제 정의는 SYSCALL_DEFINEx 매크로에 의해서 정의된다. SYSCALL_DEFINEx 매크로는 __SYSCALL_DEFINEx 를 define한다.

__SYSCALL_DEFINEx

#define __SYSCALL_DEFINEx(x, name, ...)                        
    asmlinkage long __arm64_sys##name(const struct pt_regs *regs);        
    static long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__));        
    static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__));    
    asmlinkage long __arm64_sys##name(const struct pt_regs *regs)        
    {                                    
        return __se_sys##name(SC_ARM64_REGS_TO_ARGS(x,__VA_ARGS__));    
    }                                    
    static long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__))        
    {                                    
        long ret = __do_sys##name(__MAP(x,__SC_CAST,__VA_ARGS__));    
        __MAP(x,__SC_TEST,__VA_ARGS__);                    
        __PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__));        
        return ret;                            
    }                                   
    static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))

asmlinkage 키워드는 어셈블러 코드에서 호출 할 수 있다는 의미 (이건 나중에 공부해보자)

아무튼, 어떤 곳에서 __SYSCALL_DEFINEx매크로를 호출하면, 인자로 받은 것들로 3개의 함수 코드를 생성하게 된다.

  1. __arm64_sys_clone() : __se_sys_clone 을 호출한다.
  2. __se_sys_clone() : __do_sys_clone 을 호출한다.
  3. __do_sys_clone()

__arm64_sys_clone() → __se_sys_clone() → __do_sys_clone() 과 같은 호출 스택을 갖게된다.

기존의 clone()의 시스템 콜 정의 는 이렇게 되어있다.

SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
		 int __user *, parent_tidptr,
		 unsigned long, tls,
		 int __user *, child_tidptr)
{
	struct kernel_clone_args args = {
		.flags		= (lower_32_bits(clone_flags) & ~CSIGNAL),
		.pidfd		= parent_tidptr,
		.child_tid	= child_tidptr,
		.parent_tid	= parent_tidptr,
		.exit_signal	= (lower_32_bits(clone_flags) & CSIGNAL),
		.stack		= newsp,
		.tls		= tls,
	};

	return kernel_clone(&args);
}

그러면 SYSCALL_DEFINE은 define된 매크로에 의해서 다음과 같이 코드가 변한다.

asmlinkage long __arm64_sys##name(const struct pt_regs *regs);
static long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__));
static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__));    
asmlinkage long __arm64_sys##name(const struct pt_regs *regs)        
{                                    
    return __se_sys##name(SC_ARM64_REGS_TO_ARGS(x,__VA_ARGS__));    
}                                

static long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__))        

{                                    
        long ret = __do_sys##name(__MAP(x,__SC_CAST,__VA_ARGS__));    
        return ret;                            
}                                        

static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))
{
        struct kernel_clone_args args = {
                .flags          = (lower_32_bits(clone_flags) & ~CSIGNAL),
                .pidfd          = parent_tidptr,
                .child_tid      = child_tidptr,
                .parent_tid     = parent_tidptr,
                .exit_signal    = (lower_32_bits(clone_flags) & CSIGNAL),
                .stack          = newsp,
                .tls            = tls,
        };
        return kernel_clone(&args);
}

__arm64_sys_clone() → __se_sys_clone() → __do_sys_clone() 과 같은 호출 스택을 갖는다고 했으니, 결국 __do_sys_clone()을 보면 kernel_clone()을 호출하는 것을 알 수 있다. 그러면 여기서 부터는 ftrace로 추적해봤던 그대로 kernel_clone() → copy_process() → dup_task_struct() … 같은 콜스택을 가진다.


시스템 콜 테이블

시스템콜 테이블은,arch/arm64/tools/syscall_64.tbl에 써져있다.

compat_sys_keyctl
220     common  clone                           sys_clone
221     common  execve                          sys_execve
222     32      mmap2                           sys_mmap2
222     64      mmap                            sys_mmap

빌드 스크립트가 unistd.h 혹은 syscall_64.tbl을 읽어서, syscall_table_64.h를 만든다.

그러면 asm/syscall_table_64.h는 이렇게 된다.

...
__SYSCALL(220, sys_clone)
__SYSCALL(221, sys_execve)
...
...

그리고 arch/arm64/kernel/sys.c의 소스 내용을 봐보자

#define __SYSCALL(nr, sym)    asmlinkage long __arm64_##sym(const struct pt_regs *);

#include <asm/syscall_table_64.h>
#undef __SYSCALL
#define __SYSCALL(nr, sym)    [nr] = __arm64_##sym,

const syscall_fn_t sys_call_table[__NR_syscalls] = {
    [0 ... __NR_syscalls - 1] = __arm64_sys_ni_syscall,
#include <asm/syscall_table_64.h>
};

여기서 #include <asm/syscall_table_64.h>를 만나는 순간에 코드가 삽입된다(컴파일 타임에). 여기서 아까 말했듯, asm/syscall_table_64.h는 빌드시에 빌드 스크립트가 생성해준다.

#define __SYSCALL(nr, sym)    asmlinkage long __arm64_##sym(const struct pt_regs *);

...
__SYSCALL(220, sys_clone)
__SYSCALL(221, sys_execve)
...
...

#undef __SYSCALL
#define __SYSCALL(nr, sym)    [nr] = __arm64_##sym,

const syscall_fn_t sys_call_table[__NR_syscalls] = {
    [0 ... __NR_syscalls - 1] = __arm64_sys_ni_syscall,
#include <asm/syscall_table_64.h>
};

그러면 __SYSCALL(220, sys_clone) 문장이 바로 위의 define문에 의해서 __arm64_sys_clone(pt_regs)이렇게 변신이 된다. 그 결과, _arm64_sys~~라는 코드가 수백개 생긴다.

#define __SYSCALL(nr, sym)    asmlinkage long __arm64_##sym(const struct pt_regs *);
// 이 define은 하단의 undef에 의해서 사라진다.
...
__arm64_sys_clone(const struct pt_regs *); // 이런 코드가 쭉 생긴다.
__arm64_sys_execve(const struct pt_regs *);
...
...

#undef __SYSCALL // 위의 #define을 지운다.
#define __SYSCALL(nr, sym)    [nr] = __arm64_##sym,

const syscall_fn_t sys_call_table[__NR_syscalls] = {
    [0 ... __NR_syscalls - 1] = __arm64_sys_ni_syscall,
#include <asm/syscall_table_64.h>

};

이렇게 바뀌고,

...
__arm64_sys_clone(const struct pt_regs *); // 이런 코드가 쭉 생긴다.
__arm64_sys_execve(const struct pt_regs *);
...
...

#define __SYSCALL(nr, sym)    [nr] = __arm64_##sym,

const syscall_fn_t sys_call_table[__NR_syscalls] = {
    [0 ... __NR_syscalls - 1] = __arm64_sys_ni_syscall,

#include <asm/syscall_table_64.h>
};

여기서 다시 asm/syscall_table_64.h를 include한다.

...
__arm64_sys_clone(const struct pt_regs *); // 이런 코드가 쭉 생긴다.
__arm64_sys_execve(const struct pt_regs *);
...
...

#define __SYSCALL(nr, sym)    [nr] = __arm64_##sym,

const syscall_fn_t sys_call_table[__NR_syscalls] = { // 일단 초기화
    [0 ... __NR_syscalls - 1] = __arm64_sys_ni_syscall,
    ...
    [220] = __arm64_sys_clone // 이후에 초기화 (__arm64_sys_clone()의 주소를 배열에 넣음)
    [221] = __arm64_sys_execve
    ...
};

그러면, 상단에 있는 __SYSCALL을 define했기 때문에 이런식으로 변형되고, 테이블에 _arm64_sys~의 함수 포인터가 들어가게 된다.


그러면 어디서 sys_call_table[]로 접근하는가?

시스템콜을 실행하려면, sys_call_table[]에 접근이 필수적이라는 것을 알았다. 왜냐면 이 테이블에 시스템콜 함수의 주소가 들어있기 때문이다.

do_el0_svc() → el0_svc_common() → invoke_syscall() → __invoke_syscall() → syscall_fn() → __arm64_sys_clone() → … 이렇게 줄줄이 쏘세지마냥 호출이 되는데, 여기서 sys_call_table[]을 활용한다.

호출 과정

void do_el0_svc(struct pt_regs *regs)
{
        el0_svc_common(regs, regs->regs[8], __NR_syscalls, sys_call_table);
}

여기서 sys_call_table을 el0_svc_common에 인자로 실어서 전달한다. sys_call_table은 arch/arm64/kernel/sys.c에 전역 포인터 배열로 선언되어있다.

extern const syscall_fn_t sys_call_table[];

전역 배열인 sys_call_table 배열을 arch/arm64/include/asm/syscall.h에서 extern하고 있기 때문에 시스템콜 테이블을 공유하고 있다고 보면 된다.

그러면 이제 대충 가닥이 잡히는데,

el0_svc_common(regs, regs->regs[8], __NR_syscalls, sys_call_table);

이 부분을 보면 4개의 인자를 넘긴다.

  1. struct pt_regs regs
  2. regs→regs[8] : 실행되어야하는 시스템 콜 번호는 8번 레지스터에 담긴다 (초기 VmWatcher의 원리)
  3. __NR_syscalls : 시스템 콜의 총 개수 (여기서는 467개)
  4. sys_call_table[]

이렇게 4가지 인자를 넘긴다.

el0_svc_common()은 어떤일을 하는가?

static void el0_svc_common(struct pt_regs *regs, int scno, int sc_nr,
               const syscall_fn_t syscall_table[])
{
    regs->orig_x0 = regs->regs[0];
    regs->syscallno = scno;
    ...
    invoke_syscall(regs, scno, sc_nr, syscall_table);
    ...
}

이런식으로 함수가 구성되어있고, invoke_syscall을 호출하며 인자로 syscall 번호, syscall 개수, syscall 테이블을 전달한다.

invoke_syscall()은 어떤일을 하는가?

static void invoke_syscall(struct pt_regs *regs, unsigned int scno,
               unsigned int sc_nr,
               const syscall_fn_t syscall_table[])
{
    long ret;
    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);
}

여기서 핵심 포인트는 시스템콜 테이블에서 시스템콜 핸들러의 포인터를 가져온다는 것이다.

syscall_fn = syscall_table[array_index_nospec(scno, sc_nr)]; 이렇게 가져온다. 그리고 다시 __invoke_syscall을 호출하면서 인자로 시스템콜 핸들러 포인터를 넣는다. 만약 __arm64_sys_clone()를 사용해야 한다면,

  1. syscall_fn = syscall_table[220];
  2. syscall_fn = __arm64_sys_clone();

이런식으로 된다.

__invoke_syscall(regs, syscall_fn)은 어떤일을 하는가?

static long __invoke_syscall(struct pt_regs *regs, syscall_fn_t syscall_fn)
{
    return syscall_fn(regs);
}

__invoke_syscall()의 정의는 간단하다. 그냥 전달받은 시스템콜 핸들러를 실행시킨다. 만약 지금까지 분석했던 clone을 예로 든다면, 시스템콜 테이블의 220번에 __arm64_sys_clone의 주소가 들어갔다. 즉, 여기서 syscall_fn이라는건 __arm64_sys_clone의 주소라는것! 함수 포인터!

그러면 호출순서는 이렇게 된다.

syscall_fn(regs) → __arm64_sys_clone() → __se_sys_clone() → __do_sys_clone() → kernel_clone()

이렇게 까지와서야 kernel_clone()이 발생한다…


struct pt_regs는 어떤 구조체 인가?

그러면 지금 시스템콜에 대한 과정을 보면서 struct pt_regs라는 구조를 많이 봤다. 우리가 pt_regs라는 구조체 안을 들여다보지 않아도 알 수 있었던 건, regs[8]에, 즉 8번 레지스터에 시스템콜 번호가 담긴다는 것이다. 예를 들어, clone시스템콜이 발생했다면 regs[8]의 값은 220이 될 것이다.

struct pt_regs {
	union {
		struct user_pt_regs user_regs;
		struct {
			u64 regs[31]; // 범용 레지스터 31개, 여기서 8번은 시스템콜 번호가 들어감
			u64 sp; // stack pointer
			u64 pc; // program counter
			u64 pstate; // processor state
		};
	};
	u64 orig_x0;
	s32 syscallno; // 현재 실행중인 syscall no
	u32 pmr;

	u64 sdei_ttbr1;
	struct frame_record_meta stackframe;

	/* Only valid for some EL1 exceptions. */
	u64 lockdep_hardirqs;
	u64 exit_rcu;
};

즉, pt_regs라는건 유저레벨에서 시스템콜을 호출하면 커널레벨로 들어가는데 다시 유저레벨로 복귀하기위한 그 당시의 정보를 말한다. 일종의 snapshot이다.

'Linux > start_analyse()' 카테고리의 다른 글

나의 sht20드라이버는 무엇이 부족했을까, 메인라인 코드와 비교 분석  (0) 2026.01.02
/drivers/char/mem.c 드라이버 2차 분석  (0) 2026.01.01
'Linux/start_analyse()' 카테고리의 다른 글
  • 나의 sht20드라이버는 무엇이 부족했을까, 메인라인 코드와 비교 분석
  • /drivers/char/mem.c 드라이버 2차 분석
Minu Jin
Minu Jin
정보의 바다
  • Minu Jin
    뇌 구조가 바이너리
    Minu Jin
  • 전체
    오늘
    어제
    • 분류 전체보기
      • C프로그래밍
        • 오류해결
        • 개인 공부
        • Programming Lab(학교수업)
        • MemoryTracker
      • C++
        • 개인 공부
      • 자료구조(Data Structure)
      • ARM arch
        • Cortex-M
        • FreeRTOS
      • 컴퓨터 공학(Computer Science)
        • OS
        • 컴퓨터 구조
      • Qualcomm 기업과제
      • Linux
        • start_contribute()
        • start_analyse()
      • Web
      • 똥글
      • 백준
      • Git 학습
        • 오류해결
        • 학습중
      • Python
        • 오류해결
        • 개인 공부
  • 블로그 메뉴

    • 태그
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    Branch
    rubik pi
    Git
    arm
    commit
    포인터
    커널 기여
    파일 입출력
    자료구조
    버퍼
    커널
    yolo
    이진 트리
    앤드류모튼
    파이썬
    백준
    드라이버 분석
    스택
    피보나치
    토발즈
    순환
    Qualcomm
    시스템콜
    C++
    c언어
    INIT
    rubikpi3
    동적메모리
    소수
    리눅스
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
Minu Jin
System Call 분석
상단으로

티스토리툴바