Post

System call / vDSO, vsyscall

System call

system call은 user가 Software Interrupt를 걸 수 있도록 user process에 제공되는 인터페이스(API)다. 대부분의 kernel에서는 user 어플리케이션이 kernel space에 직접 접근하지 못하도록 addr\_limit를 설정해 놓았다. 특정 프로세스가 kernel space를 수정하거나 하드웨어 디바이스를 직접적으로 조작하는 것을 막기 위해서다. 따라서 어플리케이션이 상기 작업을 처리해야 하는 경우 kernel이 대신 작업을 수행하도록 요청하고 그 결과를 반환받는 수 밖에 없는데, 이를 위해 kernel에서 제공하는 인터페이스가 system call이다.

일반적으로 많이들 사용하는 glibc 등도 내부적으로는 system call을 사용하기 때문에, system call의 wrapper라고 봐도 무방하다.

그렇다고 해서, system call 내부에서 다른 함수를 호출하지 않는다고 생각하면 안된다.

예를 들어, system call 내부에서는 driver에서 제공하는 함수 또는 vfs\_\* 계열 같은 다른 subsystem에서 제공하는 함수 등을 호출하게 된다. 이를 유저가 직접 호출하도록 하면 유저가 그런 세부적인 함수나 구현사항까지 파악하고 있어야 하기 때문에 불편하며 libc에서 처리하게 되면 빈번한 system call로 성능 저하가 우려되기 때문에 kernel mode에서 작업을 적절히 묶어 제공하는 것이 system call이라고 보면 된다.

system call 호출 시 kernel mode(ring 0)로 넘어가면서 CPU의 current privilege level(CPL)이 최상위 권한인 0으로 변경된다. ring 0에서 요청을 처리한 후 CPL을 3으로 내리고 결과를 반환하며 user mode(ring 3)로 돌아가게 된다.

system call method (instruction)

system call instruction 마다 구현 방법이나 system call handler를 호출하는 방식이 조금 씩 다르다.

   
 i386 / i686 binaryamd64 binary
legacy kernelint $0x80 / iretc syscall / sysret
vDSO를 제공하는 경우sysenter / sysexit 

int $0x80 system call process

32-bit legacy system call인 int $0x80은 Software Interrupt를 거는 방식으로 구현되어 있기 때문에, Interrupt 처리 루틴 대로 진행된다. Interrupt Descriptor Table(IDT) 확인하고 요청에 대응되는 Interrupt Service Routine( ISR) 을 주소를 얻어 이를 실행한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
mov $0xb, %al
int $0x80
========== enter kernel mode ==========
[ IDT ]
0x00
0x01
...
0x80    system\_call()    - ISR
---------------------------------------
[ system\_call()'s code : /source/arch/x86/entry/entry\_32.S]
ENTRY(entry\_INT80\_32)

ASM\_CLAC

pushl
%eax
/\* pt\_regs->orig\_ax \*/

SAVE\_ALL pt\_regs\_ax=$-ENOSYS
/\* save rest \*/

  


TRACE\_IRQS\_OFF

  


movl
%esp, %eax

call
do\_int80\_syscall\_32
....
---------------------------------------
[/source/arch/x86/entry/common.c]
\_\_visible void do\_int80\_syscall\_32(struct pt\_regs \*regs) {

enter\_from\_user\_mode();

local\_irq\_enable();

do\_syscall\_32\_irqs\_on(regs);
}
---------------------------------------
[/source/arch/x86/entry/common.c]
static \_\_always\_inline void do\_syscall\_32\_irqs\_on(struct pt\_regs \*regs) {
....
if (likely(nr < IA32\_NR\_syscalls)) {
regs->ax = ia32\_sys\_call\_table[nr](
(unsigned int)regs->bx, (unsigned int)regs->cx,
(unsigned int)regs->dx, (unsigned int)regs->si,
(unsigned int)regs->di, (unsigned int)regs->bp);
}
....
---------------------------------------
[ sys\_call\_table : /source/arch/x86/entry/syscall\_32.c]
\_\_visible const sys\_call\_ptr\_t ia32\_sys\_call\_table[\_\_NR\_syscall\_compat\_max+1] = {
[0 ... \_\_NR\_syscall\_compat\_max] = &sys\_ni\_syscall,    // == -ENOSYS  ( 초기화 )
#include <asm/syscalls\_32.h>          // 내부 macro에 의해 결과적으로 아래 코드로 변환
[0] = sys\_read,
[1] = sys\_write,
[2] = sys\_open,
...
[11] = sys\_execve,
...
};

SYSCALL system call process

int $0x80은 Software Interrupt 기반이라 속도가 느리다. 따라서 요즘은 SYSENTER를 사용한다. SYSENTERSYSCALL 같은 경우는 레지스터를 설정하기 위해 MSR(Model Specific Register)을 사용한다.

1. kernel initialization process를 진행하면서,

system call로 kernel mode에 진입할 때 General Purpose Registers(rip, rsp, cs, ss, ...)에 로드되어야 하는 값들(system call entry point, kernel stack, …)을 MSR에 저장해 놓는다.

1
2
wrmsrl(MSR\_LSTAR, entry\_SYSCALL\_64)

2. system call 호출 시

MSR\_\*STAR에 저장된 값(system call entry point)을 가져와 rip에 로드한다.

3. system call entry point에서

/v4.13.8/source/arch/x86/entry/entry_64.S preparation 과정 → system call handler 호출 → 마무리 작업 이후 sysret

  1. swapgs
  2. user context push
  3. General Purpose Register를 kernel mode에 알맞게 로드
  4. 몇 가지 check를 수행
  5. call \*sys\_call\_table(, %rax, 8) ( system call handler )
  6. user context restore
  7. swapgs
  8. sysret

vDSO(virtual Dynamic Shared Object)

kernel은 어떤 프로세스가 메모리에 loading될 때 마다 kernel space에 존재하는 \_\_vsyscall\_page를 user space에 mapping해주는데, 이 것이 vDSO다. 이 때 shared object 형태로 mapping되며, random address에 배정된다. shared object 형태이기 때문에 ELF format을 따른다.

1
2
3
32bit binary  :  linux-gate.so.1 (0xb7fd9000)
64bit binary  :  linux-vdso.so.1 (0x00007ffff7ffd000)

Note ) 어떤 vDSO가 mapping되느냐는 kernel arch가 아니라 binary가 결정한다. 즉, 32bit binary를 x86_64 환경에서 실행하나, x86 환경에서 실행하나 같은 vDSO가 mapping된다.

Note ) random address에 배정되기는 하지만, auxv를 이용하면 주소를 구할 수 있다.

