vDSO (Virtual Dynamic Shared Object) 심화

vDSO는 리눅스 커널이 유저 공간 프로세스의 주소 공간에 자동으로 매핑하는 작은 공유 라이브러리입니다. clock_gettime(), gettimeofday(), time(), getcpu() 같은 빈번히 호출되는 시스템 콜을 커널 모드 전환 없이 유저 공간에서 직접 실행할 수 있게 하여 수십 나노초의 오버헤드를 제거합니다. 이 문서는 vDSO의 내부 아키텍처, 커널 구현, 아키텍처별 차이, 성능 특성, 디버깅 기법까지 종합적으로 다룹니다.

전제 조건: 시스템 콜 (System Call)ELF (Executable and Linkable Format) 문서를 먼저 읽으세요. vDSO를 이해하려면 시스템 콜의 커널 모드 전환 비용과 ELF 공유 라이브러리의 동적 링킹 메커니즘에 대한 기본 지식이 필요합니다.
일상 비유: vDSO는 은행 ATM과 비슷합니다. 매번 은행 창구(커널)에 가서 줄을 서는 대신, 로비에 설치된 ATM(vDSO)에서 잔액 조회(시간 읽기) 같은 간단한 작업을 즉시 처리할 수 있습니다. ATM에 표시되는 잔액 정보는 은행 시스템(커널)이 주기적으로 업데이트하므로 항상 최신 상태를 유지합니다.

핵심 요약

  • vDSO -- 커널이 유저 공간에 매핑하는 가상 공유 라이브러리. 시스템 콜 없이 커널 데이터에 접근 가능
  • vvar -- 커널이 업데이트하고 유저 공간이 읽기 전용으로 접근하는 공유 데이터 페이지
  • AT_SYSINFO_EHDR -- ELF auxiliary vector를 통해 전달되는 vDSO의 기저 주소
  • vsyscall -- vDSO의 전신. 고정 주소 사용으로 ASLR 불가, 보안 취약점 존재
  • seqcount -- vvar 데이터의 일관성을 보장하는 순서 잠금 메커니즘

단계별 이해

  1. 프로세스가 시작됨
    커널의 execve() 경로에서 ELF 바이너리를 로드한 후, arch_setup_additional_pages()가 vDSO 이미지를 프로세스 주소 공간에 매핑합니다.
  2. vDSO 주소가 전달됨
    커널은 ELF auxiliary vector의 AT_SYSINFO_EHDR 엔트리에 vDSO 기저 주소를 기록합니다. 동적 링커(ld.so)가 이를 읽고 vDSO 심볼을 연결합니다.
  3. glibc가 vDSO 함수를 사용
    clock_gettime()을 호출하면 glibc는 vDSO의 __vdso_clock_gettime()을 직접 호출합니다. 시스템 콜 진입 없이 vvar 페이지에서 시간 데이터를 읽습니다.
  4. 커널이 vvar를 갱신
    타이머 인터럽트나 timekeeping 업데이트 시 커널은 vvar 페이지의 vdso_data 구조체를 갱신합니다. seqcount로 원자적 일관성을 보장합니다.

vDSO 개요와 탄생 배경

시스템 콜 오버헤드 문제

리눅스에서 유저 공간 프로세스가 커널 서비스를 요청하려면 시스템 콜을 통해 커널 모드로 전환해야 합니다. x86_64에서 SYSCALL 명령어를 통한 모드 전환은 다음과 같은 비용을 수반합니다:

이러한 오버헤드는 단일 시스템 콜당 약 100~300 나노초(KPTI 활성 시)에 달합니다. gettimeofday()clock_gettime()처럼 초당 수백만 번 호출되는 함수에서는 이 비용이 전체 성능에 심각한 영향을 미칩니다.

vsyscall의 등장과 한계

리눅스 커널 2.5 시대에 x86 아키텍처에서 vsyscall 메커니즘이 도입되었습니다. 고정된 가상 주소 0xffffffffff600000에 특수 페이지를 매핑하여 gettimeofday(), time(), getcpu() 3개 함수를 시스템 콜 없이 호출할 수 있게 했습니다.

그러나 vsyscall에는 근본적인 보안 문제가 있었습니다:

vDSO의 탄생

커널 2.6.x에서 vDSO(Virtual Dynamic Shared Object)가 vsyscall의 후속으로 도입되었습니다. vDSO는 표준 ELF 공유 라이브러리 형식을 사용하므로 동적 링커가 자연스럽게 처리할 수 있고, ASLR을 완전히 지원합니다. 커널이 프로세스 생성 시 무작위 주소에 vDSO를 매핑하므로 ROP 공격에 대한 방어가 가능합니다.

특성vsyscall (레거시)vDSO
매핑 주소고정 (0xffffffffff600000)ASLR로 무작위
형식특수 페이지표준 ELF shared object
함수 수3개 고정아키텍처별 확장 가능
보안ROP 가젯 위험ASLR 완전 지원
아키텍처x86_64 전용x86, ARM64, ARM, RISC-V, MIPS, PowerPC 등
glibc 통합하드코딩 주소AT_SYSINFO_EHDR + ELF 심볼 조회
커널 버전2.5+2.6+
참고: 최신 커널에서 vsyscall 페이지는 기본적으로 에뮬레이션 모드(vsyscall=emulate)로 동작합니다. 레거시 호환성을 위해 vsyscall 주소에 접근하면 실제 시스템 콜로 대체 실행되며, vsyscall=none으로 완전히 비활성화할 수도 있습니다.

vDSO 아키텍처

전체 구조

vDSO 메커니즘은 커널 측과 유저 측의 긴밀한 협력으로 작동합니다. 커널은 두 종류의 특수 페이지를 프로세스 주소 공간에 매핑합니다:

  1. vDSO 코드 페이지 -- 실행 가능한 ELF 공유 라이브러리 이미지. __vdso_clock_gettime() 등의 함수 코드를 포함
  2. vvar 데이터 페이지 -- 읽기 전용 데이터 페이지. 커널이 갱신하는 시간 데이터(vdso_data), 아키텍처별 데이터 등을 포함

유저 공간에서 vDSO 함수를 호출하면, 함수는 vvar 페이지의 데이터를 읽어 결과를 계산합니다. 커널 모드 전환이 전혀 발생하지 않으므로 시스템 콜 대비 10~50배 빠릅니다.

프로세스 가상 주소 공간 .text / .data / .bss (프로그램 코드 및 데이터) Heap (brk/mmap) libc.so, libpthread.so, ... vvar 페이지 (읽기 전용) vdso_data: 시간, 클럭 데이터 vDSO 코드 페이지 (실행 가능) __vdso_clock_gettime, __vdso_gettimeofday, ... Stack [vsyscall] 0xffffffffff600000 (레거시, 에뮬레이션 모드) 커널 공간 vdso_image ELF 바이너리 (빌드 시 생성, .rodata) vdso_data (커널 변수) seq, clock_mode, cycle_last, mult, shift, wall_time_*, ... timekeeping 서브시스템 update_vsyscall() 호출 하드웨어 클럭 TSC, HPET, ACPI PM Timer arch_setup_additional_pages() execve() 시 vDSO/vvar 매핑 mmap (읽기/실행) mmap (읽기전용)

vDSO 함수 호출 흐름

유저 공간에서 clock_gettime(CLOCK_MONOTONIC, &ts)를 호출하면 다음 경로를 따릅니다:

  1. glibc의 clock_gettime() 래퍼가 vDSO의 __vdso_clock_gettime() 함수 포인터를 호출
  2. vDSO 함수가 vvar 페이지의 vdso_data에서 seq(seqcount)를 읽음
  3. 현재 TSC 값을 RDTSC 명령어로 읽음
  4. vdso_datacycle_last, mult, shift를 사용하여 경과 시간 계산
  5. wall_time_sec, wall_time_snsec에 경과 시간을 더하여 최종 시간 산출
  6. seq 값이 변경되지 않았는지 확인 (변경되었으면 1단계부터 재시도)
  7. 결과를 struct timespec에 저장하고 반환
핵심: 전체 과정에서 SYSCALL/SYSRET 명령어가 실행되지 않습니다. 유저 모드(Ring 3)에서 완전히 실행되므로 커널 모드 전환 비용이 0입니다. vDSO가 지원하지 않는 클럭 ID(예: CLOCK_PROCESS_CPUTIME_ID)의 경우에만 실제 시스템 콜로 폴백합니다.

