exeGCC から exeClang への移行ガイド¶
もともと SOLID-OS と exeGCC は Clang コンパイラをサポートしているので、標準 C/C++ の範囲のソースコードは修正不要です。Clang コンパイラのバージョンが変わると警告が厳しくなったり、場合によってはエラーになることはありますが、たいていはコンパイラが出力するメッセージに従って容易に修正が可能です。
問題になりやすいのは、スタートアップルーチンとリンカスクリプトです。
スタートアップルーチンの移行ガイド¶
exeGCC は GCC 付属の libgcc、exeClang は LLVM の compiler-rt をコンパイラランタイムライブラリに使用します。基本的に libgcc と compiler-rt は同じ関数を提供し、互換性がありますが、以下の場合に修正が必要です。
AArch64 ターゲットの場合(32-bit ARM は不要です)¶
注釈
exeGCC(AARCH64) s007 以降は、コンパイラに付属のスタートアップルーチンとリンカスクリプトが exeClang と共通化されているため、それらを参考に実装した場合は、以下の修正は不要です。
AArch64 ターゲットは、libgcc を使う場合(exeGCC)、C++ の例外処理のために __register_frame_info() をスタートアップルーチンで呼び出す必要があります。しかし exeClang の compiler-rt は別の仕組みを使用するため、この関数は存在せず、リンクエラーになります。
そのため以下のように、exeClang で有効になる EXECLANG マクロを使用してコメントアウトしてください。(AARCH64 s007 より、必要部分のみを抜粋。)
#if !defined(EXECLANG)
extern void *__EH_FRAME_BEGIN__[]; /* Defined in the linker script. */
extern void __register_frame_info(void *, void *); /* in libgcc */
static int object[16]; /* struct object in unwind-dw2-fde.h */
__register_frame_info(__EH_FRAME_BEGIN__, (void *)&object);
#endif
また、これに付随して exeGCC(AARCH64) s006 以前は以下のように定義されていたリンカスクリプトを
/* BEGIN -- Exception handling */
KEEP (*(.eh_frame_hdr .eh_frame_entry .eh_frame_entry.*))
. = ALIGN(8) ;
__EH_FRAME_BEGIN__ = . ;
KEEP (*(.eh_frame .eh_frame.*))
__EH_FRAME_END__ = . ;
QUAD (0);
KEEP (*(.gcc_except_table .gcc_except_table.*))
KEEP (*(.gnu_extab .gnu_extab* .exception_ranges .exception_ranges*))
/* END -- Exception handling */
以下のように修正してください。(s007 以降の、exeGCC/exeClang 両対応の定義。)
/* BEGIN -- Exception handling */
. = ALIGN(8) ;
__eh_frame_hdr_start = . ; /* libunwind */
.eh_frame_hdr : { KEEP (*(.eh_frame_hdr)) KEEP (*(.eh_frame_entry .eh_frame_entry.*)) }
__eh_frame_hdr_end = . ;
__EH_FRAME_BEGIN__ = . ; /* libgcc */
__eh_frame_start = . ; /* libunwind */
.eh_frame : { KEEP (*(.eh_frame .eh_frame.*)) }
__eh_frame_end = . ;
__EH_FRAME_END__ = . ;
.eh_frame_epilogue : { QUAD (0) }
.gcc_except_table : { KEEP (*(.gcc_except_table .gcc_except_table.*)) }
.gnu_extab : { KEEP (*(.gnu_extab .gnu_extab* .exception_ranges .exception_ranges*)) }
/* END -- Exception handling */
新しく追加された小文字の __eh_frame_XXX シンボルは、compiler-rt によって(暗黙的に)使用されます。これらのシンボルの定義が無い場合、リンクエラーが発生します。
注釈
リンカスクリプトの修正は、次の「リンカスクリプトの移行ガイド」に含めるべき内容ですが、ここでまとめて解説しました。
リンカスクリプトの移行ガイド¶
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.
この仕組みは、以下のようにして実装されています。
非 nothrow 版
operator newの実装を__attribute__((__section__("__lcxx_override")))指定で__lcxx_overrideセクションに配置。nothrow 版の実行時に、対となる非 nothrow 版のアドレスが
__lcxx_overrideセクション(__start___lcxx_overrideと__stop___lcxx_overrideの間)に存在しないことをチェック。(存在しない場合、非 nothrow 版はオーバーライドされている。)もし非 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 は、これまでの議論からすると、変更されていないはずのグローバルなロケーションカウンタを参照しているので、正しく動かないはずです。しかし実際には期待通り動いてしまいます。
これは GNU ld のマニュアル に「the . always refers to a location in an output section」と記述されていることから、セクション終了直後の . は、セクションの終了アドレスを指しているからだと思われます。この lld の挙動(確認時は LLVM 19)は仕様として明確にドキュメント化されているわけではないので互換性は保証されません。しかし実装として自然ですし、わざわざ互換性を壊す理由も無いので、おそらく今後も有効と思われます。(現状、SOLID-OS の内部にも、この挙動に依存したリンカスクリプトが多数存在しています。)
注釈
本来は
.stacks ALIGN(16) (NOLOAD) : {
(省略)
_solid_mon_end = .;
} > solid_ram
のように書くべきですが、もし .stack セクションの位置が変わったり、後ろにセクションを追加した場合に正しく動かなくなる可能性があります。また、意味的にも .stacks セクションに所属しているわけではない、何の関係も無いシンボルがセクション定義内に存在するのは、少々気持ち悪さがあるかもしれません。
このような複数セクションにまたがる開始と終了シンボルを定義したい場合、少し冗長ですが、以下のようにシンボル定義専用のセクションを定義する方法もあります。
SECTIONS {
(省略)
.start_solid_mon (NOLOAD) : { _solid_mon_start = .; } > solid_ram
(省略)
.end_solid_mon (NOLOAD) : { _solid_mon_end = .; } > solid_ram
空セクションは ELF ファイルに出力されないので、その内部のシンボルも出力されないのでは?と心配になるかもしれませんが、サイズ 0 のセクションであっても、内部でシンボルを定義した場合は、必ず出力されることが保証され、シンボルのアドレスはセクションの開始アドレスとなります。これは GNU ld のマニュアル に明記されています。
例 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 のダイナミックローダーの制限については以下の議論を参照してください。
(5)インプットセクションで入力ファイルを指定している場合¶
通常、リンカはコマンドラインまたは INPUT コマンドで指定された .o ファイルを入力ファイルとして扱います。
また、以下のようにインプットセクションにファイル名を記述することにより、入力ファイル内の特定のセクションを任意のセクションに配置することができます。この挙動は GNU ld も lld も同じです。
.foo_data : { foo.o(.data .data.*) }
ただし GNU ld の場合、仮にこの foo.o が(コマンドラインまたは INPUT コマンドで)指定されていなくても、コマンドラインで指定された時と同様の入力ファイルとして扱うという点が lld と異なります。この挙動はマニュアルにも記載されています。(ファイル名にワイルドカード文字が使用されていない場合のみ。)
注釈
https://www.sourceware.org/binutils/docs/ld/Input-Section-Basics.html
When you use a file name which is not an ‘archive:file’specifier and does not contain any wild card characters, the linker will first see if you also specified the file name on the linker command line or in an INPUT command. If you did not, the linker will attempt to open the file as an input file, as though it appeared on the command line. Note that this differs from an INPUT command, because the linker will not search for the file in the archive search path.
例¶
以下は exeGCC(ARM) s007 で、GNU ld を使用した例です。
必要なファイルをコマンドラインで全て指定した場合、当然ですが、正常にリンクできます。
/* foo.c */
int x = 10;
/* main.c */
extern int x;
int main() {return x;}
>gcc -c foo.c
>gcc -c main.c
>gcc -T test.ld foo.o main.o
GNU ld の場合、リンカスクリプトで foo.o を指定しておけば、コマンドラインで foo.o を指定しなくても正常にリンクできます。
/* test.ld */
.data : {
_data = . ;
foo.o(.data .data.*)
*(.data .data.*)
>gcc -T test.ld main.o -Wl,--verbose
...
attempt to open foo.o succeeded
foo.o
しかし、lld の場合はリンクエラーになります。
>gcc -fuse-ld=lld -T test.ld main.o
ld.lld: error: undefined symbol: x
>>> referenced by main.c
>>> main.o:(main)
>>> referenced by main.c
>>> main.o:(main)
collect2.exe: error: ld returned 1 exit status
これは exeClang でも同じです。lld を使用する場合は、コマンドラインか INPUT コマンドで、必要なファイルを全て指定してください。
/* test.ld の先頭に追加 */
INPUT (foo.o)
>gcc -fuse-ld=lld -T test.ld main.o