
こんにちは、私はまだマルコであり、Badooのシステムプログラマでもあります。 先週 、共有ライブラリのPICに関する翻訳を公開しましたが、2番目の部分があります。x64の共有ライブラリに関するものなので、ケースを未完成のままにしないことにしました。
前の記事では、x86アーキテクチャ用にコンパイルされた例で、アドレス独立アドレス(PIC)アドレス指定がどのように機能するかについて説明しました。 別の記事でx64のPICについて話すことを約束しました[1] 。 彼女がいる。 PICが理論上どのように機能するかを既に理解していることが理解されているため、この記事の詳細ははるかに少なくなります。 本質的に、考え方は両方のアーキテクチャで同じですが、一部の詳細は機能によって異なります。
RIPアドレッシング
x86関数呼び出し(callステートメントを使用)では、オフセットはIPに対して使用されますが、データ呼び出し(movステートメントを使用)は絶対アドレスのみをサポートします。 前の記事から、PICは本質的にすべてのオフセットが相対的である必要があるため、これによりPICの効率がやや低下することがわかります。 絶対アドレスとアドレス独立性は連動しません。
x64は、メモリにアクセスするすべての64ビットmov命令のデフォルトのアドレス指定であるRIPに関連する新しいタイプのアドレス指定でこの問題を解決します(たとえば、leaなどの他の命令に使用されます)。 以下は、Intel Architecture Manual vol 2a(Intelの主要なアーキテクチャドキュメントの1つ)からの引用です。
RIP(相対命令ポインターまたは現在の命令へのポインターに対する相対)は、64ビットモードで実装される新しいタイプのアドレス指定です。 最終アドレスは、次の命令への64ビットポインターにオフセットを追加することで形成されます。
RIP相対モードで使用されるオフセットは、負方向と正方向の両方で使用できるため、32ビットのサイズです。 このアドレッシングモードでサポートされているRIPに対する最大オフセットは、±2GBです。
データアクセスを備えたx64 PIC。 例
より簡単な比較のために、前回の記事の例と同じCの例を使用します。
int myglob = 42; int ml_func(int a, int b) { return myglob + a + b; }
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
       ml_func
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    の逆アセンブルされたビューを見てみましょう: 
 00000000000005ec <ml_func>: 5ec: 55 push rbp 5ed: 48 89 e5 mov rbp,rsp 5f0: 89 7d fc mov DWORD PTR [rbp-0x4],edi 5f3: 89 75 f8 mov DWORD PTR [rbp-0x8],esi 5f6: 48 8b 05 db 09 20 00 mov rax,QWORD PTR [rip+0x2009db] 5fd: 8b 00 mov eax,DWORD PTR [rax] 5ff: 03 45 fc add eax,DWORD PTR [rbp-0x4] 602: 03 45 f8 add eax,DWORD PTR [rbp-0x8] 605: c9 leave 606: c3 ret
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
       ここで最も興味深い命令は0x5f6
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    です。myglobのアドレスをraxに配置し、GOTの要素を参照します。 ご覧のとおり、RIP相対アドレス指定を使用しています。 次の命令のアドレスに関連するため、実際には0x5fd + 0x2009db = 0x200fd8
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    ます。 したがって、myglobアドレスを含むGOT要素は0x200fd8にあります。 計算が現実からどれだけ離れているかを確認しましょう。 
 $ readelf -S libmlpic_dataonly.so There are 35 section headers, starting at offset 0x13a8: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [...] [20] .got PROGBITS 0000000000200fc8 00000fc8 0000000000000020 0000000000000008 WA 0 0 8 [...]
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
        GOTは0x200fc8
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    から始まるため、myglobは3番目の要素にあります。 また、myglobのバイナリに再配置が追加されていることがわかります。 
 $ readelf -r libmlpic_dataonly.so Relocation section '.rela.dyn' at offset 0x450 contains 5 entries: Offset Info Type Sym. Value Sym. Name + Addend [...] 000000200fd8 000500000006 R_X86_64_GLOB_DAT 0000000000201010 myglob + 0 [...]
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
       アドレス0x200fd8
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    再配置エントリがあり、文字の最終アドレスがわかっているときにアドレスmyglobを追加する0x200fd8
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    リンカーに0x200fd8
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    ます。 
 これで、コードでmyglobアドレスを取得する方法が明確になります。 次の0x5fd
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    命令は、最終アドレスを取得するためにポインターを逆参照し、それをeax [2]に入れます。 
関数アクセスを備えたx64 PIC。 例
x64のPICで関数呼び出しがどのように機能するかを見てみましょう。 そして、前の記事と同じ例を使用します。
 int myglob = 42; int ml_util_func(int a) { return a + 1; } int ml_func(int a, int b) { int c = b + ml_util_func(a); myglob += c; return b + myglob; }
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
        ml_func
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    を分解ml_func
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     、次の結果が得られます。 
 000000000000064b <ml_func>: 64b: 55 push rbp 64c: 48 89 e5 mov rbp,rsp 64f: 48 83 ec 20 sub rsp,0x20 653: 89 7d ec mov DWORD PTR [rbp-0x14],edi 656: 89 75 e8 mov DWORD PTR [rbp-0x18],esi 659: 8b 45 ec mov eax,DWORD PTR [rbp-0x14] 65c: 89 c7 mov edi,eax 65e: e8 fd fe ff ff call 560 <ml_util_func@plt> [... snip more code ...]
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
       前と同様に、呼び出しはml_util_func@plt
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    ようにml_util_func@plt
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    ます。 何があるか見てみましょう: 
 0000000000000560 <ml_util_func@plt>: 560: ff 25 a2 0a 20 00 jmp QWORD PTR [rip+0x200aa2] 566: 68 01 00 00 00 push 0x1 56b: e9 d0 ff ff ff jmp 540 <_init+0x18>
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
       実アドレスml_util_func
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    を含むGOTレコードは0x200aa2 + 0x566 = 0x201008
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    ます。 また、予想どおり、再配置レコードも配置されています。 
 $ readelf -r libmlpic.so Relocation section '.rela.dyn' at offset 0x480 contains 5 entries: [...] Relocation section '.rela.plt' at offset 0x4f8 contains 2 entries: Offset Info Type Sym. Value Sym. Name + Addend [...] 000000201008 000600000007 R_X86_64_JUMP_SLO 000000000000063c ml_util_func + 0
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
      性能
