webdevqa.jp.net

Intelx86とx64のシステムコール

X86とx64のアセンブリの違いについて読んでいます。

X86では、システムコール番号がeaxに配置され、次にint 80hが実行されてソフトウェア割り込みが生成されます。

ただし、x64では、システムコール番号がraxに配置され、syscallが実行されます。

syscallは、ソフトウェア割り込みを生成するよりも軽量で高速であると言われています。

X64ではx86よりも高速なのはなぜですか。int 80hを使用してx64でシステムコールを実行できますか?

26
becks

一般的な部分

編集:Linuxに関係のない部分が削除されました

完全に間違っているわけではありませんが、int 0x80syscallに絞り込むと、sysenterの場合と同様に、質問が単純化されすぎます。少なくとも3番目のオプションがあります。

Syscall番号、ebx、ecx、edx、esi、edi、およびebpに0x80とeaxを使用してパラメーターを渡すことは、システムコールを実装するための他の多くの選択肢の1つにすぎませんが、これらのレジスターは32ビットLinuxABIが選択したものです。 。

関連するテクニックを詳しく見る前に、すべてのプロセスが実行される特権刑務所からの脱出の問題をすべて巡回していることを述べておく必要があります。

X86アーキテクチャによって提供されるここに提示されたものに対する別の選択は、コールゲートの使用でした(参照: http://en.wikipedia.org/wiki/Call_gate

すべてのi386マシンに存在する他の唯一の可能性は、ソフトウェア割り込みを使用することです。これにより、ISR(割り込みサービスルーチンまたは単に割り込みハンドラ)以前とは異なる特権レベルで実行する。

(おもしろい事実:一部のi386 OSは、システムコールのカーネルに入るのに無効な命令例外を使用しました。これは実際には386CPUのint命令よりも高速だったためです。 OsDevsyscall/sysretおよび可能なシステムコールメカニズムの要約については、sysenter/sysexit命令を有効にします

ソフトウェア割り込み

割り込みがトリガーされると正確に何が起こるかは、ISRへの切り替えに特権の変更が必要かどうかによって異なります。

(インテル®64およびIA-32アーキテクチャーソフトウェア開発者マニュアル)

6.4.1割り込みまたは例外処理手順の呼び出しおよび戻り操作

.。

ハンドラープロシージャのコードセグメントが現在実行中のプログラムまたはタスクと同じ特権レベルを持っている場合、ハンドラープロシージャは現在のスタックを使用します。ハンドラーがより特権レベルで実行される場合、プロセッサーはハンドラーの特権レベルのスタックに切り替わります。

..。

スタックスイッチが発生した場合、プロセッサは次のことを行います。

  1. SS、ESP、EFLAGS、CS、および> EIPレジスタの現在の内容を一時的に(内部的に)保存します。

  2. 新しいスタック(つまり、呼び出されている特権レベルのスタック)のセグメントセレクターとスタックポインターをTSSからSSにロードし、ESPレジスターを作成して、新しいスタックに切り替えます。

  3. 中断されたプロシージャのスタックに対して一時的に保存されたSS、ESP、EFLAGS、CS、およびEIP値を新しいスタックにプッシュします。

  4. 新しいスタックにエラーコードをプッシュします(適切な場合)。

  5. 新しいコードセグメントのセグメントセレクタと新しい命令ポインタ(割り込みゲートまたはトラップゲートから)をそれぞれCSレジスタとEIPレジスタにロードします。

  6. 呼び出しが割り込みゲートを経由している場合は、EFLAGSレジスタのIFフラグをクリアします。

  7. 新しい特権レベルでハンドラープロシージャの実行を開始します。

...ため息をつくこれはやることがたくさんあるようで、一度終わってもそれほど良くなることはありません:

(上記と同じソースからの抜粋:インテル®64およびIA-32アーキテクチャーソフトウェア開発者マニュアル)

割り込みからの復帰または割り込みされたプロシージャとは異なる特権レベルからの例外ハンドラを実行する場合、プロセッサは次のアクションを実行します。

  1. 特権チェックを実行します。

  2. CSおよびEIPレジスタを割り込みまたは例外の前の値に復元します。

  3. EFLAGSレジスタを復元します。

  4. SSとESPレジスタを割り込みまたは例外の前の値に復元し、その結果、スタックが割り込みプロシージャのスタックに戻ります。

  5. 中断されたプロシージャの実行を再開します。

システム

質問にはまったく触れられていませんが、Linuxカーネルで使用されている32ビットプラットフォームのもう1つのオプションは、sysenter命令です。

(インテル®64およびIA-32アーキテクチャーソフトウェア開発者マニュアル第2巻(2A、2B、および2C):命令セットリファレンス、A〜Z)

説明レベル0のシステムプロシージャまたはルーチンへの高速呼び出しを実行します。 SYSENTERは、SYSEXITのコンパニオン命令です。この命令は、特権レベル3で実行されているユーザーコードから、特権レベル0で実行されているオペレーティングシステムまたは実行プロシージャへのシステムコールに最大のパフォーマンスを提供するように最適化されています。

このソリューションを使用することの欠点の1つは、すべての32ビットマシンに存在するわけではないため、CPUがそれを認識しない場合に備えて、int 0x80メソッドを提供する必要があることです。

SYSENTERおよびSYSEXIT命令は、PentiumIIプロセッサのIA-32アーキテクチャに導入されました。プロセッサでのこれらの命令の可用性は、CPUID命令によってEDXレジスタに返されるSYSENTER/SYSEXIT present(SEP)機能フラグで示されます。 SEPフラグを修飾するオペレーティングシステムは、SYSENTER/SYSEXIT命令が実際に存在することを確認するために、プロセッサフ​​ァミリとモデルも修飾する必要があります。

システムコール

最後の可能性であるsyscall命令は、sysenter命令とほぼ同じ機能を可能にします。両方の存在は、一方(systenter)がIntelによって導入され、もう一方(syscall)がAMDによって導入されたという事実によるものです。

Linux固有

Linuxカーネルでは、システムコールを実現するために、上記の3つの可能性のいずれかを選択できます。

Linuxシステムコールの決定的なガイド も参照してください。

すでに上で述べたように、int 0x80メソッドは、選択された3つの実装のうち、任意のi386 CPUで実行できる唯一の方法であるため、32ビットユーザースペースで常に使用できる唯一の方法です。

syscallは64ビットユーザースペースで常に使用できる唯一のものであり、 64ビットコードで使用する必要がある唯一のもの ; x86-64カーネルを構築できますCONFIG_IA32_EMULATIONがなくても、int 0x80は32ビットABIを呼び出し、32ビットへのポインターを切り捨てます。)

3つの選択肢すべてを切り替えることができるように、すべてのプロセス実行には、実行中のシステム用に選択されたシステムコール実装へのアクセスを提供する特別な共有オブジェクトへのアクセスが許可されます。これは、lddなどを使用しているときに未解決のライブラリとしてすでに遭遇した可能性のある奇妙な外観のlinux-gate.so.1です。

(Arch/x86/vdso/vdso32-setup.c)

 if (vdso32_syscall()) {                                                                               
        vsyscall = &vdso32_syscall_start;                                                                 
        vsyscall_len = &vdso32_syscall_end - &vdso32_syscall_start;                                       
    } else if (vdso32_sysenter()){                                                                        
        vsyscall = &vdso32_sysenter_start;                                                                
        vsyscall_len = &vdso32_sysenter_end - &vdso32_sysenter_start;                                     
    } else {                                                                                              
        vsyscall = &vdso32_int80_start;                                                                   
        vsyscall_len = &vdso32_int80_end - &vdso32_int80_start;                                           
    }   

それを利用するには、int 0x80システムコール実装およびcallメインルーチンと同様に、すべてのレジスタシステムコール番号をeaxに、パラメータをebx、ecx、edx、esi、ediにロードするだけです。 。

残念ながら、それはそれほど簡単ではありません。事前定義された固定アドレスのセキュリティリスクを最小限に抑えるために、プロセスでvdso仮想動的共有オブジェクト)が表示される場所はランダム化されます。したがって、最初に正しい場所を特定する必要があります。

