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 бита.