リンカスクリプトを理解しよう

GNU Cコンパイラを使いこなすために、リンカスクリプトを理解することは必須条件です。しかし、あまり飯のタネにならないこの特殊なスクリプトを理解するためには、そこそこのモチベーションが必要とされます。

普通は何らかの雛形があって、それをちょっと変更して動かしてみるというケースが多いのではないでしょうか。今までそうやって、その場しのぎで対応してきたリンカスクリプトに、今回は真正面から立ち向かってみたいと思います。

複雑なリンカスクリプトを読み解く

一般的なリンカスクリプトの説明はGNU Cのldコマンドのマニュアルや、よそのサイトでもやっていますので、このコーナーとしては、より実践的なスクリプトファイルを読み解くことにします。

解析するターゲットはRZ/A1LのLチカプログラムで使ったあのリンカスクリプトファイルです。全体で260行ぐらいありますが、これを順番に見ていくことにします。

全体の構造を把握する

まずは全体の構造を把握しましょう。見出し的な部分だけをピックアップすると以下のような形になっています。全体の構造を見れば楽勝っぽい感じもします。

MEMORY {}

SECTIONS
{
    .text : {} > ROM
    .data : {} > SYSTEM_RAM AT > ROM
    .bss : {} > CACHED_RAM
    .stack : {} > STACK
    .uncached_RAM (NOLOAD) : {} > UNCACHED_RAM
    .uncached_RAM2 : {} > UNCACHED_RAM AT > ROM
    .text2 : {} > ROM
    .data2 : {} > CACHED_RAM AT > ROM
    .bss2 : {} > CACHED_RAM
    .dummy : {} > ROM = 0x00
}

大きく分けてMEMORYとSECTIONSという項目があります。リンカのマニュアルによるとこれらはコマンドというようです。SECTIONSコマンドの中にはセクションの配置が細かく指定されています。

MEMORYコマンドの定義

まず最初はMEMORYコマンドが定義されています。

MEMORY {
    ROM          (rx)  : ORIGIN = 0x18000000, LENGTH = 128M
    SYSTEM_RAM   (rwx) : ORIGIN = 0x20020000, LENGTH = 0x04000
    STACK        (rw)  : ORIGIN = 0x20024000, LENGTH = 0x14000
    CACHED_RAM   (rw)  : ORIGIN = 0x20039000, LENGTH = 0x100000
    UNCACHED_RAM (rw)  : ORIGIN = 0x60139000, LENGTH = 0x100000
}

この定義は、システム全体の物理的なメモリ構成を決めます。ROMとかSYSTEM_RAMという名前は任意に付けることができます。この名前は、SECTIONSコマンドでオブジェクトの出力先を指定するために使用します。

ORIGINは配置する物理メモリのアドレスを指定し、LENGTHはそのサイズを指定します。サイズを指定することで、リンク時にオブジェクトのサイズがチェックされ、サイズがオーバーするとリンクエラーになります。LENGTHの指定にはM(メガ)、K(キロ)などの表現が可能ですので、0x100000は1Mと書き換えることができます。また、ORIGINを「O」、LENGTHを「L」と短縮して書くこともできます。

CEVボードでは0x18000000番地より128Mバイトのシリアルフラッシュメモリが搭載されており、0x20000000番地以降は、3Mバイトの内蔵RAM領域です。UNCACHED_RAMとして指定されている0x60139000番地は、0x60000000番地から始まる内蔵RAMのミラーイメージで、0x20000000番地の内容と全く同じものです。

2次キャッシュはアドレスの範囲で指定できますので、内蔵RAMの後半部分のミラーイメージを利用して、非キャッシュ領域に割り当てているんですね。

.textセクションの定義

次はSECTIONSコマンドの中を見ていきましょう。最初は.textセクションが定義されています。 .textセクションはおもにプログラムのコードが配置されるセクションです。普通の組込みシステムではROMに配置されます。

