Windows端末でCppCheckとAddressSanitizer(Asan)を使ってみました。
WSLインストール
Windows Subsystem for Linux (WSL) を使えば、Windows上でLinuxを動かすことができます。
PowerShellを管理者として実行し、以下のコマンドを実行します。
WSL --install
画面の案内に従って、ユーザ名とパスワードを適当に決めて入力します(ユーザ名としてadminとrootは使えないことを確認済み)。
インストール完了後、Windows上にLinuxのターミナルが立ち上がります。(WinキーからUbuntuを起動させてもよいです)
GCCインストール
Ubuntuでは、build-essentialをインストールすることにより、g++やmakeも一緒にインストールされます。
sudo apt update
sudo apt install -y build-essential
aptでうまくいかなかったらapt-getも試してみてください。
CppCheckインストール
sudo apt update
sudo apt install -y cppcheck
AddressSanitizerインストール
sudo apt update
sudo apt install -y gcc libasan6
サンプルコード
いかにも怪しい以下のサンプルコードに対してCppCheckとAddressSanitizerを使ってみます。
#include <stdio.h>
int main(int argc, char *argv[]) {
for (int i = 0; i <= argc; i++) {
puts(argv[i]);
}
return 0;
}
CppCheck使用
- 実行コマンド
cppcheck --enable=all main.c
- 出力結果
Checking main.c ...
main.c:1:2: information: Include file: <stdio.h> not found. Please note: Cppcheck does not need standard library headers to get proper results. [missingIncludeSystem]
#include <stdio.h>
^
main.c:3:26: style: Parameter 'argv' can be declared as const array [constParameter]
int main(int argc, char *argv[]) {
^
nofile:0:0: information: Active checkers: 119/975 (use --checkers-report=<filename> to see details) [checkersReport]
informationはともかく、
エントリポイントmainの引数argvはconst配列として宣言できますよ、と言われていますが、開発環境によっては、const char *argv[]と記述すると「エントリポイントmain関数の形式が正しくない」としてコンパイルエラーや警告が出ることがありますので、「ここは例外」ということをCppCheckに知らせましょう。
#include <stdio.h>
// cppcheck-suppress constParameter
int main(int argc, char *argv[]) {
for (int i = 0; i <= argc; i++) {
puts(argv[i]);
}
return 0;
}
あと、informationの出力も気になるなら、--suppressオプションで除外することもできます。※--suppressの引数には[]内のIdを指定します。
- 再実行
cppcheck \
--enable=all \
--inline-suppr \
--suppress=missingIncludeSystem \
--suppress=checkersReport \
main.c
- 出力結果
Checking main.c ...
最終的にはこのような感じになるかも(生成AIによる回答です)
mkdir -p /tmp/cppcheck_build
cppcheck \
--enable=all \
--check-level=exhaustive \
--inconclusive \
--force \
--max-ctu-depth=4 \
--platform=unix64 \
--library=std.cfg \
--library=posix.cfg \
--std=c++20 \
-I . \
-j $(nproc) \
--cppcheck-build-dir=/tmp/cppcheck_build \
--output-file=cppcheck-report.txt \
--suppress=missingIncludeSystem \
--suppress=checkersReport \
--inline-suppr \
src/
尚、
-
main.cの部分にディレクトリを指定した場合、ディレクトリ内すべての.cpp,.cについてチェックします -
--enableオプションで細かくチェックレベルを設定することもできます
AddressSanitizer使用
-fsanitize=address -gオプションを付けてgccコンパイルした後、実行します。
- 実行コマンド
gcc -fsanitize=address -g -Wall -Wextra -Werror main.c
./a.out
- 出力結果
AddressSanitizer:DEADLYSIGNAL
=================================================================
==7216==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x7a5ae634b326 bp 0x7ffd2624ff70 sp 0x7ffd2624f708 T0)
==7216==The signal is caused by a READ memory access.
==7216==Hint: address points to the zero page.
#0 0x7a5ae634b326 in __sanitizer::internal_strlen(char const*) ../../../../src/libsanitizer/sanitizer_common/sanitizer_libc.cpp:176
#1 0x7a5ae62789b7 in puts ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:1278
#2 0x569fe1b95203 in main main.c:5
#3 0x7a5ae5e2a600 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:59
#4 0x7a5ae5e2a717 in __libc_start_main_impl ../csu/libc-start.c:360
#5 0x569fe1b950e4 in _start (a.out+0x10e4) (BuildId: af9dd74b126ce8c4dc3ac6aa7aff6e8902eaae51)
==7216==Register values:
rax = 0x0000000000000000 rbx = 0x0000000000000000 rcx = 0x0000000000000000 rdx = 0x00007ffd262500e0
rdi = 0x0000000000000000 rsi = 0x000077eae51e0080 rbp = 0x00007ffd2624ff70 rsp = 0x00007ffd2624f708
r8 = 0x0000000000000008 r9 = 0x0000000000000000 r10 = 0x0000000000000000 r11 = 0x0000000000000202
r12 = 0x00007ffd262500d8 r13 = 0x0000000000000001 r14 = 0x00007a5ae699a000 r15 = 0x0000569fe1b97d78
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV main.c:5 in main
==7216==ABORTING
main.cの5行目puts(argv[i]);でSEGVが発生しました。これは、ループをi=argcまで回しており、argv[argc]という配列境界外アクセスを検知したからだ… というわけではないです。
実はargv[argc]はNULLであることがC言語の標準規格で定められており、結果的にputs(NULL);という未定義動作が起きたのですが、未定義動作を検知するのは次のUndefinedBehaviorSanitizer(UBSan)の仕事です。
UndefinedBehaviorSanitizer使用
- 実行コマンド
gcc -fsanitize=undefined -g -Wall -Wextra -Werror main.c
./a.out
- 出力結果
main.c:5:3: runtime error: null pointer passed as argument 1, which is declared to never be null
main.cの5行目puts(argv[i]);にて、NULLであってはならない第1引数にnullptr(NULL)が渡された、と怒られました。
そこで、main.cを修正して、再度ASan, UBSanを使ってみましょう。ASan、UBSanは同時に使え、-fsanitize=address,undefinedと指定します。
#include <stdio.h>
// cppcheck-suppress constParameter
int main(int argc, char *argv[]) {
for (int i = 0; i < argc; i++) {
puts(argv[i]);
}
return 0;
}
- 再実行
gcc -fsanitize=address,undefined -g -Wall -Wextra -Werror main.c
./a.out
- 出力結果(
argv[0]が出力されます)
./a.out
ちなみに、-gオプションを付けてコンパイルしたということは、gdbも使えますよ。興味のある方は試してみてください。
-fsanitize=addressや-fsanitize=address,undefinedを付けてコンパイルしたプログラムは、リリース用ではありません。メモリも多く使いますし実行速度も遅いです。リリースする場合は-fsanitize=addressや-fsanitize=address,undefinedを付けずにコンパイルしてください。
makefile作成
ここからは本題から外れますが、折角WSLをインストールしてLinux環境で開発できるようになったので。makefile作成に挑戦したいと思います。詳しい作り方は他の記事を参考にしてください。
CC = gcc
CFLAGS = -Wall -Wextra -Werror
TARGET = main
SRCS = main.c
all: $(TARGET)
$(TARGET): $(SRCS)
$(CC) $(CFLAGS) $(SRCS) -o $(TARGET)
check:
cppcheck --enable=all --inline-suppr --suppress=missingIncludeSystem --suppress=checkersReport $(SRCS)
debug: $(SRCS)
$(CC) -fsanitize=address,undefined -g $(CFLAGS) $(SRCS) -o $(TARGET)
release: $(SRCS)
$(CC) $(CFLAGS) -O3 $(SRCS) -o $(TARGET)
clean:
rm -f $(TARGET)
補足
makefileでcppcheckの`-j`オプション(CPUコア数)を付ける場合は`$$(nproc)`と`$`2個書きます。このmakefileと先程のmain.cを同じディレクトリに配置し、そのディレクトリで
make
make all
make main
のいずれか一つを実行すれば普通に実行ファイルmainができます。更に
make check
を実行すればCppCheckが動き、
make debug
./main
を実行すればASan, UBSanが検知し、
make release
を実行すれば最適化(-O3)された実行ファイルが作成されます。