12

callx86マシンコードの絶対ポインタへの「正しい」方法は何ですか? 単一の命令でそれを行う良い方法はありますか?

私がしたいこと:

「サブルーチンスレッド」に基づいて、一種の単純化されたミニ JIT (まだ) を構築しようとしています。これは基本的に、バイトコード インタープリターから可能な限り最短のステップ アップです。各オペコードは個別の関数として実装されるため、バイトコードの各基本ブロックは、次のような独自の新しい手順に "JIT" できます。

{prologue}
call {opcode procedure 1}
call {opcode procedure 2}
call {opcode procedure 3}
...etc
{epilogue}

したがって、すべてのブロックの実際のマシン コードはテンプレートから貼り付けることができ (必要に応じて中央部分を拡張する)、「動的に」処理する必要がある唯一のビットは、各オペコードの関数ポインターをコピーすることです。各呼び出し命令の一部として適切な場所。

私が抱えている問題call ...は、テンプレートの一部に何を使用するかを理解することです。x86 は、この種の使用法を念頭に置いてセットアップされているようには見えず、相対呼び出しと間接呼び出しを優先します。

または のいずれかを使用して、仮説的に関数を呼び出すことができるように見えます(基本的に、アセンブラーと逆アセンブラーに何かを入れて、それらが何をするかを理解するのではなく、何が有効な結果を生成したかを確認することでこれらを発見しました)が、私はそのことについて理解していませんセグメントと権限、および関連する情報を十分に確認して、違い、またはこれらがより頻繁に見られる命令とどのように異なる動作をするかを確認してください。Intel アーキテクチャ マニュアルでは、これらは 32 ビット モードでのみ有効であり、64 ビット モードでは「無効」であることが示唆されています。FF 15 EFBEADDE2E FF 15 EFBEADDEDEADBEEFcall

誰かがこれらのオペコードを説明できますか?また、この目的でそれらまたは他のオペコードをどのように使用するか、または使用するかどうかを説明できますか?

(レジスタを介した間接呼び出しを使用するという明らかな答えもありますが、それは「間違った」アプローチのようです-直接呼び出し命令が実際に存在すると仮定します。)

4

2 に答える 2

12

jmp絶対アドレスにも適用され、ターゲットを指定するための構文も同じです。質問は JITing について尋ねますが、範囲を広げるために NASM と AT&T の構文も含めました。

JITコードから事前にコンパイルされた関数を呼び出すために使用できるように、「近くの」メモリを割り当てる方法については、JIT 内の遠く離れた組み込み関数への呼び出しの処理も参照してください。rel32


x86 には、通常 (near)callまたはjmp命令でエンコードされた絶対アドレスへのエンコード がありません。必要でない場合を除いjmp farて、絶対直接呼び出し/jmp エンコードはありません。については、Intel の insn set ref マニュアル エントリをcall参照してください。(ドキュメントやガイドへの他のリンクについては、 x86 タグ wikiも参照してください。) ほとんどのコンピューター アーキテクチャでは、x86 のような通常のジャンプに相対エンコーディングを使用します。

最良のオプション (自分のアドレスを知っている位置依存コードを作成できる場合call rel32) は、フィールドが(2 の補数の 2 進整数)である通常のE8 rel32直接ニアコール エンコーディングを使用することです。rel32target - end_of_call_insn

$ は NASM で正確にどのように機能するかを参照してください。call命令を手動でエンコードする例。JITしながらそれを行うのも同じくらい簡単なはずです。

AT&T 構文の場合: call 0x1234567
NASM 構文の場合:call 0x1234567

絶対アドレスを持つ名前付きシンボル (equまたはで作成されたものなど.set) に対しても機能します。MASM に相当するものはありません。宛先としてラベルのみを受け入れるように見えるため、ツールチェーン (および/またはオブジェクト ファイル形式の再配置タイプ) の制限を回避するために非効率的な回避策を使用することがあります。

これらは、位置に依存するコード (共有ライブラリまたは PIE 実行可能ファイルではない) で問題なくアセンブルおよびリンクします。ただし、テキスト セクションが 4GiB より上にマップされている x86-64 OS X ではそうではないため、rel32.

