Показаны сообщения с ярлыком C. Показать все сообщения
Показаны сообщения с ярлыком C. Показать все сообщения

четверг, 26 февраля 2009 г.

readdir и d_type

Все-таки нужно всегда внимательно читать маны. Возьмём, например, фрагмент кода (на котором все и произошло):

while ((de = readdir(d))) {
if (de->d_type != DT_DIR)
continue;
if (!is_game(de->d_name))
continue;
n ++;
}

И вроде бы все нормально, но читаем man readdir и видим: 'If the file type could not be determined, the value DT_UNKNOWN is returned in d_type.' То есть вроде бы есть поле, вроде бы может вернуть тип. Но может и не вернуть. И что интересно, действительно не возвращает иногда. Например на raiserfs. Но не только. Интересно. И в notes:

Under glibc, programs can check for the availability of the fields not defined in POSIX.1 by testing whether the macros _DIRENT_HAVE_D_NAMLEN, _DIRENT_HAVE_D_RECLEN, _DIRENT_HAVE_D_OFF, or _DIRENT_HAVE_D_TYPE are defined. [Дальше]

четверг, 22 января 2009 г.

Расширения GCC

Довольно интересная глава в книжке Red Hat Enterprise Linux 4: Using the GNU Compiler Collection (GCC). Большинство вещей широко известны, но некоторые из них показались мне очень любопытными, чтобы о них написать.

Локальные метки
__label__ label;
или
__label__ label1, label2, /* … */;
Метка становится локальной для того блока, в котором она объявлена, например, из ядра:

#define _THIS_IP_ ({ __label__ __here; __here: (unsigned long)&&__here; })

Здесь мы видим, кстати, еще один интересный трюк.

Переопределяемые метки
Можно взять адрес метки с помощью оператора &&. Тип результата будет void*. Значение является константным и его можно использовать везде, где допускается константа этого типа. Один из примеров был приведен выше. Другой пример -- использование с goto, например:

void *ptr;
/* … */
ptr = &&foo;
goto *ptr;

Или еще хуже:

static void *array[] = { &&foo, &&bar, &&hack };
goto *array[i];

Нас предупреждают, что переходы между метками разных функций непредсказуемы.
Следующий пример, иллюстрирует адресную арифметику.

static const int array[] = { &&foo - &&foo, &&bar - &&foo,
&&hack - &&foo };
goto *(&&foo + array[i]);

Вложенные функции
Оказывается, gcc позволяет определять вложенные функции. Например:
hack (int *array, int size)
{
void store (int index, int value)
{ array[index] = value; }

intermediate (store, size);
}

Массивы с нулевым размером
Широко используется в качестве последнего элемента структуры (размер которой может варьироваться). Например:

struct line {
int length;
char contents[0];
};
struct line *thisline = (struct line *)
malloc (sizeof (struct line) + this_length);
thisline->length = this_length;

Важным является то, что в стандарте ISO C90 приходится использовать размер массива равным 1, что затрудняет вычисления правильного размера структура, а в стандарте ISO C99 нужно использовать массив с неопределенным числом элементов ([]).

Автоматические массивы с переменным размером
Пример.

FILE *
concat_fopen (char *s1, char *s2, char *mode)
{
char str[strlen (s1) + strlen (s2) + 1];
strcpy (str, s1);
strcat (str, s2);
return fopen (str, mode);
}

Или,даже, так:

struct entry
tester (int len, char data[len][len])
{
/* … */
}

Макросы с переменным числом параметров
По стандарту ISO C99:

#define debug(format, ...) fprintf (stderr, format, __VA_ARGS__)

Другой вариант, давно поддерживаемый gcc:

#define debug(format, args...) fprintf (stderr, format, args)

Правда, по стандарту C нельзя опустить третий параметр, и, например запись: debug ("A message") -- будет ошибочной. Чтобы решить эту проблему, можно использовать ##:

#define debug(format, ...) fprintf (stderr, format, ## __VA_ARGS__)

Инициализаторы
Инициализация массивов по ISO C99 (GCC позволяет делать так и в C89)

int a[6] = { [4] = 29, [2] = 15 };

Синоним:

int a[6] = { 0, 0, 15, 0, 29, 0 };

Еще одно расширение GNU:

int widths[] = { [0 ... 9] = 1, [10 ... 99] = 2, [100] = 3 };

Почти как meta tables в Lua. :)

Диапазоны в case
А это вообще убойная вещь, например:

case 'A' ... 'Z':
case 1 ... 5:

Пробелы вокруг точек обязательны.

Если эти примеры показались для вас интересными, рекомендую прочитать всю главу.
[Дальше]

суббота, 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
[Дальше]

воскресенье, 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;
}

[Дальше]

суббота, 4 октября 2008 г.

Трюки в коде ядра Linux.

В коде ядра встречаются конструкции, которые можно назвать трюками программирования. Многие из них оказываются полезными, чтобы их запомнить и использовать.

Двойное отрицание
Иногда можно встретить двойное логическое отрицание. Например:

#define likely(x)        __builtin_expect(!!(x), 1)

На самом деле, тут мы еще видим реализацию одного из способов оптимизации, но об этом в другой раз. Пример содержит двойное отрицание. Зачем?

Если x является логическим выражением, то двойное отрицание не нужно, но что будет если в коде мы напишем что-то вроде:

int counter=0x100;
/* ... */
if (likely(counter))

И тут становится понятно, что двойное отрицание выражения всегда дает булевую величину (0 или 1). Его можно назвать приведением к булевому типу, что довольно непривычно звучит в C.

Выравнивание
/* align addr on a size boundary - adjust address up/down if needed */
#define _ALIGN_UP(addr,size) (((addr)+((size)-1))&(~((size)-1)))
#define _ALIGN_DOWN(addr,size) ((addr)&(~((size)-1)))

Ну, тут все понятно.

Комментарии с помощью препроцессора
#if 0
/* XXX: let's do this when we verify it is OK */
if (ret & VM_FAULT_OOM)
ret = -ENOMEM;
#endif

Как видим, удобно для быстрого отключения/включения кода, особенно если внутри уже есть комментарии /* ... */.

Необычное сравнение с константой

if (NULL == siocb->scm)
siocb->scm = &tmp_scm;

Помогает избежать ошибок, когда вместо сравнения (==) по ошибке ставится знак присваивания (=).

Является ли значение степенью 2?

bool is_power_of_2(unsigned long n)
{
return (n != 0 && ((n & (n - 1)) == 0));
}


do { /*...*/ } while(0)

# define cap_clear(c) do { (c) = __cap_empty_set; } while (0)

Очень часто используется при определении макросов, широко известный трюк. Теперь раскрытие макроса может использоваться как вызов функции, так как при раскрытии код будет полностью размещен в своем блоке, который выполнится один раз (while(0)). Внутри блока можно определять переменные. Например:
#ifndef swap
#define swap(x, y) do { typeof(x) z = x; x = y; y = z; } while (0)
#endif


Использование goto

Можно долго говорить, что использование goto в C это плохо, но никакое правило не может быть универсальным. Иначе -- фарисейство или фанатизм... ;)
grep "goto" -R * | wc -l
в дереве исходников ядра дает результат больший чем 50 тысяч.

В большинстве случаев это попытка избежать большей вложенности кода при отработке ошибочных систуаций и освобождении ресурсов. Например:

static long do_sys_truncate(const char __user * path, loff_t length)
{
struct nameidata nd;
struct inode * inode;
int error;

error = -EINVAL;
if (length < 0) /* sorry, but loff_t says... */
goto out;
/* ... */
inode = nd.path.dentry->d_inode;

/* For directories it's -EISDIR, for other non-regulars - -EINVAL */
error = -EISDIR;
if (S_ISDIR(inode->i_mode))
goto dput_and_out;
/* ... */
error = vfs_permission(&nd, MAY_WRITE);
if (error)
goto mnt_drop_write_and_out;
/* ... */
put_write_and_out:
put_write_access(inode);
mnt_drop_write_and_out:
mnt_drop_write(nd.path.mnt);
dput_and_out:
path_put(&nd.path);
out:
return error;
}

Это гораздо проще, чем вложенные друг в друга блоки или излишнее разбиение на функции.

Возвращение кодов ошибок с типом "указатель".

Пример кода:

char *tmp = getname(filename);
int fd = PTR_ERR(tmp);
if (!IS_ERR(tmp)) {
/* ... */
}
return fd;
}

Здесь видно, что getname возвращает указатель, который тем-не менее, может содержать признак ошибки. Здесь, кстати, нужно быть осторожным! Так как это противоречит практике возвращать признак ошибки как NULL. Нужно всегда четко понимать, что возвращает функция ядра в случае ошибки: NULL или PTR_ERR. Например, если написать следующий код:

char *tmp = getname(filename);
if (!tmp)
return NULL;
/* ... do something .. */

Это уже уязвимость, хотя выглядит прилично. Если не знать что возвращает getname...

Реализация механизма крайне проста, и сама по-себе тянет на трюк.
#define MAX_ERRNO       4095
#define IS_ERR_VALUE(x) unlikely((x) >= (unsigned long)-MAX_ERRNO)

static inline long IS_ERR(const void *ptr)
{
return IS_ERR_VALUE((unsigned long)ptr);
}

Как видим, указатель содержит отрицательное значение ошибки. Таким образом, выброшенный диапазон адресов приходится на последние 4Kb виртуального адресного пространства, который функция никогда не вернет. Очевидно, что превращение кода ошибки в указатель -- это приведение типа:

static inline void *ERR_PTR(long error)
{
return (void *) error;
}

Вообще, все это дело терпимо для кода ядра Linux, но совсем нехорошо заниматься такими трюками в пользовательском (предположительно переносимом) коде, так-как все-таки это предполагает, что приведение long в void* и назад -- обратимы.

Вот и все на сегодня. :)
[Дальше]

Архив блога