va_arg 64-битная проблема

У меня такой C-код. В 64-битной системе Linux результат: 4294967264 вместо -32. И clang, и gcc выдают двоичный файл с одинаковыми неверными результатами. Проблема в линии:

* v = va_arg (args, long);
#include <stdio.h>
#include <string.h>
#include <stdarg.h>

void setter(long *v, ...)
{
        va_list args;
        va_start(args, v);
        *v = va_arg(args, long);
        va_end(args);
}

int main()
{
        long v = 0;
        setter((long *) &v, -32);
        printf("%ld\n", v);
        return 0;
}

3 ответа

Решение

Вам на самом деле нужно пройти long к вашей функции. Вы передаете int,

setter(&v, -32L);

В архитектуре x86_64 размер long это 64 бит Когда вы проходите -32 в setter(), его тип int и только 32-битный. Если ты хочешь long чтобы быть переданным, бросьте это явно. Например:

setter((long *) &v, (long)-32);

Небольшое уточнение:
Как уже говорилось, на 64-битных архитектурах long является 64-битным Однако это еще не все, поскольку C/C++ выполняет автоматическое преобразование. Здесь setter() функция принимает один указанный аргумент и ноль или более неуказанных аргументов. Аргумент -32 является одним из неуказанных аргументов, поэтому компилятор не знает, что на самом деле long предназначен и сохраняет int (32-бит). Кроме того, дополнительный ноль помещается в стек для поддержания 64-разрядного выравнивания. Это даст напечатанный результат, как указано выше. Поэтому вы должны явно указать, что вы хотите long здесь (либо -32L или же (long) -32). Но если функция на самом деле была объявлена void setter (long *v, long arg)тогда компилятор знал бы, что второй аргумент longи автоматически преобразовал int аргумент.

Еще более странная, но все же не настоящая ошибка компилятора: если вы передадите -32 значение в качестве седьмого или более позднего аргумента переменной, оно может быть расширено до 64 бит:

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

/* long long int is a 64bit datatype on both Unix and Windows */
void setter(long long int arg, ...)
{
    va_list args;
    va_start(args, arg);
    while(arg != 0) {
        printf("0x%016llx  %lld\n", arg, arg);
        arg = va_arg(args, long long int);
    }
    va_end(args);
}

int main()
{
    setter(-32, -32, -32, -32, -32, -32, -32, -32, 0);
    return 0;
}

И вывод на x64 Linux:

0xffffffffffffffe0  -32
0x00000000ffffffe0  4294967264
0x00000000ffffffe0  4294967264
0x00000000ffffffe0  4294967264
0x00000000ffffffe0  4294967264
0x00000000ffffffe0  4294967264
0xffffffffffffffe0  -32
0xffffffffffffffe0  -32

Тем не менее, вывод на x64 Windows выглядит примерно так:

0xffffffffffffffe0  -32
0x00000000ffffffe0  4294967264
0x00000000ffffffe0  4294967264
0x00000000ffffffe0  4294967264
0x00040800ffffffe0  1134700294832096
0x178bfbffffffffe0  1696726761565323232
0x00007ff6ffffffe0  140698833649632
0x00007ff6ffffffe0  140698833649632

Причина в том, что в соглашениях о вызовах 64-битных x86 ряд ведущих аргументов (6 в System V AMD64 ABI, 4 в Microsoft x64) передается через регистры ЦП, и только последующие аргументы передаются через стек.

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

Для последующих аргументов это зависит от компилятора:

  • GCC помещает расширенные 64-битные значения в стек
  • MSVC cl.exe записывает в стек все нижние 32 бита, оставляя там неинициализированные верхние 32 бита
  • не уверен насчет других компиляторов

В любом случае для каждого аргумента в стеке зарезервировано 64 бита.

Другие вопросы по тегам