在x86系统中,NOPL是什么作用?

49
在x86机器中,NOPL的作用是什么?它似乎没有做任何事情,但为什么它总是出现在汇编代码中?

5
你问“为什么总是在汇编代码中出现?” - 通常在汇编代码中不会找到很多NOP指令。你正在查看的某些特定代码是否有许多NOP指令? - ugoren
9
我希望我能够“感受”代码的运作方式! - Jim Balter
https://dev59.com/q2w15IYBdhLWcg3wLIzI#6777644 - FrankH.
nopw:https://dev59.com/questions/42445IYBdhLWcg3wkLFG - Ciro Santilli OurBigBook.com
3个回答

51
< p >< code > NOP 是一个一字节的“不执行任何操作”指令,确切地说是“无操作”。NOPW、NOPL 等等都是相当于“不执行任何操作”,但会占用字或长字节。< /p > < p >例如:< /p >
NOP // 1byte opcode
NOP // 1byte opcode

等同于执行

NOPW // 2byte opcode.

NOP指令非常方便,可以利用它填充一些字节的指令空间,使代码序列在特定的内存边界上开始,但实际上不执行任何操作。

NOP对CPU唯一的影响是将IP/EIP增加1。NOPx等效指令会将其增加2、4等等。


4
我之前从未听说过 x86 指令集中的 NOPW 和 NOPL 操作,也没有在英特尔指令集参考手册中找到它们的相关信息。也许你混淆了不同的架构。 - Jack
8
@Jack 给我感觉像是 AT&T 语法。 - harold
3
@Jack:在X86架构中,NOP只是“XCHG AX,AX”的别名,因为它是一条单周期指令,除了增加IP值外没有其他影响。有许多两字节的指令可以起到相同的作用,其中包括将一个寄存器移动到自身的指令(在某些情况下代表MOV AX,AX,而在其他情况下代表MOV EAX,EAX,但无论哪种情况,其唯一的效果都是将IP值增加2)。 - supercat
6
请注意,您不能仅仅选择一个看似幂等的操作。 XCHG AX,AXXCHG BX,BX 不是相同的。第一个是官方的“NOP”操作,并且不会导致数据依赖关系。 - MSalters
您不仅需要这些指令来填充内容。当您需要实现一个等待确切1秒钟的计时器值时,您需要考虑时钟速度和形成“外壳”的操作长度(通常是某种循环),然后使用操作填充其余部分,以确保总时间恰好达到1秒钟(当然没有中断)。在这里,nop操作非常有用。 - clockw0rk
显示剩余4条评论

47
根据John Fremlin的博客:AMD64上的NOP操作数nopwnopl等是gas语法,而不是AT&T语法。
下面是由gas为长度从3到15个字节的不同nop生成的指令编码,此处来源于gas源码。请注意,其中有些与英特尔推荐的nop形式相同(见下文),但并非全部如此。特别地,在较长的nop中,gas使用多个(最多5个)连续的0x66操作数前缀以不同的nop形式,而英特尔推荐的nop形式从未在任何单个推荐的nop指令中使用超过一个0x66操作数前缀。

nop编码来自于经过重新格式化以增加可读性的gas 2.30 源代码

/* nopl (%[re]ax) */
static const unsigned char alt_3[] = {0x0f,0x1f,0x00};
/* nopl 0(%[re]ax) */
static const unsigned char alt_4[] = {0x0f,0x1f,0x40,0x00};
/* nopl 0(%[re]ax,%[re]ax,1) */
static const unsigned char alt_5[] = {0x0f,0x1f,0x44,0x00,0x00};
/* nopw 0(%[re]ax,%[re]ax,1) */
static const unsigned char alt_6[] = {0x66,0x0f,0x1f,0x44,0x00,0x00};
/* nopl 0L(%[re]ax) */
static const unsigned char alt_7[] = {0x0f,0x1f,0x80,0x00,0x00,0x00,0x00};
/* nopl 0L(%[re]ax,%[re]ax,1) */
static const unsigned char alt_8[] = {0x0f,0x1f,0x84,0x00,0x00,0x00,0x00,0x00};
/* nopw 0L(%[re]ax,%[re]ax,1) */
static const unsigned char alt_9[] =
  {0x66,0x0f,0x1f,0x84,0x00,0x00,0x00,0x00,0x00};
/* nopw %cs:0L(%[re]ax,%[re]ax,1) */
static const unsigned char alt_10[] =
  {0x66,0x2e,0x0f,0x1f,0x84,0x00,0x00,0x00,0x00,0x00};
static const unsigned char *const alt_patt[] = {
  f32_1, f32_2, alt_3, alt_4, alt_5, alt_6, alt_7, alt_8,
  alt_9, alt_10
};

Intel使用不同的语法,对于长度从1到9个字节的所有指令,都有可用的nop。有几种不同的nop,因为所有长度大于两个字节的nop都接受1个操作数。一个字节的nop0x90)与xchg (e)ax,(e)ax同义。 Intel® 64和IA-32架构软件开发人员手册,第2卷(2A、2B和2C):指令集参考,A-Z,第4章:指令集参考,M-Z列出了不同指令长度的推荐nop形式。
Table 4-12. Recommended Multi-Byte Sequence of NOP Instruction

Length   Assembly                                   Byte Sequence
2 bytes  66 NOP                                     66 90H
3 bytes  NOP DWORD ptr [EAX]                        0F 1F 00H
4 bytes  NOP DWORD ptr [EAX + 00H]                  0F 1F 40 00H
5 bytes  NOP DWORD ptr [EAX + EAX*1 + 00H]          0F 1F 44 00 00H
6 bytes  66 NOP DWORD ptr [EAX + EAX*1 + 00H]       66 0F 1F 44 00 00H
7 bytes  NOP DWORD ptr [EAX + 00000000H]            0F 1F 80 00 00 00 00H
8 bytes  NOP DWORD ptr [EAX + EAX*1 + 00000000H]    0F 1F 84 00 00 00 00 00H
9 bytes  66 NOP DWORD ptr [EAX + EAX*1 + 00000000H] 66 0F 1F 84 00 00 00 00 00H

除了英特尔推荐的这些nop外,还有许多其他的nop。除了像Marc B在他的回答中提到的将指令对齐到特定内存边界之外,nop在自修改代码、调试和逆向工程中也非常有用。


12
请注意,在 amd64 上,“nop” 不再等同于 “xchg eax,eax”。 “nop” 不会将“eax”的高32位清零,但是“xchg eax,eax”会。 - fuz
2
实际上,如果您为x86-64组装xchg eax,eax,则必须使用2字节的opcode+modrm编码([87 C0](https://www.felixcloutier.com/x86/xchg)),因为`0x90`对RAX没有副作用。但是,`xchg eax,ecx仍然可以组装为0x91 - 只有0x90特别被nop占用 https://www.felixcloutier.com/x86/nop。但是,xchg rax,raxxchg ax,ax`仍然可以使用REX.W或66 90,因为它们没有架构效果。https://godbolt.org/z/xn5nKnWT6 - Peter Cordes

10

实际上,在代码需要修补时,汇编代码中将使用NOP。

因为新指令的大小可能与旧指令不同,所以需要填充。

填充指令应该与NOP具有相同的作用,尽管它可能占用多个字节。

我们插入更复杂的指令(如66 90),而不是几个NOP的原因是:一条指令通常比几条NOP执行得更快。


我不知道有多少人会像这样修复他们的代码,即使在2013年...那真的很老派,就像20世纪80年代一样。今天,它只是为了修补使事物保持对齐。 - Alexis Wilke

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接