Quelle est la structure d'un cadre de pile et comment est-il utilisé lors de l'appel de fonctions dans l'assembly?
Le cadre de pile x86-32 est créé en exécutant
function_start:
Push ebp
mov ebp, esp
il est donc accessible via ebp et ressemble
ebp+00 (current_frame) : prev_frame
ebp+04 : return_address
....
prev_frame : prev_prev_frame
prev_frame+04 : prev_return_address
Il y a certains avantages à utiliser ebp pour les cadres de pile par la conception des instructions d'assemblage, donc les arguments et les sections locales sont généralement accessibles à l'aide du registre ebp.
Chaque routine utilise une partie de la pile, et nous l'appelons un cadre de pile. Bien qu'un programmeur assembleur ne soit pas obligé de suivre le style suivant, il est fortement recommandé comme bonne pratique.
La trame de pile pour chaque routine est divisée en trois parties: paramètres de fonction, pointeur arrière sur la trame de pile précédente et variables locales.
Partie 1: Paramètres de fonction
Cette partie de la trame de pile d'une routine est configurée par l'appelant. En utilisant l'instruction "Push", l'appelant pousse les paramètres sur la pile. Différentes langues peuvent pousser les paramètres dans des ordres différents. C, si je me souviens bien, les pousse de droite à gauche. Autrement dit, 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, la pile se développe. C'est-à-dire que le registre de pointeur de pile est décrémenté de quatre (4) octets (en mode 32 bits), et l'élément est copié à l'emplacement de mémoire pointé par le registre de pointeur de pile. Notez que l'instruction 'call' poussera implicitement l'adresse de retour sur la pile. Le nettoyage des paramètres sera abordé dans la partie 5.
Partie 2: pointeur arrière Stackframe
À ce stade, l'instruction "appel" 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, cela peut devenir maladroit après que nous ayons créé de l'espace pour les variables locales et d'autres choses. Donc, nous utilisons un registre de pointeur de pile en plus du registre de pointeur de pile. Cependant, nous voulons que le registre stackbase-pointer soit défini sur notre trame actuelle, et non sur la fonction précédente. Ainsi, nous enregistrons l'ancien sur la pile (qui modifie les décalages des paramètres sur la pile), puis copions le registre de pointeur de pile actuel dans le registre de pointeur de base de pile.
Push ebp ; save previous stackbase-pointer register
mov ebp, esp ; ebp = esp
Parfois, cela peut se faire en utilisant uniquement l'instruction "ENTER".
Partie 3: Sculpture d'espace pour les variables locales
Les variables locales sont stockées sur la pile. Puisque la pile se développe, nous soustrayons quelques # d'octets (assez pour stocker nos variables locales):
sub esp, n_bytes ; n_bytes = number of bytes required for local variables
Partie 4: Mettre tout cela ensemble. Les paramètres sont accessibles à l'aide du 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 à l'aide du registre de pointeur de pile ...
[esp + (# - 4)] - top of local variables section
[esp + 0] - bottom of local variables section
Partie 5: Nettoyage de Stackframe
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, vous pouvez voir l'instruction "LAISSER" remplacer ces deux instructions.
Selon la langue que vous utilisiez, vous pouvez voir l'une des deux formes de l'instruction "RET".
ret
ret <some #>
Celui qui sera choisi dépendra du choix de la langue (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 (avec l'exemple foo (a, b, c), il le fera via ... add esp, 12) et c'est la façon dont 'C' le fait il. Le deuxième cas indique que l'instruction de retour va faire sauter # mots (ou # octets, je ne me souviens plus lesquels) de la pile quand elle revient, 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.
Ceci est différent selon le système d'exploitation et la langue utilisés. 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 de l'exécution d'un sous-programme de saut. Lors de l'exécution d'un sous-programme de retour, l'adresse est récupérée de la pile et placée dans le compteur de programmes (emplacement de mémoire d'où la prochaine instruction d'exécution du processeur doit être extraite)
Vous devrez consulter votre documentation pour le compilateur que vous utilisez.
Le cadre de pile x86 peut être utilisé par les compilateurs (selon le compilateur) pour passer des paramètres (ou des pointeurs vers des paramètres) et renvoyer des valeurs. Voir ceci