본문으로 바로가기

[Kernel] Linux kernel (12) - system call

category Kernel & Browser/Kernel 2020. 12. 11. 12:54

system call 호출 시 어떤 일이 일어날까?

일단 system call에 대하여 설명하기 전에, 우리는 커널(Kernel) - 운영체제에 대하여 알고있어야 합니다.

커널은 본질적으로 소프트웨어(main으로 시작하는 바이너리)입니다.


하지만, 모든 프로그램과는 다르게 커널은 메모리(CPU)에 항상 올라가 있다는 차이점이 있습니다. 커널이 아닌 프로그램들은 메모리에 있어도 상관없고, 없어도 됩니다. 필요할 때 마다 메모리에 올려 사용하면 된다는 의미이죠.

또한, UNIX 운영체제는 다중 사용자 운영체제 입니다. 따라서, 보안성이 정말 중요합니다. 만약, 다른 사용자가 나의 문서를 삭제한다던가, 원하는 대로 내 문서를 훔처보지 못하도록 막는 것 입니다.


그래서 UNIX에서 실질적인 파일 I/O(Input/Oupt)는 커널밖에 하지 못합니다. 즉, 유저가 작성한 프로그램이 필요할 때 마다 커널에게 I/O 권한을 요청하는 것 입니다. 이 과정을 System call이라고 합니다.


특히, CPU(하드웨어)에는 현재 모드가 커널 모드인지, 유저 모드인지 판별해주는 하나의 비트가 있습니다. 이 비트를 mode bit라고 합니다. 모드비트가 커널모드라면 모든 메모리에 접근 할 수 있습니다. 하지만, 유저 모드라면 자기의 메모리만 접근 할 수 있는것입니다.

간단하게 stdin(입력)과 stdout(출력)에 값을 쓰는 프로그램을 하나 작성하여 System call이 어떻게 작동하는지 알아봅시다.


#include <stdio.h>
int main(void)
{
char buf[0x100] = {0,};

scanf("%s",&buf); // stdin
printf("Hello, World!\n"); // stdout
}

작성한 프로그램은 위와 같습니다.

Linux 커널 버전 등에 의해 사소한 작동은 달라 질 수 있겠지만, System call의 기본적인 작동 원리에 대하여 알아보는 작업이므로 구체적인 환경은 적지 않겠습니다.

먼저, 해당 프로그램에서 I/O를 사용하는 부분은 두가지 입니다.


  1. scanf를 통하여 stdin에 값을 씀 -> 후에 이 값을 buf변수에 넘김

  2. printf를 통하여 stdout에 값을 씀 -> 문자열이 출력됨.


이렇게 두가지의 부분을 분석 해 봅시다. 그 전에, main함수의 어셈블리 코드를 확인 해 보았습니다.

pwndbg> disass main
Dump of assembler code for function main:
  0x00000000004005f6 <+0>: push   rbp
  0x00000000004005f7 <+1>: mov    rbp,rsp
  0x00000000004005fa <+4>: sub    rsp,0x110
  0x0000000000400601 <+11>: mov    rax,QWORD PTR fs:0x28
  0x000000000040060a <+20>: mov   QWORD PTR [rbp-0x8],rax
  0x000000000040060e <+24>: xor    eax,eax
  0x0000000000400610 <+26>: lea    rdx,[rbp-0x110]
  0x0000000000400617 <+33>: mov    eax,0x0
  0x000000000040061c <+38>: mov    ecx,0x20
  0x0000000000400621 <+43>: mov    rdi,rdx
  0x0000000000400624 <+46>: rep stos QWORD PTR es:[rdi],rax
  0x0000000000400627 <+49>: lea    rax,[rbp-0x110]
  0x000000000040062e <+56>: mov    rsi,rax
  0x0000000000400631 <+59>: mov    edi,0x4006f4
  0x0000000000400636 <+64>: mov    eax,0x0
  0x000000000040063b <+69>: call   0x4004e0 <__isoc99_scanf@plt>
  0x0000000000400640 <+74>: mov    edi,0x4006f7
  0x0000000000400645 <+79>: call   0x4004b0 <puts@plt>
  0x000000000040064a <+84>: mov    eax,0x0
  0x000000000040064f <+89>: mov    rsi,QWORD PTR [rbp-0x8]
  0x0000000000400653 <+93>: xor    rsi,QWORD PTR fs:0x28
  0x000000000040065c <+102>: je     0x400663 <main+109>
  0x000000000040065e <+104>: call   0x4004c0 <__stack_chk_fail@plt>
  0x0000000000400663 <+109>: leave  
  0x0000000000400664 <+110>: ret  