このアドレスは各プロセスに固有であり、開始されるとプロセスに渡されます。

ご存じないかもしれませんが、Linuxで起動すると、すべてのプロセスは、起動後に渡されるパラメーターへのポインターと、実行中の環境変数の説明へのポインターをスタックに渡されます。各プロセスはNULLで終了します。

これらに加えて、いわゆるelf-auxiliary-vectorsの3番目のブロックが、前述のブロックに続いて渡されます。正しい場所は、タイプ識別子AT_SYSINFOを持つこれらのいずれかにエンコードされます。

したがって、スタックレイアウトは次のようになります(アドレスは下に向かって大きくなります)。

  • パラメータ-0
  • .。
  • パラメータ-m
  • ヌル
  • 環境-0
  • ..。
  • 環境-n
  • ヌル
  • .。
  • 補助エルフベクトル:AT_SYSINFO
  • .。
  • 補助エルフベクトル:AT_NULL

使用例

正しいアドレスを見つけるには、最初にすべての引数とすべての環境ポインターをスキップしてから、以下の例に示すようにAT_SYSINFOのスキャンを開始する必要があります。

#include <stdio.h>
#include <elf.h>

void putc_1 (char c) {
  __asm__ ("movl $0x04, %%eax\n"
           "movl $0x01, %%ebx\n"
           "movl $0x01, %%edx\n"
           "int $0x80"
           :: "c" (&c)
           : "eax", "ebx", "edx");
}