.text : {
    . = 0x00000000;
    * (VECTOR_TABLE)            /*asm*/

    . = 0x00000200;
    * (RESET_HANDLER)           /*asm*/
    * (INIT_TTB)                /*asm*/
    * (INITCA9CACHE)            /*asm*/

    * (CODE_BASIC_SETUP)
    */peripheral_init_basic.o (.text .text.*)
    */bsc.o (.text .text.*)
    */bsc_userdef.o (.text .text.*)

    * (CONST_BASIC_SETUP)
    */peripheral_init_basic.o (.rodata .rodata.*)
    */bsc.o (.rodata .rodata.*)
    */bsc_userdef.o (.rodata .rodata.*)

        :(途中省略)

} > ROM

まず、一番最後の行を見ますと、「> ROM」と書かれています。これは.textセクションがMEMORYで指定されたROM領域に出力されることを示しています。つまり、0x18000000番地です。

先頭に戻りまして、最初の行から見ていきましょう。

. = 0x00000000;

「.」ドットに0を代入していますが、ドットはロケーションカウンタといって、現在の配置アドレスを示しています。オブジェクトが1バイト出力されれば、ロケーションカウンタも1バイト加算されます。ロケーションカウンタの値を見れば、オブジェクトが何バイト出力されたかを知ることができます。

ここではロケーションカウンタを0にしていますが、実際の配置アドレスは、ROMで指定されたアドレスとロケーションカウンタが加算された値となりますので、0x18000000番地となります。ロケーションカウンタは、値を代入するだけではなく「label = . ; 」のように値を参照することも可能です。

以降の行はだいたい同じパターンで、

* (VECTOR_TABLE) 

のように書かれています。「*」は任意のオブジェクトファイル名に一致するワイルドカードの指定で、VECTOR_TABLEはソースコード内で定義されたセクション名です。これは、ソースファイルvector_tables.sで、以下のように定義されています。

.section VECTOR_TABLE

このことから、Lチカプログラムの最初に配置されるセクションはVECTOR_TABLEで、オブジェクトコードは0x18000000番地から出力されることになります。

次の行はパス付のオブジェクトファイル名を指定しています。

*/peripheral_init_basic.o (.text .text.*)

「*/」で任意のパスに配置されたperipheral_init_basic.oの.textセクションと.text.で始まるセクションを出力します。

もし、一致するファイル名がない場合やセクション名がない場合はどうなるんでしょう?
実際のリンカの動作を見ると、何事もなかったかのように無視されるようです。ファイルがないとか、セクションがないとかそういったエラーにはなりませんので、間違ったセクション名を指定して、正しくリンクされないということにならないよう注意が必要です。

先ほどの定義と似ていますが、この定義はどうでしょうか。

*/peripheral_init_basic.o (.rodata .rodata.*)

.textが.rodataとなっている以外は同じです。.rodataとはconst宣言された変更不可能なデータのセクションです。変更不可能なデータですから、普通はROM 上に配置されるように指定します。

.dataセクションの定義

.dataは書き換え可能な、初期値を持つ変数を配置するためのセクションです。例えば次に示す例のように、グローバル変数や、static変数を宣言するときに初期値があると.dataセクションに配置されます。

char str[] = "abcde";       // グローバル変数
static int var = 0x1234;    // static変数

.dataセクションの定義は以下のようになっています。一見ややこしそうですが、よく見れば3つのブロックで構成されていることが分かります。

.data : {
    . = 0x00000000;
    __vect_load  = .;
    __vect_start = LOADADDR(.data) + ( __vect_load - ADDR(.data) );

    Image$$VECTOR_MIRROR_TABLE$$Base = .;
    * (VECTOR_MIRROR_TABLE)         /*asm*/

    __vect_end =  LOADADDR(.data) + ( . - ADDR(.data) );

    /********************************************************************/

    . = 0x00000100;
    __fastcode_load  = .;
    __fastcode_start = LOADADDR(.data) + ( __fastcode_load - ADDR(.data) );

    * (CODE_HANDLER_JMPTBL)
    */intc_userdef.o (.text .text.*)

    * (CONST_HANDLER_JMPTBL)
    */intc_userdef.o (.rodata .rodata.*)

        :(途中省略)

    __fastcode_end = LOADADDR(.data) + ( . - ADDR(.data) );

    /********************************************************************/

    __data_load  = .;
    __data_start = LOADADDR(.data) + ( __data_load - ADDR(.data) );

    * (DATA_HANDLER_JMPTBL)
    */intc_userdef.o (.data)

    * (DATA_BASIC_SETUP)
    */peripheral_init_basic.o (.data)

        :(途中省略)

    * (.data .data.*)

    __data_end = LOADADDR(.data) + ( . - ADDR(.data) );

} > SYSTEM_RAM AT > ROM