vsyscall과 vDSO 비교

vsyscall 페이지의 내부 구조

vsyscall은 x86_64에서만 존재하는 레거시 메커니즘입니다. 커널은 고정 가상 주소 0xffffffffff600000에 1페이지(4KB)를 매핑하고, 정확히 3개의 함수 슬롯을 배치했습니다:

가상 주소함수오프셋
0xffffffffff600000gettimeofday()+0x000
0xffffffffff600400time()+0x400
0xffffffffff600800getcpu()+0x800

이 주소는 모든 프로세스에서 동일합니다. 공격자가 메모리 취약점을 이용하여 실행 흐름을 조작할 때, vsyscall 페이지의 RET 명령어 등을 ROP 가젯으로 활용할 수 있었습니다.

vsyscall 에뮬레이션 모드

현대 커널은 vsyscall 페이지를 3가지 모드로 제어합니다:

커널 파라미터CONFIG 옵션동작보안 수준
vsyscall=emulate CONFIG_LEGACY_VSYSCALL_EMULATE vsyscall 주소 접근 시 페이지 폴트 발생 -> 커널이 실제 시스템 콜로 에뮬레이션 중간 (기본값)
vsyscall=xonly CONFIG_LEGACY_VSYSCALL_XONLY 실행만 가능, 읽기 불가 (데이터 누출 방지) 높음
vsyscall=none CONFIG_LEGACY_VSYSCALL_NONE vsyscall 페이지 완전 제거. 접근 시 SIGSEGV 최고
주의: vsyscall=none을 사용하면 매우 오래된 정적 링크 바이너리(glibc 2.13 이전)가 작동하지 않을 수 있습니다. 현대 시스템에서는 대부분 vDSO를 사용하므로 문제가 없지만, 레거시 환경에서는 vsyscall=emulate(기본값)을 유지해야 합니다.

보안 관점에서의 비교

보안 속성vsyscallvDSO
ASLR불가 (고정 주소)완전 지원 (mmap 무작위화)
NX (No Execute)전체 페이지 실행 가능코드/데이터 분리된 ELF 세그먼트
ROP 가젯 활용쉬움 (주소 예측 가능)어려움 (주소 무작위)
정보 누출가능 (emulate 모드 제외)ELF 메타데이터만 노출
seccomp 필터링에뮬레이션 모드에서만 가능폴백 시스템 콜에 대해 가능

ELF Auxiliary Vector와 vDSO

Auxiliary Vector 개요

ELF auxiliary vector는 커널이 execve() 시 유저 공간 스택에 배치하는 키-값 쌍 배열입니다. 동적 링커(ld-linux-x86-64.so.2)와 C 라이브러리가 런타임 환경 정보를 얻기 위해 사용합니다. vDSO와 관련된 주요 항목은 다음과 같습니다:

키 (AT_*)정의 위치설명
AT_SYSINFO_EHDR (33) vDSO ELF 헤더 주소 include/uapi/linux/auxvec.h vDSO의 ELF 헤더 (Ehdr) 시작 주소. glibc/musl이 이 값으로 vDSO를 파싱
AT_SYSINFO (32) vDSO 진입점 (x86_32) include/uapi/linux/auxvec.h i386에서 __kernel_vsyscall 주소. x86_64에서는 미사용
AT_HWCAP (16) 하드웨어 기능 비트맵 include/uapi/linux/auxvec.h CPU 기능 플래그. vDSO가 TSC 사용 여부를 판단하는 데 간접 활용
AT_CLKTCK (17) 클럭 틱 빈도 include/uapi/linux/auxvec.h USER_HZ 값 (보통 100). times() 시스템 콜의 해상도

커널의 auxiliary vector 설정

execve() 경로에서 create_elf_tables() 함수가 auxiliary vector를 구성합니다. vDSO 관련 엔트리는 다음과 같이 설정됩니다:

/* fs/binfmt_elf.c - create_elf_tables() */
static int create_elf_tables(struct linux_binprm *bprm,
                              const struct elfhdr *exec,
                              unsigned long interp_load_addr,
                              unsigned long e_entry,
                              struct elfhdr *interp_elf_ex)
{
    unsigned long vdso_base = current->mm->context.vdso;

    /* ... 다른 auxiliary 항목 설정 ... */

    /* vDSO 기저 주소를 AT_SYSINFO_EHDR로 전달 */
    if (vdso_base) {
        NEW_AUX_ENT(AT_SYSINFO_EHDR, vdso_base);
    }

#ifdef CONFIG_X86_32
    /* i386: __kernel_vsyscall 진입점 주소 */
    NEW_AUX_ENT(AT_SYSINFO,
                (unsigned long)VDSO32_SYMBOL(vdso_base, vsyscall));
#endif

    NEW_AUX_ENT(AT_HWCAP, ELF_HWCAP);
    NEW_AUX_ENT(AT_CLKTCK, CLOCKS_PER_SEC);
    /* ... */
}
코드 설명
  • 7행 current->mm->context.vdso에서 현재 프로세스의 vDSO 기저 주소를 가져옵니다. 이 값은 arch_setup_additional_pages()에서 설정됩니다.
  • 12-14행 AT_SYSINFO_EHDR 엔트리에 vDSO ELF 헤더 주소를 기록합니다. 동적 링커(ld.so)가 이 주소를 사용하여 vDSO 심볼을 조회합니다.
  • 17-19행 i386(x86 32비트)에서는 추가로 AT_SYSINFO__kernel_vsyscall 진입점 주소를 제공합니다. 이는 SYSENTER 기반 시스템 콜 진입에 사용됩니다.

유저 공간에서 auxiliary vector 읽기

#include <sys/auxv.h>
#include <stdio.h>

int main(void)
{
    unsigned long vdso_ehdr = getauxval(AT_SYSINFO_EHDR);
    if (vdso_ehdr)
        printf("vDSO base: %#lx\n", vdso_ehdr);
    else
        printf("vDSO not present\n");

    /* AT_HWCAP: CPU 기능 플래그 */
    unsigned long hwcap = getauxval(AT_HWCAP);
    printf("HWCAP: %#lx\n", hwcap);

    return 0;
}
execve() load_elf_binary() ELF 파싱, 세그먼트 매핑 arch_setup_additional_pages() vDSO + vvar 매핑 mm->context.vdso 설정 create_elf_tables() auxiliary vector 구성 유저 스택 (execve 후) argc, argv[], envp[] AT_SYSINFO_EHDR = 0x7ffXXXXXX000 (vDSO 기저 주소) AT_HWCAP, AT_CLKTCK, AT_PAGESZ, AT_PHDR, ... AT_NULL (종료 마커) ld.so vDSO 심볼 조회

vvar 페이지 구조

vdso_data 구조체

vvar 페이지의 핵심은 struct vdso_data 구조체입니다. 커널의 timekeeping 서브시스템이 이 구조체를 갱신하면, 유저 공간의 vDSO 함수가 동일한 물리 페이지를 읽기 전용으로 접근합니다. 아키텍처별로 약간의 차이가 있지만, 공통 구조는 다음과 같습니다:

/* include/vdso/datapage.h */
struct vdso_timestamp {
    u64    sec;            /* 초 (seconds) */
    u64    nsec;           /* 시프트된 나노초 (shifted nanoseconds) */
};

struct vdso_data {
    u32                  seq;             /* seqcount: 홀수면 업데이트 중 */
    s32                  clock_mode;      /* 클럭 모드: VDSO_CLOCKMODE_* */
    u64                  cycle_last;      /* 마지막 읽은 카운터 값 */
    u64                  mask;            /* 카운터 마스크 */
    u32                  mult;            /* 주기 -> 나노초 곱셈 계수 */
    u32                  shift;           /* 정밀도 시프트 */

    union {
        struct vdso_timestamp  basetime[VDSO_BASES];
        struct timens_offset   offset[VDSO_BASES];
    };