void putc_2 (char c, void *addr) {
  __asm__ ("movl $0x04, %%eax\n"
           "movl $0x01, %%ebx\n"
           "movl $0x01, %%edx\n"
           "call *%%esi"
           :: "c" (&c), "S" (addr)
           : "eax", "ebx", "edx");
}


int main (int argc, char *argv[]) {

  /* using int 0x80 */
  putc_1 ('1');


  /* rather nasty search for jump address */
  argv += argc + 1;     /* skip args */
  while (*argv != NULL) /* skip env */
    ++argv;            

  Elf32_auxv_t *aux = (Elf32_auxv_t*) ++argv; /* aux vector start */

  while (aux->a_type != AT_SYSINFO) {
    if (aux->a_type == AT_NULL)
      return 1;
    ++aux;
  }

  putc_2 ('2', (void*) aux->a_un.a_val);

  return 0;
}

私のシステムの/usr/include/asm/unistd_32.hの次のスニペットを見てわかるように:

#define __NR_restart_syscall 0
#define __NR_exit            1
#define __NR_fork            2
#define __NR_read            3
#define __NR_write           4
#define __NR_open            5
#define __NR_close           6

私が使用したシステムコールは、eaxレジスタで渡された4(書き込み)の番号が付けられたものです。 filedescriptor(ebx = 1)、data-pointer(ecx =&c)、およびsize(edx = 1)を引数として取り、それぞれが対応するレジスターに渡されます。

長い話を短くする

実行速度が遅いと思われるint 0x80システムコールをanyIntel CPUと、(AMDによって真に発明された)syscallを使用した(願わくば)はるかに高速な実装と比較する指示はリンゴとオレンジを比較することです。

IMHO:おそらく、int 0x80ではなくsysenter命令をここでテストする必要があります。

29
mikyra

カーネルを呼び出す(システムコールを行う)ときに発生する必要があることは3つあります。

  1. システムは「ユーザーモード」から「カーネルモード」(リング0)に移行します。
  2. スタックは「ユーザーモード」から「カーネルモード」に切り替わります。
  3. カーネルの適切な部分にジャンプします。

明らかに、カーネル内に入ると、カーネルコードは、カーネルに実際に何をさせたいかを知る必要があります。したがって、EAXに何かを入れ、「開きたいファイルの名前」のようなものがあるため、他のレジスタにもっと多くのことを入れます。 「または「ファイルからデータを読み込むためのバッファ」など。

プロセッサが異なれば、上記の3つのステップを実現する方法も異なります。 x86にはいくつかの選択肢がありますが、手書きのasmで最も一般的な2つは、int 0xnn(32ビットモード)またはsyscall(64ビットモード)です。 (32ビットモードsysenterもあります。これは、AMDが遅いint 0x80のより高速な代替手段としてsyscallの32ビットモードバージョンを導入したのと同じ理由でIntelによって導入されました。 32ビットglibcは、利用可能な効率的なシステムコールメカニズムを使用しますが、それ以上の機能がない場合は、低速のint 0x80のみを使用します。)

