exeGCC から exeClang への移行ガイド

もともと SOLID-OS と exeGCC は Clang コンパイラをサポートしているので、ソースコードの修正は不要です。Clang コンパイラのバージョンが変わると警告が厳しくなったり、場合によってはエラーになることはありますが、たいていはコンパイラが出力するメッセージに従って容易に修正が可能です。

問題になりやすいのはリンカスクリプトです。

リンカスクリプトの移行ガイド

exeGCC の GNU ld リンカと exeClang の LLVM lld リンカは高い互換性があるので、基本的にリンカスクリプトの修正は不要です。

ただし、 以下の場合に修正が必要です。

(1)プロジェクトに C++ を使用している場合

これは GCC と LLVM の、標準 C++ ライブラリの仕様の違いに由来する非互換性です。

以下の標準 C++ ライブラリ関数がリンクされた時、リンカスクリプトの修正が必要になります。

  • void* operator new(size_t);

  • void* operator new[](size_t);

  • void* operator new(size_t, std::align_val_t);

  • void* operator new[](size_t, std::align_val_t alignment);

これらは C++ の operator new 演算子の実装なので、C++ プロジェクトでは常にリンカスクリプトの修正を推奨します。修正は、.text セクションの最後に、以下の定義を追加してください。

__start___lcxx_override = . ;
*(__lcxx_override)
__stop___lcxx_override = . ;

修正例

.text _ram :
{
    _stext = . ; _rom_base = . ;
    KEEP(*crt*.o(.text .text.*))
    *(.text .stub .text.* .gnu.linkonce.t.*)
    *(.plt)

    __start___lcxx_override = . ;
    *(__lcxx_override)
    __stop___lcxx_override = . ;

    . = ALIGN(16);
    _etext  = . ;
}

これは LLVM の標準 C++ ライブラリに備えられた、operator new をオーバーライドする際のプログラムミスを検知する仕組みです。

ただし exeClang のライブラリは例外有効でビルドされているため、このプログラムミスは発生しません。リンカスクリプトの修正は、operator new の実装が正しく .text セクションに配置されるために必要です。

注釈

(以下は LLVM の標準 C++ ライブラリの operator new/delete の実装が C++ 例外無効(-fno-exceptions を使用)でビルドされている場合の話となります。exeClang を使用するだけならば読む必要はありません。)

例えば operator new(size_t) のような非 nothrow 版 operator new をオーバーライドした時、必ず対応する nothrow 版 operator new(size_t, nothrow_t) もオーバーライドしなければなりません。なぜならば、後者は前者を呼び出す可能性があるので、非 nothrow 版がメモリ割り当てに失敗した場合に終了してしまう実装だと、nothrow 版はメモリ割り当てに失敗した場合に nullptr を返すという仕様を満たせなくなる可能性があるからです。

このプログラムミス検知機能と標準入出力が有効な環境で、前述のプログラムミスがあり、オーバーライドされていない(正しく動かない可能性がある)非 nothrow 版 operator new の実装が呼び出された場合、以下のようなエラーメッセージが実行時に表示されてプログラムが終了します。

libc++abi: libc++ was configured with exceptions disabled and `operator new(size_t)` has been overridden,
but `operator new(size_t, nothrow_t)` has not been overridden. This is problematic because
`operator new(size_t, nothrow_t)` must call `operator new(size_t)`, which will terminate in case
it fails to allocate, making it impossible for `operator new(size_t, nothrow_t)` to fulfill its
contract (since it should return nullptr upon failure). Please make sure you override
`operator new(size_t, nothrow_t)` as well.

この仕組みは、以下のようにして実装されています。

  1. 非 nothrow 版 operator new の実装を __attribute__((__section__("__lcxx_override"))) 指定で __lcxx_override セクションに配置。

  2. nothrow 版の実行時に、対となる非 nothrow 版のアドレスが __lcxx_override セクション(__start___lcxx_override__stop___lcxx_override の間)に存在しないことをチェック。(存在しない場合、非 nothrow 版はオーバーライドされている。)

  3. もし非 nothrow 版がオーバーライドされている場合、上記メッセージを表示してアサーションエラー。(対となる自分自身が正しくオーバーライドされていれば、このチェック自体が存在しない。)

