web-dev-qa-db-ja.com

32ビット絶対アドレスはx86-64 Linuxでは許可されなくなりましたか?

64ビットLinuxはデフォルトでスモールメモリモデルを使用します。これにより、すべてのコードと静的データが2GBのアドレス制限を下回ります。これにより、32ビットの絶対アドレスを使用できるようになります。古いバージョンのgccは、相対アドレス計算のための追加の命令を保存するために、静的配列に32ビットの絶対アドレスを使用します。ただし、これは機能しなくなりました。アセンブリで32ビットの絶対アドレスを作成しようとすると、「共有オブジェクトの作成時に「.data」に対する再配置R_X86_64_32Sは使用できません。-fPICで再コンパイルしてください」というリンカーエラーが発生します。もちろん、このエラーメッセージは誤解を招くものです。私が共有オブジェクトを作成しておらず、-fPICが役に立たないためです。これまでにわかったことは、gccバージョン4.8.5では静的配列に32ビットの絶対アドレスを使用しますが、gccバージョン6.3.0では使用しません。バージョン5もおそらくそうではありません。 binutils 2.24のリンカーは32ビットの絶対アドレスを許可しますが、verson 2.28は許可しません。

この変更の結果、古いライブラリを再コンパイルする必要があり、レガシーアセンブリコードが破損します。

今私は尋ねたいと思います:この変更はいつ行われたのですか?どこかに文書化されていますか?また、32ビットの絶対アドレスを受け入れるリンカーオプションはありますか?

26
A Fog

あなたのディストリビューションは_--enable-default-pie_でgccを設定したので、デフォルトで位置に依存しない実行可能ファイルを作成しています(実行可能ファイルとライブラリのASLRを許可しています)。最近では、ほとんどのディストリビューションがそれを行っています。

あなたは実際にが共有オブジェクトを作成しています:PIE実行可能ファイルは、エントリポイントを持つ共有オブジェクトを使用する一種のハックです。ダイナミックリンカーはすでにこれをサポートしており、ASLRはセキュリティに優れているため、実行可能ファイルにASLRを実装する最も簡単な方法でした。

ELF共有オブジェクトでは、32ビットの絶対再配置は許可されていません。これにより、低2GiB(符号拡張された32ビットアドレスの場合)の外部にロードされなくなります。 64ビットの絶対アドレスを使用できますが、一般的には、命令の一部としてではなく、ジャンプテーブルまたはその他の静的データに対してのみ必要です。1

エラーメッセージの_recompile with -fPIC_の部分は、手書きのasmでは偽です。 _gcc -c_でコンパイルしてから_gcc -shared -o foo.so *.o_でリンクしようとする人のために書かれています。gccで_-fPIE_はではありませんデフォルト。手書きのasmをリンクすると、多くの人がこのエラーに遭遇するため、エラーメッセージはおそらく変わるはずです。


RIP相対アドレス指定の使用方法:基本

欠点がない単純なケースでは、常にRIP相対アドレッシングを使用してください。以下の脚注1および 構文のこの回答 も参照してください。有害ではなくコードサイズに実際に役立つ場合のみ、32ビット絶対アドレス指定の使用を検討してください。例えばNASM _default rel_ファイルの先頭。

AT&T foo(%rip)またはGASでは_.intel_syntax noprefix_ _[rip + foo]_を使用します。


32ビットの絶対アドレス指定を機能させるには、PIEモードを無効にします

_gcc -fno-pie -no-pie_を使用して、これをオーバーライドして以前の動作に戻します。 _-no-pie_はリンカーオプション、 _-fno-pie_はコード生成オプション 。 _-fno-pie_のみの場合、gccは_mov eax, offset .LC0_のように、まだ有効な_-pie_とリンクしないコードを作成します。

clangデフォルトでPIEを有効にすることもできます:_clang -fno-pie -nopie_を使用します。A 2017年7月のパッチ 作成_-no-pie_ an _-nopie_のエイリアス。gccとの互換性がありますが、clang4.0.1にはありません。)


64ビット(マイナー)または32ビットコード(メジャー)のPIEのパフォーマンスコスト

