Передача недостаточного числа параметров и бит NaT в Itanium
- title
- Передача недостаточного числа параметров и бит NaT в Itanium
- type
- summary
- summary
- Рэймонд Чен о сценариях сбоя при вызове функции с меньшим числом параметров и аппаратном контроле их количества в Itanium.
- sources
- oldnewthing-20260427
- tags
- calling-conventions, itanium, undefined-behavior, compilers, low-level
- created
- 2026-04-30
- updated
- 2026-04-30
- lang
- ru
- translation_of
- itanium-too-few-parameters
- 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 и чья задача поддерживать его стабильность