C++ を ARM アセンブリに変換する

Syed Hassan Sabeeh Kazmi 2024年2月15日
  1. GCC コンパイラを使用して C++ を ARM アセンブリに変換する
  2. C++ を ARM アセンブリに変換する MOD (Assembly-Time Modulus) 関数を作成する
  3. arm-linux-gnueabi-gcc コマンドを使用して C++ を ARM アセンブリに変換する
  4. Linux 用 ARM コンパイラで armclang コマンドを使用して C++ を ARM アセンブリに変換する
  5. __asm キーワードを使用して C++ を ARM アセンブリに変換する
C++ を ARM アセンブリに変換する

C++ と ARM アセンブリのインターフェイスは、多くの点でプログラマーに役立ちます。また、C++ がアセンブリ言語で定義されたさまざまな関数や変数にアクセスするのに役立つ簡単なプロセスでもあり、その逆も同様です。 このチュートリアルでは、C++ コードまたは関数を ARM アセンブリに変換する方法を説明します。

プログラマーは、別のアセンブリ コード モジュールを使用して C++ でコンパイルされたモジュールとリンクし、C++ に埋め込まれたアセンブリ変数とインライン アセンブリを使用したり、コンパイラが生成するアセンブリ コードを変更したりできます。

最も重要なことは、関数によって変更されたすべての専用レジスタを保持すること、割り込みルーチンがすべてのレジスタを保存できるようにすること、関数が C++ 宣言に従って値を正しく返すことを保証すること、.cinit セクションを使用するアセンブリ モジュールを使用しないこと、コンパイラが代入を有効にすることです。 すべての外部オブジェクトに名前をリンクし、C++ を ARM アセンブリに変換する前に、アセンブリ修飾子で C++ からアクセスまたは呼び出される .def または .global ディレクティブを使用してすべてのオブジェクトと関数を宣言します。

C を使用してアセンブリ言語から呼び出される関数 (extern C としてプロトタイプ化された関数) を C++ ファイルで定義します。 .bss セクションで変数を定義するか、リンカー シンボルを割り当てて、変換が必要な変数を後で識別します。

GCC コンパイラを使用して C++ を ARM アセンブリに変換する

gcc は、実行中に C++ コードから中間出力を取得する優れたソースです。 -Sオプションを使用してアセンブラ出力を取得する機能です。

-S オプションは、アセンブラに送信する前にコードをコンパイルした後の出力用です。

その構文は gcc –S your_program.cpp で、このコマンドを宣言するだけで、ARM アセンブリに変換する簡単な C++ プログラムを作成できます。 最も単純なアプローチの 1つであるだけでなく、その出力は複雑で、中級レベルのプログラマーにとっても理解しにくいものです。

GNN.cpp ファイル:

#include <iostream>
using namespace std;
main() {
  int i, u, div;
  i = 2;
  u = 10;
  div = i / u;
  cout << "Answer: " << div << endl;
}

Microsoft Windows の GCC で次のコマンドを実行します。

gcc –S GNN.cpp

出力:

gcc コンパイラ

一連の ASM ステートメントまたは単一の ASM ステートメントを使用して、コンパイラーが作成する C++ プログラム内のアセンブリー・ファイルにアセンブリー・コードを 1 行挿入することができます。 これらのアセンブリ ステートメントは、コードの連続行 (アセンブリ コード) をコンパイラ (C++ コンパイラ出力) に挿入します。

ただし、コンパイラは挿入された命令をチェック/分析しないため、常に C++ 環境を維持してください。 予測できない結果を生成し、コードが生成するレジスタ追跡アルゴリズムを混乱させる可能性があるため、ラベルまたは ump を C++ コードに挿入することは常に避けてください。

さらに、ASM ステートメントは、アセンブラー ディレクティブを挿入するための有効な選択肢ではありません。また、アセンブリ環境を変更せずに、また C++ コードでのアセンブリ マクロの作成を回避することなく、symdebug:dwarf コマンドまたは -g コマンドを使用できます。 情報をデバッグします。

C++ を ARM アセンブリに変換する MOD (Assembly-Time Modulus) 関数を作成する

ARM アセンブリには MOD コマンドがないため、サブルーチンを使用して MOD 関数を作成し、C++ を ARM アセンブリに簡単に変換できます。 ldr reg, =var を介して変数のメモリ アドレスをロードする必要があります。変数をロードする場合は、ldr r0, =carry ldr r0, [r0] を使用して、r0 のメモリ アドレスに格納されている値をロードします。

sdiv を使用すると、ループが 1 回か 2 回しか実行されない最小限の入力を除いて、減算ループよりもはるかに高速になります。

コンセプト:

;Precondition: R0 % R1 is the required computation
;Postcondition: R0 has the result of R0 % R1
              : R2 has R0 / R1

