ソースコードの形式的チェックに関するヒント

scan-build とはなんですか?

scan-build とは,clang を活用するソフトウェアの1つです. scan-build を用いて,clang 等によるコンパイル結果からC言語等のソースコードに含まれる典型的エラーを発見することができます.ソー スコードとしては正しい (コンパイルは通る) が,実はバグを含んでいるようなソースコードを,適正なものへと修正する際に有用です.

例えば,「1回目の malloc」で確保したメモリ領域を free していないため,メモリリークが生じている以下のソースコードを考えます.

#include <stdlib.h>

int main(void)
{
  int *array;
  array = malloc(sizeof(int) * 3); /* 1回目の malloc */
  array = malloc(sizeof(int) * 3); /* 2回目の malloc */
  array[0] = 1;
  free(array);

  return 0;
}

このソースコードのファイル名を leak.c としたとき,gcc によるコンパイルは以下のように正常に終了し,警告なしで a.out ファイルが作られます.

$ gcc -W -Wall leak.c
$

scan-build を使用する際は,基本となるコンパイルコマンド (今回の例では「gcc -W -Wall leak.c」) を scan-build の引数として与えます.
今回は以下のようにして使用してみます.

$ scan-build gcc -W -Wall leak.c
scan-build: Using '/usr/lib/llvm-6.0/bin/clang' for static analysis
leak.c:6:3: warning: Value stored to 'array' is never read
  array = malloc(sizeof(int) * 3); /* 1回目の malloc */
  ^       ~~~~~~~~~~~~~~~~~~~~~~~
leak.c:8:12: warning: Potential leak of memory pointed to by 'array'
  array[0] = 1;
  ~~~~~~~~~^~~
2 warnings generated.
scan-build: 2 bugs found.
scan-build: Run 'scan-view /tmp/scan-build-2021-11-18-173320-2085-1' to examine bug reports.

この出力から,以下の2つの潜在的エラーを確認できます.

  1. leak.c の6行目で『array』に代入された値 (メモリ領域) を使用していない」
    「array[0] = 1」というコードで「array」の値を使っていそうですが,このコードで使っているのは「2回目の malloc」で確保したメモリ領域であり,「1回目の malloc」で確保したメモリ領域ではありません.
  2. 「『array』が参照しているメモリ領域がリークしているかもしれない」
    8行目でこの警告が表示されている理由は,「array」の値が上書きされるのが7行目であり,メモリリークの発生が確定するのが8行目だからだと考えます.

なお,今回の分析結果は /tmp/scan-build-2021-11-18-173320-2085-1 というディレクトリに保存されました.このディレクトリ名は scan-build の実行時 刻に基づいてランダムに決められます.

「Run ‘scan-view /tmp/scan-build-2021-11-18-173320-2085-1’ to examine bug reports.」という出力は,「scan-view というプログラムを使って分析結果 をチェックしよう」ということを言っています.scan-view により,scan-build で検出された警告をブラザを用いて見やすく確認できます. なお,端末から SSH接続して scan-build を使った場合,一般的なブラウザを使えないため,scan-view の利用は難しいです.

scan-build (正確には clang) は多くのチェッカを有しており,今回のメモリリークはデフォルトで有効化されている unix.Malloc というチェッカにより発見されました.
ほかにも多くのチェッカが存在します.詳細は scan-build のページ を参照しましょう.

scan-build で「… warning: Division by zero」と警告されるのですが

「/」演算子あるいは「%」演算子を用いており,かつそれらの演算子で分母として使われる値が 0 になりうる場合,この警告が出されます.
分母が定数の場合,0 ではないことを確認しましょう.
分母が,例えば var という名前の変数の場合,以下のようなコードを警告された行の直前に挿入し,分母が 0 であれば異常終了すると保証することで,当該 の警告は出なくなるはずです.ここで,assert 関数を使うためには #include <assert.h> というコードにより assert.h というヘッダファイルをインクー ルドしておく必要がある点に注意しましょう.

assert (var != 0);

scan-build で「… warning: Access to field ‘next’ results in a dereference of a null pointer (loaded from field ‘next’)」などと警告されるのですが

以下の条件が成立すると,そのような警告が出るようです.

  1. next というメンバをもつ構造体へのポインタを使っている.
  2. その next が構造体へのポインタである.
  3. その next の値が NULL の場合に成立しないコードがある.
    例えば,next というメンバをもつ構造体のポインタを p とし,かつ next が指す構造体が next2 というメンバをもつとき,p->next->next2 というメンバへアクセスしている.
  4. その成立しないコードが実行されうる.

解決策は,上の「p->next->next2 というメンバへアクセスしている」という例で説明しますと,そのアクセスの直前に「p->next は NULL じゃありません」と言明することです.

具体的には,p->next->next2 というメンバへアクセスしているコードの直前に以下のコードを記入しておきます.

assert (p->next != NULL);

assert 関数を用いるため,#include <assert.h> というコードも必要な点に注意しましょう.

valgrind とはなんですか?

valgrind とは,独自の仮想機械を内部で動かし,その上でプログラムを実行することにより,メモリリークや不正なメモリアクセス などの発生を検出できるコマンドです.

