Мне интересно, какова цель JS_CANONICALIZE_NAN и всегда ли она нужна на всех платформах?
какова цель JS_CANONICALIZE_NAN в движке Spidermonkey?
Ответы (1)
Это весело! Итак, SpiderMonkey внутренне использует представление помеченных значений для представления «нетипизированных значений» JavScript — это позволяет виртуальной машине определять такие вещи, как «переменная, хранящаяся в a
, является числом, а значение, хранящееся в b
, является числом, поэтому запуск a + b
делает числовое сложение».
Существует множество различных схем для маркировки значений, и SpiderMonkey использует одну из них, называемую «упаковкой NaN». Это означает, что все нетипизированные значения в движке представлены 64-битными значениями, которые могут быть:
- двойник, или
- помеченный недвойник, который находится в «пространстве NaN» значений с плавающей запятой двойной точности IEEE.
Настоящий трюк здесь заключается в том, что современные системы обычно используют один битовый шаблон для представления NaN, который вы можете наблюдать как результат sqrt(-1)
или log(0)
в math.h. но существует множество битовых шаблонов, которые также считаются NaN в соответствии со спецификацией IEEE с плавающей запятой.
Дубль состоит из подполей:
{sign: 1, exponent: 11, significand: 52}
NaN представлены путем заполнения поля экспоненты единицами и помещения ненулевого значения в мантиссу.
Если вы запустите небольшую программу, подобную этой, чтобы увидеть значения NaN вашей платформы:
#include <stdio.h>
#include <math.h>
#include <limits>
static unsigned long long
DoubleAsULL(double d) {
return *((unsigned long long *) &d);
}
int main() {
double sqrtNaN = sqrt(-1);
printf("%5f 0x%llx\n", sqrtNaN, DoubleAsULL(sqrtNaN));
double logNaN = log(-1);
printf("%5f 0x%llx\n", logNaN, DoubleAsULL(logNaN));
double compilerNaN = NAN;
printf("%5f 0x%llx\n", compilerNaN, DoubleAsULL(compilerNaN));
double compilerSNAN = std::numeric_limits<double>::signaling_NaN();
printf("%5f 0x%llx\n", compilerSNAN, DoubleAsULL(compilerSNAN));
return 0;
}
Вы увидите такой вывод:
-nan 0xfff8000000000000 // Canonical qNaNs...
nan 0x7ff8000000000000
nan 0x7ff8000000000000
nan 0x7ff4000000000000 // sNaN (signaling)
Обратите внимание, что единственная разница для тихих NaN заключается в бите знака, за которым всегда следуют 12 битов 1, что удовлетворяет упомянутому выше требованию NaN. Последний, сигнализирующий NaN, очищает 12-й (is_quiet) бит NaN и позволяет 13-му сохранить инвариант NaN, упомянутый выше.
Кроме этого, пространство NaN свободно для игры — 11 бит для заполнения экспоненты, убедитесь, что знак не равен нулю, и у вас осталось много места. В x64 мы используем предположение о 47-битном виртуальном адресе, что оставляет нам 64 - 47 - 11 = 6
бит для аннотирования типов значений. В x86 все указатели объектов помещаются в младшие 32 бита.
Тем не менее, нам по-прежнему нужно убедиться, что неканонические NaN, если они проникают через что-то вроде js-ctypes, не производят что-то похожее на помеченные недвойные значения, потому что это может привести к эксплойтному поведению в виртуальной машине. (Обработка чисел как объектов — это очень плохие новости.) Итак, когда мы формируем двойные числа (как в DOUBLE_TO_JSVAL), мы обязательно канонизируем все двойные числа, где d != d
, в каноническую форму NaN.
Дополнительная информация содержится в ошибке 584168.