web-dev-qa-db-ja.com

ユーザースペースからシステムコールにアクセスするにはどうすればよいですか?

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ブックスの結果

17
injoy

最初に、 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ラッパーが必要です。

20

最初に、システムコールの定義をいくつか示したいと思います。システムコールは、ユーザースペースアプリケーションから特定のカーネルサービスを同期的に明示的に要求するプロセスです。同期とは、命令シーケンスを実行することにより、システムコールの動作が事前に決定されることを意味します。割り込みは、プロセッサで実行されているコードとは完全に独立してカーネルに到達するため、非同期システムサービス要求の例です。システムコールとは対照的な例外は、カーネルサービスに対する同期的ですが、暗黙的な要求です。

システムコールは、次の4つの段階で構成されます。

  1. プロセッサをユーザーモードからカーネルモードに切り替えてカーネル内の特定のポイントに制御を渡し、プロセッサをユーザーモードに切り替えて元に戻します。
  2. 要求されたカーネルサービスのIDを指定します。
  3. 要求されたサービスのパラメーターの受け渡し。
  4. サービスの結果をキャプチャします。

一般に、これらのアクションはすべて、実際のシステムコールの前および/または後にいくつかの補助アクションを実行する1つの大きなライブラリ関数の一部として実装できます。この場合、システムコールはこの関数に埋め込まれていると言えますが、この関数は一般にシステムコールではありません。別のケースでは、この4つのステップだけを実行する小さな関数を使用できます。この場合、この関数はシステムコールであると言えます。実際には、上記の4つの段階すべてを手動で実装することで、システムコール自体を実装できます。この場合、このすべてのステップは完全にアーキテクチャに依存するため、アセンブラを使用する必要があることに注意してください。

たとえば、Linux/i386環境には次のシステムコール規則があります。

  1. ユーザーモードからカーネルモードへの制御の受け渡しは、番号0x80のソフトウェア割り込み(アセンブリ命令INT 0x80)、SYSCALL命令(AMD)、またはSYSENTER命令(Intel)のいずれかによって実行できます。
  2. 要求されたシステムサービスのIDは、カーネルモードに入るときにEAXレジスタに格納された整数値によって指定されます。カーネルサービスIDは、_[〜#〜] nr [〜#〜]の形式で定義する必要があります。すべてのシステムサービスIDは、パスinclude\uapi\asm-generic\unistd.hのLinuxソースツリーにあります。
  3. 最大6つのパラメーターをレジスターEBX(1)、ECX(2)、EDX(3)、ESI(4)、EDI(5)、EBP(6)に渡すことができます。括弧内の数字は、パラメーターの連番です。
  4. カーネルは、EAXレジスタで実行されたサービスのステータスを返します。この値は通常、errno変数を設定するためにglibcによって使用されます。

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
}
4
ZarathustrA

最小限の実行可能なアセンブリの例

hello_world.asm

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

コードから、次のことを簡単に推測できます。

もちろん、Assemblyはすぐに面倒になるので、できる限りglibc/POSIXが提供するCラッパーを使用するか、できない場合はSYSCALLマクロを使用することをお勧めします。