Macで試すBinary Hacks Rebootedその2:数値表現とデータ処理

前回から日が開きましたが、引き続き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 volatilessra 命令を使う方も動きます。

$ 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.csetround2.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の浮動小数点演算の違い」に書きました。

Spread the love