System call / vDSO, vsyscall
System call
대부분의 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)
|
i386 / i686 binary |
amd64 binary |
legacy kernel |
``c int $0x80 / iret`` |
``c syscall / sysret`` |
vDSO를 제공하는 경우 |
``c sysenter / sysexit`` |
int $0x80 system call process
32-bit legacy system call인 ``c int $0x80``은 Software Interrupt를 거는 방식으로 구현되어 있기 때문에, Interrupt 처리 루틴 대로 진행된다. Interrupt Descriptor Table(IDT) 확인하고 요청에 대응되는 Interrupt Service Routine(ISR)을 주소를 얻어 이를 실행한다.
```c
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
1. kernel initialization process를 진행하면서,
system call로 kernel mode에 진입할 때 General Purpose Registers(``c rip, rsp, cs, ss, ...``)에 로드되어야 하는 값들(system call entry point, kernel stack, ...)을 MSR에 저장해 놓는다.
```c
wrmsrl(MSR_LSTAR, entry_SYSCALL_64)
```
2. system call 호출 시
3. system call entry point에서
- swapgs
- user context push
- General Purpose Register를 kernel mode에 알맞게 로드
- 몇 가지 check를 수행
- ``c call *sys_call_table(, %rax, 8)`` ( system call handler )
- user context restore
- swapgs
- sysret
vDSO(virtual Dynamic Shared Object)
kernel은 어떤 프로세스가 메모리에 loading될 때 마다 kernel space에 존재하는 ``c __vsyscall_page``를 user space에 mapping해주는데, 이 것이 vDSO다.
이 때 shared object 형태로 mapping되며, random address에 배정된다.
shared object 형태이기 때문에 ELF format을 따른다.
```bash
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를 이용하면 주소를 구할 수 있다.
```c
#include <sys/auxv.h>
void *vdso = (uintptr_t) getauxval(AT_SYSINFO_EHDR);
```
1. __kernel_vsyscall : 적절한 system call method를 선택하는 역할 ( x86 binary )
- ``c sysenter``는 vDSO에서만 사용하는 것이 원칙이다.
따라서 library 등에서는 전역 변수(``c _dl_sysinfo``)나 레지스터(`` gs``)를 이용해 vDSO에 존재하는 ``c __kernel_vsyscall``을 호출하고, 여기서 `` sysenter`` 하게 된다. - ``c __kernel_vsyscall``의 주소는 동적으로 결정된다.
- ``c %gs``는 TCB(Thread Control Block)을 가리킨다. TCB의 ``c 0x10`` 위치에 `` AT_SYSINFO``가 있다.
- ``c _dl_sysinfo``는 dynamic loader global variable로, `` vsyscall``의 주소를 저장하고 있다.
2. calling overhead를 줄여주는 역할 ( x86, x86_64 binary )
architecture에 따라 vDSO에서 제공하는 함수가 다르지만, 대체로 다음 네 가지 정도를 포함한다.
- ``c clock_gettime()``
- ``c gettimeofday()``
- ``c time()``
- ``c getcpu()``
'OS > Kernel' 카테고리의 다른 글
[kernel] LKM, Loadable Kernel Module / Kernel Compile (0) | 2017.10.14 |
---|---|
[kernel] virt_to_phys (0) | 2017.10.13 |
[kernel] exploit (0) | 2017.10.06 |
Interrupt (0) | 2016.10.07 |
Preemptive / Non-preemptive ( 선점형 / 비선점형 스케줄링 ) (0) | 2016.09.18 |