SRS本を読んで静的解析ツールclang-tidyを試した

 

 

[Clang-Tidyとは]

Clang-Tidyは、C/C++向けのリンターである。JavaでいうError Proneのようなものらしい。

clang.llvm.org

 

最近、毎日少しずつGoogleの『Building Secure and Reliable Systems』(通称SRS本)を読み進めている。セキュアなシステムを作るためのテストという文脈で、Chapter 13: Testing Codeにおいて、Unit Testing、Integration Testing、Fuzz Testingに続いて、Static Program Analysisつまり静的解析が取り上げられていた。

landing.google.com

 

その静的解析の例として、C言語のsizeof演算子を利用する際のコード例とともに、clang-tidyによる静的解析結果が掲載されていたので、これを写経してclang-tidyで実際にリントを実行してみることにした。

 

なお、手元の環境は以下の通り。

$ sw_vers 
ProductName:	Mac OS X
ProductVersion:	10.15.6
BuildVersion:	19G73

 

[Clang-TidyをmacOSにインストールする]

サクッとインストールしてみるか、とbrewコマンドを実行したところ、エラー終了してしまった。brewではインストールできないらしい。

$ brew install clang-tidy

(中略)

Error: No available formula with the name "clang-tidy" 
==> Searching for a previously deleted formula (in the last month)...
Warning: homebrew/core is shallow clone. To get complete history run:
  git -C "$(brew --repo homebrew/core)" fetch --unshallow

Error: No previously deleted formula found.
==> Searching for similarly named formulae...
Error: No similarly named formulae found.
==> Searching taps...
==> Searching taps on GitHub...
Error: No formulae found in taps.

 

仕方がないのでGoogle先生に尋ねると、例の如くStackoverflowにたどり着いた。 

stackoverflow.comclang-tidy自体はbrewには用意されていないので、clang-tidyが付属しているllvmをインストールしてパスを通せ、ということらしい。

 

brew install llvmのコマンドを気楽に実行したら、ずいぶん時間がかかった。Pythonだのopensslだの、依存するインストール済みソフトウェアのバージョンが古かったらしく、ことごとくバージョンアップが必要だったのが原因のようだ。もっとも、llvm自体のインストールにもかなり時間がかかったが……。

 

$ brew install llvm
(略)
==> Installing llvm
==> cmake -G Unix Makefiles .. -DLLVM_ENABLE_PROJECTS=clang;clang-tools-extra;ll
==> make
==> make install
==> make install-xcode-toolchain
==> Caveats
To use the bundled libc++ please add the following LDFLAGS:
  LDFLAGS="-L/usr/local/opt/llvm/lib -Wl,-rpath,/usr/local/opt/llvm/lib"

llvm is keg-only, which means it was not symlinked into /usr/local,
because macOS already provides this software and installing another version in
parallel can cause all kinds of trouble.

If you need to have llvm first in your PATH run:
  echo 'export PATH="/usr/local/opt/llvm/bin:$PATH"' >> /Users/akito/.bash_profile

For compilers to find llvm you may need to set:
  export LDFLAGS="-L/usr/local/opt/llvm/lib"
  export CPPFLAGS="-I/usr/local/opt/llvm/include"

==> Summary
🍺  /usr/local/Cellar/llvm/10.0.0_3: 7,055 files, 1GB, built in 198 minutes 24 seconds
(略)

 

ログ上はllvmのインストールだけで198分かかったらしい。他にも色々更新が入ったりして、実際にはもっと時間がかかっていたわけだが……。

 

f:id:kidani_a:20200814112342p:plain

目指すclang-tidyについては、このあたりでclang-tools-extraとして各種clang-xxx系のツールがインストールされ、その一つにclang-tidyが含まれているらしい。

 

Stackoverflowの指示にしたがって、以下のコマンドでシンボリックリンクを作成して、clang-tidyのインストールは完了である。

$ ln -s "$(brew --prefix llvm)/bin/clang-tidy" "/usr/local/bin/clang-tidy"
$ ls /usr/local/bin | grep clang
clang-format
clang-tidy
git-clang-format

 

[Clang-Tidyを使ってみる]

インストールできたclang-tidyのバージョンは次の通り。

