C++ を ARM アセンブリに変換する
- GCC コンパイラを使用して C++ を ARM アセンブリに変換する
- C++ を ARM アセンブリに変換する MOD (Assembly-Time Modulus) 関数を作成する
-
arm-linux-gnueabi-gcc
コマンドを使用して C++ を ARM アセンブリに変換する -
Linux 用 ARM コンパイラで
armclang
コマンドを使用して C++ を ARM アセンブリに変換する -
__asm
キーワードを使用して 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
出力:
一連の 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;
}
出力:
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-a
、armv8.1-a
から armv8.6-a
、armv8-m.base
、armv8-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_list
と clobbered_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 アーキテクチャ間のハードウェアの違いに起因する、未定義、実装定義、または未指定の動作やその他の移行の問題を引き起こす可能性があります。
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