; Example comments for 10 % 7
UDIV R2, R0, R1      ; 1 <- 10 / 7       ; R2 <- R0 / R1
MLS  R0, R1, R2, R0  ; 3 <- 10 - (7 * 1) ; R0 <- R0 - (R1 * R2 )
#include <iostream>
using namespace std;
main() {
  int R0, R1, R2;
  R1 = 7;
  R2 = 1;
  R0 = 10;
  int Sol1, Sol2;
  Sol1 = R2 < -R0 / R1;
  Sol2 = R0 < -R0 - (R1 * R2);

  cout << Sol1 << endl;
  cout << Sol2;
}

出力:

mod

arm-linux-gnueabi-gcc コマンドを使用して C++ を ARM アセンブリに変換する

arm-linux-gnueabi-gcc コマンドは、C++ を x86 および x64 マシン用の ARM アセンブリに変換する完璧な方法です。 gcc には利用可能な ARM ターゲットがないため、一般的なシステムでは使用できませんが、代わりに通常の gcc を使用できる ARM システムを使用している場合に限ります。

完全なコマンド arm-linux-gnueabi-gcc -S -O2 -march=armv8-a GNN.cpp は信じられないほど強力です。ここで、-S は出力アセンブリを表し、-02 はそれについて gcc に伝えます。 コード オプティマイザーと結果からのデバッグ クラッターを削減します。 -02 はオプションです。 一方、-march=armv8-a は必須であり、コンパイル中に ARM v8 ターゲットを使用するように指示します。

ARM v8 のさまざまなバージョンを使用して、コンパイル中に ARM ターゲットを変更できます。 armv8-aarmv8.1-a から armv8.6-aarmv8-m.basearmv8-m.main、および armv8.1-m.main へ 1つはわずかに異なり、詳細な分析を実行して、ニーズに完全に適合するものを選択できます。

コマンドの power.c はコンパイルするファイルを示します。-o output.asm のような出力ファイルを指定していない場合、アセンブリは同様のファイル名 power.s に出力されます。

arm-linux-gnueabi-gcc は、通常の gcc を使用してターゲットまたは出力アセンブリを提供する arm マシンでコンパイルするための優れた代替手段です。

gcc を使用すると、プログラマーは -march=xxx でターゲット アーキテクチャを指定できます。正しいものを選択するには、マシンの apt パッケージを識別する必要があります。

GNN.cpp ファイル:

#include <iostream>
using namespace std;

int power(int x, int y) {
  if (x == 0) {
    return 0;
  } else if (y < 0) {
    return 0;
  } else if (y == 0) {
    return 1;
  } else {
    return x * power(x, y - 1);
  }
}

main() {
  int x, y, sum;
  x = 2;
  y = 10;
  sum = power(x, y);
  cout << sum;
}
arm-linux-gnueabi-gcc -S -O2 -march=armv8-a GNN.cpp

出力:

power(int, int):
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     DWORD PTR [rbp-4], edi
        mov     DWORD PTR [rbp-8], esi
        cmp     DWORD PTR [rbp-4], 0
        jne     .L2
        mov     eax, 0
        jmp     .L3
.L2:
        cmp     DWORD PTR [rbp-8], 0
        jns     .L4
        mov     eax, 0
        jmp     .L3
.L4:
        cmp     DWORD PTR [rbp-8], 0
        jne     .L5
        mov     eax, 1
        jmp     .L3
.L5:
        mov     eax, DWORD PTR [rbp-8]
        lea     edx, [rax-1]
        mov     eax, DWORD PTR [rbp-4]
        mov     esi, edx
        mov     edi, eax
        call    power(int, int)
        imul    eax, DWORD PTR [rbp-4]
.L3:
        leave
        ret
main:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     DWORD PTR [rbp-4], 2
        mov     DWORD PTR [rbp-8], 10
        mov     edx, DWORD PTR [rbp-8]
        mov     eax, DWORD PTR [rbp-4]
        mov     esi, edx
        mov     edi, eax
        call    power(int, int)
        mov     DWORD PTR [rbp-12], eax
        mov     eax, DWORD PTR [rbp-12]
        mov     esi, eax
        mov     edi, OFFSET FLAT:_ZSt4cout
        call    std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
        mov     eax, 0
        leave
        ret
__static_initialization_and_destruction_0(int, int):
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     DWORD PTR [rbp-4], edi
        mov     DWORD PTR [rbp-8], esi
        cmp     DWORD PTR [rbp-4], 1
        jne     .L10
        cmp     DWORD PTR [rbp-8], 65535
        jne     .L10
        mov     edi, OFFSET FLAT:_ZStL8__ioinit
        call    std::ios_base::Init::Init() [complete object constructor]
        mov     edx, OFFSET FLAT:__dso_handle
        mov     esi, OFFSET FLAT:_ZStL8__ioinit
        mov     edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
        call    __cxa_atexit
