시스템 콜 (System Call)
사용자 공간과 커널 공간을 잇는 핵심 인터페이스인 시스템 콜을 다룹니다. x86_64 SYSCALL/SYSRET 호출 규약, entry_SYSCALL_64 진입 경로, sys_call_table, SYSCALL_DEFINE 매크로, vDSO, seccomp-bpf, 호환성 계층까지 심층적으로 분석합니다.
SYSCALL 명령어는 "번호표를 뽑고 창구로 가는 행위"에 해당합니다.
핵심 요약
- 시스템 콜 — 사용자 프로그램이 커널 서비스를 요청하는 유일한 공식 인터페이스입니다.
- SYSCALL/SYSRET — x86_64에서 사용자↔커널 전환을 수행하는 CPU 명령어입니다.
- sys_call_table — 시스템 콜 번호를 실제 커널 함수에 매핑하는 함수 포인터 테이블입니다.
- vDSO — 커널 진입 없이 사용자 공간에서 실행되는 가상 동적 공유 객체 (gettimeofday 등).
- seccomp-BPF — 프로세스가 사용할 수 있는 시스템 콜을 BPF 필터로 제한하는 보안 기능입니다.
단계별 이해
- 호출 흐름 — 사용자 프로그램 → glibc 래퍼 →
SYSCALL명령어 → 커널 진입 → 핸들러 실행 → 결과 반환.이 전체 과정이 수백 나노초 안에 완료됩니다.
- strace로 관찰 —
strace ls를 실행하면ls가 호출하는 모든 시스템 콜을 볼 수 있습니다.open(),read(),write()등 실제 시스템 콜과 그 인자/반환값을 확인합니다. - 커널 내부 —
SYSCALL_DEFINE3(read, ...)매크로가sys_read()함수를 정의합니다.시스템 콜 번호는
arch/x86/entry/syscalls/syscall_64.tbl에 정의되어 있습니다. - 보안 — seccomp-BPF로 컨테이너나 샌드박스에서 허용할 시스템 콜을 화이트리스트로 제한합니다.
Docker, Chrome 등이 seccomp을 사용하여 공격 표면을 줄입니다.
시스템 콜 개요
시스템 콜(system call)은 사용자 공간 프로그램이 커널의 서비스를 요청하기 위한 프로그래밍 인터페이스입니다. 프로세스 생성, 파일 읽기/쓰기, 네트워크 통신, 메모리 할당 등 하드웨어 자원에 접근하는 모든 작업은 시스템 콜을 통해야 합니다. Linux 커널은 약 450개 이상의 시스템 콜을 제공합니다.
보호 링과 특권 수준
x86 아키텍처는 4개의 보호 링(Ring 0~3)을 제공하지만, Linux는 Ring 0(커널 모드)과 Ring 3(사용자 모드) 두 가지만 사용합니다. 시스템 콜은 사용자 모드에서 커널 모드로의 제어된 전환을 수행하는 유일한 합법적 경로입니다.
| 특권 수준 | Ring | CPL | 접근 가능 영역 |
|---|---|---|---|
| 커널 모드 | Ring 0 | 0 | 모든 메모리, I/O 포트, 특권 명령어 |
| 사용자 모드 | Ring 3 | 3 | 사용자 공간 메모리만 (가상 주소 하위 영역) |
int 0x80에서 SYSCALL까지의 변천
x86 시스템 콜 호출 메커니즘은 성능 향상을 위해 발전해 왔습니다.
| 메커니즘 | 아키텍처 | 도입 시기 | 특징 |
|---|---|---|---|
int 0x80 |
i386 | 초기 | 소프트웨어 인터럽트, IDT 조회 필요 → 느림 |
sysenter/sysexit |
i386 (Pentium II+) | 2000년대 | Intel 전용 빠른 진입, MSR 기반 |
SYSCALL/SYSRET |
x86_64 | AMD64 | 64비트 표준, MSR_LSTAR 기반, 가장 빠름 |
int 0x80은 IDT(Interrupt Descriptor Table)를 조회하고 스택 전환을 수행하므로 수백 사이클이 소요됩니다. SYSCALL은 IDT를 우회하고 MSR에 미리 저장된 진입점으로 직접 점프하므로 훨씬 빠릅니다.
/* int 0x80 (레거시 32비트) - 느린 경로 */
mov $__NR_write, %eax /* 시스템 콜 번호 */
mov $1, %ebx /* fd = stdout */
mov $msg, %ecx /* buffer */
mov $len, %edx /* count */
int $0x80 /* 소프트웨어 인터럽트 */
/* SYSCALL (x86_64) - 빠른 경로 */
mov $1, %rax /* __NR_write */
mov $1, %rdi /* fd = stdout */
mov $msg, %rsi /* buffer */
mov $len, %rdx /* count */
syscall /* 빠른 시스템 콜 진입 */
시스템 콜 아키텍처
x86_64 레지스터 규약
x86_64에서 SYSCALL 명령어를 사용할 때의 레지스터 규약은 다음과 같습니다. 이는 C 함수 호출 규약(System V AMD64 ABI)과 유사하지만 rcx 대신 r10을 사용하는 점이 다릅니다.
| 레지스터 | 용도 | 비고 |
|---|---|---|
rax |
시스템 콜 번호 / 반환값 | 호출 전: 번호, 반환 후: 결과 또는 -errno |
rdi |
1번째 인자 | |
rsi |
2번째 인자 | |
rdx |
3번째 인자 | |
r10 |
4번째 인자 | C ABI에서는 rcx이지만, SYSCALL이 rcx를 덮어쓰므로 r10 사용 |
r8 |
5번째 인자 | |
r9 |
6번째 인자 | |
rcx |
복귀 주소 (RIP) | SYSCALL이 자동으로 RIP → RCX 저장 |
r11 |
RFLAGS 백업 | SYSCALL이 자동으로 RFLAGS → R11 저장 |
SYSCALL 명령어는 하드웨어적으로 rcx에 복귀 주소(RIP)를, r11에 RFLAGS를 저장합니다. 따라서 이 두 레지스터의 원래 값은 시스템 콜 수행 후 복원되지 않습니다. glibc의 syscall wrapper는 이를 고려하여 r10으로 4번째 인자를 전달합니다.
시스템 콜 흐름 다이어그램
시스템 콜 테이블
sys_call_table은 시스템 콜 번호를 해당 핸들러 함수 포인터에 매핑하는 배열입니다. x86_64에서는 arch/x86/entry/syscall_64.c에 정의됩니다.
/* arch/x86/entry/syscall_64.c */
#include <asm/syscalls_64.h>
#define __SYSCALL(nr, sym) [nr] = __x64_##sym,
const sys_call_ptr_t sys_call_table[] __ro_after_init = {
[0 ... __NR_syscall_max] = __x64_sys_ni_syscall,
#include <asm/syscalls_64.h>
};
이 테이블은 __ro_after_init으로 선언되어 부팅 후에는 읽기 전용이 됩니다. 이는 루트킷이 시스템 콜 테이블을 변조하는 것을 방지하는 보안 장치입니다.
시스템 콜 번호 할당
시스템 콜 번호는 arch/x86/entry/syscalls/syscall_64.tbl 테이블 파일에서 관리됩니다. 빌드 시 이 파일로부터 헤더가 자동 생성됩니다.
# arch/x86/entry/syscalls/syscall_64.tbl
# <번호> <ABI> <이름> <진입점>
0 common read sys_read
1 common write sys_write
2 common open sys_open
3 common close sys_close
...
56 common clone sys_clone
57 common fork sys_fork
59 common execve sys_execve
...
435 common clone3 sys_clone3
주요 시스템 콜 번호를 확인하려면:
# 사용자 공간에서 시스템 콜 번호 헤더 확인
$ grep -E '__NR_(read|write|open|close|fork|clone|execve) ' \
/usr/include/asm/unistd_64.h
#define __NR_read 0
#define __NR_write 1
#define __NR_open 2
#define __NR_close 3
sys_ni_syscall(not implemented)로 매핑되어 -ENOSYS를 반환합니다.
SYSCALL_DEFINE 매크로
DEFINE0~6 패밀리
커널 시스템 콜 핸들러는 SYSCALL_DEFINEN 매크로로 정의됩니다. N은 인자 개수(0~6)를 나타냅니다. 이 매크로는 함수 프로토타입 생성, 인자 타입 검증, 추적(tracing) 지원, CVE 방지용 타입 캐스팅을 자동으로 처리합니다.
/* include/linux/syscalls.h */
#define SYSCALL_DEFINE0(name) ...
#define SYSCALL_DEFINE1(name, type1, arg1) ...
#define SYSCALL_DEFINE2(name, type1, arg1, type2, arg2) ...
/* ... SYSCALL_DEFINE6까지 */
/* 내부적으로 확장되는 구조 */
#define SYSCALL_DEFINE3(name, ...) \
SYSCALL_METADATA(name, 3, __VA_ARGS__) \
__SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
SYSCALL_METADATA는 ftrace의 syscall tracepoint에 사용되며, __SYSCALL_DEFINEx는 실제 함수 본체를 정의합니다. 인자를 long으로 받아 올바른 타입으로 캐스팅하여 레지스터 상위 비트 오염 공격(CVE-2009-0029)을 방지합니다.
실제 커널 코드 예제
/* fs/read_write.c - read 시스템 콜 */
SYSCALL_DEFINE3(read,
unsigned int, fd,
char __user *, buf,
size_t, count)
{
return ksys_read(fd, buf, count);
}
/* kernel/fork.c - getpid (인자 없음) */
SYSCALL_DEFINE0(getpid)
{
return task_tgid_vnr(current);
}
/* mm/mmap.c - mmap (인자 6개, 최대) */
SYSCALL_DEFINE6(mmap,
unsigned long, addr,
unsigned long, len,
unsigned long, prot,
unsigned long, flags,
unsigned long, fd,
unsigned long, off)
{
return ksys_mmap_pgoff(addr, len, prot, flags, fd,
off >> PAGE_SHIFT);
}
시스템 콜 진입 경로 상세
entry_SYSCALL_64 분석
entry_SYSCALL_64는 x86_64에서 모든 64비트 시스템 콜의 커널 진입점입니다. MSR_LSTAR에 이 함수의 주소가 저장되어 있어, SYSCALL 명령어 실행 시 하드웨어가 이 주소로 점프합니다. 어셈블리 종합 페이지와 MSR 레지스터 페이지에서 관련 세부 사항을 확인할 수 있습니다.
/* arch/x86/entry/entry_64.S (간략화) */
SYM_CODE_START(entry_SYSCALL_64)
/* SYSCALL 직후: RCX=사용자RIP, R11=사용자RFLAGS
* RSP는 아직 사용자 스택! */
swapgs /* GS base를 커널용으로 전환 */
/* 사용자 RSP를 per-CPU 영역에 임시 저장 */
movq %rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2)
/* 커널 스택으로 전환 (per-CPU에서 로드) */
movq PER_CPU_VAR(pcpu_hot + X86_top_of_stack), %rsp
/* pt_regs 구조체 형태로 레지스터 저장 */
pushq $__USER_DS /* pt_regs->ss */
pushq PER_CPU_VAR(cpu_tss_rw + TSS_sp2) /* pt_regs->sp */
pushq %r11 /* pt_regs->flags */
pushq $__USER_CS /* pt_regs->cs */
pushq %rcx /* pt_regs->ip */
pushq %rax /* pt_regs->orig_ax (syscall nr) */
/* 나머지 범용 레지스터 저장 */
PUSH_AND_CLEAR_REGS rax=$-ENOSYS
/* C 핸들러 호출 */
movq %rsp, %rdi /* 1번째 인자: pt_regs 포인터 */
movslq %eax, %rsi /* 2번째 인자: syscall 번호 */
call do_syscall_64
/* 반환 경로: 레지스터 복원 후 SYSRET */
...
swapgs
sysretq
SYM_CODE_END(entry_SYSCALL_64)
swapgs는 사용자 GS base와 커널 GS base를 교환합니다. 이 명령어가 빠지면 커널이 사용자 제어 GS base를 사용하게 되어 보안 취약점이 발생합니다. Spectre v1 변종 공격에서 swapgs 경계가 표적이 된 사례(CVE-2019-1125)가 있습니다.
pt_regs 구조체
struct pt_regs는 시스템 콜(또는 인터럽트/예외) 진입 시 저장되는 사용자 레지스터 상태를 담는 구조체입니다. 커널 스택에 이 구조체 형태로 레지스터를 push합니다.
/* arch/x86/include/asm/ptrace.h */
struct pt_regs {
unsigned long r15, r14, r13, r12;
unsigned long bp, bx;
unsigned long r11, r10, r9, r8;
unsigned long ax, cx, dx, si, di;
unsigned long orig_ax; /* 시스템 콜 번호 */
unsigned long ip; /* 사용자 RIP (RCX에서) */
unsigned long cs;
unsigned long flags; /* 사용자 RFLAGS (R11에서) */
unsigned long sp; /* 사용자 RSP */
unsigned long ss;
};
do_syscall_64()
do_syscall_64()는 어셈블리 진입점에서 호출되는 C 함수로, 시스템 콜 번호 검증과 디스패치를 수행합니다.
/* arch/x86/entry/common.c */
__visible noinstr void do_syscall_64(
struct pt_regs *regs, int nr)
{
add_random_kstack_offset();
nr = syscall_enter_from_user_mode(regs, nr);
instrumentation_begin();
if (!do_syscall_x64(regs, nr))
do_syscall_x32(regs, nr); /* x32 ABI 폴백 */
instrumentation_end();
syscall_exit_to_user_mode(regs);
}
static __always_inline bool do_syscall_x64(
struct pt_regs *regs, int nr)
{
if (likely(nr < NR_syscalls)) {
nr = array_index_nospec(nr, NR_syscalls);
regs->ax = sys_call_table[nr](regs);
return true;
}
return false;
}
array_index_nospec()는 Spectre v1 방어를 위한 투기적 실행 차단 함수입니다.
진입 경로 상세 다이어그램
새로운 시스템 콜 추가
단계별 가이드
리눅스 커널에 새로운 시스템 콜을 추가하는 과정을 단계별로 설명합니다. 아래는 sys_hello라는 예시 시스템 콜을 추가하는 과정입니다.
1단계: 시스템 콜 테이블에 번호 등록
# arch/x86/entry/syscalls/syscall_64.tbl 끝에 추가
# 번호는 마지막 항목 + 1
451 common hello sys_hello
2단계: 시스템 콜 핸들러 구현
/* kernel/sys_hello.c */
#include <linux/syscalls.h>
#include <linux/uaccess.h>
SYSCALL_DEFINE1(hello, char __user *, buf)
{
const char msg[] = "Hello from kernel!\n";
if (copy_to_user(buf, msg, sizeof(msg)))
return -EFAULT;
return sizeof(msg);
}
3단계: 헤더에 프로토타입 선언
/* include/linux/syscalls.h 에 추가 */
asmlinkage long sys_hello(char __user *buf);
4단계: Makefile에 오브젝트 추가
# kernel/Makefile
obj-y += sys_hello.o
5단계: 사용자 공간에서 호출
/* userspace test program */
#include <unistd.h>
#include <sys/syscall.h>
#include <stdio.h>
#define __NR_hello 451
int main(void)
{
char buf[64];
long ret = syscall(__NR_hello, buf);
if (ret > 0)
printf("%s", buf);
return 0;
}
clone → clone3, epoll_create → epoll_create1).
호환성 계층
compat_sys_* 래퍼
64비트 커널에서 32비트 사용자 프로그램을 실행할 때, 시스템 콜 인자의 크기가 다릅니다 (예: long이 4바이트 vs 8바이트). compat_sys_* 래퍼는 32비트 인자를 64비트로 안전하게 변환합니다.
/* fs/read_write.c */
COMPAT_SYSCALL_DEFINE3(read,
unsigned int, fd,
char __user *, buf,
compat_size_t, count)
{
return ksys_read(fd, buf, (size_t)count);
}
/* 타입 변환이 필요한 구조체 예시 */
struct compat_stat {
compat_dev_t st_dev; /* 32비트 dev_t */
compat_ino_t st_ino; /* 32비트 ino_t */
compat_off_t st_size; /* 32비트 off_t */
/* ... */
};
32비트 호환 모드 (IA-32 Emulation)
x86_64 Linux 커널은 CONFIG_IA32_EMULATION 옵션으로 32비트 프로그램의 int 0x80 및 sysenter 호출을 지원합니다. 32비트 시스템 콜은 별도의 테이블(ia32_sys_call_table)을 사용하며, 번호 체계도 다릅니다.
/* arch/x86/entry/entry_64_compat.S */
SYM_CODE_START(entry_SYSENTER_compat)
/* 32비트 sysenter 진입점 */
swapgs
/* 32비트 레지스터에서 인자 추출 */
movl %ebp, %r10d /* ebp → r10 (4번째 인자) */
...
call do_SYSENTER_32
SYM_CODE_END(entry_SYSENTER_compat)
| 항목 | 64비트 (native) | 32비트 (compat) |
|---|---|---|
| 호출 명령어 | SYSCALL |
int 0x80 / sysenter |
| 시스템 콜 테이블 | sys_call_table |
ia32_sys_call_table |
| 번호 헤더 | unistd_64.h |
unistd_32.h |
| write 번호 | 1 | 4 |
| 인자 전달 | rdi, rsi, rdx, r10, r8, r9 | ebx, ecx, edx, esi, edi, ebp |
vDSO (virtual Dynamic Shared Object)
개념과 목적
vDSO는 커널이 사용자 공간 프로세스의 주소 공간에 매핑하는 작은 공유 라이브러리입니다. 커널 모드 전환 없이 특정 시스템 콜을 사용자 공간에서 직접 실행할 수 있게 합니다. 이를 통해 gettimeofday(), clock_gettime(), getcpu() 등 자주 호출되는 시스템 콜의 오버헤드를 제거합니다.
# vDSO 매핑 확인
$ cat /proc/self/maps | grep vdso
7ffd3c5fe000-7ffd3c600000 r-xp 00000000 00:00 0 [vdso]
# vDSO에서 제공하는 함수 목록
$ objdump -T /proc/self/root/$(readlink /proc/self/exe) 2>/dev/null || \
LD_SHOW_AUXV=1 /bin/true | grep VDSO
$ vdso=$(dd if=/proc/self/mem bs=1 skip=$((0x7ffd3c5fe000)) count=8192 2>/dev/null | file -)
vDSO가 가속하는 함수들:
__vdso_clock_gettime()- 나노초 정밀도 시간 조회__vdso_gettimeofday()- 마이크로초 정밀도 시간 조회__vdso_clock_getres()- 시계 해상도 조회__vdso_getcpu()- 현재 CPU/NUMA 노드 조회__vdso_time()- 초 단위 시간 조회
vvar 페이지(읽기 전용)를 사용자 공간에 매핑합니다. 타이머 인터럽트마다 커널이 이 페이지를 갱신하고, vDSO 함수는 이 데이터를 직접 읽어 반환합니다. 자세한 내용은 ktime / Clock 심화 페이지를 참조하세요.
구현
/* arch/x86/entry/vdso/vclock_gettime.c (간략화) */
notrace int __vdso_clock_gettime(clockid_t clock,
struct timespec *ts)
{
/* 커널 진입 없이 vvar 페이지에서 시간 데이터 읽기 */
const struct vdso_data *vd = __arch_get_vdso_data();
if (vd->clock_mode != VDSO_CLOCKMODE_NONE) {
/* TSC 기반 고속 경로 */
u64 cycles = __arch_get_hw_counter(vd->clock_mode, vd);
ns = (cycles - vd->cycle_last) * vd->mult;
ns >>= vd->shift;
ts->tv_sec = vd->basetime[clock].sec + ns / NSEC_PER_SEC;
ts->tv_nsec = ns % NSEC_PER_SEC;
return 0;
}
/* TSC 불안정 시 실제 시스템 콜로 폴백 */
return clock_gettime_fallback(clock, ts);
}
vDSO vs vsyscall
| 항목 | vsyscall (레거시) | vDSO (현재) |
|---|---|---|
| 주소 | 고정 (0xFFFFFFFFFF600000) | ASLR 적용 (매 실행마다 변경) |
| 크기 | 1 페이지 | 2~4 페이지 |
| 보안 | ROP gadget 표적 (고정 주소) | ASLR로 보호 |
| 에뮬레이션 모드 | vsyscall=emulate (기본) |
해당 없음 |
| 함수 수 | 3개 (time, gettimeofday, getcpu) | 5개+ |
| 상태 | 레거시 (호환용만 유지) | 표준 |
vsyscall=none으로 완전히 비활성화할 수도 있습니다.
Seccomp (Secure Computing Mode)
seccomp-bpf 필터링
Seccomp은 프로세스가 호출할 수 있는 시스템 콜을 제한하는 리눅스 커널 보안 기능입니다. 원래 모드(strict)는 read, write, exit, sigreturn 4개만 허용했으나, seccomp-bpf(filter 모드)에서는 BPF 프로그램으로 세밀한 필터링이 가능합니다.
/* seccomp-bpf 필터 예제: write만 허용하는 필터 */
#include <linux/seccomp.h>
#include <linux/filter.h>
#include <sys/prctl.h>
struct sock_filter filter[] = {
/* 아키텍처 검증 */
BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
offsetof(struct seccomp_data, arch)),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64,
1, 0),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS),
/* 시스템 콜 번호 로드 */
BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
offsetof(struct seccomp_data, nr)),
/* write(1) 허용 */
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_write, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
/* exit_group(231) 허용 */
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_exit_group, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
/* 나머지 모두 거부 */
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS),
};
struct sock_fprog prog = {
.len = (unsigned short)(sizeof(filter) / sizeof(filter[0])),
.filter = filter,
};
/* 필터 설치 */
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog);
seccomp 필터의 반환값(action):
| 반환값 | 동작 |
|---|---|
SECCOMP_RET_ALLOW |
시스템 콜 허용 |
SECCOMP_RET_KILL_PROCESS |
프로세스 종료 (SIGSYS) |
SECCOMP_RET_KILL_THREAD |
해당 스레드만 종료 |
SECCOMP_RET_TRAP |
SIGSYS 시그널 전송 (핸들링 가능) |
SECCOMP_RET_ERRNO |
지정한 errno 반환 |
SECCOMP_RET_TRACE |
ptrace tracer에 통지 |
SECCOMP_RET_LOG |
허용하되 로그 기록 |
SECCOMP_RET_USER_NOTIF |
사용자 공간 알림 fd로 전달 |
컨테이너 보안과 seccomp
컨테이너 런타임(Docker, containerd 등)은 seccomp 프로파일을 사용하여 컨테이너 내부에서 위험한 시스템 콜을 차단합니다. Docker의 기본 프로파일은 약 44개의 시스템 콜을 차단합니다. 자세한 컨테이너 보안 설정은 Linux Containers 심화 페이지를 참조하세요.
# Docker 기본 seccomp 프로파일에서 차단되는 주요 시스템 콜
# (보안 위험이 높은 시스템 콜)
$ docker run --rm alpine cat /proc/1/status | grep Seccomp
Seccomp: 2 # 2 = SECCOMP_MODE_FILTER
# 차단되는 예시:
# - mount, umount2 : 파일시스템 조작
# - reboot : 호스트 재부팅
# - kexec_load : 커널 교체
# - init_module : 커널 모듈 로드
# - bpf : BPF 프로그램 로드
# - userfaultfd : 사용자 페이지 폴트 핸들링 (exploit primitive)
# 커스텀 seccomp 프로파일로 컨테이너 실행
$ docker run --security-opt seccomp=my-profile.json alpine sh
fork()/clone()으로 생성된 자식 프로세스는 부모의 필터를 상속하며, 추가 필터를 설치하면 기존 필터에 AND 조건으로 결합됩니다. 따라서 필터는 점점 더 제한적으로만 변경 가능합니다.
시스템 콜 추적과 디버깅
strace
strace는 프로세스의 시스템 콜 호출을 추적하는 사용자 공간 도구입니다. 내부적으로 ptrace() 시스템 콜을 사용하여 대상 프로세스의 syscall 진입/반환을 가로챕니다.
# 기본 추적: 시스템 콜과 반환값 출력
$ strace ls /tmp
execve("/usr/bin/ls", ["ls", "/tmp"], ...) = 0
openat(AT_FDCWD, "/tmp", O_RDONLY|O_DIRECTORY) = 3
getdents64(3, ..., 32768) = 240
write(1, "file1.txt\nfile2.log\n", 20) = 20
close(3) = 0
exit_group(0) = ?
# 시간 측정: 각 시스템 콜의 소요 시간
$ strace -T -e trace=read,write cat /dev/null
read(3, "", 131072) = 0 <0.000008>
# 통계 요약: 시스템 콜별 호출 횟수와 시간
$ strace -c ls /tmp
% time calls syscall
------ -------- --------
25.00 10 mmap
18.75 5 openat
12.50 5 close
6.25 3 read
6.25 1 write
# 특정 시스템 콜만 추적
$ strace -e trace=network nginx # 네트워크 관련만
$ strace -e trace=%file ls # 파일 관련만
$ strace -e trace=%process bash # 프로세스 관련만
ftrace 시스템 콜 tracepoint
ftrace의 시스템 콜 tracepoint는 ptrace보다 훨씬 가벼운 커널 내부 추적 메커니즘입니다. SYSCALL_METADATA 매크로가 생성한 tracepoint를 활용합니다.
# 시스템 콜 tracepoint 활성화
$ cd /sys/kernel/tracing
# 사용 가능한 시스템 콜 이벤트 확인
$ ls events/syscalls/ | head
sys_enter_read
sys_exit_read
sys_enter_write
sys_exit_write
# read 시스템 콜 진입/반환 추적
$ echo 1 > events/syscalls/sys_enter_read/enable
$ echo 1 > events/syscalls/sys_exit_read/enable
$ cat trace
<...>-1234 [002] .... 12345.678: sys_read(fd: 3, buf: 7ffd..., count: 4096)
<...>-1234 [002] .... 12345.679: sys_read -> 0x100
# 필터 적용: 특정 PID의 시스템 콜만
$ echo 'common_pid == 1234' > events/syscalls/sys_enter_write/filter
audit 서브시스템
Linux audit 프레임워크는 보안 감사를 위해 시스템 콜을 기록합니다. 시스템 콜 진입 경로의 syscall_enter_from_user_mode()에서 audit 검사가 수행됩니다.
# 파일 삭제 관련 시스템 콜 감사 규칙 추가
$ auditctl -a always,exit -F arch=b64 -S unlink,unlinkat,rename,renameat \
-F auid>=1000 -k file_delete
# 감사 로그 검색
$ ausearch -k file_delete -ts recent
type=SYSCALL msg=audit(1707000000.123:456): arch=c000003e syscall=263 \
success=yes exit=0 a0=ffffff9c a1=7ffd... auid=1000 uid=1000 \
comm="rm" exe="/usr/bin/rm"
# 실행 파일 실행 감사
$ auditctl -a always,exit -F arch=b64 -S execve -k exec_log
주요 시스템 콜 분류
Linux 시스템 콜은 기능에 따라 다음과 같이 분류됩니다. 관련 문서 페이지로의 교차 참조를 포함합니다.
| 카테고리 | 대표 시스템 콜 | 관련 페이지 |
|---|---|---|
| 프로세스 | fork, clone, clone3, execve, exit, wait4, getpid |
프로세스 관리 |
| 파일 I/O | open, read, write, close, lseek, pread64, pwrite64 |
VFS, inode |
| 메모리 | mmap, munmap, mprotect, brk, madvise, mlock |
메모리 관리 |
| 네트워크 | socket, bind, listen, accept, connect, sendto, recvfrom |
네트워크 스택 |
| IPC | pipe, shmget, semget, msgget, eventfd, signalfd |
|
| 시그널 | kill, rt_sigaction, rt_sigprocmask, sigaltstack |
프로세스 관리 |
| 시간 | clock_gettime, nanosleep, timer_create, timerfd_create |
ktime / Clock, 타이머 |
| 네임스페이스 | unshare, setns, clone (CLONE_NEW*) |
네임스페이스 |
| 보안 | prctl, seccomp, setuid, setgid, capset |
Linux Containers |
| 비동기 I/O | io_uring_setup, io_uring_enter, epoll_create1, select, poll |
성능 최적화 |
| 모듈/드라이버 | init_module, finit_module, delete_module, ioctl |
커널 모듈, 디바이스 드라이버 |
| BPF | bpf, perf_event_open |
BPF/XDP |
man 2 syscalls 명령어로 현재 시스템에서 지원하는 전체 시스템 콜 목록을 확인할 수 있습니다. 커널 소스의 include/linux/syscalls.h에 모든 시스템 콜의 프로토타입이 선언되어 있습니다.