vsyscall and vDSO

vsyscalls 와 vDSO

이것은 Linux 커널에서 시스템 호출을 설명하는 챕터의 세 번째 파트이며, 사용자 공간 응용 프로그램 이전 파트에서 시스템 콜 처리 프로세스로 인한 시스템 호출 후의 준비 사항을 확인했습니다. 이 파트에서는 시스템 콜 개념에 매우 가까운 두 가지 개념인 vsyscallvdso를 살펴 보겠습니다.

우리는 이미 시스템 콜이 무엇인지 알고 있습니다. 시스템 콜은 사용자 공간 응용 프로그램이 파일 읽기 또는 쓰기, 소켓 열기 등과 같은 권한있는 작업을 수행하도록 요청하는 Linux 커널의 특수 루틴입니다. 아시다시피, 시스템 콜 호출은 Linux에서 비용이 많이 드는 작업입니다. 프로세서는 현재 실행중인 작업을 중단하고 컨텍스트를 커널 모드로 전환해야하기 때문에 시스템 호출 처리기가 작업을 완료 한 후 사용자 공간으로 다시 점프해야합니다. 이 두 가지 메커니즘인 vsyscallvdso는 특정 시스템 호출에 대해 이 프로세스의 속도를 높이도록 설계되었으며 이 부분에서는 이러한 메커니즘의 작동 방식을 이해하려고합니다.

vsyscall 소개

vsyscall 또는 virtual system call 은 특정 시스템 호출의 실행을 가속화하도록 설계된 Linux 커널에서 최초이자 가장 오래된 메커니즘입니다. vsyscall 개념의 작동 원리는 간단합니다. Linux 커널은 일부 변수와 일부 시스템 호출 구현이 포함 된 페이지를 사용자 공간에 매핑합니다. 이 메모리 공간에 대한 정보는 x86_64의 Linux 커널 문서에서 찾을 수 있습니다. :

ffffffffff600000 - ffffffffffdfffff (=8 MB) vsyscalls

or:

~$ sudo cat /proc/1/maps | grep vsyscall
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

그 후, 이러한 시스템 호출은 사용자 공간에서 실행되며 이는 [컨텍스트 전환]이 없음을 의미합니다. vsyscall 페이지의 매핑은 arch/x86/ entry/vsyscall/vsyscall_64.c 소스 코드 파일에 정의 된 map_vsyscall 함수에서 발생합니다. 이 함수는 Linux 커널을 초기화하는 동안에 arch/x86/kernel/setup.c 소스 코드 파일에 정의 된 setup_arch 함수에서 호출됩니다 (이 함수는 Linux 커널 초기화의 다섯 번째 [파트]에서 보았습니다).

map_vsyscall 함수의 구현은 CONFIG_X86_VSYSCALL_EMULATION 커널 설정 옵션에 따라 다릅니다. :

#ifdef CONFIG_X86_VSYSCALL_EMULATION
extern void map_vsyscall(void);
#else
static inline void map_vsyscall(void) {}
#endif

도움말 텍스트에서 읽을 수 있듯이 CONFIG_X86_VSYSCALL_EMULATION 설정 옵션은 vsyscall 모방을 가능하게 함 입니다. 왜 vsyscall을 모방할까요? 실제로 vsyscall은 보안상의 이유로 레거시 ABI입니다. 가상 시스템 호출의 주소는 고정되어 있습니다. 즉, vsyscall 페이지는 매번 같은 위치에 있으며 이 페이지의 위치는 map_vsyscall 함수에서 결정됩니다. 이 함수의 구현을 살펴봅시다. :

void __init map_vsyscall(void)
{
    extern char __vsyscall_page;
    unsigned long physaddr_vsyscall = __pa_symbol(&__vsyscall_page);
    ...
    ...
    ...
}

보시다시피, map_vsyscall 함수의 시작 부분에서 우리는 __pa_symbol 매크로를 가진 vsyscall 페이지의 물리적 주소를 얻습니다(우리는 이미 리눅스 커널 초기화 프로세스의 네 번째 [파트]에서 이 매크로가 구현 된 것을 보았습니다). 어셈블리 소스 코드 파일 arch/x86/entry/vsyscall/vsyscall_emu_64.S에 정의 된 __vsyscall_page 기호는 다음과 같은 가상 주소를 갖습니다. :

ffffffff81881000 D __vsyscall_page