.L10:
        nop
        leave
        ret
_GLOBAL__sub_I_power(int, int):
        push    rbp
        mov     rbp, rsp
        mov     esi, 65535
        mov     edi, 1
        call    __static_initialization_and_destruction_0(int, int)
        pop     rbp
        ret

または、module load arm<major-version>/<package-version> を実行して ARM コンパイラのモジュールをロードし、Linux 用の ARM コンパイラをインストールすることもできます。ここで、<package-version><major-version> です。 .<マイナー バージョン>{.<パッチ バージョン>}、例: モジュール ロード arm21/21.0

armclang -S <source>.c コマンドは、C++ ソースをコンパイルし、アセンブリ コード出力を指定するのに役立ちます。ここで、-S はアセンブリ コード出力を表し、<source>.s は変換されたコードを含むファイルです。 .

Linux 用 ARM コンパイラで armclang コマンドを使用して C++ を ARM アセンブリに変換する

ARM C++ コンパイラを使用して、注釈付きのアセンブリ コードを生成できます。これは、コンパイラがループをベクトル化する方法を学習するための最初のステップです。 Linux OS 用の ARM コンパイラは、C++ からアセンブリ コードを生成するための前提条件です。

ARMコンパイラのモジュールをロードした後、module load arm<major-version>/<package-version> コマンドを実行してください。例:module load arm21/21.0 のように、<package-version> をコマンドの一部として配置してください。

armclang -S <source>.cpp コマンドを使用してソース コードをコンパイルし、<source>.cpp の場所にソース ファイル名を挿入します。

ARM アセンブリ コンパイラは、GCC コンパイラとは異なる処理を行います。SIMD (Single Instruction Multiple Data) 命令とレジスタを使用してコードをベクトル化します。

GNN.cpp ファイル:

#include <iostream>
using namespace std;

void subtract_arrays(int a, int b, int c) {
  int sum;
  for (int i = 0; i < 5; i++) {
    a = (b + c) - i;
    sum = sum + a;
  }
  cout << sum;
}
int main() {
  int a = 1;
  int b = 2;
  int c = 3;
  subtract_arrays(a, b, c);
}
armclang -O1 -S -o source_O1.s GNN.cpp

出力:

subtract_arrays(int, int, int):
        push    rbp
        mov     rbp, rsp
        sub     rsp, 32
        mov     DWORD PTR [rbp-20], edi
        mov     DWORD PTR [rbp-24], esi
        mov     DWORD PTR [rbp-28], edx
        mov     DWORD PTR [rbp-8], 0
        jmp     .L2
.L3:
        mov     edx, DWORD PTR [rbp-24]
        mov     eax, DWORD PTR [rbp-28]
        add     eax, edx
        sub     eax, DWORD PTR [rbp-8]
        mov     DWORD PTR [rbp-20], eax
        mov     eax, DWORD PTR [rbp-20]
        add     DWORD PTR [rbp-4], eax
        add     DWORD PTR [rbp-8], 1
.L2:
        cmp     DWORD PTR [rbp-8], 4
        jle     .L3
        mov     eax, DWORD PTR [rbp-4]
        mov     esi, eax
        mov     edi, OFFSET FLAT:_ZSt4cout
        call    std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
        nop
        leave
        ret
main:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     DWORD PTR [rbp-4], 1
        mov     DWORD PTR [rbp-8], 2
        mov     DWORD PTR [rbp-12], 3
        mov     edx, DWORD PTR [rbp-12]
        mov     ecx, DWORD PTR [rbp-8]
        mov     eax, DWORD PTR [rbp-4]
        mov     esi, ecx
        mov     edi, eax
        call    subtract_arrays(int, int, int)
        mov     eax, 0
        leave
        ret
__static_initialization_and_destruction_0(int, int):
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     DWORD PTR [rbp-4], edi
        mov     DWORD PTR [rbp-8], esi
        cmp     DWORD PTR [rbp-4], 1
        jne     .L8
        cmp     DWORD PTR [rbp-8], 65535
        jne     .L8
        mov     edi, OFFSET FLAT:_ZStL8__ioinit
        call    std::ios_base::Init::Init() [complete object constructor]
        mov     edx, OFFSET FLAT:__dso_handle
        mov     esi, OFFSET FLAT:_ZStL8__ioinit
        mov     edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
        call    __cxa_atexit
.L8:
        nop
        leave
        ret
_GLOBAL__sub_I_subtract_arrays(int, int, int):
        push    rbp
        mov     rbp, rsp
        mov     esi, 65535
        mov     edi, 1
        call    __static_initialization_and_destruction_0(int, int)
        pop     rbp
        ret

