为什么64位VC++编译器在函数调用后添加nop指令?

c00*_*0fd 20 c++ 64-bit assembly disassembly visual-studio

我使用Visual Studio C++ 2008 SP1,x64 C++编译器编译了以下内容:

在此输入图像描述

我很好奇,为什么编译器会nop在那些calls 之后添加这些指令?

PS1.我会理解第二和第三个nops将是4字节边距上的代码对齐,但是第一个nop会打破这个假设.

PS2.编译的C++代码中没有循环或特殊的优化内容:

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());
}
Run Code Online (Sandbox Code Playgroud)

PS3.附加信息: 首先,谢谢大家的意见.

以下是其他观察结果:

  1. 我的第一个猜测是增量链接可能与它有关.但是,项目中的Release构建设置Visual Studioincremental linking关闭.

  2. 这似乎x64只影响构建.构建为x86(或Win32)的相同代码没有那些nops,即使使用的指令非常相似:

在此输入图像描述

  1. 我尝试使用更新的链接器构建它,即使看起来x64产生的代码VS 2013有些不同,它仍会nop在某些calls 之后添加s:

在此输入图像描述

  1. 另外dynamicVS static链接到MFC做这些的存在没有区别nop秒.这个是通过动态链接到MFC dll构建的VS 2013:

在此输入图像描述

  1. 还要注意那些nops可以出现在s nearfar calls 之后,它们与对齐无关.IDA如果我再深入一点,这里是我获得的代码的一部分:

在此输入图像描述

如您所见,在恰好"对齐" 地址上的下一条指令nop之后插入!如果仅为了对齐而添加这些内容就毫无意义.far callleaB

  1. 我本来倾向于认为,既然near relative callS(即那些以E8)是稍快far callS(或与启动的人FF,15在这种情况下)

在此输入图像描述

链接器可能首先尝试使用near calls,并且因为它们比far calls 短一个字节,如果成功,它可以nop在末尾用s 填充剩余空间.但是上面的例子(5)有点打败了这个假设.

所以我仍然没有明确的答案.

rus*_*tyx 5

这纯粹是一个猜测,但它可能是某种 SEH 优化。我说优化是因为 SEH 似乎在没有 NOP 的情况下也能正常工作。NOP 可能有助于加速平仓。

在下面的示例(VC2017 的现场演示NOP)中,在调用basic_string::assignintest1但不是 in 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();
}
Run Code Online (Sandbox Code Playgroud)

集会:

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
Run Code Online (Sandbox Code Playgroud)

请注意,MSVS 默认情况下使用/EHsc标志(同步异常处理)进行编译。如果没有该标志,NOPs 就会消失,而有了/EHa(同步异步异常处理),throw()则不再产生影响,因为 SEH 始终处于打开状态。


1由于某种原因throw()似乎只是减少了代码大小,使用noexcept会使生成的代码更大并召唤更多的NOPs。微软VC...


小智 -3

这是由于 x64 中的调用约定导致堆栈在任何调用指令之前必须对齐 16 字节。(据我所知)这不是硬件要求,而是软件要求。这提供了一种方法来确保在进入函数时(即在调用指令之后),堆栈指针的值始终为 8 模 16。从而允许简单的数据对齐以及从堆栈中的对齐位置存储/读取。

  • NOP 可以填充代码以对齐“RIP”(但在本例中不是这样;查看转储中的代码地址)。调用约定要求“RSP”对齐。NOP 不会修改`RSP`。 (3认同)