_-no-pie_のみ(ただし、_-fpie_)コンパイラ生成コード(CまたはC++ソースから)は必要以上に少し遅く、大きくなりますにリンクされますASLRの恩恵を受けない位置依存の実行可能ファイル。 "パフォーマンスが低下すると、PIEが多すぎます" SPEC CPU2006でx86-64の平均速度が3%低下すると報告(IDKの紙のコピーがないので上にあったハードウェア:/)。しかし、32ビットコードでは、平均の速度低下は10%、最悪の場合は25%です(SPEC CPU2006)。

Agnerが質問で説明しているように、PIE実行可能ファイルのペナルティは、静的配列のインデックス付けなどの場合にほとんどです。静的アドレスを32ビットイミディエイトとして、または_[disp32 + index*4]_アドレッシングモードの一部として使用すると、命令とレジスタが保存されます。アドレスをレジスターに取得するためのRIP相対LEA。また、静的アドレスをレジスタに取得するための7バイト_mov r32, imm32_ではなく5バイト_lea r64, [rel symbol]_は、文字列リテラルまたは他の静的データのアドレスを関数に渡すのに適しています。

_-fPIE_は、GOTを介してグローバルにアクセスする必要がある共有ライブラリの_-fPIC_とは異なり、グローバル変数/関数のシンボル挿入がないと想定しています(これは、staticを使用するもう1つの理由です)グローバルではなくファイルスコープに制限できる変数の場合)。 Linuxの動的ライブラリの申し訳ありません を参照してください。

したがって、_-fPIE_は、64ビットコードの_-fPIC_よりも悪くはありませんが、それでもRIP相対アドレッシングが利用できないため、32ビットの場合は不良です。 Godboltコンパイラエクスプローラのいくつかの例 を参照してください。平均すると、_-fPIE_は64ビットコードでのパフォーマンス/コードサイズの低下が非常に小さくなります。特定のループの最悪のケースは、ほんの数%です。ただし、32ビットのPIEの方がはるかに悪い場合があります。

これらの_-f_ code-genオプションは、リンクするだけの場合、または_.S_の手書きのasmをアセンブルする場合に違いはありません。 _gcc -fno-pie -no-pie -O3 main.c nasm_output.o_は、両方のオプションが必要な場合です。


GCC設定の確認

