Неожиданное поведение snprintf

Я заметил (на мой взгляд) очень странное поведение snprintf с С++ на нескольких платформах. Рассмотрим следующий код (минимальный рабочий пример, который вызывает наблюдаемое поведение):

#include <stdio.h>

char test1[512];
char test2[512];
char test3[1024];
char test4[1024];

int main()
{
    snprintf(test1, sizeof(test1), "test1");
    snprintf(test2, sizeof(test2), "test2");
    snprintf(test3, sizeof(test3), "%s %s", test1, test2);
    return 0;
}

При запуске, хотя valgrind с --tool = exp-sgcheck, сообщается о следующей ошибке (для третьего оператора snprintf):

==30302== Invalid read of size 1
==30302==    at 0x568E4EB: vfprintf (in /lib64/libc-2.19.so)
==30302==    by 0x56B7608: vsnprintf (in /lib64/libc-2.19.so)
==30302==    by 0x5695209: snprintf (in /lib64/libc-2.19.so)
==30302==    by 0x4006AD: main (1.cc:12)
==30302==  Address 0x601460 expected vs actual:
==30302==  Expected: global array "test1" of size 1,024 in object with soname "NONE"
==30302==  Actual:   global array "test2" of size 512 in object with soname "NONE"
==30302==  Actual:   is 0 after Expected

Таким образом, передача test1 в качестве аргумента для первого% s приводит к чтению после конца массива test1.

Такое поведение вызвало несколько ошибок страниц в драйвере Windows (да, я знаю, это статические данные...). К счастью, код переносимый, и при портировании в linux valgrind сообщалось об этой ошибке.

Но, насколько мне известно, snprintf должен заканчивать test1 с \0 на 6-м байте (что он делает, проверял это). Так почему же 3-й оператор snprintf читает после конца массива test1? Изменение 3-й инструкции snprintf на

snprintf(test3, sizeof(test3), "%.512s %s", test1, test2);

решает проблему на обеих платформах. Компиляция кода как кода C (не С++) не приводит к ошибке.

UPDATE: в Linux (и, возможно, в окнах) ошибка возникает только в том случае, если код скомпилирован с включенной отладочной информацией и отключен оптимизация (-g -O0 для gcc).

Ответ 1

Так как глобальные объекты (например, массивы в вашем примере) являются 0-инициализированными, последний snprintf никогда не должен читаться за пределами конца строки, независимо от того, скопировали ли предыдущие sprintfs завершающий 0 char или нет. Единственное объяснение заключается в том, что предыдущие snprintfs копировали гораздо больше, чем представленный "test1" в целевой test1, переписывая все 0 с не-0 (без 0-бит было бы маловероятно со случайной памятью).

Это очень маловероятно - такая очевидная ошибка была бы найдена ранее. Что касается ошибки в драйвере, я бы заподозрил, что память перезаписывается полностью несвязанным "процессом" (в общем смысле, может быть, другим драйвером). Для настольного приложения у меня нет объяснений, почему он потерпит неудачу. Попробовав свой пример на Codingground с gcc 4.8.3, он работал отлично и печатал ожидаемые строки, когда я добавлял printf() в конце.

Btw, неудивительно, что исходный код работает нормально с включенными оптимизациями: поскольку нет наблюдаемого эффекта, компилятор может испускать только NOP.