    s32                  tz_minuteswest;  /* 시간대 (서쪽 분 단위) */
    s32                  tz_dsttime;      /* DST 타입 */
    u32                  hrtimer_res;     /* hrtimer 해상도 (ns) */
    u32                  __unused;
};
코드 설명
  • 8행 seq 필드는 seqcount 잠금입니다. 커널이 데이터를 갱신할 때 홀수로 변경하고, 완료 후 짝수로 되돌립니다. 유저 공간은 읽기 전후로 이 값을 비교하여 일관성을 검증합니다.
  • 9행 clock_mode는 vDSO가 사용해야 할 클럭소스를 지정합니다. VDSO_CLOCKMODE_TSC(TSC 사용), VDSO_CLOCKMODE_NONE(vDSO 비활성 -> 시스템 콜 폴백) 등이 있습니다.
  • 10행 cycle_last는 커널이 마지막으로 읽은 TSC 카운터 값입니다. vDSO는 현재 TSC 값에서 이를 빼서 경과 사이클을 계산합니다.
  • 12-13행 multshift는 사이클을 나노초로 변환하는 공식 ns = (cycles * mult) >> shift에 사용됩니다.
  • 16행 basetime[] 배열은 각 클럭 유형(CLOCK_REALTIME, CLOCK_MONOTONIC, CLOCK_BOOTTIME 등)의 기저 시간을 저장합니다.

클럭 모드 (VDSO_CLOCKMODE)

모드의미사용 조건
VDSO_CLOCKMODE_NONE 0 vDSO 비활성, 시스템 콜로 폴백 clocksource가 vDSO 미지원 시
VDSO_CLOCKMODE_TSC 1 TSC 기반 고속 경로 x86에서 TSC가 안정적인 경우
VDSO_CLOCKMODE_HRES 2 고해상도 클럭 (ARM64 등) 아키텍처별 카운터 사용
VDSO_CLOCKMODE_TIMENS 0x7fffffff Time Namespace 오프셋 적용 컨테이너의 시간 격리 사용 시
vvar 페이지 레이아웃 vdso_data[0] (CS_HRES_COARSE) seq: u32 (seqcount) clock_mode: s32 (VDSO_CLOCKMODE_TSC) cycle_last: u64, mask: u64, mult: u32, shift: u32 basetime[CLOCK_REALTIME]: {sec, nsec} basetime[CLOCK_MONOTONIC]: {sec, nsec} basetime[CLOCK_BOOTTIME]: {sec, nsec} ... vdso_data[1] (CS_RAW) CLOCK_MONOTONIC_RAW 전용 데이터 tz_minuteswest, tz_dsttime hrtimer_res (나노초) timens_offset[] (Time Namespace, 선택) update_vsyscall() timekeeping -> vdso_data seqcount 프로토콜 1. seq++ (홀수 = 쓰기 중) 2. 데이터 갱신 3. seq++ (짝수 = 완료) 유저 공간 읽기 1. s = READ_ONCE(seq) 2. 데이터 읽기 + 시간 계산 3. s != READ_ONCE(seq)? 재시도

vvar 페이지의 물리 매핑

커널은 vvar 데이터를 위해 물리 페이지를 할당하고, 이를 커널 주소 공간과 유저 주소 공간 양쪽에 매핑합니다. 커널 측에서는 쓰기가 가능하고, 유저 측에서는 읽기 전용입니다. 이는 vm_fault 핸들러를 통해 구현됩니다:

/* arch/x86/entry/vdso/vma.c */
static vm_fault_t vvar_fault(const struct vm_special_mapping *sm,
                            struct vm_area_struct *vma,
                            struct vm_fault *vmf)
{
    if (vmf->pgoff == 0) {
        /* 페이지 0: vdso_data (timekeeping 데이터) */
        return vmf_insert_pfn(vma, vmf->address,
                __pa_symbol(&vdso_data) >> PAGE_SHIFT);
    }
#ifdef CONFIG_TIME_NS
    if (vmf->pgoff == VVAR_TIMENS_PAGE_OFFSET) {
        /* Time Namespace 오프셋 페이지 */
        struct timens_offset *timens;
        timens = get_timens_vvar_page(vma->vm_mm);
        if (!timens)
            return VM_FAULT_SIGBUS;
        return vmf_insert_pfn(vma, vmf->address,
                page_to_pfn(virt_to_page(timens)));
    }
#endif
    return VM_FAULT_SIGBUS;
}

clock_gettime 고속 경로

__vdso_clock_gettime 구현

clock_gettime()의 vDSO 구현은 커널 소스의 lib/vdso/gettimeofday.c에 있습니다. 이 파일은 아키텍처 독립적인 공통 구현으로, 빌드 시 vDSO 이미지에 포함됩니다. 핵심 로직은 다음과 같습니다:

/* lib/vdso/gettimeofday.c */
static int do_hres(const struct vdso_data *vd,
                    clockid_t clk,
                    struct __kernel_timespec *ts)
{
    const struct vdso_timestamp *vdso_ts = &vd->basetime[clk];
    u64 cycles, ns;
    u32 seq;

    do {
        /* 1. seqcount 읽기 시작: 짝수가 될 때까지 스핀 */
        seq = vdso_read_begin(vd);

        /* 2. 클럭 모드 확인 */
        if (unlikely(vd->clock_mode == VDSO_CLOCKMODE_NONE))
            return -1;  /* 시스템 콜로 폴백 */

        /* 3. 현재 하드웨어 카운터 읽기 (x86: RDTSC) */
        cycles = __arch_get_hw_counter(vd->clock_mode, vd);

        /* 4. 경과 나노초 계산: (cycles - cycle_last) * mult >> shift */
        ns = vdso_ts->nsec;
        ns += vdso_calc_delta(cycles, vd->cycle_last, vd->mask, vd->mult);
        ns >>= vd->shift;

        /* 5. 초 단위 오버플로 처리 */
        ts->tv_sec = vdso_ts->sec;
        ts->tv_nsec = ns;
        if (ns >= NSEC_PER_SEC) {
            ts->tv_sec += ns / NSEC_PER_SEC;
            ts->tv_nsec = ns % NSEC_PER_SEC;
        }

    /* 6. seqcount 검증: 변경되었으면 재시도 */
    } while (vdso_read_retry(vd, seq));

    return 0;
}
코드 설명
  • 11행 vdso_read_begin()seq 값을 읽고, 홀수(쓰기 중)이면 짝수가 될 때까지 스핀합니다. 이는 커널의 read_seqcount_begin()과 동등합니다.
  • 14-15행 clock_modeVDSO_CLOCKMODE_NONE이면 vDSO가 비활성 상태입니다. 이 경우 -1을 반환하여 호출자가 실제 시스템 콜로 폴백하게 합니다. clocksource가 TSC에서 HPET로 전환된 경우 등에 발생합니다.
  • 18행 __arch_get_hw_counter()는 아키텍처별 하드웨어 카운터를 읽습니다. x86에서는 RDTSC 또는 RDTSCP 명령어를 실행합니다.
  • 21-22행 vdso_calc_delta()(cycles - cycle_last) & mask로 경과 사이클을 구하고, * mult를 곱합니다. nsec는 사전 시프트된 나노초 값이므로 전체에 >> shift를 적용합니다.
  • 33행 vdso_read_retry()는 현재 seq 값과 시작 시 읽은 값을 비교합니다. 다르면 커널이 중간에 데이터를 갱신한 것이므로 전체 루프를 재시도합니다.
clock_gettime(CLOCK_MONOTONIC, &ts) glibc: __vdso_clock_gettime seq = vdso_read_begin() clock_mode? NONE: syscall 폴백 TSC cycles = RDTSC/RDTSCP ns = basetime.nsec + (delta * mult) >> shift seq 변경? 재시도 결과 반환 (0)

seqcount 동작 원리

vDSO에서 seqcount는 락 없이 읽기 일관성을 보장하는 핵심 메커니즘입니다. 커널(쓰기자)과 유저 공간(읽기자)의 프로토콜은 다음과 같습니다:

/* 커널 측 (쓰기) - kernel/time/vsyscall.c */
void update_vsyscall(struct timekeeper *tk)
{
    struct vdso_data *vdata = __arch_get_k_vdso_data();

    vdso_write_begin(vdata);          /* seq++ (홀수: 쓰기 시작) */
    smp_wmb();                         /* 메모리 배리어 */

    vdata->clock_mode    = cycl->vdso_clock_mode;
    vdata->cycle_last    = tk->tkr_mono.cycle_last;
    vdata->mult          = tk->tkr_mono.mult;
    vdata->shift         = tk->tkr_mono.shift;
    vdata->mask          = tk->tkr_mono.mask;
    /* ... basetime 갱신 ... */

    smp_wmb();                         /* 메모리 배리어 */
    vdso_write_end(vdata);             /* seq++ (짝수: 쓰기 완료) */
}