例によって、まず最後の行を見ますと、「> SYSTEM_RAM AT > ROM」となっています。.textで見た指定とは違って、「AT > ROM」というキーワードが追加されていますね。これは、実際にプログラムが動作するときのアドレス(SYSTEM_RAM)と、データを保持するためのアドレス(ROM)を指定するやり方です。

.dataというのは初期値を持ったデータですので、電源を切っても消えないところにその初期値を保持しておく必要があります。組込みシステムでは普通、ROMに保持するのですが、そのときの指定が「AT > ROM」なんです。

そして、その内容をまるごとRAM領域(SYSTEM_RAM)にコピーします。なぜRAMにコピーするかというと、.dataセクションは書き換え可能な変数領域だからです。もし、「int var = 1;」と宣言された変数がROM領域のままだと、「var = 20;」のような書き換えができないことになります。

「変数のアドレス(&var)はコピー先のRAMを指しているの?」と疑問に思われるかもしれませんが、問題ありません。変数のアドレスはRAMを指しつつ、オブジェクトはROM領域に配置するというのが「AT > ROM」の機能です。

では、RAM領域へのコピーはだれがするのでしょう。これは、main関数を実行する前のスタートアップルーチンがやってくれるのが普通です。Lチカのプログラムではinitsect.sというアセンブラソースでコピーしています。

さらに突っ込んで、ではROMの領域のどこからどこまでをコピーするのでしょう。
ヒントは以下の行にあります。

. = 0x00000000;
__vect_load  = .;
__vect_start = LOADADDR(.data) + ( __vect_load - ADDR(.data) );

    :(途中省略)

__vect_end =  LOADADDR(.data) + ( . - ADDR(.data) );

これらの意味するところですが、まず最初の行でロケーションカウンタを0に設定しています。その次に「__vect_load = . ; 」でロケーションカウンタの値を変数__vect_loadに代入します。実際にはロケーションカウンタと、「> SYSTEM_RAM」で示されるアドレスを足した値、0x20020000が代入されます。

次の行、「__vect_start  = ・・・」では「LOADADDR」と「ADDR」という関数が使われています。この2つの関数の意味ですが、LOADADDR(.data)は.dataがROM上での配置アドレスを返し、ADDR(.data)は.dataが実際に使われるときのRAM上の配置アドレスを返します。計算すると、

__vect_start = LOADADDR(.data) + ( __vect_load - ADDR(.data) );
             ↓
__vect_start = 0x18000xxx + ( 0x20020000 - 0x20020000 );
             ↓
__vect_start = 0x18000xxx + 0;

のようになります。つまり、これは.dataが配置されるROMのアドレスを求めています。

同様に「__vect_end  = ・・・」は出力したオブジェクトのROM上の終了アドレスを求めています。

__vect_end = LOADADDR(.data) + ( . - ADDR(.data) );
                    ↓
__vect_end = 0x18000xxx + ( 0x20020yyy - 0x20020000 );
                    ↓
__vect_end = 0x18000xxx + yyy;

最終的に__vect_startから__vect_endまでが、出力オブジェクトのアドレス範囲を示しています。この変数を使えば、コピー元のROMの領域分かりますので、コピーすることができるわけです。なるほど、うまいやりかたですね。

ちなみに、リンカスクリプトでシンボルに代入を行うと、そのシンボルはC言語やアセンブラから参照できるグローバルな変数となります。ただし、C言語から扱う場合は以下のように、外部参照宣言してからvoidポインタでアクセスします。

extern short __vect_start;
extern short __vect_end;
        :