この仕組みを実現する operator new/delete の実装は libc++abi と libc++ の両方に存在し、デフォルト( LIBCXXABI_ENABLE_NEW_DELETE_DEFINITIONS=ON , LIBCXX_ENABLE_NEW_DELETE_DEFINITIONS=OFF )では libc++abi の実装が使用されます。exeClang も libc++abi の実装を使用しています。

(2)``MEMORY`` コマンドを使用している場合

lld では、 リンカスクリプト内で MEMORY コマンドを使用している場合、アウトプットセクション(以後「アウトプット」を省略)の定義の外側でロケーションカウンタ( . )を変更しても、セクション定義の内側でロケーションカウンタを参照した時、外側での変更は反映されません。セクションは MEMORY コマンド内で定義された、いずれかのメモリーリージョン(以後「メモリー」を省略)にアサインされていて、それぞれのリージョンが個別のロケーションカウンタを保持しているからです。

つまり、ロケーションカウンタ( . )の参照や変更は、原則としてアウトプットセクションの定義の内側でのみ行うように修正してください、という結論になります。

しかし、この説明だけではなかなか理解が難しいと思うので、以下に SOLID-OS で実際に問題になった例と、正しい(期待通りに動作する)書き方を示します。

注釈

GNU ld のマニュアルMEMORY コマンドに関する記述は簡素で、付属のリンカスクリプトも MEMORY コマンドを使用していないなど、公式の情報源は乏しいです。そのため本マニュアルの解説は GNU ld および LLVM lld の公式の見解ではなく、あくまでも本ドキュメントの執筆者がマニュアルから読み解いた仕様と実際の動作に基づいたものにすぎない、ということに注意してください。

例 1

SOLID-OS のリンカスクリプトには、以下のようにロケーションカウンタをセクションの外側で変更している記述が至る所にありましたが、lld では期待通りに動作しませんでした。

注釈

MEMORY コマンドを使用していない場合は問題なく動作します。

MEMORY {
    solid_ram (RWX) : ORIGIN = _smm_SOLID_PhysicalAddress, LENGTH = _smm_SOLID_Size
}

(省略)

