ホーム > 読んだ > 国内

セーフティプログラミング

書誌

text唯野
author森誠
publisherソフトバンク
year『C MAGAZINE 2000.6』p.6-36
price?

履歴

2000.6.27読了
2000.7.10公開
2002.1.6修正
file error - parse error - input text line 223-1061: unexpected token (]) [% d] = %d\n", i, ary1[i]); } for(i = 0; i < 20; i++) { printf("ary2[%d] = %d\n", i, ary2[i]); } DeleteCriticalSection(&cs1); DeleteCriticalSection(&cs2); return(0); } void foo1(void* p) { int i; time_t t; int tmpary[20]; for(i = 0; i < 10; i++) { tmpary[i] = 19 - i; } EnterCriticalSection(&cs2); for(; i < 20; i++) { tmpary[i] = ary2[i]; } t = time(NULL); // 他のスレッドを割り込ませやすくするためのダミー処理 while(t + 5 > time(NULL)); LeaveCriticalSection(&cs2); EnterCriticalSection(&cs1); for(i = 0; i < 20; i++) { ary1[i] = tmpary[i]; } LeaveCriticalSection(&cs1); cnt--; return; } void foo2(void* p) { int i; time_t t; int tmpary[20]; for(i = 0; i < 10; i++) { tmpary[i] = i; } EnterCriticalSection(&cs1); for(; i < 20; i++) { tmpary[i] = ary1[i]; } t = time(NULL); // 他のスレッドを割り込ませやすくするためのダミー処理 while(t + 5 > time(NULL)); LeaveCriticalSection(&cs1); EnterCriticalSection(&cs2); for(i = 0; i < 20; i++) { ary1[i] = tmpary[i]; } LeaveCriticalSection(&cs2); cnt--; return; }

C での例外

C は標準で例外機能を持っていないが、Windows 側が持つ例外機能を用いることで例外処理を実装することはできる。例えば VC++ では以下のような文法を使う。このとき OS は例外ハンドラに制御が渡ると、まずフィルタを評価する。フィルタはスコープ内の変数も参照でき、その戻り値で後の動作が決定される。

__try
{
    // プログラム
}
__except(フィルタ)
{
    // プログラム (例外ハンドラ)
}

また、例外ハンドラとは別に終了ハンドラを利用することもできる。終了ハンドラはプログラムの終了方法に関係なくリソースの閉じることを実現する。終了ハンドラはスタックを解放する中で実行されるので、スタックフレームがネストしている場合にはスタックフレームごとに解放されていく。なお、終了ハンドラはリソースをクローズするために、まず実際にオープンされているリソースの調査を行っている。

__try
{
    // プログラム
}
__finally
{
    // プログラム (正常終了時、及び例外発生時)
}

リソースの代入ハンドラを NULL で初期化
↓<
__try ブロックでリソースを割り当て (ハンドラに正の値をセット)

__finally ブロックでハンドラが NULL 以外ならばリソースを解放

ちなみに、__finally ブロックから例外ハンドラ/終了ハンドラを更にネストさせることはできない。但し __try から外部へのジャンプは可能である。(__finally からジャンプしたり return すると制御が通常コードに戻るため正常終了してしまう。)

ユーザ関数でのエラー対処

ユーザ定義関数でのエラー処理のポイント。

戻り値の一貫性

  • 成功したら 0 を返す(関数戻り値をそのままエラーコードとして返すとき)
  • エラーのときに 0 を返す(成功してハンドラのコードなどを返すとき 失敗時には負(-1)の値を使う)

引数の範囲を内部でチェック

  • 引数の範囲チェックを関数内部に組み込んでおく
  • switch 文には default をつける
  • 配列は添字の範囲をチェックする
  • ポインタが NULL でないかチェックする

NDEBUG の利用

エラー処理による実行速度が気になる場合には、NDEBUG マクロを用いてデバッグ時とリリース時を切り分けてもよい。NDEBUG を使うことにより、デバッグオプションの有無に応じたコード生成を切り分けできる。NDEBUG は assert マクロでも使われており、NDEBUG が定義されていると空文扱いにできる。

入力データ判定とメモリ管理

致命的でないまでも処理が無効となる場合への対処方針。

日付の範囲チェック

配列データを用いてコーディング量を減らす
あらかじめ計算されたデータを持つことで複雑さを軽減する

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