呼び出したい絶対アドレスの範囲内に JIT バッファーを割り当てます。 たとえば、mmap(MAP_32BIT)Linux では、+-2GB がその領域内の他のアドレスに到達できる下位 2GB にメモリを割り当てるか、ジャンプ先の近くのどこかに非 NULL ヒント アドレスを提供します。(ただし、使用しないMAP_FIXEDでください。ヒントが既存のマッピングと重複する場合は、カーネルに別のアドレスを選択させるのがおそらく最善です。)

(Linux の非 PIE 実行可能ファイルは、下位 2 GB の仮想アドレス空間にマップされるため、[disp32 + reg]符号拡張された 32 ビット絶対アドレスで配列インデックスを使用したりmov eax, imm32、ゼロ拡張絶対アドレスを使用して静的アドレスをレジスタに配置したりできます。したがって、下位 2 GB、 4GB 未満ではありません。 しかし、PIE 実行可能ファイルは標準になりつつあるため、ビルド + リンクを使用しない限り、メインの実行可能ファイルの静的アドレスが 32 未満であると想定しないでください-no-pie -fno-pie。また、OS X などの他の OS では、実行可能ファイルは常に 4GB を超えて配置されます。 .)


call rel32使えるようにできない場合

しかし、それ自身の絶対アドレスを知らない位置に依存しないコードを作成する必要がある場合、または呼び出す必要があるアドレスが呼び出し元から +-2GiB 以上離れている場合(64 ビットでは可能ですが、コードが十分に近い)、レジスタ間接を使用する必要がありますcall

; use any register you like as a scratch
mov   eax, 0xdeadbeef               ; 5 byte  mov r32, imm32
     ; or mov rax, 0x7fffdeadbeef   ; for addresses that don't fit in 32 bits
call  rax                           ; 2 byte  FF D0

または AT&T 構文

mov   $0xdeadbeef, %eax
# movabs $0x7fffdeadbeef, %rax      # mov r64, imm64
call  *%rax

r10明らかに、x86-64 System V で呼び出しが上書きされているが、引数の受け渡しには使用されていない、または任意のレジスタを使用できますr11。AL = 可変引数関数への XMM 引数の数。 x86-64 System V 呼び出し規約での可変個引数関数の呼び出し。

本当にレジスタの変更を避ける必要がある場合は、絶対アドレスをメモリ内の定数として保持し、call次のように RIP 相対アドレス指定モードでメモリ間接を使用してください。

NASM call [rel function_pointer] ; reg
AT&Tを削除できない場合call *function_pointer(%rip)


間接呼び出し/ジャンプは、特に同じプロセス内で信頼されていないコードのサンドボックスの一部として JIT を実行している場合、コードを Spectre 攻撃に対して脆弱にする可能性があることに注意してください。(その場合、カーネル パッチだけでは保護されません)。

パフォーマンスを犠牲にしてスペクターを軽減するために、通常の間接ブランチの代わりに「retpoline」が必要になる場合があります。

call rel32間接ジャンプは、直接 ( )よりも分岐予測ミスのペナルティがわずかに悪くなります。通常の直接callinsn の宛先は、デコードされるとすぐにわかります。パイプラインの早い段階で、そこに分岐があることが検出されるとすぐにわかります。

一般に、間接分岐は最新の x86 ハードウェアで適切に予測され、動的ライブラリ/DLL の呼び出しによく使用されます。それはひどいものではありませんが、call rel32間違いなく優れています。

ただし、パイプラインのバブルを完全に回避するには、直接でもcall分岐予測が必要です。(デコード前に予測が必要です。たとえば、このブロックをフェッチしたばかりで、フェッチ ステージで次にフェッチする必要があるブロックを考えると、jmp next_instruction 分岐予測エントリが不足すると、一連の処理が遅くなります)。 mov+call regコードサイズが大きく、uops が多いため、完全な分岐予測を使用しても間接はさらに悪化しますが、それはかなり最小限の影響です。余分なmovものが問題になる場合は、可能であれば、コードを呼び出す代わりにインライン化することをお勧めします。