GCCがこのように構成されている場合、_gcc -v |& grep -o -e '[^ ]*pie'_は_--enable-default-pie_を出力します。この構成オプションのサポートは、gccに early 2015 で追加されました。 Ubuntuは16.10でそれを有効にし、Debianはgcc _6.2.0-7_でほぼ同時に有効にしました(カーネルビルドエラーの原因: https://lkml.org/lkml/2016/10/21/904 )。

関連: PIEとして圧縮x86カーネルをビルド も、変更されたデフォルトの影響を受けました。

なぜLinuxは実行可能コードセグメントのアドレスをランダム化しないのですか? なぜそれが以前にデフォルトでなかったのか、または有効になる前に古いUbuntuのいくつかのパッケージでのみ有効にされたのかについての古い質問です全面的に。


ld自体はデフォルトを変更しなかったであることに注意してください。それはまだ正常に動作します(少なくともbinutils 2.28を備えたArch Linuxでは)。変更点は、明示的に_-pie_または_-static_を使用しない限り、gccはデフォルトで_-no-pie_をリンカーオプションとして渡すことです。

NASMソースファイルでは、_a32 mov eax, [abs buf]_を使用して絶対アドレスを取得しました。 (私は小さな絶対アドレスをエンコードする6バイトの方法(address-size + mov eax、moffs:_67 a1 40 f1 60 00_)がIntel CPUでLCPストールを持っているかどうかをテストしていました。 そうします 。)

_nasm -felf64 -Worphan-labels -g -Fdwarf testloop.asm &&
ld -o testloop testloop.o              # works: static executable

gcc -v -nostdlib testloop.o            # doesn't work
...
..../collect2  ... -pie ...
/usr/bin/ld: testloop.o: relocation R_X86_64_32 against `.bss' can not be used when making a shared object; recompile with -fPIC
/usr/bin/ld: final link failed: Nonrepresentable section on output
collect2: error: ld returned 1 exit status

gcc -v -no-pie -nostdlib testloop.o    # works
gcc -v -static -nostdlib testloop.o    # also works: -static implies -no-pie
_

GCCは_-static-pie_を使用して「静的PIE」を作成することもできます。動的ライブラリやELFインタープリターによってASLRされません。 _-static -pie_と同じではありません。これらは互いに競合します(静的な非PIEを取得します) 変更される可能性があります

関連: libcあり/なしの静的/動的実行可能ファイルの構築、__start_またはmain の定義。


既存の実行可能ファイルがPIEかどうかの確認

fileおよびreadelfは、PIEはELF実行可能ファイルではなく「共有オブジェクト」であると述べています。 ELFタイプEXECをPIEにすることはできません。

_$ gcc -fno-pie  -no-pie -O3 hello.c
$ file a.out
a.out: ELF 64-bit LSB executable, ...

$ gcc -O3 hello.c
$ file a.out
a.out: ELF 64-bit LSB shared object, ...

 ## Or with a more recent version of file:
a.out: ELF 64-bit LSB pie executable, ...
_

gcc _-static-pie_は、現在のfileとともに_LSB pie executable_、_dynamically linked_として表示されます。 ELFタイプのDYNですが、readelfは_.interp_を表示せず、lddは静的にリンクされていることを示します。 GDB startiおよび_/proc/maps_は、ELFインタープリターではなく、その__start_の先頭から実行が開始されることを確認します。

これも質問されています: Linuxバイナリが位置独立コードとしてコンパイルされたかどうかをテストする方法?


準関連(ただし、実際にはそうではありません):別の最近のgcc機能は_gcc -fno-plt_です。最後に、共有ライブラリへの呼び出しは、PLTトランポリンなしで、単に_call [rip + symbol@GOTPCREL]_(AT&T call *puts@GOTPCREL(%rip))にすることができます。

これのNASMバージョンは_call [rel puts wrt ..got]_です
_call puts wrt ..plt_の代替として。 64ビットLinuxでアセンブリ(yasm)コードからC標準ライブラリ関数を呼び出せない を参照してください。これはPIEまたは非PIEで機能し、リンカーがPLTスタブを作成するのを回避します。

一部のディストリビューションはそれを有効にし始めました。また、書き込み可能な+実行可能なメモリページを必要としないため、コードインジェクションに対するセキュリティに優れています。これは、多くの共有ライブラリ呼び出しを行うプログラム(たとえば、 x86-64 _clang -O2 -g_のコンパイルtramp3dは、どのハードウェアでも41.6秒から36.8秒になります パッチ作成者がテストしたもの 。 (clangは、共有ライブラリー呼び出しの最悪のシナリオであり、小さなLLVMライブラリー関数を何度も呼び出します。)

遅延動的リンクの代わりに事前バインディングが必要なので、すぐに終了する大きなプログラムの場合は遅くなります。 (例:_clang --version_または_hello.c_のコンパイル)。この速度低下は、どうやらプレリンクで軽減できる可能性があります。

ただし、これによって共有ライブラリのPICコードの外部変数のGOTオーバーヘッドが削除されるわけではありません。 (上記のgodboltリンクを参照してください)。


脚注1

Linux ELF共有オブジェクトでは、実際には64ビットの絶対アドレスが許可されており、異なるアドレス(ASLRおよび共有ライブラリ)でのロードを可能にする text relocations が指定されています。これにより、ランタイム初期化子なしで_section .rodata_または_static const int *foo = &bar;_にジャンプテーブルを作成できます。

したがって、_mov rdi, qword msg_は機能します(10バイトのNASM/YASM構文 _mov r64, imm64_ 、別名AT&T構文movabs、64ビットの即時を使用できる唯一の命令)。しかし、それは_lea rdi, [rel msg]_よりも大きく、通常は低速です。これは、_-pie_を無効にしない場合に使用する必要があります。 Agner Fogのmicroarch pdf によると、64ビットイミディエイトはSandybridgeファミリCPUのuopキャッシュからフェッチするのに時間がかかります。 (はい、この質問をした人と同じ人です:)

すべての_default rel_アドレッシングモードで指定する代わりに、NASMの_[rel symbol]_を使用できます。 32ビットの絶対アドレス指定を回避するための詳細については、 Mach-O 64ビット形式は32ビットの絶対アドレスをサポートしていません。NASMは配列にアクセスしています も参照してください。 OS Xは32ビットのアドレスをまったく使用できないため、RIP相対アドレス指定も最善の方法です。

位置依存コード(_-no-pie_)では、レジスタにアドレスが必要な場合は_mov edi, msg_を使用する必要があります。 5バイトの_mov r32, imm32_は、RIP相対LEAよりもさらに小さく、より多くの実行ポートで実行できます。

39
Peter Cordes