main

snprintf и слишком умный компилятор

Наткнулся на забавное. При попытке скомпилировать достаточно тривиальный код:

gcc -Wall -Wextra -pedantic -D_FORTIFY_SOURCE=2 -O2 -o test test.c

...получаем две предупреждения от компилятора:

test.c: In function ‘main’:
test.c:22:74: warning: ‘%08X’ directive output may be truncated writing 8 bytes into a region of size between 7 and 85 [-Wformat-truncation=]
   snprintf(buf, sizeof(buf), "event=match name=%s addr=%s stag=%08X ftag=%08X score=%d",
                                                                          ^~~~
test.c:22:30: note: directive argument in the range [-32768, 32767]
   snprintf(buf, sizeof(buf), "event=match name=%s addr=%s stag=%08X ftag=%08X score=%d",
                              ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In file included from /usr/include/stdio.h:873,
                 from test.c:1:
/usr/include/x86_64-linux-gnu/bits/stdio2.h:67:10: note: ‘__builtin___snprintf_chk’ output between 60 and 143 bytes into a destination of size 128
   return __builtin___snprintf_chk (__s, __n, __USE_FORTIFY_LEVEL - 1,
          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        __bos (__s), __fmt, __va_arg_pack ());
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Конкретная строка, которая вызывает ошибку:

snprintf(buf, sizeof(buf), "event=match name=%s addr=%s stag=%08X ftag=%08X score=%d",
  name, addr, match.stag, match.ftag, match.score);

Ну казалось бы: обычное дело, не хватает размера буфера. Однако посмотрите внимательно на 3-5 строки. Дело в том, что ftag (четвёртый параметр) - это uint32_t, но компилятор его опознаёт как "directive argument in the range [-32768, 32767]", т.е. int16_t. Такой тип имеет среди параметров только пятый - score.

Получается, что в вызове snprintf() компилятор по какой-то причине выкидывает четвёртый параметр, и пытается применить к следующему по списку (score) непредназначенный для него спецификатор (%08X), указанный в шаблоне форматирования. Это довольно неожиданное поведение для snprintf(), поскольку она специально предназначена записи в буфер известного размера, и в спецификации указано именно что "урезание" (truncate) получившейся строки до размера буфера при недостатке места, но никак не изменение порядка параметров в самой функции.

Это очевидно вызвано "переоптимизацией" компилятора, поскольку проблема пропадает при любом из следующих действий:

  • удаления из флагов препроцессора -D_FORTIFY_SOURCE=2
  • уменьшения уровня оптимизации до -O0
  • ну и разумеется, увеличения размера буфера

...хотя у меня нет уверенности, что пропадает сама проблема, а не только предупреждение о ней.

В любом случае, включение дополнительных проверок компилятора, не полагаясь на дефолтный -Wall - это хорошая идея и значительная экономия времени на отладку уже в рантайме.