시스템 콜 번호 정의는 어디에 되어있는가?
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개의 함수 코드를 생성하게 된다.
- __arm64_sys_clone() : __se_sys_clone 을 호출한다.
- __se_sys_clone() : __do_sys_clone 을 호출한다.
- __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개의 인자를 넘긴다.
- struct pt_regs regs
- regs→regs[8] : 실행되어야하는 시스템 콜 번호는 8번 레지스터에 담긴다 (초기 VmWatcher의 원리)
- __NR_syscalls : 시스템 콜의 총 개수 (여기서는 467개)
- 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()를 사용해야 한다면,
- syscall_fn = syscall_table[220];
- 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 |