/* 유저 측 (읽기) - vDSO 코드 */
static inline u32 vdso_read_begin(const struct vdso_data *vd)
{
    u32 seq;
    while ((seq = READ_ONCE(vd->seq)) & 1)
        cpu_relax();    /* 홀수면 쓰기 중 - 스핀 대기 */
    smp_rmb();              /* 읽기 배리어 */
    return seq;
}

static inline bool vdso_read_retry(const struct vdso_data *vd, u32 start)
{
    smp_rmb();              /* 읽기 배리어 */
    return READ_ONCE(vd->seq) != start;
}
성능 특성: seqcount는 읽기 측에서 락을 전혀 취득하지 않습니다. 커널의 쓰기 갱신은 타이머 인터럽트(보통 초당 100~1000회)에서만 발생하므로, 읽기 측의 재시도는 극히 드뭅니다. 대부분의 경우 단일 반복으로 완료됩니다.

vDSO가 지원하는 클럭 ID

클럭 IDvDSO 고속 경로설명
CLOCK_REALTIME지원 (고해상도)UTC 벽시계 시간
CLOCK_MONOTONIC지원 (고해상도)단조 증가 시간 (절전 제외)
CLOCK_BOOTTIME지원 (고해상도)부팅 이후 시간 (절전 포함)
CLOCK_REALTIME_COARSE지원 (저해상도)저해상도 UTC (tick 기반)
CLOCK_MONOTONIC_COARSE지원 (저해상도)저해상도 단조 증가 (tick 기반)
CLOCK_MONOTONIC_RAW지원 (CS_RAW)NTP 보정 없는 원시 클럭
CLOCK_TAI지원 (고해상도)국제 원자 시간 (윤초 미적용)
CLOCK_PROCESS_CPUTIME_ID미지원 (syscall 폴백)프로세스 CPU 시간
CLOCK_THREAD_CPUTIME_ID미지원 (syscall 폴백)스레드 CPU 시간

gettimeofday 가속

__vdso_gettimeofday 구현

gettimeofday()의 vDSO 구현은 clock_gettime()의 결과를 struct timeval 형식으로 변환합니다. 내부적으로 do_hres()를 재사용하되, 나노초를 마이크로초로 변환하는 단계가 추가됩니다.

/* lib/vdso/gettimeofday.c */
static __always_inline int
__cvdso_gettimeofday_data(const struct vdso_data *vd,
                         struct __kernel_old_timeval *tv,
                         struct timezone *tz)
{
    if (likely(tv != NULL)) {
        struct __kernel_timespec ts;

        /* CLOCK_REALTIME의 고속 경로 시도 */
        if (do_hres(vd, CLOCK_REALTIME, &ts))
            return gettimeofday_fallback(tv, tz);

        /* 나노초 -> 마이크로초 변환 */
        tv->tv_sec  = ts.tv_sec;
        tv->tv_usec = (u32)ts.tv_nsec / NSEC_PER_USEC;
    }

    if (unlikely(tz != NULL)) {
        /* 시간대 정보는 vvar에서 직접 읽기 */
        tz->tz_minuteswest = vd->tz_minuteswest;
        tz->tz_dsttime     = vd->tz_dsttime;
    }

    return 0;
}
코드 설명
  • 11-12행 do_hres()가 -1을 반환하면(vDSO 비활성) gettimeofday_fallback()이 실제 __NR_gettimeofday 시스템 콜을 수행합니다.
  • 15-16행 timespec(나노초)에서 timeval(마이크로초)로 변환합니다. NSEC_PER_USEC은 1000입니다.
  • 19-22행 시간대(timezone) 정보는 seqcount 보호 없이 직접 읽습니다. 이 값은 거의 변경되지 않으며, 약간의 불일치가 허용됩니다.

gettimeofday vs clock_gettime 선택 가이드

속성gettimeofday()clock_gettime()
해상도마이크로초 (usec)나노초 (nsec)
클럭 선택CLOCK_REALTIME 고정임의 클럭 ID 지정 가능
POSIXPOSIX.1-2001 (deprecated 권고)POSIX.1-2001 (권장)
vDSO 성능동일 (내부적으로 동일 경로)동일
시간대tz 매개변수로 제공미제공 (별도 localtime 필요)
권장: 새로운 코드에서는 clock_gettime()을 사용하세요. 나노초 해상도를 제공하고, CLOCK_MONOTONIC 등 용도에 맞는 클럭을 선택할 수 있습니다. gettimeofday()는 레거시 호환성을 위해서만 유지되며, POSIX에서 향후 제거가 논의되고 있습니다.

time과 getcpu

__vdso_time 구현

time()은 현재 시간을 초 단위로 반환하는 가장 간단한 시간 함수입니다. vDSO 구현은 CLOCK_REALTIME의 coarse(저해상도) 경로를 사용합니다:

/* lib/vdso/gettimeofday.c */
static __always_inline __kernel_old_time_t
__cvdso_time_data(const struct vdso_data *vd, __kernel_old_time_t *t)
{
    __kernel_old_time_t sec;
    const struct vdso_timestamp *vdso_ts;

    /* coarse 데이터에서 초만 읽기 - TSC 불필요 */
    vdso_ts = &vd->basetime[CLOCK_REALTIME];
    sec = (__kernel_old_time_t)READ_ONCE(vdso_ts->sec);

    if (t)
        *t = sec;
    return sec;
}

time()은 초 단위만 필요하므로 seqcount 루프조차 필요하지 않습니다. READ_ONCE()로 단일 원자적 읽기만 수행하며, 이는 약 1~5 나노초 수준의 극도로 빠른 성능을 보입니다.

__vdso_getcpu 구현

getcpu()는 현재 스레드가 실행 중인 CPU 번호와 NUMA 노드를 반환합니다. x86에서는 RDTSCP 또는 LSL(Load Segment Limit) 명령어를 사용하여 GDT 엔트리에 인코딩된 CPU/노드 정보를 읽습니다:

/* arch/x86/include/asm/vdso/getcpu.h */
static inline unsigned long __getcpu(void)
{
    unsigned long cpu;

    /*
     * RDTSCP는 TSC 값과 함께 IA32_TSC_AUX MSR 값을
     * ECX에 반환합니다. 커널은 이 MSR에 CPU 번호와
     * NUMA 노드를 인코딩해 둡니다.
     *
     * IA32_TSC_AUX = (node_id << 12) | cpu_id
     */
    asm volatile (
        "rdtscp"
        : "=c" (cpu)
        :
        : "eax", "edx"
    );

    return cpu;
}

/* vDSO의 getcpu 래퍼 */
static __always_inline int
__cvdso_getcpu(unsigned *cpu, unsigned *node)
{
    unsigned long p = __getcpu();

    if (cpu)
        *cpu  = p & 0xfff;         /* 하위 12비트: CPU ID */
    if (node)
        *node = p >> 12;            /* 상위 비트: NUMA 노드 */

    return 0;
}
코드 설명
  • 13-18행 RDTSCP 명령어는 TSC 값(EAX:EDX)과 함께 IA32_TSC_AUX MSR 값(ECX)을 반환합니다. 커널의 __switch_to()에서 이 MSR에 현재 CPU/노드를 기록합니다.
  • 30-32행 하위 12비트에 CPU ID(최대 4096개 CPU 지원), 상위 비트에 NUMA 노드 번호가 인코딩되어 있습니다.
ARM64에서의 getcpu: ARM64는 RDTSCP가 없으므로 MRS 명령어로 TPIDRRO_EL0 레지스터를 읽어 CPU 번호를 얻습니다. 이 레지스터도 컨텍스트 스위치 시 커널이 갱신합니다.

커널 측 구현

vdso_image 구조체

커널은 빌드 시 생성된 vDSO ELF 바이너리를 struct vdso_image 형태로 보관합니다. x86에서는 64비트와 32비트 각각 별도의 이미지가 존재합니다:

/* arch/x86/include/asm/vdso.h */
struct vdso_image {
    void          *data;          /* vDSO ELF 이미지 데이터 */
    unsigned long  size;          /* 이미지 크기 (바이트) */

    unsigned long  alt;           /* 대체 명령어 패치 테이블 오프셋 */
    unsigned long  alt_len;       /* 대체 패치 테이블 길이 */

