windows 中 asm("nop") 的实现

Implementations for asm("nop") in windows

以分号结尾的空代码行是否等同于asm("nop")指令?

volatile int x = 5;

if(x == 5){
  printf("x has not been changed yet\n");
}
else{
  ;//Is this the same as asm("nop") or __asm nop in windows?
  //alternatively could use __asm nop or __nop();
}

我查看了这个答案,它让我不想使用使用内联汇编的 x86 特定实现。 Is `__asm nop` the Windows equivalent of `asm volatile("nop");` from GCC compiler

我可以用这个 void __nop(); msdn似乎推荐的功能,但如果不需要,我不想拖入库。 https://docs.microsoft.com/en-us/cpp/intrinsics/nop?view=vs-2017

有没有一种便宜、便携的方法来添加不会被编译出来的 nop 指令?我以为一个空分号要么是 nop 要么是编译出来的,但由于某种原因我今晚找不到任何关于它的信息。

澄清编辑 我可以使用内联 asm 为 x86 执行此操作,但我希望它是可移植的。我可以使用 windows 库 __nop() 但我不想将该库导入到我的项目中,这是不受欢迎的开销。

我正在寻找一种切割方法来生成 NOP 指令,该指令不会被优化(最好使用标准 C 语法),可以制作成宏并在整个项目中使用,具有最小的开销和工作(或可以很容易地改进工作)在 windows/linux/x86/x64.

谢谢。

Is an empty line of code that ends with a semicolon equivelent to an asm("nop") instruction?

不,当然不是。你可以自己简单地尝试一下。 (在你自己的机器上,或者在 Godbolt 编译器资源管理器上,https://godbolt.org/

如果 FOO(x); 扩展为 ;,您不希望无辜的 CPP 宏引入 NOP,因为在这种情况下 FOO() 的适当定义是空字符串。


__nop() 不是库函数。这是一个 内在的,完全可以满足您的需求。 例如

#ifdef USE_NOP

#ifdef _MSC_VER
#include <intrin.h>
#define NOP() __nop()       // _emit 0x90
#else
// assume __GNUC__ inline asm
#define NOP() asm("nop")    // implicitly volatile
#endif

#else
#define NOP()  // no NOPs
#endif

int idx(int *arr, int b) {
    NOP();
    return arr[b];
}

使用 Clang7.0 -O3 for x86-64 Linux 编译到这个 asm

idx(int*, int):
    nop
    movsxd  rax, esi                     # sign extend b
    mov     eax, dword ptr [rdi + 4*rax]
    ret

用 32 位 x86 MSVC 19.16 -O2 -Gv 编译到这个 asm

int idx(int *,int) PROC                                    ; idx, COMDAT
    npad    1                           ; pad with a 1 byte NOP
    mov     eax, DWORD PTR [ecx+edx*4]  ; __vectorcall arg regs
    ret     0

并用 x64 MSVC 19.16 -O2 -Gv 编译成这个 asm (Godbolt for all of them):

int idx(int *,int) PROC                             ; idx, COMDAT
    movsxd  rax, edx
    npad    1                           ; pad with a 1 byte NOP
    mov     eax, DWORD PTR [rcx+rax*4]  ; x64 __vectorcall arg regs
    ret     0

有趣的是,b 到 64 位的符号扩展是在 NOP 之前完成的。显然 x64 MSVC 要求(默认情况下)函数以至少 2 字节或更长的指令开始(在 1 字节 push 指令的序言之后,也许吧?),因此它们支持使用 jmp rel8.


如果您在 1 指令函数中使用它,您会在 x64 MSVC 的 npad 1 之前得到一个 npad 2(2 字节 NOP):

int bar(int a, int b) {
    __nop();
    return a+b;
}
;; x64 MSVC 19.16
int bar(int,int) PROC                                  ; bar, COMDAT
    npad    2
    npad    1
    lea     eax, DWORD PTR [rcx+rdx]
    ret     0

我不确定 MSVC 会对纯寄存器指令重新排序 NOP 的积极程度如何,但是 __nop() 之后的 a^=b; 实际上会导致 xor ecx, edx NOP 指令之前。

但是wrt。内存访问,在这种情况下,MSVC 决定不重新排序任何内容来填充该 2 字节槽。

int sink;
int foo(int a, int b) {
    __nop();
    sink = 1;
    //a^=b;
    return a+b;
}
;; MSVC 19.16 -O2
int foo(int,int) PROC                                  ; foo, COMDAT
    npad    2
    npad    1
    lea     eax, DWORD PTR [rcx+rdx]
    mov     DWORD PTR int sink, 1             ; sink
    ret     0

它首先执行 LEA,但在 __nop() 之前不移动它;似乎是一个明显错过的优化,但是如果你插入 __nop() 指令,那么优化显然不是优先事项。


如果你编译成 .obj.exe 然后反汇编,你会看到一个普通的 0x90 nop。但不幸的是,Godbolt 不支持 MSVC,只有 Linux 编译器,所以我能做的就是复制 asm 文本输出。

如您所料,在 __nop() ifdefed 的情况下,函数正常编译为相同的代码,但没有 npad 指令。


nop 指令将 运行 与 NOP() 宏在 C 抽象机中执行的次数一样多。 订购 wrt。优化器或 wrt 不保证周围的非 volatile 内存访问。寄存器中的计算。

如果您希望它成为编译时内存重新排序屏障,对于 GNU C,请使用 asm("nop" ::: "memory");`。对于 MSVC,那必须是分开的,我想。

I mean i don't want to add a library just to force the compiler to add a NOP.

...以一种独立于编译器设置(例如优化设置)并且适用于所有 Visual C++ 版本(甚至可能是其他编译器)的方式:

不可能:只要汇编代码具有 C 代码所描述的行为,编译器就可以自由决定其生成代码的方式。

并且由于 NOP 指令不会改变程序的行为,编译器可以自由添加或删除它。

即使您找到了强制编译器生成 NOP 的方法:编译器的一次更新或 Windows 更新修改了某些文件,编译器也可能不会生成 NOP指令不再。

I can use inline asm to do this for x86 but I would like it to be portable.

正如我在上面所写的,任何强制编译器编写 NOP 的方法都只适用于特定 CPU.

的特定编译器版本

使用内联汇编或__nop()您可能会涵盖某个制造商的所有编译器(例如:所有 GNU C 编译器或 Visual C++ 的所有变体等...)。

另一个问题是:您是否明确需要 "official" NOP 指令,或者您可以接受任何不执行任何操作的指令?

如果您可以接受任何(几乎)什么都不做的指令,读取全局或静态 volatile 变量可以替代 NOP:

static volatile char dummy;
    ...
else
{
    (void)dummy;
}

这应该强制编译器添加一个 MOV 指令读取变量 dummy

背景:

如果你写了一个设备驱动程序,你可以 link 变量 dummy 到读取变量 "side-effects" 的某个位置。示例:正在读取 位于 VGA 显存中的变量可能会影响屏幕内容!

使用volatile关键字,您不仅告诉编译器变量的值可能随时改变,而且读取变量可能会产生这样的效果。

这意味着编译器必须假定不读取变量会导致程序无法正常运行。它无法优化掉(实际上不必要的)MOV 读取变量的指令。