int datelimit[2][12] =
    {{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
     {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}};  // 閏年

// 整数化された日付のチェック
int checkDate(int year, int month, int day)
{
    int leap_year;

    if(year < 1)
    {
        return 1;
    }

    // 4 で割り切れて 100 で割り切れない年か 400 で割り切れる年ならば閏年
    if((year % 100 != 0 && year % 4 == 0) || year % 400 == 0)
    {
        leap_year = 1;  // 配列インデクスを指す
    }
    else
    {
        leap_year = 0;
    }

    if((month < 1 || month > 12) ||                           // 月の範囲チェック
       (day < 1 || day > datelimit[leap_year][(month - 1)]))  // 日の範囲チェック
    {
        return 1;
    }

    return 0;
}

// 日付構造体
typedef struct DATEINFO
{
    int year;
    int month;
    int day;
} DateInfo;

// yyyy年mm月dd日のチェック
int checkDateString(char* date, DateInfo *di)
{
    int i, y, m, d;

    for(i = 0; date[i] && isspace(date[i]); i++);  // 処理位置を進める

    if(date[i] && isdigit(date[i]))
    {
        y = atoi(&date[i]);  // 西暦

        for(; isdigit(date[i]); i++);

        if(strncmp("年", &date[i], 2))
        {
            return 1;
        }

        i += 2;

        if(! date[i] || ! isdigit(date[i]))
        {
            return 1;
        }

        m = atoi(&date[i]);

        for(; isdigit(date[i]); i++);  // i は初期化せず流用

        if(strncmp("月", &date[i], 2))
        {
            return 1;
        }

        i += 2;


        if(! date[i] || ! isdigit(date[i]))
        {
            return 1;
        }

        d = atoi(&date[i]);

        for(; isdigit(date[i]); i++);

        if(strncmp("日", &date[i], 2))
        {
            return 1;
        }
    }
    else
    {
        return 1;
    }

    if(di)
    {
        di->year  = y;
        di->month = m;
        di->day   = d;
    }

    return checkDate(y, m, d);
}

char* testDate[] =
    {"2000年5月18日", "100年12月31日", "2010年01月1日", "1999年2月28日",
     "2000年0月18日", "2000年5月32日", "2000年2月29日", "2100年2月29日"};

int main()
{
    DateInfo date_inf;
    int i;

    for(i = 0; i < sizeof(testDate) / sizeof(testDate[0]); i++)
    {
        if(checkDateString(testDate[i], &date_inf))
        {
            printf("%s->Error!\n", testDate[i]);
        }
        else
        {
            printf("%s->%d年%d月%d日\n", testDate[i], date_inf.year, date_inf.month, date_inf.day);
        }
    }

    return 0;
}

関数の引数範囲チェック

関数ごとの引数の範囲チェックはマクロや関数で定型化する
(個別の if 文では境界条件の混在時に混乱をきたすため)
そして定義したマクロ類は一箇所に集めておく
 関数 : 実行時速度でのデメリット
 マクロ : コードサイズでのデメリット、型にとらわれないメリット

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

// 引数はチェック値、最小値、最大値 (最小/最大値が無効な値の場合のマクロ定義)
#define  checkValidValue(v,mn,mx)  ((v)>(mn)&&(v)<(mx)?0:1)

// マクロバージョン
#define  checkHour(v)  checkValidValue(v, 0, 23)
#define  checkMinute(v)  checkValidValue(v, 0, 59)
#define  checkSecond(v)  checkValidValue(v, 0, 59)

#define USE_MACRO

#ifndef NDEBUG
    #ifndef USE_MACRO
    // 関数バージョン
    int checkValidValue(int val, int min, int max)
    {
        if(val < min || val > max)
        {
            return 1;
        }

        return 0;
    }
    #else
        #define  checkValidValue(v, min, max) \
            ((v) < (min) || (v) > (max)?1:0)
    #endif
#else
    #define checkValidValue(v, min, max)  0
#endif

typedef struct TESTDATE
{
    int hour, minute, second;
} TestDate;

TestDate tb[] =
{
    {0, 0, 0},{23, 59, 59},{-1, 12, 12},{13, 60, 59},{0, 59, 61}
};

int main()
{
    int i;

    for(i = 0; i < sizeof(tb)/sizeof(tb[0]); i++)
    {
        printf("\tTest:%d:%d:%d\n", tb[i].hour, tb[i].minute, tb[i].second);

        if(checkHour(tb[i].hour))
        {
            printf("Hour Error:%d\n", tb[i].hour);
        }

        if(checkMinute(tb[i].minute))
        {
            printf("Hour Error:%d\n", tb[i].minute);
        }

        if(checkSecond(tb[i].second))
        {
            printf("Hour Error:%d\n", tb[i].second);
        }
    }

    return 0;
}
#include <stdio.h>
#include <stdlib.h>

int limitValue[] =
{
    // Min   Max
        0,   13,
       27,   56,
      132,  256,
      512, 1111,
};

int checkMultiRange(int val, int* range)
{
    int i;

    for(i = 0; i < sizeof(limitValue)/sizeof(limitValue[0]); i += 2)
    {
        if(val >= range[i] && val <= range[i + 1])
        {
            return 0;
        }
    }

    return 1;
}

#define checkSample(v)  checkMultiRange(v, limitValue)

int testdata[] =
{
    -1, 1, 14, 27, 100, 200, 257, 500, 700, 1000, 1111, 1112
};

int main()
{
    int i;

    for(i = 0; i < sizeof(testdata)/sizeof(testdata[0]); i++)
    {
        if(checkSample(testdata[i]))
        {
            printf("Out of range : %d\n", testdata[i]);
        }
        else
        {
            printf("In the range : %d\n", testdata[i]);
        }
    }

    return 0;
}

計算結果の範囲チェック

マクロ呼び出しで常に必要なパラメータ自体をマクロ側で展開
チェックを作業用の値で行ってから実際の代入を行う

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

#define chkCredit(x)  checkValidValue(x,0,10000000)

#define USE_MACRO

#ifndef USE_MACRO
int checkValidValue(int val, int min, int max)
{
    if( val < min || val > max)
    {
        return 1;
    }

    return 0;
}
#else
    #define checkValidValue(val,min,max) \
        (((val)<(min)||(val)>(max))?1:0)
#endif

int subCredit(int* rest, int* draw)
{
    int work = *rest;

    if(chkCredit(work))
    {
        return 1;
    }

    for(work = *rest; *draw; draw++)
    {
        work -= *draw;
    }

    if(chkCredit(work))
    {
        return 1;
    }

    *rest = work;

    return 0;
}

int main()
{
    int sum = 5000000;
    int buyList[] = {500000, 4320, 32520, 2512, 0};
    int buyList2[] = {4500000, 4320, 32520, 2512, 0};

    printf("sum = %d\n", sum);
    subCredit(&sum, buyList);
    printf("sum = %d\n", sum);
    subCredit(&sum, buyList2);
    printf("sum = %d\n", sum);

    return 0;
}

文字コードの範囲チェック

配列に範囲内かどうかを示すフラグとなるチェック値を持つ。あらかじめ関数を呼び出す前に isupper で大文字/小文字を揃える。関数にはチェックする文字から 'A' の引いたものを渡す。

更にフラグの方をビットフィールドに押し込んで配列の容量を節約したり、呼び出すチェック処理を関数ポインタにしてしまうなどがある

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

// 有効な文字に対する処理を関数ポインタとして呼び出す
void fooB()  // B
{
	printf("fooB 'B'\n");
}

// 有効なコードの情報を格納した配列
void (*chklst[])() = 
{
	NULL,  // A
	fooB,  // B
	NULL   // C
		   // 以下、続くものとする
};

int chkCode(int c)
{
	if(isupper(c))  // チェックする値が大文字なので大文字にする
	{
		if(chklst[c - 'A'])  // アルファベットならば
		{
			// 戻り値を見ないので無効な値は無視される
			chklst[c - 'A']();  // 関数ポインタの呼び出し
			return 0;  // 正常で 0 を返す
		}
		else
		{
			return 1;
		}
	}
	else
	{
		return 1;
	}
}

int main()
{
	int i;

	for(i = 0; i < 3; i++)
	{
		chkCode(i + 'A');
	}

	return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <mbstring.h>
#include <mbctype.h>
#include <ctype.h>
#include <string.h>

enum CODETYPE
{
    NULL_PTR_ERROR = -1,
    ALPHABET_UPPER,
    ALPHABET_LOWER,
    DIGIT,  // 数字
    KANJI,
    GRAPH,  // 記号
    KATAKANA,
    OTHERS,  // 制御コードなど
    ILLEGAL_CODE,  // 不正なコード  1バイト目がSJISなのに2バイトが範囲外
    UNKWOWN_CODE   // 上記以外のコード  基本的にありえない戻り値
};

int checkCodeSet(char* ptr1, char** ptr2);  // プロトタイプ

int main()
{
    char data[] = "aB1漢@カ\t";
    char* ptr;
    char* _ptr;

    printf("「%s」を調査\n", data);

    for(ptr = data; *ptr; ptr = _ptr)
    {
        switch(checkCodeSet(ptr, &_ptr))
        {
        case ALPHABET_UPPER:
            printf("大文字のアルファベット\n");
            break;
        case ALPHABET_LOWER:
            printf("小文字のアルファベット\n");
            break;
        case DIGIT:
            printf("数字\n");
            break;
        case KANJI:
            printf("漢字\n");
            break;
        case GRAPH:
            printf("1バイト記号\n");
            break;
        case KATAKANA:
            printf("半角カタカナ\n");
            break;
        case OTHERS:
            printf("その他の文字\n");
            break;
        case ILLEGAL_CODE:
            printf("不正なコード\n");
            break;
        default:
            printf("未知のコード\n");
            break;
        }
    }

    return 0;
}


int checkCodeSet(char* ptr1, char** ptr2)  // 第2引数は第1引数の次の文字を指すポインタを返す
{                                          // 2 バイト文字なら +2
    char c;

    if(ptr1)
    {
        c = *ptr1;
    }
    else
    {
        return NULL_PTR_ERROR;
    }

    if(ptr2)
    {
        *ptr2 = ptr1 + 1;
    }

    switch(_mbbtype(c, 0))
    {
    case _MBC_LEAD:
        if(_mbbtype(ptr1[1],1) == _MBC_TRAIL)
        {
            if(ptr2)
            {
                *ptr2 = ptr1 + 1;
            }
            return KANJI;
        }
        else if(_mbbtype(ptr1[1],1) == _MBC_ILLEGAL)
        {
            return ILLEGAL_CODE;;
        }
        break;
    case _MBC_SINGLE:
        if(_ismbbkana(c))
        {
            return KATAKANA;
        }
        else if(isalpha(c))
        {
            if(islower(c))
            {
                return ALPHABET_LOWER;
            }
            else
            {
                return ALPHABET_UPPER;
            }
        }
        else if(isdigit(c))
        {
            return DIGIT;
        }
        else if(isgraph(c))
        {
            return GRAPH;
        }
        else
        {
            return OTHERS;
        }
        break;
    case _MBC_ILLEGAL:
        return OTHERS;
    default:
        return UNKWOWN_CODE;
    }

    return UNKWOWN_CODE;
}

メモリリーク

動的記憶を用いたメモリリークは free によって明示的に解放するかプログラムを終了しない限りメモリ上に残る。free によるメモリの解放し忘れがメモリリークで、メモリリークはシステム全体の他のプログラムをもメモリ不足に陥れるので重大といえる。メモリリークの原因としては主に以下のものが考えられる。

  • サブルーチン内で宣言した動的メモリの解放し忘れ(それを指すポインタ変数(自動変数)が関数を抜けた時点で解放されてしまっている。C++ ならば確保をコンストラクタ、解放をデストラクタで行うようにすることで、一応、回避できる)
  • ポインタ変数への上書き(上書きされたメモリブロックが解放できずに残るような場合。これは上書きするポインタが NULL でなければ解放してから割り当てるようにすることで回避できる)

後者の場合には、ポインタを常に NULL で初期化するようにして(ポインタ宣言用のマクロで NULL にするなど)解放後も NULL を入れるようにする方法などがある。

// ポインタ上書きチェックマクロ
#include <stdio.h>
#include <stdlib.h>

#define  MyMalloc(a,b)  ((a)?(free(a),(a)=malloc(b)):((a)=malloc(b)))
#define  MyFree(ptr)    (free(ptr),(ptr=NULL))
#define  DEC_PTR(t,a)   t* a=NULL

int main()
{
    DEC_PTR(char, ptr);  // NULL で初期化されるポインタ変数
    printf("ptr = %p\n", ptr);

    // C++ だと明示的なキャストが必要
    MyMalloc((void*)ptr, 1000);
    printf("ptr = %p\n", ptr);

    MyMalloc((void*)ptr, 2000);
    printf("ptr = %p\n", ptr);

    MyFree(ptr);
    return 0;
}

_heapwalk 関数

ヒープメモリのデバッグで役に立つ関数として _heapwalk がある。これは malloc したメモリブロックの情報(ヒープエントリ)を調査して次のヒープエントリ(_HEAPINFO 構造体)のポインタを返す。引数となる _HEAPINFO 構造体は malloc.h で定義されており以下のメンバがある。ヒープ内の最初のエントリを得るには _pentry に NULL をセットして呼び出せばよい。

int*    pentry  ヒープエントリポインタ
size_t  _size   ヒープエントリのサイズ
int _   useflag 使用中かどうかのフラグ

_heapwalk の戻り値となる定数も malloc.h で定義されており、以下のものがある。そして、_HEAPOK を返したとき _useflag に _FREEENTRY (割り付けられていないエントリ)か _USEDENTRY (使用中のエントリ) のいずれかが設定される。また、エラーが発生すると _heapwalk は errno に ENOSYS をセットする。

_HEAPBADBEGIN初期ヘッダ情報を検出できない/または無効
_HEAPBADNODE不正なノードの検出/またはヒープが壊れている
_HEAPBADPTR_pentry にヒープへの有効なポインタが格納されていない
_HEAPENDヒープの終わりに到達した
_HEAPEMPTYヒープが初期化されていない
_HEAPOKエラーがなく _HEAPINFO 構造体に次のエントリ情報がある

他に _heapchk 関数は標準で用意される HEAPINFO のチェックを行い、戻り値として上記のいずれかを返す。また、_heapset 関数は割り付けられていないエントリに指定した値をセットできる。(戻り値は _heapchk と同じ。)例えば、デバッグなどで指定した値が後でセットされていなければポインタがエラーだと調べることができる。

// _heapwalk によるチェック
#include <stdio.h>
#include <malloc.h>

// checkHeap がエラーを返したとき、直前のOKとの間に問題のあることが分かる
void checkHeap(void);

void main(void)
{
    char* buf;

    checkHeap();
    if((buf = (char*)malloc(59)) != NULL)  // 領域を確保できたならば
    {
        checkHeap();
        free(buf);
    }
    checkHeap();
}

void checkHeap(void)
{
    _HEAPINFO hi;
    int hs;

    hi._pentry = NULL;  // 最初のヒープエントリを指すために NULL をセット

    while((hs = _heapwalk(&hi)) == _HEAPOK)  // エントリが正常な場合
    {
        printf("%6s メモリブロック位置 %Fp 大きさ %4.4X\n",
            (hi._useflag == _USEDENTRY ? "使用中" : "未使用"), hi._pentry, hi._size);
    }

    switch(hs)
    {
    case _HEAPEMPTY:
        printf("ヒープが初期化されていません\n");
        break;
    case _HEAPEND:
        printf("ヒープの終端まで正常に達しました\n");
        break;
    case _HEAPBADPTR:
        printf("_HEAPINFO._pentry にヒープへの有効なポインタがありません\n");
        break;
    case _HEAPBADBEGIN:
        printf("初期ヘッダ情報を検出できないか無効です\n");
        break;
    case _HEAPBADNODE:
        printf("不正なノードが検出されたかヒープが壊れています\n");
        break;
    default:
        printf("未知のエラーです\n");
    }
}

オーバーラン/アンダーラン

オーバーラン : 割り付けられたメモリ範囲外の上位アドレスへのアクセス
アンダーラン : 割り付けられたメモリ範囲外の下位アドレスへのアクセス

これらのチェックには malloc するメモリの前後に余分なメモリを割り当て、そこに特定パターンのコードを書き込んでおき、それをチェックすることで調査する方法などがある。(これにより少なくとも 1 回は不正なメモリへのアクセスをチェックできる。)

その他

  • 配列の大きさチェックには sizeof を使う(配列要素数は 配列全体サイズ/一要素サイズだから sizeof(array_ptr)/sizeof(array_ptr[0]) で取得できる、char 以外の場合は要素の型の sizeof で結果を求める必要がある)
  • gets や scanf の代わりに fgets を使うのと同様、文字列コピーには strncpy を使う(いずれにしてもサイズを指定してコピーを行うようにするということ。そしてサイズは sizeof の結果を指定するようにする)
  • 構造体は要素間の空きがありえるので単純な memcmp の結果は信用できない(VC++ ではデバッグ時と実行時でデータの格納のされ方に違いのあることがある)
  • 単純な ctype.h とは別にマルチバイト用の mbctype.h というヘッダファイルがある。関連関数を使えば 2 バイト文字を考慮したチェックを行うことができる。例えば、mbstring.h をインクルードして _mbbyte を使うと、文字がシングルバイト文字かマルチバイト文字か調査できる。

参考情報

「Borland C++Builder4 Pro Help」Inprise

[% END %]

Up