LoginSignup
3
2

More than 1 year has passed since last update.

関数の引数に指定する関数呼び出し順序がclangとgccで異なる

Last updated at Posted at 2022-10-22

はじめに

C/C++ のアンチパターンだけど警告されない上、clangとgccで動きが異なること(題記の通り 関数の引数に指定する関数呼び出し順序がclangとgccで異なる という事)に起因して作り込んだ極悪なバグにエンカウントして、原因追求に大分苦労しました。

という訳で情報を共有します。

実装例(C++)

#include <stdio.h>

class Test {
  public:
    int value;
    Test() { value = 0; }
    int get() { return ++value; }
};

int main()
{
    Test test;
    printf("%d%d\n", test.get(), test.get());
    return 0;
}

実行結果1(clang++)

% clang++ test.cpp 
% ./a.out 
12

引数2 → 引数3 の順次で関数呼び出しされる。

コンパイラバージョン:

% clang++ -v
Apple clang version 14.0.0 (clang-1400.0.29.102)
Target: x86_64-apple-darwin21.6.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin

実行結果2(g++)

$ g++ test.cpp
$ ./a.out
21

引数3 → 引数2 の順次で関数呼び出しされる。

コンパイラバージョン:

$ g++ -v
組み込み spec を使用しています。
COLLECT_GCC=g++
COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-redhat-linux/7/lto-wrapper
ターゲット: x86_64-redhat-linux
configure 設定: ../configure --enable-bootstrap --enable-languages=c,c++,objc,obj-c++,fortran,ada,go,lto --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enable-shared --enable-threads=posix --enable-checking=release --enable-multilib --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --enable-linker-build-id --with-gcc-major-version-only --with-linker-hash-style=gnu --enable-plugin --enable-initfini-array --with-isl --enable-libmpx --enable-libsanitizer --enable-gnu-indirect-function --enable-libcilkrts --enable-libatomic --enable-libquadmath --enable-libitm --with-tune=generic --with-arch_32=x86-64 --build=x86_64-redhat-linux
スレッドモデル: posix
gcc バージョン 7.3.1 20180712 (Red Hat 7.3.1-15) (GCC) 

AWS/EC2 の Amazon Linux2 で適当にインストールした g++ ですが、g++-10 とかでも同様の結果でした。

参考: 極悪なバグ(実際のやらかし箇所)

先述した「極悪なバグ」の内容は次の通りです。

- unsigned short addr = ctx->make16BitsFromLE(ctx->fetch(3), ctx->fetch(3));
+ unsigned char l = ctx->fetch(3);
+ unsigned char h = ctx->fetch(3);
+ unsigned short addr = ctx->make16BitsFromLE(l, h);
  • ctx->fetch : メモリから1バイト読み込む
    • プログラムカウンタが インクリメント される(=順序性が動作に影響する)
  • make16BitsFromLE : 引数1を下位8bits、引数2を上位8bitsとして16bitsにする

機械的な対処方法が無いかも?

直感的には clang の呼び出し順であって欲しいと想定していましたが、gccで逆順となっているということはコンパイラ依存(言語規格として未規定のパターン)だと思われます。

【追記】 コメントで教えて頂いた以下のカンファレンスで紹介されてました。
https://wiki.sei.cmu.edu/confluence/display/c/EXP30-C.+Do+not+depend+on+the+order+of+evaluation+for+side+effects
以下、該当箇所抜粋します。

Noncompliant Code Example
The order of evaluation for function arguments is unspecified. This noncompliant code example exhibits unspecified behavior but not undefined behavior:
【翻訳】 関数の引数の評価順序は未定義です。この非準拠のコード例は、未定義の動作ではなく、未指定の動作を示している。

extern void c(int i, int j);
int glob;
 
int a(void) {
  return glob + 10;
}

int b(void) {
  glob = 42;
  return glob;
}
  
void func(void) {
  c(a(), b());
}

