strace에 clock_gettime()이 안 보이는 이유: 리눅스 vDSO

프로그램은 분명 clock_gettime()을 부르는데 strace에는 안 나올 때가 있습니다. 리눅스 커널이 프로세스에 넣어 주는 vDSO가 무엇이고, 추적 결과를 어떻게 읽어야 하는지 정리합니다.

linuxvdsostracesyscallclock_gettime

시간을 자주 읽는 프로그램을 strace로 추적해 보면 clock_gettime이 한 줄도 나오지 않는 경우가 있습니다. 코드에는 분명 clock_gettime() 호출이 있고 출력도 정상인데 말입니다. 추적이 실패한 것이 아니라, 그 호출이 시스템 콜 없이 끝났을 가능성이 큽니다. 리눅스에서 이것을 가능하게 하는 장치가 vDSO입니다.

이 글은 리눅스 커널 문서와 Linux man-pages를 기준으로 vDSO가 무엇인지, strace 결과를 어떻게 해석해야 하는지를 다룹니다. 어떤 시계(clock ID)가 무엇을 재는지는 time 결과의 real, user, sys는 무엇이 다를까에서 따로 다뤘으니 여기서는 반복하지 않습니다.

vDSO는 커널이 넣어 주는 작은 공유 라이브러리

vDSO(virtual dynamic shared object)는 커널이 사용자 공간 프로그램의 주소 공간에 자동으로 매핑하는 작은 공유 라이브러리입니다. 커널이 프로그램을 적재할 때 완전한 형태의 ELF 이미지를 직접 매핑해 줍니다. 커널의 ABI 문서는 이를 “일부 아키텍처에서” 일어나는 일로 설명하고, vdso(7) 매뉴얼도 기준 주소를 “vDSO가 있다면” 넘겨준다고 적습니다. 모든 리눅스 환경에 반드시 있다고 가정하면 안 됩니다.

이름도 아키텍처마다 다릅니다. vdso(7)의 표에 따르면 x86-64와 aarch64에서는 linux-vdso.so.1, i386에서는 linux-gate.so.1, ppc/64에서는 linux-vdso64.so.1입니다. glibc의 ldd 출력에 이 이름이 보이는 경우가 많지만, 매뉴얼은 정확한 이름에 기대는 코드를 쓰지 말라고 합니다.

왜 시스템 콜을 건너뛸 수 있나

시스템 콜에는 사용자 공간을 빠져나가 커널로 들어가는 전환 비용이 듭니다. vdso(7)는 어떤 시스템 콜은 너무 자주 불려서 이 비용이 전체 성능을 좌우할 정도가 된다고 설명합니다. 현재 시각을 묻는 호출이 대표적입니다. 타임스탬프, 시간 측정 루프, 폴링 모두 “지금 몇 시인가”를 계속 묻습니다.

현재 시각은 비밀 정보도 아닙니다. root든 일반 사용자든 같은 답을 받습니다. 그래서 커널은 답을 계산하는 데 필요한 정보를 프로세스가 읽을 수 있는 메모리에 올려 두고, vDSO 안의 함수가 그 메모리를 읽어 답을 만듭니다. vdso(7)의 표현으로는 gettimeofday(2) 호출이 “시스템 콜에서 일반 함수 호출과 몇 번의 메모리 접근”으로 바뀝니다.

이 경로가 쓰이려면 해당 아키텍처와 커널이 그 함수를 vDSO로 제공해야 합니다. 얼마나 빨라지는지에 대한 수치는 원문에 없고, 이 글도 제시하지 않습니다.

직접 부르는 것이 아니라 libc가 쓴다

일반 애플리케이션 코드는 vDSO를 신경 쓸 필요가 없습니다. 평소처럼 clock_gettime()을 부르면 C 라이브러리가 vDSO에 쓸 수 있는 기능이 있는지 보고 알아서 사용합니다. vdso(7)는 C 라이브러리를 거치지 않고 애플리케이션에서 vDSO를 직접 부르려 한다면 “아마 잘못하고 있는 것”이라고까지 적습니다. 커널 ABI 문서는 glibc에 동적으로 링크된 프로그램은 vDSO를 자동으로 사용한다고 설명합니다. 정적 링크나 다른 libc에 대해서는 이 문서가 같은 보장을 하지 않으므로, 그런 환경이라면 해당 libc의 문서를 따로 확인해야 합니다.

