Как реализовать assert(), чтобы использовать статический анализ потока данных оптимизатора gcc?
Рассмотрим следующий код....
#include <stdio.h>
void func( char * a, int i)
{
printf( "%c\n", a[i]);
}
int main( int argc __attribute__((unused)), char * argv[] __attribute__((unused)))
{
int i = 10;
char a[] = "abc";
func( a, i);
return 0;
}
Компилировать с..
gcc -W -Wall -Wextra -o b b.c
и никаких предупреждений.
Запустите его, он работает, но печатает мусор.
Компилировать с
gcc -O3 -W -Wall -Wextra -o b b.c
И gcc правильно указывает на ошибку....
b.c: In function ‘main’:
b.c:5:21: warning: ‘*((void *)&a+10)’ is used uninitialized in this function [-Wuninitialized]
printf( "%c\n", a[i]);
^
b.c:11:9: note: ‘a’ was declared here
char a[] = "abc";
Привет! Это довольно умно для gcc, он проанализировал границы функции! (Я наблюдал на больших проектах, gcc теперь поразительно умен в этом!)
Теперь интуитивно противостоять, добавив утверждения делают вещи хуже!
Рассматривать....
#include <stdlib.h>
#include <stdio.h>
#include <assert.h>
void func( char * a, int i)
{
assert( i < 4);
printf( "%c\n", a[i]);
}
int main( int argc __attribute__((unused)), char * argv[] __attribute__((unused)))
{
int i = 10;
char a[] = "abc";
func( a, i);
return 0;
}
Компиляция без оптимизации снова не выдает никаких предупреждений, но во время выполнения вы, правильно....
a: a.c:7: func: Assertion `i < 4' failed.
Compilation aborted (core dumped) at Fri May 4 10:52:26
Но скомпилируйте с...
gcc -O3 -W -Wall -Wextra -o a a.c
... теперь не выводит предупреждений!
то есть. Хотя gcc знает, что assert сработает во время выполнения... он не может сказать мне больше во время компиляции.
то есть. Интуитивно счетчик, добавление подтверждений и проверок на ошибки в мой код сделало меня менее безопасным.
Я не могу не чувствовать, что должен быть какой-то хитрый вдохновленный Ктулху способ использования того, что ясно знает gcc, чтобы провалить утверждение во время компиляции!
Какие-либо предложения?
Обновление: вот немного другой вариант....
#include <stdlib.h>
#include <stdio.h>
#include <assert.h>
static char _rangeCheckVariable;
void bunc( int i)
{
if( __builtin_constant_p( (4))) {
char __rangeCheck[(4)]="abc";
_rangeCheckVariable = __rangeCheck[(i)];
} else {
assert( (i) < (4));
}
printf( "%d\n",i);
}
void func( int i)
{
bunc( i);
}
int main( int argc __attribute__((unused)), char * argv[] __attribute__((unused)))
{
int i = 10;
func( i);
return 0;
}
Компилирование с
gcc -g3 -ggdb3 -O3 -W -Wall -Wextra -c d.c ;objdump -S d.o
Результаты в
d.c: In function ‘main’:
d.c:11:41: warning: array subscript is above array bounds [-Warray-bounds]
_rangeCheckVariable = __rangeCheck[(i)];
^
то есть. Вы можете почти полностью отсортировать макрос check_range( i, size), который проверял во время компиляции, что я меньше размера.
Обновление 2: еще страннее.... Следующие компиляции без предупреждений, но.....
#include <stdlib.h>
#include <stdio.h>
#include <assert.h>
static char __rangeCheckVariable;
#define asshurt(exp) \
do { \
char __rangeCheck[2]="a"; \
__rangeCheckVariable = \
__rangeCheck[(exp) ? 0 : 10]; \
} while(0)
void bunc( int i)
{
asshurt( i< 4);
assert( i<4);
printf( "%d\n",i);
}
void func( int i)
{
bunc( i);
}
int main( int argc __attribute__((unused)), char * argv[] __attribute__((unused)))
{
int i = 10;
func( i);
return 0;
}
Убрал строку assert( i< 4);
и вы получите
gcc -g3 -ggdb3 -O3 -W -Wall -Wextra -c d.c ;objdump -S d.o
d.c: In function ‘main’:
d.c:11:22: warning: array subscript is above array bounds [-Warray-bounds]
__rangeCheck[(exp) ? 0 : 10]; \
^
d.c:17:4: note: in expansion of macro ‘asshurt’
asshurt( i< 4);
^
Обновление 3: еще страннее.
Поиграйте с этим на этом замечательном сайте Godbolt.... Попробуйте переключиться между настройками оптимизации -Os и -O2.
#include <stdlib.h>
void assertFailure( void) __attribute__((warning("Compile time assertion failure")));
int z(void);
int main()
{
int j;
for( j=0;j<4;j++) {
if( z()) break;
}
if( __builtin_constant_p(!(j < 4)))
{
if(!(j < 4))
assertFailure();
}
else
if( !(j < 4))
abort();
return 0;
}
На -O2 нет предупреждений (правильно) на -O2 и выше... он неправильно собирается при вызове assertFailure.
Обновление 4: вариант, который работает для всех настроек оптимизации (но не для C++)
#include <stdlib.h>
#include <stdio.h>
void assertFailure( void) __attribute__((warning("Compile time assertion failure")));
static unsigned u;
int z(void) { return u++ % 2u;}
#define assert(exp) \
__builtin_choose_expr( __builtin_constant_p(!(exp)), \
((!(exp)) ? assertFailure() : (void)0), \
((__builtin_expect( !(exp), 0)) ? abort() : (void)0))
void bunc( char * a, unsigned j)
{
printf( "%c\n", a[j]);
// assert( j < 4);
}
void func( char * a)
{
bunc( a, 5);
}
int main()
{
int j;
char a[]="abc";
func( a);
for( j=0;j<4;j++) {
if( z()) break;
}
assert( j < 4);
assert( 4 < 5);
// assert( 5 < 4);
return 0;
}
Вы можете играть с ним на Godbolt. Интересная разница в том, что предупреждения gcc-5.1 кажутся лучше, чем 8.1!
1 ответ
Примерно так будет работать в вашем примере:
void assert_warn_abort (void)
__attribute__ ((warning ("assertion failure"), noreturn));
#define assert(expr) \
({ \
if (expr) \
(void)0; \
else \
{ \
if (__builtin_constant_p (expr)) \
assert_warn_abort (); \
else \
abort (); \
} \
})
Тем не менее, предупреждение о каждом статически известном сбое утверждения может привести к множеству ложных предупреждений с тяжелым встраиванием.