    unsigned long  sym_vvar_start;  /* vvar 시작 주소 오프셋 */
    unsigned long  sym_vvar_page;   /* vvar 페이지 오프셋 */
    unsigned long  sym_pvclock_page; /* pvclock 페이지 (KVM) */
    unsigned long  sym_hvclock_page; /* Hyper-V 클럭 페이지 */
    unsigned long  sym_timens_page;  /* Time Namespace 페이지 */
};

/* 전역 vDSO 이미지 변수 */
extern const struct vdso_image vdso_image_64;   /* x86_64 */
extern const struct vdso_image vdso_image_32;   /* x86 32비트 (compat) */

arch_setup_additional_pages()

execve() 경로에서 ELF 바이너리를 로드한 후, arch_setup_additional_pages()가 호출되어 vDSO와 vvar 페이지를 프로세스 주소 공간에 매핑합니다:

/* arch/x86/entry/vdso/vma.c */
int arch_setup_additional_pages(struct linux_binprm *bprm, int uses_interp)
{
    struct mm_struct *mm = current->mm;
    const struct vdso_image *image;
    unsigned long addr, vdso_addr;
    int ret;

    /* 32비트 compat 프로세스이면 32비트 이미지 사용 */
    if (in_ia32_syscall())
        image = &vdso_image_32;
    else
        image = &vdso_image_64;

    /* ASLR을 적용한 vDSO 주소 계산 */
    vdso_addr = vdso_addr(mm, image->size);

    mmap_write_lock(mm);

    /* 1. vvar 페이지 매핑 (읽기 전용, 실행 불가) */
    addr = _install_special_mapping(mm, vdso_addr - image->sym_vvar_start,
                                     image->sym_vvar_start,
                                     VM_READ | VM_MAYREAD,
                                     &vvar_mapping);
    if (IS_ERR_VALUE(addr)) {
        ret = (int)addr;
        goto err;
    }

    /* 2. vDSO 코드 페이지 매핑 (읽기+실행, 쓰기 불가) */
    vdso_addr = _install_special_mapping(mm, vdso_addr, image->size,
                                          VM_READ | VM_EXEC |
                                          VM_MAYREAD | VM_MAYWRITE |
                                          VM_MAYEXEC,
                                          &vdso_mapping);
    if (IS_ERR_VALUE(vdso_addr)) {
        ret = (int)vdso_addr;
        goto err;
    }

    /* 3. mm->context.vdso에 기저 주소 저장 */
    mm->context.vdso = (void *)vdso_addr;

    mmap_write_unlock(mm);
    return 0;

err:
    mmap_write_unlock(mm);
    return ret;
}
코드 설명
  • 10-13행 32비트 호환(compat) 프로세스이면 vdso_image_32를, 64비트 프로세스이면 vdso_image_64를 사용합니다.
  • 16행 vdso_addr()는 ASLR을 적용하여 vDSO를 매핑할 무작위 주소를 계산합니다. 보통 스택 아래 영역에 배치됩니다.
  • 21-24행 vvar 페이지를 VM_READ 권한으로 매핑합니다. 유저 공간은 읽기만 가능하며, vvar_fault() 핸들러가 실제 물리 페이지를 연결합니다.
  • 31-35행 vDSO 코드 페이지를 VM_READ | VM_EXEC 권한으로 매핑합니다. 쓰기 불가이므로 코드 무결성이 보장됩니다.
  • 42행 vDSO 기저 주소를 mm->context.vdso에 저장합니다. 이 값은 create_elf_tables()에서 AT_SYSINFO_EHDR로 전달됩니다.

update_vsyscall()

커널의 timekeeping 서브시스템은 타이머 인터럽트가 발생할 때마다 update_vsyscall()을 호출하여 vvar 페이지의 vdso_data를 갱신합니다. 이 함수는 kernel/time/vsyscall.c에 정의되어 있습니다:

/* kernel/time/vsyscall.c */
void update_vsyscall(struct timekeeper *tk)
{
    struct vdso_data *vdata = __arch_get_k_vdso_data();
    struct vdso_timestamp *vdso_ts;
    s32 clock_mode;
    u64 nsec;

    /* clocksource의 vDSO 클럭 모드 가져오기 */
    clock_mode = tk->tkr_mono.clock->vdso_clock_mode;

    /* seqcount 쓰기 시작 */
    vdso_write_begin(vdata);

    /* 클럭 매개변수 갱신 */
    vdata[CS_HRES_COARSE].clock_mode   = clock_mode;
    vdata[CS_HRES_COARSE].cycle_last    = tk->tkr_mono.cycle_last;
    vdata[CS_HRES_COARSE].mask          = tk->tkr_mono.mask;
    vdata[CS_HRES_COARSE].mult          = tk->tkr_mono.mult;
    vdata[CS_HRES_COARSE].shift         = tk->tkr_mono.shift;

    /* CLOCK_REALTIME 기저 시간 */
    vdso_ts = &vdata[CS_HRES_COARSE].basetime[CLOCK_REALTIME];
    vdso_ts->sec  = tk->xtime_sec;
    vdso_ts->nsec = tk->tkr_mono.xtime_nsec;

    /* CLOCK_MONOTONIC 기저 시간 */
    vdso_ts = &vdata[CS_HRES_COARSE].basetime[CLOCK_MONOTONIC];
    vdso_ts->sec  = tk->xtime_sec + tk->wall_to_monotonic.tv_sec;
    nsec          = tk->tkr_mono.xtime_nsec;
    nsec += ((u64)tk->wall_to_monotonic.tv_nsec << tk->tkr_mono.shift);
    /* ... BOOTTIME, TAI 등 다른 클럭도 갱신 ... */

    /* 시간대 갱신 */
    vdata[CS_HRES_COARSE].tz_minuteswest = sys_tz.tz_minuteswest;
    vdata[CS_HRES_COARSE].tz_dsttime     = sys_tz.tz_dsttime;

    /* seqcount 쓰기 완료 */
    vdso_write_end(vdata);
}
타이머 인터럽트 (tick / hrtimer) timekeeping_advance() timekeeper 갱신 update_vsyscall() vdso_write_begin() vdso_write_end() vdso_data (vvar 페이지) 갱신 데이터 항목 clock_mode: clocksource의 vdso_clock_mode cycle_last: 마지막 TSC/카운터 값 mult, shift, mask: 사이클->나노초 변환 계수 basetime[REALTIME]: {xtime_sec, xtime_nsec} basetime[MONOTONIC]: wall_to_monotonic 보정 basetime[BOOTTIME]: sleep 시간 포함 유저 공간 (vDSO) vdso_data 읽기 + RDTSC -> 시간 계산 (syscall 없음) 읽기 전용 접근

vDSO 빌드 프로세스

빌드 단계 개요

vDSO는 일반 커널 코드와 다른 특수한 빌드 과정을 거칩니다. 유저 공간에서 실행되는 코드이므로 커널의 링킹과는 별도로 독립적인 ELF 공유 라이브러리로 빌드됩니다:

  1. 소스 컴파일: arch/x86/entry/vdso/의 C/어셈블리 소스를 유저 공간 ABI로 컴파일 (-fPIC, -shared)
  2. 링커 스크립트: vdso.lds를 사용하여 ELF 공유 라이브러리 형식으로 링킹
  3. 바이너리 변환: vdso2c 도구로 ELF 바이너리를 C 배열로 변환
  4. 커널 포함: 변환된 C 배열이 커널 이미지의 .rodata 섹션에 포함

링커 스크립트 (vdso.lds)

/* arch/x86/entry/vdso/vdso.lds.S */
SECTIONS
{
    . = SIZEOF_HEADERS;

    .hash           : { *(.hash) }          /* ELF 해시 */
    .gnu.hash       : { *(.gnu.hash) }      /* GNU 해시 */
    .dynsym         : { *(.dynsym) }        /* 동적 심볼 */
    .dynstr         : { *(.dynstr) }        /* 동적 문자열 */
    .gnu.version    : { *(.gnu.version) }   /* 심볼 버저닝 */
    .gnu.version_d  : { *(.gnu.version_d) } /* 버전 정의 */

    .text           : {
        *(.text*)
    } :text = 0x90909090  /* NOP으로 패딩 */

    .altinstructions : { *(.altinstructions) }
    .altinstr_replacement : { *(.altinstr_replacement) }

    .rodata         : { *(.rodata*) }

    .dynamic        : { *(.dynamic) } :text :dynamic

    .note           : { *(.note.*) } :text :note

    /* vvar 심볼 정의: 커널이 이 오프셋에 데이터 페이지를 매핑 */
    . = ALIGN(PAGE_SIZE);
    vvar_start = . - 3 * PAGE_SIZE;
    vvar_page  = vvar_start;
    pvclock_page = vvar_start + PAGE_SIZE;
    hvclock_page = vvar_start + 2 * PAGE_SIZE;
    timens_page  = vvar_start + 3 * PAGE_SIZE;
}

