最後の記事で 、私はそれを自分で言及し、コメントで尋ねました-わかりました、科学的な突く方法を使用して、スタックのサイズを選択しましたが、何も落ちていないようですが、それが何であり、誰がそんなに食べたかをより確実に評価できますか?
簡単に答えます:はい、しかしいいえ。
いいえ、静的解析の方法を使用すると、プログラムが必要とするスタックのサイズを正確に測定することはできませんが、これらの方法は便利です。
答えはもう少し長いです-カットの下で。
狭い人々に広く知られているように、スタック上の場所は、実際には、現在実行中の関数で使用されるローカル変数に割り当てられます-静的に割り当てられたメモリの変数を除いて、静的に割り当てられたメモリのbssエリアに保存されます関数呼び出し間の意味。
関数が実行されると、コンパイラは必要な変数のためにスタックにスペースを追加し、完了時にこのスペースを解放します。 すべてが単純なように見えますが、-そしてそれは非常に大胆ですが -私たちにはいくつかの問題があります:
- 関数はスタックも必要とする他の関数内で呼び出します
- 関数は、直接参照ではなく、関数へのポインタによって他の関数を呼び出すことがあります
- 原則として、それは可能です-絶対に避けるべきですが-AがBを呼び出すときの再帰的な関数呼び出し
- 割り込みが発生する可能性があります。そのハンドラーは、独自のスタックを必要とする同じ関数です
- 割り込みの階層がある場合、割り込み内で別の割り込みが発生する可能性があります!
明確に、再帰関数呼び出しはこのリストから削除する必要があります。それらの存在はスタックサイズを考慮せず、コードの作成者に意見を表明するための言い訳であるためです。 悲しいかな、他のすべてを消すことはできません(特に微妙な違いがある場合があります:たとえば、RIOT OSのように、すべての割り込みは設計上同じ優先順位を持つことができ、ネストされた割り込みはありません)。
油絵を想像してください:
- スタックで100バイトを消費する関数Aは、50バイトを必要とする関数Bを呼び出します
- B、A自体の実行時には、明らかに、まだ終了していないため、その100バイトは解放されないため、スタックにはすでに150バイトがあります。
- 関数Bは関数Cを呼び出し、ポインターに従ってこれを行います。これは、プログラムロジックに応じて、5〜50バイトのスタックを消費する6個の異なる関数を指すことができます。
- ランタイムCで、比較的長く実行され、20バイトのスタックを消費する重いハンドラーで割り込みが発生します
- 割り込み処理中に、別の優先度の高い割り込みが発生し、そのハンドラーは10バイトのスタックを必要とします
この美しいデザインでは、すべての状況が特にうまく一致するため、少なくとも5つのアクティブな機能 (A、B、C、および2つの割り込みハンドラー)があります。 さらに、それらの1つはスタック消費定数を持っていません。なぜなら、それは異なるパスで単に異なる関数になる可能性があり、互いに割り込みの可能性または不可能性を理解するには、少なくとも異なる優先度の割り込みがあるかどうかを知る必要があります、最大値-それらが互いにオーバーラップできるかどうかを理解する。
明らかに、自動静的コードアナライザーの場合、このタスクは非常に圧倒的であり、上限推定値の大まかな近似でのみ実行できます。
- すべての割り込みハンドラーのスタックを合計する
- 同じコードブランチで実行される関数のスタックを合計する
- 関数とその呼び出しへのすべてのポインターを見つけて、これらのポインターが指す関数の最大スタックサイズをスタックサイズとして取得しようとする
ほとんどの場合、一方では非常に高い推定値が得られ、他方では、ポインターを使用して特に難しい関数呼び出しをスキップする機会が得られます。
したがって、一般的な場合、 このタスクは自動的に解決されません 。 手動による解決策-このプログラムのロジックを知っている人-は、かなりの数の数字を掘る必要があります。
それにもかかわらず、スタックのサイズの静的な推定は、ソフトウェアを最適化するのに非常に役立ちます-少なくとも誰がたくさん食べているかを理解するという単純な目的のために、あまり多くありません。
GNU / gccツールチェーンには、これに非常に役立つツールが2つあります。
- フラグ-fstack-usage
- cflowユーティリティ
-fstack-usageをgccフラグ(たとえば、CFLAGSの行のMakefile)に追加すると、コンパイルされた各 file%filename%.cに対して、コンパイラーはfile%filename%.suを作成し、その中にシンプルでクリアなテキストが含まれます。
たとえば、 この巨大なフットクロスのtarget.suを見てください 。
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
ここに表示される各関数のスタックの実際の消費量を確認します。そこから、いくつかの結論を導き出すことができます。たとえば、RAMが不足した場合、まず最適化を試みる価値があります。
同時に、 このファイルは 、 他の関数が呼び出される関数のスタックの実際の消費に関する正確な情報を実際には提供しません !
総消費量を理解するには、呼び出しツリーを構築し、各ブランチに含まれるすべての関数のスタックを要約する必要があります。 これは、たとえばGNU cflowユーティリティを使用して、1つ以上のファイルに設定することで実行できます。
ここでの排気量は1桁大きくなります。同じtarget.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()
そしてそれはツリーの半分でもありません。
スタックの実際の消費量を理解するには、そこに記載されている各関数の消費量を取得し、各ブランチのこれらの値を合計する必要があります。
そして、ポインターと割り込みによる関数呼び出しはまだ考慮していませんが、 ネストされています(特にこのコードでは、ネストできます)。
ご想像のとおり、コードを変更するたびにこれを行うのは、控えめに言っても難しいことです。だから、通常は誰もしません。
それでも、スタック充填の原則を理解する必要があります-これにより、プロジェクトコードに特定の制限が生じ、スタックオーバーフローの防止(たとえば、ネストされた割り込みやポインターによる関数呼び出しの禁止)に関する信頼性が高まり、特に-fstack-usage RAMが不足しているシステムでのコードの最適化を支援します。