Share via


Несмотря на то, что соглашение вызовов x64 резервирует небольшое пространство для параметров, вы не обязаны использовать его в таком качестве

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

У нас есть тестовая программа, которая работает корректно при отключенной оптимизации, но когда она скомпилирована с максимальным уровнем оптимизации, все сразу же идет наперекосяк. Вот эта функция не получает корректных значений параметров argc и argv:

    int __cdecl     wmain( int argc, WCHAR** argv ) { ... }

При отключенной оптимизации генерируется корректный код:

            mov          [rsp+10h],rdx  // argv             mov          [rsp+8],ecx    // argc             sub          rsp,158h       // локальные переменные             mov          [rsp+130h],0FFFFFFFFFFFFFFFEh             ...

Но когда мы запускаем компиляцию с оптимизацией, то происходит какая-то путаница:

            mov          rax,rsp             push         rsi             push         rdi             push         r13             sub          rsp,0E0h             mov          qword ptr [rsp+78h],0FFFFFFFFFFFFFFFEh             mov          [rax+8],rbx    // ??? должно быть ecx (argc)             mov          [rax+10h],rbp  // ??? должно быть edx (argv)

Когда оптимизация отключена, компилятор Visual C++ x64 помещает все регистровые параметры в соответствующие слоты. У такого варианта есть приятный побочный эффект, заключающийся в небольшом упрощении отладки, но основная причина простоты отладки заключается в том, что вы отключили оптимизацию и теперь компилятор генерирует простой, прямолинейный код, без претензий на интеллектуальность.

Когда оптимизации включены, компилятор становится более агрессивным в отношении удаления ненужных операций и использования памяти для нескольких целей сразу, когда жизненные циклы переменных не пересекаются. Если он обнаруживает, что параметр argc не требуется сохранять в памяти (возможно, он поместит его в регистр), тогда он может использовать слот параметра argc для чего-нибудь другого, в данном случае он используется для сохранения значения rbx.

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

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

Дополнение: В последующем уточнении (что, к сожалению, бывает очень редко) клиент признался, что проблема была в их программе. Они поместили вызов функции внутрь операции assert и отключили операции assert в неотладочной сборке (передав компилятору параметр /DNDEBUG), что означает, что в такой сборке эта функция никогда не вызывалась.

В продолжение темы: Сложности отладки оптимизированного x64-кода. Эта команда .frame /r действительно приводит к значительной экономии времени.