- 現場のコードでたまに「__attribute__((section(".my_code")))」「#pragma section "my_code" text」という記述を見るけど、これは一体どんなことをやっているの?
- セクションは変数やプログラムコードをROM/RAMのどこに配置するかを決めるものらしいけど、何のために配置を決めるの?
- セクションと併せてリンカスクリプトというものも編集しなくてはいけないらしいけど、これは何をするためのもの?
これらの疑問に回答します。
セクションやリンカスクリプトはマイコン制御を行う上では必須の設定になるものの、こちらを解説した書籍やネット記事は驚くほど数がありません。僕自身何度かここの設定をしたことがありますが、それもあくまでコーディングルールに沿って機械的に実施したのみで、中身の機能についてはよく理解していませんでした。
現在であれば僕も中身については理解していますので、初心者でも分かるようにこの記事で解説していきたいと思います。
セクションとは?
マイコンのプログラムを書く際に、「セクション」という概念がよく出てきます。これは、プログラムのデータやコードを特定のメモリ領域に配置する仕組みのことを指します。
セクションとは?
C言語のプログラムをコンパイルすると、コードやデータはROMやRAMの決められた領域に格納されます。この配置を管理するために、セクション(section)という仕組みが使われます。
例えば、以下のように分けられます。
- .text セクション:プログラムの実行コード(関数の処理)を格納
- .data セクション:初期値を持つ変数(グローバル変数など)を格納
- .bss セクション:初期値を持たない変数を格納
- .rodata セクション:変更されないデータ(定数など)を格納
各セクションの役割
セクション名 | 内容 | 例 |
---|---|---|
.text | プログラムの実行コード | void func() { /* 何かの処理 */ } |
.data | 初期値を持つ変数 | int x = 10; |
.bss | 初期値なしの変数 | int y; (デフォルトで0になる) |
.rodata | 読み取り専用データ | const char msg[] = "Hello"; |
セクションを明示的に指定する方法
組み込み開発では、特定の変数を特定のメモリ領域に配置したいことがあります。その場合、セクションを指定することができます。
例えば、__attribute__(GCCコンパイラ)や#pragma(ルネサス系コンパイラ)を使うと、以下のように特定のセクションに変数を配置できます。またセクションについては自作することも可能です。(セクションを自作した場合は後述するリンカスクリプトでの追記が必要になります)
1 2 |
__attribute__((section(".my_section"))) int special_var = 42; |
1 2 3 |
#pragma section my_section int special_var = 42; #pragma section //セクション指定の終わりを明示する |
これにより、special_var は .my_section という特別なセクションに配置されます。
なぜセクションを使うのか?
あるべきデータをあるべきメモリに配置することでプログラムを正しく動作させるためにセクションは使われるのですが、最近は特にセクションを指定していいなくてもコンパイラ側が自動で判断して最適な位置に配置してくれるそうです。
そしてもうひとつセクションを使う目的としては「どのコンポーネントがどれだけROM/RAMを使用しているか一発で分かるようになる」というメリットがあります。
先ほどセクションは自作することが可能と言いましたが、特定のコンポーネントのROM/RAMを特定のセクションに配置しておくとあとからコンパイルツールを使えばセクションごとのメモリ使用量が一覧で見えるようになります。
リンカスクリプトファイルについて
簡単に言うと、
「コンパイルしてできたプログラムを、マイコンのどこに置くかを指示する設計図」
です。
リンカスクリプトがやること
リンカスクリプトファイルは「***.ld」という拡張子で表され、中身は以下のようになっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
MEMORY /* メモリ領域(ROM、RAM)の情報を定義開始 */ { ROM (rx) : ORIGIN = 0x08000000, LENGTH = 512K /* ROM領域を定義(読み取り・実行可能)、開始アドレス0x08000000、サイズ512KB */ RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K /* RAM領域を定義(読み取り・書き込み・実行可能)、開始アドレス0x20000000、サイズ128KB */ } SECTIONS /* プログラムの各セクション(コードやデータ)をメモリにどう配置するかを定義開始 */ { .text : { /* 出力セクション .text を定義(実行コード+定数をまとめる領域) */ *(.text) /* 入力セクション .text(関数やプログラムコード)をすべてここにまとめる */ *(.rodata) /* 入力セクション .rodata(const定義された定数データ)をここにまとめる */ } > ROM /* この .text セクションを ROM領域に配置する指示 */ .data : { /* 出力セクション .data を定義(初期値ありのグローバル変数領域) */ *(.data) /* 入力セクション .data(初期値ありの変数)をすべてここにまとめる */ } > RAM AT> ROM /* .data は実行時にRAMに配置されるが、初期値はROMに格納されているという指示 */ .bss : { /* 出力セクション .bss を定義(初期値なしのグローバル変数領域) */ *(.bss) /* 入力セクション .bss(初期化されない変数)をすべてここにまとめる */ } > RAM /* この .bss セクションを RAM領域に配置する指示 */ } |
MEMORY/SECTIONブロックでやることについて
・MEMORYブロック
→マイコンが持つ物理メモリの範囲を定義。 ROMは512KB、RAMは128KB、など
・SECTIONブロック
→プログラムやデータをどこに置くか決める。 .textはROM、.dataと.bssはRAM
自作のセクションをリンカスクリプトファイルに追加する方法
先ほどセクションのお話で「.my_section」という領域を作成する例を挙げました。
この場合のリンカスクリプトファイルの修正の仕方を下記に示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
MEMORY { ROM (rx) : ORIGIN = 0x08000000, LENGTH = 512K /* ROM領域を定義(読み取り・実行可能) */ RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K /* RAM領域を定義(読み取り・書き込み・実行可能) */ } SECTIONS { .text : { /* 出力セクション .text を定義(実行コード+定数をまとめる領域) */ *(.text) /* .textセクション(関数コードなど)をここにまとめる */ *(.rodata) /* .rodataセクション(const定数データなど)もここにまとめる */ } > ROM /* これらはROMに配置する */ .my_section : { /* 出力セクション .my_section を定義(初期値ありの自作変数をまとめる領域) */ *(.my_section) /* C側でsection(".my_section")指定されたデータをここにまとめる */ } > RAM AT> ROM /* 実行時はRAM上だが、初期値はROMに格納される設定 */ .data : { /* 出力セクション .data を定義(通常の初期値ありグローバル変数) */ *(.data) /* .dataセクションのデータをまとめる */ } > RAM AT> ROM /* .dataも同様にRAMに展開、初期値はROMに保存 */ .bss : { /* 出力セクション .bss を定義(初期値なしのグローバル変数) */ *(.bss) /* .bssセクションのデータをまとめる */ } > RAM /* .bssはRAM上にゼロクリア配置 */ } |
14~16行目が追加されたところですね。
実質は.dataと同じ扱いなので、こちらを真似して.my_sectionを追加すればOKです。
セクション定義をしていると出来ること
セクションを定義していると、そこで定義している変数だけ定周期で値をインクリメントするということも可能です。
ですのでカウンタ変数だけ特定のセクションに配置してしまえば、そこに配置されている変数は勝手にカウントアップという動作も出来るのです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#include <stdint.h> /* インクリメント対象のセクションに配置する変数群 */ __attribute__((section(".inc_var_section"))) volatile uint16_t counter1 = 0; /* インクリメント対象 その1 */ __attribute__((section(".inc_var_section"))) volatile uint16_t counter2 = 0; /* インクリメント対象 その2 */ /* セクション範囲の先頭アドレスと末尾アドレスをリンカスクリプトで定義するため、外部参照する */ extern uint16_t __start_inc_var_section; extern uint16_t __stop_inc_var_section; /* 1秒ごとにインクリメントする処理関数 */ void increment_section_variables(void) { volatile uint16_t* p = &__start_inc_var_section; /* セクションの先頭アドレスを取得 */ while (p < &__stop_inc_var_section) /* セクション末尾までループ */ { (*p)++; /* 現在の変数をインクリメント */ p++; /* 次の変数へ */ } } |
以上が「セクション」と「リンカスクリプト」の解説でした。
コンパイラが関わってくるところはなかなか書籍でもネット記事でも解説がされていないので、本記事でみなさんの理解が深まれば良いなと思います!