web-dev-qa-db-fra.com

Comment fonctionne la pile en langage assembleur?

J'essaie actuellement de comprendre comment fonctionne la pile, j'ai donc décidé de m'apprendre langage d'assemblage , j'utilise ce livre:

http://savannah.nongnu.org/projects/pgubook/

J'utilise Gas et je fais mon développement sur Linux Mint .

Je suis un peu confus par quelque chose:

Pour autant que je sache, une pile est simplement une structure de données. J'ai donc supposé que si je codais dans Assembly, je devrais implémenter la pile moi-même. Cependant, cela ne semble pas être le cas car il existe des commandes comme

pushl
popl

Ainsi, lors du codage dans Assembly pour l'architecture x86 et en utilisant la syntaxe Gas: la pile n'est-elle qu'une structure de données déjà implémentée? Ou est-il réellement implémenté au niveau matériel? Ou s'agit-il d'autre chose? De plus, la plupart des langages d'assemblage pour d'autres jeux de puces ont-ils déjà mis en place la pile?

Je sais que c'est un peu une question stupide, mais je suis en fait assez confus par cela.

70
bplus

Je pense que vous vous confondez principalement entre un program's stack et any old stack.

Une pile

Est une structure de données abstraite qui se compose d'informations dans un système Last In First Out. Vous placez des objets arbitraires sur la pile, puis vous les retirez à nouveau, un peu comme un bac d'entrée/sortie, l'élément supérieur est toujours celui qui est retiré et vous placez toujours le dessus.

Une pile de programmes

Est une pile, c'est une section de mémoire qui est utilisée pendant l'exécution, elle a généralement une taille statique par programme et est fréquemment utilisée pour stocker des paramètres de fonction. Vous poussez les paramètres sur la pile lorsque vous appelez une fonction et la fonction adresse directement la pile ou fait disparaître les variables de la pile.

Une pile de programmes n'est généralement pas matérielle (bien qu'elle soit conservée en mémoire afin de pouvoir être argumentée comme telle), mais le pointeur de pile qui pointe vers une zone actuelle de la pile est généralement un registre CPU. Cela le rend un peu plus flexible qu'une pile LIFO car vous pouvez changer le point auquel la pile s'adresse.

Vous devriez lire et vous assurer que vous comprenez l'article wikipedia car il donne une bonne description de la pile de matériel, ce à quoi vous avez affaire.

Il y a aussi ce tutoriel qui explique la pile en termes des anciens registres 16 bits mais pourrait être utile et n autre spécifiquement sur la pile.

De Nils Pipenbrinck:

Il est à noter que certains processeurs n'implémentent pas toutes les instructions pour accéder et manipuler la pile (Push, pop, pointeur de pile, etc.) mais le x86 le fait en raison de sa fréquence d'utilisation. Dans ces situations, si vous vouliez une pile, vous devrez l'implémenter vous-même (certains MIPS et certains ARM processeurs sont créés sans piles).

Par exemple, dans les MIP, une instruction Push serait implémentée comme:

addi $sp, $sp, -4  # Decrement stack pointer by 4  
sw   $t0, ($sp)   # Save $t0 to stack  

et une instruction Pop ressemblerait à:

lw   $t0, ($sp)   # Copy from stack to $t0  
addi $sp, $sp, 4   # Increment stack pointer by 4  
73
Henry B

