EnglishРусский Map

Передача недостаточного числа параметров и бит NaT в Itanium

title
Передача недостаточного числа параметров и бит NaT в Itanium
type
summary
summary
Рэймонд Чен о сценариях сбоя при вызове функции с меньшим числом параметров и аппаратном контроле их количества в Itanium.
tags
calling-conventions, itanium, undefined-behavior, compilers, low-level
created
2026-04-30
updated
2026-04-30
lang
ru
source_updated
2026-04-30
translated
2026-09-01
translator
lllm/antigravity/gemini-3.7-flash-medium

В посте от 27 апреля 2026 года Рэймонд Чен разбирает обманчиво простой вопрос: если функция игнорирует второй параметр при положительном первом, безопасно ли вызывать её лишь с одним аргументом? Стандарты C и C++ отвечают отрицательно - вызов функции с неверным числом параметров считается неопределённым поведением (UB), и в заметке подробно описано, как именно это UB проявляется на реальном железе.

Сценарии сбоя на традиционных архитектурах

Разбалансировка стека (соглашения callee-clean). В соглашениях вроде stdcall очистка стека от аргументов лежит на вызываемой функции. Если вызывающий код передал на один параметр меньше ожидаемого, функция снимет со стека лишний слот, и указатель стека собьётся. Последующие адреса возврата, сохранённые регистры и локальные переменные сместятся, что обычно приводит к повреждению памяти или переходу по произвольному адресу. См. oldnewthing-20260423 (упомянуто в посте) о том, как инъекция кода деинсталлятора вызывала подобное в Explorer.

Переиспользование слота неиспользуемого параметра. Даже когда соглашение о вызове оставляет очистку стека вызывающему (caller-clean), вызываемая функция рассчитывает, что место под параметр выделено. Компилятор может разместить в этом слоте несвязанную мёртвую локальную переменную. Пример Чена:

int blah(int a, int b)
{
    if (a <= 0) {
        int c = f1();
        f2(a);
        return c;
    } else {
        return f3(a, b);
    }
}

Здесь компилятор может решить, что c и b могут делить память, поскольку b мертва в ветке a <= 0, и переменная b становится слотом под c. Если вызывающий код не зарезервировал память под b, присваивание c перезапишет данные в стековом кадре вызывающей функции.

Чтение неинициализированного регистра. Когда параметры передаются через регистры (как в большинстве современных ABI), пропущенный параметр означает, что вызываемая функция прочитает случайный мусор из регистра. На x86-64, ARM64 и других архитектурах это проявляется как искажённое значение - результат вычислений будет неверным, но аппаратного сбоя обычно не происходит.

Чем отличается Itanium

Itanium заходит дальше всех остальных архитектур из обзора Чена. В нём работают два механизма:

Бит NaT ("Not a Thing"). Каждый регистр общего назначения в Itanium снабжён дополнительным битом, указывающим на валидность значения в регистре. Чаще всего в состояние NaT попадают при неудачной спекулятивной загрузке либо в вычислениях, где хотя бы один операнд уже был NaT. Чтение NaT-регистра само по себе не вызывает прерывания, однако сброс (spill) NaT-значения в память порождает исключение "NaT consumption". В примере Чена функция берёт адрес неиспользуемого параметра и аварийно завершается, даже не считывая его значение, так как компилятору пришлось сбросить регистр в стек, чтобы выделить для него адрес.

Аппаратные стековые регистровые кадры. В Itanium правила вызова функций (ABI) контролируются аппаратно, а не просто соглашением. Вызывающий код объявляет: "я передаю N выходных регистров" через механизм register-stack engine; вызываемая функция видит их перенумерованными, начиная с r32. Чтение стекового регистра за пределами текущего кадра не определено, а запись за пределами текущего кадра обязана вызывать сбой Illegal Operation. Листовая функция (без собственного кадра) наследует ровно регистры входных параметров и ничего сверх того. Если вызывающий код объявил два выходных регистра, а вызываемая функция скомпилирована в расчёте на три, чтение третьего официально не определено, и архитектура вправе выбросить аппаратный сбой.

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

Общий вывод

Стандарты C/C++ прямо говорят: "не делайте так". Большинство архитектур деградируют мягче (испорченные значения, разбалансировка стека, отложенный segfault). Itanium же превращал то же самое UB в аппаратный сбой ровно на той инструкции, которая нарушила соглашение. Это укладывается в общую картину строгости Itanium: архитектура рассматривает нарушение соглашений как аппаратную ошибку, а не как незаметную порчу состояния.

Это одна из причин, почему под Itanium было так трудно адаптировать существующий код на C/C++: оптимизации, проходившие незаметно на x86 (повторное использование слота параметра, удаление неиспользуемых записей между вызовами), на Itanium приводили к явным сбоям, поскольку архитектура следила за деталями, которые никакой другой процессор не проверял.

Связанные страницы

  • oldnewthing-blog - блог Рэймонда Чена (сущность)
  • abi-stability - насколько строгим является ABI и чья задача поддерживать его стабильность