在我正在阅读的一本书中,我们给出了以下片段和问题:
此函数使用 SCAS 和 STOS 的组合来完成其工作。首先,分别解释第1行和第8行中的[EBP+8]和[EBP+C]的类型是什么。接下来,解释一下这个片段的作用:
01: 8B 7D 08 mov edi, [ebp+8]
02: 8B D7 mov edx, edi
03: 33 C0 xor eax, eax
04: 83 C9 FF or ecx, 0FFFFFFFFh
05: F2 AE repne scasb
06: 83 C1 02 add ecx, 2
07: F7 D9 neg ecx
08: 8A 45 0C mov al, [ebp+0Ch]
09: 8B AA mov edi, edx
10: F3 AA rep stosb
11: 8B C2 mov eax, edx
Run Code Online (Sandbox Code Playgroud)
在使用在线解决方案(https://johannesbader.ch/2014/05/practical-reverse-engineering-exercises-page-11/)检查后,我几乎弄清楚了一切,但是,此代码段中的一个步骤仍然没有感觉我。
根据在线解决方案,当我们or ecx, 0FFFFFFFFh在第 4 行运行命令时,它说
我们 [现在] 将 ECX 解释为有符号整数 -1
为了知道命令的结果是什么or,我们是否不需要事先知道值ECX是什么?为什么值是-1?
谢谢
32位二进制补码表示的-1是0xFFFFFFFF(全1)。 1 OR xis always 1,因此无条件设置ecx为 -1。这个技巧只适用于 -1,因为 OR 只能设置位,不能将它们清零。
您引用的解决方案的一部分,关于将“解释ecx为有符号整数 -1”,仅在以下 gdb 命令的上下文中才有意义: (gdb) p/d $ecx -> $7 = -1。
rep前缀将 ecx 视为无符号计数器。将 ecx 设置为 -1 / UINT_MAX 意味着repne scasb只有在内存中找到零时才会停止,而不是因为ecx一直倒计时。(理论上,如果没有零,它会倒计时并以这种方式结束,但实际上它会首先出现段错误。 -1不是 的特例rep)。
or:代码大小“正常”的方式寄存器设置为除零以外的任何是具有5个字节mov r32, imm32的insn,例如 B9 FF FF FF FF mov ecx,-1。
如果您更关心代码大小而不是速度,或者您知道ecx这里的错误依赖不是问题,您可以通过使用符号扩展的 8 位立即数来节省两个字节:or r/m32, imm8。
83 C9 FF or ecx, 0FFFFFFFFh
Run Code Online (Sandbox Code Playgroud)
结果中的任何位实际上都不依赖于 ecx 的旧值,因为。但是,真正的 CPU 不会对此进行特殊处理,因此在ecx准备就绪之前无法开始乱序执行。这是对 ecx 旧值的错误依赖。 mov打破对先前值的依赖。(有关这方面的更多信息,请参阅x86标签 wiki,尤其是Agner Fog 的指南)。
or ecx, imm8需要一个 ModRM 字节来将目标编码为 ecx,这与mov每个目标寄存器都有一个单独的操作码的形式不同。不幸的是没有操作码mov r/m32, imm8,这会在许多指令中节省 2 个字节的代码。
如果英特尔愿意放弃与未记录指令的向后兼容性,他们本可以添加它。(8086 没有它,因为在将立即数移动到内存时它只会帮助 16 位代码。他们已经将 8 个操作码用于mov r16, imm16,这是 16 位模式下的 3 个字节,它不需要操作数大小前缀,就像不存在的mov r/m16, imm8一样。)
因此,在优化代码大小时,这是一个有用的习惯用法,例如用于引导加载程序或https://codegolf.stackexchange.com/上的机器代码答案。(是的,这是一回事。)
另一个相关的技巧是使用 3 字节lea来创建一个常量,如果另一个寄存器中已经有另一个常量。 例如,对于x86-64 Adler32,我需要两个归零的寄存器和一个1,所以我使用了
401120: 31 c0 xor eax,eax
401122: 99 cdq # zero rdx by sign-extending eax (0) into edx
401123: 8d 7a 01 lea edi,[rdx+0x1] # edi=0+1, using a reg + disp8 addressing mode
Run Code Online (Sandbox Code Playgroud)