vdso2c 변환 도구

arch/x86/entry/vdso/vdso2c.c는 빌드 호스트에서 실행되는 도구로, 링킹된 vDSO ELF 바이너리를 분석하여 C 소스 파일(vdso-image-64.c)로 변환합니다:

/* 자동 생성된 파일 (vdso-image-64.c) 예시 */
static unsigned char raw_data[8192]
    __ro_after_init __aligned(PAGE_SIZE) = {
    0x7f, 0x45, 0x4c, 0x46,  /* ELF 매직 넘버 */
    0x02, 0x01, 0x01, 0x00,  /* 64비트, LE, ELF v1 */
    /* ... 전체 vDSO ELF 이미지 바이트 ... */
};

const struct vdso_image vdso_image_64 = {
    .data            = raw_data,
    .size            = sizeof(raw_data),
    .sym_vvar_start  = -0x3000,    /* vvar 시작 오프셋 */
    .sym_vvar_page   = -0x3000,    /* vvar 페이지 오프셋 */
    .sym_pvclock_page = -0x2000,   /* pvclock 오프셋 */
    .sym_hvclock_page = -0x1000,   /* Hyper-V 클럭 오프셋 */
    .sym_timens_page  = -0x4000,   /* timens 오프셋 */
};

Makefile 빌드 규칙

# arch/x86/entry/vdso/Makefile (핵심 부분)

# vDSO 소스 파일들
vobjs-y := vdso-note.o vclock_gettime.o vgetcpu.o

# 유저 공간 컴파일 플래그
CFLAGS_REMOVE_vclock_gettime.o = -pg    # ftrace 제외
CFLAGS_vclock_gettime.o = -fPIC -shared \
    -fno-stack-protector -fno-jump-tables

# vDSO 링킹
$(obj)/vdso64.so.dbg: $(obj)/vdso.lds $(vobjs)
    $(call if_changed,vdso64)

# vdso2c로 C 배열 변환
$(obj)/vdso-image-64.c: $(obj)/vdso64.so.dbg $(obj)/vdso2c
    $(obj)/vdso2c $< $@

아키텍처별 vDSO 구현

아키텍처별 비교

속성 x86_64 x86_32 (i386) ARM64 (AArch64) RISC-V
하드웨어 카운터 RDTSC/RDTSCP RDTSC/RDTSCP CNTVCT_EL0 (Generic Timer) RDTIME/RDCYCLE
getcpu 방식 RDTSCP (ECX) 또는 LSL LSL (GDT 엔트리) MRS TPIDRRO_EL0 tp 레지스터 (SBI 확장)
시스템 콜 진입 SYSCALL SYSENTER/INT 0x80 SVC #0 ECALL
vDSO 함수 수 4 (time, gettimeofday, clock_gettime, getcpu) 5 (+ __kernel_vsyscall, clock_gettime64) 4 (time, gettimeofday, clock_gettime, clock_getres) 4 (time, gettimeofday, clock_gettime, clock_getres)
ELF 형식 ET_DYN (x86-64) ET_DYN (i386) ET_DYN (AArch64) ET_DYN (RISC-V 64/32)
소스 위치 arch/x86/entry/vdso/ arch/x86/entry/vdso/ arch/arm64/kernel/vdso/ arch/riscv/kernel/vdso/
vsyscall 지원 레거시 에뮬레이션 해당 없음 해당 없음 해당 없음

x86_64: RDTSC 기반 고속 경로

x86_64에서 vDSO의 핵심은 RDTSC/RDTSCP 명령어입니다. TSC(Time Stamp Counter)는 CPU 코어의 하드웨어 카운터로, 유저 공간에서 직접 읽을 수 있습니다:

/* arch/x86/include/asm/vdso/gettimeofday.h */
static __always_inline u64
__arch_get_hw_counter(s32 clock_mode, const struct vdso_data *vd)
{
    if (likely(clock_mode == VDSO_CLOCKMODE_TSC))
        return (u64)__native_read_tsc();

    /*
     * TSC 불안정 시 HPET 등을 직접 읽을 수 없으므로
     * VDSO_CLOCKMODE_NONE -> 시스템 콜 폴백
     */
    return U64_MAX;  /* 호출자가 폴백 처리 */
}

/* RDTSC 인라인 어셈블리 */
static __always_inline unsigned long long
__native_read_tsc(void)
{
    unsigned hi, lo;
    asm volatile (
        "rdtsc"
        : "=a" (lo), "=d" (hi)
    );
    return ((unsigned long long)hi << 32) | lo;
}

ARM64: Generic Timer 기반

ARM64에서는 CPU의 Generic Timer 카운터(CNTVCT_EL0)를 사용합니다. 이 레지스터는 유저 공간에서 MRS 명령어로 직접 읽을 수 있습니다 (CNTKCTL_EL1.EL0VCTEN 비트가 설정된 경우):

/* arch/arm64/include/asm/vdso/gettimeofday.h */
static __always_inline u64
__arch_get_hw_counter(s32 clock_mode, const struct vdso_data *vd)
{
    u64 res;

    /*
     * CNTVCT_EL0: 가상 카운터 레지스터
     * ISB: 명령어 직렬화 배리어 (순서 보장)
     */
    asm volatile(
        "isb\n"
        "mrs %0, cntvct_el0"
        : "=r" (res)
        :
        : "memory"
    );

    return res;
}
x86_64 RDTSC / RDTSCP SYSCALL / SYSRET IA32_TSC_AUX (getcpu) vsyscall 레거시 vDSO 함수 __vdso_clock_gettime __vdso_gettimeofday __vdso_time, __vdso_getcpu ARM64 CNTVCT_EL0 (Generic Timer) SVC #0 MRS TPIDRRO_EL0 (getcpu) (vsyscall 없음) vDSO 함수 __vdso_clock_gettime __vdso_gettimeofday __vdso_clock_getres RISC-V RDTIME (CSR time) ECALL tp 레지스터 (SBI) (vsyscall 없음) vDSO 함수 __vdso_clock_gettime __vdso_gettimeofday __vdso_clock_getres, __vdso_flush_icache

KVM/가상화 환경에서의 vDSO

가상화 환경에서는 vDSO의 동작에 특수한 고려사항이 있습니다:

유저 공간 vDSO 파싱

glibc의 vDSO 통합

glibc는 프로세스 시작 시 AT_SYSINFO_EHDR에서 vDSO 기저 주소를 읽고, ELF 동적 심볼 테이블을 파싱하여 vDSO 함수 포인터를 설정합니다. 이후 clock_gettime() 등의 호출은 시스템 콜 대신 vDSO 함수를 직접 호출합니다.

수동 vDSO 파싱 예제

glibc에 의존하지 않는 환경(정적 링크 바이너리, musl, 커스텀 런타임)에서 vDSO를 직접 파싱하여 사용하는 방법입니다:

#include <elf.h>
#include <sys/auxv.h>
#include <string.h>
#include <time.h>

/* vDSO 함수 타입 정의 */
typedef int (*vdso_clock_gettime_t)(clockid_t, struct timespec *);