__asm キーワードを使用して C++ を ARM アセンブリに変換する

これは、コンパイラがインライン アセンブラを提供して C++ ソース コードにアセンブリ コードを記述し、C++ の一部ではない、または C++ から利用できないターゲット プロセッサの機能にアクセスできるようにするため、最も有効なアプローチであることが知られています。

GNU インライン アセンブリ構文を使用して、_arm キーワードを使用すると、インライン アセンブリ コードを関数に組み込んだり、関数に記述したりできます。

ただし、インライン アセンブラは armasm アセンブリ構文で記述された従来のアセンブリ コードをサポートしていないため、armasm 構文のアセンブリ コードを GNU 構文に移行するのは適切な方法ではありません。

__asm [volatile] (コード); /* 基本的なインライン アセンブリ構文 */ インライン アセンブリ ステートメントは、_arm ステートメントの一般的な形式を示しています。また、以下のコード例にあるインライン アセンブリ構文の拡張バージョンもあります。

アセンブラー命令に volatile 修飾子 を使用することは有益ですが、コンパイラーが認識しない可能性のあるいくつかの欠点があります。 コンパイラがコード ブロックを削除する可能性がある特定のコンパイラの最適化を無効にする可能性。

volatile 修飾子はオプションであるため、これを使用すると、コンパイラが -01 以上でコンパイルするときにアセンブリ コード ブロックを削除しないようにすることができます。

#include <stdio.h>

int add(int x, int y) {
  int sum = 0;
  __asm("ADD %[_sum], %[input_x], %[input_y]"
        : [_sum] "=r"(sum)
        : [input_x] "r"(x), [input_y] "r"(y));
  return sum;
}

int main(void) {
  int x = 1;
  int y = 2;
  int z = 0;

  z = add(x, y);

  printf("Result of %d + %d = %d\n", x, y, z);
}

出力:

add(int, int):
        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-20], edi
        mov     DWORD PTR [rbp-24], esi
        mov     DWORD PTR [rbp-4], 0
        mov     eax, DWORD PTR [rbp-20]
        mov     edx, DWORD PTR [rbp-24]
        ADD eax, eax, edx
        mov     DWORD PTR [rbp-4], eax
        mov     eax, DWORD PTR [rbp-4]
        pop     rbp
        ret
.LC0:
        .string "Result of %d + %d = %d\n"
main:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     DWORD PTR [rbp-4], 1
        mov     DWORD PTR [rbp-8], 2
        mov     DWORD PTR [rbp-12], 0
        mov     edx, DWORD PTR [rbp-8]
        mov     eax, DWORD PTR [rbp-4]
        mov     esi, edx
        mov     edi, eax
        call    add(int, int)
        mov     DWORD PTR [rbp-12], eax
        mov     ecx, DWORD PTR [rbp-12]
        mov     edx, DWORD PTR [rbp-8]
        mov     eax, DWORD PTR [rbp-4]
        mov     esi, eax
        mov     edi, OFFSET FLAT:.LC0
        mov     eax, 0
        call    printf
        mov     eax, 0
        leave
        ret

_arm アセンブリ ステートメントの code キーワードはアセンブリ命令であり、code_template はそのテンプレートです。 code ではなくそれのみを指定する場合は、オプションの input_operand_listclobbered_register_list を指定する前に output_operand_list を指定する必要があります。

output_operand_list (出力オペランド リストとして) はコンマで区切られ、各オペランドは、[result] "=r" (res) 形式の角括弧で囲まれた記号名で構成されます。

インライン アセンブリを使用して、__asm (".global __use_no_semihosting\n\t"); のようなシンボルを定義できます。 または、__asm ("my_label:\n\t"); のように、ラベル名の後に : 記号を使用してラベルを定義します。

さらに、同じ _asm ステートメント内に複数の命令を記述でき、__attribute__((naked)) キーワードを使用して組み込みアセンブリを記述できます。

Microsoft C++ コンパイラ (MSVC) は、同じ C++ ソース コードの x86 または x64 マシンまたはアーキテクチャとは異なる結果を ARM アーキテクチャで提供する可能性があり、多くの移行または変換の問題が発生する可能性があります。

この問題は、C++ 標準との相互作用が異なる ARM と x86 または x64 アーキテクチャ間のハードウェアの違いに起因する、未定義、実装定義、または未指定の動作やその他の移行の問題を引き起こす可能性があります。

Syed Hassan Sabeeh Kazmi avatar Syed Hassan Sabeeh Kazmi avatar

Hassan is a Software Engineer with a well-developed set of programming skills. He uses his knowledge and writing capabilities to produce interesting-to-read technical articles.

GitHub