warning: iteration 1024 invokes undefined behavior
警告:ループカウンタ 1024 で未定義動作
[-Waggressive-loop-optimizations]
[gcc -fsanitize=addres]
[gcc -Wall -Wextra -O3]
■この記事の概要
この記事では、C言語におけるメモリ破壊の原因と影響、strcpy
やstrncpy
などの関数を使用する際の注意点とサンプルコードを用いて詳しく解説しています。
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");
}