GNU/Linux >> Tutoriales Linux >  >> Linux

¿Qué partes de este código ensamblador de HelloWorld son esenciales si tuviera que escribir el programa en ensamblador?

El mínimo absoluto que funcionará en la plataforma que parece ser es

        .globl main
main:
        pushl   $.LC0
        call    puts
        addl    $4, %esp
        xorl    %eax, %eax
        ret
.LC0:
        .string "Hello world"

Pero esto rompe una serie de requisitos de ABI. El mínimo para un programa compatible con ABI es

        .globl  main
        .type   main, @function
main:
        subl    $24, %esp
        pushl   $.LC0
        call    puts
        xorl    %eax, %eax
        addl    $28, %esp
        ret
        .size main, .-main
        .section .rodata
.LC0:
        .string "Hello world"

Todo lo demás en su archivo de objeto es el compilador que no optimiza el código lo más ajustado posible u opcional anotaciones que se escribirán en el archivo de objeto.

El .cfi_* las directivas, en particular, son anotaciones opcionales. Son necesarios si y solo si la función puede estar en la pila de llamadas cuando se lanza una excepción de C++, pero son útiles en cualquier programa del que desee extraer un seguimiento de la pila. Si va a escribir código no trivial a mano en lenguaje ensamblador, probablemente valga la pena aprender a escribirlos. Desafortunadamente, están muy mal documentados; Actualmente no encuentro nada a lo que creo que valga la pena vincular.

La linea

.section    .note.GNU-stack,"",@progbits

también es importante saber si está escribiendo lenguaje ensamblador a mano; es otra anotación opcional, pero valiosa, porque lo que significa es "nada en este archivo de objeto requiere que la pila sea ejecutable". Si todos los archivos de objeto en un programa tienen esta anotación, el kernel no hará que la pila sea ejecutable, lo que mejora un poco la seguridad.

(Para indicar que usted hace necesitas que la pila sea ejecutable, pones "x" en lugar de "" . GCC puede hacer esto si usa su extensión de "función anidada". (No hagas eso.))

Probablemente valga la pena mencionar que en la sintaxis de ensamblaje "AT&T" utilizada (por defecto) por GCC y GNU binutils, hay tres tipos de líneas:Una línea con un solo token, que termina en dos puntos, es una etiqueta. (No recuerdo las reglas sobre qué caracteres pueden aparecer en las etiquetas). Una línea cuyo primero token comienza con un punto y no terminan en dos puntos, es una especie de directiva para el ensamblador. Cualquier otra cosa es una instrucción de montaje.


relacionado:¿Cómo eliminar el "ruido" de la salida del ensamblado GCC/clang? El .cfi Las directivas no son directamente útiles para usted y el programa funcionaría sin ellas. (Se necesita información de desbloqueo de pila para el manejo de excepciones y rastreos, por lo que -fomit-frame-pointer se puede habilitar de forma predeterminada. Y sí, gcc emite esto incluso para C.)

En cuanto a la cantidad de líneas fuente de ASM necesarias para producir un programa Hello World de valor, obviamente queremos usar las funciones de libc para hacer más trabajo por nosotros.

La respuesta de @ Zwol tiene la implementación más corta de su código C original.

Esto es lo que podrías hacer a mano , si no le importa el estado de salida de su programa, simplemente imprima su cadena.

# Hand-optimized asm, not compiler output
    .globl main            # necessary for the linker to see this symbol
main:
    # main gets two args: argv and argc, so we know we can modify 8 bytes above our return address.
    movl    $.LC0, 4(%esp)     # replace our first arg with the string
    jmp     puts               # tail-call puts.

# you would normally put the string in .rodata, not leave it in .text where the linker will mix it with other functions.
.section .rodata
.LC0:
    .asciz "Hello world"     # asciz zero-terminates

La C equivalente (acabas de pedir el Hello World más corto, no uno que tenga la misma semántica):

int main(int argc, char **argv) {
    return puts("Hello world");
}

Su estado de salida está definido por la implementación, pero definitivamente se imprime. puts(3) devuelve "un número no negativo", que podría estar fuera del rango de 0 a 255, por lo que no podemos decir nada acerca de que el estado de salida del programa sea 0/distinto de cero en Linux (donde el estado de salida del proceso es el 8 bajo). bits del entero pasado al exit_group() llamada al sistema (en este caso por el código de inicio CRT que llamó a main()).

