Linuxシステムコール:Linuxのシステムコール実装
Linuxのシステムコール実装
これまで述べてきたようにOS(カーネル)の持つ機能へのインターフェイスがシステムコールですが、システムコールの実態はカーネルの中にあります。このため、ユーザープログラムがシステムコールを呼び出すとユーザーモードからカーネルモードへの切り替わりが発生し、カーネル内部での処理完了でユーザーモードに復帰します。
例として、ファイルからデータを読み出す場合を見てみましょう。
ユーザープログラムがファイルを読むため、システムコールを呼び出します。このときCPUのソフトウェア割り込み、またはシステムコール命令が発行されることで、CPUはカーネルモードに移行します。システムコール命令を発行するとき、特定のレジスタに実行したいシステムコールの番号を設定しておきます。CPUはカーネルモードに切り替わるとシステムコール番号からシステムコールテーブルを参照して、カーネル内部の所定の処理を呼び出します。
この例では、ファイル読み込みが呼ばれ、その中で物理的なファイルを読み込み、ユーザー領域のバッファにデータを転送し、ユーザープログラムがシステムコールを呼んだ場所にリターンします。ユーザー空間にリターンすることで、CPUのモードもカーネルモードからユーザーモードに移行します。
システムコールを呼び出すことで、これらの一連の処理が実行されています。
少し余談になりますが、ライブラリもハードウェアとして実態のファイルにアクセスするためにシステムコールを使っています。なぜ同じような目的なのにわざわざライブラリにしているのでしょうか?
目的は2つあります。1つはシステムコールのラッパーとしての役割、もう一つは抽象化したシステムの提供です。
OSがWindowsである場合、ファイル読み込みのシステムコールは ReadFile() ですが、Windows上で動作するc言語のstdioで定義されているファイル読み込みは同じ fread() で、Linux上で動作する fread() と同様の動作をします。
このように、OSが変わっても同様のプログラミングが可能なようにする役割もライブラリは持っています。
また、抽象化とは、今回の例ではファイルの読み込みですが、ファイル読み込みはシステムコールでは read() です。C言語のライブラリでは fread() です。ライブラリ関数では、ファイルをストリームの概念でアクセスできるようにしています。
カーネルモードへの移行は、これまではソフトウェア割り込みを発生させることによるカーネルモード移行でしたが、割り込みの場合、割り込み回路を駆動 → 割り込みベクタからジャンプ → システムコールテーブルでジャンプ。。。と、CPUの負荷が大きいため、最近ではCPUのシステムコール命令でカーネルモードへ移行するようになってきています。
さて、ココから先は実際のLinuxコードがどうなっているか?
についてもう少し詳しく見ていきたいと思います。
下記は、テキストファイルを読むだけの簡単なプログラムです。
(エラー処理とかちゃんと入っていません!)
このソースでは3つのシステムコールを使っています。
open(), read(), close()
実際にシステムコールがどのように呼ばれるのか?
上記のソースをコンパイラでアセンブラを出力してみます。(gcc -S)
アセンブラからのシステムコール呼び出しは、bl命令で呼ばれています。このbl命令はリンクレジスタにbl命令の次のアドレスを設定して分岐します。分岐先でのRET命令でこのbl命令の次に戻ってきます。つまり、サブルーチンの呼び出し(Call)と考えてよいでしょう。
じつは、システムコールもイキナリ、ソフトウェア割り込み命令に展開されるのではなく、システムコールのラッパーを呼び出しています。
システムコールのラッパーでは、ARM の swi 命令を実行してカーネルモードに移行し、システムコールを実行します。
このような実装となっているのは、カーネルモードへの移行方法がCPU依存になっているからです。カーネルとのインターフェイスは、ABI(Application Binary Interface)として定義されていますが、ARMでは2つのABIが定義されています。最近使われているABIは、EABIと呼ばれるインターフェイスで、EはEmbeddedの頭文字です。もう一つはOABIでこちらは古いと言う意味からOldのOが割り当てられています。これらは、クロスコンパイラなどのツールーチェーンのプレフクス表示に確認することができます。
これまで述べてきたようにOS(カーネル)の持つ機能へのインターフェイスがシステムコールですが、システムコールの実態はカーネルの中にあります。このため、ユーザープログラムがシステムコールを呼び出すとユーザーモードからカーネルモードへの切り替わりが発生し、カーネル内部での処理完了でユーザーモードに復帰します。
例として、ファイルからデータを読み出す場合を見てみましょう。
ユーザープログラムがファイルを読むため、システムコールを呼び出します。このときCPUのソフトウェア割り込み、またはシステムコール命令が発行されることで、CPUはカーネルモードに移行します。システムコール命令を発行するとき、特定のレジスタに実行したいシステムコールの番号を設定しておきます。CPUはカーネルモードに切り替わるとシステムコール番号からシステムコールテーブルを参照して、カーネル内部の所定の処理を呼び出します。
この例では、ファイル読み込みが呼ばれ、その中で物理的なファイルを読み込み、ユーザー領域のバッファにデータを転送し、ユーザープログラムがシステムコールを呼んだ場所にリターンします。ユーザー空間にリターンすることで、CPUのモードもカーネルモードからユーザーモードに移行します。
システムコールを呼び出すことで、これらの一連の処理が実行されています。
少し余談になりますが、ライブラリもハードウェアとして実態のファイルにアクセスするためにシステムコールを使っています。なぜ同じような目的なのにわざわざライブラリにしているのでしょうか?
目的は2つあります。1つはシステムコールのラッパーとしての役割、もう一つは抽象化したシステムの提供です。
OSがWindowsである場合、ファイル読み込みのシステムコールは ReadFile() ですが、Windows上で動作するc言語のstdioで定義されているファイル読み込みは同じ fread() で、Linux上で動作する fread() と同様の動作をします。
このように、OSが変わっても同様のプログラミングが可能なようにする役割もライブラリは持っています。
また、抽象化とは、今回の例ではファイルの読み込みですが、ファイル読み込みはシステムコールでは read() です。C言語のライブラリでは fread() です。ライブラリ関数では、ファイルをストリームの概念でアクセスできるようにしています。
カーネルモードへの移行は、これまではソフトウェア割り込みを発生させることによるカーネルモード移行でしたが、割り込みの場合、割り込み回路を駆動 → 割り込みベクタからジャンプ → システムコールテーブルでジャンプ。。。と、CPUの負荷が大きいため、最近ではCPUのシステムコール命令でカーネルモードへ移行するようになってきています。
さて、ココから先は実際のLinuxコードがどうなっているか?
についてもう少し詳しく見ていきたいと思います。
下記は、テキストファイルを読むだけの簡単なプログラムです。
(エラー処理とかちゃんと入っていません!)
#include <stdio.h> #include <string.h> #include <unistd.h> #include <fcntl.h> int main() { int fd, ret; char buf[16]; fd = open("test.txt", O_RDONLY ); memset(buf, 0, sizeof(buf)); read(fd, buf, sizeof(buf)); printf("[%s]\n", buf); close(fd); } |
このソースでは3つのシステムコールを使っています。
open(), read(), close()
実際にシステムコールがどのように呼ばれるのか?
上記のソースをコンパイラでアセンブラを出力してみます。(gcc -S)
.arch armv5t .fpu softvfp .eabi_attribute 20, 1 .eabi_attribute 21, 1 .eabi_attribute 23, 3 .eabi_attribute 24, 1 .eabi_attribute 25, 1 .eabi_attribute 26, 2 .eabi_attribute 30, 6 .eabi_attribute 34, 0 .eabi_attribute 18, 4 .file "reader.c" .section .rodata .align 2 .LC0: .ascii "test.txt\000" .align 2 .LC1: .ascii "[%s]\012\000" .text .align 2 .global main .syntax unified .arm .type main, %function main: @ args = 0, pretend = 0, frame = 24 @ frame_needed = 1, uses_anonymous_args = 0 push {fp, lr} add fp, sp, #4 sub sp, sp, #24 ldr r3, .L4 ldr r3, [r3] str r3, [fp, #-8] mov r1, #0 ldr r0, .L4+4 bl open str r0, [fp, #-28] sub r3, fp, #24 mov r2, #16 mov r1, #0 mov r0, r3 bl memset sub r3, fp, #24 mov r2, #16 mov r1, r3 ldr r0, [fp, #-28] bl read sub r3, fp, #24 mov r1, r3 ldr r0, .L4+8 bl printf ldr r0, [fp, #-28] bl close mov r3, #0 mov r0, r3 ldr r3, .L4 ldr r2, [fp, #-8] ldr r3, [r3] cmp r2, r3 beq .L3 bl __stack_chk_fail .L3: sub sp, fp, #4 @ sp needed pop {fp, pc} .L5: .align 2 .L4: .word __stack_chk_guard .word .LC0 .word .LC1 .size main, .-main .ident "GCC: (Ubuntu/Linaro 5.4.0-6ubuntu1~16.04.9) 5.4.0 20160609" .section .note.GNU-stack,"",%progbits |
アセンブラからのシステムコール呼び出しは、bl命令で呼ばれています。このbl命令はリンクレジスタにbl命令の次のアドレスを設定して分岐します。分岐先でのRET命令でこのbl命令の次に戻ってきます。つまり、サブルーチンの呼び出し(Call)と考えてよいでしょう。
じつは、システムコールもイキナリ、ソフトウェア割り込み命令に展開されるのではなく、システムコールのラッパーを呼び出しています。
システムコールのラッパーでは、ARM の swi 命令を実行してカーネルモードに移行し、システムコールを実行します。
このような実装となっているのは、カーネルモードへの移行方法がCPU依存になっているからです。カーネルとのインターフェイスは、ABI(Application Binary Interface)として定義されていますが、ARMでは2つのABIが定義されています。最近使われているABIは、EABIと呼ばれるインターフェイスで、EはEmbeddedの頭文字です。もう一つはOABIでこちらは古いと言う意味からOldのOが割り当てられています。これらは、クロスコンパイラなどのツールーチェーンのプレフクス表示に確認することができます。
Lightning Brains
コメント
コメントを投稿