syscall命令 の64ビットバージョンは、システムコールを入力するためのより高速な方法としてx86-64アーキテクチャで導入されました。これには、ジャンプ先のアドレスRIP、CSおよびSSにロードするセレクター値、およびRing3からRing0への遷移を実行するためのレジスターのセット(x86 MSRメカニズムを使用)があります。また、リターンアドレスをECX/RCXに保存します。 [この命令のすべての詳細については、命令セットのマニュアルをお読みください-それは完全に些細なことではありません!]。プロセッサはこれがRing0に切り替わることがわかっているので、正しいことを直接行うことができます。

重要なポイントの1つは、syscallはレジスタのみを操作することです。ロードやストアは行いません。(これが、RCXを保存されたRIPで上書きし、R11を保存されたRFLAGSで上書きする理由です)。メモリアクセスはページテーブルに依存し、ページテーブルエントリにはユーザースペースではなくカーネルに対してのみ有効にすることができるビットがあるため、メモリアクセスを実行するとwhile特権レベルの変更は待機する必要がある場合がありますvs 。レジスタを書き込むだけです。カーネルモードに入ると、カーネルは通常、swapgsまたはカーネルスタックを見つける他の方法を使用します。 (syscall does not RSPを変更します。カーネルへのエントリ時にユーザースタックをポイントします。)

SYSRET命令を使用して戻る場合、値はレジスタ内の所定の値から復元されます。これも、プロセッサがいくつかのレジスタを設定するだけでよいため、迅速です。プロセッサは、Ring0からRing3に変更されることを認識しているため、適切な処理をすばやく実行できます。

(AMDCPUは32ビットユーザースペースからのsyscall命令をサポートします。IntelCPUはサポートしません。x86-64は元々AMD64でした。これが64ビットモードでsyscallを使用する理由です。AMD syscallのカーネル側を64ビットモード用に再設計したため、64ビットのsyscallカーネルエントリポイントは、64の32ビットのsyscallエントリポイントとは大幅に異なります。ビットカーネル。)

32ビットモードで使用されるint 0x80バリアントは、割り込み記述子テーブルの値に基づいて何をするかを決定します。これは、メモリからの読み取りを意味します。そこで、新しいCS値とEIP/RIP値が見つかります。新しいCSレジスタは、新しい「リング」レベル(この場合はRing0)を決定します。次に、新しいCS値を使用して(TRレジスタに基づく)タスク状態セグメントを調べて、どのスタックポインタ(ESP/RSPおよびSS)を見つけ、最後に新しいアドレスにジャンプします。これは直接的ではなく、より一般的なソリューションであるため、速度も遅くなります。古いEIP/RIPとCSは、SSとESP/RSPの古い値とともに新しいスタックに保存されます。

戻るとき、IRET命令を使用して、プロセッサはスタックから戻りアドレスとスタックポインタ値を読み取り、スタックから新しいスタックセグメントとコードセグメント値もロードします。この場合も、プロセスは一般的であり、かなりの数のメモリ読み取りが必要です。これは汎用であるため、プロセッサは「モードをRing0からRing3に変更するかどうか、変更する場合はこれらを変更するか」も確認する必要があります。

したがって、要約すると、そのように機能するように意図されていたため、より高速です。

32ビットコードの場合、はい、必要に応じて、低速で互換性のあるint 0x80を確実に使用できます。

64ビットコードの場合、int 0x80syscallよりも遅く、ポインタを32ビットに切り捨てるので、使用しないでください。 2ビットint 0x80 Linux ABIを64ビットコードで使用するとどうなりますか? さらに、int 0x80はすべてのカーネルで64ビットモードで使用できるわけではないため、使用できません。ポインタ引数をとらないsys_exitでも安全です:CONFIG_IA32_EMULATIONは無効にできます。特に、Linux用のWindowsサブシステムではis無効にできます。

16
Mats Petersson