$ clang-tidy --version
LLVM (http://llvm.org/):
  LLVM version 10.0.0
  Optimized build.
  Default target: x86_64-apple-darwin19.6.0
  Host CPU: skylake

 

早速使ってみる。テスト対象となる問題のあるコードは以下の通り(出典:Building Secure and Reliable Systems, p.293)。

sizeof.c
-------------
#include <string.h>
const char* kMessage = "Hello World!";
int main() {
  char buf[128];
  memcpy(buf, kMessage, sizeof(kMessage));
  return 0;
}

 

本来はsizeofで文字数をカウントするつもりが、ポインタのサイズをカウントしてしまう、というC/C++ではありがちなミスらしい。

clang.llvm.org

 

このファイルに対してclang-tidyを実行すると、たしかにSRS本に掲載されているとおり警告が表示された。よし。

$ clang-tidy sizeof.c 
(略)
Running without flags.
1 warning generated.
sizeof.c:5:31: warning: 'memcpy' call operates on objects of type 'const char' while the size is based on a different type 'const char *' [clang-diagnostic-sizeof-pointer-memaccess]
        memcpy(buf, kMessage, sizeof(kMessage));
                                     ^
sizeof.c:5:31: note: did you mean to provide an explicit length?

 

[Clang単体でも同じことができる]

メモリ確保に失敗した状態でbufを出力した結果が気になったので、以下のようにコードを書き換えた。

#include <stdio.h>
#include <string.h>

const char* kMessage = "Hello World!";

int main() {
  char buf[128];
  memcpy(buf, kMessage, sizeof(kMessage));
  puts(buf);
  return 0;
}

 

上記のソースコードコンパイルしたバイナリを実行しようとしたところ、いくつかのことに気がついた。

macOSの場合、Xcodeに付属するclangを利用してC/C++ソースコードコンパイルすることができる。

$ clang -v
Apple clang version 11.0.3 (clang-1103.0.32.62)
Target: x86_64-apple-darwin19.6.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin

 

このclangコンパイルしたときにも、似たような警告が表示された*1

$ clang sizeof.c 
sizeof.c:7:31: warning: 'memcpy' call operates on objects of type 'const char'
      while the size is based on a different type 'const char *'
      [-Wsizeof-pointer-memaccess]
        memcpy(buf, kMessage, sizeof(kMessage));
                    ~~~~~~~~         ^~~~~~~~
sizeof.c:7:31: note: did you mean to provide an explicit length?
        memcpy(buf, kMessage, sizeof(kMessage));
                                     ^~~~~~~~
1 warning generated.

f:id:kidani_a:20200814115606p:plain

clangでも目的としていた警告が表示できるということは、数時間かけてLLVMをインストールした時間は一体なんだったのか......。

 

もっとも、clangとclang-tidyの大きな違いとして、バイナリを生成するかどうか、という違いがある。バイナリは不要で、静的解析の結果だけが必要な場合にはclang-tidyを利用すると良いのかもしれない。

 

ちなみに、生成されたバイナリを実行してみると、以下のようになった。期待した文字列が出力されていないし、出力結果が実行する度に変わるので、バグっているな、と分かる。

$ ./a.out 
Hello Wod??
$ ./a.out 
Hello Wod"?

 

[gccで警告を表示する]

ちなみに、C/C++コンパイラといえばgccが有名だ。VS Code拡張機能Remote - Containersを利用して、gccをインストールしてみた。

Remote - Containersで利用したDockerfileは次のとおり。

FROM ubuntu:latest

RUN apt update
RUN apt install -y gcc

 

gccのバージョン情報は次の通り。

$ gcc -v      
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/7/lto-wrapper
OFFLOAD_TARGET_NAMES=nvptx-none
OFFLOAD_TARGET_DEFAULT=1
Target: x86_64-linux-gnu
(略)
Thread model: posix
gcc version 7.5.0 (Ubuntu 7.5.0-3ubuntu1~18.04) 

 

普通にコンパイルしようとすると、何も警告が表示されずに終わってしまう。

$ gcc sizeof.c
$ ls | grep out
a.out

 

gccのヘルプをみると、warningsというコマンドがあることがわかった。

$ gcc --help
Usage: gcc [options] file...
Options:
  -pass-exit-codes         Exit with highest error code from a phase.
  --help                   Display this information.
  --target-help            Display target specific command line options.
  --help={common|optimizers|params|target|warnings|[^]{joined|separate|undocumented}}[,...].
                           Display specific types of command line options.
(略)

 

ということでwarningsコマンドのヘルプを参照する。

$ gcc --help=warnings
The following options control compiler warning messages:
  --all-warnings              Same as -Wall.  Use the latter option instead.
(略)
  -Wall                       Enable most warning messages.
(略)

細かく警告すべき内容を指定できるオプションが大量に表示されたが、とりあえず全部の警告を利用すれば良さそうだったので、-Wallを追加してgccコンパイルしてみた。

$ gcc -Wall sizeof.c
sizeof.c: In function 'main':
sizeof.c:8:30: warning: argument to 'sizeof' in 'memcpy' call is the same expression as the source; did you mean to provide an explicit length? [-Wsizeof-pointer-memaccess]
  memcpy(buf, kMessage, sizeof(kMessage));
                              ^

こちらの場合も、clangでコンパイルした場合と同様、警告を表示することができた。コンパイル時のオプションを追加する必要があるので、やや面倒か。

個別に警告を出す対象の問題を指定したい場合は、今回であればgcc -Wsizeof-pointer-memaccess sizeof.cのようにコマンドを実行すればよい。ただし、他に問題がある場合に見逃してしまうので、やはり-Wallを利用するのが無難だろうか。

 

[まとめ]

  • C++用のClang-Tidyというリンターを利用してみた
  • macOSLLVMbrewでインストールするとものすごく時間がかかった
  • ClangであればLLVMに付属するClang-Tidyを使わなくても、clang-tidyと同様の結果を得られた(ただしバイナリが生成される)
  • gccでも-Wallオプションを追加するとclang-tidyと同様の結果を得られた(こちらも当然バイナリが生成される)
  • 今回利用したソースコードは以下のリポジトリにまとめてある

github.com

 

C/C++は普段使わないので、コンパイル周りとなると本当に全然分からない……疲労コンパイル

*1:ここではmacOS上で実行したが、後述のRemote - Containersを利用してUbuntuにclangをインストールして試したところ、Ubuntu上でも同様の結果が得られた。