.data..page_aligned, aw section에서 다음 세 가지 시스템 콜에 대한 호출을 포함합니다. :

  • gettimeofday;

  • time;

  • getcpu.

Or:

__vsyscall_page:
    mov $__NR_gettimeofday, %rax
    syscall
    ret

    .balign 1024, 0xcc
    mov $__NR_time, %rax
    syscall
    ret

    .balign 1024, 0xcc
    mov $__NR_getcpu, %rax
    syscall
    ret

우선 map_vsyscall 함수의 구현으로 돌아가고 나중에 __vsyscall_page의 구현으로 돌아갑시다. __vsyscall_page의 물리적 주소를 수신 한 후 vsyscall_mode 변수의 값을 확인하고 __set_fixmap 매크로를 사용하여 vsyscall 페이지의 fix-mapped 주소를 설정합니다. :

if (vsyscall_mode != NONE)
    __set_fixmap(VSYSCALL_PAGE, physaddr_vsyscall,
                 vsyscall_mode == NATIVE
                             ? PAGE_KERNEL_VSYSCALL
                             : PAGE_KERNEL_VVAR);

__set_fixmap 은 세 개의 매개변수를 가집니다.: 첫 번째는 fixed_addresses 열거형의 인덱스입니다. 우리의 경우 VSYSCALL_PAGEx86_64 아키텍처에 대한 fixed_addresses 열거형의 첫 번째 원소입니다. :

