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 어셈블리로 변환

ARM 어셈블리와 C++의 인터페이스는 프로그래머에게 다양한 방식으로 도움이 되며 C++가 어셈블리 언어에 정의된 다양한 기능과 변수에 액세스하는 데 도움이 되는 간단한 프로세스이기도 합니다. 이 자습서에서는 C++ 코드 또는 함수를 ARM 어셈블리로 변환하는 방법을 알려줍니다.

프로그래머는 별도의 어셈블리 코드 모듈을 사용하여 C++ 컴파일된 모듈과 연결하여 C++에 포함된 어셈블리 변수 및 인라인 어셈블리를 사용하거나 컴파일러가 생성하는 어셈블리 코드를 수정할 수 있습니다.

가장 중요한 것은 함수에 의해 수정된 전용 레지스터를 보존하고, 인터럽트 루틴이 모든 레지스터를 저장하도록 하고, 함수가 C++ 선언에 따라 값을 올바르게 반환하는지 확인하고, .cinit 섹션을 사용하는 어셈블리 모듈이 없도록 하고, 컴파일러가 할당할 수 있도록 해야 합니다. 이름을 모든 외부 개체에 연결하고 C++를 ARM 어셈블리로 변환하기 전에 어셈블리 수정자에서 C++에서 액세스하거나 호출하는 .def 또는 .global 지시문을 사용하여 모든 개체와 함수를 선언합니다.

C++ 파일에서 C(extern C로 프로토타입화된 함수)를 사용하여 어셈블리 언어에서 호출된 함수를 정의합니다. .bss 섹션에서 변수를 정의하거나 나중에 변환이 필요한 변수를 식별하기 위해 링커 기호를 할당합니다.

GCC 컴파일러를 사용하여 C++를 ARM 어셈블리로 변환

gcc는 실행 중에 C++ 코드에서 중간 출력을 얻을 수 있는 훌륭한 소스입니다. -S 옵션을 사용하여 어셈블러 출력을 가져오는 기능입니다.

-S 옵션은 어셈블러로 보내기 전에 코드를 컴파일한 후 출력을 위한 것입니다.

구문은 gcc –S your_program.cpp이며 이 명령을 선언하기만 하면 ARM 어셈블리로 변환하는 간단한 C++ 프로그램을 작성할 수 있습니다. 가장 간단한 접근 방식 중 하나일 뿐만 아니라 출력이 복잡하고 중급 프로그래머도 이해하기 어렵습니다.

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 컴파일러

컴파일러가 생성하는 C++ 프로그램 내의 어셈블리 파일에 한 줄의 어셈블리 코드 삽입을 위해 일련의 ASM 문 또는 단일 ASM 문을 사용할 수 있습니다. 이러한 어셈블리 문은 개입 코드 없이(코드 중단 없이) 순차적인 코드 줄(어셈블리 코드)을 컴파일러(C++ 컴파일러 출력)에 배치합니다.

단, 삽입된 명령어는 컴파일러에서 확인/분석하지 않으므로 항상 C++ 환경을 유지하십시오. 예측할 수 없는 결과를 생성하고 코드가 생성하는 레지스터 추적 알고리즘을 혼동할 수 있으므로 레이블이나 ump를 C++ 코드에 삽입하지 마십시오.

또한 ASM 문은 어셈블러 지시문을 삽입하기 위한 유효한 선택이 아니며 어셈블리 환경을 변경하지 않고 symdebug:dwarf 명령 또는 -g 명령을 사용할 수 있으며 C++ 환경은 C++ 코드에서 어셈블리 매크로 생성을 방지합니다. 정보를 디버깅합니다.

C++를 ARM 어셈블리로 변환하는 MOD(Assembly-Time Modulus) 함수 생성

ARM 어셈블리에는 MOD 명령이 없으므로 subs로 MOD 함수를 만들고 C++를 ARM 어셈블리로 쉽게 변환할 수 있습니다. ldr reg, =var를 통해 변수의 메모리 주소를 로드해야 하며 변수를 로드하려는 경우 ldr r0, =carry ldr r0과 같이 해당 reg를 사용하여 또 다른 ldr을 수행해야 합니다. , [r0]r0의 메모리 주소에 저장된 값을 로드합니다.

루프가 한 번 또는 두 번만 실행되는 최소 입력을 제외하고 빼기 루프보다 훨씬 빠르기 때문에 sdiv를 사용하십시오.

개념:

;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는 출력 어셈블리를 나타내고 gcc에 이에 대해 알려줍니다. -02는 코드 옵티마이저는 결과에서 디버그 혼란을 줄입니다. -02는 선택 사항입니다. 반면 -march=armv8-a는 필수이며 컴파일하는 동안 ARM v8 대상을 사용하도록 지시합니다.

다음을 포함하여 다양한 버전의 ARM v8을 사용하여 컴파일하는 동안 ARM 대상을 변경할 수 있습니다. armv8-a, armv8.1-a에서 armv8.6-a, armv8-m.base, armv8-m.mainarmv8.1-m.main으로 각각 하나는 약간 다르며 심층 분석을 수행하고 필요에 완벽하게 맞는 것을 선택할 수 있습니다.

명령의 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>입니다. .<minor-version>{.<patch-version>}, 예: module load 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> 명령을 실행합니다. 예: <major-version>.<minor를 입력하여 module load arm21/21.0 -version>{.<패치 버전>} 여기서 <패키지 버전>은 명령의 일부입니다.

armclang -S <source>.cpp 명령을 사용하여 소스 코드를 컴파일하고 <source>.cpp 위치에 소스 파일 이름을 삽입합니다.

ARM 어셈블리 컴파일러는 코드를 벡터화하기 위해 SIMD(Single Instruction Multiple Data) 명령어 및 레지스터를 사용하여 GCC 컴파일러와 다른 작업을 수행합니다.

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 [휘발성](코드); /* 기본 인라인 어셈블리 구문 */ 인라인 어셈블리 문은 _arm 문의 일반 형식을 보여주며 아래 예제 코드에서 찾을 수 있는 인라인 어셈블리 구문의 확장 버전도 있습니다.

어셈블러 명령어에 휘발성 한정자를 사용하면 유익하지만 다음과 같이 컴파일러가 인식하지 못할 수 있는 몇 가지 단점이 있을 수 있습니다. 컴파일러가 코드 블록을 제거하게 할 수 있는 특정 컴파일러 최적화를 비활성화할 가능성.

휘발성 한정자는 선택적이므로 이를 사용하면 -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