/* ELF 동적 심볼에서 특정 이름의 함수를 찾기 */
static void *vdso_find_sym(uintptr_t base, const char *name)
{
    const Elf64_Ehdr *ehdr = (const Elf64_Ehdr *)base;
    const Elf64_Phdr *phdr = (const Elf64_Phdr *)(base + ehdr->e_phoff);
    const Elf64_Dyn  *dyn  = NULL;
    const Elf64_Sym  *symtab = NULL;
    const char       *strtab = NULL;
    const Elf64_Word *hashtab = NULL;

    /* PT_DYNAMIC 세그먼트 찾기 */
    for (int i = 0; i < ehdr->e_phnum; i++) {
        if (phdr[i].p_type == PT_DYNAMIC) {
            dyn = (const Elf64_Dyn *)(base + phdr[i].p_offset);
            break;
        }
    }
    if (!dyn) return NULL;

    /* 동적 섹션에서 심볼/문자열/해시 테이블 추출 */
    for (; dyn->d_tag != DT_NULL; dyn++) {
        switch (dyn->d_tag) {
        case DT_SYMTAB:
            symtab = (const Elf64_Sym *)(base + dyn->d_un.d_ptr);
            break;
        case DT_STRTAB:
            strtab = (const char *)(base + dyn->d_un.d_ptr);
            break;
        case DT_HASH:
            hashtab = (const Elf64_Word *)(base + dyn->d_un.d_ptr);
            break;
        }
    }
    if (!symtab || !strtab || !hashtab) return NULL;

    /* 해시 테이블을 순회하며 이름 매칭 */
    Elf64_Word nbucket = hashtab[0];
    Elf64_Word nchain  = hashtab[1];
    const Elf64_Word *buckets = &hashtab[2];
    const Elf64_Word *chains  = &hashtab[2 + nbucket];

    for (Elf64_Word i = 0; i < nchain; i++) {
        if (strcmp(strtab + symtab[i].st_name, name) == 0) {
            return (void *)(base + symtab[i].st_value);
        }
    }
    return NULL;
}

/* 사용 예 */
int main(void)
{
    uintptr_t vdso_base = getauxval(AT_SYSINFO_EHDR);
    if (!vdso_base) return 1;

    vdso_clock_gettime_t vdso_cgt =
        (vdso_clock_gettime_t)vdso_find_sym(vdso_base,
                                             "__vdso_clock_gettime");
    if (vdso_cgt) {
        struct timespec ts;
        vdso_cgt(CLOCK_MONOTONIC, &ts);
        /* ts에 시간이 기록됨 - 시스템 콜 없이! */
    }
    return 0;
}
코드 설명
  • 11-12행 vDSO 기저 주소를 ELF 헤더로 캐스팅합니다. vDSO는 표준 ELF 형식이므로 일반 ELF 파싱 코드로 처리 가능합니다.
  • 20-26행 ELF Program Header Table에서 PT_DYNAMIC 타입 세그먼트를 찾습니다. 이 세그먼트에 동적 링킹 정보가 포함되어 있습니다.
  • 29-42행 동적 섹션에서 심볼 테이블(DT_SYMTAB), 문자열 테이블(DT_STRTAB), 해시 테이블(DT_HASH)의 주소를 추출합니다.
  • 49-53행 심볼 체인을 순회하며 요청된 함수 이름과 일치하는 심볼을 찾고, 기저 주소에 심볼 값을 더하여 함수 포인터를 반환합니다.
  • 58행 getauxval(AT_SYSINFO_EHDR)로 vDSO 기저 주소를 얻습니다. vDSO가 없는 환경에서는 0을 반환합니다.

vDSO 심볼 이름 규칙

심볼 이름대응 시스템 콜아키텍처
__vdso_clock_gettimeclock_gettime()모든 아키텍처
__vdso_gettimeofdaygettimeofday()모든 아키텍처
__vdso_timetime()x86, ARM64, RISC-V
__vdso_getcpugetcpu()x86
__vdso_clock_getresclock_getres()ARM64, RISC-V
__vdso_clock_gettime64clock_gettime64()x86_32 (Y2038 대응)
__kernel_vsyscall(시스템 콜 진입)x86_32 (SYSENTER)
__vdso_flush_icache(캐시 플러시)RISC-V

성능 분석과 벤치마크

시스템 콜 vs vDSO 레이턴시 비교

다음은 x86_64 환경(Intel Core i9, 5.15 커널)에서 측정한 대표적인 레이턴시입니다. 각 함수를 1,000만 회 반복 호출하여 평균값을 산출했습니다:

함수시스템 콜 (KPTI 비활성)시스템 콜 (KPTI 활성)vDSO가속 비율
clock_gettime(CLOCK_MONOTONIC) 약 60~80 ns 약 150~250 ns 약 15~25 ns 4~10배
clock_gettime(CLOCK_REALTIME) 약 60~80 ns 약 150~250 ns 약 15~25 ns 4~10배
gettimeofday() 약 60~80 ns 약 150~250 ns 약 15~25 ns 4~10배
clock_gettime(CLOCK_REALTIME_COARSE) 약 50~70 ns 약 140~230 ns 약 5~10 ns 10~30배
time() 약 50~70 ns 약 140~230 ns 약 2~5 ns 25~50배
getcpu() 약 50~70 ns 약 140~230 ns 약 5~10 ns 10~25배
KPTI 영향: Meltdown 취약점 완화를 위한 KPTI(Kernel Page Table Isolation)는 시스템 콜 오버헤드를 약 2~3배 증가시킵니다. vDSO는 커널 모드로 전환하지 않으므로 KPTI의 영향을 받지 않습니다. 이는 Spectre/Meltdown 시대에 vDSO의 가치가 더욱 커진 이유입니다.

벤치마크 방법

#include <time.h>
#include <stdio.h>
#include <stdint.h>

#define ITERATIONS 10000000

static inline uint64_t rdtsc_fenced(void)
{
    uint32_t lo, hi;
    asm volatile (
        "lfence\n\trdtsc"
        : "=a"(lo), "=d"(hi)
    );
    return ((uint64_t)hi << 32) | lo;
}

int main(void)
{
    struct timespec ts;
    uint64_t start, end;

    /* vDSO 경로 벤치마크 (clock_gettime 사용) */
    start = rdtsc_fenced();
    for (int i = 0; i < ITERATIONS; i++)
        clock_gettime(CLOCK_MONOTONIC, &ts);
    end = rdtsc_fenced();

    printf("vDSO clock_gettime: %.1f cycles/call\n",
           (double)(end - start) / ITERATIONS);

    /* 시스템 콜 강제 경로 벤치마크 (syscall 직접 호출) */
    start = rdtsc_fenced();
    for (int i = 0; i < ITERATIONS; i++)
        syscall(__NR_clock_gettime, CLOCK_MONOTONIC, &ts);
    end = rdtsc_fenced();

    printf("syscall clock_gettime: %.1f cycles/call\n",
           (double)(end - start) / ITERATIONS);

    return 0;
}

실제 워크로드에서의 영향

vDSO의 성능 이점은 시간 함수를 빈번히 호출하는 워크로드에서 두드러집니다:

vDSO 디버깅

/proc/PID/maps에서 vDSO 확인

