Si vous programmez sur un "gros" ordinateur, alors vous n'avez probablement pas une telle question. Il y a beaucoup de pile pour le déborder, vous devez essayer. Dans le pire des cas, vous cliquez sur OK dans une fenêtre comme celle-ci et allez le découvrir.
Mais si vous programmez des microcontrôleurs, le problème semble un peu différent. Vous devez d'abord
remarquer que la pile est pleine.
Dans cet article, je parlerai de mes propres recherches sur ce sujet. Depuis que je programme principalement sous STM32 et sous Milander 1986 - je me suis concentré sur eux.
Présentation
Imaginons le cas le plus simple - nous écrivons du code simple à un seul thread sans aucun système d'exploitation, c'est-à-dire nous n'avons qu'une seule pile. Et si vous, comme moi, programmez dans uVision Keil, alors la mémoire est distribuée en quelque sorte comme ceci:

Et si vous, comme moi, considérez la mémoire dynamique sur les microcontrôleurs comme mauvaise, alors comme ceci:

Au faitSi vous souhaitez interdire l'utilisation du tas, vous pouvez le faire comme ceci:
#pragma import(__use_no_heap_region)
Détails
ici OK, quel est le problème? Le problème est que Keil place la pile
immédiatement derrière la zone de données statiques. Et la pile dans Cortex-M croît dans le sens de la diminution des adresses. Et quand il déborde, il sort simplement de la mémoire allouée. Et remplace toutes les variables statiques ou globales.
Particulièrement bien si la pile ne déborde qu'en entrant dans l'interruption. Ou, mieux encore, dans une interruption imbriquée! Et gâte discrètement une variable utilisée dans une section de code complètement différente. Et le programme se bloque à l'assertion. Si vous avez de la chance. Heisenbag sphérique, on peut chercher une telle semaine entière avec une lampe de poche.
Faites immédiatement une réservation: si vous utilisez un tas, le problème ne va nulle part, juste au lieu des variables globales, le tas se gâte. Pas beaucoup mieux.
D'accord, le problème est clair. Que faire
MPU
Le plus simple et le plus évident consiste à utiliser MPU (en d'autres termes, Memory Protection Unit). Vous permet d'attribuer différents attributs à différents morceaux de mémoire; en particulier, vous pouvez entourer la pile de régions en lecture seule et intercepter MemFault lorsque vous y écrivez.
Par exemple, dans stm32f407 MPU est. Malheureusement, dans beaucoup d'autres stm "juniors", ce n'est pas le cas. Et dans le Milandrovsky 1986VE1, il n'est pas là non plus.
C'est-à-dire La solution est bonne, mais pas toujours abordable.
Commande manuelle
Lors de la compilation, Keil peut générer (et le fait par défaut) un rapport html avec un graphique d'appel (option de l'éditeur de liens "--info = stack"). Et ce rapport donne également des informations sur la pile utilisée. Gcc peut aussi le faire (option -fstack-usage). En conséquence, vous pouvez parfois consulter ce rapport (ou écrire un script qui le fait pour vous et l'appeler avant chaque génération).
De plus, au tout début du rapport, un chemin est écrit qui conduit à l'utilisation maximale de la pile:

Le problème est que si votre code a des appels de fonction par des pointeurs ou des méthodes virtuelles (et je les ai), ce rapport peut grandement sous-estimer la profondeur maximale de la pile. Eh bien, les interruptions, bien sûr, ne sont pas prises en compte. Pas un moyen très fiable.
Placement de pile délicat
J'ai découvert cette méthode dans
cet article . L'article porte sur la rouille, mais l'idée principale est la suivante:

Lorsque vous utilisez gcc, cela peut être fait en utilisant le "
double lien ".
Et dans Keil, l'emplacement des zones peut être modifié en utilisant votre propre script pour l'éditeur de liens (fichier scatter dans la terminologie de Keil). Pour ce faire, ouvrez les options du projet et décochez «Utiliser la disposition de la mémoire à partir de la boîte de dialogue cible». Ensuite, le fichier par défaut apparaîtra dans le champ «Fichier Scatter». Cela ressemble à ceci:
; ************************************************************* ; *** Scatter-Loading Description File generated by uVision *** ; ************************************************************* LR_IROM1 0x08000000 0x00020000 { ; load region size_region ER_IROM1 0x08000000 0x00020000 { ; load address = execution address *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) } RW_IRAM1 0x20000000 0x00005000 { ; RW data .ANY (+RW +ZI) } }
Que faire ensuite? Options possibles.
La documentation officielle suggère de définir des sections avec des noms réservés - ARM_LIB_HEAP et ARM_LIB_STACK. Mais cela entraîne des conséquences désagréables, du moins pour moi - la taille de la pile et du tas devra être définie dans le fichier scatter.
Dans tous les projets que j'utilise, les tailles de pile et de tas sont définies dans le fichier de démarrage de l'assembleur (que Keil génère lors de la création du projet). Je ne veux pas vraiment le changer. Je veux juste inclure un nouveau fichier scatter dans le projet, et tout ira bien. Je suis donc allé un peu différemment:
Spoiler #! armcc -E ; with that we can use C preprocessor #define RAM_BEGIN 0x20000000 #define RAM_SIZE_BYTES (4*1024) #define FLASH_BEGIN 0x8000000 #define FLASH_SIZE_BYTES (32*1024) ; This scatter file places stack before .bss region, so on stack overflow ; we get HardFault exception immediately LR_IROM1 FLASH_BEGIN FLASH_SIZE_BYTES { ; load region size_region ER_IROM1 FLASH_BEGIN FLASH_SIZE_BYTES { ; load address = execution address *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) } ; Stack region growing down REGION_STACK RAM_BEGIN { *(STACK) } ; We have to define heap region, even if we don't actually use heap REGION_HEAP ImageLimit(REGION_STACK) { *(HEAP) } ; this will place .bss region above the stack and heap and allocate RAM that is left for it RW_IRAM1 ImageLimit(REGION_HEAP) (RAM_SIZE_BYTES - ImageLength(REGION_STACK) - ImageLength(REGION_HEAP)) { *(+RW +ZI) } }
Ensuite, j'ai dit que tous les objets nommés STACK devraient être situés dans la région REGION_STACK, et tous les objets HEAP devraient être situés dans la région REGION_HEAP. Et tout le reste se trouve dans la région RW_IRAM1. Et il a arrangé les régions dans cet ordre - le début de l'opérateur, la pile, le tas, tout le reste. Le calcul est que dans le fichier de démarrage de l'assembleur, la pile et le tas sont définis à l'aide de ce code (c'est-à-dire sous forme de tableaux avec les noms STACK et HEAP):
Spoiler Stack_Size EQU 0x00000400 AREA STACK, NOINIT, READWRITE, ALIGN=3 Stack_Mem SPACE Stack_Size __initial_sp Heap_Size EQU 0x00000200 AREA HEAP, NOINIT, READWRITE, ALIGN=3 __heap_base Heap_Mem SPACE Heap_Size __heap_limit PRESERVE8 THUMB
D'accord, vous pourriez demander, mais qu'est-ce que cela nous donne? Et voici quoi. Désormais, en sortant de la pile, le processeur essaie d'écrire (ou de lire) de la mémoire qui n'existe pas. Et sur STM32, une interruption se produit en raison d'une exception - HardFault.
Ce n'est pas aussi pratique que MemFault en raison de la MPU, car HardFault peut se produire pour de nombreuses raisons, mais au moins l'erreur est forte et non silencieuse. C'est-à-dire cela se produit immédiatement, et non après une période de temps inconnue, comme c'était le cas auparavant.
Mieux encore, nous n'avons rien payé pour cela, pas de temps d'exécution! Ouah. Mais il y a un problème.
Cela ne fonctionne pas sur Milander.Oui Bien sûr, sur la Milandra (je suis principalement intéressé par 1986BE1 et BE91), la carte mémoire est différente. Dans STM32, avant le début de l'intervention, il n'y a rien, et sur la Milandra, avant l'intervention, se trouve la zone du bus externe.
Mais même si vous n'utilisez pas de bus externe, vous ne recevrez aucun HardFault. Ou peut-être l'obtenir. Ou peut-être l'obtenir, mais pas tout de suite. Je n'ai pu trouver aucune information à ce sujet (ce qui n'est pas surprenant pour Milander), et les expériences n'ont donné aucun résultat intelligible. HardFault
se produisait
parfois si la taille de la pile était un multiple de 256. Parfois, HardFault se produisait si la pile allait trop loin dans la mémoire inexistante.
Mais cela n'a même pas d'importance. Si HardFault ne se produit pas à chaque fois, le simple fait de déplacer la pile au début de la RAM ne nous sauve plus. Et pour être tout à fait honnête, STM n'est pas non plus obligé de lever une exception en même temps, la spécification de base Cortex-M ne semble rien dire de concret à ce sujet.
Donc, même sur STM, cela ressemble plus à un hack, mais pas très sale.
Donc, vous devez chercher une autre façon.
Accéder au point d'arrêt enregistré
Si nous déplaçons la pile au début de la RAM, la valeur limite de la pile sera toujours la même - 0x20000000. Et nous pouvons simplement mettre un point d'arrêt sur le dossier dans cette cellule. Cela peut être fait avec la commande et même enregistré en autorun en utilisant le fichier .ini:
// breakpoint on stackoverflow BS Write 0x20000000, 1
Mais ce n'est pas un moyen très fiable. Ce point d'arrêt se déclenchera à chaque initialisation de la pile. Il est facile de le battre accidentellement en cliquant sur "Tuer tous les points d'arrêt". Et il ne vous protégera qu'en présence d'un débogueur. Pas bon.
Protection dynamique contre les débordements
Une recherche rapide sur ce sujet m'a conduit aux options de Keil --protect_stack et --protect_stack_all. Des options utiles, malheureusement, elles protègent non pas du débordement de la pile entière, mais de l'insertion d'une autre fonction dans le cadre de la pile. Par exemple, si votre code dépasse les limites d'un tableau ou échoue avec un nombre variable de paramètres. Gcc, bien sûr, peut le faire aussi (-fstack-protector).
L'essence de cette option est la suivante: une «variable de garde» est ajoutée à chaque cadre de pile, c'est-à-dire un numéro de garde. Si ce nombre a changé après avoir quitté la fonction, la fonction de gestion des erreurs est appelée. Détails
ici .
Une chose utile, mais pas tout à fait ce dont j'ai besoin. J'ai besoin d'une vérification beaucoup plus simple - de sorte que lors de la saisie de chaque fonction, la valeur du registre SP (Stack Pointer) soit vérifiée par rapport à une valeur minimale précédemment connue. Mais n'écrivez pas ce test avec vos mains à l'entrée de chaque fonction?
Contrôle SP dynamique
Heureusement, gcc a la merveilleuse option "-finstrument-functions", qui vous permet d'appeler une fonction définie par l'utilisateur lorsque vous entrez dans chaque fonction et lorsque vous quittez chaque fonction. Ceci est généralement utilisé pour générer des informations de débogage, mais quelle est la différence?
Encore plus heureusement, Keil copie délibérément la fonctionnalité gcc, et là la même option est disponible sous le nom "--gnu_instrument" (
détails ).
Après cela, il vous suffit d'écrire ce code:
Et le tour est joué! Maintenant, lors de la saisie de chaque fonction (y compris les gestionnaires d'interruption), une vérification sera effectuée pour le dépassement de pile. Et si la pile déborde, il y aura une assertion.
Une petite explication:- Oui, bien sûr, vous devez vérifier le débordement avec une certaine marge, sinon il y a un risque de "sauter" par-dessus la pile.
- Image $$ REGION_STACK $$ RW $$ Base est une magie spéciale pour obtenir des informations sur les zones de mémoire en utilisant les constantes générées par l'éditeur de liens. Détails (bien que peu intelligibles par endroits) ici .
La solution est-elle parfaite? Bien sûr que non.
Premièrement, cette vérification est loin d'être gratuite, le code en gonfle de 10% .Eh bien, le code fonctionnera plus lentement (même si je ne l'ai pas mesuré). Que ce soit critique ou non dépend de vous; à mon avis, c'est un prix raisonnable pour la sécurité.
Deuxièmement, cela ne fonctionnera probablement pas lors de l'utilisation de bibliothèques précompilées (mais comme je ne les utilise pas du tout, je n'ai pas vérifié).
Mais cette solution est potentiellement adaptée aux programmes multithreads, puisque nous faisons nous-mêmes la vérification. Mais je n'ai pas vraiment pensé à cette idée, donc je vais la retenir pour l'instant.
Pour résumer
Il s'est avéré trouver des solutions de travail pour stm32 et pour Milander, bien que pour ce dernier, j'ai dû payer des frais généraux.
Pour moi, la chose la plus importante était un petit changement dans le paradigme de la pensée. Avant l'
article susmentionné, je ne pensais pas du tout que vous pourriez vous protéger en quelque sorte contre le débordement de la pile. Je n'ai pas perçu cela comme un problème à résoudre, mais plutôt comme un certain phénomène naturel - parfois il pleut et parfois la pile déborde, eh bien, il n'y a rien à faire, il faut mordre la balle et tolérer.
Et je remarque généralement assez souvent pour moi-même (et pour d'autres personnes) ceci - au lieu de passer 5 minutes sur Google et de trouver une solution triviale - je vis avec mes problèmes depuis des années.
C’est tout pour moi. Je comprends que je n'ai rien découvert de fondamentalement nouveau, mais je n'ai trouvé aucun article prêt à l'emploi avec une telle décision (au moins, Joseph Yu lui-même ne le propose pas directement dans un
article sur ce sujet). J'espère que dans les commentaires, ils me diront si j'ai raison ou non, et quels sont les pièges de cette approche.
UPD: Si, lors de l'ajout d'un fichier scatter, Keil commence à émettre un avertissement incompréhensible ala "AppData \ Local \ Temp \ p17af8-2 (33): avertissement: # 1-D: la dernière ligne de fichier se termine sans nouvelle ligne" - mais ce fichier lui-même n'est pas s'ouvre, car il est temporaire, puis ajoutez simplement le saut de ligne avec le dernier caractère du fichier scatter.