【问题标题】:Is there a portable way of generating a nop opcode in C/C++?是否有在 C/C++ 中生成 nop 操作码的可移植方式?
【发布时间】:2018-06-23 02:00:42
【问题描述】:

在汇编中,有nop 操作码。假设我想单独使用 C/C++ 语法在生成的程序集中拥有该操作码。有什么便携的方法可以做到这一点吗?

阅读this后,我尝试在最新的MSVC 15.5.2中编译(void)0;,但没有成功生成nop指令,无论是否启用优化。

由于内联汇编 is not 是 ISO C 标准的一部分,并且它在 C++ 中的支持是 conditional,我想避免这种情况。

在 Microsoft Visual Studio 中,我知道我可以使用在标题 <intrin.h> 中定义的 __nop() 函数(如文档中的 here 所述)在生成的程序集中成功创建单个 nop

【问题讨论】:

  • nop 操作码是特定于平台的,甚至不能保证在每个带有 C++ 编译器的处理器上都存在,那么如何有一种可移植的方式来生成它呢?
  • 唯一的“便携”方式是拥有大量#ifs,并采用各种方式。
  • 你必须查一下。
  • 在高层次上没有“nop”,通常你不需要关心它。如有必要,编译器将自动引入 NOP 以进行对齐或管道执行
  • 标准不支持,但是一些操作系统内核声明了一个带有可变索引变量的延迟循环,作为告诉编译器不要优化循环的一种方式。

标签: c++ c


【解决方案1】:

不,没有便携的方式。

虽然编译器通常为或多或少的特殊情况提供相当多的内在函数,并且通常还提供一些内联汇编程序代码的方法,但这些都不是标准化的。

并且任何明显对应于“无操作”的不受保护的源级构造都不会有机会反对最基本的优化。

【讨论】:

  • 关于编译器优化的要点,因为无论 OP 计划做什么,都可能取决于编译后的可执行文件中剩余的指令。
【解决方案2】:

不,并且它没有意义在高级别的 nop 操作映射到程序集 nop。为什么?因为as if rule 声明编译器“需要模拟(仅)可观察行为”。根据定义,nop 操作没有任何可观察的行为(在标准中定义的“抽象机器”上)。

从更实际的方面来看,从 C++ 源文件生成的汇编指令与 C++ 指令没有一对一的关系。大多数情况下,一条 C++ 指令由多条汇编指令组成,多条 C++ 指令被一条汇编指令替换,甚至一些 C++ 指令由于算法转换或死代码消除以及在该指令之上没有任何类型的汇编对应部分重新排列,算法在各处进行了转换。


例如

我能想到的最激进的例子之一就是编译器可以做的算法转换是将递归函数转换为简单的迭代循环:

auto sum(int* v, int len)
{
    if (len == 0)
        return 0;

    return v[0] + sum(v + 1, len - 1);
}

一个简单的递归函数,用于计算向量元素的总和。

这是clang-O1 生成的:

sum(int*, int): # @sum(int*, int)
  xor eax, eax
  test esi, esi
  je .LBB0_2
.LBB0_1: # =>This Inner Loop Header: Depth=1
  add eax, dword ptr [rdi]
  add rdi, 4
  add esi, -1
  jne .LBB0_1
.LBB0_2:
  ret

从递归到循环的巧妙算法转换。 nop C++ 指令适合在程序集中的什么位置?

-O3生成的程序集中怎么样?:

sum(int*, int): # @sum(int*, int)
  test esi, esi
  je .LBB0_1
  lea edx, [rsi - 1]
  add rdx, 1
  xor eax, eax
  cmp rdx, 8
  jae .LBB0_4
  mov rcx, rdi
  jmp .LBB0_7
.LBB0_1:
  xor eax, eax
  ret
.LBB0_4:
  mov r8d, esi
  and r8d, 7
  sub rdx, r8
  sub esi, edx
  lea rcx, [rdi + 4*rdx]
  add rdi, 16
  pxor xmm0, xmm0
  pxor xmm1, xmm1
.LBB0_5: # =>This Inner Loop Header: Depth=1
  movdqu xmm2, xmmword ptr [rdi - 16]
  paddd xmm0, xmm2
  movdqu xmm2, xmmword ptr [rdi]
  paddd xmm1, xmm2
  add rdi, 32
  add rdx, -8
  jne .LBB0_5
  paddd xmm1, xmm0
  pshufd xmm0, xmm1, 78 # xmm0 = xmm1[2,3,0,1]
  paddd xmm0, xmm1
  pshufd xmm1, xmm0, 229 # xmm1 = xmm0[1,1,2,3]
  paddd xmm1, xmm0
  movd eax, xmm1
  test r8d, r8d
  je .LBB0_8
.LBB0_7: # =>This Inner Loop Header: Depth=1
  add eax, dword ptr [rcx]
  add rcx, 4
  add esi, -1
  jne .LBB0_7
.LBB0_8:
  ret

编译器在这里进行一些循环展开。循环现在被转换为一个头部,它使所有元素达到倍数,一个主体,其中使用矢量化同时添加对齐的元素组,以及一个尾部,它获取无法填充对齐组的其余元素.而且……有趣的是……原来的 C++ 源代码甚至没有循环。因此,如果您在源代码中有nop,您会将它放在程序集中的什么位置?它没有可观察到的效果,因此编译器没有理由可以利用它来确定将它放在这个经过大量转换的代码中的什么位置。

即使您想到了一些巧妙的规则并设法指定C++ nop 将映射到程序集的位置,那会有多大用处?程序集nop 的用途与程序算法无关,但与架构实现细节有关,例如 RAW 依赖项等。使用 C++,您无需对架构细节进行建模(至少不是在标准 C++ 中),而是与架构无关的算法,因此您无法拥有完全与架构细节相关的指令。


问题是 C++ 确实有一个 nop 指令。它是空语句;;。它在 C++ 语法和语义级别上很有用:

// find the end of the string:

for (const ch* end = str; *ch != '\0'; ++end)
    ; // <-- empty statement. A nop instruction.

但是由于上述规则和共鸣,生成程序集nop 指令没有意义。

【讨论】:

    【解决方案3】:

    不,大多数现代编译器会自动检查您的代码并重新排列代码,以尝试针对您选择的架构进行优化。在重新排列您的代码时,编译器会消除任何没有后续依赖关系的代码(在 Visual Studio 中,您通常可以观察到 IDE 下划线变量,这些变量在以后不会使用;那些在编译时被消除)。如果你想维护一个 nop,你将不得不坚持使用特定的编译器。

    【讨论】:

      猜你喜欢
      • 2011-12-20
      • 1970-01-01
      • 2023-04-02
      • 1970-01-01
      • 2013-01-28
      • 1970-01-01
      • 2011-04-19
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多