SNSへはこちら

リンカスクリプトの書き方

組み込み用途でプログラムの配置をする際に必要となるリンカスクリプト。本記事ではその書き方と意味を(備忘録を兼ねて)ご説明します。

複数のサイトで説明がありますが、それほど数は多くないので本記事でもまとめさせてもらいます。アクセス数欲しいし。

参考

以下のサイトを参考にしています。これを見ればたいてい分かります。

リンカスクリプトとは

プログラムを書いたら書き込み時にもちろん ROM に書き込まれます。では書き込みの場所はどうやって決めているのでしょうか?もっと言うと、割り込みベクタ(関数テーブル)はメモリ空間上の位置(アドレス)が決まっています。そのアドレス指定はどうやるのでしょうか?C言語上で指定する?

実際C言語上でアドレス指定は無理と言っていいでしょう。そこでリンカスクリプトを使うことで、コンパイル・バイナリ生成時にアドレスや配置を決める事ができるのです。一般のPCで gcc するときもこれは利用されています。

多くのマイコンでは専用の開発環境、IDE が提供されています。が、例えば昔の 8bit or 16ibt マイコンでよくあった日本メーカーの「開発ツールは個人用途じゃ提供しないよ」状態に遭遇したら環境を自作しなければなりません。またこれが一番の理由ですがリンカスクリプトなるわけのわからないものを理解せず使うとは何事だという人もいると思います(僕だけ?)。ということでよく使われる or これだけ知っていれば作れるぞ!という程度の説明を行います。

なお、リンカはスクリプトに誤り(未定義の変数やセクション)があっても、文法の誤りが無い限りはエラーとして吐いてくれませんので注意。このエラーは基本的に無視されるだけで終わるという害悪仕様です。

エントリポイント等

実際に各種コマンドを書く前に書くべきことがあります。3つほど紹介します。

OUTPUT_FORMAT(elf32-littlemips)
OUTPUT_ARCH(mips)
ENTRY(start)

OUTPUT_FORMAT

出力バイナリのフォーマットです。

OUTPUT_ARCH

出力されるバイナリをどのアーキテクチャ向けにするか指定します。

ENTRY

リセット時に一番最初に実行される関数シンボル名です。

C言語で必要となるセクション

初めてのC言語 – 第9回 リンカスクリプトを参考にしてください。

また、constructor 属性や destructor 属性を使う場合に備えて、*(.init)*(.fini) の配置もしておきましょう。

基本コマンド

主に MEMORYSECTIONS に分かれます。MEMORYは必須ではないですが、これがないと不便極まりないので、今回は必須ということで説明します。

MEMORY

物理的なメモリ領域を定義するコマンドです。例えば RAM だとか、コンフィグ用の ROM だとか。後に SECTIONS コマンドで「この関数は .text セクションにある。それ全体は ROM に置く」と書くことがあるので、そのために定義しておきます。

/* ARM Cortex-M のやつ */
MEMORY { /* memory command defining memory area */
    RAM (xrw): ORIGIN = 0x20000000, LENGTH = 12K /* executable & readable & writable */
    ROM (xrw): ORIGIN = 0x08000000, LENGTH = 64K /* flash rom area */
}

RAM のところを見ていきましょう。この RAM は領域名で、自由に決められます。その後の (xrw)は属性を指定していて、実行・リード・ライト可能という意味です。もっとも、属性は省略可能ですが。

ORIGIN はその領域の開始物理アドレス、LENGTH はその名の通りサイズを指定します。

SECTIONS

セクションを定義、配置します。上の MEMORY コマンドを受けてセクションをどこに置くのかという記述を行うものです。ここでは物理アドレスも仮想アドレスもどちらも記述可能です。まずは記述例です。以下は ARM Cortex-M のものです。

SECTIONS {
    .isr_vector : {
        . = ALIGN(4);
        KEEP(*(.isr_vector)) /* vector array. '*' outside of () means 'any object file'. */
        /* KEEP() does not allow unused symbols into garbage collections. */
        . = ALIGN(4);
    } > ROM /* the function pointer is written on ROM. */

    .text : { /* program area */
        . = ALIGN(4);
        *(.text)
        *(.text*)
        . = ALIGN(4);
    } > ROM
...
}

.isr_vectorではそのような名前のセクションを作っています。まず最初にアラインメントを揃えるためにロケーションカウンタに ALIGN(4)を代入しています。ロケーションカウンタとはメモリでのアドレスを表すもので、何かしらオブジェクトを配置したら自動でインクリメントされるものです。例えば 0x0000 にサイズ 4bytes のオブジェクトを配置したらロケーションカウンタは4だけ加算されて . == 0x0004 となっています。

