为啥 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,x64
C++
编译器编译了以下内容:
我很好奇,为什么编译器会在那些 call
s 之后添加那些 nop
指令?
PS1。我知道第二个和第三个 nop
s 将在 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
)构建的相同代码没有nop
s,即使使用的指令非常相似:
-
我尝试使用更新的链接器构建它,尽管
VS 2013
生成的x64
代码看起来有些不同,但它仍然在一些nop
s 之后添加了nop
s:
-
另外
dynamic
与static
链接到MFC 对nop
s 的存在没有影响。这是使用VS 2013
动态链接到 MFC dll 构建的:
-
还要注意那些
nop
s 也可以出现在near
和far
call
s 之后,它们与对齐无关。这是我从IDA
获得的部分代码,如果我再往前走一点:
如您所见,nop
插入在 far
call
之后,恰好“对齐”B
地址上的下一个 lea
指令!如果这些只是为了对齐而添加的,那是没有意义的。
-
我最初倾向于相信,因为
near
relative
call
s(即以E8
开头的那些)是somewhat faster而不是far
call
s(或以call
s开头的那些) FF
,15
在这种情况下)
链接器可能会先尝试使用near
call
s,因为这些比far
call
s 短一个字节,如果成功,它可能会用nop
s 填充剩余空间在末尾。但是上面的例子(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
标志(同步异常处理)进行编译。如果没有该标志,NOP
s 就会消失,而使用/EHa
(同步和异步异常处理),throw()
将不再有任何区别,因为 SEH 始终处于打开状态。
1 出于某种原因,只有throw()
似乎减少了代码大小,使用noexcept
会使生成的代码更大,并召唤更多NOP
s。 MSVC...
【讨论】:
【参考方案2】:这是一个特殊的填充物,让异常处理程序/展开函数能够正确检测它是否是函数的序言/尾声/主体。
【讨论】:
【参考方案3】:这是由于 x64 中的调用约定要求堆栈在任何调用指令之前对齐 16 个字节。这不是(据我所知)硬件要求,而是软件要求。这提供了一种方法来确保在进入函数时(即在调用指令之后),堆栈指针的值始终为 8 模 16。因此允许简单的数据对齐和从堆栈中对齐的位置存储/读取。
【讨论】:
虽然你的说法是正确的,但与我原来的问题无关。 您问为什么将 nop 添加到程序集中,这就是原因。这怎么不能回答你的问题? 我问为什么在call
指令之后添加nop
s 而不是函数本身。您提到的适用于函数的主体(或代码),而不是 call
指令。
NOP 可以填充代码以对齐RIP
(但在这种情况下不是这样;请查看转储中的代码地址)。调用约定要求对齐RSP
。 NOP 不会修改RSP
。以上是关于为啥 64 位 VC++ 编译器在函数调用后添加 nop 指令?的主要内容,如果未能解决你的问题,请参考以下文章
VC++ LNK2001:仅在 64 位编译时无法解析外部符号
在 MS VC 2013 Express 中将 C++ dll 从 32 位转换为 64 位