SECTIONS {

(省略)

    . = ALIGN(8); /* セクション定義の外側でロケーションカウンタを変更しても */

    .data : {

        /* 定義の内側で . を参照した時、8 バイトアラインされていません */

    } > solid_ram

GNU ld のマニュアルには明記されていませんが、lld のこの挙動は、 MEMORY コマンドの意味を考えると妥当と思われます。

MEMORY コマンドでリージョンを定義した場合、ロケーションカウンタは 1 つではなく、 定義されたリージョンの数だけ存在すると考えられるからです。

GNU ld のマニュアルにはそのような用語は記載されていませんが、ここで便宜上、「グローバルな」ロケーションカウンタの他に、リージョンの数だけ「リージョンローカルな」ロケーションカウンタが存在すると考えてみると、アウトプットセクションの外側での「グローバルな」(つまり、どのリージョンにも所属していない、どのリージョンにも影響を与えない)ロケーションカウンタの変更が、リージョンの(ローカルな)ロケーションカウンタに影響を与えないのは、妥当で正しい挙動に思われます。

(そもそも、 MEMORY コマンドを使用した場合、グローバルなロケーションカウンタは使用するべきではないのだろうと思われます。実際、インターネットを検索してみると、 MEMORY コマンドを使用しているリンカスクリプトで、セクションの外側でロケーションカウンタを参照や変更している例は見つけられませんでした。)

セクションの先頭アドレスを 8 バイトアラインしたい場合は、以下のように記述してください。

.data ALIGN(8) : {

注釈

弊社の SOLID 開発メンバーから、「LLD のマニュアルを見ると ALIGN() はコロン( : )の右側に定義するのが正しいのではないか?」という質問がありました。

https://lld.llvm.org/ELF/linker_script.html#output-section-description

section [address] [(type)] : [AT(lma)] [ALIGN(section_align)] [SUBALIGN](subsection_align)] {
  output-section-command
  ...
} [>region] [AT>lma_region] [:phdr ...] [=fillexp] [,]

これは非常に紛らわしいのですが、このコロンの左側の ALIGN(8) は、 構文ではなく GNU LD および LLD の組み込み関数です。

https://sourceware.org/binutils/docs/ld/Builtin-Functions.html

ALIGN(align) 関数は、現在のロケーションカウンタを align バイトアラインした値を返します。その値をセクションの開始アドレス [addresss] に指定しています。

コロンの右側に ALIGN() を記述した場合は ALIGN(section_align) 構文扱いとなり、アウトプットセクションがインプットセクション(関数や変数のシンボル)を扱う際のアライメントを指定したことになり、アウトプットセクションの開始アドレス(暗黙的に現在のロケーションカウンタが使われます)は変わりません。

例 2

これは逆に、期待通り動かないように思われるのに、動いてしまう例です。

SECTIONS {

    /* セクション定義の外側で . を変更し、外側で参照しているので問題なし */

    . = _smm_SOLID_PhysicalAddress;

    _ram_start = .;
    _solid_mon_start = .;

    .text ALIGN(8) : {
        (省略)
    } > solid_ram

    (省略)

    .stacks ALIGN(16) (NOLOAD) : {
        (省略)
    } > solid_ram

    /* 外側で . の変更は一切行われていないにもかかわらず */

    _solid_mon_end = .; /* 正しく .stacks セクションの終了アドレスを取得できる */

最初の _solid_mon_start などは、セクションの外側でグローバルな . を変更して、外側で参照しているので問題ありません。しかし最後の _solid_mon_end は、これまでの議論からすると、変更されていないはずのグローバルなロケーションカウンタを参照しているので、正しく動かないはずです。しかし実際には期待通り動いてしまいます。

ドキュメント化されていない挙動と思われますが、どうもセクション終了直後の . は、セクションの終了アドレスを指しているように思われます。

本来は

.stacks ALIGN(16) (NOLOAD) : {
    (省略)
    _solid_mon_end = .;
} > solid_ram

のように書くべきですが、もし .stack セクションの位置が変わったり、後ろにセクションを追加した場合に正しく動かなくなる可能性があります。また、意味的にも .stacks セクションに所属しているわけではない、何の関係も無いシンボルがセクション定義内に存在するのは、少々気持ち悪さがあるかもしれません。

様々な理由で SOLID-OS では実際このような書き方をしている所が多々ありますが、本来はあまり良くないと理解しておくことは重要です。

また、この lld の挙動は LLVM 19 時点のものですが、仕様としてドキュメント化されているわけではないので、今後の互換性は保証されません。

例 3

以下の例では、RAM の先頭から 64KB 離れたアドレスに .solid_cs セクションが配置されることを期待しています。

しかし、 MEMORY コマンドを使用していて、 .solid_cs セクションは solid_ram に配置するように指示されています。この時、これまでの例と同様に、セクションの外側でのロケーションカウンタの変更は無視されるので、期待通りに動作しません。

MEMORY {
    solid_ram (RWX) : ORIGIN = _smm_SOLID_PhysicalAddress, LENGTH = _smm_SOLID_Size
}

(省略)

SECTIONS {

    (省略)

    . = _smm_SOLID_PhysicalAddress + 64K;

    .solid_cs ALIGN(4K) : {
        (省略)
    } > solid_ram

正しい(期待通りに動く)書き方はいろいろ考えられますが、今回は RAM の先頭の 64KB はブートローダーが使用するという仕様になっているのて、 .solid_cs は必ず _smm_SOLID_PhysicalAddress + 64K から開始しなければいけませんでした。

そこで、以下のように直接セクションの開始アドレスを記述しました。(4KB アラインなのは自明なので省略されています。)

.solid_cs (_smm_SOLID_PhysicalAddress + 64K) : {
    (省略)
} > solid_ram

本来は、このようにアドレスを直書きしてしまうと、自動でセクションをリージョンに配置するという MEMORY コマンドの利点が失われるので、あまり良い書き方ではないと思われます。

まとめ

MEMORY コマンドを使う場合、ロケーションカウンタ( . )の参照や変更は、アウトプットセクションの定義の内側でのみ行うようにしましょう。