시스템 호출(system call
) 이란 사용자 수준 응용 프로그램에게 커널이 자신의 서비스를 제공하는 인터페이스이다.
리눅스 커널은 각 시스템 호출을 함수(시스템 호출 핸들러)로 구현해놓고, 각 시스템 호출이 요청되었을 때 대응되는 함수를 호출하여 서비스를 제공한다.
libc.a
의 fork()
호출순서 | x86 | ARM |
---|---|---|
1 | movl eax, %2 | STR r7, 2 |
2 | int 0x80 | swi |
3 | 사용자 > 커널 수준 (문맥교환) | 동일 |
4 | system_call(eax) | eax > r7 외 동일 |
5 | sys_call_table 인덱싱 | 동일 |
6 | sys_fork() 호출 | 동일 |
------- | -------------------------- | --------------------- |
여기에서 eax
혹은 r7
에 들어가는 2 는 fork()
에 할당된 고유 번호이다. 레지스터에 시스템 번호에 해당하는 값을 해당한 후 int
혹은 swi
를 통해 인터럽트(트랩)를 발생시킨다.
새로운 시스템 호출의 구현은 아래의 순서로 이뤄진다:
ARM64
의 syscall table
이다. 현재 441
개의 시스템 콜이 사용 중으로 확인된다. (Linux Kernel 5.10
)
아키텍쳐에 따른 시스템 호출 테이블은 아래의 경로에 존재한다:
ARM64
- include/uapi/asm-generic/unistd.h
ARM32
- arch/arm/tools/syscall.tbl
x86
- arch/x86/syscalls/syscall_64.tbl
앞서 말했듯 시스템 호출을 위해선 시스템 호출 함수에 대응하는 고유한 번호가 필요하다. 따라서 시스템 호출 핸들러 구현에 앞서 시스템 호출 번호를 등록해야 한다:
다음의 내용을 include/uapi/asm-generic/unistd.h
파일의 맨 아래에 기입했다. 새로운 시스템 호출 번호로 460
을 등록했다.
asmlinkage long sys_newsyscall(void);
위 내용을 include/linux/syscalls.h
에 등록했다. 새롭게 등록할 시스템 호출인 sys_newsyscall
은 인자를 받지 않는 아주 단순한 함수이다.
sys_newsyscall
시스템 호출은 Hello Linux, I'm in kernel!
이라는 단순한 문자열을 출력한다. 위 함수는 kernel/sys.c
에 구현했다.
make O=../build/arm64 ARCH=64 CROSS_COMPILE=aarch64-linux-gnu -j$(nproc)
다음과 같이 크로스 컴파일을 진행했다.
컴파일 결과로 완성된 Image
파일 (arch/arm6/boot/Image
) 을 가지고 QEMU
를 통해 실행했다.
자세한 실행 방법은 필자가 작성한 다음의 글을 참고하길 바란다.
실행결과는 위와 같았다.
기본적으로 유저 모드의 프로그램은 커널 영역의 자료 구조에 직접적으로 접근할 수 없다. 이번에는 커널 모드에서만 접근 가능한 자료 구조의 내용을 출력해보려 한다:
#include <linux/unistd.h> #include <linux/errno.h> #include <linux/kernel.h> #include <linux/sched.h> #include <linux/syscalls.h> SYSCALL_DEFINE0(gettaskinfo) { printk("<0> PID: %d \n", current->pid); printk("<0> TGID: %d \n", current->tgid); printk("<0> PPID: %d \n", current->parent->pid); printk("<0> STATE: %ld \n", current->state); printk("<0> PRIORITY: %d \n", current->prio); printk("<0> POLICY: %d \n", current->policy); printk("<0> Number of MAJOR FAULT: %ld \n", current->maj_flt); printk("<0> Number of MINOR FAULT: %ld \n", current->min_flt); return 0; }
2. 새로운 시스템 호출 구현
에선 kernel/sys.c
에 함수를 작성했지만, 새로운 소스에 작성하는 것도 가능하다.
kernel/gettaskinfo.c
라는 파일을 생성하여 위 내용을 기입했고, kernel/Makefile
를 아래와 같이 수정했다:
obj-y = fork.o exec_domain.o panic.o \ cpu.o exit.o softirq.o resource.o \ sysctl.o capability.o ptrace.o user.o \ signal.o sys.o umh.o workqueue.o pid.o task_work.o \ extable.o params.o \ kthread.o sys_ni.o nsproxy.o \ notifier.o ksysfs.o cred.o reboot.o \ async.o range.o smpboot.o ucount.o regset.o \ gettaskinfo.o
obj-y
변수에 gettaskinfo.o
라는 오브젝트 파일을 추가하여 커널 컴파일 시, kernel/gettaskinfo.c
라는 파일이 컴파일 될 수 있게 만든다.
실행 결과는 아래와 같았다:
유저 모드의 프로그램이 커널 영역의 자료구조에 접근할 수 없는 것과 마찬가지로, 커널 모드에서 실행되고 있는 프로그램 역시 유저 영역에 직접 접근할 수 없고, copy_to_user
와 같은 매크로를 통해서만 접근 가능하다.
#include <linux/unistd.h> #include <linux/kernel.h> #include <linux/syscalls.h> #include <asm/uaccess.h> SYSCALL_DEFINE3(access_user, int, x, int, y, int *, res) { int err, i; int compute; err = access_ok(res, sizeof(*res)); if (err < 0) { printk("failed to access_ok(): %d\n", err); return err; } compute = x * y; i = copy_to_user(res, &compute, sizeof(compute)); return 0; }
SYSCALL_DEFINE3
매크로를 통해 두 개의 정수와 하나의 포인터 주소를 인자로 가지는 access_user
라는 시스템 호출을 정의한다. 이 시스템 호출은 인자로 전달받은 두 개의 정수를 곱하여 세 번째 포인터 주소를 통해 반환한다.
#include <stdio.h> #include <linux/unistd.h> int main(void) { int num1, num2, num3; syscall(460); syscall(461); printf("Enter two number: "); scanf("%d %d", &num1, &num2); syscall(462, num1, num2, &num3); printf("num3: %d\n", num3); return 0; }
위 코드를 컴파일 하여 실행 결과를 확인해보면 아래와 같다.
리눅스 커널은 모놀리딕(monolithic
) 커널으로 운영체제가 제공해야 하는 모든 기능(태스크 관리, 메모리 관리, 파일 시스템, 디바이스 드라이버, 통신 프로토콜)을 단일한 커널 공간에 구현한다.
반대인 마이크로 커널(micro kernel
) 은 커널 공간에 반드시 필요한 기능(문맥 교환, 주소 교환, 시스템 호출 처리, 디바이스 드라이버 일부)들만 구현한다.
리눅스 커널은 모놀리딕 커널이지만, 많은 기능들 중 필요한 기능만을 적재하여 사용하는 모듈 개념을 적용하여 마이크로 커널의 장점을 취할 수 있다.
모듈 프로그래밍 방법은 다음의 글을 참조하라.
[책] 리눅스 커널: 내부구조 (백승제, 최종무 저)
[사이트] https://reverseengineering.stackexchange.com/questions/16917/arm64-syscalls-table
[사이트] https://www.linuxbnb.net/home/adding-a-system-call-to-linux-arm-architecture/
혹시 include/uapi/asm-generic/unistd.h 등록할 떄 __NR_syscalls 도 바꿔줘야 하나요?
NR_syscalls 값은436이고 시스템콜은 435까지 차있어서 하나 추가하면 NR_syscalls도 1 더하는게 맞는건가요?
감사합니다 ㅜ,ㅜ