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

Posted

技术标签:

【中文标题】为啥 64 位 VC++ 编译器在函数调用后添加 nop 指令?【英文标题】:Why does 64-bit VC++ compiler add nop instruction after function calls?为什么 64 位 VC++ 编译器在函数调用后添加 nop 指令? 【发布时间】:2017-06-30 20:37:47 【问题描述】:

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

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

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());

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

以下是补充意见:

    我的第一个猜测是incremental linking 可能与它有关。但是,项目的Visual Studio 中的Release 构建设置关闭了incremental linking

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

    我尝试使用更新的链接器构建它,尽管VS 2013 生成的x64 代码看起来有些不同,但它仍然在一些nops 之后添加了nops:

    另外dynamicstatic 链接到MFC 对nops 的存在没有影响。这是使用VS 2013 动态链接到 MFC dll 构建的:

    还要注意那些nops 也可以出现在nearfar calls 之后,它们与对齐无关。这是我从IDA 获得的部分代码,如果我再往前走一点:

如您所见,nop 插入在 far call 之后,恰好“对齐”B 地址上的下一个 lea 指令!如果这些只是为了对齐而添加的,那是没有意义的。

    我最初倾向于相信,因为nearrelativecalls(即以E8开头的那些)是somewhat faster而不是farcalls(或以calls开头的那些) FF,15 在这种情况下)

链接器可能会先尝试使用nearcalls,因为这些比farcalls 短一个字节,如果成功,它可能会用nops 填充剩余空间在末尾。但是上面的例子(5)有点推翻了这个假设。

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

【问题讨论】:

填充对齐? 看起来很像 RIP 相关的间接调用,链接器对直接调用放宽了——间接调用在 x86 上要长一个字节,因此链接器插入了 nop 以使它们具有相同的长度。跨度> 我怀疑@Fanael 是正确的。摆脱那个 NOP 意味着改变所有的代码。但是转移所有代码会改变很多地址。似乎是通过 NOP 解决的先有鸡还是先有蛋的问题。 @Mysticial Weird 这对于 ELF 如何进行(动态)链接来说是不必要的。 @fuz 使用 ELF 函数调用共享库中的函数总是通过存根函数进行。在 Windows 上,对 DLL 中的函数的函数调用通常是使用间接调用指令直接进行的。上面反汇编中的call cs:LoadIconW 指令就是一个例子。反汇编程序调用LoadIconW 的位置包含一个指向实际LoadIconW 函数的指针。 【参考方案1】:

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

在以下示例 (live demo with VC2017) 中,在调用 basic_string::assign 后插入了一个 NOP 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 标志(同步异常处理)进行编译。如果没有该标志,NOPs 就会消失,而使用/EHa(同步异步异常处理),throw() 将不再有任何区别,因为 SEH 始终处于打开状态。


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

【讨论】:

【参考方案2】:

这是一个特殊的填充物,让异常处理程序/展开函数能够正确检测它是否是函数的序言/尾声/主体。

【讨论】:

【参考方案3】:

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

【讨论】:

虽然你的说法是正确的,但与我原来的问题无关。 您问为什么将 nop 添加到程序集中,这就是原因。这怎么不能回答你的问题? 我问为什么在call 指令之后添加nops 而不是函数本身。您提到的适用于函数的主体(或代码),而不是 call 指令。 NOP 可以填充代码以对齐RIP(但在这种情况下不是这样;请查看转储中的代码地址)。调用约定要求对齐RSP。 NOP 不会修改RSP

以上是关于为啥 64 位 VC++ 编译器在函数调用后添加 nop 指令?的主要内容,如果未能解决你的问题,请参考以下文章

VC++ LNK2001:仅在 64 位编译时无法解析外部符号

关于libsvm工具箱在64位matlab下的安装说明

在 MS VC 2013 Express 中将 C++ dll 从 32 位转换为 64 位

Visual Studio 2010 64 位 COM 互操作问题

为啥在进行 64 位操作时编译器会发生变化?

openssl windows编译 32位&64位