今回の例では直接ロケーションカウンタに代入をしていて、もし配置位置が 4bytes でアラインメントされていなければインクリメントしてしまうという処理を行っています。

最後の > ROM は、「このセクションを ROM に配置する」という MEMORY コマンドの領域名を記したものです。

続いて中にある記述についてです。*(.isr_vector) がありますが、例えば init.o(.isr_vector)とすると「init.o 内にある、ソースコード上で.isr_vector セクションと指定されたオブジェクトを配置する」という意味です。ちょっとわかりにくいので次の例を見てください。

SECTIONS {
    .text: {
        . = ALIGN(4);
        *(.text)
        *(.text.*)
        init.o(.hello)
    } > ROM
}

init.o(.hello) が追加されていますが、これは「init.o というオブジェクトファイルに記載された、.hello というセクション指定に該当するオブジェクトを配置してね」という意味になります。よって以下のC言語ソースコードが対象になります。

// init.c
__attribute__((section(".hello")))
void func(void) {
    ;
}

これをコンパイル・リンクすると関数 func.text セクションに配置されます。

一方で * が使われていると思うのですがこれはワイルドカードで、() の外部に用いられる場合は「任意のファイル名」であること、() の内部の場合はセクション名に関して任意のマッチを表します。

上では .text だけでなく .text.* がありますが、これは実際にコンパイラの -ffunfction-sections-fdata-sections に対応するために付けておいたほうが良いでしょう。また、KEEP() はリンカによるリンク省略を避けるための対策です。

続いて以下の例も見てみます。

SECTIONS {
...
    _sidata = LOADADDR(.data); /* LOADADDR() returns the address on ROM(AT>ROM), whereas ADDR() RAM(>RAM). */
    .data : { /* variables with its initial value */
        . = ALIGN(4);
        _sdata = .;
        *(.data)
        *(.data*)
        . = ALIGN(4);
        _edata = .;
    } > RAM AT > ROM 
...
}

上では_sidata を定義していますが、これには型はありません。定義したシンボルはアドレスの別名を指すので、使用する際はポインタ型ならば何でも良いのです。LOADADDR に関しては後に述べます。

ここで着目点は AT >です。これはただの > とは異なり、「一旦 ROM に配置する」という意味です。直接 RAM にデータを配置できたら良いのですが、一般に RAM は揮発性なので、一旦 ROM に書いてから、マイコン起動時に RAM にコピーをするという作業をします。このコピーの作業はスタートアップルーチンで行うとC言語では規定されています。

ということで、AT > を使うと「バイナリとしては配置先はとりあえず AT > の部分に、プログラム中の分岐等、アドレスを用いる値は > に」という事になっているわけです。

仮想アドレス と AT

上では物理アドレスをそのまま使っていましたが、仮想アドレスも用いることができます。例えば MIPS アーキテクチャを用いている PIC32 ではよく使うことになります。まずは例です。

_offset_kseg0 = 0x80000000;
SECTIONS {
    .text
    _offset_kseg0 + ORIGIN(ROM) : {
        . = ALIGN(4);
        *(.text)
        *(.text.*)
        *(.rodata)
        *(.rodata.*)
    } AT > ROM
...
}

これまでと違うのは .text の後に謎の数値があることです。そこから : があり中括弧が始まります。ここの部分が仮想アドレスで、外部から見たアドレスとは違って、CPU から見たアドレスになります。

_offset_kseg0 は変数で、こうやって SECTIONS コマンドの外部でも定義できます。実際 PIC32 の仮想アドレスは、物理アドレスから 0x80000000 だけ加算されたものになります。こうするとどうなるかと言うと、以下に列挙します。

  • ロケーションカウンタは物理アドレスのままになる。
  • が、オブジェクト中のアドレスは仮想アドレスになる。
  • 一時格納先を AT > で指定する。

LOADADDR, ADDR, ORIGIN, LENGTH

これらは “関数” ですので、以下にそれぞれ列挙して説明します。

  • LOADADDR() は括弧で囲まれたセクションの一時配置先(AT >)アドレスを値で返す
  • ADDR() は括弧で囲まれたセクションの配置先( >)アドレスを値で返す
  • ORIGIN() は括弧で囲まれた MEMORY 内の領域の ORIGIN アドレスを値で返す
  • LENGTH() は括弧で囲まれた MEMORY 内の領域の LENGTH を値で返す

PROVIDE

変数を weak で定義します。

PROVIDE(end = .);

/DISCARD/

ここに記述された オブジェクトセクションはリンクされません。

SECTIONS {
    /DISCARD/ : {
        *(.tmp)
        *(.tmp.*)
    }
}

だいたいこれで所望のリンカスクリプトは書けるのではないでしょうか。ということで書き方説明の記事でした。