суббота, 22 ноября 2008 г.

Неизвестная известная snprintf

Довольно часто при работе с форматными строками на C используется функция snprintf. При этом, типичной является конструкция, например, следующего вида:

for (...) {
offset += snprintf(buf + offset, PAGE_SIZE - offset, " %02x", val);
...

Постепенно заполняем буфер форматированной строкой. При этом часто негласно принимается такое предположение, что функция вернет количество записанных в буфер байт (не считая последнего 0). Интересно, что это не всегда так.

Дело в том, что если почитать man по snprintf, то мы обнаружим, что в случае если размер буфера не достаточен для строки, то функция возвращает количество байт без последнего 0, которые БЫЛИ БЫ записаны, в случае, если БЫ размер буфера БЫЛ БЫ достаточен! То есть, если в приведенном выше примере произойдет отсечение результата, то на следующей итерации цикла мы полезем за границу буфера с отрицательным размером буфера (тип которого приведется к беззнаковому size_t)!

Интересно, что в реализации ядра Linux, функция vsnprintf содержит такую проверку:

if (unlikely((int) size < 0)) {
/* There can be only one.. */
static char warn = 1;
WARN_ON(warn);
warn = 0;
return 0;
}

То-есть, если в результате итераций в первом примере мы получаем отрицательный размер буфера, мы об этом узнаем... ;)

Кроме того, в ядре Linux есть реализация функции scnprintf, которая выглядит следующим образом:

int scnprintf(char * buf, size_t size, const char *fmt, ...)
{
va_list args;
int i;

va_start(args, fmt);
i = vsnprintf(buf, size, fmt, args);
va_end(args);
return (i >= size) ? (size - 1) : i;
}


Как видим, это та функция, которая всегда возвращает число записанных в буфер байт не считая 0 и -1 если размер буфера 0. Хотя в документации написано: "If size is <= 0 the function returns 0", и это мне не понятно, так как из кода следует, что все-таки это будет -1.

А вот другой пример контроля за размером буфера:

for (i = 0; i < npids; i++)
cnt += snprintf(buf + cnt, max(sz - cnt, 0), "%d\n", a[i]);

Правда типы sz и cnt в данном случае не size_t, а int, что не очень красиво.

Ну и, наконец, можно просто проверять накопленное смещение:

if ((offset += snprintf(...)) >= buffsize) {
/* ... error handling ... */
}


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


sz = snprintf(NULL, 0, " ..... ", .... ) + 1;
ptr = malloc(sz);
snprintf(ptr, sz, " .... ", ... );


Ссылки: snprintf() confusion' 2004 by corbet

Комментариев нет:

Архив блога