첫번째로 눈에 띄는 점은 printf가 아니라 puts를 사용한다는 점 입니다. 분명 코드에는 printf를 사용 했는데 말입니다.

그것은 바로 printf("123\n")꼴의 함수를 컴파일러가 puts로 인식하여 자동으로 최적화 해 준 것입니다. printf함수는 스택(메모리)사용량이 큰 함수이기 때문입니다.


pwndbg> b*main+69
Breakpoint 1 at 0x40063b
pwndbg> b*main+79
Breakpoint 2 at 0x400645

각각의 함수에 breakpoint를 걸고, 분석을 진행 해 보겠습니다.


scanf()

scanf에서는 내부적으로 _IO_vfscanf함수를 호출합니다.


또한, 몇가지 루틴을 따라가면 _IO_new_file_underflow함수를 호출합니다.


최종적으로는 __GI__IO_file_read함수를 호출하게 됩니다. 이 함수가 바로 read함수 입니다. read함수는 system call을 사용하는 함수이고, 결국 scanf는 리눅스 환경에서 이렇게 read함수로 변경되어 동작한다 라는 것을 알 수 있습니다.


결국, 0번 fd(stdin)에 값을 쓰고, 그 값을 0x602010에 넘겨주는 작업이 시행됩니다. 이 작업은 우리가 작성했던 코드에서의 scanf의 역할과 완벽히 일치합니다.

또, 여기서 rax의 값이 0으로 세팅되었는데, 이 값을 syscall number라고 합니다. 커널 내부에는 syscall table이 배열형태로 존재하는데, 여기서 해당 배열을 rax를 통해서 접근합니다.

각각에 인덱스에는 system call function들이 매핑되어있고, 여기서 0번 인덱스는 read함수 인 것입니다.


puts()

이번에는 puts입니다.


puts함수는 내부적으로 _IO_new_file_xsputn함수를 먼저 호출합니다.


그 후에는 _IO_new_file_overflow함수가 호출됩니다.


최종적으로는 _IO_do_write로 jmp하게 됩니다. 여기서 write는 system call을 사용하는 함수입니다. read함수와 마찬가지로 syscall을 사용하게 된다는 것을 알 수 있습니다.


결론

scanfputs같이 stdin,stdout 등 파일에 값을 쓰는 함수는 내부적으로 커널의 도움을 빌려서 syscall을 사용합니다.

과정은 아래와 같습니다.

  1. 유저 프로그램이 system call을 호출함

  2. mode bit가 커널 모드로 바뀜

  3. 함수의 syscall number 확인

  4. syscall table에서 올바른 syscall number인지 검사

  5. system call 함수의 주소를 가져옴

  6. 함수를 실행시켜 작업함

  7. 유저 프로그램의 영역으로 돌아가고, mode bit를 다시 유저 영역으로 바꿈

UNIX/LINUX운영체제는 멀티 유저 시스템으로 작동하고 보안성이 몹시 중요하기때문에 파일에 값을 읽고 쓰는 행위는 오직 kernel만이 할 수 있습니다.

따라서 syscall을 통해 kernel의 기능을 빌려 사용합니다.