시스템 콜(system call) 개요 – 리눅스 x86-64 어셈블리어 1

시스템 콜(system call)

시스템 콜은 유저 공간(user space)과 커널(kernel)이 상호작용하는 인터페이스(interface)입니다. 음… 이 인터페이스라는 말을 한글로 대체하고 싶은데 100% 대체가 가능한 단어가 잘 떠오르지 않습니다. 인터페이스는 인터페이스이니까요. 요즘 많이들 이용하는 API와도 비슷한 개념입니다.

어셈블리어의 어려움

어셈블리어는 고수준 언어들(high level languages)처럼 적당히 문법만 알면 직관적으로 시작할 수 있는 언어와는 조금 거리가 있습니다. 예를 들어 거의 모든 프로그래밍 언어를 시작하는 국룰은 ‘Hello World’ 입니다. C언어 창시자인 데니스 리치와 그의 동료 브라이언 커니핸이 1978년에 공동 저술한 <C언어 프로그래밍>의 전통을 따르는 것 입니다. 뭔가를 모니터 스크린에 출력함으로써 사람들은 직관적으로 아! 내가 코딩한 글자가 스크린에 뜨는구나- 라는 감명을 받고 어떻게 프로그래밍을 해야할 지 감을 잡을 수 있습니다. 그러기 위해서 C에서 필요한 코드는…

#include<stdio.h>

int main()
{
    printf("Hello World!");

    return 0;
}

이 정도 입니다. 컴퓨터를 모르는 사람은 알 수 없겠고, 영어를 모르는 사람도 알 수 없을 겁니다. 반면 영어를 아는 사람은 70%는 직감적으로 이해할 겁니다.

include – 포함해 stdio.h라는 파일을, int main 뭔지 모르겠지만 이게 메인인 것 같고, printf(“Hello World”) 누가 봐도 네이티브 영어 사용자는 print – 출력하다 Hello World 란 단어를 출력하는 것으로 알 수 있습니다. 마지막으로 return 0은 0을 돌려준다는 말인데 여기까지 보면 컴퓨터를 모르는 사람도 영어만 알면 70% 이상 추리가 가능합니다. 센스가 좋은 사람은 프로그래밍을 전혀 배우지 않아도 원리를 알 수 있을 겁니다.

약간 이게 한글을 사용하는 한국 사람들의 비애인데… 한국 사람들은 대체로 영어에 약합니다. 그래서 C언어 코드를 직감적으로 해석하기 전에 강좌의 해설을 먼저 듣게 되지요. 뭐냐면 C언어를 개발한 데니스 리치는 C언어를 사람이 이해할 수 있는 언어로 만들었습니다. 왜 그랬을까요?

어셈블리어로 콘솔에 같은 메시지를 출력하려면 아래와 같습니다. 여기서 답이 나오지요. 아래의 코드는 네이티브 영어 사용자라도 이해할 수가 없습니다. C언어는 이해가 가능한데 어셈블리어는 안됩니다.

이것을 영어 사용자의 입장에서 보면 엄청난 차이가 있습니다. 지금 아래의 코드는 CPU에 전달할 명령어(instruction)를 CPU에 가까운 방식으로 나열한 것 입니다. 여기서 눈여겨 볼 것은 syscall이라는 명령줄입니다. C언어는 syscall 이 없이 그냥 printf 라는 함수가 있습니다.

section .data
    msg: db "Hello World!",10
    len: equ $-msg
    
section .text
    global _start
_start:
    mov rax, 1
    mov rdi, 1
    mov rsi, msg
    mov rdx, len
    syscall

    mov rax, 60
    mov rdi, 0
    syscall

syscall 은 system call의 약자로 운영체제의 커널에 요청을 보내는 명령어입니다. 응용 프로그램은 소프트웨어 인터럽트라는 과정을 통해 운영체제에게 필요한 요청을 전달하는데 이것이 시스템 콜입니다. printf 는 기계어에서 원초적으로 일어나는 syscall을 감싸는 랩퍼(wrapper)라고 볼 수도 있습니다. 위의 코드를 보면 알겠지만 사람이 효율적으로 사용할 수 있는 방법이 아닙니다. 반면 printf 함수는 잘 몰라도 사람이 좀 더 이해할 수 있는 형태로 되어 있습니다.

