Convertir C++ a ensamblaje ARM

Syed Hassan Sabeeh Kazmi 15 febrero 2024
  1. Use el compilador GCC para convertir C++ a ensamblaje ARM
  2. Cree una función MOD (módulo de tiempo de ensamblaje) para convertir C++ a ensamblaje ARM
  3. Utilice el comando arm-linux-gnueabi-gcc para convertir C++ a ensamblaje ARM
  4. Use el comando armclang en ARM Compiler para Linux para convertir C++ a ensamblaje ARM
  5. Utilice la palabra clave __asm para convertir C++ a ensamblaje ARM
Convertir C++ a ensamblaje ARM

La interfaz de C++ con ensamblador ARM sirve a los programadores de muchas maneras, y también es un proceso sencillo que ayuda a C++ a acceder a varias funciones y variables definidas en lenguaje ensamblador y viceversa. Este tutorial le enseñará cómo convertir código o funciones de C++ a ensamblaje ARM.

Los programadores pueden usar módulos de código ensamblador separados para vincularlos con módulos compilados en C++ para usar las variables de ensamblado y el ensamblado en línea incrustado en C++ o modificar el código ensamblador que produce el compilador.

Lo que es más importante, debe conservar todos los registros dedicados modificados por una función, habilitar las rutinas de interrupción para guardar todos los registros, asegurarse de que las funciones devuelvan los valores correctamente de acuerdo con su declaración de C++, ningún módulo ensamblador que use la sección .cinit, permitir que el compilador asigne vincule nombres a todos los objetos externos y declare cada objeto y función con la directiva .def o .global a la que se accede o llama desde C++ en el modificador de ensamblado antes de convertir C++ al ensamblado ARM.

Definir las funciones llamadas desde el lenguaje ensamblador con C (funciones prototipadas como una C externa) en un archivo C++. Defina variables en la sección .bss o asígneles un símbolo de enlace para identificar posteriormente cuál requiere conversión.

Use el compilador GCC para convertir C++ a ensamblaje ARM

El gcc es una gran fuente para obtener resultados intermedios del código C++ durante su ejecución. Es una función que obtiene la salida del ensamblador usando la opción -S.

La opción -S es para la salida después de compilar el código antes de enviarlo al ensamblador.

Su sintaxis es gcc –S your_program.cpp, y puede escribir un programa C++ simple para convertirlo en ensamblador ARM simplemente declarando este comando. Además de ser uno de los enfoques más simples, su salida es compleja y difícil de entender, incluso para programadores de nivel intermedio.

Archivo GNN.cpp:

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

Ejecute este comando en GCC en Microsoft Windows:

gcc –S GNN.cpp

Producción:

compilador gcc

Es posible usar una serie de declaraciones ASM o una sola declaración ASM para una sola línea de inserción de código ensamblador en el archivo ensamblador dentro de su programa C++ que crea el compilador. Estas declaraciones de ensamblaje colocan líneas secuenciales de código (código de ensamblaje) en el compilador (salida del compilador de C++) sin código intermedio (sin interrupciones de código).

Sin embargo, mantenga siempre el entorno C++ porque el compilador no comprueba/analiza las instrucciones insertadas. Siempre evite insertar etiquetas o umps en el código C++, ya que pueden producir resultados impredecibles y confundir los algoritmos de seguimiento de registro que genera el código.

Además, las instrucciones ASM no son una opción válida para insertar directivas de ensamblador, y puede usar el comando symdebug:dwarf o el comando -g sin cambiar el entorno de ensamblaje y evitando la creación de macros de ensamblaje en código C++ porque el entorno C++ información de depuración.

Cree una función MOD (módulo de tiempo de ensamblaje) para convertir C++ a ensamblaje ARM

Como ARM Assembly carece de los comandos MOD, puede crear una función MOD con subs y convertir fácilmente C++ a ARM Assembly. Debe cargar la dirección de memoria de la variable a través de ldr reg, =var, y en caso de que quiera cargar la variable, requiere hacer otro ldr con ese reg como ldr r0, =carry ldr r0, [r0] para cargar el valor almacenado en la dirección de memoria en r0.

Use sdiv porque es mucho más rápido que un ciclo de resta, excepto por entradas mínimas, donde el ciclo solo se ejecuta una o dos veces.

Concepto:

;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;
}

Producción:

modificación

Utilice el comando arm-linux-gnueabi-gcc para convertir C++ a ensamblaje ARM

El comando arm-linux-gnueabi-gcc es una forma perfecta de convertir C++ a ensamblaje ARM para máquinas x86 y x64. Como el gcc no tiene objetivos ARM disponibles, no puede usarlo para sistemas generales, pero solo si está en un sistema ARM donde puede usar el gcc normal en su lugar.

El comando completo arm-linux-gnueabi-gcc -S -O2 -march=armv8-a GNN.cpp es increíblemente fuerte donde -S representa el ensamblado de salida y le dice a gcc sobre él, -02 es un optimizador de código y reduce el desorden de depuración del resultado. El -02 es opcional; por otro lado, el -march=armv8-a es obligatorio y le dice que use el objetivo ARM v8 durante la compilación.