void *p_start = &__vect_start;
void *p_end = &__vect_end;

リンカスクリプトの変数はC言語では型が未定義となります。ここではshortで宣言していますが、void以外なら何でもかまいません。

.dataセクションは、__vect_load、__fastcode_load、__data_loadの3つで構成されています。これらを区別することで、システム構成に合わせてコピー先を変えるのだと思われます。例えば__fastcode_loadは速いメモリに配置するとか・・・。

.bssセクションの定義

.bssセクションも.dataと同じく変数の格納領域となっています。.dataとの違いは変数の宣言時に初期値がないことです。

.bss : {
    __bss_start = .;

    * (BSS_HANDLER_JMPTBL)
    */intc_userdef.o (.bss)

    * (BSS_BASIC_SETUP)
    */peripheral_init_basic.o (.bss)
    */bsc.o (.bss)
    */bsc_userdef.o (.bss)

    * (BSS_HANDLER)
    */intc_handler.o (.bss)

    * (BSS_FPU_INIT)            /*nothing*/

    * (BSS_RESET)
    */port_init.o (.bss)
    */stb_init.o (.bss)
    */resetprg.o (.bss)
    */l2_cache_init.o (.bss)

    * (BSS_IO_REGRW)
    */rza_io_regrw.o (.bss)

    __bss_end = .;
} > CACHED_RAM

.bssは.dataと違って、コピーするデータを持ちませんので、そのままRAMに配置します。ただし、C言語の仕様で、初期値の無いグローバル変数やstatic変数は0に初期化すべしとありますので、.bssセクションはスタートアップルーチンで0にクリアします。

.stackセクションの定義

.stackセクションはスタックの領域ですが、ここではArmのアーキテクチャ特有の書き方になっていますので、少し複雑です。

.stack : {
    . = ALIGN( 0x10 );
    Image$$ARM_LIB_STACK$$ZI$$Base = .;
    . += 0x00008000;
    Image$$ARM_LIB_STACK$$ZI$$Limit = .;

    . = ALIGN( 0x10 );
    Image$$IRQ_STACK$$ZI$$Base = .;
    . += 0x00002000;
    Image$$IRQ_STACK$$ZI$$Limit = .;

    . = ALIGN( 0x10 );
    Image$$FIQ_STACK$$ZI$$Base = .;
    . += 0x00002000;
    Image$$FIQ_STACK$$ZI$$Limit = .;

    . = ALIGN( 0x10 );
    Image$$SVC_STACK$$ZI$$Base = .;
    . += 0x00002000;
    Image$$SVC_STACK$$ZI$$Limit = .;

    . = ALIGN( 0x10 );
    Image$$ABT_STACK$$ZI$$Base = .;
    . += 0x00002000;
    Image$$ABT_STACK$$ZI$$Limit = .;

    . = ALIGN( 0x4000 );
    Image$$TTB$$ZI$$Base = .;
    . += 0x00004000;
    Image$$TTB$$ZI$$Limit = .;
} > STACK

最初に「.= ALIGN( 0x10 );」とあるのはロケーションカウンタを16バイトに境界調整しています。スタックはキリのいい数字(普通は4バイト境界)を好みますので、こういう調整が必要です。もし、境界調整をせずにスタックが奇数アドレスになった場合は、スタックのアクセス時に即、アボート例外が発生します。

その次の行からを理解するには、Armのアーキテクチャの例外処理を抑えておく必要があります。

Armの例外処理にはリセット、割り込み、アボートなどがあります。Armは例外が発生すると、レジスタバンクとなっているR13(つまり、スタックポインタ)を切り替えて、例外用のスタックで動作します。この.stackセクションで用意されているのが、まさに例外用のスタックなのです。

ここではロケーションカウンタに0x8000や0x2000バイトを加算して、Image$$で始まる変数にアドレスを代入しています。この変数をソースコードで参照して、例外用のスタックポインタを初期化しています。初期化コードはreset_handler.sで以下のように書かれています。

    LDR sp, =Image$$SVC_STACK$$ZI$$Limit

    CPS #IRQ_MODE
    LDR sp, =Image$$IRQ_STACK$$ZI$$Limit

    CPS #FIQ_MODE
    LDR sp, =Image$$FIQ_STACK$$ZI$$Limit

    CPS #ABT_MODE
    LDR sp, =Image$$ABT_STACK$$ZI$$Limit

    CPS #SYS_MODE
    LDR sp, =Image$$ARM_LIB_STACK$$ZI$$Limit