직접 부르지 말아야 할 이유는 문서 곳곳에 있습니다.

vDSO 함수의 호출 규약은 플랫폼 ABI를 따릅니다. 커널 ABI 문서에 따르면 많은 플랫폼에서는 보통의 C 함수처럼 부르지만 powerpc 같은 곳에서는 시스템 콜과 같은 규약을 씁니다. 잘못된 포인터를 넘기면 -EFAULT 대신 SIGSEGV를 받을 수도 있다고 합니다.

심볼 이름과 목록도 아키텍처마다 다릅니다. vdso(7)의 표를 보면 x86-64에서는 __vdso_clock_gettime이고 aarch64에서는 __kernel_clock_gettime입니다. 어떤 아키텍처에는 시간 관련 심볼이 아예 표에 없습니다.

vDSO 심볼에는 버전이 붙습니다(GNU 심볼 버전 형식). 예를 들어 x86-64의 __vdso_clock_gettime은 LINUX_2.6, aarch64의 __kernel_clock_gettime은 LINUX_2.6.39입니다. 심볼을 찾을 때는 기대하는 버전을 함께 지정해야 합니다. 커널 ABI 문서는 한 버전의 심볼 집합과 ABI는 안정적이라고 하면서도 아키텍처마다 다를 수 있다고 덧붙이고, 이 문서가 확인된 것은 x86_64뿐이라는 메모를 남겨 두었습니다.

프로세스는 vDSO 위치를 어떻게 아나

커널은 프로그램을 실행할 때 보조 벡터(auxiliary vector)에 AT_SYSINFO_EHDR 항목으로 vDSO의 기준 주소를 넣어 줍니다. 이 값은 glibc의 getauxval()로 읽을 수 있습니다. 주소는 보통 execve 때마다 무작위로 정해지므로 특정 위치에 있다고 가정하면 안 됩니다.

getauxval()은 POSIX 함수가 아니라 GNU 확장이고 glibc 2.16에 들어왔습니다. 찾는 항목이 없으면 0을 돌려주며, glibc 2.19부터는 이때 errno를 ENOENT로 설정합니다. 2.19 이전에는 “항목이 없음”과 “값이 실제로 0”을 구분할 방법이 없었습니다. 그래서 확인하려면 호출 전에 errno를 0으로 두고, 반환값이 0일 때 errno를 함께 봐야 합니다. 0이면서 errno가 ENOENT라면 항목이 없는 것입니다. 0인데 errno가 바뀌지 않았다면 2.19 이전 glibc에서는 항목이 없는지 값이 0인지 알 수 없으므로, 아래 예제는 이 경우를 따로 출력합니다.

무엇이 POSIX이고 무엇이 리눅스 구현인가

clock_gettime() 자체는 POSIX 인터페이스입니다. 매뉴얼의 STANDARDS 항목은 POSIX.1-2024이고, POSIX.1-1996부터 있던 함수입니다. 반면 vDSO는 리눅스의 구현 방식입니다. clock_gettime(2) 매뉴얼은 vDSO를 표준 설명이 아니라 “C library/kernel differences” 항목에서 “일부 아키텍처에서는 clock_gettime()의 구현이 vDSO에 제공된다”고만 언급합니다.

즉, 프로그램이 기대할 수 있는 것은 clock_gettime()이 정의된 대로 동작한다는 사실까지입니다. 그 호출이 시스템 콜을 거치는지, vDSO로 끝나는지는 아키텍처, 커널, C 라이브러리의 조합에 따라 달라지는 구현 세부입니다.

clock_gettime()이 항상 vDSO로 끝나지는 않는다

vDSO에 clock_gettime이 있다고 해서 모든 호출이 시스템 콜 없이 끝나는 것은 아닙니다. vdso(7)에는 실제로 그렇지 않은 사례가 적혀 있습니다. ppc/64에서는 Linux 4.16 이전, ppc/32에서는 Linux 5.6 이전에 CLOCK_REALTIME_COARSE와 CLOCK_MONOTONIC_COARSE가 vDSO의 __kernel_clock_gettime에서 지원되지 않았고, 이때는 실제 시스템 콜로 넘어갔습니다. clock_gettime(2) 매뉴얼도 이 두 COARSE 시계에 대해 아키텍처별 지원과 아마도 vDSO 쪽 지원까지 필요하다고 적습니다.

