воскресенье, 9 ноября 2008 г.

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

Для начала небольшой эксперимент с подвохом. Исходный код test.c

#include <string.h>
char *mystrncpy(char *dest, const char *src, size_t n)
{
size_t i;
for (i = 0 ; i < n && src[i] != '\0' ; i++)
dest[i] = src[i];
if (i < n)
dest[i] = '\0';
return dest;
}
#define BUFF_SIZE 4096
int main(int argc, char **argv)
{
int i;
char buff[BUFF_SIZE];
for (i = 0; i < 0x100000; i++) {
#ifdef STRNCPY
strncpy(buff, "test", sizeof(buff));
#else
mystrncpy(buff, "test", sizeof(buff));
#endif
}
}

Тест выполняет 0x100000 копирований строки. Причем при сборке с ключом -DSTRNCPY будет использоваться функция glibc, в противном случае будет использоваться "самописная" mystrncpy. Проверяем...

peter@crashed:/tmp$ gcc test.c -O2 -DSTRNCPY
peter@crashed:/tmp$ time ./a.out
real 0m6.368s
peter@crashed:/tmp$ gcc test.c -O2
peter@crashed:/tmp$ time ./a.out
real 0m0.062s

Интересно, разница видна, как говорится, невооруженным глазом. Да, да... Мы не ошиблись -- примитивная функция mystrncpy работает быстрее на порядок!!! В чем подвох?

Если мы внимательно прочитаем man по strncpy, то ответ будет быстро найден: мы забыли, что функция strncpy всегда дополняет нулями целевой буфер! Довольно часто для C программиста эта особенность не является важной и функция strncpy используется для пересылки строки в другой буфер. Но чем длиннее принимающий буфер, тем дольше будет выполняться вызов strncpy! Большинство программистов избегает лишних вызовов strlen, но в данном случае даже strlen и memcpy сработают быстрее одного strncpy. Вот такая неизвестная известная функция.

Другой неприятной особенностью функции strncpy является некоторая запутанность логики в том смысле, что мы можем вообще не получить null terminated string если размер буфера оказался мал. Из-за этой особенности strncpy часто используется совместно с занулением последнего байта буфера. То есть копирование выглядит примерно так:

strncpy(buff, string, sizeof(buff) - 1);
buff[sizeof(buff)-1] = 0;

Но и этот вариант не является идеальным. Мы не можем отследить факт отсечения строки, который мог произойти во время копирования.

Похожие вещи обстоят и с другой функцией: strncat. Любопытно, что в ядре Linux довольно широко используется strncpy с большими буферами и далеко не всегда заполнение нулями необходимо.

Архитектурно-независимая реализация strncpy в ядре:

char *strncpy(char *dest, const char *src, size_t count)
{
char *tmp = dest;

while (count) {
if ((*tmp = *src) != 0)
src++;
tmp++;
count--;
}
return dest;
}

То-есть strncpy ведет себя также как и glibc версия -- цикл всегда выполняется count (размер буфера) раз.

Замечательно, что в OpenBSD 2.4 были включены функции strlcpy/strlcat. strlcpy and strlcat--Consistent, Safe, String Copy and Concatenation (at USENIX99).

Если кратко, то это именно те функции, которые чаще всего и нужны. Они, например, всегда гарантируют получение null-terminated string в буфере без лишних усилий. Кроме того, не зануляют остаток буфера. Вот как реализована strlcpy в ядре Linux:

size_t strlcpy(char *dest, const char *src, size_t size)
{
size_t ret = strlen(src);

if (size) {
size_t len = (ret >= size) ? size - 1 : ret;
memcpy(dest, src, len);
dest[len] = '\0';
}
return ret;
}

Те самые strlen с memcpy о которых мы говорили в начале. Как видим из кода, по результату функции можно определить было ли отсечение результата или нет.

На данный момент, если верить Википедии, многие библиотеки и приложения содержат в себе копии реализаций strlcpy/strlcat. Там же можно прочитать и про спорные моменты по введению и использованию этих функций.

Что, однако, настораживает:

peter@crashed:/tmp/linux-2.6-2.6.26$ grep -R "strlcpy" --include "*.c" * -l | wc -l
419
peter@crashed:/tmp/linux-2.6-2.6.26$ grep -R "strlcpy" --include "*.c" * -l | while read f; do grep "strncpy" $f; done | wc -l
38


Как видим в ядре Linux практически везде, где используется strlcpy, strncpy не используется, но и наоборот также. Это скорее похоже на привычку программистов, чем на систему.

Ну и в заключение код strlcat из ядра Linux.

size_t strlcat(char *dest, const char *src, size_t count)
{
size_t dsize = strlen(dest);
size_t len = strlen(src);
size_t res = dsize + len;

/* This would be a bug */
BUG_ON(dsize >= count);

dest += dsize;
count -= dsize;
if (len >= count)
len = count-1;
memcpy(dest, src, len);
dest[len] = 0;
return res;
}

2 комментария:

L.Dvoryansky комментирует...

Ты не бросаешь свои привычки : )
Вот интересно там в условии BUG_ON не ошибка ли?

gl00my комментирует...

Размер строки больше буфера -- это ненормальная ситуация -- сделать BUG().

Архив блога