Quelle est la structure d'une trame de pile et comment est-elle utilisée lors de l'appel de fonctions en assembleur ?
Réponses
Trop de publicités?Chaque routine utilise une partie de la pile, et nous l'appelons une trame de pile. Bien qu'un programmeur assembleur ne soit pas obligé de suivre le style suivant, il est fortement recommandé comme bonne pratique.
Le cadre de pile de chaque routine est divisé en trois parties : les paramètres de la fonction, le pointeur arrière vers le cadre de pile précédent et les variables locales.
Partie 1 : Paramètres des fonctions
Cette partie de la pile d'une routine est mise en place par l'appelant. À l'aide de l'instruction "push", l'appelant place les paramètres sur la pile. Selon les langues, les paramètres peuvent être placés dans un ordre différent. Le C, si je me souviens bien, les pousse de droite à gauche. C'est-à-dire que si vous appelez ...
foo (a, b, c);
L'appelant le convertira en ...
push c
push b
push a
call foo
Au fur et à mesure que chaque élément est poussé sur la pile, celle-ci s'allonge. C'est-à-dire que le registre stack-pointer est décrémenté de quatre (4) octets (en mode 32 bits), et l'élément est copié à l'emplacement mémoire pointé par le registre stack-pointer. Notez que l'instruction 'call' pousse implicitement l'adresse de retour sur la pile. Le nettoyage des paramètres sera abordé dans la partie 5.
Partie 2 : Pointeur arrière du cadre de pile
À ce stade, l'instruction "call" a été émise et nous sommes maintenant au début de la routine appelée. Si nous voulons accéder à nos paramètres, nous pouvons y accéder comme ...
[esp + 0] - return address
[esp + 4] - parameter 'a'
[esp + 8] - parameter 'b'
[esp + 12] - parameter 'c'
Cependant, cette méthode peut s'avérer maladroite une fois que l'on a fait de la place pour les variables locales et autres. Nous utilisons donc un registre stackbase-pointer en plus du registre stack-pointer. Cependant, nous voulons que le registre stackbase-pointer soit défini sur l'image actuelle, et non sur la fonction précédente. Ainsi, nous sauvegardons l'ancienne sur la pile (ce qui modifie les offsets des paramètres sur la pile) puis nous copions le registre stack-pointer actuel dans le registre stackbase-pointer.
push ebp ; save previous stackbase-pointer register
mov ebp, esp ; ebp = esp
Il arrive parfois que l'on utilise uniquement l'instruction 'ENTER'.
Partie 3 : Créer de l'espace pour les variables locales
Les variables locales sont stockées sur la pile. Comme la pile s'agrandit, nous soustrayons un certain nombre d'octets (assez pour stocker nos variables locales) :
sub esp, n_bytes ; n_bytes = number of bytes required for local variables
Partie 4 : Tout mettre en place. Les paramètres sont accessibles en utilisant le registre stackbase-pointer ...
[ebp + 16] - parameter 'c'
[ebp + 12] - parameter 'b'
[ebp + 8] - parameter 'a'
[ebp + 4] - return address
[ebp + 0] - saved stackbase-pointer register
Les variables locales sont accessibles en utilisant le registre stack-pointer ...
[esp + (# - 4)] - top of local variables section
[esp + 0] - bottom of local variables section
Partie 5 : Nettoyage du cadre de pile
Lorsque nous quittons la routine, le cadre de la pile doit être nettoyé.
mov esp, ebp ; undo the carving of space for the local variables
pop ebp ; restore the previous stackbase-pointer register
Parfois, l'instruction "LEAVE" peut remplacer ces deux instructions.
Selon la langue que vous utilisez, vous pouvez voir l'une des deux formes de l'instruction "RET".
ret
ret <some #>
Le choix de l'un ou l'autre dépendra du choix du langage (ou du style que vous souhaitez suivre si vous écrivez en assembleur). Le premier cas indique que l'appelant est responsable de la suppression des paramètres de la pile (dans l'exemple foo(a,b,c), il le fera via ... add esp, 12) et c'est la façon dont le 'C' le fait. Le second cas indique que l'instruction de retour fera sortir # mots (ou # octets, je ne me souviens plus) de la pile lorsqu'elle retournera, supprimant ainsi les paramètres de la pile. Si je me souviens bien, c'est le style utilisé par Pascal.
C'est long, mais j'espère que cela vous aidera à mieux comprendre les stackframes.
La trame de la pile x86-32 est créée en exécutant
function_start:
push ebp
mov ebp, esp
donc il est accessible par ebp et ressemble à ceci
ebp+00 (current_frame) : prev_frame
ebp+04 : return_address
....
prev_frame : prev_prev_frame
prev_frame+04 : prev_return_address
Il y a quelques avantages à utiliser ebp pour les trames de pile par la conception des instructions d'assemblage, ainsi les arguments et les locals sont généralement accessibles en utilisant le registre ebp.
Celle-ci est différente selon le système d'exploitation et la langue utilisée. Parce qu'il n'y a pas de format général pour la pile dans ASM, la seule chose que la pile fait dans ASM est de stocker l'adresse de retour lors d'un saut de sous-routine. Lors de l'exécution d'un retour de sous-routine, l'adresse est récupérée de la pile et placée dans le compteur de programme (emplacement mémoire où l'instruction suivante du CPU doit être exécutée).
Vous devrez consulter la documentation du compilateur que vous utilisez.