시스템 콜 테이블
커널이 각 시스템 콜에 고유 번호를 부여하고, 번호에 대응하는 커널 함수(핸들러) 주소를 매핑해 놓은 배열입니다.
왜 필요한가?
사용자 프로세스는 커널 함수의 주소를 직접 알 수 없고, 알더라도 보안상 직접 호출할 수 없습니다. 대신 번호만 전달하면 커널이 테이블에서 해당 핸들러를 찾아 실행합니다.
구조
시스템 콜 테이블 (sys_call_table)
┌────────┬──────────────────────┐
│ 번호 │ 커널 함수 주소 │
├────────┼──────────────────────┤
│ 0 │ → sys_read() │
│ 1 │ → sys_write() │
│ 2 │ → sys_open() │
│ 3 │ → sys_close() │
│ ... │ ... │
│ 39 │ → sys_getpid() │
│ 57 │ → sys_fork() │
│ 59 │ → sys_execve() │
│ 60 │ → sys_exit() │
│ ... │ ... │
│ ~450 │ (Linux 6.x 기준) │
└────────┴──────────────────────┘
시스템 콜 처리 흐름에서의 역할
① 사용자 프로세스: read(fd, buf, size) 호출
② C 라이브러리(glibc): 시스템 콜 번호(0)를 rax 레지스터에 저장
③ syscall 명령어 실행 → 커널 모드 전환
④ 커널: rax 값으로 sys_call_table[0] 조회
⑤ sys_read() 함수 실행
⑥ 결과를 rax에 저장 후 사용자 모드 복귀
사용자 프로세스 C 라이브러리 커널
┌──────────┐ ┌──────────────┐ ┌─────────────────────┐
│ read() │──→ │ rax = 0 │──→ │ sys_call_table[rax] │
│ 호출 │ │ rdi = fd │ │ │ │
│ │ │ rsi = buf │ │ ▼ │
│ │ │ rdx = size │ │ sys_read() 실행 │
│ │ │ syscall 실행 │ │ │ │
│ 결과 수신│ ←────│ │←────│ 결과 반환 │
└──────────┘ └──────────────┘ └─────────────────────┘
레지스터 규약 (x86-64 Linux)
| 레지스터 | 용도 |
|---|---|
| rax | 시스템 콜 번호 (입력) / 반환값 (출력) |
| rdi | 첫 번째 인자 |
| rsi | 두 번째 인자 |
| rdx | 세 번째 인자 |
| r10 | 네 번째 인자 |
| r8 | 다섯 번째 인자 |
| r9 | 여섯 번째 인자 |
예시: read(fd=3, buf=0x7fff1000, size=100)
rax = 0 ← 시스템 콜 번호 (sys_read)
rdi = 3 ← fd
rsi = 0x7fff1000 ← buf
rdx = 100 ← size
syscall ← 커널 모드 전환
IDT vs 시스템 콜 테이블
두 테이블은 역할이 다르며, 시스템 콜 처리 시 순차적으로 사용됩니다.
| 항목 | IDT | 시스템 콜 테이블 |
|---|---|---|
| 매핑 | 벡터 번호 → 인터럽트/예외 핸들러 | 시스템 콜 번호 → 커널 함수 |
| 용도 | 모든 인터럽트·예외 처리 | 시스템 콜 전용 |
| 엔트리 수 | 256개 | ~450개 (Linux 6.x) |
| 사용 시점 | 인터럽트 발생 직후 | 시스템 콜 핸들러 진입 후 |
syscall 명령어 실행
│
▼
IDT (또는 MSR) → system_call 핸들러 진입 ← 1단계: "시스템 콜이구나"
│
▼
sys_call_table[rax] → 구체적인 커널 함수 ← 2단계: "어떤 시스템 콜인지"
│
▼
sys_read(), sys_write() 등 실행
Linux 커널 소스에서의 실제 구현
// arch/x86/entry/syscall_64.c (Linux 커널 소스 간략화)
typedef long (*sys_call_ptr_t)(unsigned long, unsigned long,
unsigned long, unsigned long,
unsigned long, unsigned long);
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,
// ...
};
// 시스템 콜 디스패처 (간략화)
void do_syscall(struct pt_regs *regs) {
unsigned long nr = regs->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에서의 흐름
FileInputStream fis = new FileInputStream("/tmp/test.txt");
int data = fis.read();
Java: fis.read()
→ JVM native: read0()
→ glibc: read(fd, buf, 1)
→ rax = 0 (sys_read 번호), syscall 실행
→ 커널: sys_call_table[0] → sys_read() 실행
→ 디스크에서 데이터 읽기
→ 결과 반환 → 사용자 모드 복귀