valgrind を実行する際は,メモリリークなどをチェックする対象であるプログラムおよびそれへのオプションを valgrind へ引数として与えます. 例えば,上述した leak.cをコンパイルして作成したプログラムのファイル名を leak-01 としたときの,valgrind の実行例は以下になります.

$ valgrind ./leak-01
==2912== Memcheck, a memory error detector
==2912== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==2912== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==2912== Command: ./leak-01
==2912==
==2912== error calling PR_SET_PTRACER, vgdb might block
==2912==
==2912== HEAP SUMMARY:
==2912==     in use at exit: 12 bytes in 1 blocks
==2912==   total heap usage: 1 allocs, 0 frees, 12 bytes allocated
==2912==
==2912== LEAK SUMMARY:
==2912==    definitely lost: 12 bytes in 1 blocks
==2912==    indirectly lost: 0 bytes in 0 blocks
==2912==      possibly lost: 0 bytes in 0 blocks
==2912==    still reachable: 0 bytes in 0 blocks
==2912==         suppressed: 0 bytes in 0 blocks
==2912== Rerun with --leak-check=full to see details of leaked memory
==2912==
==2912== For counts of detected and suppressed errors, rerun with: -v
==2912== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

「LEAK SUMMARY」という部分に「definitely lost: 12 bytes in 1 blocks」と出力されており,メモリリークがあると報告されています.

「Rerun with –leak-check=full to see details of leaked memory」とあるので,「–leak-check=full」というオプションを valgrind へ追加してみます. このオプションは,leak-01 へのオプションではなく valgrind へのオプションであり,「./leak-01」よりも前に指定している点に注意しましょう.

$ valgrind --leak-check=full ./leak-01
==2918== Memcheck, a memory error detector
==2918== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==2918== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==2918== Command: ./leak-01
==2918==
==2918== error calling PR_SET_PTRACER, vgdb might block
==2918==
==2918== HEAP SUMMARY:
==2918==     in use at exit: 12 bytes in 1 blocks
==2918==   total heap usage: 1 allocs, 0 frees, 12 bytes allocated
==2918==
==2918== 12 bytes in 1 blocks are definitely lost in loss record 1 of 1
==2918==    at 0x4C31B0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==2918==    by 0x10865B: main (in /home/inamoto/tmp/leak-01)
==2918==
==2918== LEAK SUMMARY:
==2918==    definitely lost: 12 bytes in 1 blocks
==2918==    indirectly lost: 0 bytes in 0 blocks
==2918==      possibly lost: 0 bytes in 0 blocks
==2918==    still reachable: 0 bytes in 0 blocks
==2918==         suppressed: 0 bytes in 0 blocks
==2918==
==2918== For counts of detected and suppressed errors, rerun with: -v
==2918== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

「12 bytes in 1 blocks are definitely lost in loss record 1 of 1」という箇所の下に「by 0x10865B: main」で始まる行があり,main関数内でメモリリー クが生じていることがわかります.

どこでメモリリークが生じているかわかりにくいため,leak-01 のコンパイル時に「-g3」というオプションを追加しデバッグ情報を leak-01 へ付与してみます.

$ gcc -g3 -W -Wall -o leak-01 leak.c

改めて valgrind を実行してみます.

valgrind --leak-check=full ./leak-01
==2939== Memcheck, a memory error detector
==2939== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==2939== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==2939== Command: ./leak-01
==2939==
==2939== error calling PR_SET_PTRACER, vgdb might block
==2939==
==2939== HEAP SUMMARY:
==2939==     in use at exit: 12 bytes in 1 blocks
==2939==   total heap usage: 1 allocs, 0 frees, 12 bytes allocated
==2939==
==2939== 12 bytes in 1 blocks are definitely lost in loss record 1 of 1
==2939==    at 0x4C31B0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==2939==    by 0x10865B: main (leak.c:6)
==2939==
==2939== LEAK SUMMARY:
==2939==    definitely lost: 12 bytes in 1 blocks
==2939==    indirectly lost: 0 bytes in 0 blocks
==2939==      possibly lost: 0 bytes in 0 blocks
==2939==    still reachable: 0 bytes in 0 blocks
==2939==         suppressed: 0 bytes in 0 blocks
==2939==
==2939== For counts of detected and suppressed errors, rerun with: -v
==2939== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

「by 0x10865B: main (leak.c:6)」という出力から,leak.c の6行目が怪しいとわかります.
実際,leak.c の6行目には「array = malloc(sizeof(int) * 3)」とあり,ここで確保したメモリ領域がリークしたのだと判断できます.

つづいて,不正なメモリアクセスを検出してみましょう.
以下のソースコードのファイル名を ob.c とします.

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
  int x;
  int *array;

  array = malloc(sizeof(int) * 3);
  array[-1] = 1;
  x = array[3];
  free(array);

  printf("x=%d\n", x);

  return 0;
}

「-g3」オプションを付けてob.c をコンパイルして作成したプログラムファイルを ob-01 とします.
ob-01 のチェック結果は以下になりました.

