시스템 콜 테이블

커널이 각 시스템 콜에 고유 번호를 부여하고, 번호에 대응하는 커널 함수(핸들러) 주소를 매핑해 놓은 배열입니다.

왜 필요한가?

사용자 프로세스는 커널 함수의 주소를 직접 알 수 없고, 알더라도 보안상 직접 호출할 수 없습니다. 대신 번호만 전달하면 커널이 테이블에서 해당 핸들러를 찾아 실행합니다.

구조

시스템 콜 테이블 (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() 실행
          → 디스크에서 데이터 읽기
        → 결과 반환 → 사용자 모드 복귀

CSLOG에서 보기