추상성(abstraction)과 가상화(virtualization)

컴퓨터 시스템은 하드웨어와 소프트웨어의 추상화(abstraction) 단계를 높이는 방식으로 발달합니다. 기계에 가까울 수록 저수준(low level)이고 사람에 가까울 수록 고수준(low level)입니다. 개별 특성의 추상성이 어떤 임계점을 넘어설 때 완전히 다른 새로운 기계(하드웨어, 소프트웨어 적으로)가 만들어지는데 이는 가상화(Virtualization)라고 부를 수 있습니다. 가상화는 VR(Virtual Reality)같은 장비로 이젠 익숙할 것 입니다.

컴퓨터 공학에서 가상화라고 하면 첫 번째로 생각나는 것은 가상 메모리입니다.(Virtual Memory) 예를 들어 8기가의 삼성전자 메모리를 사서 PC에 설치하면 이 시스템에는 8기가의 물리적 메모리가 있습니다. 그런데 고화질 동영상을 재생하는 등 멀티태스킹을 하다보니 금방 8기가가 다 점유되버렸습니다. 그런 경우에도 PC는 느려질 망정 다운되거나 하지 않습니다. 왜냐하면 500기가의 하드디스크의 일부분을 메모리로 바꾸는 가상 메모리를 만들었기 때문입니다. 분명 나는 8기가의 메모리에만 돈을 지불했는데 그보다 많은 메모리를 사용하고 있습니다. 뭔가 이상하지요. 메모리의 특성을 하드디스크라는 가상의 공간에 똑같이 구현했기 때문입니다.

일단 가상메모리가 설치되면 CPU는 데이타를 가져오는데 그게 물리적으로 삼성 8기가 램이건 500기가 하드이건 간에 전혀 신경쓰지 않습니다. CPU는 명령어(instruction)를 처리하면 그만이고 중간에 가상화 장치만 돌아가면 그만입니다. CPU는 메모리에서 데이타를 가져온다고 착각을 하고 있는 것이지요. 이것이 가상화입니다. 착각을 한다 -> CPU를 속인다. 속인다면 웬지 나쁜 짓 같은데 그렇지 않습니다. 나는 8기가의 메모리에 돈을 지불했는데 단가가 싼 하드디스크를 통해 더 많은 메모리를 사용할 수 있습니다. 단지 CPU는 메모리에서 데이터를 가져오는 것보다 한참 늦어진 것에 의아해할 수는 있겠지요(CPU에게 의식이 있다면…)

좀더 인간적인 비유를 하면 VR 가상현실에서 사람의 뇌는 내가 컴퓨터 앞에 앉아있는게 아니라 새로운 현실 속에 있는 것 처럼 착각을 합니다. 요즘 유행하는 가상인간(?)들도 마찬가지 겠지요. 실제 없는 사람이 있다고 믿으니까 광고효과가 나옵니다. 이제 가상인간이 라이브 커머스도 한다고 하지요. 사람이 없는데 가상적으로 만들었습니다.

앤드루 타넨바움 교수는 컴퓨터 구조에 대한 그의 저서에서 가상 기계 레벨에 대한 이론을 설명했는데 가상기계는 n 차까지 만들 수 있다고 합니다. 즉 이론상 가상화는 무한대 레벨이 가능하다고 합니다.

(타넨바움 교수는 리누스 토발즈가 리눅스 커널을 개발하기 위해 사용한 0386 아키텍처 미닉스 운영체제를 개발했다)

저수준 언어인 어셈블리어와 고수준 언어인 C언어를 비교할 때 프로그래밍 언어의 추상성과 가상화에 대해서 생각할 필요가 있습니다. 최근에는 파이썬과 자바 스크립트 같은 인터프리터 언어들이 나오는데 그 특성을 잘 보면 다음 단계의 가상화가 진행된다는 것을 볼 수 있습니다. 그러면 거기서 끝나느냐? 그렇지 않을 겁니다. 타넨바움 교수의 이론 처럼 시간에 따라 n차로 진행될 것으로 예상할 수 있습니다.

