Bonjour, Habr!
Dans le
dernier article , je l'ai mentionné moi-même et demandé dans les commentaires - ok, eh bien, en utilisant la méthode du poke scientifique, nous avons sélectionné la taille de la pile, il semble que rien ne tombe, mais pouvons-nous en quelque sorte évaluer de manière plus fiable ce qu'elle est égale et qui a mangé autant?
Nous répondons brièvement: oui, mais non.
Non, en utilisant les méthodes d'analyse statique, il est impossible de mesurer avec précision la taille de la pile nécessaire au programme - mais, néanmoins, ces méthodes peuvent être utiles.
La réponse est un peu plus longue - sous la coupe.
Comme cela est largement connu d'un cercle restreint de personnes, la place sur la pile est allouée, en fait, aux variables locales que la fonction en cours d'exécution utilise - à l'exception des variables avec le modificateur statique, qui sont stockées dans la mémoire allouée statiquement, dans la zone bss, car elles doivent enregistrer leurs significations entre les appels de fonction.
Lorsque la fonction est exécutée, le compilateur ajoute de l'espace sur la pile pour les variables dont il a besoin, et à la fin, il libère cet espace. Il semblerait que tout soit simple, mais - et c'est très audacieux
mais - nous avons plusieurs problèmes:
- les fonctions appellent à l'intérieur d'autres fonctions qui ont également besoin d'une pile
- parfois, les fonctions appellent d'autres fonctions non pas par leur référence directe, mais par un pointeur sur une fonction
- en principe, il est possible - bien que cela devrait être évité par tous les moyens - un appel de fonction récursive lorsque A appelle B, B appelle C et C à l'intérieur de lui-même appelle à nouveau A
- à tout moment une interruption peut se produire, dont le gestionnaire est la même fonction qui veut son propre morceau de la pile
- si vous avez une hiérarchie d'interruptions, une autre interruption peut se produire à l'intérieur de l'interruption!
Sans ambiguïté, les appels de fonction récursifs doivent être supprimés de cette liste, car leur présence est une excuse pour ne pas considérer la taille de la pile, mais pour aller exprimer votre avis à l'auteur du code. Tout le reste, hélas, ne peut pas être barré dans le cas général (bien qu'il puisse y avoir en particulier des nuances: par exemple, toutes les interruptions pour vous peuvent avoir la même priorité par conception, par exemple, comme dans RIOT OS, et il n'y aura pas d'interruptions imbriquées).
Imaginez maintenant une peinture à l'huile:
- la fonction A, mangeant 100 octets sur la pile, appelle la fonction B, qui a besoin de 50 octets
- au moment de l'exécution de B, A lui-même n'était évidemment pas encore terminé, donc ses 100 octets n'étaient pas libérés, nous avons donc déjà 150 octets sur la pile
- la fonction B appelle la fonction C, et elle le fait selon un pointeur qui, selon la logique du programme, peut pointer vers une demi-douzaine de fonctions différentes consommant de 5 à 50 octets de pile
- au moment de l'exécution C, une interruption se produit avec un gestionnaire lourd fonctionnant relativement longtemps et consommant 20 octets de pile
- pendant le traitement d'interruption, une autre interruption de priorité plus élevée se produit, dont le gestionnaire veut 10 octets de pile
Dans cette belle conception, avec une coïncidence particulièrement réussie de toutes les circonstances, vous aurez
au moins cinq fonctions actives simultanément - A, B, C et deux gestionnaires d'interruption. De plus, l'un d'eux n'a pas de constante de consommation de pile, car il peut simplement s'agir d'une fonction différente dans différents passages, et pour comprendre la possibilité ou l'impossibilité de s'interrompre, vous devez au moins savoir si vous avez des interruptions avec des priorités différentes. , et au maximum - pour comprendre si elles peuvent se chevaucher.
De toute évidence, pour tout analyseur de code statique automatique, cette tâche est extrêmement proche d'être écrasante, et elle ne peut être effectuée que dans une approximation approximative de l'estimation supérieure:
- additionner les piles de tous les gestionnaires d'interruption
- résumer des piles de fonctions qui s'exécutent dans la même branche de code
- essayez de trouver tous les pointeurs vers les fonctions et leurs appels, et prenez la taille maximale de la pile parmi les fonctions vers lesquelles ces pointeurs pointent comme taille de pile
Dans la plupart des cas, vous obtiendrez, d'une part, une estimation très élevée, et d'autre part, une chance d'ignorer un appel de fonction particulièrement délicat via des pointeurs.
Par conséquent, dans le cas général, nous pouvons simplement dire:
cette tâche n'est pas automatiquement résolue . Une solution manuelle - une personne qui connaît la logique de ce programme - nécessite de creuser plusieurs chiffres.
Néanmoins, une estimation statique de la taille de la pile peut être très utile pour optimiser un logiciel - au moins dans le simple but de comprendre qui mange beaucoup, et pas trop.
Il existe deux outils extrêmement utiles pour cela dans la chaîne d'outils GNU / gcc:
- flag -fstack-usage
- utilitaire cflow
Si vous ajoutez -fstack-usage aux drapeaux gcc (par exemple, au Makefile dans la ligne avec CFLAGS), alors pour
chaque fichier compilé% filename% .c, le compilateur créera le fichier% filename% .su, dans lequel il y aura du texte simple et clair.
Prenez, par exemple, target.su pour
ce gigantesque chausson :
target.c:159:13:save_settings 8 static target.c:172:13:disable_power 8 static target.c:291:13:adc_measure_vdda 32 static target.c:255:13:adc_measure_current 24 static target.c:76:6:cpu_setup 0 static target.c:81:6:clock_setup 8 static target.c:404:6:dma1_channel1_isr 24 static target.c:434:6:adc_comp_isr 40 static target.c:767:6:systick_activity 56 static target.c:1045:6:user_activity 104 static target.c:1215:6:gpio_setup 24 static target.c:1323:6:target_console_init 8 static target.c:1332:6:led_bit 8 static target.c:1362:6:led_num 8 static
Ici, nous voyons la consommation réelle de la pile pour chaque fonction qui y apparaît, à partir de laquelle nous pouvons tirer quelques conclusions pour nous-même, par exemple, qu'il vaut la peine d'essayer d'optimiser tout d'abord, si nous rencontrons un manque de RAM.
Dans le même temps, attention,
ce fichier ne fournit pas réellement d'informations précises sur la consommation réelle de la pile pour les fonctions à partir desquelles d'autres fonctions sont appelées !
Pour comprendre la consommation totale, nous devons construire une arborescence d'appels et résumer les piles de toutes les fonctions incluses dans chacune de ses branches. Cela peut être fait, par exemple, avec l'utilitaire
GNU cflow en le définissant sur un ou plusieurs fichiers.
L'échappement ici nous obtenons un ordre de grandeur plus lourd, je n'en donnerai qu'une partie pour la même cible.c:
olegart@oleg-npc /mnt/c/Users/oleg/Documents/Git/dap42 (umdk-emb) $ cflow src/stm32f042/umdk-emb/target.c adc_comp_isr() <void adc_comp_isr (void) at src/stm32f042/umdk-emb/target.c:434>: TIM_CR1() ADC_DR() ADC_ISR() DMA_CCR() GPIO_BSRR() GPIO_BRR() ADC_TR1() ADC_TR1_HT_VAL() ADC_TR1_LT_VAL() TIM_CNT() DMA_CNDTR() DIV_ROUND_CLOSEST() NVIC_ICPR() clock_setup() <void clock_setup (void) at src/stm32f042/umdk-emb/target.c:81>: rcc_clock_setup_in_hsi48_out_48mhz() crs_autotrim_usb_enable() rcc_set_usbclk_source() dma1_channel1_isr() <void dma1_channel1_isr (void) at src/stm32f042/umdk-emb/target.c:404>: DIV_ROUND_CLOSEST() gpio_setup() <void gpio_setup (void) at src/stm32f042/umdk-emb/target.c:1215>: rcc_periph_clock_enable() button_setup() <void button_setup (void) at src/stm32f042/umdk-emb/target.c:1208>: gpio_mode_setup() gpio_set_output_options() gpio_mode_setup() gpio_set() gpio_clear() rcc_peripheral_enable_clock() tim2_setup() <void tim2_setup (void) at src/stm32f042/umdk-emb/target.c:1194>: rcc_periph_clock_enable() rcc_periph_reset_pulse() timer_set_mode() timer_set_period() timer_set_prescaler() timer_set_clock_division() timer_set_master_mode() adc_setup_common() <void adc_setup_common (void) at src/stm32f042/umdk-emb/target.c:198>: rcc_periph_clock_enable() gpio_mode_setup() adc_set_clk_source() adc_calibrate() adc_set_operation_mode() adc_disable_discontinuous_mode() adc_enable_external_trigger_regular() ADC_CFGR1_EXTSEL_VAL() adc_set_right_aligned() adc_disable_temperature_sensor() adc_disable_dma() adc_set_resolution() adc_disable_eoc_interrupt() nvic_set_priority() nvic_enable_irq() dma_channel_reset() dma_set_priority() dma_set_memory_size() dma_set_peripheral_size() dma_enable_memory_increment_mode() dma_disable_peripheral_increment_mode() dma_enable_transfer_complete_interrupt() dma_enable_half_transfer_interrupt() dma_set_read_from_peripheral() dma_set_peripheral_address() dma_set_memory_address() dma_enable_circular_mode() ADC_CFGR1() memcpy() console_reconfigure() tic33m_init() strlen() tic33m_display_string()
Et ce n'est même pas la moitié de l'arbre.
Pour comprendre la consommation réelle de la pile, nous devons prendre la consommation pour
chacune des fonctions qui y sont mentionnées et additionner ces valeurs pour chacune des branches.
Et bien que nous ne prenions toujours pas en compte les appels de fonction par des pointeurs et des interruptions, incl. imbriqués (et spécifiquement dans ce code, ils peuvent être imbriqués).
Comme vous pouvez le deviner, faire cela chaque fois que vous changez le code est, pour le dire légèrement, difficile - c'est pourquoi personne ne le fait habituellement.
Néanmoins, il est nécessaire de comprendre les principes du remplissage de la pile - cela peut conduire à certaines restrictions sur le code du projet, augmentant sa fiabilité du point de vue de la prévention du débordement de la pile (par exemple, interdiction des interruptions imbriquées ou des appels de fonction par des pointeurs), et en particulier -fstack-usage peut grandement aide à l'optimisation du code sur les systèmes avec un manque de RAM.