Подтвердить что ты не робот

Является ли GCC неправильным указателем на va_list, переданным функции?

Вопрос "Передать va_list или указатель на va_list?" содержит ответ, который цитирует стандарт (переменные аргументы ISO/IEC 9899: 1999 - §7.15) <stdarg.h>, сноска 212), так как явно сказано, что:

Разрешено создавать указатель на va_list и передавать этот указатель на другую функцию, и в этом случае исходная функция может дополнительно использовать исходный список после возвращения другой функции.

Я компилирую код, который можно проиллюстрировать следующим (реальный код очень значительно сложнее, а оригинальные функции выполняют гораздо больше работы, чем показано здесь).

vap.c

#include <stdarg.h>
#include <stdio.h>

static void test_ptr(const char *fmt, va_list *argp)
{
    int x;
    x = va_arg(*argp, int);
    printf(fmt, x);
}

static void test_val(const char *fmt, va_list args)
{
    test_ptr(fmt, &args);
}

static void test(const char *fmt, ...)
{
    va_list args;
    va_start(args, fmt);   /* First use */
    test_val(fmt, args);
    va_end(args);
    va_start(args, fmt);   /* Second use */
    test_ptr(fmt, &args);
    va_end(args);
}

int main(void)
{
    test("%d", 3);
    return 0;
}

Сообщения об ошибках

Когда я его компилирую (на RHEL5 с GCC 4.1.2 или 4.5.1), я получаю следующие сообщения об ошибках. Обратите внимание на то, что более информативное сообщение об ошибке 4.5.1 - команда GCC должна быть поздравлена ​​с улучшением!

$ gcc --version
gcc (GCC) 4.5.1
Copyright (C) 2010 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ /usr/bin/gcc --version
gcc (GCC) 4.1.2 20080704 (Red Hat 4.1.2-44)
Copyright (C) 2006 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ gcc -c vap.c
vap.c: In function ‘test_val’:
vap.c:13:5: warning: passing argument 2 of ‘test_ptr’ from incompatible pointer type
vap.c:4:13: note: expected ‘struct __va_list_tag (*)[1]’ but argument is of type ‘struct __va_list_tag **’
$ /usr/bin/gcc -c vap.c
vap.c: In function ‘test_val’:
vap.c:13: warning: passing argument 2 of ‘test_ptr’ from incompatible pointer type
$ 

Я получаю те же сообщения на MacOS X Lion с GCC/LLVM 4.2.1 и с GCC 4.6.1:

$ /usr/bin/gcc --version
i686-apple-darwin11-llvm-gcc-4.2 (GCC) 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2335.15.00)
Copyright (C) 2007 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ gcc --version
gcc (GCC) 4.6.1
Copyright (C) 2011 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$

Вопросы

  • Может ли кто-нибудь сформулировать, почему функция test_val() не может передать va_list, переданную как аргумент test_ptr(), тогда как функция test() (которая создала va_list) может?

  • Правильно ли GCC жаловаться на косвенную передачу указателя в test_val()?

В обоих случаях я вижу ответ нечетко, но я не могу описать его лаконично. Я думаю, что код в test_val() злоупотребляет va_list, и хорошо, что код не будет компилироваться, но я хотел бы быть уверен, прежде чем я его исправлю.


Обновление 2012-03-30

На этой неделе я занялся проблемным кодом. Прежде чем вносить изменения, я пошел, чтобы найти, где используются бесполезные функции — и это не так! Итак, я решил проблему с ошибкой компиляции, удалив функции (4 внешне видимых, но неиспользуемых, плюс 2 статических, содержащих содержащийся в нем код проблемы). Это было намного проще, чем придумать, как бороться с беспорядком. (Это также объясняет, почему никогда не было доказательств проблемы времени выполнения, вызванной кодом.)

4b9b3361

Ответ 1

Это известная проблема. На некоторых архитектурах (в частности, x86-64) va_list должен быть более сложным, чем простой указатель на стек, например, потому что некоторые аргументы могут быть переданы в регистрах или вне диапазона каким-либо другим способом (см. этот ответ для определения va_list на x86-64).

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

