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 статические, которые содержали проблемный код). Это было гораздо проще, чем разбираться с беспорядком. (Это также объясняет, почему никогда не было никаких признаков проблем во время выполнения, вызванных кодом.)
4 ответа
Это известная проблема. На некоторых архитектурах (в частности, x86-64), va_list
должен быть более сложным, чем простой указатель на стек, например, потому что некоторые аргументы могут передаваться в регистрах или вне диапазона каким-либо другим способом (см. этот ответ для определения va_list
на x86-64).
На таких архитектурах принято делать va_list
тип массива, так что параметры типа va_list
будет адаптирован к типам указателей, и вместо всей структуры необходимо передать только один указатель.
Это не должно нарушать стандарт С, который говорит только о том, что va_list
должен быть полным типом объекта и даже явно учитывать тот факт, что передача va_list
аргумент не может на самом деле клонировать необходимое состояние: va_list
объекты имеют неопределенное значение, если они передаются в качестве аргументов и используются в вызываемой функции.
Но даже если сделать 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);
}
Это должно работать на практике, но технически это может быть или не быть неопределенным поведением, в зависимости от вашей интерпретации стандарта:
Если 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));
}
Как уже отмечали другие, эта проблема проявляется, когда 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);
}
Проблема не характерна для va_list
, Следующий код приводит к похожему предупреждению:
typedef char *test[1];
void x(test *a)
{
}
void y(test o)
{
x(&o);
}
Проблема связана с тем, как С обрабатывает функциональные переменные, которые также являются массивами, возможно, из-за того, что массивы передаются как ссылки, а не по значению. Тип o
не совпадает с типом локальной переменной типа test
, в этом случае: char ***
вместо char *(*)[1]
,
Возвращаясь к исходной проблеме, простой способ обойти ее - использовать структуру контейнера:
struct va_list_wrapper {
va_list v;
};
и не было бы проблем с печатью, передавая точку в этом.
Я думаю va_list
должен быть объявлен как тип массива, который "сводится" к типу указателя при объявлении в качестве параметра функции. Следовательно &
применяется к va_list
введите test_val
дает указатель на тип указателя, а не указатель на тип массива, однако, test_ptr
Функция объявляет один из своих параметров в качестве указателя на тип массива, который фактически предоставляется в test
функция.