# 현재 프로세스의 vDSO/vvar 매핑 확인
$ cat /proc/self/maps | grep -E 'vdso|vvar|vsyscall'
7fff12ffe000-7fff13000000 r--p 00000000 00:00 0  [vvar]
7fff13000000-7fff13002000 r-xp 00000000 00:00 0  [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0  [vsyscall]

출력에서 주목할 점:

vDSO ELF 추출 및 분석

# vDSO 이미지를 파일로 추출
$ dd if=/proc/self/mem of=vdso.so bs=1 \
    skip=$(($(cat /proc/self/maps | grep '\[vdso\]' | \
    cut -d'-' -f1 | xargs printf '%d\n' 0x))) \
    count=$((0x2000)) 2>/dev/null

# 또는 GDB를 사용한 추출
$ gdb -batch -ex 'set confirm off' \
    -ex 'dump binary memory vdso.so 0x7fff13000000 0x7fff13002000' \
    -ex 'quit' /bin/true

# ELF 헤더 확인
$ readelf -h vdso.so
ELF Header:
  Class:                             ELF64
  Type:                              DYN (Shared object file)
  Machine:                           Advanced Micro Devices X86-64
  Entry point address:               0x...

# 동적 심볼 확인
$ readelf -Ws vdso.so
Symbol table '.dynsym' contains N entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     1: 0000000000000b70   ...  FUNC    WEAK   DEFAULT   11 clock_gettime@@LINUX_2.6
     2: 0000000000000c80   ...  FUNC    WEAK   DEFAULT   11 __vdso_gettimeofday@@LINUX_2.6
     3: 0000000000000d30   ...  FUNC    WEAK   DEFAULT   11 __vdso_time@@LINUX_2.6
     4: 0000000000000d50   ...  FUNC    WEAK   DEFAULT   11 __vdso_getcpu@@LINUX_2.6

# 디스어셈블리
$ objdump -d vdso.so | head -50

Auxiliary Vector 확인

# LD_SHOW_AUXV로 auxiliary vector 출력
$ LD_SHOW_AUXV=1 /bin/true
AT_SYSINFO_EHDR: 0x7ffd3f9f0000
AT_HWCAP:        0x178bfbff
AT_PAGESZ:       4096
AT_CLKTCK:       100
AT_PHDR:         0x5621e4400040
AT_PHENT:        56
AT_PHNUM:        11
AT_BASE:         0x7f8b2c000000
AT_FLAGS:        0x0
AT_ENTRY:        0x5621e4401090
AT_UID:          1000
AT_EUID:         1000
AT_GID:          1000
AT_EGID:         1000
AT_SECURE:       0
AT_RANDOM:       0x7ffd3f9cf209
AT_EXECFN:       /bin/true
AT_PLATFORM:     x86_64

GDB에서 vDSO 디버깅

# GDB로 vDSO 함수에 브레이크포인트 설정
$ gdb ./my_program
(gdb) break __vdso_clock_gettime
Breakpoint 1 at 0x7ffff7fce0b0

# vDSO 공유 라이브러리 정보 확인
(gdb) info sharedlibrary
From                To                  Shared Object Library
0x00007ffff7fce000  0x00007ffff7fcf1a0  linux-vdso.so.1

# vDSO 함수 디스어셈블
(gdb) disas __vdso_clock_gettime
Dump of assembler code for function __vdso_clock_gettime:
   0x00007ffff7fce0b0:  push   %rbp
   0x00007ffff7fce0b1:  mov    %rsp,%rbp
   ...

strace로 vDSO 동작 확인

# strace는 vDSO 경로를 추적하지 못함 (시스템 콜 아님)
# vDSO를 사용하는 프로그램에서는 clock_gettime이 보이지 않음
$ strace -e trace=clock_gettime ./my_program
# (출력 없음 - vDSO 경로 사용 중)

# 시스템 콜을 강제하면 strace에 나타남
$ strace -e trace=clock_gettime -- \
    python3 -c "import ctypes; ctypes.CDLL(None).syscall(228, 1, 0)"
clock_gettime(CLOCK_MONOTONIC, {tv_sec=..., tv_nsec=...}) = 0
ltrace 사용: ltrace는 라이브러리 함수 호출을 추적하므로 glibc의 clock_gettime() 래퍼 호출은 확인할 수 있습니다. 다만 vDSO 내부의 실제 실행까지는 추적하지 못합니다.

보안 고려사항

ASLR과 vDSO 무작위화

vDSO는 매 execve()마다 무작위 주소에 매핑됩니다. 이는 커널의 ASLR 메커니즘과 통합되어 있습니다:

/* arch/x86/entry/vdso/vma.c */
static unsigned long vdso_addr(struct mm_struct *mm,
                               unsigned long image_size)
{
    unsigned long addr, end;

    /* 스택 영역 위에 배치 */
    end = (mm->stack_start + STACK_TOP_MAX) & PAGE_MASK;
    end -= image_size;

    /* 무작위 오프셋 적용: PMD 크기 범위 내에서 무작위화 */
    if (current->flags & PF_RANDOMIZE) {
        addr = end - get_random_long() % (VDSO_RANDOMIZE_SIZE);
    } else {
        addr = end;
    }

    /* 페이지 정렬 */
    addr = PAGE_ALIGN(addr);

    return addr;
}

vDSO 관련 공격 벡터와 방어

공격 벡터위험도방어 메커니즘
vDSO 주소 정보 누출 중간 ASLR, /proc/[pid]/maps 접근 제한 (hidepid=2)
vsyscall ROP 가젯 높음 (레거시) vsyscall=emulate/none으로 네이티브 코드 제거
vvar 데이터 조작 낮음 vvar은 유저 측에서 읽기 전용. 쓰기 시도 시 SIGSEGV
vDSO 코드 덮어쓰기 낮음 vDSO는 r-x 매핑. mprotect()로 쓰기 가능으로 변경 시도 시 실패
Side-channel (TSC 기반) 낮음 TSC는 코어 로컬, 타 프로세스 시간 측정 불가

seccomp과 vDSO의 관계

vDSO 함수는 시스템 콜을 통하지 않으므로 seccomp 필터의 적용을 받지 않습니다. 이는 의도된 동작입니다:

주의: seccomp으로 clock_gettime() 호출을 감사(audit)하려면 vDSO를 비활성화해야 합니다. 커널에서 clocksource를 vDSO 미지원 소스(예: HPET)로 강제 전환하거나, glibc 환경 변수를 사용하는 방법이 있지만, 일반적으로 권장되지 않습니다.

커널 설정

vDSO 관련 CONFIG 옵션

CONFIG 옵션기본값설명
CONFIG_GENERIC_VDSO y (자동) 아키텍처 독립 vDSO 프레임워크 활성화. lib/vdso/의 공통 코드 사용
CONFIG_HAVE_GENERIC_VDSO y (자동) 아키텍처가 제네릭 vDSO를 지원함을 선언
CONFIG_LEGACY_VSYSCALL_EMULATE y (x86) vsyscall을 에뮬레이션 모드로 동작 (기본 권장)
CONFIG_LEGACY_VSYSCALL_XONLY n vsyscall 페이지 실행만 허용, 읽기 차단
CONFIG_LEGACY_VSYSCALL_NONE n vsyscall 완전 비활성화 (최고 보안)
CONFIG_X86_X32_ABI n x32 ABI 지원 시 별도 vDSO 이미지 빌드
CONFIG_IA32_EMULATION y 32비트 호환 모드에서 32비트 vDSO 사용
CONFIG_TIME_NS y Time Namespace 지원. vvar에 timens 오프셋 페이지 추가
CONFIG_PARAVIRT_CLOCK y (KVM) pvclock 반가상화 클럭. vvar에 pvclock 페이지 추가

부트 파라미터

파라미터설명
vsyscall=emulate vsyscall 주소 접근 시 시스템 콜로 에뮬레이션 (기본값)
vsyscall=xonly vsyscall 페이지 실행만 허용, 데이터 읽기 차단
vsyscall=none vsyscall 완전 비활성화. 접근 시 SIGSEGV
vdso=0 vDSO 매핑 비활성화 (디버깅/테스트 용도)
vdso32=0 32비트 vDSO만 비활성화
clocksource=tsc TSC를 clocksource로 강제. vDSO 고속 경로 보장
clocksource=hpet HPET를 clocksource로 강제. vDSO가 비활성화될 수 있음
tsc=reliable TSC 안정성 검사 우회. 가상화 환경에서 유용할 수 있음

런타임 확인

# 현재 clocksource 확인
$ cat /sys/devices/system/clocksource/clocksource0/current_clocksource
tsc

# 사용 가능한 clocksource 목록
$ cat /sys/devices/system/clocksource/clocksource0/available_clocksource
tsc hpet acpi_pm

# vDSO 크기 확인
$ cat /proc/self/maps | grep vdso
7fff25dfe000-7fff25e00000 r-xp 00000000 00:00 0  [vdso]
# 크기: 0x2000 = 8KB (2페이지)

# vsyscall 모드 확인 (커널 커맨드라인)
$ cat /proc/cmdline | grep -o 'vsyscall=[^ ]*'
vsyscall=emulate

# CONFIG 확인
$ zcat /proc/config.gz | grep -i vdso
CONFIG_GENERIC_VDSO=y
CONFIG_HAVE_GENERIC_VDSO=y

$ zcat /proc/config.gz | grep -i vsyscall
CONFIG_LEGACY_VSYSCALL_EMULATE=y
# CONFIG_LEGACY_VSYSCALL_XONLY is not set
# CONFIG_LEGACY_VSYSCALL_NONE is not set

참고자료

다음 학습:
  • 시스템 콜 -- vDSO의 폴백 경로인 시스템 콜의 전체 진입/복귀 경로
  • ktime / Clock -- vDSO가 읽는 clocksource와 timekeeper 서브시스템
  • 타이머 -- hrtimer와 tick 기반 타이머의 vDSO 갱신 트리거
  • Time Namespaces -- vvar의 timens 오프셋 페이지와 컨테이너 시간 격리