exeGCC から exeClang への移行ガイド

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

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

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

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

ただし、 以下の場合に修正が必要になることがあります。

(1)セクションの開始アドレスよりも大きなアライメントを要求する変数が存在する場合

より正確には、アウトプットセクション(以後「アウトプット」を省略)の開始アドレス sh_addr が指定されていて、セクション中の変数が要求する最大のアライメントを sh_addralign とした時、sh_addr % sh_addralign == 0 が満たされていない場合、lld は警告を出し、条件を満たすようにセクションの開始アドレスを変更(sh_addr を sh_addralign アライメント)します。

たいていの場合この警告は無害(無視したとしても、アライメントは適切に行われます)ですが、セクションが必ず特定のアドレスから開始しなければいけない場合には注意が必要です。

以下に具体例を示します。

/* qemu_warn.ld */

.data : /* 開始アドレス sh_addr を指定していない */
{
    (省略)

    . = ALIGN(8);
    _edata = . ;
}

.bss _edata : /* 開始アドレスを指定している */
{
    (省略)
}

この時、 .bss セクション内に、例えば以下のような 16 バイトアライメントを要求する型の変数が存在し(sh_addralign == 16)

/* test_lld.c */

int a[1000] __attribute__((aligned(16)));

int main() {
  return 0;
}

なおかつ _edata が 8 バイトアライメントしか満たしていない(アドレスの末尾が 8)時、以下のような警告が出ます。

arm-kmc-eabi-ld.lld: warning: address (0x40006668) of section .bss is not a multiple of alignment (16)

この警告は、セクションの開始や終了アドレスのアライメントを 8 バイトに設定していることが多い ARM(32-bit)で発生しやすいと思われます。(AArch64 では常に FPU と NEON 拡張が有効で、128-bit 型をユーザープログラムが使用していなくてもコンパイラが生成する場合があるため、GNU ld でもアライメントは 16 バイト確保していることが多く、問題になりにくいと思われます。)

exeGCC でも clang と lld を使用した場合は発生するので、デフォルトの GNU ld と挙動を比較することができます。

GNU ld の場合、同じプログラムとリンカスクリプトでも警告は出ません。(以下は exeGCC4(ARM) s007 の出力結果です。GNU ld のバージョンは GNU Binutils 2.38 です。)

>clang test_lld.c -T qemu_warn.ld

一方、clang と lld(s007 の GCC は lld をサポートしていません)を使用すると、exeClang と同様に警告が出ます。

>clang test_lld.c -fuse-ld=lld -T qemu_warn.ld
ld.lld: warning: address (0x40003f78) of section .bss is not a multiple of alignment (16)

この場合は、プログラムの内容に応じて、適切なアライメントを指定してください。

/* qemu_ok.ld */

.data :
{
    (省略)

    . = ALIGN(16);
    _edata = . ;
}
>clang test_lld.c -fuse-ld=lld -T qemu_ok.ld

注釈

この GNU ld と lld の挙動の違いは、以下のようにドキュメント化されています。

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

The ELF specification says:

> The value of sh_addr must be congruent to 0, modulo the value of sh_addralign.

The presence of address can cause the condition unsatisfied.
LLD will warn.
GNU ld from Binutils 2.35 onwards will reduce sh_addralign so that sh_addr=0 (modulo sh_addralign).

lld は警告を出しますが、Binutils 2.35 以降の GNU ld は sh_addr % sh_addralign == 0 条件を満たすまで、sh_addralign を減少させます。つまり、開始アドレスは変わりません。

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

注釈

これは GNU ld と lld ではなく、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 の実装を使用しています。

(3) 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 コマンドを使う場合、ロケーションカウンタ( . )の参照や変更は、セクションの定義の内側でのみ行うようにしましょう。

(4) RELRO セクションが単一の連続領域になっていない場合

RELRO(RELocation Read Only)セクションとは、再配置情報を格納する読み取り専用セクションのことです。再配置情報は SOLID インテリジェントローダー など、動的リンクでのみ使用されるので、基本的にユーザーが気にする必要はありません。

RELRO は動的リンクのセキュリティを高めるために使用されます。ライブラリが動的リンクされている場合、実行時にダイナミックローダーが再配置情報を元にライブラリをリンクします。もしプログラムの実行中に再配置情報が書き換え可能であると、攻撃者が任意のコードを実行できる非常に危険な脆弱性が発生する可能性があります。そのため動的リンクが終わった後の再配置情報は読み取り専用であることが望ましいので、コンパイラは再配置情報を RELRO セクションに生成します。

ELF の仕様では RELRO セクションおよびそれが連続した RELRO セグメントの数に制限はありませんが、Linux(glibc)のダイナミックローダは(おそらく実装の単純化や高速化などの理由で)単一の RELRO セグメントのみをサポートします。GNU ld は RELRO セグメントが複数存在(全ての RELRO セクションが連続していない)していてもエラーにならず警告も出ませんが、lld は全ての RELRO セクションが連続していない場合はリンクエラーになります。

ld.lld : error : section: .tdata is not contiguous with other relro sections

このエラーが発生するのは、SOLID-OS が用意する DLL 用リンカスクリプトの lld 対応が不完全であるか、ユーザーがリンカスクリプトを修正した際に、RELRO セクションの間に新たな非 RELRO セクションを追加した場合のみとなります。その場合は、全ての RELRO セクションが連続するようにリンカスクリプトを修正する必要があります。

注釈

lld がこのような実装になっている理由は以下の議論を参照してください。

https://reviews.llvm.org/D40359

glibc のダイナミックローダーの制限については以下の議論を参照してください。

https://reviews.llvm.org/D40029