GNU/Linux >> Tutoriels Linux >  >> Linux

Quelles parties de ce code assembleur HelloWorld sont essentielles si je devais écrire le programme en assembleur ?

Le strict minimum absolu qui fonctionnera sur la plate-forme que cela semble être, est

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

Mais cela enfreint un certain nombre d'exigences ABI. Le minimum pour un programme compatible ABI est

        .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"

Tout le reste dans votre fichier objet est soit le compilateur n'optimisant pas le code aussi étroitement que possible, soit facultatif annotations à écrire dans le fichier objet.

Le .cfi_* les directives, en particulier, sont des annotations facultatives. Ils sont nécessaires si et seulement si la fonction peut être sur la pile des appels lorsqu'une exception C++ est levée, mais elles sont utiles dans n'importe quel programme à partir duquel vous pourriez vouloir extraire une trace de pile. Si vous allez écrire du code non trivial à la main en langage d'assemblage, cela vaudra probablement la peine d'apprendre à les écrire. Malheureusement, ils sont très mal documentés; Je ne trouve actuellement rien qui mérite d'être lié.

La ligne

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

est également important de savoir si vous écrivez le langage d'assemblage à la main ; c'est une autre annotation facultative, mais précieuse, car cela signifie que "rien dans ce fichier objet n'exige que la pile soit exécutable". Si tous les fichiers objets d'un programme ont cette annotation, le noyau ne rendra pas la pile exécutable, ce qui améliore un peu la sécurité.

(Pour indiquer que vous faites besoin que la pile soit exécutable, vous mettez "x" au lieu de "" . GCC peut le faire si vous utilisez son extension "fonction imbriquée". (Ne faites pas ça.))

Il est probablement utile de mentionner que dans la syntaxe d'assemblage "AT&T" utilisée (par défaut) par GCC et les binutils GNU, il existe trois types de lignes :Une ligne avec un seul jeton dessus, se terminant par deux-points, est une étiquette. (Je ne me souviens pas des règles régissant les caractères pouvant apparaître dans les étiquettes.) Une ligne dont le premier le jeton commence par un point et ne le fait pas se termine par deux-points, est une sorte de directive pour l'assembleur. Tout le reste est une instruction de montage.


connexe :comment supprimer le "bruit" de la sortie de l'assemblage GCC/clang ? Le .cfi les directives ne vous sont pas directement utiles et le programme fonctionnerait sans elles. (Ce sont des informations de déroulement de pile nécessaires pour la gestion des exceptions et les backtraces, donc -fomit-frame-pointer peut être activé par défaut. Et oui, gcc émet cela même pour C.)

En ce qui concerne le nombre de lignes source asm nécessaires pour produire un programme Hello World de valeur, nous voulons évidemment utiliser les fonctions libc pour faire plus de travail pour nous.

La réponse de @ Zwol a la mise en œuvre la plus courte de votre code C d'origine.

Voici ce que vous pourriez faire à la main , si vous ne vous souciez pas de l'état de sortie de votre programme, juste qu'il imprime votre chaîne.

# 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

L'équivalent C (vous venez de demander le Hello World le plus court, pas celui qui avait une sémantique identique) :

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