1
2
3
#include <sys/auxv.h>
void \*vdso = (uintptr\_t) getauxval(AT\_SYSINFO\_EHDR);

vDSO의 역할은 크게 2가지로 볼 수 있다.

1. __kernel_vsyscall : 적절한 system call method를 선택하는 역할 ( x86 binary )

legacy system call int 0x80같은 경우 full interrupt-handling paths를 타야하기 때문에 overhead가 꽤 크다. 이를 해결하기 위해 sysenter가 고안되면서 system call method가 두 개가 되어버려 어떤 system call method를 선택할지를 결정하는 루틴이 필요해졌다. vDSO가 없다면 이를 library에서 처리했어야 하겠지만, 이를 vDSO의 \_\_kernel\_vsyscall에서 처리하기 때문에 library에서는 이를 신경 쓸 필요 없이 그냥 system call을 호출하면 된다.

1
2
3
4
5
6
0xb7ed0cec <\_\_write\_nocancel+18>:    call   DWORD PTR gs:0x10
or                                   call \*\_dl\_sysinfo  ( if static )
...
0xb7fd9ce5 <\_\_kernel\_vsyscall+5>:    sysenter
0xb7fd9ce7 <\_\_kernel\_vsyscall+7>:    int    0x80

  • sysenter는 vDSO에서만 사용하는 것이 원칙이다. 따라서 library 등에서는 전역 변수(\_dl\_sysinfo)나 레지스터(gs)를 이용해 vDSO에 존재하는 \_\_kernel\_vsyscall을 호출하고, 여기서 sysenter 하게 된다.
  • \_\_kernel\_vsyscall의 주소는 동적으로 결정된다.
  • %gs는 TCB(Thread Control Block)을 가리킨다. TCB의 0x10위치에 ` AT_SYSINFO`가 있다.
  • \_dl\_sysinfo는 dynamic loader global variable로, vsyscall의 주소를 저장하고 있다.

Note ) x86_64 binary는 syscall instruction만 사용하기 때문에 이 기능이 필요없다. 그래서 x86_64 binary에 mapping되는 vDSO에는 \_\_kernel\_vsyscall이 없다.

2. calling overhead를 줄여주는 역할 ( x86, x86_64 binary )

system call 하는 것에도 일종의 overhead가 존재하는데, kernel mode와 user mode를 왔다갔다 할 때 마다 context switching해야 하기 때문이다. vDSO는 user space에 mapping되기 때문에, user mode에서 실행해도 상관없는 몇몇 system call의 실제 구현부를 vDSO에 넣으면 이 system call은 context switch없이 user mode에서 처리할 수 있게 된다. 이는 원래 x86_64의 vsyscall의 기능이었으나 vDSO에도 포함되었다.

Note ) 모든 system call implementation이 vDSO에 들어간다고 생각하면 안되는게, vDSO로 지원하지 않는 system call의 경우 일단 vDSO로 이동한 다음 결과적으로는 모두 \_\_kernel\_vsyscall로 이동해 sysenter하게 되기 때문에, kernel mode로 context switch하게 된다.

architecture에 따라 vDSO에서 제공하는 함수가 다르지만, 대체로 다음 네 가지 정도를 포함한다.

  • clock\_gettime()
  • gettimeofday()
  • time()
  • getcpu()

vsyscall ( x86_64 obsolete ) 원래 x86_64에서 calling overhead를 줄여주는 역할을 수행했으나, 모든 프로세스에 매번 같은 주소로 mapping된다는 단점 때문에 해당 기능을 vDSO가 대신하면서 더 이상 사용하지 않는다.

This post is licensed under CC BY 4.0 by the author.