新し目の AArch64 のクロスコンパイル用ツールチェーンを作ろうとして、かなりハマったのでメモしておきます。
基本的には前回(2019年 4月 29日の日記参照)ご紹介した手順でビルドします。GCC と glibc のコードを変えなくて良い組み合わせは下記の通りです。特に新しいバージョンを使う理由がなければ、この組み合わせが無難です。
私は新しい GCC が使いたかったので、HEAD にしました(バージョン的には 10.0 相当)。どうやら GCC のエラーチェックが厳しくなるらしく、glibc のビルドが通らなくなります。たくさんエラーが出ますが、一例を挙げると、下記のようなエラーです。
./../include/libc-symbols.h:534:26: error: '__EI___errno_location' specifies less restrictive attributes than its target '__errno_location': 'const', 'nothrow' [-Werror=missing-attributes] 534 | extern __typeof (name) __EI_##name \ | ^~~~~
エラーは glibc を新しくすると解決されるかと思いきや、よりおかしなことになります。例えば GCC 8.3 のまま glibc 2.30(おそらく 2.29 でも同じ症状が出る)にすると、下記のような変なエラーが出ます。
crosstool-builder-new-aarch64/buildroot/lib/gcc/aarch64-unknown-linux-gnu/8.3.0/../../../../aarch64-unknown-linux-gnu/bin/ld: crosstool-builder-new-aarch64/build/glibc/support/links-dso-program.o: Relocations in generic ELF (EM: 62) crosstool-builder-new-aarch64/buildroot/lib/gcc/aarch64-unknown-linux-gnu/8.3.0/../../../../aarch64-unknown-linux-gnu/bin/ld: crosstool-builder-new-aarch64/build/glibc/support/links-dso-program.o: Relocations in generic ELF (EM: 62) ...
エラーの原因となっているオブジェクト links-dso-program.o を調べると、AArch64 向けにビルドしているにも関わらず、なぜか x86_64 用のオブジェクトが生成されています。
build/glibc/support$ file *.o echo-container.o: ELF 64-bit LSB relocatable, ARM aarch64, version 1 (SYSV), with debug_info, not stripped links-dso-program.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), with debug_info, not stripped ★★★★これ★★★★ shell-container.o: ELF 64-bit LSB relocatable, ARM aarch64, version 1 (SYSV), with debug_info, not stripped stamp.o: empty test-container.o: ELF 64-bit LSB relocatable, ARM aarch64, version 1 (SYSV), with debug_info, not stripped true-container.o: ELF 64-bit LSB relocatable, ARM aarch64, version 1 (SYSV), with debug_info, not stripped
いったい何ですかね、これ。バグなのか、仕様なのかわかりません……。
GCC と glibc をお互い最新にした組み合わせ、すなわち下記の組み合わせにしたとき、
先ほど説明した、両方のエラーに遭遇してビルドできませんので、glibc にパッチを当ててビルドエラーを回避します。
まずはコンパイルエラーを無視するパッチです。本来はエラーを無視するのではなく、エラーが指摘している事項を直すべきですけど、今回の主眼ではないのと、いずれ glibc 本家が直るだろうことを期待しておきます。
diff --git a/Makeconfig b/Makeconfig
index fd36c58c04..106688e210 100644
--- a/Makeconfig
+++ b/Makeconfig
@@ -916,7 +916,8 @@ ifeq "$(strip $(+cflags))" ""
endif # $(+cflags) == ""
+cflags += $(cflags-cpu) $(+gccwarn) $(+merge-constants) $(+math-flags) \
- $(+stack-protector)
+ $(+stack-protector) \
+ -Wno-zero-length-bounds -Wno-array-bounds -Wno-maybe-uninitialized
+gcc-nowarn := -w
# Each sysdeps directory can contain header files that both will be
次の links-dso-program はコミットログを見る限り、テストのサポート用ライブラリなので、とりあえず無くても動くはずです。ビルド自体をやめるパッチをあてます。
diff --git a/support/Makefile b/support/Makefile
index ab66913a02..19c3de2043 100644
--- a/support/Makefile
+++ b/support/Makefile
@@ -184,12 +184,12 @@ CFLAGS-support_paths.c = \
-DSBINDIR_PATH=\"$(sbindir)\" \
-DROOTSBINDIR_PATH=\"$(rootsbindir)\"
-ifeq (,$(CXX))
-LINKS_DSO_PROGRAM = links-dso-program-c
-else
-LINKS_DSO_PROGRAM = links-dso-program
-LDLIBS-links-dso-program = -lstdc++ -lgcc -lgcc_s $(libunwind)
-endif
+#ifeq (,$(CXX))
+#LINKS_DSO_PROGRAM = links-dso-program-c
+#else
+#LINKS_DSO_PROGRAM = links-dso-program
+#LDLIBS-links-dso-program = -lstdc++ -lgcc -lgcc_s $(libunwind)
+#endif
ifeq (yes,$(have-selinux))
LDLIBS-$(LINKS_DSO_PROGRAM) += -lselinux
クロスコンパイル環境は、各モジュールのバージョンアップですぐ壊れてしまって辛いです。ARM がこれだけ覇権を握っているにも関わらず、gcc も glibc もあまりチェックしてないんですかね……??
わざわざ Makefile を書き換えなくても make LINKS_DSO_PROGRAM= のように、make 実行時に LINKS_DSO_PROGRAM 変数の値を強制的に空文字列に上書きすれば回避可能でした。理由は昔の日記で書いた通り(2019年 9月 17日の日記参照)、コマンドラインからの変数指定は Makefile 内の代入より強いからです。
Makefile 書き換えよりは多少スマートですけども、クロスコンパイルの時だけこんな指定が必要なのは妙ですね。まだ何か見落としているんでしょうかね?
くそ長いですが、C 言語の未定義動作怖いね、printf でタイミング以外も動き変えられるよ、という話です。
環境ですが x86_64 向け Debian GNU/Linux 9.2 で実行しています。また GCC のバージョンは gcc (Debian 9.2.1-22) 9.2.1 20200104 です。
未定義動作のため、コンパイラの種類や、GCC のバージョンにより結果が変わると思われます。お家のマシンで試すならご留意ください。
この日記の最後に貼ったプログラム(このプログラムをコンパイルすると、激しい警告が出ます)を gcc -Wall -O2 a.c && ./a.out のように実行すると、
0: 0 0 0 1: 0 1 0 2: 0 2 0 3: 0 3 0 4: 0 4 0 ... 47: 0 47 0 48: 0 48 0 49: 0 49 0 1770: 1770
こうなります。0〜59 の和は 1770 です。あってます。良かったですね。
なに?そういう問題じゃない?「なぜ array 終端を超えて guard2 にバッファオーバーランしない?」と考えた方、するどいです。しかし世の中そう単純ではありません。
10行目の printf のコメントを外してローカル変数のアドレスを表示させると、
0x7ffd9b348a10 0x7ffd9b348ae0 0x7ffd9b348bb0 0: 0 0 52 1: 0 1 53 2: 0 2 54 3: 0 3 55 4: 0 4 56 ... 45: 0 45 0 46: 0 46 0 47: 0 47 0 48: 0 48 0 49: 0 49 0 1770: 1770
こうなります。突然オーバーランするようになりました。printf が何かしたんでしょうか、不思議ですね?
どうして for ループを無意味に 2分割したのか?くっつけてみたらわかります。Segmentation Fault します。
0: 0 0 0 1: 0 1 0 2: 0 2 0 3: 0 3 0 4: 0 4 0 ... 45: 0 45 0 46: 0 46 0 47: 0 47 0 48: 0 48 0 49: 0 49 0 Segmentation fault
もう意味不明ですよね。何が起こっているんでしょう?
この 60回の for ループは「配列の終端を超えたアクセス」が C 言語仕様上の未定義動作なので、何が起きても正しい、つまりどの結果も正しいです。
これだけだと、何言ってんのか意味不明だと思うので「printf 有効/無効」「for ループ 1つ/2つ」に着目して説明します。
3番目の実験の裏打ちとして、試しにループ回数を 80回くらいにすると for ループが 1つだろうが 2つだろうが、リターンアドレスがぶっ壊れて Segmentation Fault します。10行目の printf を有効にすると guard1, guard2 がスタックに配置されて、受け止めてくれるので、80回でも耐えます。
バッファオーバーランを期待していた向きには残念(?)かもしれませんが、guard1, guard2 はメモリ上に置いても置かなくても、C 言語仕様に矛盾しないなら、どっちでも良いです。もっというと C 言語仕様に矛盾しないなら、コンパイラの最適化は何をやっても OK です。
この「C 言語仕様に矛盾しないなら」はおそらくコンパイラ開発者には常識なのでしょうけども、C 言語の仕様は人間に優しくないのと、大多数の C 言語プログラマは言語仕様(特に未定義動作)を理解しておらず、何となく使っています。
難解な仕様、曖昧な理解、過激な最適化の相乗効果により、今日も世界のどこかで
「最適化で動きが変になっちゃったよ……。どうして…どうして……?」
とコンパイラとすれ違ったプログラマが泣いているでしょう。。。
大したものではありませんが、ソースコードを載せておきます。
#include <stdio.h>
#include <string.h>
int undefined()
{
int guard1[50];
int array[50];
int guard2[50];
int sum = 0, i;
memset(guard1, 0, sizeof(guard1));
memset(guard2, 0, sizeof(guard2));
//printf("%p %p %p\n", &guard1[0], &array[0], &guard2[0]);
for (i = 0; i < 60; i++) {
array[i] = i;
}
for (i = 0; i < 60; i++) {
sum += array[i];
}
for (i = 0; i < 50; i++) {
printf("%2d: %d %d %d\n", i, guard1[i], array[i], guard2[i]);
}
return sum;
}
int main(int argc, char *argv[])
{
int sum1 = 0, sum2 = 0, i;
sum1 = undefined();
for (i = 0; i < 60; i++) {
sum2 += i;
}
printf("%d: %d\n", sum1, sum2);
return 0;
}
先日(2020年 1月 12日の日記参照)の続きです。
あまりにも glibc フルアセンブラ版 memset の実装が速くて勝てないので、観念して実装を見たのですが、序盤(1バイト〜32バイト)が弱い理由と、以降(33バイト〜)で勝てない理由がわかりました。
他の実装と違って glibc はサイズの大きい方から条件を見ています。どうしても条件分岐命令を通る回数が増えるため、序盤に弱いです。
中盤は 96 バイトまでは NEON store x 4 と分岐で捌いていて、ループを使いません。分岐も cmp して branch ではなく、ビットセットされていたら分岐する命令(tbz, tbnz)を使っています(※)。
つまり私が書いた memset はループで処理している時点で、ほぼ勝ち目がなかったということです。
グラフでは 63バイトまでしか測っていなかったから気づかなかったのですが、ループの 2週目に入る 65バイトから、さらにボロ負けです。いやはや、これは勝てないですね……。
(※)cmp, branch の 2命令を tbz 1命令にする辺り、AArch64 アセンブラならではの実装に見えますが、実は C でも if (a & 0x10) とか書くとコンパイラが tbz 命令を使います。コンパイラ侮りがたし。
先日 memset を書いていたとき(2020年 1月 12日の日記参照)に気づいたのですが、glibc のフルアセンブラ版 memset の性能が 2通り(遅い、速い)あることに気づきました。だいたい 1割くらい性能が変わります。
遅いときと比較すると、自作の memset の方が速いですが、速いときと比較するとボロ負けします。割と性能が迫っているためか、影響が大きいです。
何が違うんでしょうね?コードは当然同じですから、違いは memset 関数のロードされるアドレスくらいです。まさかなと思って、スタティックリンクしたら安定して速くなりました。
ダイナミックリンクだと、アプリ側は 0xaaaac4fba560 で、glibc だけ 0xffffbf2dce00 のような遠いアドレスに飛ばされます。ベンチマーク中は、アプリのコード ←→ glibc のコードを頻繁に行き来することになるので、TLB ミスヒットの影響が出ているんですかね……??
真因はわかりませんが、アドレスが関係している可能性は高いです。今後、似たようなことをやるときは、スタティックリンクで測った方が良さそうです。
最近、見かける SIMD 命令セット(AVX も NEON も)には、レジスタ下位 [7:0] の 1バイトを、レジスタ上位 ... [31:24] [23:16] [15:8] の各バイトに配る命令が用意されています。
この命令はどういう需要があるんだろうか……?memset の実装では超役に立ちましたが、他の使い道が良くわかりません。
Facebook で上記の話をしていたところ、
と教えてもらいました。なるほど、スカラベクトル積のスカラ側を配るときに便利ですね。
ちなみに SIMD のない処理系はどうしているのか見てみると、
int a = (何かの数字);
としたときに、
a &= 0xff;
a *= 0x01010101;
のように and, mov, mul を使っていました。もちろん、
a &= 0xff;
a |= a << 8;
a |= a << 16;
のように and, shift, or, shift, or でもできますが、今日日のプロセッサだと整数乗算の方が速そうですね。
< | 2020 | > | ||||
<< | < | 01 | > | >> | ||
日 | 月 | 火 | 水 | 木 | 金 | 土 |
- | - | - | 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 | - |
合計:
本日:
管理者: Katsuhiro Suzuki(katsuhiro( a t )katsuster.net)
This is Simple Diary 1.0
Copyright(C) Katsuhiro Suzuki 2006-2021.
Powered by PHP 5.2.17.
using GD bundled (2.0.34 compatible)(png support.)