楽しい事実:call 0xdeadbeefリンカー スクリプトを使用して.textセクション/テキスト セグメントをそのアドレスの近くに配置しない限り、アセンブルはしますが、Linux で 64 ビットの静的実行可能ファイルにリンクしません。.textセクションは通常0x400080、静的実行可能ファイル (または非 PIE 動的実行可能ファイル) で始まります。つまり、すべての静的コード/データがデフォルト コード モデルに存在する下位 2GiB の仮想アドレス空間です。ただし0xdeadbeef、下位 32 ビットの上位半分 (つまり、下位 4G ではあるが下位 2G ではない) にあるため、ゼロ拡張 32 ビット整数として表すことはできますが、符号拡張 32 ビットとして表すことはできません。また0x00000000deadbeef - 0x0000000000400080、正しく 64 ビットに拡張される符号付き 32 ビット整数には適合しません。(マイナスで到達できるアドレス空間の部分rel32下位アドレスからラップアラウンドするのは、64 ビットアドレス空間の上位 2GiB です。通常、アドレス空間の上半分はカーネルが使用するために予約されています。)

で問題なく組み立てられyasm -felf64 -gdwarf2 foo.asm、次のようにobjdump -drwC -Mintel表示されます。

foo.o:     file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <.text>:
    0:   e8 00 00 00 00       call   0x5   1: R_X86_64_PC32        *ABS*+0xdeadbeeb

しかし、 .textがldで始まる静的な実行可能ファイルに実際にリンクしようとすると、 .0000000000400080ld -o foo foo.ofoo.o:/tmp//foo.asm:1:(.text+0x1): relocation truncated to fit: R_X86_64_PC32 against '*ABS*'

32 ビット コードでは、どこからでもどこにでもアクセスできるcall 0xdeadbeefため、問題なくアセンブルおよびリンクできます。rel32相対変位は 64 ビットに符号拡張する必要はありません。ラップアラウンドできるかどうかに関係なく、32 ビットのバイナリ加算だけです。


ダイレクト farcallエンコーディング (遅い、使用しないでください)

callおよびのマニュアル エントリでjmp、命令に直接エンコードされた絶対ターゲット アドレスを持つエンコーディングがあることに気付くかもしれません。しかし、それらは「遠い」call/新しいコードセグメントセレクターにjmpも設定されているためだけに存在しますが、これは遅いです(Agner Fog のガイドを参照してください)CS

CALL ptr16:32(「Call far, absolute, address given in operand」)には、通常のアドレス指定モードで指定された場所からデータとしてロードするのではなく、6 バイトのセグメント: オフセットが命令にエンコードされています。したがって、絶対アドレスへの直接呼び出しです。

Farcallも EIP だけでなく CS:EIP をリターン アドレスとしてプッシュするため、callEIP のみをプッシュする通常の (near) とは互換性がありません。これは にとっては問題ではありませんjmp ptr16:32。速度が遅く、セグメント部分に何を配置するかを考え出すだけです。

CS の変更は、通常、32 ビット モードから 64 ビット モードに、またはその逆に変更する場合にのみ役立ちます。通常、カーネルのみがこれを行いますが、GDT に 32 ビットおよび 64 ビットのセグメント記述子を保持するほとんどの通常の OS では、ユーザー空間でこれを行うことができます。ただし、それは有用なものというよりはばかげたコンピューターのトリックです。iret(64 ビット カーネルは、またはおそらく を使用して 32 ビット ユーザー空間に戻りますsysexit。ほとんどの OS は、起動時に far jmp を 1 回だけ使用して、カーネル モードで 64 ビット コード セグメントに切り替えます。)

主流の OS は、変更する必要のないフラット メモリ モデルを使用しており、ユーザー空間プロセスに使用される値はcs標準化されていません。csfar を使用したい場合でもjmp、セグメント セレクター部分にどのような値を入れるかを考え出す必要があります。(JIT 中は簡単: で現在csを読み取るだけmov eax, csです。ただし、事前コンパイルのために移植するのは困難です。)


call ptr16:64far direct エンコーディングは、16 ビット コードと 32 ビット コードに対してのみ存在します。64 ビット モードでは、 のようなcall10 バイトのm16:64メモリ オペランドでのみ far- できますcall far [rdi]。または、スタックにセグメント:オフセットをプッシュして使用しますretf

于 2016-04-09T00:54:29.613 に答える
1

たった1つの指示でそれを行うことはできません。それを行う適切な方法は、MOV + CALL を使用することです。

0000000002347490: 48b83412000000000000  mov rax, 0x1234
000000000234749a: 48ffd0                call rax

呼び出すプロシージャのアドレスが変更された場合は、オフセット 2 から始まる 8 バイトを変更します。0x1234 を呼び出しているコードのアドレスが変更された場合、アドレス指定は絶対的であるため、何もする必要はありません。

于 2016-04-08T23:30:16.420 に答える