今回はソフトウェア割り込み経由のシステムコール呼び出しを実装してみる。
※過去回の記事とソースコードは下記から入手できる
https://github.com/Shadow5523/osdev/blob/master/README.md
また、ここで使用するコードはGitHubからダウンロードできる。
https://github.com/Shadow5523/osdev/releases/tag/version0.9.0
目次
概要
前に「OSを自作してみる6 ~割り込み経由でのキー入力~」と「OSを自作してみる5 ~IDT/PICの初期化~」の記事で割り込みの設定をし、キーボードの入力を割り込み経由で行うようにした。これはハードウェア割り込みといい、キーボードでキーを押す度にCPUへ割り込み信号 + INT命令が行きキー入力処理を行っている。こうすることによってハードウェアからの応答性が向上し、キーの入力漏れとかがなくなるからである。
割り込みはハードウェアからだけではなくソフトウェアからも呼び出すことが可能で、これをそのまま「ソフトウェア割り込み」といい、主にシステムコールを呼び出すときに使われる。Linuxでいえばwrite()やread()などがシステムコールで、キーボード同様に応答性が求められる処理で用いられる。また応答性だけではなく、ユーザモードでは実行できない命令をこのソフトウェア割り込みの後なら実行することができる。
また、x86のCPUではこの上記のソフトウェア割り込み以外にもシステムコールを呼び出す方法があり、32bitOSでは「sysenter/sysexit」、64bitOSでは「syscall/sysret」を使用する。これらを使用したシステムコールはこれからやるソフトウェア割り込みより高速にシステムコールを呼び出すことが可能で、おそらく今存在するOSのほとんどはこれを使用している。しかし、少し複雑な為ここではとりあえずint命令を用いたソフトウェア割り込みのやり方を紹介する。
システムコール呼び出し、int 0x80命令を送る
1.まずは、システムコールを呼び出した時の処理を記述していく。ここではsh_write()をシステムコールとして実装し、このsh_write()が呼び出されたらsystem_call()へ受け取った引数を渡すように記述する。
※sh_write()本体で行う処理についてはまた後の記事で記述する。
// sh_libc/io.c
1 2 3 4 5 |
#include "include/io.h" size_t sh_write(int fd, const void* buffer, size_t byte){ return system_call(SYSCALL_WRITE, fd, buffer, byte); } |
2.次に「io.c」で使われる関数のプロトタイプを記述。またここでインクルードする「syscall.h」と「sysdep.h」は後ほど記述する。
// sh_libc/include/io.h
1 2 3 4 5 6 7 8 9 10 |
#ifndef _IO_H_ #define _IO_H_ #include <stddef.h> #include "../../include/syscall.h" #include "sysdep.h" size_t sh_write(int, const void*, size_t); #endif _IO_H_ |
3.「io.h」と同じフォルダにsysdep.hを作成する。ここではシステムコールがら受け取った引数に応じて呼び出す関数を変えるマクロを記述する。こうすることで、引数の数によって呼び出す関数(system_call1 ~ 5)を変えることが可能になる。system_callの一番目の引数にはシステムコール番号を格納することで、呼び出し元のシステムコールを特定する。
// sh_libc/include/sysdep.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#ifndef _SYSDEP_H_ #define _SYSDEP_H_ #include <stdint.h> #define GET_MACRO(_1, _2, _3, _4, _5, _6, NAME, ...) NAME #define system_call(...) GET_MACRO(__VA_ARGS__, system_call5, system_call4, system_call3, \ system_call2, system_call1)(__VA_ARGS__) uint32_t system_call5(uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t); uint32_t system_call4(uint32_t, uint32_t, uint32_t, uint32_t, uint32_t); uint32_t system_call3(uint32_t, uint32_t, uint32_t, uint32_t); uint32_t system_call2(uint32_t, uint32_t, uint32_t); uint32_t system_call1(uint32_t, uint32_t); #endif _SYSDEP_H_ |
4.system_call()の本体を記述する。この関数ではまずソフトウェア割り込みの信号であるint 0x80をCPUへ送り、レジスタに引数を格納していく。
// sh_libc/sysdep.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
#include "include/sysdep.h" uint32_t system_call5(uint32_t syscall_num, uint32_t arg1, uint32_t arg2, uint32_t arg3, uint32_t arg4, uint32_t arg5){ uint32_t ret; asm volatile("\tint $0x80\n" :"=a"(ret) : "a"(syscall_num), "b"(arg1), "c"(arg2), "d"(arg3), "D"(arg4), \ "S"(arg5)); return ret; } uint32_t system_call4(uint32_t syscall_num, uint32_t arg1, uint32_t arg2, uint32_t arg3, uint32_t arg4){ uint32_t ret; asm volatile("\tint $0x80\n" :"=a"(ret) : "a"(syscall_num), "b"(arg1), "c"(arg2), "d"(arg3), "D"(arg4)); return ret; } uint32_t system_call3(uint32_t syscall_num, uint32_t arg1, uint32_t arg2, uint32_t arg3){ uint32_t ret; asm volatile("\tint $0x80\n" :"=a"(ret) : "a"(syscall_num), "b"(arg1), "c"(arg2), "d"(arg3)); return ret; } uint32_t system_call2(uint32_t syscall_num, uint32_t arg1, uint32_t arg2){ uint32_t ret; asm volatile("\tint $0x80\n" :"=a"(ret) : "a"(syscall_num), "b"(arg1), "c"(arg2)); return ret; } uint32_t system_call1(uint32_t syscall_num, uint32_t arg1){ uint32_t ret; asm volatile("\tint $0x80\n" :"=a"(ret) : "a"(syscall_num), "b"(arg1)); return ret; } |
asm volatileを使ってインラインアセンブラを記述している。1行目でint 0x80の命令をCPUへ送り、2行目で現在のEAXの値を変数retへ格納している。3行目では実際に受け取った引数をレジスタへ格納していて、関数によって格納する引数の数が異なる。”a”や”D”などは制約文字と呼ばれていてx86では以下のレジスタを指定していることになる。
変数名 | 役割 |
a | EAXレジスタ |
b | EBXレジスタ |
c | ECXレジスタ |
d | EDXレジスタ |
D | EDIレジスタ |
S | ESIレジスタ |
IDTにソフトウェア割り込みを登録
1.「idt.c」を開き、実際にint 0x80の信号を受けっとったときに実行したい関数を記述する。ここの所はキーボード割り込みで行ったこととほぼ同じ感じ。
ただ、set_gate_desc()の最後の引数の所だけ違う値を使用しており、0x8fを使用する。(基本的にはソフトウェア割り込みはトラップゲートで行いたいため)
// arch/idt.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#include "../include/idt.h" void idt_init(void){ idtr idt; terminal_writestring("Initialize IDT..."); for (size_t i = 0; i < IDT_LEN; i++) { set_gate_desc(i, 0, 0, 0); } set_gate_desc(33, (uint32_t)as_keyboard_interrupt, 0x08, 0x8e); set_gate_desc(128, (uint32_t)as_software_interrupt, 0x08, 0x8f); //追加 idt.idt_size = IDT_LEN * sizeof(gate_desc) - 1; idt.base = (uint32_t)idt_entries; load_idtr((uint32_t)&(idt)); terminal_writestring(" OK!\n"); } ・・・ |
2.idtに登録した「as_software_interrupt」が別の場所で宣言されているのでexternで宣言する。
// include/idt.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#ifndef _IDT_H_ #define _IDT_H_ #include <stdint.h> #include <stddef.h> #include "interrupt.h" #ifdef __cplusplus extern "C" void load_idtr(uint32_t); #else extern void load_idtr(uint32_t); #endif extern as_keyboard_interrupt(void); extern as_software_interrupt(void); //追加 ・・・ |
3.as_software_interruptが呼び出された処理を「interrupt.s」に記述していく。単純にESPレジスタからEAXレジスタ(ESIからEAXまでは引数)をプッシュし、次にcallでシステムコールの本処理を書いた関数を記述する。
呼び出し終わったらpopl命令で各種レジスタのデータをスタックから取り出し最後にiretl命令で割り込み処理を抜けるようにする。
// kernel/interrupt.s
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
.global as_keyboard_interrupt .global as_software_interrupt //追加 .extern keyboard_interrupt .extern syscall_interrupt //追加 ・・・ //ここから下がソフトウェア割り込み時に呼び出される as_software_interrupt: pushl %esp pushl %ebp pushl %esi pushl %edi pushl %edx pushl %ecx pushl %ebx pushl %eax call syscall_interrupt popl %eax popl %ebx popl %ecx popl %edx popl %edi popl %esi popl %ebp popl %esp iretl |
システムコールの本処理
1.まず、システムコール呼び出した時に、どのシステムコールなのかを判別するために定数を設定し判断できるようにしてみる。「syscallnum.h」を以下のように記述する。
// include/syscallnum.h
1 2 3 4 5 6 7 |
#ifndef _SYSCALLNUM_H_ #define _SYSCALLNUM_H_ #define SYSCALL_WRITE 0 #define SYSCALL_READ 1 #endif _SYSCALLNUM_H_ |
2.同じincludeフォルダに「syscall.h」を記述する。このファイルでは割り込み処理で呼び出されたときに、どのシステムコールなのかを判定させる処理をする関数のプロトタイプを記述する。
// include/syscall.h
1 2 3 4 5 6 7 8 9 10 |
#ifndef _SYSCALL_H_ #define _SYSCALL_H_ #include <stdint.h> #include "syscallnum.h" uint32_t syscall_interrupt(uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t); #endif _SYSCALL_H_ |
3.「syscall.c」を作製する。将来、write()やread()のシステムコールを実装しようと思ってるけど、今回は呼び出し元のシステムコールを判別するところだけ記述する。
// kernel/syscall.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#include "../include/syscall.h" #include "../sh_libc/include/stdio.h" uint32_t syscall_interrupt(uint32_t syscall_num, uint32_t arg1, uint32_t arg2, uint32_t arg3, uint32_t arg4, uint32_t arg5){ switch (syscall_num) { case SYSCALL_WRITE: break; case SYSCALL_READ: break; } return 0; } |
4.次にカーネル側から呼び出せるように「kernel.h」へ以下を追記。
// include/kernel.h
1 2 3 4 5 6 7 8 9 10 |
・・・ #include "idt.h" #include "multiboot.h" #include "getmmap.h" #include "../sh_libc/include/stdio.h" #include "../sh_libc/include/math.h" #include "../sh_libc/include/io.h" //追加 ・・・ |
5.最後に、Makefileに新しく追加したファイルがコンパイルされるように追加する。以下の所を編集する。またmake cleanコマンドで消せるファイル(Linuxなどで自動生成される~付きのファイルなど)を追加した。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
all: create OBJ = kernel.o terminal.o boot.o inb_outb.o keyboard.o \ keymap.o gdt.o idt.o pic.o interrupt.o stdio.o \ stdlib.o string.o math.o getmmap.o io.o sysdep.o \ //io.o sysdep.o syscall.oを追加 syscall.o ・・・ //追加 syscall.o: $(KRNDIR)syscall.c $(INCLUDEDIR)syscall.h $(INCLUDEDIR)syscallnum.h $(CC) -c $^ -std=gnu99 $(CFLAGS) -Wextra ・・・ //追加 syscall.o: $(KRNDIR)syscall.c $(INCLUDEDIR)syscall.h $(INCLUDEDIR)syscallnum.h $(CC) -c $^ -std=gnu99 $(CFLAGS) -Wextra ・・・ //以下の所を編集 clean: rm -f *.o myos.bin myos.iso isodir/boot/myos.bin rm -f ./*~ $(ARCHDIR)*~ $(INCLUDEDIR)*~ $(SHLIBCINC)*~ rm -f $(SHLIBCINC)*gch $(INCLUDEDIR)*gch |
実際に呼び出されてるか確認
1.実際にシステムコールが呼び出されてるかをテストしてみる。まずは「kernel.c」へ以下を追記する。(場所はループの外ならどこでもOK)
// kernel/kernel.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
・・・ void kernel_main(multiboot_info_t* mbt, uint32_t magic){ terminal_initialize(); sh_printf("Initialize Terminal... OK\n"); gdt_init(); pic_init(); idt_init(); key_init(); getmmap(mbt); sh_printf("Hello, kernel World! \n\n"); //system call test sh_write(1, "test string", 12); //追加 prompt(); } ・・・ |
2.「syscall.c」でちゃんと引数などが渡されてるかを確認する為、以下のコードを追加する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
#include "../include/syscall.h" #include "../sh_libc/include/stdio.h" // kernel/syscall.c uint32_t syscall_interrupt(uint32_t syscall_num, uint32_t arg1, uint32_t arg2, uint32_t arg3, uint32_t arg4, uint32_t arg5){ switch (syscall_num) { case SYSCALL_WRITE: //ここから追加 sh_printf("-----call system_call------\n"); sh_printf("systemcall number = %d\n", syscall_num); sh_printf("fd = %d\n", arg1); sh_printf("buffer = %s\n", arg2); sh_printf("byte = %d\n", arg3); sh_printf("---------------------------"); //ここまで break; case SYSCALL_READ: break; } return 0; } |
3.コンパイルして実行し、以下のようにsh_write()に渡した引数が表示されれば成功。