【C言語】
メモリ破壊の原因と対策
-発生パターンと解決方法-

warning: iteration 1024 invokes undefined behavior

警告:ループカウンタ 1024 で未定義動作
[-Waggressive-loop-optimizations]

[gcc -fsanitize=addres]
[gcc -Wall -Wextra -O3]


■この記事の概要

この記事では、C言語におけるメモリ破壊の原因と影響、strcpystrncpyなどの関数を使用する際の注意点とサンプルコードを用いて詳しく解説しています。

GCCの警告とエラーを踏まえたプログラムの安全性向上についても紹介します。


■メモリ破壊とは

メモリ破壊とは、簡単に言うと
主に配列からはみ出た領域に
書き込みする事です。

■strcpyで発生するメモリ破壊

#include <stdio.h>
#include <string.h>
//gcc -Wall -O
//warning: ‘__builtin___strcpy_chk’ writing 94 bytes 
//into a region of size 16 overflows the destination 
//[-Wstringop-overflow=]
int main(void) {
    char    buf[16];
    char    *jugemu = "寿限無寿限無五劫のすり切れ海砂利水魚の水行末雲来末風来末~~~";
    strcpy(buf, jugemu);
    puts(buf);
}

この問題は
gcc -Wall -Wextra -Oで検出できます。  
※ーOで最適化しないと検出できません。

参考:

汝が
“foo”とタイプするところで
“supercalifragilisticexpialidocious”とタイプする者が、
いつの日か必ずいるからである


■strncpyで発生するメモリ破壊

#include <stdio.h>
#include <string.h>
//clang -Wall 
//warning: 'strncpy' size argument is too large; 
//destination buffer has size 4, 
//but size argument is 1024 
//[-Wfortify-source]
int main(void) {
    char    buf[4];
    strncpy(buf,"abc",1024);
    puts(buf);
}

4byte領域に”abc”3byteと\0をコピーするので
問題なさそうですが、
第三引数の1024がくせ者です。

■strcatで発生するメモリ破壊

#include <stdio.h>
#include <string.h>
//gcc strcat.c -Wall  -O2
// warning: ‘__builtin___strcat_chk’ forming offset [4, 6] is 
//out of the bounds
//[0, 4] of object ‘buf1’ with type ‘char[4]’ 
//[-Warray-bounds]
int main(void){
    char    buf1[] = "abc";
    char    buf2[] = "def";
    strcat(buf1,buf2);
    puts(buf1);
}

第一引数に第二引数を加える領域がありません。

■memcpyで発生するメモリ破壊

#include <stdio.h>
#include <string.h>
//gcc -Wall -Wextra  -O -fsanitize=address
//warning: ‘__builtin_memcpy’ forming offset [4, 1023] is 
//out of the bounds [0, 4] 
//[-Warray-bounds]
int main(void){
    char buf[1024];
    //OK:"abc"の後ろは0で埋まる
    strncpy(buf,"abc",sizeof(buf));
    puts(buf);

    //NG:"def"の後ろを領域外参照する
    memcpy(buf,"def",sizeof(buf));
    puts(buf);
}

文字列定数”def”の後ろを参照してしまいます。

■sprintfで発生するメモリ破壊

#include <stdio.h>
//gcc -fsanitize=address
int main(void){
    
    char     input[4] = "abcd";//終端文字が無い
    char    output[5];
    
    sprintf(output,"%s",input);
    puts(output);
}

inputに終端文字\0が無いので
暴走して
\0を見つけるまでoutputに書き込みします。

■scanfで発生するメモリ破壊

#include <stdio.h>
//gcc -fsanitize=address
int main(void){  
    printf("4桁の数字を入れてね");

    char    buf[4];//終端文字を考慮していない
    scanf("%s",buf);
    printf("%sが入力されました\n",buf);
}

4桁の数字文字列を収容するには終端文字\0を入れて5byte必要です。

■fgetsで発生するメモリ破壊

#include <stdio.h>
//warning: 
//‘fgets’ writing 8192 bytes into a region of size 4 
//overflows the destination
//[-Wstringop-overflow=]
int main(void){  
    int     num;
    char    buf[4];

    printf("4桁の数字を入れてね");
    fgets(buf,BUFSIZ,stdin);
    sscanf(buf,"%d",&num);
    printf("%dが入力されました\n",num);
}

4桁の数字文字列を収容するには終端文字\0を入れて5byte必要です。

■for文で<と<=を間違えて発生するメモリ破壊

static int  ary[1024];
int main(void){
    for(int i = 0 ; i <= 1024 ;i++){//★NG
        ary[i] = 0 ;
    }
}

➡修正例1.<に変更する

static int  ary[1024];
int main(void){
    for(int i = 0 ; i < 1024 ;i++){
        ary[i] = 0 ;
    }
}

■要素数とサイズを間違えてメモリ破壊

#include <stdint.h>
static char aryC[1024];
static int  aryI[1024];
int main(void){
    //たまたま上手くいく例
    for(uint32_t i = 0 ; i < sizeof(aryC) ;i++){
        aryC[i] = 0 ;
    }
    //配列の要素数とサイズは違う
    for(uint32_t i = 0 ; i < sizeof(aryI) ;i++){//★NG
        aryI[i] = 0 ;
    }
}

要素数はサイズではありません。
sizeof(aryI) != 1024
sizeof(aryI) == sizeof(int)*1024 
です。

このコードではLP64モードの場合、4096回転するので
配列のメモリ破壊が発生します。

➡修正例

#include <stdint.h>
static char aryC[1024];
static int  aryI[1024];
int main(void){
    const uint32_t maxC = sizeof(aryC)/sizeof(aryC[0]);
    for(uint32_t i = 0 ; i < maxC ;i++){
        aryC[i] = 0 ;
    }
    
    const uint32_t maxI = sizeof(aryI)/sizeof(aryI[0]);
    for(uint32_t i = 0 ; i < maxI ;i++){
        aryI[i] = 0 ;
    }
}

配列の要素数を求める定石 sizeof(配列)/sizeof(配列[0])を使います。


warning: array subscript -1 is below array bounds of ‘char[10]’

警告:配列[-1]を参照した

[-fsanitize=address -O]

[-Warray-bounds]

バッファアンダーフローとは

//clang  -fsanitize=address -O
#include <stdio.h>
int main(void) {
    int     idx  = -1 ;
    char buf[10]="0123456789";
    printf("%c\n",buf[idx]);
}

buf[-1]を参照してしまいます。
プログラムが異常終了するかどうかは
運しだいです。

想定外の入力で配列[-1]を参照

//gcc   -fsanitize=address
//数字を入れないで CTRL-dを入力すると
//異常終了する(時もある)
#include <stdio.h>
int main() {
    puts("一桁の数字を入れてね");
    int     idx  = getchar() ;
    printf("idx=%d\n",idx);

    char ascii[127];
    ascii[idx] = idx ;
    printf("%c\n",ascii[idx]);
}

数字を入れないで CTRL-dを入力すると
getchar()はEOF(-1)を返すので
ascii[-1]を参照してしまします。

降順ループで配列[-1]を参照

//gcc -fsanitize=address
#include <stdio.h>
#include <string.h>
int main(void){
    char    str[] = "ABCDEFG";
    for(int i = strlen(str);i>=0;i--){
        printf("src[%d]=:%x:=%c:\n",
            i-1,
            str[i-1],
            str[i-1]
        );
    }
    printf("\n");
}

降順ループのこの例ではiが0の時
src[i-1]で
src[-1]を参照してしまいます。