It is unspecified what order a() and b() are called in; the only guarantee is that both a() and b() will be called before c() is called. If a() or b() rely on shared state when calculating their return value, as they do in this example, the resulting arguments passed to c() may differ between compilers or architectures.
【翻訳】 a()とb()がどのような順序で呼ばれるかは不定であり、唯一の保証はc()が呼ばれる前にa()とb()の両方が呼ばれることである。この例のように、a()やb()が戻り値を計算する際に共有状態に依存している場合、c()に渡される引数はコンパイラやアーキテクチャによって異なる可能性があります。

機械的に検出可能なパターンなので警告を出して欲しいところですが、clang では -Wall はもちろん -Weverything でも警告が出ませんでした。g++ でも(上記はバージョンが古いですがg++-10あたりでも) -Wall でも警告が出ません。

現状 clang or gcc のコンパイラ頼みでは機械的な対処方法が無い...のかな?

試しに g++ -Q --help=warnings で出てきた -W オプションを片っ端から付与してみましたが怒られませんでした...

$ g++ -Wabi -Wabi-tag -Waddress -Waggregate-return -Waggressive-loop-optimizations -Waliasing -Walign-commons -Wall -Walloc-zero -Walloca -Wampersand -Wargument-mismatch -Warray-bounds -Warray-temporaries -Wassign-intercept -Wattributes -Wbad-function-cast -Wbidi-chars -Wbool-compare -Wbool-operation -Wbuiltin-declaration-mismatch -Wbuiltin-macro-redefined -Wc++-compat -Wc++0x-compat -Wc++11-compat -Wc++14-compat -Wc++17-compat -Wc++1z-compat -Wc-binding-type -Wc90-c99-compat -Wc99-c11-compat -Wcast-align -Wcast-qual -Wchar-subscripts -Wcharacter-truncation -Wchkp -Wclobbered -Wcomment -Wcomments -Wcompare-reals -Wconditionally-supported -Wconversion -Wconversion-extra -Wconversion-null -Wcoverage-mismatch -Wcpp -Wctor-dtor-privacy -Wdangling-else -Wdate-time -Wdeclaration-after-statement -Wdelete-incomplete -Wdelete-non-virtual-dtor -Wdeprecated -Wdeprecated-declarations -Wdesignated-init -Wdisabled-optimization -Wdiscarded-array-qualifiers -Wdiscarded-qualifiers -Wdiv-by-zero -Wdouble-promotion -Wduplicate-decl-specifier -Wduplicated-branches -Wduplicated-cond -Weffc++ -Wempty-body -Wendif-labels -Wenum-compare -Werror-implicit-function-declaration -Wexpansion-to-defined -Wextra -Wfloat-conversion -Wfloat-equal -Wformat -Wformat-contains-nul -Wformat-extra-args -Wformat-nonliteral -Wformat-overflow -Wformat-security -Wformat-signedness -Wformat-truncation -Wformat-y2k -Wformat-zero-length -Wframe-address -Wfree-nonheap-object -Wfunction-elimination -Whsa -Wignored-attributes -Wignored-qualifiers -Wimplicit -Wimplicit-fallthrough -Wimplicit-function-declaration -Wimplicit-int -Wimplicit-interface -Wimplicit-procedure -Wincompatible-pointer-types -Winherited-variadic-ctor -Winit-self -Winline -Wint-conversion -Wint-in-bool-context -Wint-to-pointer-cast -Winteger-division -Wintrinsic-shadow -Wintrinsics-std -Winvalid-memory-model -Winvalid-offsetof -Winvalid-pch -Wjump-misses-init  -Wline-truncation -Wliteral-suffix -Wlogical-not-parentheses -Wlogical-op -Wlong-long -Wlto-type-mismatch -Wmain -Wmaybe-uninitialized -Wmemset-elt-size -Wmemset-transposed-args -Wmisleading-indentation -Wmissing-braces -Wmissing-declarations -Wmissing-field-initializers -Wmissing-format-attribute -Wmissing-include-dirs -Wmissing-noreturn -Wmissing-parameter-type -Wmissing-prototypes -Wmultichar -Wmultiple-inheritance -Wnamespaces -Wnarrowing -Wnested-externs -Wnoexcept -Wnoexcept-type -Wnon-template-friend -Wnon-virtual-dtor -Wnonnull -Wnonnull-compare -Wnormalized -Wnull-dereference -Wodr -Wold-style-cast -Wold-style-declaration -Wold-style-definition -Wopenmp-simd -Woverflow -Woverlength-strings -Woverloaded-virtual -Woverride-init -Woverride-init-side-effects -Wpacked -Wpacked-bitfield-compat -Wpadded -Wparentheses -Wpedantic -Wplacement-new -Wpmf-conversions -Wpointer-arith -Wpointer-compare -Wpointer-sign -Wpointer-to-int-cast -Wpragmas -Wproperty-assign-default -Wprotocol -Wpsabi -Wreal-q-constant -Wrealloc-lhs -Wrealloc-lhs-all -Wredundant-decls -Wregister -Wreorder -Wrestrict -Wreturn-local-addr -Wreturn-type -Wscalar-storage-order -Wselector -Wsequence-point -Wshadow -Wshadow-compatible-local -Wshadow-ivar -Wshadow-local -Wshift-count-negative -Wshift-count-overflow -Wshift-negative-value -Wshift-overflow -Wsign-compare -Wsign-conversion -Wsign-promo -Wsized-deallocation -Wsizeof-array-argument -Wsizeof-pointer-memaccess -Wstack-protector -Wstrict-aliasing -Wstrict-null-sentinel -Wstrict-overflow -Wstrict-prototypes -Wstrict-selector-match -Wstringop-overflow -Wsubobject-linkage -Wsuggest-attribute=const -Wsuggest-attribute=format -Wsuggest-attribute=noreturn -Wsuggest-attribute=pure -Wsuggest-final-methods -Wsuggest-final-types -Wsuggest-override -Wsurprising -Wswitch -Wswitch-bool -Wswitch-default -Wswitch-enum -Wswitch-unreachable -Wsync-nand -Wsynth -Wsystem-headers -Wtabs -Wtarget-lifetime -Wtautological-compare -Wtemplates -Wterminate -Wtraditional -Wtraditional-conversion -Wtrampolines -Wtrigraphs -Wtype-limits -Wundeclared-selector -Wundef -Wundefined-do-loop -Wunderflow -Wuninitialized -Wunknown-pragmas -Wunreachable-code -Wunsafe-loop-optimizations -Wunsuffixed-float-constants -Wunused -Wunused-but-set-parameter -Wunused-but-set-variable -Wunused-const-variable -Wunused-dummy-argument -Wunused-function -Wunused-label -Wunused-local-typedefs -Wunused-macros -Wunused-parameter -Wunused-result -Wunused-value -Wunused-variable -Wuse-without-only -Wuseless-cast -Wvarargs -Wvariadic-macros -Wvector-operation-performance -Wvirtual-inheritance -Wvirtual-move-assign -Wvla -Wvolatile-register-var -Wwrite-strings -Wzero-as-null-pointer-constant -Wzerotrip -frequire-return-statement test.cpp 
cc1plus: 警告: コマンドラインオプション ‘-Waliasing’ は Fortran 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Walign-commons’ は Fortran 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wampersand’ は Fortran 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wargument-mismatch’ は Fortran 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Warray-temporaries’ は Fortran 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wassign-intercept’ は ObjC/ObjC++ 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wbad-function-cast’ は C/ObjC 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wc++-compat’ は C/ObjC 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wc-binding-type’ は Fortran 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wc90-c99-compat’ は C/ObjC 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wc99-c11-compat’ は C/ObjC 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wcharacter-truncation’ は Fortran 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wcompare-reals’ は Fortran 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wconversion-extra’ は Fortran 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wdeclaration-after-statement’ は C/ObjC 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wdesignated-init’ は C/ObjC 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wdiscarded-array-qualifiers’ は C/ObjC 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wdiscarded-qualifiers’ は C/ObjC 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wduplicate-decl-specifier’ は C/ObjC 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wfunction-elimination’ は Fortran 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wimplicit’ は C/ObjC 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wimplicit-function-declaration’ は C/ObjC 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wimplicit-int’ は C/ObjC 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wimplicit-interface’ は Fortran 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wimplicit-procedure’ は Fortran 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wincompatible-pointer-types’ は C/ObjC 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wint-conversion’ は C/ObjC 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Winteger-division’ は Fortran 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wintrinsic-shadow’ は Fortran 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wintrinsics-std’ は Fortran 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wjump-misses-init’ は C/ObjC 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wline-truncation’ は Fortran 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wmissing-parameter-type’ は C/ObjC 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wmissing-prototypes’ は C/ObjC 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wnested-externs’ は C/ObjC 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wold-style-declaration’ は C/ObjC 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wold-style-definition’ は C/ObjC 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Woverride-init’ は C/ObjC 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Woverride-init-side-effects’ は C/ObjC 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wpointer-sign’ は C/ObjC 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wpointer-to-int-cast’ は C/ObjC 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wproperty-assign-default’ は ObjC/ObjC++ 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wprotocol’ は ObjC/ObjC++ 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wreal-q-constant’ は Fortran 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wrealloc-lhs’ は Fortran 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wrealloc-lhs-all’ は Fortran 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wselector’ は ObjC/ObjC++ 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wshadow-ivar’ は ObjC/ObjC++ 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wstrict-prototypes’ は C/ObjC 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wstrict-selector-match’ は ObjC/ObjC++ 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wsurprising’ は Fortran 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wtabs’ は Fortran 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wtarget-lifetime’ は Fortran 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wtraditional’ は C/ObjC 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wtraditional-conversion’ は C/ObjC 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wundeclared-selector’ は ObjC/ObjC++ 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wundefined-do-loop’ は Fortran 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wunderflow’ は Fortran 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wunsuffixed-float-constants’ は C/ObjC 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wunused-dummy-argument’ は Fortran 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wuse-without-only’ は Fortran 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-Wzerotrip’ は Fortran 用としては有効ですが、C++ 用としては有効ではありません
cc1plus: 警告: コマンドラインオプション ‘-frequire-return-statement’ は Go 用としては有効ですが、C++ 用としては有効ではありません
In file included from /usr/include/stdio.h:41:0,
                 from test.cpp:1:
