
2026/04/17 19:32
コルテックス M 搭載プロセッサにおける浮動小数点処理に関する趣事談
RSS: https://news.ycombinator.com/rss
要約▶
日本語訳:
nRF52840 プロセッサには浮動小数点演算ユニット(FPU)が搭載されていますが、Zephyr のデフォルト設定では FPU を無効にしてソフトウェアエミュレーションに依存します。記事は、リンカリング時に互換性のないアプリケーションバイナリインターフェース(ABI)バリエーション(
soft、softfp、hard)を混在させることが、コンパイラが浮動小数点引数に対して異なるレジスタ規約を期待しているため、リンカーの不一致や実行時の NOCP 使用障害などの致命的なエラーを引き起こすと指摘しています。安定性を確保するためには、開発者は CONFIG_FPU カーネルオプションとライブラリ関数を使用するリンカ -mfloat-abi フラグを一致させる必要があります。FPU を最適化のために即席で有効にする動的アプローチも存在します(例:実行の直前、特定の関数内で有効化)。しかし、指令の実行中に FPU が無効になっている場合、使用障害を防ぐためには極めて注意が必要です。結局のところ、ハードウェアサポートとコンパイラフラグを適切に設定することで即時クラッシュを防止し、安全なファームウェアの実行を保証します。本文
最近の PSA Crypto API に関する投稿において、Arm Cortex-M4 プロセッサと Arm TrustZone CryptoCell 310 セキュリティサブシステム間の通信を管理するクローズドソースライブラリ内で ECDSA 署名操作を実行する方法を、nRF52840 および ESP32-S3 という異なる MCU(マイクロコントローラー)において示しました。前者(nRF52840)の場合の投稿にあるリンクの穴へ足を踏み入れた読者の方々は、
nrf_cc310_mbedcrypto ライブラリのハードウェア浮動小数点型(hard-float)とソフトウェア浮動小数点型(soft-float)のバリエーションが存在することを気付いたかもしれません。もし以下形式のリンカエラーに遭遇した経験がある方なら、その理由はまさにこれだと言えます:
ld.bfd: error: X uses VFP register arguments, Y does not ld.bfd: failed to merge target specific data of file
Arm は、
-mfloat-abi コンパイラフラグで制御される 3 つの浮動小数点アプリケーションバイナリインターフェース(ABI)オプションを定義しています。
- soft: FPU ハードウェアなしのソフト ABI。すべての浮動小数点演算はランタイムライブラリ関数によって処理されます。値は整数レジスタバンクを通じて渡されます。
- softfp: FPU ハードウェアありのソフト ABI。コンパイルされたコードが直接 FPU にアクセスするコードを生成することを可能にします。ただし、計算でランタイムライブラリ関数を必要とする場合は、ソフト・フロート呼び出し規約を使用します。値は整数レジスタバンクを通じて渡されます。
- hard: ハード ABI。コンパイルされたコードが直接 FPU にアクセスし、ランタイムライブラリ関数を呼ぶ際に FPU 固有の呼び出し規約を使用することを可能にします。
Arm は、ほとんどの指令セットアーキテクチャ(ISA)と同様に、サブルーチンへの引数を一般目的レジスタ(GPR)、具体的には r0-r3 に渡します。引数の数またはサイズが利用可能な GPR を超えた場合、残りの引数は「スピン」(spill)され、 callee(呼び出された関数)によってアクセスできるようスタック上に格納されます。ただし、プロセッサが浮動小数点ユニット(FPU)を備えている場合(具体的には Armv7-M プロセッサでは C10 および C11 コプロセッサ)、浮動小数点拡張に伴い 32 個の浮動小数点レジスタ(s0-s31)を持つ追加のレジスタバンクが存在します。
補足: Armv7-M プロセッサ(例えば Cortex-M4)における浮動小数点に関する記述で見られる「ベクトル浮動小数点(VFP)」という用語についてです。リファレンスマニュアルでは、その理由は次のように説明されています。「ARMv7-A および ARMv7-R アーキテクチャプロファイルにおいて、浮動小数点指令は VFP 指令と呼ばれ、mnemonic が V で始まります。ARM アセンブラはアーキテクチャバージョンおよびプロファイルを超えて非常に一貫しているため、ARMv7-M もこれらの mnemonic を保持していますが、通常は単に浮動小数点指令または FP 指令として説明されます」。
ハード ABI を使用する場合、s0-s15 レジスタをサブルーチンへの引数の渡しに利用できます。「hard」の使用は、ルーチン内で浮動小数点指令(ロードとストア、レジスタ転送、データ処理)を使用することを可能にするという点を示しています。Softfp を使用する場合は、ルーチン内で浮動小数点指令の使用が許可されますが、引数は浮動小数点レジスタを通じて渡されることはできません。「soft」は Softfp と同じ呼び出し規約を使用するため互換性がありますが、浮動小数点指令の使用を許可しません。浮動小数点指令へのサポートがない状態で浮動小数点演算が行われる場合は、ソフトウェアにおいてエミュレートされなければなりません。
本稿の冒頭で述べたエラーが出現するのは、ソフト/Softfp をハードと混在させているためです。リンカは、各バリエーションで異なる Arm 属性セクションを見て、オブジェクトファイルの ABI を判定することが可能です。例えば、nRF52840 上では属性は以下のようになります(
readelf で抽出):
hard
Attribute Section: aeabi File Attributes Tag_CPU_name: "7E-M" Tag_CPU_arch: v7E-M Tag_CPU_arch_profile: Microcontroller Tag_THUMB_ISA_use: Thumb-2 Tag_FP_arch: VFPv4-D16 Tag_ABI_PCS_wchar_t: 4 Tag_ABI_FP_denormal: Needed Tag_ABI_FP_exceptions: Needed Tag_ABI_FP_number_model: IEEE 754 Tag_ABI_align_needed: 8-byte Tag_ABI_align_preserved: 8-byte, except leaf SP Tag_ABI_enum_size: small Tag_ABI_HardFP_use: SP only Tag_ABI_VFP_args: VFP registers Tag_ABI_optimization_goals: Aggressive Speed Tag_CPU_unaligned_access: v6
softfp
Attribute Section: aeabi File Attributes Tag_CPU_name: "7E-M" Tag_CPU_arch: v7E-M Tag_CPU_arch_profile: Microcontroller Tag_THUMB_ISA_use: Thumb-2 Tag_FP_arch: VFPv4-D16 Tag_ABI_PCS_wchar_t: 4 Tag_ABI_FP_rounding: Needed Tag_ABI_FP_denormal: Needed Tag_ABI_FP_exceptions: Needed Tag_ABI_FP_user_exceptions: Needed Tag_ABI_FP_number_model: IEEE 754 Tag_ABI_align_needed: 8-byte Tag_ABI_enum_size: small Tag_ABI_HardFP_use: SP only Tag_ABI_optimization_goals: Aggressive Size Tag_CPU_unaligned_access: v6 Tag_ABI_FP_16bit_format: IEEE 754
soft
Attribute Section: aeabi File Attributes Tag_CPU_name: "7E-M" Tag_CPU_arch: v7E-M Tag_CPU_arch_profile: Microcontroller Tag_THUMB_ISA_use: Thumb-2 Tag_ABI_PCS_wchar_t: 4 Tag_ABI_FP_denormal: Needed Tag_ABI_FP_exceptions: Needed Tag_ABI_FP_number_model: IEEE 754 Tag_ABI_align_needed: 8-byte Tag_ABI_align_preserved: 8-byte, except leaf SP Tag_ABI_enum_size: small Tag_CPU_unaligned_access: v6
プラクティスにおける浮動小数点 ABI
各 ABI でコンパイルした出力は、インライン化または完全に取り除かれないようにオプティマイゼーションをオフにした以下の単純な関数を用いて観察できます。
float __attribute__((optimize("O0"))) addf(float a, float b) { return a + b; }
コンパイラを直接呼び出す場合は
-mfloat-abi オプションを渡すことができますが、ビルドシステムを活用している場合、プラットフォーム固有の設定を特定する必要があります。例えば、Zephyr を使用する場合は CONFIG_FPU=y とすることで FPU が有効化されます。このオプションは CONFIG_CPU_HAS_FPU に依存しており、FPU が存在してもデフォルトでは false に設定されています。
config FPU bool "Floating point unit (FPU)" depends on CPU_HAS_FPU help This option enables the hardware Floating Point Unit (FPU), in order to support using the floating point registers and instructions. When this option is enabled, by default, threads may use the floating point registers only in an exclusive manner, and this usually means that only one thread may perform floating point operations. If it is necessary for multiple threads to perform concurrent floating point operations, the "FPU register sharing" option must be enabled to preserve the floating point registers across context switches. Note that this option cannot be selected for the platforms that do not include a hardware floating point unit; the floating point support for those platforms is dependent on the availability of the toolchain- provided software floating point library.
nRF52840 は FPU を有しているため、
CONFIG_CPU_HAS_FPU が選択されます。
config SOC_NRF52840 select CPU_CORTEX_M_HAS_DWT select CPU_HAS_FPU
FPU がデフォルトでオフ(無効)になっているため、
west build はソフト ABI を使用するようになります。
west build -p -b nrf52840dk/nrf52840 .
期待通り、引数は GPR 経由で渡され、浮動小数点加法演算は実装をソフトウェアで行うランタイムライブラリ関数
__addsf3 を呼び出します。
0001a860 <addf>: 1a860: b580 push {r7, lr} 1a862: b082 sub sp, #8 1a864: af00 add r7, sp, #0 1a866: 6078 str r0, [r7, #4] 1a868: 6039 str r1, [r7, #0] 1a86a: 6839 ldr r1, [r7, #0] 1a86c: 6878 ldr r0, [r7, #4] 1a86e: f7e5 fc4d bl 10c <__addsf3> 1a872: 4603 mov r3, r0 1a874: 4618 mov r0, r3 1a876: 3708 adds r7, #8 1a878: 46bd mov sp, r7 1a87a: bd80 pop {r7, pc}
CONFIG_FPU=y を設定して再度ビルドすると、デフォルトでハードフロート ABI が使用され、浮動小数点引数は addf に浮動小数点レジスタを通じて渡され、加法演算には vadd.f32 が使用されます。
0001b5ae <addf>: 1b5ae: b480 push {r7} 1b5b0: b083 sub sp, #12 1b5b2: af00 add r7, sp, #0 1b5b4: ed87 0a01 vstr s0, [r7, #4] 1b5b8: edc7 0a00 vstr s1, [r7] 1b5bc: ed97 7a01 vldr s14, [r7, #4] 1b5c0: edd7 7a00 vldr s15, [r7] 1b5c4: ee77 7a27 vadd.f32 s15, s14, s15 1b5c8: eeb0 0a67 vmov.f32 s0, s15 1b5cc: 370c adds r7, #12 1b5ce: 46bd mov sp, r7 1b5d0: f85d 7b04 ldr.w r7, [sp], #4 1b5d4: 4770 bx lr
CONFIG_FP_SOFTABI=y を設定すると、代わりに softfp が使用されます。
choice prompt "Floating point ABI" default FP_HARDABI depends on FPU config FP_HARDABI bool "Floating point Hard ABI" help This option selects the Floating point ABI in which hardware floating point instructions are generated and uses FPU-specific calling conventions. config FP_SOFTABI bool "Floating point Soft ABI" help This option selects the Floating point ABI in which hardware floating point instructions are generated but soft-float calling conventions. endchoice
このフラグの有無は、
-v フラグ(つまり west -v build)を付けて提供することで観察できます。
-mcpu=cortex-m4 -mthumb -mabi=aapcs -mfpu=fpv4-sp-d16 -mfloat-abi=softfp -mfp16-format=ieee
最後に、観測された出力はソフト呼び出し規約(GPR への引数渡し)を含みますが、ルーチン内では浮動小数点レジスタおよび指令を利用しています。
0001b59a <addf>: 1b59a: b480 push {r7} 1b59c: b083 sub sp, #12 1b59e: af00 add r7, sp, #0 1b5a0: 6078 str r0, [r7, #4] 1b5a2: 6039 str r1, [r7, #0] 1b5a4: ed97 7a01 vldr s14, [r7, #4] 1b5a8: edd7 7a00 vldr s15, [r7] 1b5ac: ee77 7a27 vadd.f32 s15, s14, s15 1b5b0: ee17 3a90 vmov r3, s15 1b5b4: 4618 mov r0, r3 1b5b6: 370c adds r7, #12 1b5b8: 46bd mov sp, r7 1b5ba: f85d 7b04 ldr.w r7, [sp], #4 1b5be: 4770 bx lr
ボーナスラウンド:FPU の動的有効化
上記の
CONFIG_FPU の説明を読むと、これにはソフト fp およびハード ABI の設定だけでなく、リセット時に FPU を有効化する機能も含まれていることがわかります。プロセッサに FPU が存在するかどうか(CONFIG_CPU_HAS_FPU)に関わらず、z_prep_c() から z_arm_floating_point_init() が呼び出されます。
FUNC_NORETURN void z_prep_c(void) { soc_prep_hook(); relocate_vector_table(); #if defined(CONFIG_CPU_HAS_FPU) z_arm_floating_point_init(); #endif arch_bss_zero(); arch_data_copy(); #if defined(CONFIG_ARM_CUSTOM_INTERRUPT_CONTROLLER) /* Invoke SoC-specific interrupt controller initialization */ z_soc_irq_init(); #else z_arm_interrupt_init(); #endif /* CONFIG_ARM_CUSTOM_INTERRUPT_CONTROLLER */ #if CONFIG_ARCH_CACHE arch_cache_init(); #endif #ifdef CONFIG_NULL_POINTER_EXCEPTION_DETECTION_DWT z_arm_debug_enable_null_pointer_detection(); #endif z_cstart(); CODE_UNREACHABLE; }
CONFIG_FPU の値や他の設定に応じて、z_arm_floating_point_init() は FPU を適切にセットアップします。これは、まずコプロセッサアクセス制御レジスタ(CPACR)をクリアし、次に CONFIG_FPU=y の場合、浮動小数点コプロセッサへのアクセスを有効にするために CP10 (CPACR_CP10_PRIV_ACCESS) および CP11 (CPACR_CP11_PRIV_ACCESS) フラグを設定することで達成されます。その後、コンテキスト状態のスタッキング(ASPEN)および怠慢なコンテキスト保存(LSPEN)のための FP コンテキスト制御レジスタ(FPCCR)フラグが設定され、浮動小数点状態および制御レジスタ(FPSCR)がクリアされます。
#if defined(CONFIG_CPU_HAS_FPU) static inline void z_arm_floating_point_init(void) { /* * Upon reset, the Co-Processor Access Control Register is, normally, * 0x00000000. However, it might be left un-cleared by firmware running * before Zephyr boot. */ SCB->CPACR &= (~(CPACR_CP10_Msk | CPACR_CP11_Msk)); #if defined(CONFIG_FPU) /* * Enable CP10 and CP11 Co-Processors to enable access to floating * point registers. */ #if defined(CONFIG_USERSPACE) /* Full access */ SCB->CPACR |= CPACR_CP10_FULL_ACCESS | CPACR_CP11_FULL_ACCESS; #else /* Privileged access only */ SCB->CPACR |= CPACR_CP10_PRIV_ACCESS | CPACR_CP11_PRIV_ACCESS; #endif /* CONFIG_USERSPACE */ /* * Upon reset, the FPU Context Control Register is 0xC0000000 * (both Automatic and Lazy state preservation is enabled). */ #if defined(CONFIG_MULTITHREADING) && !defined(CONFIG_FPU_SHARING) /* Unshared FP registers (multithreading) mode. We disable the * automatic stacking of FP registers (automatic setting of * FPCA bit in the CONTROL register), upon exception entries, * as the FP registers are to be used by a single context (and * the use of FP registers in ISRs is not supported). This * configuration improves interrupt latency and decreases the * stack memory requirement for the (single) thread that makes * use of the FP co-processor. */ FPU->FPCCR &= (~(FPU_FPCCR_ASPEN_Msk | FPU_FPCCR_LSPEN_Msk)); #else /* * FP register sharing (multithreading) mode or single-threading mode. * * Enable both automatic and lazy state preservation of the FP context. * The FPCA bit of the CONTROL register will be automatically set, if * the thread uses the floating point registers. Because of lazy state * preservation the volatile FP registers will not be stacked upon * exception entry, however, the required area in the stack frame will * be reserved for them. This configuration improves interrupt latency. * The registers will eventually be stacked when the thread is swapped * out during context-switch or if an ISR attempts to execute floating * point instructions. */ FPU->FPCCR = FPU_FPCCR_ASPEN_Msk | FPU_FPCCR_LSPEN_Msk; #endif /* CONFIG_FPU_SHARING */ /* Make the side-effects of modifying the FPCCR be realized * immediately. */ barrier_dsync_fence_full(); barrier_isync_fence_full(); /* Initialize the Floating Point Status and Control Register. */ #if defined(CONFIG_ARMV8_1_M_MAINLINE) /* * For ARMv8.1-M with FPU, the FPSCR[18:16] LTPSIZE field must be set * to 0b100 for "Tail predication not applied" as it's reset value */ __set_FPSCR(4 << FPU_FPDSCR_LTPSIZE_Pos); #else __set_FPSCR(0); #endif /* * Note: * The use of the FP register bank is enabled, however the FP context * will be activated (FPCA bit on the CONTROL register) in the presence * of floating point instructions. */ #endif /* CONFIG_FPU */ /* * Upon reset, the CONTROL.FPCA bit is, normally, cleared. However, * it might be left un-cleared by firmware running before Zephyr boot. * We must clear this bit to prevent errors in exception unstacking. * * Note: * In Sharing FP Registers mode CONTROL.FPCA is cleared before switching * to main, so it may be skipped here (saving few boot cycles). * * If CONFIG_INIT_ARCH_HW_AT_BOOT is set, CONTROL is cleared at reset. */ #if (!defined(CONFIG_FPU) || !defined(CONFIG_FPU_SHARING)) && \ (!defined(CONFIG_INIT_ARCH_HW_AT_BOOT)) __set_CONTROL(__get_CONTROL() & (~(CONTROL_FPCA_Msk))); #endif }
z_arm_floating_point_init() は、CONFIG_FP_HARDABI=y または CONFIG_FP_SOFTABI=y である場合にいつでも FPU が有効化されることを保証しているため、浮動小数点指令の実行が例外を発生させることはありません。しかし、FPU が浮動小数点指令の実行前に有効化されていない場合、NOCP(コプロセッサなし)使用フォールトが発生します。これを実証するには、CONFIG_FPU を設定せず、次の行を CMakeLists.txt に追加することです。
list(APPEND TOOLCHAIN_C_FLAGS -mfloat-abi=softfp) list(APPEND TOOLCHAIN_CXX_FLAGS -mfloat-abi=softfp)
GDB によってプログラムの実行ステップを行うと、フォールトが観測されます。
(gdb) b addf Breakpoint 1 at 0x1a662: file main.c. (gdb) c Continuing. Breakpoint 1, addf (a=1.87308763e-40, b=1.09258461e-19) at main.c 141 { (gdb) s 142 return a + b; (gdb) x/i $pc => 0x1a66c <addf+10>: vldr s14, [r7, #4] (gdb) s z_arm_usage_fault () at zephyr/arch/arm/core/cortex_m/fault_s.S:80 80 mrs r0, MSP (gdb) x/1xh 0xe000ed2a 0xe000ed2a: 0x0008
期待通り、使用フォールトステータスレジスタ(0xe000ed2a)のビット 3 が設定されています(1000 = 0x0008)、これは NOCP 使用フォールトに相当します。
しかし、リセット時に FPU を有効化する必要があるか、あるいは連続して有効にする必要があるという特定のリターンは存在しません。同様の操作列を
addf 関数に追加することで、「必要になった時に」と(just in time)、FPU を有効化することも可能です。
float __attribute__((optimize("O0"))) addf(float a, float b) { SCB->CPACR &= (~(CPACR_CP10_Msk | CPACR_CP11_Msk)); SCB->CPACR |= CPACR_CP10_PRIV_ACCESS | CPACR_CP11_PRIV_ACCESS; FPU->FPCCR = FPU_FPCCR_ASPEN_Msk | FPU_FPCCR_LSPEN_Msk; barrier_dsync_fence_full(); barrier_isync_fence_full(); __set_FPSCR(0); return a + b; }
nRF52840 を再コンパイルしてフラッシュすると、関数内の浮動小数点演算が正常に実行されます。同様の動作は、NOCP ビットが設定されている場合 FPU を有効化し、フォールトを発生させた指令への実行制御を戻すように使用フォールトハンドラを調整することで達成できます。
この方法で FPU をオンオフすることにより問題が発生する可能性があり、極めて慎重に使用する必要がある多くの理由がありますが、浮動小数点ハードウェアを有効化する時間を制限するための正当なユースケースが存在します。これらのシナリオと、ハードウェアおよびソフトウェアの浮動小数点の間にあるトレードオフについては、次の投稿で探求いたします。