Многократный вызов variadic-метода Obj-C приводит к появлению недопустимых аргументов - PullRequest
0 голосов
/ 01 марта 2019

У меня есть метод с переменным значением, в который я хочу передать несколько значений перечисления:

typedef NS_ENUM(NSInteger, Enum) {
    Enum0 = 0,
    Enum1 = 1,
    Enum2 = 2,
    Enum3 = 3,
    Enum4 = 4,
    Enum5 = 5,
    Enum6 = 6,
};

@interface Variadics : NSObject
- (void)test:(Enum)enm, ...;
@end

вот реализация

@implementation Variadics
- (void)test:(Enum)enm, ... {
    va_list args;
    va_start(args, enm);
    for (; enm != 0; enm = va_arg(args, Enum))
    {
        NSAssert(enm <= Enum6, @"invalid enum");
    }
    va_end(args);
}
@end

И вот как я ее называю:

Variadics* var = [[Variadics alloc] init];
[var test:Enum1, Enum2, Enum3, Enum4, Enum5, Enum6, 0];

, который работает отлично!Но если я изменю это на следующее:

Variadics* var = [[Variadics alloc] init];
[var test:Enum1, Enum2, Enum3, Enum4, Enum5, Enum6, 0];
[var test:Enum1, Enum2, Enum3, Enum4, Enum5, Enum6, 0];
[var test:Enum1, Enum2, Enum3, Enum4, Enum5, Enum6, 0];
[var test:Enum1, Enum2, Enum3, Enum4, Enum5, Enum6, 0];

внезапно я ударю это утверждение и enum == 4294967296.Еще более странно, я ударил утверждение при первом вызове .Три более поздних звонка никогда не выполняются.

Что происходит?

1 Ответ

0 голосов
/ 01 марта 2019

Ваше нулевое завершение в конце списка аргументов передается вашей функции как int (32-битное) значение, но ваше va_arg вытягивает значение NSInteger (иначе long)из стека.Таким образом, вы извлекаете дополнительные 32 бита из стека и смотрите на все это как на одно 64-битное значение, половина которого - это ноль, который вы намеревались, а половина - то, что произошло рядом с ним в памяти.в этот раз.

Вы должны были бы сделать это, чтобы получить поведение, которое вы хотите:

[var test:Enum1, Enum2, Enum3, Enum4, Enum5, Enum6, (NSInteger)0];

Как поясняет Роб Мейофф в комментариях ниже, лицевой нулевой литерал, являющийся целочисленным значением,трактуется как int.Применяются обычные целочисленные правила продвижения C *;в списке varargs, поскольку аргументы не имеют объявленного типа, более целочисленные типы повышаются до - и передаются как - int s.Поскольку компилятор не имеет представления о реальных типах, которые вы ожидаете увидеть во время выполнения, то int не будет преобразован в long для вас, поэтому он попадает в стек как int.

Как правило, завершение varargs выполняется либо неявно (предвидение стиля printf числа и типов arg), либо с константой, подобной nil, которая будет правильной ширины, и эти подходы позволяют избежать этой проблемы.В старом 32-битном мире, где значения enum были int s, и NSInteger были int, а целочисленное продвижение по умолчанию было , а также до int, этиразличия были скрыты.

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

* См. Стандарт C18 §6.3.1.1 ;-)


Объяснитель бонуса: Почему вы увидели значение enum == 4294967296?

Десятичное число 4294967296 равно 0x0000000100000000.Вторая (нижняя) половина этого значения была 32-битным нулем, который вы поставили туда.Верхняя половина выглядит так, как будто это просто число 1.Сначала я предположил, что это будет какая-то другая (действительная) часть текущего стекового фрейма, но некоторые исследования (на 64-битном Mac с использованием текущего llvm с Xcode и т. Д.) Показывают, что компилятор устанавливает вызов -test:...путем нажатия на enum args с помощью movq (move quad = 64bit) и конечного нулевого arg с movl (move long = 32bit).Поскольку стек выровнен на 64 бита, в стеке памяти между предпоследним и конечным аргументами остается «дыра» в 32 бита (т.е. не перезаписывается).Это место, которое содержало 0x1.Таким образом, вы не читаете соседний аргумент или другое значение, которое является допустимым, но используется для чего-то другого.Вы читаете призрачное значение из рабочего пространства вызова более ранней функции.В моей отладке, это не похоже на alloc / init Variable - это что-то предшествующее и не связанное с тестовым кодом под рукой.

...