0. 도입
System Call이란 OS에서 제공된 programming interface이다. User가 함부로 I/O와 같은 Privileged 한 instruction을 수행할 수 없게 하기 위한 보호 장치라고 할 수 있다. System Call이 발생하면 user mode에서 kernel mode로 바뀌면서 interrupt(trap)이 발생한다. 보통은 API를 사용하여 System Call을 호출하고 이에 해당하는 작업을 kernel이 수행한다.
오늘은 이러한 System Call의 흐름이 linux kernel에서 어떻게 발생하는지 알아보고, 새로운 System Call을 추가하기 위해서는 어떤 작업을 해야 되는지 알아보자.
1. System Call을 정의하기
우선 기본적으로 System Call을 호출했을 때 할 일을 정의해주어야 된다. linux kernel에서 System Call들은 C언어로 작성이 되어있다. (가끔 Assembly 언어) 다른 C함수와 비슷한 형식으로 작성해주면 된다. 중요한 것은 Kernel이 Make 되는 과정에 포함이 되어야 되기 때문에 Makefile에도 추가를 해줘야 된다는 것이다. 어디에 정의를 하고, 어느 Makefile에 추가를 해야 되는지 살펴보자.
#include <linux/kernel.h>
#include <linux/syscalls.h>
SYSCALL_DEFINE0(hello){
printk("Hello Wrold\n");
return 0;
}
<linux-5.4.93/hello/hello.c>
코딩 언어를 모르는 사람도 한 번쯤을 들어봤을 "Hello world"를 출력하기 위한 system call이다. 우선 어디에 작성하느냐, 이것에 대한 자료는 여러 곳에서 찾을 수 있었다. <linux-5.4.93/kernel> 파일 안에 포함한 뒤 이 'kernel'의 Makefile에 추가하는 경우도 있었고, 내가 한 방법은 'linux-5.4.93'파일 안에 새로운 Directory를 만들어 그 안에 선언한 뒤 'linux-5.4.93'의 main Makefile에 추가하는 방법이었다. 이렇게 할 경우 유지보수에 더 유리할 것으로 생각되었다. 이 과정은 너무 간단하고 기본적인 linux command이기 때문에 간단하게 설명하면,
1. mkdir hello
2. vi hello.c
3. 위의 내용을 적은 후 esc+':wq'
로 할 수 있다. 이제 더 자세히 살펴보자. 우선 prink는 우리가 <stdio.h>에서 사용하는 printf와 마찬가지로 <linux/kernel.h> 안에 kernel로의 출력을 위한 함수이다. kernel로의 출력은 다시 말해 우리가 보이는 CLI가 아닌 kernel log로의 출력을 뜻한다. 사용법은 printf와 비슷하다고 한다. C언어를 배운 사람이라면 'return 0'는 이해했을 것인데 함수의 형태가 약간 이상한 것을 알 수 있다. 정확히 말하면 저것은 함수가 아닌 함수를 선언하는 메크로이다. 'SYSCALL_DEFINE0'이란 <linux/syscalls.h>에 정의 도어 있는 메크로로 코드 내에서 찾아보면 다음과 같다.
.
#define SYSCALL_DEFINE0(name) asmlinkage long sys##name(void)
.
<linux/syscalls.h>
간단히 정리하면 DEFINE뒤에 숫자가 인자의 수를 나타내고 '('와 ')'사이가 함수의 이름인 것이다. asmlinkage는 어셈블리 코드에서 직접 호출할 수 있다는 의미이며, 함수의 반환 값은 'long'이라고 해석할 수 있다. 함수 하나를 분석하려 해도 공부할 것이 이렇게 많다...
그다음 'hello' directory안에도 Makefile을 만들어서 hello.c의 object file이 제대로 생성되도록 하였다.
obj-y := hello.o
<linux-5.4.93/hello/Makefile>
'obj-y :='의 의미는 빌트 인으로 파일을 커널에 컴파일한다는 뜻이다. 이러한 명령어로 built-in 컴파일된 파일들은 하나의 파일로 뭉쳐 vmlinux라는 binary파일로 나온다고 한다. 더 자세한 내용은 밑에 <출처 1>에 찾아보시길.
이제 다시 <linux-5.4.93> directory로 나와서 메인 Makefile에 hello라는 directory도 컴파일 과정에 포함해달라고 말해야 된다. 이는 다음 코드를 수정하면 된다.
.
core-y += kernel/ certs/ mm/ fs/ ipc/ security/ crypto/ block/ hello/
.
<linux-5.4.93/Makefile>
마지막에 'hello/'를 추가하면 된다. 위에서 언급한 최종 vmlinux라는 파일을 만들기 위해서 내부 directory의 Makefile과 메인 Makefile이 협동을 하는데, 이때 들려야 되는(?), Makefile이 존재하는(?) 파일들을 core-y에 추가해주는 것 같다. core-y 말고 head-y, init-y, libs-y, drivers-y, net-y도 존재하며 자세한 것은 추후에 더 공부해봐야겠다.
우리가 여태까지 한 것은 'hello.c'라는 파일이 linux 커널 build과정에 포함되도록 한 것이다. 하지만 아직 우리가 접근할 수 있는 경로는 생성되어 있지 않다. 이제 그 경로를 한번 이어보자.
2. System Call header file에 추가
위에서 함수를 정의했지만, 찾아가기 위해서는 header파일에도 추가해주어야 된다. 이 header파일은 어디에 있느냐?
<linux-5.4.93/include/linux/syscalls.h>에 위치해있다. 열어서 보면 알겠지만 기본적인 system call들의 함수 원형이 모두 여기에 있는 것을 알 수 있다. 위에서 선언한 함수도 여기에 추가해주자.
.
asmlinkage long sys_hello(void);
#endif
<linux-5.4.93/include/linux/syscalls.h>
가장 마지막의 #endif바로 위에 선언해주면 된다. (무려 1400줄의 header파일) 다른 구조체도 있어서 크기가 매우 크다. 위에서 함수의 이름은 hello였지만 메크로에 의해 sys_hello로 선언되기 때문에 sys_hello를 써주면 된다. 이제 우리가 hello라는 system call을 호출하면 찾을 함수가 보일 것이다.
3. System Call에 번호를 붙이기 (System Call Table에 추가)
사실 순서상 이걸 가장 먼저 했어야 되나 생각이 들지만, 모두 개별적인 절차라 큰 상관은 없어 보인다. 접근 경로를 먼저 설정하고 정의하느냐, 정의하고 경로를 이어 주느냐의 차이인 것 같다. Linux kernel에서 system call을 호출하면 발생하는 과정에 대한 설명은 따로 하고, 간단하게 설명하면 interrupt를 걸고 system call이 발생하였다는 것을 알리는 것과 동시에 그 system call을 처리하기 위한 인자와 system call에 해당하는 고유 번호를 넘겨준다. (이 과정은 assembly파일로 이루어졌던 것 같다...) 그럼 이 번호를 토대로 kernel에서 어떤 system call이 호출되었는지 확인하고 그에 해당하는 작업을 수행한다.
.
333 common io_pgetevents __x64_sys_io_pgetevents
334 common rseq __x64_sys_rseq
335 common hello __x64_sys_hello
.
<linux-5.4.93/arch/x86/entry/syscalls/syscall_64.tbl>
파일을 확인하면 다음 화면과 같이 숫자와 함께 System Call들이 정리된 것을 알 수 있다. (예: 0번 System Call을 read) 여기서 ‘tbl’ 파일이란 table의 약자로 스타크래프트에서 많이 사용된다고 한다. 여기서는 system call table을 저장하기 위해 사용된 것 같다. 오늘 실습은 ‘Telomere’이라는 문구를 출력하는 System Call을 작성하는 것으로 매우 간단하다. 테이블의 형식을 한번 알아보자.
<System Call #> <ABI> <System Call Name> <Entry Point>
- System Call #: 각 System Call마다의 고유한 번호.
- ABI: Application Binary Interface로 두 개의 이진 프로그램 사이의 Interface이다. ABI는 자료구조 등이 기계어에서 어떻게 돌아가는지에 대한 아주 저급, 하드웨어에 기반한 포맷이다. 이 자리에는 ‘32’, ‘64’ 그리고 둘 다 사용이 가능한 ‘common’을 적을 수 있다
- System Call Name: 새로 만든 System Call의 이름을 붙인다
- Entry Point: 정확한 역할을 모르겠다. System Call Name앞에 ‘__x64_sys'를 붙이는 것 같다. 원래는 __x64_는 안 붙였지만 kernel 5.x.x부터는 확실히 __x64의 prefix를 사용해야 된다.
이렇게 table에 새로 생성한 system call을 추가하면 kernel이. tbl파일을 바로 사용하는 것이 아니라 kernel recompiling과정에서 다른 파일을 만드는 데 사용된다.. tbl파일은 C파일이 아니기 때문에 직접적으로 사용할 수 없기 때문이다.
.
#ifdef CONFIG_x86
__SYSCALL_64(335, __x64_sys_hello, )
#else /* CONFIG_UML */
__SYSCALL_64(335, sys_hello, )
#endif
.
<linux-5.4.93/arch/x86/include/generated/asm/syscalls_64.h>
compile 하기 전에는 보이지 않지만 "make"를 하고 나면 위에 파일이 생성된 것을 확인할 수 있다. 실제로 사용하는 위 파일의 형식을 한번 알아보자. __SYSCALL_64는 메크로로 형식으로 간단히 정리하면 다음과 같다.
#define __SYSCALL_64(nr, sym, qual) extern asmlinkage long sym(const struct pt_regs *);
<linux-5.4.93/arch/x86/entry/syscall_64.c>
어떻게 보면 앞에서 한 함수 원형의 선언과 같다. 이 부분은 더 공부를 해봐야겠다. runtime에 kernel이 사용하는 system call table을 생성하는 과정에서 사용된다고 대충 이해를 하였다.
4. 커널 build
원래 이 글 이전에 커널 build에 관한 글이 있어야 된다. 하지만 system call을 새로 implement 한 이후에도 kernel build가 이루어져야 되기 때문에 이 글보다 뒤로 미루었다. 다음 글로 등록해야겠다.
어쨌든, 이제 커널 build를 하고 다시 reboot를 하면 새로운 system call이 추가되었을 것이다.
5. System Call을 사용하기
우선 보통의 경우에 Application은 System Call을 그대로 사용하지 않는다. API(Application Programming Interface)를 사용하며, 이러한 interface는 하나 또는 그 이상(혹은 0개)의 System Call로 이루어져 있다고 보면 된다. 왜 System Call을 바로 사용하지 않을까? 가장 큰 이유는 편의를 위해서이다. 즉, 새로운 System Call을 만들고 나서 해당 System Call을 사용(호출)하는 API도 만들어야 완료가 되는 것이다. 오늘(실제로는 며칠에 걸린)할 실습은 이 과정까지 하기에는 아직 나의 능력이 너무 부족하기 때문에 "syscall()"이라는 함수를 사용하기로 하였다.
#include <unistd.h>
#include <sys/syscall.h>
long syscall(long number,...);
"syscall()"함수는 gerneric library function으로, C library에 wrapper function(API와 같은..?)이 없는 새로 만든 System Call 등을 시험, 사용할 때 유용하다. 실제로 실행시킬 파일은 다음과 같다.
#include <stdio.h>
#include <linux/kernel.h>
#include <sys/syscall.h>
#include <unistd.h>
int main() {
long int temp = syscall(335);
if(! temp){
printf("System Call well Implemented! Returned: % ld\n", temp);
}
else {
printf("Fail...\n");
}
return 0;
}
<test.c>
system call은 보통 성공적으로 수행되면 0 또는 양수를 반환한다. 실패의 경우에는 음수를 반환한다.
gcc를 사용하여 컴파일한 뒤 실행하면 위와 같은 결과 화면이 나온다. 우리가 구현한 system call의 결과를 완전히 확인하려면 kernel log도 확인해야 되는데, 이 명령어는 "dmesg"이다. "mesg"명령어는 kernel ring buffer의 message들을 출력한다. 다른 "dmesg"도 해석해보면 바로 위에 logitech키보드로 입력한 것도 나온다.
이렇게 system call을 추가해보았다. 찾아본 결과 이렇게 system call을 직접 추가하는 일은 별로 많지 않다고 한다. 호환성의 문제라고... 하지만 앞으로 linux kernel에 대한 더 많은 이해를 하는 데 있어 첫 단계라고 생각한다. 다음번에는 조금 더 복잡한 system call을 구현해봐야겠다. 또한 makefile 등은 수정할 필요가 없기 때문에 compile시간은 단축될 것으로 예상된다.
<출처 1>:
"obj-y :="에 대한 설명: m.blog.naver.com/PostView.nhn?blogId=y12133&logNo=220512849922&proxyReferer=https:%2F%2Fwww.google.com%2F
obj-y 와 obj-m 이란?
출처 : http://timewizhan.tistory.com/29obj-y 와 obj-m 이란?커널 Makef...
blog.naver.com
'리눅스' 카테고리의 다른 글
Cross Development Environment(교차 개발 환경) (0) | 2021.03.12 |
---|---|
[linux kernel] CFS Scheduler에서 VR(T) 조정(fork) (0) | 2021.02.21 |
[linux kernel] CFS Scheduler에서 VR(T) 조정(wake_up) (0) | 2021.02.19 |
Linux Kernel build_환경 구축(2) (0) | 2021.01.27 |
Linux Kernel build_환경 구축(1) (0) | 2021.01.27 |