/usr/include/libio.h:156:8: 警告: padding struct size to alignment boundary [-Wpadded]
 struct _IO_marker {
        ^~~~~~~~~~
/usr/include/libio.h:247:9: 警告: padding struct to align ‘_IO_FILE::_IO_read_ptr’ [-Wpadded]
   char* _IO_read_ptr; /* Current read pointer */
         ^~~~~~~~~~~~
/usr/include/libio.h:280:15: 警告: padding struct to align ‘_IO_FILE::_lock’ [-Wpadded]
   _IO_lock_t *_lock;
               ^~~~~
test.cpp: コンストラクタ ‘Test::Test()’ 内:
test.cpp:6:5: 警告: ‘Test::value’ should be initialized in the member initialization list [-Weffc++]
     Test() { value = 0; }
     ^~~~

という訳で、「こういうコードは書かないようにする」ぐらいしか今の所有効な対処方法が無いかもしれません。(静的解析サービスとかならあるかもしれませんが)

今回のケースを事前に特定できたポイント & 再発防止策

幸いにも、この問題は clang と gcc で挙動が異なってくれるので事前に問題を特定できました。

恐らく、clang だけでテストしていたら不具合に気づくことができず、この極悪なバグが対策されずにマージされていたと思います。

とある事情でローカルでは clang、CIでは gcc でテストをする形になっていたことが功を奏しました。

複数コンパイラを使った自動テストをCIで行うこと が、この問題に対する一番有効な対処策かもしれません。

という訳で「CI では clang/gcc の両方 で 今回の不具合を検出したテストプログラム(Z80 Instruction Exerciser)を実行する」という再発防止策を実施しました。

ci:
+	@echo Test zexall with gcc
	g++-10 -std=c++2a $(COMMON_FLAGS) -Wclass-memaccess cpm.cpp -lstdc++ -o cpm
	./cpm -e -n zexall.cim
+	@echo Test zexdoc with clang
+	clang -std=c++17 $(COMMON_FLAGS) cpm.cpp -lstdc++ -o cpm
+	./cpm -e -n zexdoc.cim

これで、少なくとも今回やらかしたリポジトリでは(仮に私の手が離れても)同じ過ちを繰り返すことはないと思います。

3
2
7

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2