$ valgrind ./ob-01
==2989== Memcheck, a memory error detector
==2989== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==2989== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==2989== Command: ./ob-01
==2989==
==2989== error calling PR_SET_PTRACER, vgdb might block
==2989== Invalid write of size 4
==2989==    at 0x1086F8: main (ob.c:10)
==2989==  Address 0x522f03c is 4 bytes before a block of size 12 alloc'd
==2989==    at 0x4C31B0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==2989==    by 0x1086EB: main (ob.c:9)
==2989==
==2989== Invalid read of size 4
==2989==    at 0x108702: main (ob.c:11)
==2989==  Address 0x522f04c is 0 bytes after a block of size 12 alloc'd
==2989==    at 0x4C31B0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==2989==    by 0x1086EB: main (ob.c:9)
==2989==
x=0
==2989==
==2989== HEAP SUMMARY:
==2989==     in use at exit: 0 bytes in 0 blocks
==2989==   total heap usage: 2 allocs, 2 frees, 1,036 bytes allocated
==2989==
==2989== All heap blocks were freed -- no leaks are possible
==2989==
==2989== For counts of detected and suppressed errors, rerun with: -v
==2989== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)

「ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)」と出力されており,メモリアクセス上のエラーがあることがわかります.

==2989== Invalid write of size 4
==2989==    at 0x1086F8: main (ob.c:10)
==2989==  Address 0x522f03c is 4 bytes before a block of size 12 alloc'd
==2989==    at 0x4C31B0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==2989==    by 0x1086EB: main (ob.c:9)

という出力から,「ob.c の9行目で確保したメモリ領域に対して,ob.c の10行目で不正な書き込みを行っている」ことがわかります.
実際,10行目には「array[-1] = 1」とあり,確保された領域の先頭よりも手前に対して書き込んでいます.

==2989== Invalid read of size 4
==2989==    at 0x108702: main (ob.c:11)
==2989==  Address 0x522f04c is 0 bytes after a block of size 12 alloc'd
==2989==    at 0x4C31B0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==2989==    by 0x1086EB: main (ob.c:9)

という出力から,「ob.c の9行目で確保したメモリ領域に対して,ob.c の11行目で不正な読み出し行っている」ことがわかります.
実際,11行目には「x = array[3]」とあり,アクセスできる添字の最大値 2 を超えたメモリ領域を読み取っています.

scan-build を使ったときに「warning: … is a garbage value」という警告が出るのですが

不明な値 (garbage value) を保持している変数を使っていると表示されます.

例えば以下のソースコードを branch.c という名前で保存します.

#include <stdio.h>

int main(void)
{
  int x;

  if (x > 0) {
    printf("positive\n");
  } else {
    printf("not positive\n");
  }

  return 0;
}

このソースに対して scan-build を実行すると以下のように出力されました.

$ scan-build clang branch.c
scan-build: Using '/usr/lib/llvm-6.0/bin/clang' for static analysis
branch.c:7:9: warning: The left operand of '>' is a garbage value
  if (x > 0) {
      ~ ^
1 warning generated.
scan-build: 1 bug found.
scan-build: Run 'scan-view /tmp/scan-build-2021-11-17-174053-1038-1' to examine bug reports.

今回の branch.c は意味のあるプログラムではありませんが,とりあえず if文の前に (変数 x を使う前に),変数 x の値を初期化し ておけば,この種の警告は出なくなるはずです.

valgrind で実行時に「Conditional jump or move depends on uninitialised value(s)」と出るんですが

多くの場合,if文の条件判定箇所で初期化していない変数を用いているときに出力される警告です.

上述の branch.c というソースコードをコンパイルして作成したプログラムでも同じ警告が出力されます.

なお,つぎのソースコードのように,変数 x の初期化をべつの変数 y の値を用いて行っている場合は,変数 y を初期化していないと branch.c と同様 の警告が出力されます.

#include <stdio.h>

int main(void)
{
  int x, y;

  x = 0;
  x = y;

  if (x > 0) {
    printf("positive\n");
  } else {
    printf("not positive\n");
  }

  return 0;
}

要するにどんなスクリーンショットが撮れればいいんですか?

  • scan-build のスクリーンショットでは,エラーや警告が検出されない (「error」や「warning」が表示されない) 結果が得られたなら OK です.
  • valgrind のスクリーンショットでは,以下の2つの条件を満たせば OK です.
    • 「LEAK SUMMARY」においてメモリリークが生じていると出力されていない.
    • 「ERROR SUMMARY」において不正なメモリアクセスが生じていると出力されていない.

提出されたスクリーンショットが条件を満たしていない場合,ソースコードに明白なミスがあると考え,レポートは原則として返却となります.

参考:ソフトウェアのバージョン

本ページを作成する際に使用した OS,およびコマンドのバージョンを以下に示します.

  • OS: Ubuntu 18.04.5 LTS
  • gcc: gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0
  • clang: clang version 6.0.0-1ubuntu2 (tags/RELEASE_600/final)
  • scan-build: clang-tools 1:6.0-41~exp5~ubuntu1
  • valgrind: valgrind-3.13.0

scan-buildvalgrind 等のバージョンがこれらとは異なる場合,チェック結果等も上述のようにはならないかもしれません.