機能検出にパッチを適用するための簡単なランタイム メソッドはないようです。この検出は、動的リンカー (ld.so) のかなり早い段階で行われます。
現時点では、リンカーにバイナリ パッチを適用するのが最も簡単な方法のようです。 @osgx は、ジャンプが上書きされる 1 つの方法について説明しました。別のアプローチは、cpuid の結果を偽造することです。通常は cpuid(eax=0)
eax
でサポートされている最高の関数を返します 製造元 ID はレジスタ ebx、ecx、および edx で返されます。このスニペットは glibc 2.25 sysdeps/x86/cpu-features.c
にあります。 :
__cpuid (0, cpu_features->max_cpuid, ebx, ecx, edx);
/* This spells out "GenuineIntel". */
if (ebx == 0x756e6547 && ecx == 0x6c65746e && edx == 0x49656e69)
{
/* feature detection for various Intel CPUs */
}
/* another case for AMD */
else
{
kind = arch_kind_other;
get_common_indeces (cpu_features, NULL, NULL, NULL, NULL);
}
__cpuid
行は /lib/ld-linux-x86-64.so.2
のこれらの命令に変換されます (/lib/ld-2.25.so
):
172a8: 31 c0 xor eax,eax
172aa: c7 44 24 38 00 00 00 mov DWORD PTR [rsp+0x38],0x0
172b1: 00
172b2: c7 44 24 3c 00 00 00 mov DWORD PTR [rsp+0x3c],0x0
172b9: 00
172ba: 0f a2 cpuid
したがって、ブランチにパッチを当てるのではなく、 cpuid
を変更することもできます nop
に 最後の else
を呼び出す命令 ブランチ (レジスタには「GenuineIntel」が含まれないため)。最初から eax=0
、 cpu_features->max_cpuid
も 0 になり、if (cpu_features->max_cpuid >= 7)
もバイパスされます。
バイナリパッチ cpuid(eax=0)
nop
で これは、このユーティリティで実行できます (x86 と x86-64 の両方で機能します):
#!/usr/bin/env python
import re
import sys
infile, outfile = sys.argv[1:]
d = open(infile, 'rb').read()
# Match CPUID(eax=0), "xor eax,eax" followed closely by "cpuid"
o = re.sub(b'(\x31\xc0.{0,32}?)\x0f\xa2', b'\\1\x66\x90', d)
assert d != o
open(outfile, 'wb').write(o)
同等の Perl バリアント -0777
改行でレコードを区切るのではなく、ファイルが一度に読み取られるようにします:
perl -0777 -pe 's/\x31\xc0.{0,32}?\K\x0f\xa2/\x66\x90/' < /lib/ld-linux-x86-64.so.2 > ld-linux-x86-64-patched.so.2
# Verify result, should display "Success"
cmp -s /lib/ld-linux-x86-64.so.2 ld-linux-x86-64-patched.so.2 && echo 'Not patched' || echo Success
それは簡単な部分でした。ここで、システム全体の動的リンカーを置き換えたくはありませんでしたが、このリンカーを使用して特定のプログラムを 1 つだけ実行しました。確かに、それは ./ld-linux-x86-64-patched.so.2 ./a
で実行できます 、しかし単純な gdb 呼び出しはブレークポイントの設定に失敗しました:
$ gdb -q -ex "set exec-wrapper ./ld-linux-x86-64-patched.so.2" -ex start ./a
Reading symbols from ./a...done.
Temporary breakpoint 1 at 0x400502: file a.c, line 5.
Starting program: /tmp/a
During startup program exited normally.
(gdb) quit
$ gdb -q -ex start --args ./ld-linux-x86-64-patched.so.2 ./a
Reading symbols from ./ld-linux-x86-64-patched.so.2...(no debugging symbols found)...done.
Function "main" not defined.
Temporary breakpoint 1 (main) pending.
Starting program: /tmp/ld-linux-x86-64-patched.so.2 ./a
[Inferior 1 (process 27418) exited normally]
(gdb) quit
手動の回避策は、カスタム elf インタープリターを使用してプログラムをデバッグする方法で説明されています。動作しますが、残念ながら add-symbol-file
を使用した手動アクションです .ただし、GDB キャッチポイントを使用して少し自動化できるはずです。
バイナリ リンクを行わない別の方法は LD_PRELOAD
です。 memcpy
のカスタム ルーチンを定義するライブラリを作成する 、 memove
など。これは、glibc ルーチンよりも優先されます。関数の完全なリストは sysdeps/x86_64/multiarch/ifunc-impl-list.c
にあります .現在の HEAD には、glibc 2.25 リリースと比較して、合計でより多くのシンボルがあります (grep -Po 'IFUNC_IMPL \(i, name, \K[^,]+' sysdeps/x86_64/multiarch/ifunc-impl-list.c
):
memchr,memcmp,__memmove_chk,memmove,memrchr,__memset_chk,memset,rawmemchr,strlen,strnlen,stpncpy,stpcpy,strcasecmp,strcasecmp_l,strcat,strchr,strchrnul,strrchr,strcmp,strcpy,strcspn,strncasecmp,strncasecmp_l,strncat,strncpy, strpbrk,strspn,strstr,wcschr,wcsrchr,wcscpy,wcslen,wcsnlen,wmemchr,wmemcmp,wmemset,__memcpy_chk,memcpy,__memcpy_chk,mempcpy,strncmp,__wmemset_chk,
glibc の最近のバージョンで実装された、これに対する優れた回避策があるようです。最適化された文字列関数の選択をガイドする「調整可能」機能です。この機能の概要はこちらで、glibc 内の関連コードは ifunc-impl-list.c にあります。
これが私がそれを理解した方法です。まず、gdb から苦情が寄せられているアドレスを取得しました:
Process record does not support instruction 0xc5 at address 0x7ffff75c65d4.
次に、共有ライブラリの表で調べました:
(gdb) info shared
From To Syms Read Shared Object Library
0x00007ffff7fd3090 0x00007ffff7ff3130 Yes /lib64/ld-linux-x86-64.so.2
0x00007ffff76366b0 0x00007ffff766b52e Yes /usr/lib/x86_64-linux-gnu/libubsan.so.1
0x00007ffff746a320 0x00007ffff75d9cab Yes /lib/x86_64-linux-gnu/libc.so.6
...
このアドレスは glibc 内にあることがわかります。しかし、具体的にはどのような機能でしょうか?
(gdb) disassemble 0x7ffff75c65d4
Dump of assembler code for function __strcmp_avx2:
0x00007ffff75c65d0 <+0>: mov %edi,%eax
0x00007ffff75c65d2 <+2>: xor %edx,%edx
=> 0x00007ffff75c65d4 <+4>: vpxor %ymm7,%ymm7,%ymm7
ifunc-impl-list.c を調べて、avx2 バージョンの選択を制御するコードを見つけることができます:
IFUNC_IMPL (i, name, strcmp,
IFUNC_IMPL_ADD (array, i, strcmp,
HAS_ARCH_FEATURE (AVX2_Usable),
__strcmp_avx2)
IFUNC_IMPL_ADD (array, i, strcmp, HAS_CPU_FEATURE (SSE4_2),
__strcmp_sse42)
IFUNC_IMPL_ADD (array, i, strcmp, HAS_CPU_FEATURE (SSSE3),
__strcmp_ssse3)
IFUNC_IMPL_ADD (array, i, strcmp, 1, __strcmp_sse2_unaligned)
IFUNC_IMPL_ADD (array, i, strcmp, 1, __strcmp_sse2))
AVX2_Usable
のようです 無効にする機能です。それに応じて gdb を再実行しましょう:
GLIBC_TUNABLES=glibc.cpu.hwcaps=-AVX2_Usable gdb...
この反復では、__memmove_avx_unaligned_erms
について不平を言いました 、 AVX_Usable
によって有効になったようです - しかし、 AVX_Fast_Unaligned_Load
によって有効化された ifunc-memmove.h で別のパスを見つけました .振り出しに戻る:
GLIBC_TUNABLES=glibc.cpu.hwcaps=-AVX2_Usable,-AVX_Fast_Unaligned_Load gdb ...
この最終ラウンドで rdtscp
を発見しました 命令を ASAN 共有ライブラリに追加したため、アドレス サニタイザーを使用せずに再コンパイルしたところ、ようやく機能しました。
要約すると、いくつかの作業を行うことで、コマンド ラインからこれらの命令を無効にして、重大なハックなしで gdb の記録機能を使用することが可能です。
私も最近この問題に遭遇し、動的CPUIDフォルトを使用してCPUID命令の実行を中断し、その結果をオーバーライドして解決しました。これにより、glibcまたは動的リンカに触れることを回避できます。これには、CPUID フォールト (Ivy Bridge+) に対するプロセッサ サポートと、ARCH_GET_CPUID
を介してユーザー空間に公開するための Linux カーネル サポート (4.12+) が必要です。 と ARCH_SET_CPUID
arch_prctl()
のサブ関数 .この機能を有効にすると、SIGSEGV
シグナルは CPUID の実行ごとに配信されるため、シグナル ハンドラは命令の実行をエミュレートし、結果をオーバーライドできます。
ハードウェア機能の検出が glibc 2.26+ から動的リンカに移動されたため、動的リンカも挿入する必要があるため、完全なソリューションは少し複雑です。完全なソリューションを https://github.com/ddcc/libcpuidoverride にオンラインでアップロードしました。