어떤 시계가 지금 쓰는 시스템에서 vDSO로 처리되는지는 이 문서들만으로 일반화할 수 없습니다. 이 글도 특정 clock ID가 모든 시스템에서 빠르다고 약속하지 않습니다.

strace 결과를 읽는 법

strace는 시스템 콜을 추적하는 도구입니다. vdso(7)는 vDSO가 내보내는 심볼(시스템 콜)은 strace 출력에 나타나지 않는다고 분명히 적습니다. 같은 이유로 seccomp(2) 필터에도 보이지 않습니다.

그러니 해석은 이렇게 됩니다.

  • 추적에 clock_gettime 줄이 없다고 해서 프로그램이 시간을 묻지 않았다는 증거는 아닙니다. vDSO로 처리됐을 수 있습니다.
  • 추적에 clock_gettime 줄이 있다면 그 호출은 실제로 커널에 들어간 것입니다. vDSO가 그 경우를 처리하지 못했거나 C 라이브러리가 vDSO를 쓰지 않은 것일 수 있지만, 어느 쪽인지는 추적만으로 알 수 없습니다.
  • seccomp 필터로 시간 관련 시스템 콜을 막거나 기록하려 할 때도 같은 점을 고려해야 합니다.

직접 확인해 보기

아래는 vDSO 기준 주소를 출력하고 clock_gettime()을 세 번 부르는 작은 C 프로그램입니다. 이 글을 쓰면서 이 코드를 실행하거나 컴파일하지는 않았으므로, 아래에 출력 예시는 싣지 않습니다.

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

int main(void)
{
    errno = 0;
    unsigned long base = getauxval(AT_SYSINFO_EHDR);
    if (base != 0)
        printf("AT_SYSINFO_EHDR = %#lx\n", base);
    else if (errno == ENOENT)
        printf("AT_SYSINFO_EHDR not found\n");
    else
        printf("AT_SYSINFO_EHDR: 0 or not found (ambiguous before glibc 2.19)\n");

    for (int i = 0; i < 3; i++) {
        struct timespec ts;
        if (clock_gettime(CLOCK_MONOTONIC, &ts) == -1) {
            perror("clock_gettime");
            return 1;
        }
        printf("%lld.%09ld\n", (long long) ts.tv_sec, ts.tv_nsec);
    }
    return 0;
}

glibc 2.17 이상이면 cc -o vdso-demo vdso-demo.c로 빌드할 수 있습니다. 그보다 오래된 glibc에서는 clock_gettime()이 librt에 있어서 -lrt가 필요합니다.

strace ./vdso-demo

출력에서 clock_gettime 줄이 몇 번 나오는지 보면 됩니다. printf가 출력하는 과정의 write 같은 다른 시스템 콜은 보일 수 있습니다. 시각 값이 세 번 출력됐는데 clock_gettime 줄이 없다면, 그 호출들은 시스템 콜 없이 끝났다고 읽을 수 있습니다. 반대로 세 줄이 보인다면 이 환경에서는 시스템 콜 경로를 탔다는 뜻입니다.

보조 벡터는 프로그램을 고치지 않고도 볼 수 있습니다. getauxval(3) 매뉴얼은 LD_SHOW_AUXV 환경 변수를 설정하고 프로그램을 실행하면 그 프로그램이 받은 보조 벡터를 볼 수 있다고 설명하며, 아래 예를 듭니다.

LD_SHOW_AUXV=1 sleep 1

여기에 AT_SYSINFO_EHDR 항목이 있으면 커널이 vDSO를 넘겨준 것입니다. 다만 그 사실만으로 clock_gettime()의 특정 시계가 vDSO로 처리된다는 것까지 확인되지는 않습니다. 실행 중인 다른 프로세스의 보조 벡터는 권한이 있으면 /proc/<pid>/auxv로 읽을 수 있습니다.

참고 자료

확인 날짜: 2026-10-18. man-pages의 git 원문과 man7.org의 HTML(man-pages 6.19 기준)을 함께 참고했습니다.

블로그로