CPS命令でCPUの動作モードを切り替えて、各々の例外SPを初期化しているんですね。

.stackの最後に書かれている以下の行ですが、境界調整が16KBも指定されていて、他と比べてなんか異質です。

    . = ALIGN( 0x4000 );
    Image$$TTB$$ZI$$Base = .;
    . += 0x00004000;
    Image$$TTB$$ZI$$Limit = .;

実はこれ、MMUで使用するTTBというアドレス変換テーブルなんです。「LチカプログラムでMMUなんか使っていないでしょ」と思われるかもしれませんが、実際には2次キャッシュを設定するときに使用しています。Armの2次キャッシュはMMUで設定するのです。

その他のセクション

あとはザコ的な扱いになってしまいますが、ざっと見ていきましょう。

.uncached_RAM (NOLOAD) : {
    * (BSS_RIIC_SAMPLE)                 /* RIIC sample transfer work area */
    * (BSS_RSPI_SAMPLE)                 /* RSPI sample transfer work area */
    * (BSS_SCIF_SYNC_SAMPLE)            /* SCIF_SYNC sample work area */
    * (BSS_NANDNC)                      /* FLCTL work */
} > UNCACHED_RAM

このセクションは非キャッシュエリアに配置されますが、「NOLOAD」の指定があるため、オブジェクトの実体として出力されません。このセクションは、Lチカプログラムでは使用していませんでしたが、通信用のバッファとして確保しているようです。ですから、非キャッシュになっているものと推察されます。

次はキャッシュ関連をまとめているようです。

.uncached_RAM2 : {
    __cache_operation_load  = .;
    __cache_operation_start = LOADADDR(.uncached_RAM2) + ( __cache_operation_load - ADDR(.uncached_RAM2) );

    * (L1_CACHE_OPERATION)
    * (CODE_CACHE_OPERATION)
    */cache.o (.text .text.*)

    * (CONST_CACHE_OPERATION)
    */cache.o (.rodata .rodata.*)

    * (DATA_CACHE_OPERATION)
    */cache.o (.data)

    * (BSS_CACHE_OPERATION)
    */cache.o (.bss)

    __cache_operation_end = LOADADDR(.uncached_RAM2) + ( . - ADDR(.uncached_RAM2) );
} > UNCACHED_RAM AT > ROM

キャッシュ関連の操作は非キャッシュ領域で動作させる必要があるため、最初にROMに配置して、後から非キャッシュ領域にコピーして実行するようになっています。

残りのセクションは、今までにどのセクションにも該当しなかったオブジェクトを救い上げるためにあるようです。マップファイルを見ると分かりますが、実際にはこれらの定義は機能していません。

.text2 : {
    * (.text .text.*)
    * (.rodata .rodata.*)
} > ROM

.data2 : {
    __data2_load  = .;
    __data2_start = LOADADDR(.data2) + ( __data2_load - ADDR(.data2) );

    * (.data .data.*)

    __data2_end = LOADADDR(.data2) + ( . - ADDR(.data2) );
} > CACHED_RAM AT > ROM

.bss2 : {
    __bss2_start = .;

    * (BSS_DMAC_SAMPLE_INTERNAL_RAM)    /* DMAC sample transfer source work area */

    * (.bss .bss.*)
    * (COMMON)

    __bss2_end = .;

    . = ALIGN( 0x4 );

    end = .;
    . = . + 0x00080000;
    stack_ptr = .;

} > CACHED_RAM

.dummy : {
    . = 0x0100;
} > ROM = 0x00

以上で、Lチカプログラムのリンカスクリプトを見てきました。

リンカスクリプトの仕様としてはまだまだ奥深いものがありますが、それはまた次の機会にしたいと思います。