ジェネラルパート
編集:Linux の無関係な部分を削除
完全に間違っているわけではありませんが、08
に絞り込む と 16
29
のように質問を単純化しすぎている 少なくとも 3 番目のオプションがあります。
システムコール番号、ebx、ecx、edx、esi、edi、および ebp に 0x80 と eax を使用してパラメーターを渡すことは、システム コールを実装するために考えられる他の多くの選択肢の 1 つにすぎませんが、これらのレジスターは 32 ビット Linux ABI が選択したものです。 .
関連する手法を詳しく見ていく前に、すべてのプロセスが実行される特権刑務所から逃れるという問題を一巡していることを述べておく必要があります。
x86 アーキテクチャによって提供される、ここで提示されたものの別の選択肢は、コール ゲートの使用でした (参照:http://en.wikipedia.org/wiki/Call_gate)
すべての i386 マシンに存在する他の唯一の可能性は、ソフトウェア割り込みを使用することです。これにより、ISR (Interrupt Service Routine または単に 割り込みハンドラ ) 以前とは異なる特権レベルで実行します。
(面白い事実:一部の i386 OS は、無効な命令の例外を使用して、システム コールのためにカーネルに入る。 386 CPU の命令。可能なシステム コール メカニズムの概要については、OsDev syscall/sysret および sysenter/sysexit 命令を参照してください。)
ソフトウェア割り込み
割り込みがトリガーされると正確に何が起こるかは、ISR への切り替えに権限の変更が必要かどうかによって異なります。
(インテル® 64 および IA-32 アーキテクチャー・ソフトウェア開発者マニュアル)
<ブロック引用>6.4.1 割り込みまたは例外処理手順の呼び出しと戻り操作
...
ハンドラ プロシージャのコード セグメントが現在実行中のプログラムまたはタスクと同じ特権レベルを持っている場合、ハンドラ プロシージャは現在のスタックを使用します。ハンドラーが別の特権レベルで実行される場合、プロセッサはハンドラーの特権レベルのスタックに切り替えます。
....
スタック切り替えが発生した場合、プロセッサは次のことを行います:
<オール>SS、ESP、EFLAGS、CS、および> EIP レジスタの現在の内容を一時的に (内部的に) 保存します。
新しいスタック (つまり、呼び出されている特権レベルのスタック) のセグメント セレクターとスタック ポインターを TSS から SS および ESP レジスターにロードし、新しいスタックに切り替えます。
中断されたプロシージャのスタックの一時的に保存された SS、ESP、EFLAGS、CS、および EIP 値を新しいスタックにプッシュします。
新しいスタックにエラー コードをプッシュします (該当する場合)。
新しいコード セグメントのセグメント セレクターと新しい命令ポインター (割り込みゲートまたはトラップ ゲートから) を、それぞれ CS レジスターと EIP レジスターにロードします。
呼び出しが割り込みゲートを介している場合、EFLAGS レジスタの IF フラグをクリアします。
新しい特権レベルでハンドラ プロシージャの実行を開始します。
... はぁ、これはやるべきことがたくさんあるように思えます。
(上記と同じソースからの抜粋:Intel® 64 and IA-32 Architectures Software Developer's Manual)
<ブロック引用>中断されたプロシージャとは異なる特権レベルからの割り込みまたは例外ハンドラからの復帰を実行する場合、プロセッサは次のアクションを実行します:
<オール>権限チェックを実行します。
CS および EIP レジスタを割り込みまたは例外の前の値に復元します。
EFLAGS レジスタを復元します。
SS および ESP レジスタを割り込みまたは例外の前の値に復元し、割り込みを受けたプロシージャのスタックにスタック スイッチを戻します。
中断された手順の実行を再開します。
システム入力者
あなたの質問にはまったく言及されていませんが、それでも Linux カーネルによって利用される 32 ビット プラットフォームの別のオプションは 48
です。
(Intel® 64 and IA-32 Architectures Software Developer's Manual Volume 2 (2A, 2B &2C):Instruction Set Reference, A-Z)
<ブロック引用>説明 レベル 0 システム プロシージャまたはルーチンへの高速呼び出しを実行します。 SYSENTER は、SYSEXIT のコンパニオン命令です。この命令は、特権レベル 3 で実行されているユーザー コードからオペレーティング システムまたは特権レベル 0 で実行されている実行手順へのシステム コールに最大のパフォーマンスを提供するように最適化されています。
このソリューションを使用することの 1 つの欠点は、すべての 32 ビット マシンに存在するわけではないため、57
CPU が認識しない場合に備えて、メソッドを提供する必要があります。
SYSENTER および SYSEXIT 命令は、Pentium II プロセッサの IA-32 アーキテクチャに導入されました。プロセッサ上でこれらの命令が使用できるかどうかは、CPUID 命令によって EDX レジスタに返される SYSENTER/SYSEXITpresent (SEP) 機能フラグで示されます。 SEP フラグを認定するオペレーティング システムは、SYSENTER/SYSEXIT 命令が実際に存在することを確認するために、プロセッサ ファミリとモデルも認定する必要があります。
シスコール
最後の可能性、65
72
とほとんど同じ機能を可能にします。 命令。両方が存在するのは、一方が (88
) は Intel によって導入され、もう一方 (92
) ) は AMD によって導入されました。
Linux 固有
Linux カーネルでは、上記の 3 つの可能性のいずれかを選択して、システム コールを実現できます。
Linux システム コール決定版ガイドもご覧ください。 .
すでに上で述べたように、104
メソッドは、3 つの選択された実装のうちの唯一の実装であり、任意の i386 CPU で実行できるため、32 ビットのユーザー空間で常に使用できるのはこれだけです。
(117
64 ビットのユーザー空間で常に使用できる唯一のものであり、64 ビット コードで使用する必要がある唯一のものです。 x86-64 カーネルは 123
なしでビルドできます 、および 137
32 ビットへのポインターを切り捨てる 32 ビット ABI を引き続き呼び出します。)
3 つの選択肢すべてを切り替えることができるようにするために、実行中のシステム用に選択されたシステム コール実装へのアクセスを提供する特別な共有オブジェクトへのアクセスがすべてのプロセス実行に与えられます。これは奇妙に見える 144
です 155
を使用しているときに未解決のライブラリとして既に遭遇している可能性があります など。
(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;
}
それを利用するには、160
のようにすべてのレジスタ システム コール番号を eax に、パラメータを ebx、ecx、edx、esi、edi にロードするだけです。 システムコールの実装と 178
メインルーチン。
残念ながら、それほど簡単ではありません。 187
(仮想動的共有オブジェクト ) はランダム化されたプロセスで表示されるため、最初に正しい場所を把握する必要があります。
このアドレスは各プロセスに固有であり、プロセスが開始されるとプロセスに渡されます。
ご存じないかもしれませんが、Linux で起動すると、すべてのプロセスは、起動後に渡されたパラメーターへのポインターと、スタック上で実行されている環境変数の説明へのポインターを取得します。それぞれのプロセスは NULL で終了します。
これらに加えて、いわゆる elf-auxiliary-vectors の 3 番目のブロックが、前述のものに続いて渡されます。正しい場所は、タイプ識別子 196
を運ぶこれらのいずれかにエンコードされます .
したがって、スタック レイアウトは次のようになります (アドレスは下に向かって成長します):
- パラメータ-0
- ...
- パラメータ-m
- ヌル
- 環境-0
- ....
- 環境-n
- ヌル
- ...
- 補助エルフ ベクトル:
209
- ...
- 補助エルフ ベクトル:
219
使用例
正しいアドレスを見つけるには、最初にすべての引数とすべての環境ポインターをスキップしてから、223
のスキャンを開始する必要があります。 以下の例に示すように:
#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;
}
次の 237
のスニペットを見ればわかるように、 私のシステムで:
#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
私が使用した syscall は、eax レジスタに渡された 4 (書き込み) の番号が付けられたものです。
簡単に言うと
おそらく遅い実行中の 242
の比較 any のシステム コール (本当は AMD によって発明された) 253
を使用した (できれば) はるかに高速な実装を備えた Intel CPU 命令はリンゴとオレンジを比較しています。
IMHO:おそらく 263
275
の代わりの命令 ここでテストする必要があります。
カーネルを呼び出すとき (システム コールを作成するとき) に必要なことが 3 つあります。
<オール>明らかに、カーネルの内部に入ると、カーネルコードはカーネルに実際に何をさせたいかを知る必要があるため、EAXに何かを入れ、「開きたいファイルの名前" または "ファイルからデータを読み込むバッファ" など
プロセッサが異なれば、上記の 3 つの手順を実行する方法も異なります。 x86 にはいくつかの選択肢がありますが、手書きの asm で最も人気のある 2 つは 285
です。 (32 ビット モード) または 298
(64 ビット モード)。 (32ビットモード 302
もあります AMD が 313
の 32 ビット モード バージョンを導入したのと同じ理由で、Intel によって導入されました。 :遅い 325
のより高速な代替手段として . 32 ビット glibc は、利用可能な効率的なシステム コール メカニズムを使用しますが、低速の 336
のみを使用します。 これ以上のものがない場合)
345
の 64 ビット版 命令は、システム コールに入る高速な方法として x86-64 アーキテクチャで導入されました。これには、ジャンプ先のアドレス RIP、CS および SS にロードするセレクタ値、および Ring3 から Ring0 への遷移を実行するための一連のレジスタ (x86 MSR メカニズムを使用) があります。また、リターン アドレスを ECX/RCX に格納します。 [この命令のすべての詳細については、命令セットのマニュアルをお読みください - それは完全に簡単ではありません!].プロセッサはこれが Ring0 に切り替わることを知っているので、正しいことを直接行うことができます。
重要なポイントの 1 つは、351
です。 レジスタのみを操作します。読み込みや保存は行いません。 (これが、保存された RIP で RCX を上書きし、保存された RFLAGS で R11 を上書きする理由です)。メモリ アクセスはページ テーブルに依存し、ページ テーブル エントリには、ユーザー空間ではなくカーネルに対してのみ有効にするビットがあるため、その間メモリ アクセスを行う 特権レベルの変更は、レジスターへの書き込みだけではなく、待機する必要がある場合があります。カーネル モードになると、カーネルは通常 364
を使用します。 またはカーネルスタックを見つける他の方法。 (378
しません RSP を変更します。カーネルへのエントリでは、まだユーザー スタックを指しています。)
SYSRET命令を使用して戻る場合、値はレジスタ内の所定の値から復元されるため、プロセッサはいくつかのレジスタをセットアップするだけでよいため、これも迅速です.プロセッサは、Ring0 から Ring3 に変わることを認識しているため、適切な処理を迅速に行うことができます。
(AMD CPU は 382
をサポートします 32 ビットのユーザー空間からの命令。 Intel CPU にはありません。 x86-64 は元々 AMD64 でした。これが 395
がある理由です 64ビットモードで。 AMD は 404
のカーネル側を再設計しました 64 ビット モードの場合、64 ビットの 412
カーネル エントリ ポイントは、32 ビットの 428
とは大きく異なります。 64 ビット カーネルのエントリ ポイント)
430
32 ビット モードで使用されるバリアントは、メモリからの読み取りを意味する割り込み記述子テーブルの値に基づいて何をすべきかを決定します。そこで、新しい CS と EIP/RIP の値を見つけます。新しい CS レジスタは、新しい「リング」レベル (この場合は Ring0) を決定します。次に、新しい CS 値を使用して (TR レジスタに基づいて) タスク状態セグメントを調べ、どのスタック ポインター (ESP/RSP および SS) を見つけ、最後に新しいアドレスにジャンプします。これは直接的ではなく、より一般的なソリューションであるため、速度も遅くなります。古い EIP/RIP および CS は、SS および ESP/RSP の古い値とともに、新しいスタックに格納されます。
戻るとき、IRET 命令を使用して、プロセッサはスタックから戻りアドレスとスタック ポインタ値を読み取り、スタックから新しいスタック セグメントとコード セグメント値もロードします。繰り返しますが、このプロセスは一般的なものであり、かなりの数のメモリ読み取りが必要です。これは一般的なものであるため、プロセッサは「Ring0 から Ring3 にモードを変更するかどうか、変更する場合はこれらを変更するかどうか」も確認する必要があります。
つまり、要約すると、そのように動作することを意図していたため、高速です。
32 ビット コードの場合は、確かに低速で互換性のある 440
を使用できます。
64 ビット コードの場合、450
461
より遅い ポインターを 32 ビットに切り詰めるので、使用しないでください。 64 ビット コードで 32 ビット int 0x80 Linux ABI を使用するとどうなりますか? を参照してください。さらに、478
すべてのカーネルで 64 ビット モードで使用できるわけではないため、486
でも安全ではありません。 ポインタ引数を取らない:498
無効にすることができ、特には Linux 用 Windows サブシステムでは無効です。