用語:「オペコード」は、演算を選択する命令の一部であり、オペランドや、演算を変更する必須ではないプレフィックス(オペランドサイズなど)は含まれません。シェルコードについて話している人によってかなり頻繁に行われていますが、命令全体を参照するために「オペコード」を使用することは正しくありません。
それはあなたが経験から知っておくべきことですか
機械語を調べた経験、または特にコードサイズを最適化した経験があると、そうです。繰り返し調べたものを思い出し始め、asm行を調べて、命令の長さを知る方法を学びます。バイトが何になるかを覚えずに。
オペランドのエンコード規則はオペコードに依存しないため、オペコードの長さと、オペランドのエンコードにModR/Mバイトを使用しない特殊な場合の短い形式を覚えておく必要があります。そして、オペランドのエンコード規則を個別に覚えておいてください。
個人的には、このようなコードゴルフの質問にx86マシンコードで答えるのが好きです。(x86 / x64マシンコードでのゴルフのヒントも参照してください)。私はNASMに書き込み、各命令の長さを計画/把握し、アセンブラーに実際のマシンコードの16進ダンプをリストとして生成させます。コードゴルフに役立つ短い命令については、最近、命令の長さについて間違っていたことを覚えていませんが、興味深いと思う詳細(x86命令セットなど)のメモリが十分にあるのは幸運です。またはたくさん使う。rorx
(私はそれがどれくらいの長さであるかを見ようとしなければなりませんでした。)
マシンコードバイトを自分で入力することはありません。手作業でそれを行うには、マニュアルで各手順を調べる必要があります。x86にはPC相対アドレス指定用の短いエンコーディングがないため、マシンコード内で有用な定数(データを兼ねることができる)を見つけて作成することは重要ではないため、コードゴルフが数値を記憶することは一般的に有用ではありません命令エンコーディングの詳細。
パフォーマンスを最適化する場合、通常、他のすべてが等しい場合は小さい方が良いため、コードサイズ、特に配置を気にすることは間違いなくパフォーマンスの一部です。
または、どのオペランド/演算子の組み合わせが何バイトを占めるかを調べる方法はありますか?
これはマニュアルに詳しく記載されています。いくつかの特殊なケースの1バイト命令を除いて、オペランドのエンコードは(ほとんど)すべてで同じです。
ほとんどのx86命令のマシンコードエンコーディングは、次のパターンに従います( @Mehrdadの回答にあるIntelのより良い図バージョン):
[prefixes] opcode ModR/M [extra addressing-mode bytes] [immediate]
(明示的なオペランドのない命令には、ModR / Mバイトはなく、オペコードバイトのみがあります)。
x86オペコードは、最も一般的な命令、特に8086以降に存在する命令では1バイトです。後で追加される命令(たとえばbsf
、 386など)は、エスケープバイトmovsx
付きの2バイトのオペコードを使用することがよくあります。SOをぶらぶらしていると、8086について具体的に(特に)0f
尋ねる質問がたくさん表示されます。emu8086
これが、8086で使用できなかった命令について私が知っている主な理由です。履歴の詳細がない2バイトのオペコードを持つ命令を直接覚えておきたい場合は、まったく問題ありません。または、マニュアルで毎回調べてください:P
たとえば0f b6 c0 movzx eax,al
、0F B6はのオペコードであり、C0はeaxを宛先(フィールド= 0)、ソースのレジスタダイレクトモード(上位2ビット= 11)、およびソースレジスタ(上位2ビット= 11)mov r32, r/m8
としてエンコードするModR/Mバイトです。フィールド=0)。/r
al
/m
すべての例()にIntel構文を使用してmnemonic dst, src1 [,src2, ...]
います。これは、IntelおよびAMDのマニュアルに記載されているものと一致するためです。AFAIK、AT&T構文を使用する詳細な命令エンコードマニュアルはありません。また、8086が持っていたものについて話すときでも、32ビットまたは64ビットの例を使用しています。もちろん、8086には16ビットのリアルモードしかありませんでしたが、64ビットモードでも同じオペコードとエンコーディングが使用されています(これは最近私たちが気にかけていることです)。
Intelの命令セットref。マニュアル(SDM vol.2)には、1、2、3バイトのオペコード(付録A.3)のオペコードマップがあるため、オペコードエンコーディングの選択にいくつかのパターンが見られます。または、特定の手順については、そのマニュアルの完全な説明とともにリストされているエンコーディングを参照してください。( https://github.com/HJLebbink/asm-dude/wikiやhttp://felixcloutier.com/x86/のように、命令ごとに1ページの優れたオンライン抜粋も参照してください。HJLebbinkのページでは、各命令にタグが付けられています。が導入されたためadd
、は8086、新しい形式のシフトの場合は386、およびの場合は386が表示されますmovzx
。
shl
またはのような一部の1オペランド命令は、ModR/Mバイトのフィールドを追加のオペコードビットとしてnot
使用することに注意してください。/r
また、イミディエートのあるほとんどの命令は、/r
フィールドをオペコードビットとして使用するため、依然として破壊的です。 imul r32, r/m32, imm32
(386)はこのルールの例外であり、両方のオペランドに即時で完全なModR/Mバイトを使用します。(ModR / Mはレジスタまたはメモリオペランドにのみ信号を送ることができることに注意してください。のエンコーディングadd r/m32, imm8
は、オペコードを使用してイミディエートがあることを示します。ただし、メインのオペコードバイトは複数の命令で共有されるため、/r
フィールドはオペコードの一部として使用されます。そのため、私たちは持っていませんadd r/m32, r32, imm8
。しかし、ADD / SUBの場合lea ecx, [rax + 1]
、コピーアンドアドとして使用できます。)
オペランドエンコーディング:
イミディエートオペランドを持つほとんどの命令は、レジスタ/メモリソースバージョンと同じ長さに、イミディエートをエンコードするためのバイトを加えたものです。イミディエイトはimm8またはimm32のいずれかであるため、-128..127からの値はよりコンパクトです。(16ビットモードでは、imm8またはimm16のいずれかです)。
ModR / Mバイトは、レジスタダイレクト、または変位のない最も単純な1レジスタアドレッシングモードに必要なすべてです。(を除く[esp]
)。add eax, ecx
と同じように、2バイトの長さですadd eax, [ecx]
。インデックス付きアドレッシングモード(およびesp
/rsp
をベースレジスタとするモード)には、SIB(スケール/インデックス/ベース)バイトが必要です。
アドレッシングモードでの一定の変位には、ModR / M +オプションのSIBに加えて、追加の1バイトまたは4バイト(符号拡張されたdisp8またはdisp32)が必要です。
disp8を使用するAVX512EVEXは、disp8をベクトル幅でスケーリングするため、わずかvaddps zmm31, zmm30, [rsi + 256]
7バイト(4バイトEVX + opcode = 0x58 + modrm + disp8)ですが、vaddps zmm31, zmm30, [rsi + 16]
11バイトです。+16
64の倍数。ただし、xmm
レジスタを使用した同じ命令で。を使用できますdisp8
。
詳細については、Intelのマニュアルを参照してください。
最も一般的な指示の特別な短縮形
コードサイズを節約するために、8086(およびそれ以降のx86)は、いくつかの非常に一般的な命令に対してModR/Mバイトのない特別なエンコーディングを提供します。命令がこれらのいずれでもない場合は、ModR/Mバイトを使用します
- add / adc / sub / cmp / test / and / or / xor/etc。レジスタと同じサイズのイミディエートを持つAL/AX/EAX。例:
and eax, imm32
(5バイト)またはand al,imm8
(2バイト)。and eax, imm8
ただし、 ;の特別なエンコーディングはありません。それでも3バイトのand r/m32, imm8
エンコーディングを使用する必要があります。8ビットデータを操作する場合、特に部分レジスタのストールやパフォーマンスの問題を引き起こす誤った依存関係をal
回避したり心配したりしない場合は、コードサイズに非常に適しています。
カウントが1のshift/rotate:8086にはimm8が回転せず、暗黙の1によって、または暗黙の1によってのみ回転するため、暗黙の場合のcl
ようなオペコードがあります。shl r/m32,1
1
imm8
エンコーディングを使用すると、パフォーマンスに影響があります。実行するまでimm8がゼロかどうかをチェックしないため、P6ファミリでストールが発生する可能性があります。ただし、 Skylakeを含むSandybridgeファミリでは、rol r32,1
短縮形は2 uopsであるのに対し、rol r32, imm8
(imm8が1の場合でも)1です。短い形式は、rcl r32,1
imm8よりもはるかに高速です。(Skylakeでは3 uops対8)。
また、レジスタが命令バイトの下位3ビットでエンコードされ、これらの命令のレジスタオペランド形式を1バイト短くするために、8バイトのオペコードコーディングスペースを効果的に使用するものもあります。
mov r8, imm8
:一般的なmov r/m8, imm8
エンコーディングでは3バイトではなく2バイト。
mov r32, imm32
:の6バイトではなく5バイトmov r/m32, imm32
。おもしろい事実:x86-64では、短い形式のオペコードのREX.W = 1バージョンが、64ビットのイミディエートを使用できる唯一の命令です。10バイトmov r64, imm64
。REX.W = 1バージョンのr/m32
オペコードは引き続き32ビットのイミディエート(通常のように符号拡張)を使用するため、mov rax, -1
その方法でエンコードするのが最適で、5バイトに対して7バイトを使用しますmov eax,-1
。(または、コードサイズを最適化する場合は、CPUレジスタのすべてのビットを効率的に1に設定するも参照してください。)
push
/pop
レジスタ、1バイト対pop r/m32
エンコーディングの2バイト。
push
/pop
セグメントレジスタ(FS / GS以外)。これらのar/m16エンコーディングはありませんが。
inc r32
/ dec r32
(16/32ビットモードのみ:0x4Xバイトはx86-64のREXプレフィックスであるためinc eax
、2バイトinc r/m32
エンコーディングを使用する必要があります)。
xchg eax, reg
:これはどこ0x90 nop
から来たのか:の短い形式xchg eax,eax
(または16ビットモードの場合xchg ax,ax
)。x86-64では、90nop
もそうではありませんxchg eax,eax
。これは、EAXをRAXにゼロ拡張するためです。代わりに、独自の命令セットの手動エントリがあります。
xchg reg,reg
コンパイラによって使用されることはなく、通常は3mov
命令より高速ではないため、将来の拡張に役立つ7つのオペコードバイトがあれば便利です。(またはnop
、別のオペコードに移動された場合は8 ...)。アキュムレータが「より特別」だった8086では、より便利でした。たとえばcbw
、ALをAXに符号拡張することmovsx
が、存在しなかったための唯一の(良い)方法でした。そして、1-オペランドmul
/のみimul
が利用可能でした。
xchg eax, r32
それでもコードゴルフには最適です。たとえば、8バイトのx8632ビットマシンコードのGCDです。さまざまなコードサイズのトリックについては、他のコードゴルフの回答も参照してください(主にパフォーマンスを犠牲にして、それがコードゴルフのポイントです)。
r/m32
これは、エンコーディングも含む命令の1バイトの特殊なケースをすべてカバーしていると思います。
この答えは網羅的であることを意味するものではありません。最近の指示についてはあまり話していませんが、まれな指示の場合は特別な場合がたくさんあります。REXプレフィックスまたはオペランドサイズプレフィックスが必要な場合のルールは非常に簡単です。より一般的なルールは次のとおりです。
- SSE1 / SSE3
ABCps
命令には2バイトのオペコード(0F xx)があります
- SSE2整数/倍精度命令には、通常3バイトのオペコード(66 0F xxまたは同様のもの)があります。
- SSSE3 / SSE4.x命令には、4バイトのオペコード(3つの必須プレフィックス)があります
SSEバージョンがSSE3以前であり、2番目のソースレジスタが「ハイ」レジスタ(xmm / ymm8-15)でない場合、 VEXコード化命令は2バイトのVEXプレフィックスを使用できます。同じ命令のXMMバージョンとYMMバージョンは、常に同じサイズです。(ただし、上位の半分をゼロにする必要がない場合は、明示的なymmではなく暗黙的なゼロ拡張を使用したxmmを使用してください。)
vpxor ymm8,ymm8,ymm5 ; 2-byte VEX
vpxor ymm7,ymm7,ymm8 ; 3-byte VEX
vpxor ymm7,ymm8,ymm7 ; 2-byte VEX
したがって、3バイトのVEXを必要とせずに、宛先または最初のソースとして「高」レジスタを使用できますが、2番目のソース(全体で3番目のオペランド)としては使用できません。可換演算の場合、low8を2番目のソースとして配置することでサイズを節約できます。
vblendvps
のような4オペランドの命令の場合、4番目のオペランドは。にエンコードされることに注意してくださいimm8
。したがって、必要なVEXプレフィックスのサイズに影響を与えるのは、最後のオペランドではなく、依然として3番目のオペランド(2番目のソース)です。ただし、SSE4.1であるため、プレフィックスフィールドのエンコーディングblendvps
を表すには、常に3バイトのVEXプレフィックスが必要です。66.0F3A