LKDのいくつかの段落を読みました1 そして、私は以下の内容を理解することができません:
ユーザースペースからのシステムコールへのアクセス
通常、Cライブラリはシステムコールのサポートを提供します。ユーザーアプリケーションは、標準ヘッダーから関数プロトタイプを取得し、Cライブラリとリンクして、システムコール(またはシステムコールを使用するライブラリルーチン)を使用できます。ただし、システムコールを作成したばかりの場合は、glibcがすでにシステムコールをサポートしていることは疑わしいです。
ありがたいことに、Linuxには、システムコールへのアクセスをラップするための一連のマクロが用意されています。レジスタの内容を設定し、トラップ命令を発行します。これらのマクロの名前は
_syscalln()
です。ここで、n
は0から6の間です。この数は、syscallに渡されるパラメーターの数に対応します。これは、マクロが予期するパラメーターの数を認識し、その結果、レジスターにプッシュする必要があるためです。たとえば、次のように定義されたシステムコールopen()
について考えてみます。_long open(const char *filename, int flags, int mode)
_明示的なライブラリサポートなしでこのシステムコールを使用するsyscallマクロは次のようになります。
_#define __NR_open 5 _syscall3(long, open, const char *, filename, int, flags, int, mode)
_次に、アプリケーションは単に
open()
を呼び出すことができます。マクロごとに、2 + 2×nパラメータがあります。最初のパラメーターは、syscallの戻り値の型に対応します。 2番目はシステムコールの名前です。次に、各パラメータのタイプと名前をシステムコール順に示します。 _
__NR_open
_定義は_<asm/unistd.h>
_にあります。これはシステムコール番号です。 __syscall3
_マクロは、インラインアセンブリを使用してC関数に展開されます。アセンブリは、前のセクションで説明した手順を実行して、システムコール番号とパラメータを正しいレジスタにプッシュし、ソフトウェア割り込みを発行してカーネルにトラップします。このマクロをアプリケーションに配置するだけで、open()
システムコールを使用できます。素晴らしい新しい
foo()
システムコールを使用するマクロを記述してから、いくつかのテストコードを記述して私たちの取り組みを披露しましょう。_#define __NR_foo 283 __syscall0(long, foo) int main () { long stack_size; stack_size = foo (); printf ("The kernel stack size is %ld\n", stack_size); return 0; }
_
アプリケーションが単にopen()
を呼び出すことができるとはどういう意味ですか?
さらに、最後のコードでは、foo()
の宣言はどこにありますか?そして、どうすればこのコードをコンパイル可能で実行可能にすることができますか?インクルードする必要のあるヘッダーファイルは何ですか?
__________
1Linuxカーネル開発、RobertLoveによる。 wordpress.comのPDFファイル (81ページに移動); Googleブックスの結果 。
最初に、 linux kernel の役割とは何か、そしてアプリケーションがカーネルonlyを介して相互作用することを理解する必要があります システムコール 。
事実上、アプリケーションはカーネルによって提供される「仮想マシン」で実行されます。アプリケーションは ユーザースペース で実行され、-で許可されている一連のマシン命令のみを(最下位のマシンレベルで)実行できます。 ユーザーCPUモード システムコールを行うために使用される命令(例:SYSENTER
またはINT 0x80
...)によって拡張されます。したがって、ユーザーレベルのアプリケーションの観点からは、syscallはアトミックな疑似マシン命令です。
Linux Assembly Howto は、アセンブリ(つまりマシン命令)レベルでシステムコールを実行する方法を説明しています。
GNU libc は、syscallに対応するC関数を提供しています。したがって、たとえば open 関数は、番号NR__open
のシステムコールの上にある小さな接着剤(つまりラッパー)です(システムコールを作成してからerrno
を更新します)。アプリケーションは通常、syscallを実行する代わりに、libcでそのようなC関数を呼び出します。
他のlibc
を使用できます。たとえば、 MUSL libc はなんとなく「シンプル」であり、そのコードはおそらく読みやすいでしょう。また、生のシステムコールを対応するC関数にラップしています。
独自のシステムコールを追加する場合は、同様のC関数も(独自のライブラリに)実装することをお勧めします。したがって、ライブラリのヘッダーファイルも必要です。
intro(2) および syscall(2) および syscalls(2) のマニュアルページ、および VDSO in syscalls 。
syscalls はC関数ではないことに注意してください。それらはコールスタックを使用しません(スタックなしで呼び出すこともできます)。システムコールは基本的に、NR__open
の<asm/unistd.h>
のような番号であり、SYSENTER
マシン命令であり、どのレジスタがsyscallの引数の前に保持され、どのレジスタがsyscallの結果の後に保持されるかについての規則があります(失敗の結果、syscallをラップするCライブラリにerrno
を設定します)。 syscallの規則は、ABI仕様のC関数の呼び出し規則ではありません(例: x86-64 psABI )。したがって、Cラッパーが必要です。
最初に、システムコールの定義をいくつか示したいと思います。システムコールは、ユーザースペースアプリケーションから特定のカーネルサービスを同期的に明示的に要求するプロセスです。同期とは、命令シーケンスを実行することにより、システムコールの動作が事前に決定されることを意味します。割り込みは、プロセッサで実行されているコードとは完全に独立してカーネルに到達するため、非同期システムサービス要求の例です。システムコールとは対照的な例外は、カーネルサービスに対する同期的ですが、暗黙的な要求です。
システムコールは、次の4つの段階で構成されます。
一般に、これらのアクションはすべて、実際のシステムコールの前および/または後にいくつかの補助アクションを実行する1つの大きなライブラリ関数の一部として実装できます。この場合、システムコールはこの関数に埋め込まれていると言えますが、この関数は一般にシステムコールではありません。別のケースでは、この4つのステップだけを実行する小さな関数を使用できます。この場合、この関数はシステムコールであると言えます。実際には、上記の4つの段階すべてを手動で実装することで、システムコール自体を実装できます。この場合、このすべてのステップは完全にアーキテクチャに依存するため、アセンブラを使用する必要があることに注意してください。
たとえば、Linux/i386環境には次のシステムコール規則があります。
include\uapi\asm-generic\unistd.h
のLinuxソースツリーにあります。Linuxの最新バージョンでは、_syscallマクロはありません(私が知る限り)。代わりに、Linuxカーネルのメインインターフェイスライブラリであるglibcライブラリは、特別なマクロINTERNAL_SYSCALL
を提供します。これは、インラインアセンブラ命令によって入力される小さなコードに展開されます。このコードは特定のハードウェアプラットフォームを対象としており、システムコールのすべての段階を実装します。このため、このマクロはシステムコール自体を表します。別のマクロもあります-INLINE_SYSCALL
。最後の1つのマクロは、glibcのようなエラー処理を提供します。これに従って、システムコールが失敗すると、-1が返され、エラー番号がerrno
変数に格納されます。両方のマクロは、glibcパッケージのsysdep.h
で定義されています。
次の方法でシステムコールを呼び出すことができます。
#include <sysdep.h>
#define __NR_<name> <id>
int my_syscall(void)
{
return INLINE_SYSCALL(<name>, <argc>, <argv>);
}
ここで、<name>
はsyscall名文字列に、<id>
-必要なシステムサービス番号IDに、<argc>
-は実際のパラメーター数(0から6)に、<argv>
-コンマで区切られた実際のパラメーター(パラメーターが存在する場合はコンマで開始)。
例えば:
#include <sysdep.h>
#define __NR_exit 1
int _exit(int status)
{
return INLINE_SYSCALL(exit, 1, status); // takes 1 parameter "status"
}
または別の例:
#include <sysdep.h>
#define __NR_fork 2
int _fork(void)
{
return INLINE_SYSCALL(fork, 0); // takes no parameters
}
最小限の実行可能なアセンブリの例
section .rodata
hello_world db "hello world", 10
hello_world_len equ $ - hello_world
section .text
global _start
_start:
mov eax, 4 ; syscall number: write
mov ebx, 1 ; stdout
mov ecx, hello_world ; buffer
mov edx, hello_world_len
int 0x80 ; make the call
mov eax, 1 ; syscall number: exit
mov ebx, 0 ; exit status
int 0x80
コンパイルして実行します。
nasm -w+all -f elf32 -o hello_world.o hello_world.asm
ld -m elf_i386 -o hello_world hello_world.o
./hello_world
コードから、次のことを簡単に推測できます。
eax
には、システムコール番号が含まれます。例: 4
書き込み用。カーネルソースの完全な32ビットリスト: https://github.com/torvalds/linux/blob/v4.9/Arch/x86/entry/syscalls/syscall_32.tbl#L1ebx
、ecx
、およびedx
には、入力引数が含まれています。これらは、カーネルソース内の各システムコールの署名から推測できる必要があります。参照: x86-64でのUNIXおよびLinuxシステムコールの呼び出し規約は何ですか および アセンブリ言語のLinuxシステムコールテーブルまたはチートシートint 0x80
呼び出しを行いますが、より良いメソッドがあります: 「int0x80」または「syscall」のどちらが良いですか?もちろん、Assemblyはすぐに面倒になるので、できる限りglibc/POSIXが提供するCラッパーを使用するか、できない場合はSYSCALL
マクロを使用することをお勧めします。