fork
↓ call
__sys_ret(sys_fork())
↓ call
sys_fork()에서 __NR_clone일때, return my_syscall5(__NR_clone, SIGCHLD, 0, 0, 0, 0); 반환
(ARM64는 __NR_fork가 아니라, __NR_clone임)
my_syscall5는 이렇게 정의되어있음.
#define my_syscall5(num, arg1, arg2, arg3, arg4, arg5) \
({ \
register long _num __asm__ ("x8") = (num); \
register long _arg1 __asm__ ("x0") = (long)(arg1); \
register long _arg2 __asm__ ("x1") = (long)(arg2); \
register long _arg3 __asm__ ("x2") = (long)(arg3); \
register long _arg4 __asm__ ("x3") = (long)(arg4); \
register long _arg5 __asm__ ("x4") = (long)(arg5); \
\
__asm__ volatile ( \
"svc #0\n" \
: "=r" (_arg1) \
: "r"(_arg1), "r"(_arg2), "r"(_arg3), "r"(_arg4), "r"(_arg5), \
"r"(_num) \
: "memory", "cc" \
); \
_arg1; \
})
시스템 콜 요청. 인자 num은 시스템콜 번호를 의미함.
__NR_clone은 220번이므로 x8레지스터에 220이 저장될 것으로 예상할 수 있다.
svc #0 실행 : 220번 시스템호출 실행 요청.
코드에서, __NR_clone은
include/uapi/asm-generic/unistd.h에서, 220번으로 시스템콜에 등록되어있음.
#define __NR_clone 220
__SYSCALL(__NR_clone, sys_clone);
sys_clone은 __NR_clone시스템콜 번호가 호출되면 실행 될 핸들러.
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
int __user *, parent_tidptr,
unsigned long, tls,
int __user *, child_tidptr)
#elif defined(CONFIG_CLONE_BACKWARDS2)
SYSCALL_DEFINE5(clone, unsigned long, newsp, unsigned long, clone_flags,
int __user *, parent_tidptr,
int __user *, child_tidptr,
unsigned long, tls)
#elif defined(CONFIG_CLONE_BACKWARDS3)
SYSCALL_DEFINE6(clone, unsigned long, clone_flags, unsigned long, newsp,
int, stack_size,
int __user *, parent_tidptr,
int __user *, child_tidptr,
unsigned long, tls)
#else
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
int __user *, parent_tidptr,
int __user *, child_tidptr,
unsigned long, tls)
#endif
{
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_DEFINE5 매크로에 의해 clone는 이름이 바뀌는데 마지막에 확인해보자.
결국 220번 시스템콜이 발생하면 kernel_clone호출하는데,
ftrace에 등록되어있다.
간단한 테스트 프로그램을 작성하고 의도적으로 fork를 발생시켜 보았다.
ftrace로 kernel_clone를 추적시켜 콜스택을 추적해보았다.

여기까지 분석 후 흐름을 대강 정리해보자.
fork는 결국 my_syscall5를 호출하고, svc명렁으로 시스템콜 호출을 요청한다.
invoke_syscall -> __arm64_sys_clone -> __do_sys_clone -> kernel_clone의 순서로 함수가 호출된다.
그러면 __arm64_sys_clone은 어디서 나오나?
나머지 invoke_syscall을 분석해보자.
static long __invoke_syscall(struct pt_regs *regs, syscall_fn_t syscall_fn)
{
return syscall_fn(regs);
}
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);
/*
* 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());
}
typedef long (*syscall_fn_t)(const struct pt_regs *regs);
참고로 syscall_fn_t는 포인터로 long 형 포인터로 정의되어있고,
syscall_fn과 syscall_table은 syscall_fn_t형 변수이다.
syscall_table은 do_el0_svc -> el0_svc_common에서 인자로 전달하는 sys_call_table이고, 핸들러 함수의 포인터가 담긴 포인터 배열이라고 예상한다.
즉, invoke_syscall에서 syscall_table에서 핸들러 함수의 포인터를 syscall_fn에 저장한다.
그리고 __invoke_syscall을 호출한다.
그래서, sys_call_table을 찾아보았다.
const syscall_fn_t sys_call_table[__NR_syscalls] = {
[0 ... __NR_syscalls - 1] = __arm64_sys_ni_syscall,
#include <asm/syscall_table_64.h>
};
예상했듯 sys_call_table은 핸들러 테이블임을 알 수 있고,
호출스택에서 보았던 __arm64_sys_clone이 여기서 나왔음을 볼 수 있다.
__arm64_sys_clone은 __do_sys_clone을 호출하는데,
__do_sys_clone은 매크로에 의해 생긴다.
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
int __user *, parent_tidptr,
unsigned long, tls,
int __user *, child_tidptr)
여기서 clone이 인자로 들어가고,
SYSCALL_DEFINE5는 __SYSCALL_DEFINEx의 alias이다.
__SYSCALL_DEFINEx 매크로를 찾아보자.
#ifndef __SYSCALL_DEFINEx
#define __SYSCALL_DEFINEx(x, name, ...) \
__diag_push(); \
__diag_ignore(GCC, 8, "-Wattribute-alias", \
"Type aliasing is used to sanitize syscall arguments");\
asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__)) \
__attribute__((alias(__stringify(__se_sys##name)))); \
ALLOW_ERROR_INJECTION(sys##name, ERRNO); \
static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__));\
asmlinkage long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__)); \
asmlinkage 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; \
} \
__diag_pop(); \
static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))
#endif /* __SYSCALL_DEFINEx */
완벽하게 해석하지는 못해도,
인자로 전달받은 clone을 __do_sys_clone으로 바꾼다는 것을 알 수 있다.
방금전의 SYSCALL_DEFINE5의 코드를 전체적으로 다시 봐보자.
{
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);
}
clone이 __do_sys_clone으로 바뀐다고 했으므로, clone은 __do_sys_clone과 동일하다.
그러면, __do_sys_clone에서는 kernel_clone을 호출한다.
이로써, ftrace로 kernel_clone을 찍어보고
모든 콜스택을 조사해보았다.
이제 kernel_clone_args같은 구조체들을 파보자.
'Linux' 카테고리의 다른 글
| 메모리 프로파일러 (시스템 콜 후킹) (0) | 2025.10.05 |
|---|---|
| 프로세스 생성 분석 fork (태스크 복사 및 초기화 부분) - 2 (0) | 2025.09.30 |
| 커널에서 static inline 함수선언과 wrapping (0) | 2025.09.20 |
| linux/drivers/char/mem.c 드라이버 분석 (0) | 2025.09.20 |
| objdump활용 vmlinux 분석시 start address의미 (0) | 2025.09.11 |