両方の例で、x64のPICはx86の同じコードよりも少ない命令で済むことがわかります。 x86では、GOTアドレスは2つの段階でレジスタにロードされます(契約によるとebx)。最初に特別な呼び出しで命令アドレスを取得し、次にGOTにオフセットを追加します。 GOTへの相対オフセットはリンカに認識されており、RIP相対アドレス指定の命令で単純に使用できるため、これらのステージはx64では必要ありません。
関数を呼び出すとき、x86とは異なり、スプリングボードはRIP相対アドレッシングを介してGOTの要素に直接アクセスするため、スプリングボード用にebxでGOTアドレスを準備する必要もありません。
x64のPICは、PICのないコードと比較して追加の命令を必要としますが、オーバーヘッドは小さくなります。 レジスタ全体を使用してGOTへのポインタを格納するコストも不要になりました。 RIP相対アドレッシングは、追加のレジスタを必要としません[3] 。 その結果、x64でのPICのオーバーヘッドはx86と比較してはるかに小さくなり、これによりPICの人気がさらに高まります。 このアーキテクチャで共有ライブラリを作成する場合、PICがデフォルトの選択肢であるため非常に人気があります。
好奇心のために:x64にPICはありません
GCCは、x64上の共有ライブラリにPICを使用するよう推奨するだけでなく、デフォルトでこれを必要とします。 たとえば、-fpic [4]なしで最初の例をコンパイルし、-sharedを使用して共有ライブラリを構築しようとすると、リンカーからエラーが発生します。
 /usr/bin/ld: ml_nopic_dataonly.o: relocation R_X86_64_PC32 against symbol `myglob' can not be used when making a shared object; recompile with -fPIC /usr/bin/ld: final link failed: Bad value collect2: ld returned 1 exit status
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
       何が起こっているの?  ml_nopic_dataonly.o
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     [5]の逆アセンブルされたビューを見てみましょう。 
 0000000000000000 <ml_func>: 0: 55 push rbp 1: 48 89 e5 mov rbp,rsp 4: 89 7d fc mov DWORD PTR [rbp-0x4],edi 7: 89 75 f8 mov DWORD PTR [rbp-0x8],esi a: 8b 05 00 00 00 00 mov eax,DWORD PTR [rip+0x0] 10: 03 45 fc add eax,DWORD PTR [rbp-0x4] 13: 03 45 f8 add eax,DWORD PTR [rbp-0x8] 16: c9 leave 17: c3 ret
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
      ここで、0xaの手順でmyglobにアクセスする方法に注意してください。 リンカはオペランドのmyglobに実際のアドレスを置くことが期待されます(つまり、GOTなし):
 $ readelf -r ml_nopic_dataonly.o Relocation section '.rela.text' at offset 0xb38 contains 1 entries: Offset Info Type Sym. Value Sym. Name + Addend 00000000000c 000f00000002 R_X86_64_PC32 0000000000000000 myglob - 4 [...]
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
       そして、ここに再配置R_X86_64_PC32
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    がありますが、これについてはリンカが不満を言いました。 このような再配置のあるオブジェクトを共有ライブラリにリンクすることはできません。 なんで?  ripに対して行うオフセットは32ビットに収まらなければならないため、これで十分だとは言えません。 結局のところ、巨大なアドレス空間を備えた本格的な64ビットアーキテクチャがあります。 キャラクターは、最終的には遠く離れた共有ライブラリに配置される可能性があり、それにアクセスするのに十分な32ビットがありません。 したがって、再配置R_X86_64_PC32
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     、x64上の共有ライブラリには適していません。 
 しかし、x64で何らかの方法で非PICコードを作成できますか? できる! コンパイラに、いわゆる「大規模コードモデル」を使用するように指示する必要があります。 これは、 -mcmodel=large
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    フラグを追加することにより行われます。 コードモデルのトピックは確かに興味深いものですが、その説明はこの記事の目的からは程遠いものになります[6] 。 したがって、コードモデルはプログラマとコンパイラの間の合意のようなものであると簡単に言います。プログラマは、プログラムで使用されるオフセットのサイズに関してコンパイラにいくつかの約束をします。 代わりに、コンパイラーはより良いコードを生成できます。 
 コンパイラがリンカが配置するx64で非PICコードを生成するには、「大規模なコードモデル」のみが最も要求の厳しいものとして適していることがわかります。 オフセットが32ビットを超える可能性があるため、単純な再配置がx64で十分でない理由についての私の説明を覚えていますか? 以下は、「大きなコードモデル」は単に何も想定せず、すべてのデータアクセスに最大の64ビットオフセットを使用します。 これにより、再配置は安全であり、x64でPICコードを使用しないと言うことができます。  -fpic
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    を使用せずに-mcmodel=large:
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    を-mcmodel=large:
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    コンパイルした最初の例の逆アセンブルビューを見てみましょう-mcmodel=large:
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     
 0000000000000000 <ml_func>: 0: 55 push rbp 1: 48 89 e5 mov rbp,rsp 4: 89 7d fc mov DWORD PTR [rbp-0x4],edi 7: 89 75 f8 mov DWORD PTR [rbp-0x8],esi a: 48 b8 00 00 00 00 00 mov rax,0x0 11: 00 00 00 14: 8b 00 mov eax,DWORD PTR [rax] 16: 03 45 fc add eax,DWORD PTR [rbp-0x4] 19: 03 45 f8 add eax,DWORD PTR [rbp-0x8] 1c: c9 leave 1d: c3 ret
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
        0xaの命令は、myglobのアドレスをeaxに入れます。 彼女の議論はまだゼロであり、これは再配置がここで期待されることを示唆していることに注意してください。 さらに、彼女は完全な64ビット引数を持っています。  RIP相対ではなく絶対的です[7] 。 さて、 myglob
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    値をeaxに入れるには、ここの2つの指示が必要であることに注意してください。 これが、「大規模なコードモデル」が他の方法よりも効率が低い理由の1つです。 
      
        
        
        
      
     次に、再配置を見てみましょう。 
 $ readelf -r ml_nopic_dataonly.o Relocation section '.rela.text' at offset 0xb40 contains 1 entries: Offset Info Type Sym. Value Sym. Name + Addend 00000000000c 000f00000001 R_X86_64_64 0000000000000000 myglob + 0 [...]
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
       再配置タイプがR_X86_64_64
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    変更されR_X86_64_64
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     。 これは、64ビット値を持つ絶対アドレスを使用した再配置です。 リンカはこのオブジェクトを共有ライブラリにリンクすることに喜んで同意しています。 
いくつかの批判的な考えから、このコンパイラがデフォルトでブート時の再配置に適さないコードを生成する理由を疑問に思うかもしれません。 答えはとても簡単です。 通常、コードはバイナリに直接リンクし、再配置を必要としないことを忘れないでください。 また、デフォルトでは、コンパイラは最も効率的なコードを作成するために「小さなコードモデル」を想定しています。 コードが共有ライブラリにあることがわかっていて、PICを使用したくない場合は、明示的にコンパイラに伝えてください。 私には、gccの動作が非常に適切であるように思われます。
もう1つの質問は、「小さなコードモデル」を使用するときにPICに問題がない理由です。 その理由は、GOTは常に、アクセスするコードと同じ共有ライブラリ内にあるためです。 また、共有ライブラリが32ビットアドレス空間に収まるほど大きくない場合、アドレス指定の問題はありません。 したがって、大規模な共有ライブラリはありそうにありませんが、ある場合、AMD64のABIには「PICを使用した大きなコードモデル」があります。
おわりに
この記事は、PICがx64アーキテクチャでどのように機能するかを説明することで、 前の記事を補完します。 このアーキテクチャは、PICの高速化に役立つ新しいアドレス指定モデルを使用するため、共有ライブラリ(x86と比較して)に適しています。 x64は現在、サーバー、デスクトップ、ラップトップで最も一般的なアーキテクチャであるため、これは非常に重要です。
[1]
いつものように、x86は、x86-64、AMD64、またはIntel 64として知られるアーキテクチャの便利な短縮名として使用します。
[2]
  myglob
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     int
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    型をmyglob
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    いるため、 rax
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    ではなくeax
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    で、これはx64でも32ビットです。 
[3]
ところで、レジスタを使用することは、x86の2倍のレジスタを持っているため、x64での問題ははるかに少なくなります。
[4]
 これは、 gcc
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    引数として-fno-pic
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    を渡すことで、PICが-fno-pic
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    あることを明示的に示した場合にも発生します。 
[5]
この記事および前回の記事で検討した他の逆アセンブラの結論とは異なり、これはオブジェクトファイルであり、ライブラリまたはバイナリではないことに注意してください。 そのため、リンカの再配置が含まれます。
[6]
 詳細については、 AMD64 ABI
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    およびman gcc
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     。 
[7]
 一部のアセンブラは、このmovabs
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    命令を呼び出して、相対アドレスを受け入れる他のmov命令と区別します。 ただし、Intelアーキテクチャの命令では、単にmov
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    と呼びます。 そのオペコード形式はREX.W + B8 + rd
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    です。