Почему 64-битный компилятор VС++ добавляет команду nop после вызовов функций?
Я скомпилировал следующее, используя компилятор Visual Studio С++ 2008 SP1, x64
C++
:
![введите описание изображения здесь]()
Мне любопытно, почему компилятор добавил эти nop
инструкции после этих call
s?
PS1. Я бы понял, что 2-й и 3-й nop
будут выровнять код по 4-байтовому полю, но 1-й nop
нарушит это предположение.
PS2. Код С++, который был скомпилирован, не содержал в нем циклов или специальных элементов оптимизации:
CTestDlg::CTestDlg(CWnd* pParent /*=NULL*/)
: CDialog(CTestDlg::IDD, pParent)
{
m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
//This makes no sense. I used it to set a debugger breakpoint
::GdiFlush();
srand(::GetTickCount());
}
PS3. Дополнительная информация: Прежде всего, спасибо всем за ваш вклад.
Здесь дополнительные наблюдения:
-
Мое первое предположение заключалось в том, что инкрементная привязка могла иметь какое-то отношение к ней. Но настройки сборки Release
в Visual Studio
для проекта имеют incremental linking
off.
-
Это, по-видимому, влияет только на сборки x64
. Тот же код, построенный как x86
(или Win32
), не имеет этих nop
s, хотя используемые команды очень похожи:
![введите описание изображения здесь]()
- Я попытался создать его с помощью нового компоновщика, и хотя код
x64
, созданный VS 2013
, выглядит несколько иначе, он по-прежнему добавляет те nop
после некоторых call
s:
![введите описание изображения здесь]()
- Также
dynamic
vs static
, связанная с MFC, не имело никакого отношения к присутствию этих nop
s. Это построено с динамическим связыванием с dll MFC с помощью VS 2013
:
![введите описание изображения здесь]()
- Также обратите внимание, что те
nop
могут появляться после near
и far
call
, и они не имеют никакого отношения к выравниванию. Вот часть кода, который я получил от IDA
, если я немного пошагую дальше:
![введите описание изображения здесь]()
Как вы видите, nop
вставлен после far
call
, который происходит, чтобы "выровнять" следующую инструкцию lea
по адресу B
! Это не имеет смысла, если они были добавлены только для выравнивания.
- Я изначально был склонен полагать, что поскольку
near
relative
call
(т.е. те, которые начинаются с E8
), несколько быстрее чем far
call
(или те, которые начинаются с FF
, 15
в этом случае)
![введите описание изображения здесь]()
компоновщик может сначала попытаться перейти с near
call
, и поскольку они являются одним байтом, короче far
call
s, если он преуспеет, он может заполнить оставшееся пространство с помощью nop
на конец. Но тогда пример (5) выше своего рода побеждает эту гипотезу.
Таким образом, у меня до сих пор нет четкого ответа на этот вопрос.
Ответы
Ответ 1
Это чисто догадка, но это может быть какая-то оптимизация SEH. Я говорю оптимизацию, потому что SEH, похоже, отлично работает без NOP. NOP может ускорить разматывание.
В следующем примере (живая демонстрация с VC2017) добавлен NOP
после вызова basic_string::assign
в test1
но не в test2
(идентичен, но объявлен как неброска 1).
#include <stdio.h>
#include <string>
int test1() {
std::string s = "a"; // NOP insterted here
s += getchar();
return (int)s.length();
}
int test2() throw() {
std::string s = "a";
s += getchar();
return (int)s.length();
}
int main()
{
return test1() + test2();
}
Сборка:
test1:
. . .
call std::basic_string<char,std::char_traits<char>,std::allocator<char> >::assign
npad 1 ; nop
call getchar
. . .
test2:
. . .
call std::basic_string<char,std::char_traits<char>,std::allocator<char> >::assign
call getchar
Обратите внимание, что MSVS компилируется по умолчанию с флагом /EHsc
(синхронная обработка исключений). Без этого флага NOP
исчезают, а с /EHa
(синхронная и асинхронная обработка исключений) throw()
больше не имеет значения, потому что SEH всегда включен.
1 По какой-то причине только throw()
уменьшает размер кода, используя noexcept
, делает сгенерированный код еще большим и вызывает еще больше NOP
s. MSVC...
Ответ 2
Это связано с соглашением о вызове в x64, которое требует, чтобы стек был выровнен по 16 байт до любой команды вызова. Это не (для моего knwoledge) аппаратное требование, а программное обеспечение. Это дает возможность убедиться, что при вводе функции (то есть после инструкции вызова) значение указателя стека всегда равно 8 по модулю 16. Таким образом, разрешается простое выравнивание данных и хранение/чтение из выровненного местоположения в стеке.