(J'ai fait un Gist de tout le code dans cette réponse au cas où vous voudriez jouer avec)

Je n'ai jamais fait la plupart des choses de base en asm pendant mon cours CS101 en 2003. Et je n'avais jamais vraiment "compris" comment asm et stack fonctionnent jusqu'à ce que je réalise que tout est fondamentalement comme la programmation en C ou C++ ... mais sans variables, paramètres et fonctions locaux. Cela ne semble probablement pas encore facile :) Permettez-moi de vous montrer (pour asm x86 avec syntaxe Intel ).


1. Quelle est la pile

La pile est un morceau de mémoire contigu alloué à chaque thread au démarrage. Vous pouvez y stocker tout ce que vous voulez. Dans le langage C++ ( extrait de code # 1):

const int STACK_CAPACITY = 1000;
thread_local int stack[STACK_CAPACITY];

2. Haut et bas de la pile

En principe, vous pouvez stocker des valeurs dans des cellules aléatoires du tableau stack ( snippet # 2.1):

cin >> stack[333];
cin >> stack[517];
stack[555] = stack[333] + stack[517];

Mais imaginez à quel point il serait difficile de se rappeler quelles cellules de stack sont déjà utilisées et lesquelles sont "libres". C'est pourquoi nous stockons les nouvelles valeurs sur la pile côte à côte.

Une chose étrange à propos de la pile (x86) asm est que vous y ajoutez des éléments en commençant par le dernier index et que vous vous déplacez vers des index inférieurs: pile [999], puis pile [998] et ainsi de suite ( extrait # 2.2):

cin >> stack[999];
cin >> stack[998];
stack[997] = stack[999] + stack[998];

Et encore (coution, vous allez être confus maintenant) le nom "officiel" de stack[999] Est en bas de la pile .
La dernière cellule utilisée (stack[997] Dans l'exemple ci-dessus) est appelée en haut de la pile (voir Où le haut de la pile est sur x86 ).


3. Pointeur de pile (SP)

La pile n'est pas la seule chose visible partout dans votre code asm. Vous pouvez également manipuler les registres CPU (voir Registres à usage général ). Ce sont vraiment des variables globales:

int AX, BX, SP, BP, ...;
int main(){...}

Il y a un registre CPU dédié (SP) pour garder une trace du dernier élément ajouté à la pile. Comme son nom l'indique, c'est bien un pointeur (contient une adresse mémoire comme 0xAAAABBCC). Mais pour les besoins de cet article, je vais l'utiliser comme index.

Au début du thread SP == STACK_CAPACITY Et ensuite vous le décrémentez selon les besoins. La règle est que vous ne pouvez pas écrire dans des cellules de pile au-delà du sommet de la pile et tout index inférieur à SP n'est pas valide, donc vous d'abord décrémente SP et alors écrivez une valeur dans la cellule nouvellement allouée.

Lorsque vous savez que vous allez ajouter plusieurs valeurs sur la pile d'affilée, vous pouvez réserver de la place pour chacune d'elles à l'avance ( extrait # 3):

SP -= 3;
cin >> stack[999];
cin >> stack[998];
stack[997] = stack[999] + stack[998];

Remarque. Vous pouvez maintenant voir pourquoi "l'allocation" sur la pile est si rapide. Vous n'allouez en fait rien (comme dans le mot clé new ou malloc), c'est juste une décrémentation d'un seul entier.


4. Se débarrasser des variables locales

Prenons cette fonction simpliste ( extrait # 4.1):

int triple(int a) {
    int result = a * 3;
    return result;
}

et le réécrire sans variable locale ( snippet # 4.2):

int triple_noLocals(int a) {
    SP -= 1; // move pointer to unused cell, where we can store what we need
    stack[SP] = a * 3;
    return stack[SP];
}

utilisation ( extrait # 4.3):

// SP == 1000
someVar = triple_noLocals(11);
// now SP == 999, but we don't need the value at stack[999] anymore
// and we will move the stack index back, so we can reuse this cell later
SP += 1; // SP == 1000 again

5. Push/pop

L'ajout d'un nouvel élément en haut de la pile est une opération si fréquente que les CPU ont une instruction spéciale pour cela, Push. Nous allons l'implémenter comme ceci ( snippet 5.1):

void Push(int value) {
    --SP;
    stack[SP] = value;
}

De même, en prenant l'élément supérieur de la pile ( snippet 5.2):

void pop(int& result) {
    result = stack[SP];
    ++SP; // note that `pop` decreases stack's size
}

Le modèle d'utilisation courant pour Push/pop enregistre temporairement une certaine valeur. Disons, nous avons quelque chose d'utile dans la variable myVar et pour une raison quelconque, nous devons faire des calculs qui la remplaceront ( snippet 5.3):

int myVar = ...;
Push(myVar); // SP == 999
myVar += 10;
... // do something with new value in myVar
pop(myVar); // restore original value, SP == 1000

6. Se débarrasser des paramètres

Passons maintenant les paramètres en utilisant stack ( snippet # 6):

int triple_noL_noParams() { // `a` is at index 999, SP == 999
    SP -= 1; // SP == 998, stack[SP + 1] == a
    stack[SP] = stack[SP + 1] * 3;
    return stack[SP];
}

int main(){
    Push(11); // SP == 999
    assert(triple(11) == triple_noL_noParams());
    SP += 2; // cleanup 1 local and 1 parameter
}

7. Se débarrasser des instructions return

Renvoyons la valeur dans le registre AX ( extrait # 7):

void triple_noL_noP_noReturn() { // `a` at 998, SP == 998
    SP -= 1; // SP == 997

    stack[SP] = stack[SP + 1] * 3;
    AX = stack[SP];

    SP += 1; // finally we can cleanup locals right in the function body, SP == 998
}

void main(){
    ... // some code
    Push(AX); // save AX in case there is something useful there, SP == 999
    Push(11); // SP == 998
    triple_noL_noP_noReturn();
    assert(triple(11) == AX);
    SP += 1; // cleanup param
             // locals were cleaned up in the function body, so we don't need to do it here
    pop(AX); // restore AX
    ...
}

8. Pointeur de base de pile (BP) (également appelé pointeur de trame) et cadre de pile

Prenons une fonction plus "avancée" et réécrivons-la dans notre C++ de type asm ( snippet # 8.1):

int myAlgo(int a, int b) {
    int t1 = a * 3;
    int t2 = b * 3;
    return t1 - t2;
}

void myAlgo_noLPR() { // `a` at 997, `b` at 998, old AX at 999, SP == 997
    SP -= 2; // SP == 995

    stack[SP + 1] = stack[SP + 2] * 3; 
    stack[SP]     = stack[SP + 3] * 3;
    AX = stack[SP + 1] - stack[SP];

    SP += 2; // cleanup locals, SP == 997
}

int main(){
    Push(AX); // SP == 999
    Push(22); // SP == 998
    Push(11); // SP == 997
    myAlgo_noLPR();
    assert(myAlgo(11, 22) == AX);
    SP += 2;
    pop(AX);
}

Imaginez maintenant que nous avons décidé d'introduire une nouvelle variable locale pour y stocker les résultats avant de revenir, comme nous le faisons dans tripple (extrait # 4.1). Le corps de la fonction sera ( extrait # 8.2):

SP -= 3; // SP == 994
stack[SP + 2] = stack[SP + 3] * 3; 
stack[SP + 1] = stack[SP + 4] * 3;
stack[SP]     = stack[SP + 2] - stack[SP + 1];
AX = stack[SP];
SP += 3;

Vous voyez, nous avons dû mettre à jour chaque référence aux paramètres de fonction et aux variables locales. Pour éviter cela, nous avons besoin d'un index d'ancrage, qui ne change pas lorsque la pile se développe.

Nous allons créer l'ancre dès l'entrée de la fonction (avant d'allouer de l'espace aux locaux) en enregistrant le sommet actuel (valeur de SP) dans le registre BP. Extrait # 8.3:

void myAlgo_noLPR_withAnchor() { // `a` at 997, `b` at 998, SP == 997
    Push(BP);   // save old BP, SP == 996
    BP = SP;    // create anchor, stack[BP] == old value of BP, now BP == 996
    SP -= 2;    // SP == 994

    stack[BP - 1] = stack[BP + 1] * 3;
    stack[BP - 2] = stack[BP + 2] * 3;
    AX = stack[BP - 1] - stack[BP - 2];

    SP = BP;    // cleanup locals, SP == 996
    pop(BP);    // SP == 997
}

La tranche de pile, à laquelle appartient et contrôle entièrement la fonction, est appelée cadre de pile de la fonction . Par exemple. Le cadre de pile de myAlgo_noLPR_withAnchor Est stack[996 .. 994] (Les deux idéx inclus).
Le cadre commence au BP de la fonction (après l'avoir mis à jour dans la fonction) et dure jusqu'au prochain cadre de pile. Les paramètres de la pile font donc partie du cadre de pile de l'appelant (voir la note 8a).

Remarques:
8a.Wikipedia dit le contraire sur les paramètres, mais ici j'adhère à manuel du développeur du logiciel Intel , voir vol. 1, section 6.2.4.1 Pointeur de base de cadre de pile et Figure 6-2 dans la section 6.3.2 Opérations CALL et RET éloignées. Les paramètres de la fonction et le cadre de pile font partie de l'enregistrement d'activation de la fonction (voir Les périlogues de la fonction gen ).
8b. décalages positifs du point BP aux paramètres de fonction et les décalages négatifs pointent vers les variables locales. C'est assez pratique pour le débogage
8c.stack[BP] Stocke l'adresse du cadre de pile précédent, stack[stack[BP]] Stocke le cadre de pile précédent et ainsi de suite. En suivant cette chaîne, vous pouvez découvrir les cadres de toutes les fonctions du programme, qui ne sont pas encore revenus. Voici comment les débogueurs vous montrent la pile d'appels
8d. les 3 premières instructions de myAlgo_noLPR_withAnchor, Où nous configurons le cadre (enregistrer l'ancien BP, mettre à jour BP, réserver de l'espace pour les locaux) sont appelées prologue de la fonction


9. Conventions d'appel

Dans l'extrait 8.1, nous avons poussé les paramètres de myAlgo de droite à gauche et renvoyé le résultat dans AX. Nous pourrions aussi bien passer les paramètres de gauche à droite et revenir dans BX. Ou passez les paramètres dans BX et CX et revenez dans AX. De toute évidence, l'appelant (main()) et la fonction appelée doivent convenir où et dans quel ordre tous ces éléments sont stockés.

La convention d'appel est un ensemble de règles sur la façon dont les paramètres sont passés et le résultat est renvoyé.

Dans le code ci-dessus, nous avons utilisé convention d'appel cdecl:

  • Les paramètres sont passés sur la pile, avec le premier argument à l'adresse la plus basse de la pile au moment de l'appel (dernière poussée <...>). L'appelant est responsable de récupérer les paramètres de la pile après l'appel.
  • la valeur de retour est placée dans AX
  • EBP et ESP doit être préservé par l'appelé (fonction myAlgo_noLPR_withAnchor Dans notre cas), de sorte que l'appelant (fonction main) peut se fier à ces registres non ayant été changé par un appel.
  • Tous les autres registres (EAX, <...>) peuvent être modifiés librement par l'appelé; si un appelant souhaite conserver une valeur avant et après l'appel de fonction, il doit enregistrer la valeur ailleurs (on le fait avec AX)

(Source: exemple "cdecl 32 bits" de Stack Overflow Documentation; copyright 2016 par icktoofay et Peter Cordes ; sous licence CC BY-SA 3.0. An archive du contenu complet de la documentation de débordement de pile peut être trouvé sur archive.org, dans lequel cet exemple est indexé par le sujet ID 3261 et l'exemple ID 11196.)


10. Se débarrasser des appels de fonction

Maintenant, la partie la plus intéressante. Tout comme les données, le code exécutable est également stocké en mémoire (sans aucun lien avec la mémoire de la pile) et chaque instruction a une adresse.
Sauf indication contraire, la CPU exécute les instructions l'une après l'autre, dans l'ordre où elles sont stockées en mémoire. Mais nous pouvons ordonner au CPU de "sauter" vers un autre emplacement en mémoire et d'exécuter des instructions à partir de là. Dans asm, il peut s'agir de n'importe quelle adresse, et dans des langages plus avancés comme C++, vous ne pouvez accéder qu'aux adresses marquées par des étiquettes ( il existe des solutions mais elles ne sont pas jolies, c'est le moins qu'on puisse dire).

Prenons cette fonction ( extrait # 10.1):

int myAlgo_withCalls(int a, int b) {
    int t1 = triple(a);
    int t2 = triple(b);
    return t1 - t2;
}

Et au lieu d'appeler la méthode C++ tripple, procédez comme suit:

  1. copier le corps entier de tripple dans myAlgo
  2. à l'entrée myAlgo, sautez par-dessus le code de tripple avec goto
  3. lorsque nous devons exécuter le code de tripple, enregistrez sur l'adresse de pile de la ligne de code juste après l'appel de tripple, afin que nous puissions revenir ici plus tard et continuer l'exécution (Push_ADDRESS macro ci-dessous)
  4. passer à l'adresse de la fonction tripple et l'exécuter jusqu'à la fin (3. et 4. forment ensemble la macro CALL)
  5. à la fin de la tripple (après avoir nettoyé les sections locales), prenez l'adresse de retour du haut de la pile et sautez là (macro RET)

Parce qu'il n'y a pas de moyen facile de passer à une adresse de code particulière en C++, nous utiliserons des étiquettes pour marquer les emplacements des sauts. Je n'entrerai pas dans le détail du fonctionnement des macros ci-dessous, croyez-moi simplement qu'elles font ce que je dis qu'elles font ( snippet # 10.2):

// pushes the address of the code at label's location on the stack
// NOTE1: this gonna work only with 32-bit compiler (so that pointer is 32-bit and fits in int)
// NOTE2: __asm block is specific for Visual C++. In GCC use https://gcc.gnu.org/onlinedocs/gcc/Labels-as-Values.html
#define Push_ADDRESS(labelName) {               \
    void* tmpPointer;                           \
    __asm{ mov [tmpPointer], offset labelName } \
    Push(reinterpret_cast<int>(tmpPointer));    \
}

// why we need indirection, read https://stackoverflow.com/a/13301627/264047
#define TOKENPASTE(x, y) x ## y
#define TOKENPASTE2(x, y) TOKENPASTE(x, y)

// generates token (not a string) we will use as label name. 
// Example: LABEL_NAME(155) will generate token `lbl_155`
#define LABEL_NAME(num) TOKENPASTE2(lbl_, num)

#define CALL_IMPL(funcLabelName, callId)    \
    Push_ADDRESS(LABEL_NAME(callId));       \
    goto funcLabelName;                     \
    LABEL_NAME(callId) :

// saves return address on the stack and jumps to label `funcLabelName`
#define CALL(funcLabelName) CALL_IMPL(funcLabelName, __LINE__)

// takes address at the top of stack and jump there
#define RET() {                                         \
    int tmpInt;                                         \
    pop(tmpInt);                                        \
    void* tmpPointer = reinterpret_cast<void*>(tmpInt); \
    __asm{ jmp tmpPointer }                             \
}

void myAlgo_asm() {
    goto my_algo_start;

triple_label:
    Push(BP);
    BP = SP;
    SP -= 1;

    // stack[BP] == old BP, stack[BP + 1] == return address
    stack[BP - 1] = stack[BP + 2] * 3;
    AX = stack[BP - 1];

    SP = BP;     
    pop(BP);
    RET();

my_algo_start:
    Push(BP);   // SP == 995
    BP = SP;    // BP == 995; stack[BP] == old BP, 
                // stack[BP + 1] == dummy return address, 
                // `a` at [BP + 2], `b` at [BP + 3]
    SP -= 2;    // SP == 993

    Push(AX);
    Push(stack[BP + 2]);
    CALL(triple_label);
    stack[BP - 1] = AX;
    SP -= 1;
    pop(AX);

    Push(AX);
    Push(stack[BP + 3]);
    CALL(triple_label);
    stack[BP - 2] = AX;
    SP -= 1;
    pop(AX);

    AX = stack[BP - 1] - stack[BP - 2];

    SP = BP; // cleanup locals, SP == 997
    pop(BP);
}

int main() {
    Push(AX);
    Push(22);
    Push(11);
    Push(7777); // dummy value, so that offsets inside function are like we've pushed return address
    myAlgo_asm();
    assert(myAlgo_withCalls(11, 22) == AX);
    SP += 1; // pop dummy "return address"
    SP += 2;
    pop(AX);
}

Remarques:
10a. parce que l'adresse de retour est stockée sur la pile, en principe nous pouvons la changer. Voici comment attaque par écrasement de pile fonctionne
10b. les 3 dernières instructions à la "fin" de triple_label (Nettoyage des locaux, restauration de l'ancien BP, retour) sont appelées épilogue de la fonction


11. Assemblage

Examinons maintenant le vrai asm pour myAlgo_withCalls. Pour ce faire dans Visual Studio:

  • définir la plate-forme de construction sur x86
  • type de construction: débogage
  • définir un point d'arrêt quelque part à l'intérieur de myAlgo_withCalls
  • exécuter, et lorsque l'exécution s'arrête au point d'arrêt, appuyez sur Ctrl + Alt + D

Une différence avec notre C++ de type asm est que la pile d'asm fonctionne sur des octets plutôt que sur des entiers. Donc, pour réserver de l'espace pour un int, SP sera décrémenté de 4 octets.
C'est parti ( extrait # 11.1, les numéros de ligne dans les commentaires proviennent de Gist ):

;   114: int myAlgo_withCalls(int a, int b) {
 Push        ebp        ; create stack frame 
 mov         ebp,esp  
; return address at (ebp + 4), `a` at (ebp + 8), `b` at (ebp + 12)

 sub         esp,0D8h   ; reserve space for locals. Compiler can reserve more bytes then needed. 0D8h is hexadecimal == 216 decimal 

 Push        ebx        ; cdecl requires to save all these registers
 Push        esi  
 Push        edi  

 ; fill all the space for local variables (from (ebp-0D8h) to (ebp)) with value 0CCCCCCCCh repeated 36h times (36h * 4 == 0D8h)
 ; see https://stackoverflow.com/q/3818856/264047
 ; I guess that's for ease of debugging, so that stack is filled with recognizable values
 ; 0CCCCCCCCh in binary is 110011001100...
 lea         edi,[ebp-0D8h]     
 mov         ecx,36h    
 mov         eax,0CCCCCCCCh  
 rep stos    dword ptr es:[edi]  

;   115:    int t1 = triple(a);
 mov         eax,dword ptr [ebp+8]   ; Push parameter `a` on the stack
 Push        eax  

 call        triple (01A13E8h)  
 add         esp,4                   ; clean up param 
 mov         dword ptr [ebp-8],eax   ; copy result from eax to `t1`

;   116:    int t2 = triple(b);
 mov         eax,dword ptr [ebp+0Ch] ; Push `b` (0Ch == 12)
 Push        eax  

 call        triple (01A13E8h)  
 add         esp,4  
 mov         dword ptr [ebp-14h],eax ; t2 = eax

 mov         eax,dword ptr [ebp-8]   ; calculate and store result in eax
 sub         eax,dword ptr [ebp-14h]  

 pop         edi  ; restore registers
 pop         esi  
 pop         ebx  

 add         esp,0D8h  ; check we didn't mess up esp or ebp. this is only for debug builds
 cmp         ebp,esp  
 call        __RTC_CheckEsp (01A116Dh)  

 mov         esp,ebp  ; destroy frame
 pop         ebp  
 ret  

Et asm pour tripple ( extrait # 11.2):

 Push        ebp  
 mov         ebp,esp  
 sub         esp,0CCh  
 Push        ebx  
 Push        esi  
 Push        edi  
 lea         edi,[ebp-0CCh]  
 mov         ecx,33h  
 mov         eax,0CCCCCCCCh  
 rep stos    dword ptr es:[edi]  
 imul        eax,dword ptr [ebp+8],3  
 mov         dword ptr [ebp-8],eax  
 mov         eax,dword ptr [ebp-8]  
 pop         edi  
 pop         esi  
 pop         ebx  
 mov         esp,ebp  
 pop         ebp  
 ret  

J'espère qu'après avoir lu ce post, l'Assemblée n'a pas l'air aussi énigmatique qu'avant :)


Voici les liens du corps de l'article et quelques lectures supplémentaires:

20
Alexander Malakhov

Quant à savoir si la pile est implémentée dans le matériel, cela article Wikipedia pourrait aider.

Certaines familles de processeurs, comme le x86, ont des instructions spéciales pour manipuler la pile du thread en cours d'exécution. Les autres familles de processeurs, notamment PowerPC et MIPS, ne prennent pas explicitement en charge la pile, mais s'appuient plutôt sur la convention et délèguent la gestion de la pile à l'interface binaire d'application (ABI) du système d'exploitation.

Cet article et les autres auxquels il renvoie peuvent être utiles pour avoir une idée de l'utilisation de la pile dans les processeurs.

7
Leaf Garland

Le concept

Pensez d'abord à tout cela comme si vous étiez la personne qui l'a inventé. Comme ça:

Pensez d'abord à un tableau et à la façon dont il est implémenté au bas niveau -> il s'agit essentiellement d'un ensemble d'emplacements de mémoire contigus (emplacements de mémoire qui sont côte à côte). Maintenant que vous avez cette image mentale dans votre tête, pensez au fait que vous pouvez accéder à N'IMPORTE QUEL de ces emplacements de mémoire et la supprimer à votre guise lorsque vous supprimez ou ajoutez des données dans votre tableau. Pensez maintenant à ce même tableau mais au lieu de la possibilité de supprimer n'importe quel emplacement, vous décidez de supprimer uniquement le DERNIER emplacement lorsque vous supprimez ou ajoutez des données dans votre tableau. Maintenant, votre nouvelle idée de manipuler les données dans ce tableau de cette façon s'appelle LIFO ce qui signifie Last In First Out. Votre idée est très bonne car elle facilite le suivi du contenu de ce tableau sans avoir à utiliser un algorithme de tri chaque fois que vous en supprimez quelque chose. De plus, pour savoir à tout moment quelle est l'adresse du dernier objet du tableau, vous dédiez un registre dans le CPU pour en garder la trace. Maintenant , la façon dont ce registre en assure le suivi est que chaque fois que vous supprimez ou ajoutez quelque chose à votre tableau, vous décrémentez ou incrémentez également la valeur de l'adresse dans votre registre par la quantité d'objets que vous avez supprimés ou ajoutés du tableau (par le Vous devez également vous assurer que la quantité par laquelle vous décrémentez ou incrémentez ce registre est fixée à une quantité (comme 4 emplacements de mémoire, soit 4 octets) par objet, encore une fois, pour faciliter la garder une trace et aussi pour permettre d'utiliser ce registre avec une boucle c onstructs car les boucles utilisent une incrémentation fixe par itération (par ex. pour boucler votre tableau avec une boucle, vous construisez la boucle pour incrémenter votre registre de 4 à chaque itération, ce qui ne serait pas possible si votre tableau contient des objets de tailles différentes). Enfin, vous choisissez d'appeler cette nouvelle structure de données une "pile", car elle vous rappelle une pile d'assiettes dans un restaurant où elles retirent ou ajoutent toujours une assiette en haut de cette pile.

L'implémentation

Comme vous pouvez le voir, une pile n'est rien de plus qu'un tableau d'emplacements de mémoire contigus où vous avez décidé comment la manipuler. De ce fait, vous pouvez voir que vous n'avez même pas besoin d'utiliser les instructions spéciales et les registres pour contrôler la pile. Vous pouvez l'implémenter vous-même avec les instructions de base mov, add et sub et en utilisant des registres à usage général à la place du ESP et EBP comme ceci:

mov edx, 0FFFFFFFFh

; -> ce sera l'adresse de début de votre pile, la plus éloignée de votre code et de vos données, elle servira également de registre qui garde la trace du dernier objet de la pile que j'ai expliqué plus tôt . Vous l'appelez le "pointeur de pile", vous choisissez donc le registre EDX pour lequel ESP est normalement utilisé pour.

sous edx, 4

mov [edx], dword ptr [someVar]

; -> ces deux instructions décrémenteront votre pointeur de pile de 4 emplacements de mémoire et copieront les 4 octets à partir de l'emplacement de mémoire [someVar] vers l'emplacement de mémoire vers lequel EDX pointe maintenant, tout comme une instruction Push décrémente l'ESP, seulement ici vous l'avez fait manuellement et vous avez utilisé EDX. Donc, l'instruction Push n'est en fait qu'un opcode plus court qui fait cela avec ESP.

mov eax, dword ptr [edx]

ajouter edx, 4

; -> et ici nous faisons le contraire, nous copions d'abord les 4 octets en commençant à l'emplacement de mémoire vers lequel EDX pointe maintenant dans le registre EAX (choisi arbitrairement ici, nous aurions pu le copier n'importe où voulait). Et puis nous incrémentons notre pointeur de pile EDX de 4 emplacements de mémoire. C'est ce que fait l'instruction POP.

Vous pouvez maintenant voir que les instructions Push et POP et les registres ESP ans EBP viennent d’être ajoutés par Intel pour faciliter l’écriture et la lecture du concept de structure de données "pile" ci-dessus. Il existe encore des processeurs RISC (Reduced Instruction Set) qui n'ont pas les instructions Push et POP et les registres dédiés pour la manipulation de la pile, et lors de l'écriture de programmes d'assemblage pour ces processeurs, vous devez implémenter la pile par vous-même comme je l'ai montré toi.

4
Zod

Je pense que la réponse principale que vous cherchez a déjà été suggérée.

Lorsqu'un ordinateur x86 démarre, la pile n'est pas configurée. Le programmeur doit explicitement le configurer au démarrage. Cependant, si vous êtes déjà dans un système d'exploitation, cela a été pris en charge. Voici un exemple de code d'un simple programme bootstrap.

Tout d'abord, les registres de données et de segments de pile sont définis, puis le pointeur de pile est défini à 0x4000 au-delà.


    movw    $BOOT_SEGMENT, %ax
    movw    %ax, %ds
    movw    %ax, %ss
    movw    $0x4000, %ax
    movw    %ax, %sp

Après ce code, la pile peut être utilisée. Maintenant, je suis sûr que cela peut se faire de différentes manières, mais je pense que cela devrait illustrer l'idée.

3
Mr. Shickadance

Vous confondez une pile abstraite et la pile matériellement implémentée. Ce dernier est déjà implémenté.

3
sharptooth

La pile est juste une façon dont les programmes et les fonctions utilisent la mémoire.

La pile m'a toujours confondu, alors j'ai fait une illustration:

The stack is like stalactites

( version svg ici )

2
Alexander

Qu'est-ce que la pile? Une pile est un type de structure de données - un moyen de stocker des informations dans un ordinateur. Lorsqu'un nouvel objet est entré dans une pile, il est placé au-dessus de tous les objets précédemment entrés. En d'autres termes, la structure des données de la pile est semblable à une pile de cartes, papiers, envois de cartes de crédit ou tout autre objet du monde réel auquel vous pouvez penser. Lors de la suppression d'un objet d'une pile, celui du haut est supprimé en premier. Cette méthode est appelée LIFO (dernier entré, premier sorti).

Le terme "pile" peut également être court pour une pile de protocoles réseau. Dans la mise en réseau, les connexions entre les ordinateurs sont établies via une série de connexions plus petites. Ces connexions, ou couches, agissent comme la structure de données de la pile, en ce sens qu'elles sont construites et éliminées de la même manière.

1
rahul soni

La pile existe déjà, vous pouvez donc supposer cela lors de l'écriture de votre code. La pile contient les adresses de retour des fonctions, les variables locales et les variables qui sont passées entre les fonctions. Il existe également des registres de pile tels que BP, SP (Stack Pointer) intégré que vous pouvez utiliser, d'où les commandes intégrées que vous avez mentionnées. Si la pile n'a pas déjà été implémentée, les fonctions ne pouvaient pas s'exécuter et le flux de code ne pouvait pas fonctionner.

1
Gal Goldman

La pile est "implémentée" au moyen du pointeur de pile, qui (en supposant ici l'architecture x86) pointe dans la pile segment. Chaque fois que quelque chose est poussé sur la pile (au moyen de pushl, call ou un opcode de pile similaire), il est écrit à l'adresse vers laquelle pointe le pointeur de pile et le pointeur de pile décrémenté (la pile est croissant vers le bas, c'est-à-dire des adresses plus petites). Lorsque vous sortez quelque chose de la pile (popl, ret), le pointeur de la pile est incrémenté et la valeur lue dans la pile.

Dans une application en espace utilisateur, la pile est déjà configurée pour vous au démarrage de votre application. Dans un environnement à noyau, vous devez d'abord configurer le segment de pile et le pointeur de pile ...

1
DevSolar

Je n'ai pas vu spécifiquement l'assembleur de gaz, mais en général la pile est "implémentée" en maintenant une référence à l'emplacement en mémoire où réside le haut de la pile. L'emplacement de la mémoire est stocké dans un registre, qui a des noms différents pour différentes architectures, mais peut être considéré comme le registre de pointeur de pile.

Les commandes pop et push sont implémentées dans la plupart des architectures pour vous en s'appuyant sur des micro-instructions. Cependant, certaines "architectures pédagogiques" nécessitent que vous les mettiez en œuvre vous-même. Fonctionnellement, Push serait implémenté un peu comme ceci:

   load the address in the stack pointer register to a gen. purpose register x
   store data y at the location x
   increment stack pointer register by size of y

De plus, certaines architectures stockent la dernière adresse mémoire utilisée comme pointeur de pile. Certains stockent la prochaine adresse disponible.

1
Charlie White

L'appel de fonctions, qui nécessite d'enregistrer et de restaurer l'état local de manière LIFO mode (par opposition à une approche de co-routine généralisée), s'avère être un besoin si incroyablement courant que les langages d'assemblage et le CPU les architectures intègrent essentiellement cette fonctionnalité dans. La même chose pourrait probablement être dite pour les notions de thread, de protection de la mémoire, de niveaux de sécurité, etc. En théorie, vous pouvez implémenter votre propre pile, les conventions d'appel, etc., mais je suppose que certains opcodes et la plupart des runtimes existants s'appuyer sur ce concept natif de "pile".

0
aaron

Vous avez raison de dire qu'une pile est "juste" une structure de données. Ici, cependant, il se réfère à une pile implémentée matériellement utilisée dans un but spécial - "La pile".

De nombreuses personnes ont commenté la pile implémentée par matériel par rapport à la structure de données de pile (logicielle). Je voudrais ajouter qu'il existe trois principaux types de structure de pile -

  1. Une pile d'appels - Quelle est celle que vous demandez! Il stocke les paramètres de fonction et l'adresse de retour, etc. Lisez les fonctions du chapitre 4 (Tout sur la 4e page, c'est-à-dire la page 53) dans ce livre. Il y a une bonne explication.
  2. Une pile générique que vous pourriez utiliser dans votre programme pour faire quelque chose de spécial ...
  3. Une pile matérielle générique
    Je ne suis pas sûr de cela, mais je me souviens avoir lu quelque part qu'il existe une pile matérielle implémentée à usage général disponible dans certaines architectures. Si quelqu'un sait si c'est correct, veuillez commenter.

La première chose à savoir est l'architecture pour laquelle vous programmez, ce que le livre explique (je viens de le chercher - lien). Pour vraiment comprendre les choses, je vous suggère de vous renseigner sur la mémoire, l'adressage, les registres et l'architecture de x86 (je suppose que c'est ce que vous apprenez - du livre).

0
batbrat

La pile d'appels est implémentée par le jeu d'instructions x86 et le système d'exploitation.

Des instructions telles que Push and pop ajustent le pointeur de pile tandis que le système d'exploitation s'occupe d'allouer de la mémoire à mesure que la pile augmente pour chaque thread.

Le fait que la pile x86 "croisse" d'adresses supérieures à inférieures rend cette architecture plus sensible à l'attaque par dépassement de tampon.

0
Maurice Flanagan

stack fait partie de la mémoire. il utilise pour input et output de functions. il l'utilise également pour se souvenir du retour de la fonction.

esp le registre se souvient de l'adresse de la pile.

stack et esp sont implémentés par le matériel. vous pouvez également l'implémenter vous-même. cela rendra votre programme très lent.

exemple:

nop // esp = 0012ffc4

Appuyez sur 0 // esp = 0012ffc0, Dword [0012ffc0] = 00000000

appelez proc01 // esp = 0012ffbc, Dword [0012ffbc] = eip, eip = adrr [proc01]

pop eax // eax = Dword [esp], esp = esp + 4

0
Amir

Je cherchais comment la pile fonctionne en termes de fonction et j'ai trouvé ce blog son impressionnant et son concept d'explication de la pile à partir de zéro et comment la valeur de stockage de la pile dans la pile.

Maintenant sur votre réponse. Je vais expliquer avec python mais vous aurez une bonne idée du fonctionnement de la pile dans n'importe quelle langue.

enter image description here

C'est un programme:

def hello(x):
    if x==1:
        return "op"
    else:
        u=1
        e=12
        s=hello(x-1)
        e+=1
        print(s)
        print(x)
        u+=1
    return e

hello(3)

enter image description here

enter image description here

Source: Cryptroix

une partie de son sujet qu'il couvre dans le blog:

How Function work ?
Calling a Function
 Functions In a Stack
 What is Return Address
 Stack
Stack Frame
Call Stack
Frame Pointer (FP) or Base Pointer (BP)
Stack Pointer (SP)
Allocation stack and deallocation of stack
StackoverFlow
What is Heap?

Mais son explication avec le langage python donc si vous voulez vous pouvez jeter un oeil.

0
user6932350

Vous avez raison de dire qu'une pile est une structure de données. Souvent, les structures de données (piles incluses) avec lesquelles vous travaillez sont abstraites et existent en tant que représentation en mémoire.

La pile avec laquelle vous travaillez dans ce cas a une existence plus matérielle - elle correspond directement aux registres physiques réels du processeur. En tant que structure de données, les piles sont des structures FILO (premier entré, dernier sorti) qui garantissent que les données sont supprimées dans l'ordre inverse de leur saisie. Voir le logo StackOverflow pour un visuel! ;)

Vous travaillez avec pile d'instructions. Il s'agit de la pile d'instructions réelles que vous alimentez le processeur.

0
Dave Swersky