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
直接ニアコール エンコーディングを使用することです。rel32
target - 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
間接ジャンプは、直接 ( )よりも分岐予測ミスのペナルティがわずかに悪くなります。通常の直接call
insn の宛先は、デコードされるとすぐにわかります。パイプラインの早い段階で、そこに分岐があることが検出されるとすぐにわかります。
一般に、間接分岐は最新の 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
で始まる静的な実行可能ファイルに実際にリンクしようとすると、 .0000000000400080
ld -o foo foo.o
foo.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 をリターン アドレスとしてプッシュするため、call
EIP のみをプッシュする通常の (near) とは互換性がありません。これは にとっては問題ではありませんjmp ptr16:32
。速度が遅く、セグメント部分に何を配置するかを考え出すだけです。
CS の変更は、通常、32 ビット モードから 64 ビット モードに、またはその逆に変更する場合にのみ役立ちます。通常、カーネルのみがこれを行いますが、GDT に 32 ビットおよび 64 ビットのセグメント記述子を保持するほとんどの通常の OS では、ユーザー空間でこれを行うことができます。ただし、それは有用なものというよりはばかげたコンピューターのトリックです。iret
(64 ビット カーネルは、またはおそらく を使用して 32 ビット ユーザー空間に戻りますsysexit
。ほとんどの OS は、起動時に far jmp を 1 回だけ使用して、カーネル モードで 64 ビット コード セグメントに切り替えます。)
主流の OS は、変更する必要のないフラット メモリ モデルを使用しており、ユーザー空間プロセスに使用される値はcs
標準化されていません。cs
far を使用したい場合でもjmp
、セグメント セレクター部分にどのような値を入れるかを考え出す必要があります。(JIT 中は簡単: で現在cs
を読み取るだけmov eax, cs
です。ただし、事前コンパイルのために移植するのは困難です。)
call ptr16:64
far direct エンコーディングは、16 ビット コードと 32 ビット コードに対してのみ存在します。64 ビット モードでは、 のようなcall
10 バイトのm16:64
メモリ オペランドでのみ far- できますcall far [rdi]
。または、スタックにセグメント:オフセットをプッシュして使用しますretf
。