뭐 요즘엔 구글과 MS가 웬만한 엔지니어 보다 코딩을 잘하는 AI를 개발하고 있다고 합니다. 이런 것도 n차 가상화 진행의 한 단계라 볼 수도 있겠지요. 중요한 것은 다음 차수는 어떻게 바뀔지 짐작은 가지만 완벽한 예측은 어렵다는 것 입니다.

커널(Kernel)

운영체제의 심장을 커널(Kernel)이라고 이야기 합니다. 컴퓨터의 전원을 올리면 BIOS(Basic Input Output System)가 시작되고 부트로더가 커널이 작동할 수 있는 환경을 세팅합니다. 운영체제의 부팅이 끝나면? 이제 사용자가 컴퓨터를 사용할 수 있도록 서비스가 준비되야 하는데 커널이 그 일을 합니다. 뭐라고 할까… 또 인간적인 비유를 하자면 사용자는 PC를 키고 앉아서 부팅이 되기를 기다립니다. 부팅이 끝나면 이제 키보드를 치건 마우스를 클릭하건 자유가 주어지는데 이 모든 것은 커널이 수행하는데 개인 비서같은 것 입니다.

메모장에 타이핑을 하건 마우스를 움직이건 시스템 콜(system call)을 통합니다. 시스템 콜의 요청을 받고 처리하는 것은 시스템 콜 핸들러(system call handler)입니다. 그게 워낙 빨라서 우리는 잘 감지를 못하지만 우리가 PC를 사용하고 있는 동안 커널은 계속적으로 시스템 콜을 받아 처리하고 있는 것 입니다. 일반 GUI 프로그램도 사용자가 마우스를 움직일 때 초당 수백회가 넘는 시스템 콜을 처리하고 있습니다.

이렇듯 커널은 상시 대기하고 있는데 이 커널을 누가 마음대로 조작할 수 있다면 큰일입니다. 그런 권한은 PC의 주인에게도 함부로 주지 않습니다. 왜냐하면 시스템이 뻑날 수 있기 때문입니다. 예전에 윈도우 파란화면에서 많이 봤었지요. (파란화면 오류가 심하면 컴퓨터 자체가 망가지기도 했다) 커널을 사용할 때는 시스템 콜을 사용해야 합니다. 다시 말하면 사용자는 시스템 콜을 통해서 운영체제의 각종 자원을 사용할 수 있습니다. 그런데 시스템 콜을 무리하게 사용하다 커널이 멈추면 시스템에 문제가 되기 때문에 자연히 엄격한 규칙이 생겨납니다. 리눅스에는 system call table 이 있습니다.

사용자 공간(User Space)

컴퓨터 시스템은 보호 레벨에 따라 protection ring 으로 구분되어 있습니다. 이를 protected ring 혹은 protection ring 이라고 하는데 0부터 3까지 있습니다. 0은 kernel 로 모든 영역에 접근이 가능하고. ring 1, 2는 드라이버 영역입니다. ring 3이 사용자 공간(user space) 으로 응용프로그램에게 주어진 공간입니다. Protection ring에 의하여 사용자 공간의 권한(privilege)은 제한되어 있습니다. 예를 들어 user1이 사용자 공간에서 user2의 파일을 마음대로 접근하고 수정할 수 있다면 문제가 될 것 입니다. 다른 프로그램의 메모리를 읽고 해킹도 마음대로 할 수 있을 겁니다.

Protection Ring

여기서 Ring 3의 사용자 공간은 커널에 요청을 함으로써 시스템의 자원들을 사용할 수 있습니다. 그 요청의 의름이 system call 입니다. 시스템 콜을 받으면 커널이 알아서 처리해야 합니다. 단순한 일 같지만 커널의 사용자(프로세스)가 여러개 있을 때도 하드웨어 등 자원의 사용을 배분하고 시스템 전체에 문제가 없도록 처리해야 하는 어려운 일입니다.