Это не должно нарушать стандарт C, который говорит только, что va_list должен быть полным типом объекта и даже явно указывает на то, что передача аргумента va_list может фактически не клонировать необходимое состояние: va_list objects имеют неопределенное значение, если они передаются в качестве аргументов и потребляются в вызываемой функции.

Но даже если тип массива va_list является законным, он все же приводит к проблемам, которые вы испытывали: поскольку параметры типа va_list имеют тип "неправильный", например struct __va_list_tag * вместо struct __va_list_tag [1], это будет взорваться в случаях, когда имеет значение разница между массивами и указателями.

Реальная проблема заключается не в несовместимости типов gcc, а по указателю вместо семантики передачи аргументов по значению: &args в test_val() указывает на промежуточную переменную-указатель вместо объекта va_list; игнорирование предупреждения означает, что вы вызовете va_arg() в test_ptr() в переменной указателя, которая должна возвращать мусор (или segfault, если вам повезет) и повреждает стек.

Один способ обернуть ваш va_list в структуру и передать это вместо этого. Другое решение Я видел в дикой природе, даже здесь, на SO, есть для использования va_copy для создания локальной копии аргумента, а затем передать указатель на это:

static void test_val(const char *fmt, va_list args)
{
    va_list args_copy;
    va_copy(args_copy, args);
    test_ptr(fmt, &args_copy);
    va_end(args_copy); 
}

Это должно работать на практике, но технически это может быть или не быть undefined поведением, в зависимости от вашей интерпретации стандарта:

Если va_copy() реализован как макрос, настройки параметров не выполняются, и может иметь значение, что args не имеет тип va_list. Однако, поскольку неясно, является ли va_copy() макросом или функцией, можно утверждать, что он, по крайней мере, может быть функцией, и корректировки параметров неявно предполагаются в прототипе, указанном для макроса. Может быть, неплохо попросить чиновников разъяснить или даже подать отчет о дефекте.

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

#ifdef HAVE_VA_LIST_AS_ARRAY
#define MAKE_POINTER_FROM_VA_LIST_ARG(arg) ((va_list *)(arg))
#else
#define MAKE_POINTER_FROM_VA_LIST_ARG(arg) (&(arg))
#endif

static void test_val(const char *fmt, va_list args)
{
    test_ptr(fmt, MAKE_POINTER_FROM_VA_LIST_ARG(args));
}

Ответ 2

Проблема не относится к va_list. Следующий код приводит к аналогичному предупреждению:

typedef char *test[1];

void x(test *a)
{
}

void y(test o)
{
    x(&o);
}

Проблема связана с тем, как C обрабатывает функциональные переменные, которые также являются массивами, вероятно, из-за того, что массивы передаются как ссылка, а не по значению. Тип o не совпадает с типом локальной переменной типа test, в данном случае: char *** вместо char *(*)[1].

Возвращаясь к исходной проблеме, простой способ обойти ее - использовать контейнерную структуру:

struct va_list_wrapper {
    va_list v;
};

и не будет проблем с типизацией, передающих ему точку.

Ответ 3

Как отмечали другие, этот вопрос проявляется, когда va_list является типом массива. Это разрешено стандартом, который говорит только, что va_list должен быть "типом объекта".

Вы можете исправить функцию test_val() следующим образом:

static void test_val(const char *fmt, va_list args)
{
    va_list args_copy;

    /* Note: This seemingly unnecessary copy is required in case va_list
     * is an array type. */
    va_copy(args_copy, args);
    test_ptr(fmt, &args_copy);
    va_end(args_copy);
}

Ответ 4

Я думаю, что va_list должен быть объявлен как тип массива, который "сводится" к типу указателя, объявленного как параметр функции. Поэтому &, примененный к типу va_list в test_val, дает указатель на тип указателя, а не указатель на тип массива, однако функция test_ptr объявляет один из своих параметров как указатель на массив тип, который фактически предусмотрен в функции test.