2019年04月22日
GCC/Clangで複数命令を含むインラインアセンブラを記述する際の注意点
とある開発中のプロジェクトで、GCC/Clang のインラインアセンブラが意図しない不正コードを生成し、バグではないか?と調査しました。
その結果、これはどうやら正しい仕様らしい、しかし非常に間違いやすく混乱しやすいケースだと思ったのでメモしておきます。
解決策だけを先に言いますと、GCC/Clang のインラインアセンブラを、可能ならば常に 1 文に 1 命令のスタイルで記述するようにすれば、この類の混乱は避けられそうです。
その結果、これはどうやら正しい仕様らしい、しかし非常に間違いやすく混乱しやすいケースだと思ったのでメモしておきます。
解決策だけを先に言いますと、GCC/Clang のインラインアセンブラを、可能ならば常に 1 文に 1 命令のスタイルで記述するようにすれば、この類の混乱は避けられそうです。
以下が、問題となった AArch64 のコード片を少し修正したものです。シンプルさを優先しているので型等が実際とは異なります。(asm1.c)
解決の手掛かりになったのは以下のバグ報告です。
GNU Arm Embedded Toolchain Incorrect register allocation in inline assembly code
どうも GCC/Clang のインラインアセンブラは、入力オペランドを全て使用してから、出力オペランドに書き込む、と仮定しているようです。
6.47.2 Extended Asm - Assembler Instructions with C Expression Operands
今回のパターンのように、入力オペランドを使用する命令の前に実行される命令に含まれる出力オペランドを earlyclobber オペランドと言うそうで、この問題を回避するためには & を付けてその意図を明示する必要があるそうです。earlyclobber 出力オペランドと入力オペランドはオーバーラップしないことが保証されます。
6.47.3.3 Constraint Modifier Characters
以下のように修正し(asm2.c)
int SetPriorityMask(int threshold) { long pmr, mask; pmr = threshold; asm volatile ("MRS %0, S3_0_C4_C6_0\n\t" /* ICC_PMR_EL1 */ "MSR S3_0_C4_C6_0, %1\n\t" /* ICC_PMR_EL1 */ "ISB" : "=r"(mask) : "r"(pmr)); return (int)mask; }このコードをコンパイルすると、以下のような明らかにおかしい(ローカル変数 pmr と mask が同じレジスタにアロケーションされてしまっている)コードが生成されます。GCC でも Clang でも同じようなコードが出るので、単にコンパイラのバグというわけでも無さそうです。
>gcc -v ... gcc version 6.4.0 (GCC) >gcc -S asm1.c -o gcc_asm1.s >type gcc_asm1.s ... // 5 "asm1.c" 1 MRS x0, S3_0_C4_C6_0 MSR S3_0_C4_C6_0, x0 ISB // 0 "" 2 ... >clang -v clang version 6.0.0 (tags/RELEASE_600/final) >clang -S asm1.c -o clang_asm1.s >type clang_asm1.s ... //APP mrs x8, ICC_PMR_EL1 msr ICC_PMR_EL1, x8 isb //NO_APP ...構文的には何も問題が無く、情報も少ないので非常に悩みました。
解決の手掛かりになったのは以下のバグ報告です。
GNU Arm Embedded Toolchain Incorrect register allocation in inline assembly code
どうも GCC/Clang のインラインアセンブラは、入力オペランドを全て使用してから、出力オペランドに書き込む、と仮定しているようです。
6.47.2 Extended Asm - Assembler Instructions with C Expression Operands
GCC may allocate the output operand in the same register as an unrelated input operand, on the assumption that the assembler code consumes its inputs before producing outputs. This assumption may be false if the assembler code actually consists of more than one instruction. (GCC は、(拡張)アセンブラコードは出力を生成する前に入力オペランドは消費済みであると仮定し、無関係な入力オペランドと同じレジスタを出力オペランドに割り当てるかもしれない。この仮定は(拡張)アセンブラコードが実際には複数命令で構成される場合に偽になるかもしれない。)
今回のパターンのように、入力オペランドを使用する命令の前に実行される命令に含まれる出力オペランドを earlyclobber オペランドと言うそうで、この問題を回避するためには & を付けてその意図を明示する必要があるそうです。earlyclobber 出力オペランドと入力オペランドはオーバーラップしないことが保証されます。
6.47.3.3 Constraint Modifier Characters
以下のように修正し(asm2.c)
... : "=&r"(mask) : "r"(pmr)); ...以下のように期待通りの結果が得られました。
>type gcc_asm2.s ... // 5 "asm2.c" 1 MRS x0, S3_0_C4_C6_0 MSR S3_0_C4_C6_0, x1 ISB // 0 "" 2 ... >type clang_asm2.s ... //APP mrs x9, ICC_PMR_EL1 msr ICC_PMR_EL1, x8 isb //NO_APP ...