어떻게 보면 커널이 사용자 공간에 대해서 하는 일은 클라이언트가 웹서버에 요청하는 것과 비슷합니다. 요청하고 응답받는 서로 역할이 나뉘어 있습니다. 커널과 시스템 콜은 매우 방대한 내용이기 때문에 이 포스팅에서 자세한 내용을 다루기는 어렵습니다만 한 가지 꼭 알아야할 것은 커널이 사용자 공간에서 시스템 콜을 받아서 처리한다는 개념입니다.

시스템 콜은 커널 버전에 따라 다르지만 현재 500개 이상의 시스템 콜이 있습니다 리누스 토발즈의 깃허브에서 syscall (시스템 콜을 줄여서) 테이블을 확인할 수 있습니다.

linux/syscall_64.tbl at master · torvalds/linux (github.com)

리눅스 시스템 콜 테이블

Chromium OS Docs – Linux System Call Table (googlesource.com)

system call table을 보면 인수(arg)가 있고 레지스터(rax …)가 있는데 거기 써있는 내용들은 C언어와 어셈블리어의 짬뽕입니다. 어쨋든 이 내용을 이해하면 C언어나 어셈블리어로 시스템 콜을 사용할 수 있습니다.

리눅스에서 man man 을 실행하면 2번이 시스템 콜에 관한 메뉴얼입니다. (function provided by the kernel – 커널에서 제공하는 함수)

리눅스에서 시스템 콜 프로그래밍에 관한 메뉴얼은 다음과 같이 검색합니다.

  1. 시스템 콜 이름을 확인한다
  2. [man 2 시스템 콜 이름] 의 형식으로 메뉴얼을 체크한다

man 은 manual의 약자이고 2는 system call section 입니다. 예를 들어 시스템 콜 write 에 대해 알고 싶다면 man 2 write 라고 칩니다. 아래와 같은 메뉴얼이 나옵니다. SYNOPSIS는 C언어 에서의 사용법입니다. 어셈블리어에서는 좀 더 원초적으로 system call table을 보고 사용합니다만, C언어의 최적화가 워작 잘되어 있어서 어슬픈 어셈블리어 코드보다 성능이 뛰어나다는 것은 사실입니다. 어차피 리눅스에서 어셈블리어를 하더라도 C언어의 함수들을 사용할 것이니까 이 메뉴얼에 유의할 필요가 있습니다.

온라인에도 문서가 있으니까 참고 합니다.

write(2) – Linux manual page (man7.org)

요약

리눅스 시스템 콜(system call)의 개요를 대략적으로 설명했습니다. 어셈블리어로 무언가 프로그램을 작성하는 일은 어렵지 않습니다만, 이것이 동작하는 이유를 모르면 별 소용이 없기 때문에 컴퓨터 구조에 대해서 어느 정도 지식이 필요합니다. 그래서인지 어셈블리어에 대한 교과서들은 보통 절반 정도를 이론적인 설명에 할애합니다. 어떤 교재는 보다 보면 이게 컴퓨터 구조 교과서인지 프로그래밍 교본인지 헷갈리기도 합니다. 어셈블리어가 시스템 프로그래밍 언어다 보니까 그런 부담이 있습니다. 반면 장점도 있는데 CPU와 운영체제를 이해하는데는 역시 어셈블리어가 상당히 도움이 될 수 있다고 봅니다.

리눅스에는 GNU 어셈블러도 있지만 NASM 이 시작하기 편합니다. 이번 어셈블리어 시리즈는 NASM x86-64 시리즈를 중심으로 만들 계획입니다. 어셈블러 자체의 지시자나 기능이 따로 있지만 핵심은 기계어와 대응이기 때문에 하나의 어셈블리어를 할 수 있게 되면 다른 어셈블리어도 어렵지 않게 적응할 수 있을 겁니다.

이 포스팅 시리즈는 다른 언어들 보다 아키텍쳐나 운영체제 등 배경 지식에 대한 설명이 필요한데 완벽하지는 않아도 그때그때 적당히 설명할 생각입니다.

참고문서

Protection ring – Wikipedia

The Linux Kernel documentation

syscalls(2) – Linux manual page (man7.org)

Leave a Comment