Son état de sortie est défini par l'implémentation mais il s'imprime définitivement. puts(3) renvoie "un nombre non négatif", qui pourrait être en dehors de la plage 0..255, nous ne pouvons donc rien dire sur le statut de sortie du programme étant 0 / non nul sous Linux (où le statut de sortie du processus est le bas 8 bits de l'entier passé au exit_group() appel système (dans ce cas par le code de démarrage du CRT qui a appelé main()).

L'utilisation de JMP pour implémenter l'appel de queue est une pratique standard, et couramment utilisée lorsqu'une fonction n'a rien à faire après le retour d'une autre fonction. puts() reviendra éventuellement à la fonction qui a appelé main() , comme si puts() était revenu à main() puis main() était revenu. L'appelant de main() doit toujours gérer les arguments qu'il a mis sur la pile pour main(), car ils sont toujours là (mais modifiés, et nous sommes autorisés à le faire).

gcc et clang ne génèrent pas de code qui modifie l'espace de passage d'arguments sur la pile. Il est cependant parfaitement sûr et conforme à l'ABI :les fonctions "possèdent" leurs arguments sur la pile, même s'ils étaient const . Si vous appelez une fonction, vous ne pouvez pas supposer que les arguments que vous mettez sur la pile sont toujours là. Pour passer un autre appel avec les mêmes arguments ou des arguments similaires, vous devez tous les stocker à nouveau.

Notez également que cela appelle puts() avec le même alignement de pile que nous avions à l'entrée de main() , donc encore une fois, nous sommes conformes à l'ABI en préservant l'alignement 16B requis par la version moderne de l'ABI x86-32 alias i386 System V (utilisé par Linux).

.string zéro termine les chaînes, comme .asciz , mais j'ai dû chercher pour vérifier. Je recommanderais simplement d'utiliser .ascii ou .asciz pour vous assurer que vous savez clairement si vos données ont un octet de fin ou non. (Vous n'en avez pas besoin si vous l'utilisez avec des fonctions de longueur explicite comme write() )

Dans l'ABI x86-64 System V (et Windows), les arguments sont passés dans des registres. Cela rend l'optimisation des appels de fin beaucoup plus facile, car vous pouvez réorganiser les arguments ou en passer plus args (tant que vous ne manquez pas de registres). Cela rend les compilateurs prêts à le faire dans la pratique. (Parce que, comme je l'ai dit, ils n'aiment pas actuellement générer du code qui modifie l'espace arg entrant sur la pile, même si l'ABI est clair qu'ils sont autorisés à le faire, et les fonctions générées par le compilateur supposent que les appelés encombrent leur pile args .)

clang ou gcc -O3 effectuera cette optimisation pour x86-64, comme vous pouvez le voir sur l'explorateur du compilateur 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"

Les adresses de données statiques tiennent toujours dans les 31 bits inférieurs de l'espace d'adressage, et l'exécutable n'a pas besoin de code indépendant de la position, sinon le mov serait lea .LC0(%rip), %rdi . (Vous obtiendrez ceci de gcc s'il a été configuré avec --enable-default-pie pour créer des exécutables indépendants de la position.)

Comment charger l'adresse de la fonction ou de l'étiquette dans le registre de GNU Assembler

Hello World avec Linux x86 32 bits int 0x80 appels système directement, pas de libc

Voir Bonjour, le monde en langage assembleur avec des appels système Linux ? Ma réponse a été écrite à l'origine pour SO Docs, puis déplacée ici pour la mettre lorsque SO Docs a fermé. Cela n'avait pas vraiment sa place ici, alors je l'ai déplacé vers une autre question.

connexe:Un didacticiel Whirlwind sur la création d'exécutables ELF vraiment Teensy pour Linux. Le plus petit fichier binaire que vous pouvez exécuter qui fait juste un appel système exit(). Il s'agit de minimiser la taille binaire, pas la taille de la source ou même simplement le nombre d'instructions qui s'exécutent réellement.


Linux
  1. MySQL vs MariaDB :quelles sont les principales différences entre eux

  2. Quelles sont les principales différences entre Bsd et Gnu/linux Userland ?

  3. Linux - Quelles sont les principales différences entre les systèmes d'exploitation basés sur Bsd et Linux ?

  4. Linux - Parties propriétaires ou fermées du noyau ?

  5. Quels sont les avantages de CloudLinux ?

Qu'est-ce qu'Intel SGX et quels en sont les avantages ?

Quels sont les différents types de shells sous Linux ?

useradd vs adduser :quelles sont les différences ?

GDB peut-il modifier le code assembleur d'un programme en cours d'exécution ?

Qu'est-ce qui est vulnérable dans ce code C ?

Quels sont les avantages du gestionnaire de volumes logiques