Почему эта переменная функция не работает с 4-м параметром в Windows x64? - PullRequest
3 голосов
/ 08 апреля 2009

Ниже приведен код, который включает в себя функцию с переменными числами и вызывает функцию с переменными числами. Я ожидаю, что он выведет каждую последовательность чисел соответственно. Это происходит при компиляции как 32-битный исполняемый файл, но не при компиляции как 64-битный исполняемый файл.

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

#ifdef _WIN32
#define SIZE_T_FMT "%Iu"
#else
#define SIZE_T_FMT "%zu"
#endif


static void dumpargs(size_t count, ...) {

    size_t i;
    va_list args;

    printf("dumpargs: argument count: " SIZE_T_FMT "\n", count);

    va_start(args, count);

    for (i = 0; i < count; i++) {

        size_t val = va_arg(args, size_t);
        printf("Value=" SIZE_T_FMT "\n", val);
    }
    va_end(args);
}

int main(int argc, char** argv) {

    (void)argc;
    (void)argv;

    dumpargs(1, 10);
    dumpargs(2, 10, 20);
    dumpargs(3, 10, 20, 30);
    dumpargs(4, 10, 20, 30, 40);
    dumpargs(5, 10, 20, 30, 40, 50);

    return 0;
}

Вот вывод при компиляции для 64-бит:

dumpargs: argument count: 1
Value=10
dumpargs: argument count: 2
Value=10
Value=20
dumpargs: argument count: 3
Value=10
Value=20
Value=30
dumpargs: argument count: 4
Value=10
Value=20
Value=30
Value=14757395255531667496
dumpargs: argument count: 5
Value=10
Value=20
Value=30
Value=14757395255531667496
Value=14757395255531667506

Edit:

Обратите внимание, что причина, по которой функция с переменным значением вытягивает значение size_t, заключается в том, что в реальности это используется для функции с переменным числом, которая принимает список указателей и длин. Естественно, аргумент длины должен быть size_t. И в некоторых случаях вызывающая сторона может передать известную длину для чего-то:

void myfunc(size_t pairs, ...) {
    va_list args;
    va_start(args, count);

    for (i = 0; i < pairs; i++) {
        const void* ptr = va_arg(args, const void*);
        size_t len = va_arg(args, size_t);
        process(ptr, len);
    }
    va_end(args);
}

void user(void) {
    myfunc(2, ptr1, ptr1_len, ptr2, 4);
}

Обратите внимание, что 4, переданный в myfunc, может столкнуться с проблемой, описанной выше. И да, действительно, вызывающий должен использовать sizeof или результат strlen или просто поместить число 4 в size_t где-нибудь. Но дело в том, что компилятор не улавливает это (обычная опасность для переменных функций).

Правильно сделать здесь - исключить функцию переменной и заменить ее более совершенным механизмом, обеспечивающим безопасность типов. Однако я хотел бы задокументировать эту проблему и собрать более подробную информацию о том, почему именно эта проблема существует на этой платформе и проявляется так, как она есть.

Ответы [ 5 ]

6 голосов
/ 11 апреля 2009

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

Причина, по которой это начинает происходить 4-го числа, заключается в том, что соглашение о вызовах используется в x86-64 . Насколько мне известно, и Visual C ++, и GCC используют регистры для первых нескольких параметров, а затем после этого используют стек.

Я предполагаю, что это имеет место даже для функций с переменным числом (что делает меня странным, так как это усложнит макросы va_ *).

На x86 стандартное соглашение о вызовах C - это всегда использование стека.

4 голосов
/ 08 апреля 2009

Проблема в том, что вы используете size_t для представления типа значений. Это неверно, значения на самом деле являются нормальными 32-битными значениями на Win64.

Size_t следует использовать только для значений, которые изменяют размер в зависимости от 32- или 64-разрядной платформы (например, указатели). Измените код на int или __int32, и это должно решить вашу проблему.

Причина, по которой это хорошо работает на Win32, заключается в том, что size_t - это тип другого размера, в зависимости от платформы. Для 32-битных окон это будет 32 бита, а для 64-битных - 64-битное. Так что в 32-битных окнах это просто соответствует размеру используемого вами типа данных.

