서로 다른 시스템 콜을 어떻게 구분할 수 있을까요?
각 시스템 콜에 부여된 고유 번호(시스템 콜 번호)를 RAX 레지스터에 저장하고, 커널이 시스템 콜 테이블(sys_call_table)에서 해당 번호로 커널 함수를 조회하여 구분합니다.
구분 메커니즘 요약
사용자 프로세스: "read()를 호출하고 싶다"
│
▼
C 라이브러리: RAX = 0 (sys_read의 고유 번호)
│
▼
syscall 명령어 → 커널 모드 전환
│
▼
커널 디스패처: RAX 값 확인 → sys_call_table[0] 조회
│
▼
sys_read() 함수 실행
구분 과정 상세
1단계: 라이브러리가 시스템 콜 번호를 RAX에 저장
read(fd, buf, size) 호출 시 → RAX = 0
write(fd, buf, size) 호출 시 → RAX = 1
open(path, flags) 호출 시 → RAX = 2
fork() 호출 시 → RAX = 57
각 시스템 콜마다 고유 번호가 미리 정의되어 있음
2단계: syscall 명령어로 커널 진입
사용자 모드 커널 모드
┌──────────────────┐ ┌──────────────────────┐
│ RAX = 0 │ │ │
│ RDI = fd │ syscall │ system_call 핸들러 │
│ RSI = buf │ ────────→ │ 진입 │
│ RDX = size │ │ │
└──────────────────┘ └──────────────────────┘
3단계: 커널 디스패처가 RAX로 테이블 조회
시스템 콜 디스패처 (do_syscall)
│
├── RAX 범위 검사: 0 <= RAX < NR_syscalls ?
│ ├── Yes → 다음 단계
│ └── No → -ENOSYS 에러 반환 ("존재하지 않는 시스템 콜")
│
└── sys_call_table[RAX] → 커널 함수 주소 획득 → 호출
sys_call_table
┌────────┬───────────────────┐
│ 인덱스 │ 커널 함수 주소 │
├────────┼───────────────────┤
│ 0 │ → sys_read() │ ← RAX=0 이면 여기
│ 1 │ → sys_write() │ ← RAX=1 이면 여기
│ 2 │ → sys_open() │ ← RAX=2 이면 여기
│ 3 │ → sys_close() │
│ ... │ ... │
│ 57 │ → sys_fork() │ ← RAX=57 이면 여기
│ 59 │ → sys_execve() │
│ 60 │ → sys_exit() │
│ ... │ ... │
│ ~450 │ (Linux 6.x 기준) │
└────────┴───────────────────┘
커널 소스로 보는 구분 과정
// 1. 시스템 콜 번호 정의 (커널 헤더)
// include/uapi/asm-generic/unistd.h
#define __NR_read 0
#define __NR_write 1
#define __NR_open 2
#define __NR_close 3
#define __NR_fork 57
#define __NR_execve 59
#define __NR_exit 60
// 2. 시스템 콜 테이블 정의
// arch/x86/entry/syscall_64.c
const sys_call_ptr_t sys_call_table[] = {
[0] = sys_read,
[1] = sys_write,
[2] = sys_open,
[3] = sys_close,
// ...
[57] = sys_fork,
[59] = sys_execve,
[60] = sys_exit,
};
// 3. 디스패처 — RAX로 테이블 조회
// arch/x86/entry/common.c (간략화)
void do_syscall(struct pt_regs *regs) {
unsigned long nr = regs->rax; // ① RAX에서 번호 추출
if (nr < NR_syscalls) { // ② 범위 검사
regs->rax = sys_call_table[nr]( // ③ 테이블에서 함수 조회 후 호출
regs->rdi, // 첫 번째 인자
regs->rsi, // 두 번째 인자
regs->rdx, // 세 번째 인자
regs->r10, // 네 번째 인자
regs->r8, // 다섯 번째 인자
regs->r9 // 여섯 번째 인자
);
} else {
regs->rax = -ENOSYS; // 잘못된 번호 → 에러
}
}
전체 흐름 시각화
Java: fis.read(buf)
│
▼
JVM native: read0()
│
▼
glibc: read(3, buf, 100)
│
├── RAX = 0 ← 시스템 콜 번호로 구분
├── RDI = 3
├── RSI = buf 주소
├── RDX = 100
│
▼
syscall 명령어 → 커널 모드 전환
│
▼
커널 디스패처:
├── RAX = 0 확인
├── sys_call_table[0] 조회 → sys_read 주소
└── sys_read(3, buf, 100) 호출 ← 올바른 함수 실행
│
▼
처리 완료 → RAX = 100 (읽은 바이트) → sysret → 사용자 모드 복귀
다른 시스템 콜 호출 시 비교
| 호출 | RAX 값 | 테이블 조회 | 실행 함수 |
|---|---|---|---|
read(3, buf, 100) | 0 | sys_call_table[0] | sys_read() |
write(1, "hi", 2) | 1 | sys_call_table[1] | sys_write() |
open("/tmp/a", 0) | 2 | sys_call_table[2] | sys_open() |
fork() | 57 | sys_call_table[57] | sys_fork() |
exit(0) | 60 | sys_call_table[60] | sys_exit() |
결국 모든 시스템 콜의 구분은:
RAX 레지스터의 값
│
▼
sys_call_table[RAX]
│
▼
해당 커널 함수 실행
이 단순한 "번호 → 테이블 인덱싱" 방식으로 이루어집니다.