enum fixed_addresses {
...
...
...
#ifdef CONFIG_X86_VSYSCALL_EMULATION
    VSYSCALL_PAGE = (FIXADDR_TOP - VSYSCALL_ADDR) >> PAGE_SHIFT,
#endif
...
...
...

이는 511과 같습니다. 두 번째 매개변수는 매핑되어야하는 페이지의 실제 주소이고, 세 번째 매개변수는 페이지의 플래그입니다. VSYSCALL_PAGE의 플래그는 vsyscall_mode 변수에 의존합니다. vsyscall_mode 변수가 NATIVE이면 PAGE_KERNEL_VSYSCALL이고, 그렇지 않으면 PAGE_KERNEL_VVAR입니다. 두 매크로(PAGE_KERNEL_VSYSCALLPAGE_KERNEL_VVAR)는 다음 플래그로 확장됩니다. :

#define __PAGE_KERNEL_VSYSCALL          (__PAGE_KERNEL_RX | _PAGE_USER)
#define __PAGE_KERNEL_VVAR              (__PAGE_KERNEL_RO | _PAGE_USER)

이 플래그는 vsyscall 페이지에 대한 액세스 권한을 나타냅니다. 두 플래그는 동일한 _PAGE_USER 플래그를 가지며 이는 더 낮은 권한 레벨에서 실행되는 사용자 모드 프로세스에 의해 페이지에 액세스 할 수 있음을 의미합니다. 두 번째 플래그는 vsyscall_mode 변수의 값에 따라 다릅니다. vsyscall_modeNATIVE인 경우 첫 번째 플래그 (__PAGE_KERNEL_VSYSCALL)가 설정됩니다. 즉, 가상 시스템 호출은 기본적으로 syscall 명령입니다. 다른 방법으로 vsyscall_mode 변수가 emulate이면 vsyscall은 PAGE_KERNEL_VVAR을 갖습니다. 이 경우 가상 시스템 호출이 트랩으로 바뀌고 합리적으로 모방됩니다. vsyscall_mode 변수는 vsyscall_setup 함수에서 값을 가져옵니다.:

static int __init vsyscall_setup(char *str)
{
    if (str) {
        if (!strcmp("emulate", str))
            vsyscall_mode = EMULATE;
        else if (!strcmp("native", str))
            vsyscall_mode = NATIVE;
        else if (!strcmp("none", str))
            vsyscall_mode = NONE;
        else
            return -EINVAL;

        return 0;
    }

    return -EINVAL;
}

이는 초기 커널 매개 변수 파싱 중에 호출됩니다. :

early_param("vsyscall", vsyscall_setup);

early_param 매크로에 대한 자세한 내용은 Linux 커널 초기화 프로세스를 설명하는 챕터의 여섯 번째 파트에서 읽을 수 있습니다.

vsyscall_map 함수의 끝에서 vsyscall 페이지의 가상 주소가 BUILD_BUG_ON 매크로를 사용하여 VSYSCALL_ADDR의 값과 같은지 확인합니다. :

BUILD_BUG_ON((unsigned long)__fix_to_virt(VSYSCALL_PAGE) !=
             (unsigned long)VSYSCALL_ADDR);

이게 전부 입니다. vsyscall 페이지가 설정되었습니다. 위의 모든 결과는 다음과 같습니다. vsyscall = native 매개 변수를 커널 명령 행에 전달하면 가상 시스템 호출은 arch/x86/entry/vsyscall/vsyscall_emu_64.S에서 기본 syscall 명령으로 처리됩니다. glibc는 가상 시스템 호출 핸들러의 주소를 알고 있습니다. 가상 시스템 호출 핸들러는1024(또는 '0x400`) 바이트로 정렬됩니다. :

__vsyscall_page:
    mov $__NR_gettimeofday, %rax
    syscall
    ret

    .balign 1024, 0xcc
    mov $__NR_time, %rax
    syscall
    ret

    .balign 1024, 0xcc
    mov $__NR_getcpu, %rax
    syscall
    ret

vsyscall 페이지의 시작 주소는 항상 ffffffffff600000입니다. 따라서 glibc는 모든 가상 시스템 호출 처리기의 주소를 알고 있습니다. 이 주소들의 정의는 glibc소스 코드에서 찾을 수 있습니다. :

#define VSYSCALL_ADDR_vgettimeofday   0xffffffffff600000
#define VSYSCALL_ADDR_vtime           0xffffffffff600400
#define VSYSCALL_ADDR_vgetcpu          0xffffffffff600800

모든 가상 시스템 콜 요청은 __vsyscall_page + VSYSCALL_ADDR_vsyscall_name 오프셋에 속하며, 가상 시스템 호출 수를 rax 범용 [레지스터]((https://en.wikipedia.org/wiki/Processor_register)에 넣고 x86_64 syscall 명령의 기본 명령이 실행됩니다.

두 번째 경우, vsyscall = emulate 매개 변수를 커널 명령 행에 전달하면, 가상 시스템 호출 핸들러는 page fault 예외를 발생시킬 것입니다. 물론, vsyscall 페이지에는 실행을 금지하는 __PAGE_KERNEL_VVAR 액세스 권한이 있습니다. do_page_fault 함수는 # PF 또는 페이지 결함 처리기입니다. 마지막 페이지 결함의 원인을 이해하려고 시도합니다. 가상 시스템 콜이 호출되고 vsyscall모드가 emulate인 상황이 그 원인 중 하나 일 수 있습니다. 이 경우 vsyscallarch/x86/entry/vsyscall/vsyscall_64.c 소스 코드 파일에 정의 된 emulate_vsyscall 함수에 의해 처리됩니다.

emulate_vsyscall 함수는 가상 시스템 호출의 수를 가져 와서 확인한 후, 오류를 출력하고 세그먼트 폴트를 간단히 보냅니다. :

...
...
...
vsyscall_nr = addr_to_vsyscall_nr(address);
if (vsyscall_nr < 0) {
    warn_bad_vsyscall(KERN_WARNING, regs, "misaligned vsyscall...);
    goto sigsegv;
}
...
...
...
sigsegv:
    force_sig(SIGSEGV, current);
    reutrn true;

가상 시스템 호출 수를 확인 했으므로 access_ok 위반과 같은 또 다른 확인을 수행하고, 가상 시스템 콜 수에 따라 다른 시스템 콜 함수를 실행합니다. :

switch (vsyscall_nr) {
    case 0:
        ret = sys_gettimeofday(
            (struct timeval __user *)regs->di,
            (struct timezone __user *)regs->si);
        break;
    ...
    ...
    ...
}

결국 우리는 sys_gettimeofday 또는 다른 가상 시스템 호출 핸들러의 결과를 ax 범용 레지스터에 넣었다. 우리는 일반적인 시스템 호출로 했던 것처럼 instruction pointer 레지스터를 복원하고 8 바이트를 스택 포인터 레지스터에 추가한다. 이 작업은 ret 명령을 에뮬레이트합니다.

    regs->ax = ret;

do_ret:
    regs->ip = caller;
    regs->sp += 8;
    return true;

이것으로 끝입니다. 이제 현대적인 개념인 vDSO를 보겠습니다.

vDSO 소개

위에서 이미 언급했듯, vsyscall은 이제 쓸모없는 개념이며 vDSO 또는 virtual dynamic shared object로 대체되었습니다. vsyscallvDSO 메커니즘의 주된 차이점은 vDSO는 메모리 페이지를 공유 객체 form의 각 프로세스에 매핑하지만, vsyscall은 메모리에서 정적이며 매번 같은 주소를 갖는다는 것입니다. x86_64 아키텍처의 경우 이름은 linux-vdso.so.1입니다. glibc에 동적으로 연결되는 모든 사용자 공간 응용 프로그램은 vDSO를 자동으로 사용합니다. 예를 들면 다음과 같습니다. :

~$ ldd /bin/uname
    linux-vdso.so.1 (0x00007ffe014b7000)
    libc.so.6 => /lib64/libc.so.6 (0x00007fbfee2fe000)
    /lib64/ld-linux-x86-64.so.2 (0x00005559aab7c000)

또는 :

~$ sudo cat /proc/1/maps | grep vdso
7fff39f73000-7fff39f75000 r-xp 00000000 00:00 0       [vdso]

여기서 uname util이 세 개의 라이브러리와 연결되어 있음을 알 수 있습니다. :

  • linux-vdso.so.1;

  • libc.so.6;

  • ld-linux-x86-64.so.2.

첫 번째는 vDSO 기능을 제공하고, 두 번째는 C 표준 라이브러리이고, 세 번째는 프로그램 번역기입니다 (자세한 내용은 링커에 대해 설명하는 부분에서 읽을 수 있음). 따라서 vDSOvsyscall의 한계를 해결합니다. vDSO의 구현은 vsyscall과 유사합니다.

vDSO의 초기화는 arch/x86/entry/vdso/vma.c 소스 코드 파일에 정의 된 init_vdso 함수에서 시작합니다. 이 함수는 32비트 와 64비트에 대한 vDSO 이미지의 초기화에서 시작되며 CONFIG_X86_X32_ABI 커널 설정 옵션에 따라 다릅니다. :

static int __init init_vdso(void)
{
    init_vdso_image(&vdso_image_64);

#ifdef CONFIG_X86_X32_ABI
    init_vdso_image(&vdso_image_x32);
#endif

두 함수 모두 vdso_image 구조체를 초기화합니다. 이 구조체는 생성된 두 소스 코드 파일인 arch/x86/entry/vdso/vdso-image-64.carch/x86/entry/vdso/vdso-image-32.c에 정의되어 있습니다. 다른 소스 코드 파일에서 vdso2c 프로그램에 의해 생성 된 이러한 소스 코드 파일들은 int 0x80, sysenter 등과 같은 시스템 콜을 호출하는 다른 접근 방식을 보여줍니다. 이미지의 전체 세트는 커널 설정에 따라 다릅니다.

예를 들어 x86_64 Linux 커널의 경우 vdso_image_64가 포함됩니다. :

#ifdef CONFIG_X86_64
extern const struct vdso_image vdso_image_64;
#endif

But for the x86 - vdso_image_32:

#ifdef CONFIG_X86_X32
extern const struct vdso_image vdso_image_x32;
#endif

커널이 x86 아키텍처 또는 x86_64와 호환성 모드로 설정된 경우 int 0x80 인터럽트로 시스템 콜을 호출 할 수 있습니다. 호환성 모드가 활성화 된 경우에는 다른 방식으로 기본 syscall instruction 또는 sysenter 명령으로 시스템 콜을 호출 할 수 있습니다. :

#if defined CONFIG_X86_32 || defined CONFIG_COMPAT
  extern const struct vdso_image vdso_image_32_int80;
#ifdef CONFIG_COMPAT
  extern const struct vdso_image vdso_image_32_syscall;
#endif
 extern const struct vdso_image vdso_image_32_sysenter;
#endif

vdso_image 구조체의 이름에서 알 수 있듯이 시스템 콜 엔트리의 특정 모드에 대한 vDSO 이미지를 나타냅니다. 이 구조체는 항상 PAGE_SIZE (4096 바이트)의 배수인 vDSO 영역의 바이트 사이즈에 대한 정보, 텍스트 매핑에 대한 포인터, alternatives(특정한 타입의 프로세스를 위한 더 나은 대안의 명령어 세트)의 시작과 끝 주소 등을 포함합니다. 예를 들어 vdso_image_64 는 다음과 같습니다. :

const struct vdso_image vdso_image_64 = {
    .data = raw_data,
    .size = 8192,
    .text_mapping = {
        .name = "[vdso]",
        .pages = pages,
    },
    .alt = 3145,
    .alt_len = 26,
    .sym_vvar_start = -8192,
    .sym_vvar_page = -8192,
    .sym_hpet_page = -4096,
};

raw_data는 8 Kilobytes 크기 또는 2 페이지 크기인 64 비트 vDSO 시스템 호출의 raw 이진 코드를 포함하거나 : Where the raw_data contains raw binary code of the 64-bit vDSO system calls which are 2 page size:

static struct page *pages[2];

init_vdso_image 함수는 동일한 소스 코드 파일에 정의되어 있으며 vdso_image.text_mapping.pages 만 초기화합니다. 우선 이 함수는 페이지 수를 계산하고 주어진 주소를 page 구조체로 변환하는 virt_to_page 매크로로 각 vdso_image.text_mapping.pages [number_of_page]를 초기화합니다.

void __init init_vdso_image(const struct vdso_image *image)
{
    int i;
    int npages = (image->size) / PAGE_SIZE;

    for (i = 0; i < npages; i++)
        image->text_mapping.pages[i] =
            virt_to_page(image->data + i*PAGE_SIZE);
    ...
    ...
    ...
}

subsys_initcall 매크로에 전달 된 init_vdso 함수는 주어진 함수를 initcalls 목록에 추가합니다. 이 목록의 모든 함수는 init/main.c 소스 코드 파일의 do_initcalls 함수에서 호출됩니다. :

subsys_initcall(init_vdso);

자, 우리는 vDSO의 초기화와 vDSO 시스템 호출을 포함하는 메모리 페이지와 관련된 page 구조체의 초기화를 보았습니다. 그렇다면 이들의 페이지는 어디로 매핑될까요? 실제로 바이너리를 메모리에 로드할 때 커널에 의해 매핑됩니다. 리눅스 커널은 arch/x86/entry/vdso/vma.c 소스 코드 파일에서 arch_setup_additional_pages 함수를 호출하여 x86_64에 대해 vDSO가 활성화되어 있는지 확인하고 map_vdso 함수를 호출합니다. :

int arch_setup_additional_pages(struct linux_binprm *bprm, int uses_interp)
{
    if (!vdso64_enabled)
        return 0;

    return map_vdso(&vdso_image_64, true);
}

map_vdso 함수는 동일한 소스 코드 파일에서 정의되며 vDSO와 공유 vDSO 변수에 대한 페이지를 매핑합니다. 이것으로 끝입니다. vsyscallvDSO 개념의 주요 차이점은 vsyscall은 고정 주소 ffffffffff600000을 가지며 3 시스템 콜을 구현하는 반면,vDSO는 동적으로 로드되고 4 개의 시스템 콜을 구현한다는 것입니다. :

  • __vdso_clock_gettime;

  • __vdso_getcpu;

  • __vdso_gettimeofday;

  • __vdso_time.

끝났습니다.

결론

이것으로 리눅스 커널의 시스템 호출 개념에 대한 세 번째 파트의 끝이 났습니다. 이전의 파트에서 시스템 호출 전에 Linux 커널에서의 준비 구현과, 시스템 콜 핸들러에서의 exit 프로세스의 구현에 대해 논의했습니다. 이 파트에서 우리는 계속해서 시스템 콜 개념과 관련된 내용을 살펴보고 시스템 콜과 매우 유사한 두 가지 새로운 개념인 vsyscallvDSO를 배웠습니다.

이 세 파트을 모두 진행한 후에는 시스템 콜과 관련된 거의 모든 사항을 알고 시스템 콜이 무엇이며 사용자 응용 프로그램에 필요한 이유를 알고 있습니다. 또한 사용자 응용 프로그램이 시스템 콜을 호출 할 때 발생하는 일과 커널이 시스템 콜을 처리하는 방법도 알고 있습니다.

다음 파트는 이 챕터의 마지막 파트이며 사용자가 프로그램을 실행할 때 어떤 일이 발생하는지 볼 수 있습니다.

질문이나 제안 사항이 있으면 twitter에 의견이나 핑을 남겨주시거나. email 보내주시거나, issue를 만들어주세요.

영어는 제 모국어가 아닙니다. 그리고 여타 불편하셨던 점에 대해서 정말로 사과드립니다. 만약 실수를 찾아내셨다면 부디 linux-insides 원본으로, 번역에 대해서는 linux-insides 한글 번역으로 PR을 보내주세요.

Last updated