2 голосов
/ 08 апреля 2009

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

В этом случае size_t является 32-разрядным на Win32 и 64-разрядным на Win64. Он должен варьироваться по размеру, чтобы выполнять определенную роль. Таким образом, чтобы переменная функция правильно выводила аргументы типа size_t, вызывающая сторона должна была убедиться, что компилятор мог сказать, что аргумент этого типа был во время компиляции в вызывающем модуле.

К сожалению 10 является константой типа int. Не существует определенной буквы суффикса, которая помечает константу типа size_t. Вы могли бы скрыть этот факт внутри макроса для конкретной платформы, но это было бы не проще, чем написать (size_z)10 на сайте вызовов.

Похоже, что он работает частично из-за фактического соглашения о вызовах, используемого в Win64. Из приведенных примеров мы можем сказать, что первые четыре интегральных аргумента функции передаются в регистрах, а остальные в стеке. Это позволило считать и считать первые три параметра переменной правильно.

Однако только работает . Вы на самом деле стоите прямо на территории неопределенного поведения, и «неопределенный» действительно означает «неопределенный»: все может случиться. На других платформах тоже может случиться что угодно.

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

В некоторых случаях, когда интерфейсы хорошо известны, можно предупредить о несоответствии типов. Например, gcc часто может распознать, что тип аргумента printf() не соответствует строке формата, и выдает предупреждение. Но сделать это в общем случае для всех функций с переменными значениями - трудно .

2 голосов
/ 08 апреля 2009

Причина этого в том, что size_t определяется как 32-разрядное значение в 32-разрядной Windows и 64-разрядное значение в 64-разрядной Windows. Когда 4-й аргумент передается в функцию variadic, верхние биты оказываются неинициализированными. 4-е и 5-е значения, которые вытащены на самом деле:

Value=0xcccccccc00000028
Value=0xcccccccc00000032

Я могу решить эту проблему с помощью простого приведения всех аргументов, таких как:

dumpargs(5, (size_t)10, (size_t)20, (size_t)30, (size_t)40, (size_t)50);

Это не отвечает на все мои вопросы, однако; такие как:

  • Почему это 4-й аргумент? Вероятно, потому что первые 3 находятся в регистрах?
  • Как можно избежать этой ситуации безопасным переносимым способом?
  • Это происходит на других 64-битных платформах с использованием 64-битных значений (игнорируя, что size_t может быть 32-битным на некоторых 64-битных платформах)?
  • Должен ли я извлекать значения в виде 32-разрядных значений независимо от целевой платформы, и это вызовет проблемы, если 64-разрядное значение будет введено в функцию с переменными числами?
  • Что стандарты говорят об этом поведении?

Edit:

Я действительно хотел получить цитату из Стандарт , но это то, что не поддерживает гиперссылки и стоит покупки и загрузки . Поэтому я считаю, что цитирование будет нарушением авторских прав.

Ссылаясь на comp.lang.c FAQ , стало ясно, что при написании функции, которая принимает переменное число аргументов , вы ничего не можете сделать для безопасности типов. Вызывающий объект должен убедиться, что каждый аргумент либо полностью соответствует, либо явно приведен . Не существует неявных преобразований.

Это должно быть очевидно для тех, кто понимает C и printf (обратите внимание, что у gcc есть возможность проверять строки формата printf-стиля ), но не так очевидно, что не только типы не приводятся неявно, но если размер типов не соответствует тому, что извлечено, вы можете иметь неинициализированные данные или вообще неопределенное поведение. «Слот», в котором помещается аргумент, может быть не инициализирован в 0, и не может быть «слота» - на некоторых платформах вы можете передавать 64-битное значение и извлекать два 32-битных значения внутри функции variadic. , Это неопределенное поведение.

1 голос
/ 08 апреля 2009

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

Вы уже обнаружили, что C играет быстро и свободно с типами (см. Также подпись и продвижение), поэтому явное приведение является наиболее очевидным решением. Это часто наблюдается, когда целочисленные константы явно определены с такими вещами, как UL или ULL.

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

Наилучшим практическим решением было бы тщательно документировать, выполнять проверки кода и / или писать модульные тесты с учетом этой ошибки.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...