Puede cambiar el destino de ARM mientras compila usando las diferentes versiones de ARM v8, que incluyen; armv8-a, armv8.1-a a armv8.6-a, armv8-m.base, armv8-m.main y armv8.1-m.main donde cada uno es ligeramente diferente, y puede realizar un análisis en profundidad y seleccionar el que mejor se adapte a sus necesidades.

El power.c del comando indica qué archivo compilar, y si no ha especificado un archivo de salida como -o output.asm, el ensamblaje se generará con el nombre de archivo similar power.s.

El arm-linux-gnueabi-gcc es una excelente alternativa a la compilación en una máquina arm que proporciona el ensamblaje de destino o de salida con gcc regular.

El gcc permite a los programadores especificar la arquitectura de destino con -march=xxx, y debe saber identificar el paquete apt de su máquina para seleccionar el correcto.

Archivo 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

Producción :

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

Alternativamente, puede instalar el compilador ARM para Linux cargando el módulo para el compilador ARM ejecutando module load arm<versión-principal>/<versión-paquete> donde <versión-paquete> es <versión-principal> .<versión-menor>{.<versión-parche>}, por ejemplo: module load arm21/21.0.

El comando armclang -S <fuente>.c puede ayudarlo a compilar su fuente C++ y especificar una salida de código ensamblador donde -S representa la salida del código ensamblador y <fuente>.s es el archivo que contendrá el código convertido .

Use el comando armclang en ARM Compiler para Linux para convertir C++ a ensamblaje ARM

Puede producir código ensamblador anotado utilizando el compilador ARM C++, que es el primer paso para aprender cómo el compilador vectoriza los bucles. Un compilador ARM para el sistema operativo Linux es un requisito previo para generar el código ensamblador desde C++.

Después de cargar el módulo para el compilador ARM, ejecute el comando module load arm<versión-principal>/<versión-paquete>, por ejemplo: module load arm21/21.0 poniendo <versión-principal>.<menor -version>{.<versión-parche>} donde <versión-paquete> forma parte del comando.

Compile su código fuente usando el comando armclang -S <fuente>.cpp e inserte el nombre del archivo fuente en la ubicación de <fuente>.cpp.

El compilador de ensamblaje ARM hace algo diferente al compilador GCC, usando instrucciones y registros SIMD (instrucción única, datos múltiples) para vectorizar el código.

Archivo 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

Producción :

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

Utilice la palabra clave __asm para convertir C++ a ensamblaje ARM

Se sabe que es el enfoque más válido, ya que el compilador proporciona un ensamblador en línea para escribir código ensamblador en su código fuente de C++ y le permite acceder a funciones del procesador de destino que no forman parte de C++ ni están disponibles en él.

Utilizando la sintaxis de ensamblaje en línea de GNU, la palabra clave _arm lo ayuda a incorporar o escribir código de ensamblaje en línea en una función.

Sin embargo, no es un buen enfoque para migrar el código ensamblador de sintaxis armasm a la sintaxis GNU, ya que el ensamblador en línea no es compatible con el código ensamblador heredado escrito en la sintaxis ensambladora armasm.

El __asm [volátil] (código); /* Sintaxis de ensamblado en línea básica */ La declaración de ensamblado en línea muestra la forma general de una declaración _arm, y también hay una versión extendida de la sintaxis de ensamblado en línea que encontrará en el código de ejemplo a continuación.

Usar el calificador volátil para las instrucciones del ensamblador es beneficioso, pero puede tener algunos inconvenientes que el compilador podría no conocer, entre ellos; las posibilidades de deshabilitar ciertas optimizaciones del compilador que pueden llevar al compilador a eliminar el bloque de código.

Como el calificador volátil es opcional, usarlo puede garantizar que el compilador no elimine los bloques de código ensamblador al compilar con -01 o superior.

#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);
}

Producción :

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

La palabra clave code en la declaración de ensamblado _arm es la instrucción de ensamblado, y code_template es su plantilla; si solo lo especifica en lugar de código, debe especificar output_operand_list antes de especificar input_operand_list y clobbered_register_list opcionales.

La lista_operando_salida (como lista de operandos de salida) está separada por comas, y cada operando consta de un nombre simbólico entre corchetes con el formato [resultado] "=r" (res).

Puede usar el ensamblado en línea para definir símbolos como __asm (".global __use_no_semihosting\n\t"); o para definir etiquetas usando el signo : después del nombre de la etiqueta como __asm ("my_label:\n\t");.

Además, le permite escribir varias instrucciones dentro de la misma declaración _asm y también le permite escribir un ensamblaje incrustado utilizando la palabra clave __attribute__((naked)).

El compilador de Microsoft C++ (MSVC) puede proporcionar resultados diferentes en la arquitectura ARM que en máquinas o arquitecturas x86 o x64 para el mismo código fuente de C++, y es posible que encuentre muchos problemas de migración o conversión.

Los problemas pueden invocar un comportamiento indefinido, definido por la implementación o no especificado y otros problemas de migración atribuidos a las diferencias de hardware entre las arquitecturas ARM y x86 o x64 que interactúan con el estándar C++ de manera diferente.

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