El uso de JMP para implementar la llamada final es una práctica estándar y se usa comúnmente cuando una función no necesita hacer nada después de que regresa otra función. puts() eventualmente regresará a la función que llamó a main() , como si puts() hubiera regresado a main() y luego main() hubiera regresado. La persona que llama a main() todavía tiene que lidiar con los argumentos que puso en la pila para main(), porque todavía están allí (pero modificados, y podemos hacerlo).

gcc y clang no generan código que modifique el espacio de paso de argumentos en la pila. Sin embargo, es perfectamente seguro y compatible con ABI:las funciones "poseen" sus argumentos en la pila, incluso si fueran const . Si llama a una función, no puede asumir que los argumentos que colocó en la pila todavía están allí. Para realizar otra llamada con argumentos iguales o similares, debe almacenarlos todos nuevamente.

También tenga en cuenta que esto llama a puts() con la misma alineación de pila que teníamos en la entrada a main() , por lo que nuevamente cumplimos con ABI al preservar la alineación 16B requerida por la versión moderna de x86-32, también conocido como i386 System V ABI (usado por Linux).

.string cadenas terminadas en cero, igual que .asciz , pero tuve que buscarlo para comprobarlo. Recomiendo simplemente usar .ascii o .asciz para asegurarse de tener claro si sus datos tienen un byte de terminación o no. (No necesita uno si lo usa con funciones de longitud explícita como write() )

En x86-64 System V ABI (y Windows), los argumentos se pasan en registros. Esto hace que la optimización de llamadas de cola sea mucho más fácil, porque puede reorganizar argumentos o pasar más args (siempre y cuando no se quede sin registros). Esto hace que los compiladores estén dispuestos a hacerlo en la práctica. (Porque, como dije, actualmente no les gusta generar código que modifique el espacio de argumentos entrantes en la pila, a pesar de que la ABI tiene claro que pueden hacerlo, y las funciones generadas por el compilador asumen que los llamados golpean sus argumentos de pila .)

clang o gcc -O3 harán esta optimización para x86-64, como puede ver en el explorador del compilador Godbolt :

#include <stdio.h>
int main() { return puts("Hello World"); }

# clang -O3 output
main:                               # @main
    movl    $.L.str, %edi
    jmp     puts                    # TAILCALL

 # Godbolt strips out comment-only lines and directives; there's actually a .section .rodata before this
.L.str:
    .asciz  "Hello World"

Las direcciones de datos estáticos siempre caben en los 31 bits bajos del espacio de direcciones, y el ejecutable no necesita un código independiente de la posición, de lo contrario, el mov seria lea .LC0(%rip), %rdi . (Obtendrá esto de gcc si se configuró con --enable-default-pie para hacer ejecutables independientes de la posición.)

Cómo cargar la dirección de la función o etiqueta en el registro en GNU Assembler

Hola mundo con Linux x86 de 32 bits int 0x80 llamadas al sistema directamente, sin libc

¿Ver Hello, world en lenguaje ensamblador con llamadas al sistema Linux? Mi respuesta allí se escribió originalmente para SO Docs, luego se trasladó aquí como un lugar para colocarla cuando SO Docs cerró. Realmente no pertenecía aquí, así que lo moví a otra pregunta.

relacionado:Un tutorial torbellino sobre la creación de ejecutables ELF realmente pequeños para Linux. El archivo binario más pequeño que puede ejecutar que solo hace una llamada al sistema exit(). Se trata de minimizar el tamaño binario, no el tamaño de la fuente o incluso solo la cantidad de instrucciones que realmente se ejecutan.


Linux
  1. MySQL vs. MariaDB:¿Cuáles son las principales diferencias entre ellos?

  2. ¿Qué son los separadores de palabras de Readline?

  3. ¿Cuáles son las principales diferencias entre Bsd y Gnu/linux Userland?

  4. Linux:¿cuáles son las principales diferencias entre los sistemas operativos basados ​​en Bsd y Linux?

  5. Fedora vs Ubuntu:¿Cuáles son las diferencias clave?

¿Qué es Intel SGX y cuáles son los beneficios?

¿Cuáles son los tipos de servidores DNS?

useradd vs adduser:¿Cuáles son las diferencias?

¿Puede GDB cambiar el código ensamblador de un programa en ejecución?

¿Cuáles son los programas CLI estándar para administrar usuarios y grupos?

¿Cuáles son los beneficios del Administrador de volúmenes lógicos?