Подтвердить что ты не робот

Почему 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, хотя используемые команды очень похожи:

введите описание изображения здесь

  1. Я попытался создать его с помощью нового компоновщика, и хотя код x64, созданный VS 2013, выглядит несколько иначе, он по-прежнему добавляет те nop после некоторых call s:

введите описание изображения здесь

  1. Также dynamic vs static, связанная с MFC, не имело никакого отношения к присутствию этих nop s. Это построено с динамическим связыванием с dll MFC с помощью VS 2013:

введите описание изображения здесь

  1. Также обратите внимание, что те nop могут появляться после near и far call, и они не имеют никакого отношения к выравниванию. Вот часть кода, который я получил от IDA, если я немного пошагую дальше:

введите описание изображения здесь

Как вы видите, nop вставлен после far call, который происходит, чтобы "выровнять" следующую инструкцию lea по адресу B! Это не имеет смысла, если они были добавлены только для выравнивания.

  1. Я изначально был склонен полагать, что поскольку near relative call (т.е. те, которые начинаются с E8), несколько быстрее чем far call (или те, которые начинаются с FF, 15 в этом случае)

введите описание изображения здесь

компоновщик может сначала попытаться перейти с near call, и поскольку они являются одним байтом, короче far call s, если он преуспеет, он может заполнить оставшееся пространство с помощью nop на конец. Но тогда пример (5) выше своего рода побеждает эту гипотезу.

Таким образом, у меня до сих пор нет четкого ответа на этот вопрос.

4b9b3361

Ответ 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. Таким образом, разрешается простое выравнивание данных и хранение/чтение из выровненного местоположения в стеке.