前回から日が開きましたが、引き続きMacでBinary Hacks Rebootedの内容を試していきます。
今回は「7章 数値表現とデータ処理Hack」の内容を試していきます。サンプルコードは https://github.com/oreilly-japan/binary-hacks-rebooted から取得できます。
$ git clone https://github.com/oreilly-japan/binary-hacks-rebooted.git
$ cd binary-hacks-rebooted
私の環境はApple M4 Pro / macOS Sequoia 15.1.1です。以下のアセンブリコードはAArch64向けです。
Hack #68 整数表現の基礎知識
add_int8.c
をコンパイルしてみます。
$ cd ch07_data/68_numeric_basics/
$ clang -S -O3 add_int8.c
$ cat add_int8.s
.section __TEXT,__text,regular,pure_instructions
.build_version macos, 15, 0 sdk_version 15, 1
.globl _add_int8 ; -- Begin function add_int8
.p2align 2
_add_int8: ; @add_int8
.cfi_startproc
; %bb.0:
ldrb w8, [x0]
ldrb w9, [x1]
add w8, w9, w8
strb w8, [x2]
ret
.cfi_endproc
; -- End function
.subsections_via_symbols
本文中にもAArch64向けのアセンブリコードが載っていますが、同様のものが出力されました。違いは、シンボルの先頭にアンダースコアがついていること(LinuxとmacOSの違いです)、使用しているレジスターが違うことです(GCCとLLVMの違いだと思われます)。
add_int8_as_int32
も同様です。
$ clang -S -O3 add_int8_as_int32.c
$ cat add_int8_as_int32.s
...略...
.globl _add_int8_as_int32 ; -- Begin function add_int8_as_int32
.p2align 2
_add_int8_as_int32: ; @add_int8_as_int32
.cfi_startproc
; %bb.0:
ldrsb w8, [x0]
ldrsb w9, [x1]
add w0, w9, w8
ret
.cfi_endproc
; -- End function
...略...
実行結果も同様です。
$ clang -O3 add_int8_as_int32.c && ./a.out
fffffffd
load_uint32_be.c
は _bswap
というx86向けの組み込み関数を使っているので、AArch64では動かせません。x86_64向けにコンパイルしてRosetta 2を使えば動きます。
$ clang -O3 -arch x86_64 load_uint32_be.c && ./a.out
03020100, 00010203
ただし、Armにも「レジスター上でバイトオーダーを並び替える」REV命令があり、C言語から __rev
という組み込み関数として使えます。やってみましょう:
$ cat load_uint32_be_arm.c
#include <stdint.h>
#include <stdio.h>
#include <arm_acle.h>
uint32_t load_uint32(const uint8_t *p) {
return *((const uint32_t *)p);
}
uint32_t load_uint32_be(const uint8_t *p) {
uint32_t raw = *((const uint32_t *)p);
return __rev(raw);
}
int main(void) {
uint8_t bytes[4] = {0, 1, 2, 3}; // ビッグエンディアンで解釈すると
// 0x00010203
printf("%08x, %08x\n", load_uint32(bytes), load_uint32_be(bytes));
}
$ clang load_uint32_be_arm.c
$ ./a.out
03020100, 00010203
命令の詳細はArm Architecture Reference Manualを、C言語の組み込み関数の詳細はACLE https://arm-software.github.io/acle/main/acle.html#miscellaneous-data-processing-intrinsics を見てください。
Hack #69 さまざまな整数表現
最初のサンプルでGMPを使っていますが、GMPはXcodeには付属しません。なので、Homebrew等で入れることになると思います。Homebrewで入れたGMPを使用するには、pkg-config
を使うとHomebrewのパスをハードコードする必要がなくて便利です。
$ clang -O3 $(pkg-config --cflags gmp) gmp-pow.c $(pkg-config --libs gmp) && ./a.out
7888609052210118054117285652827862296732064351090230047702789306640626
参考までに、pkg-config
の出力結果を載せておきます。
$ pkg-config --cflags gmp
-I/opt/homebrew/Cellar/gmp/6.3.0/include
$ pkg-config --libs gmp
-L/opt/homebrew/Cellar/gmp/6.3.0/lib -lgmp
固定小数点数の例は普通に動きます。
$ clang -O3 fixed-point-mla.c && ./a.out
0x00000240
アセンブリソースを眺めると、ssra
命令を使っていることがわかります。
$ clang -O3 -S fixed-point-mla.c
$ cat fixed-point-mla.s
...略...
.globl _inner_product ; -- Begin function inner_product
.p2align 2
_inner_product: ; @inner_product
.cfi_startproc
; %bb.0:
cbz x2, LBB0_3
; %bb.1:
cmp x2, #16
b.hs LBB0_4
; %bb.2:
mov x9, #0 ; =0x0
mov w8, #0 ; =0x0
b LBB0_7
LBB0_3:
mov w8, #0 ; =0x0
mov x0, x8
ret
LBB0_4:
and x9, x2, #0xfffffffffffffff0
add x8, x0, #32
add x10, x1, #32
movi.2d v0, #0000000000000000
mov x11, x9
movi.2d v1, #0000000000000000
movi.2d v2, #0000000000000000
movi.2d v3, #0000000000000000
LBB0_5: ; =>This Inner Loop Header: Depth=1
ldp q4, q5, [x8, #-32]
ldp q6, q7, [x8], #64
ldp q16, q17, [x10, #-32]
ldp q18, q19, [x10], #64
movi.4s v20, #128
mla.4s v20, v16, v4
movi.4s v4, #128
mla.4s v4, v17, v5
movi.4s v5, #128
mla.4s v5, v18, v6
movi.4s v6, #128
mla.4s v6, v19, v7
ssra.4s v0, v20, #8
ssra.4s v1, v4, #8
ssra.4s v2, v5, #8
ssra.4s v3, v6, #8
subs x11, x11, #16
b.ne LBB0_5
; %bb.6:
add.4s v0, v1, v0
add.4s v1, v3, v2
add.4s v0, v1, v0
addv.4s s0, v0
fmov w8, s0
cmp x9, x2
b.eq LBB0_9
LBB0_7:
sub x10, x2, x9
lsl x11, x9, #2
add x9, x1, x11
add x11, x0, x11
mov w12, #128 ; =0x80
LBB0_8: ; =>This Inner Loop Header: Depth=1
ldr w13, [x11], #4
ldr w14, [x9], #4
madd w13, w14, w13, w12
add w8, w8, w13, asr #8
subs x10, x10, #1
b.ne LBB0_8
LBB0_9:
mov x0, x8
ret
.cfi_endproc
; -- End function
...略...
もちろん、asm volatile
で ssra
命令を使う方も動きます。
$ clang -O3 fixed-point-mla-ssra.c && ./a.out
0x00000240
VByteの例も普通に動きます。
$ clang vbyte.c && ./a.out
original: 300
encoded: ac,02
decoded: 300
Hack #70 浮動小数点数のビット列表現を理解する
float
型のビット列表現を見てみます。
$ clang -o f32rep f32rep.c
$ ./f32rep
-1.0: 0xbf800000
0.0: 0x00000000
0xcafep-149: 0x0000cafe
1.0/0.0: 0x7f800000
0.0/0.0: 0x7fc00000
x86-64 Linuxの実行例を比較すると、0.0/0.0
のビット列表現の最上位ビット(符号ビット)が立っていないことがわかります。これは本文中で言及している、x86-64とAArch64の違いです。余談ですが、Armの拡張FEAT_AFPを使うと、Armでも生成されるNaNの符号ビットを立てるようにできます(Armv8.7のFEAT_AFPをApple M4で試す、あるいはx86とArmの浮動小数点演算の違い)。
十進の方は、GCCが必要です。Homebrew等で入れてください。
$ gcc-14 --version
gcc-14 (Homebrew GCC 14.2.0_1) 14.2.0
Copyright (C) 2024 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
$ gcc-14 -o d32rep d32rep.c
$ ./d32rep
1.: 0x32800001
1.0: 0x3200000a
1.000000: 0x2f8f4240
1.0000000: 0x2f8f4240
0.0: 0x32000000
42e-101: 0x0000002a
1.0/0.0: 0x78000000
0.0/0.0: 0x7c000000
実行結果はx86-64 Linuxと同一になりました。
Hack #71 浮動小数点例外
状態フラグを見る例は普通に動きます。
$ clang -Wall -o exception exception.c
$ ./exception
No exception
FE_INVALID,
FE_DIVBYZERO,
FE_OVERFLOW, FE_INEXACT,
FE_UNDERFLOW, FE_INEXACT,
トラップの例は、trap-aarch64.c
を動かせます。
$ clang -Wall -o trap-aarch64 trap-aarch64.c && ./trap-aarch64
Traps are supported:
Input Denormal exception (IDE) trap enabled.
Inexact exception (IXE) trap enabled.
Underflow exception (UFE) trap enabled.
Overflow exception (OFE) trap enabled.
Divide by Zero exception (DZE) trap enabled.
Invalid Operation exception (IOE) trap enabled.
zsh: illegal hardware instruction ./trap-aarch64
Apple M4でもトラップは使えました。そして相変わらずSIGFPEではなくSIGILLが発生しています。
flush to zeroの例を動かしてみます。
$ clang -Wall -o flushtozero flushtozero.c
$ ./flushtozero
FE_UNDERFLOW is not set.
0x1.bd5b7ddep-1023
FE_UNDERFLOW is not set.
0x1.0a920888p-1025
$ ./flushtozero FZ
FE_UNDERFLOW is set.
0x0p+0
FE_UNDERFLOW is not set.
0x1p-1022
本文と同じですね。
Hack #72 浮動小数点数の丸め方を変える
現在のAArch64 Macで動かせるのは setround1.c
と setround2.c
です。
$ clang -o setround1 setround1.c && ./setround1
0x1.0000000000001p+0
$ clang -o setround2 setround2.c && ./setround2
0x1.0000000000001p+0
Hack #73 浮動小数点環境を触るコードに対するコンパイラの最適化と戦う
まず、「意図通りに動かなくなる例」です。
$ clang -O0 -o fpopt0 fpopt0.c && ./fpopt0
up: 0x1.0000000000001p+0
down: 0x1p+0
$ clang -O2 -o fpopt0 fpopt0.c && ./fpopt0
up: 0x1p+0
down: 0x1p+0
これは著者もClangを使っていたから本と同じ結果になったのであって、GCCだと違う結果になったと思います。
pragmaは、最近のClangは対応しています。えらい!
$ clang -Wall -O2 -o fpopt1 fpopt1.c && ./fpopt1
up: 0x1.0000000000001p+0
down: 0x1p+0
$ gcc-14 -Wall -O2 -o fpopt1 fpopt1.c && ./fpopt1
fpopt1.c:4: warning: ignoring '#pragma STDC FENV_ACCESS' [-Wunknown-pragmas]
4 | #pragma STDC FENV_ACCESS ON
up: 0x1.0000000000001p+0
down: 0x1.0000000000001p+0
volatileのやつとasmのやつをGCC 14で動かした例も載せておきます。
$ gcc-14 -Wall -O2 -o fpopt2 fpopt2.c && ./fpopt2
up: 0x1.0000000000001p+0
down: 0x1p+0
$ gcc-14 -Wall -O2 -o fpopt3 fpopt3.c && ./fpopt3
up: 0x1.0000000000001p+0
down: 0x1p+0
Hack #74 NaNを深掘りする
signaling NaNの例は普通に動きます。
$ clang -o snan1 snan1.c
$ ./snan1
FE_INVALID is not set
nan
FE_INVALID is set
nan
AArch64の呼び出し規約では、signaling NaNを関数から値として返すこともできます。本文中のx86-64 Linuxでの実行結果と同様です。
$ clang -o snan2 snan2.c
$ ./snan2
FE_INVALID is not set
nan
FE_INVALID is set
nan
Clangの float.h
はまだC23の FLT_SNAN
マクロに対応していないようです。一方、GCCでは使えました。
$ clang --version
Apple clang version 16.0.0 (clang-1600.0.26.4)
Target: arm64-apple-darwin24.1.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin
$ clang -std=c2x -o snan3 snan3.c
snan3.c:9:13: error: use of undeclared identifier 'FLT_SNAN'
9 | float x = FLT_SNAN;
| ^
1 error generated.
$ gcc-14 -std=c2x -o snan3 snan3.c
$ ./snan3
FE_INVALID is not set
nan
FE_INVALID is set
nan
NaNのペイロードの伝播は本に載せた通りです。
$ clang -o nan-propagation -Wall nan-propagation.c
$ ./nan-propagation
a: 0x7fc0cafe
b: 0xffc01234
- a: 0xffc0cafe
fabs(b): 0x7fc01234
a + 1.0: 0x7fc0cafe
1.0 + b: 0xffc01234
a + b: 0x7fc0cafe
b + a: 0xffc01234
a * 2.0: 0x7fc0cafe
2.0 * b: 0xffc01234
a * b: 0x7fc0cafe
b * a: 0xffc01234
$ clang -O2 -o nan-propagation -Wall nan-propagation.c
$ ./nan-propagation
a: 0x7fc0cafe
b: 0xffc01234
- a: 0xffc0cafe
fabs(b): 0x7fc01234
a + 1.0: 0x7fc0cafe
1.0 + b: 0xffc01234
a + b: 0x7fc0cafe
b + a: 0x7fc0cafe
a * 2.0: 0x7fc0cafe
2.0 * b: 0xffc01234
a * b: 0x7fc0cafe
b * a: 0x7fc0cafe
Hack #75 浮動小数点数のアーキテクチャごとの差異に触れる
アンダーフローの判定のタイミングは、実行結果を含めて本に載せました。
$ clang -Wall -o tininess tininess.c && ./tininess
0x1.0000001p-1022 * 0x1.ffffffep-1 = 0x1p-1022, with UNDERFLOW
Tininess is detected before rounding
FMAとinvalid operation例外も同様です。
$ clang -Wall -O -o fma-exception fma-exception.c && ./fma-exception
FP_FAST_FMA is defined
fma(0, INFINITY, NAN) = nan, raises INVALID
本文には載せていませんが、Armの代替動作(FEAT_AFP)を試せるサンプルをGitHubの方に載せています。Apple M4での実行結果は次のようになります:
$ clang -o arm-altfp arm-altfp.c && ./arm-altfp
0x1.0000001p-1022 * 0x1.ffffffep-1, which yields 0x1p-1022, raises UNDERFLOW; underflow is detected before rounding
FPCR.AH set.
0x1.0000001p-1022 * 0x1.ffffffep-1, which yields 0x1p-1022, does not raise UNDERFLOW; underflow is detected after rounding
つまり、FPCR.AHをセットしたことによりアンダーフローの判定タイミングが変わります。
FEAT_AFPについては詳しい記事を書きました:Armv8.7のFEAT_AFPをApple M4で試す、あるいはx86とArmの浮動小数点演算の違い
Hack #76 SIMD命令セットの基礎知識
macOSでは sysctl -a hw
と打つとCPUの機能を出力してくれます。
$ sysctl -a hw
...略...
hw.optional.arm.FEAT_FlagM: 1
hw.optional.arm.FEAT_FlagM2: 1
hw.optional.arm.FEAT_FHM: 1
hw.optional.arm.FEAT_DotProd: 1
hw.optional.arm.FEAT_SHA3: 1
hw.optional.arm.FEAT_RDM: 1
hw.optional.arm.FEAT_LSE: 1
hw.optional.arm.FEAT_SHA256: 1
hw.optional.arm.FEAT_SHA512: 1
hw.optional.arm.FEAT_SHA1: 1
hw.optional.arm.FEAT_AES: 1
hw.optional.arm.FEAT_PMULL: 1
hw.optional.arm.FEAT_SPECRES: 0
hw.optional.arm.FEAT_SB: 1
hw.optional.arm.FEAT_FRINTTS: 1
hw.optional.arm.FEAT_LRCPC: 1
hw.optional.arm.FEAT_LRCPC2: 1
hw.optional.arm.FEAT_FCMA: 1
hw.optional.arm.FEAT_JSCVT: 1
hw.optional.arm.FEAT_PAuth: 1
hw.optional.arm.FEAT_PAuth2: 1
hw.optional.arm.FEAT_FPAC: 1
hw.optional.arm.FEAT_DPB: 1
hw.optional.arm.FEAT_DPB2: 1
hw.optional.arm.FEAT_BF16: 1
hw.optional.arm.FEAT_I8MM: 1
hw.optional.arm.FEAT_WFxT: 1
hw.optional.arm.FEAT_RPRES: 1
hw.optional.arm.FEAT_ECV: 1
hw.optional.arm.FEAT_AFP: 1
hw.optional.arm.FEAT_LSE2: 1
hw.optional.arm.FEAT_CSV2: 1
hw.optional.arm.FEAT_CSV3: 1
hw.optional.arm.FEAT_DIT: 1
hw.optional.arm.FEAT_FP16: 1
hw.optional.arm.FEAT_SSBS: 0
hw.optional.arm.FEAT_BTI: 1
hw.optional.arm.FEAT_SME: 1
hw.optional.arm.FEAT_SME2: 1
hw.optional.arm.SME_F32F32: 1
hw.optional.arm.SME_BI32I32: 1
hw.optional.arm.SME_B16F32: 1
hw.optional.arm.SME_F16F32: 1
hw.optional.arm.SME_I8I32: 1
hw.optional.arm.SME_I16I32: 1
hw.optional.arm.FEAT_SME_F64F64: 1
hw.optional.arm.FEAT_SME_I16I64: 1
hw.optional.arm.FP_SyncExceptions: 1
...略...
CPUID命令はAArch64にはありませんが、せっかくなのでRosetta 2が何を返すか見ておきましょう。
$ clang -arch x86_64 detect_cpu_features.c
$ ./a.out
SSE4.1: 1, AVX2: 0, AVX-512F: 0
Intel Macの話になりますが、x86-64のmacOSでAVX-512を検出するためにコツが要るというのは「CPUの機能を実行時に検出する:x86編」に書きました。
執筆裏話をしておくと、Armの命令セット拡張の「オプショナルで含むISA」の記述がArm Architecture Reference Manualの版によって違う疑惑が締め切りギリギリに発覚して大変でした。
Hack #77 SIMD並列化したコードを書く
add16
の例はAArch64だとこのようになります:
$ clang -S -O3 add16.c
$ cat add16.c
...略...
.globl _add16 ; -- Begin function add16
.p2align 2
_add16: ; @add16
.cfi_startproc
; %bb.0:
ldp q0, q1, [x0]
ldp q2, q3, [x1]
fadd.4s v0, v0, v2
fadd.4s v1, v1, v3
stp q0, q1, [x2]
ldp q0, q1, [x0, #32]
ldp q2, q3, [x1, #32]
fadd.4s v0, v0, v2
fadd.4s v1, v1, v3
stp q0, q1, [x2, #32]
ret
.cfi_endproc
; -- End function
...略...
fadd.4s
がArmのSIMD命令となっています。GCCが出力したものと表記が違いますね。
Apple Clangは -fopenmp
オプションを受け付けないようです。Homebrew等で入れたLLVM Clangは -fopenmp
に対応しているようです。
$ $(brew --prefix llvm)/bin/clang -O3 -S -fopenmp add16_openmp.c
まあ、ArmのNEONはSIMD幅が128ビット固定なので、simdlen(8)
は効きませんが。
Hack #78 SIMD命令を使ったさまざまなテクニック
本では、Linuxのlibc(glibc)を逆アセンブルしています。我らがmacOSのlibcであるlibSystemも逆アセンブルしてみましょう……
……と、言いたいところですが、最近のmacOSにはファイルシステム上に libSystem
の実体が存在しません。悲しい……。
$ otool -L a.out
a.out:
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1351.0.0)
$ file /usr/lib/libSystem.B.dylib
/usr/lib/libSystem.B.dylib: cannot open `/usr/lib/libSystem.B.dylib' (No such file or directory)
$ ls /usr/lib/libSystem*
zsh: no matches found: /usr/lib/libSystem*
仕方がないので、デバッガーで実行時に strlen
の実体を読み取ることにします。
#include <string.h>
int main(void)
{
size_t (*p)(const char *) = strlen;
__asm__ volatile("brk #0");
}
$ clang -g test.c
$ lldb a.out
(lldb) target create "a.out"
Current executable set to '.../a.out' (arm64).
(lldb) run
Process 31159 launched: '.../a.out' (arm64)
Process 31159 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BREAKPOINT (code=1, subcode=0x100003f98)
frame #0: 0x0000000100003f98 a.out`main at test.c:6:5
3 int main(void)
4 {
5 size_t (*p)(const char *) = strlen;
-> 6 __asm__ volatile("brk #0");
7 }
Target 0: (a.out) stopped.
(lldb) disassemble -a p
libsystem_platform.dylib`_platform_strlen:
...
(lldb) memory read ...
...
ここまで書いて思いましたが、逆アセンブル・逆コンパイルはいかにも「リバースエンジニアリング」に該当しそうですね。なので、ここでは「SIMD命令を使って16バイトごとに処理していそうな気がする」とだけ言っておきます。
というわけで、Binary Hacks Rebootedの第7章をMacで実践してみました。まあ、この章はOS依存な項目が少ないですし、AArch64での動作結果も載せていたりするので、この記事を見なくてもすんなり試せたのではないかと思います。
あとは、Apple M3以降(M2は不明)ではFEAT_AFPを試せるので、ぜひ試してください。詳しくは「Armv8.7のFEAT_AFPをApple M4で試す、あるいはx86とArmの浮動小数点演算の違い」に書きました。