【问题标题】:OR instruction in assembly into ECX register汇编中的 OR 指令到 ECX 寄存器中
【发布时间】:2016-12-01 20:01:36
【问题描述】:

在我正在阅读的一本书中,我们得到了以下 sn-p 和问题:

此函数使用 SCAS 和 STOS 的组合来完成其工作。第一的, 解释第 1 行和第 8 行中 [EBP+8] 和 [EBP+C] 的类型是什么, 分别。接下来解释一下这个sn-p做了什么:

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

在使用在线解决方案 (https://johannesbader.ch/2014/05/practical-reverse-engineering-exercises-page-11/) 进行检查后,我几乎想通了一切,但是,这个 sn-p 中的一步对我来说仍然没有意义。

根据网上的解决方案,当我们在第4行运行命令or ecx, 0FFFFFFFFh时,它说

我们 [现在] 将 ECX 解释为有符号整数 -1

为了知道or 命令的结果是什么,难道我们不需要事先知道ECX 的值是什么吗?为什么值是-1?

谢谢

【问题讨论】:

标签: assembly x86


【解决方案1】:

-1 的 32 位 two's complement representation0xFFFFFFFF(全为一)。 1 OR x 总是 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:代码大小

将寄存器设置为anything other than zero 的“正常”方式是使用 5 字节的mov r32, imm32 insn,例如B9 FF FF FF FF mov ecx,-1

如果您更关心代码大小而不是速度,或者您知道对 ecx 的错误依赖在这里不是问题,您可以使用符号扩展的 8 位立即数节省两个字节:@987654325 @。

83 C9 FF    or ecx, 0FFFFFFFFh

结果中的所有位实际上都不依赖于 ecx 的旧值,因为。但是,真正的 CPU 不会对此进行特殊处理,因此在 ecx 准备好之前,无法开始乱序执行。这是 对 ecx 旧值的错误依赖mov 打破了对前一个值的依赖。 (有关此内容的更多信息,请参阅 标签 wiki,尤其是 Agner Fog's guides)。

or ecx, imm8 需要一个 ModRM 字节来将目标编码为 ecx,这与 mov 的形式不同,其中每个目标寄存器都有一个单独的操作码。很遗憾,mov r/m32, imm8 没有操作码,这样可以在许多指令中节省 2 个字节的代码。

如果英特尔愿意放弃backwards compatibility with undocumented instructions,他们本可以添加它。 (8086 没有它,因为在将立即数移动到内存时它只会帮助 16 位代码。他们已经将 8 个操作码专用于 mov r16, imm16,这在 16 位模式下是 3 个字节,不需要操作数大小前缀,就像不存在的 mov r/m16, imm8 一样。)


所以在优化代码大小时这是一个有用的习惯用法,例如对于引导加载程序,或https://codegolf.stackexchange.com/ 上的机器代码答案。 (是的,这是一回事。)

另一个相关的技巧是使用 3 字节的 lea 创建一个常量,如果您已经在另一个寄存器中有另一个常量。 例如对于x86-64 Adler32, I needed two zeroed registers and a 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

【讨论】:

  • 彼得,你能不能写一下add ecx, 2; neg ecx 的工作原理?我的意思是我明白它在做什么,但是怎么做呢?这些指令下有什么“位数学”?如果这个数学存在的话。谢谢!
  • @RuslanGaripov:它正在执行 ecx = -(ecx+2) = -ecx - 2。在此之前,ECX=(-1 - length)(因为 REP 具有该起始值),所以它正在执行 ECX = -(-1 - length) - 2 = 1+length - 2 = length - 1。即字符串长度不包括终止 0,即strlen(edi)。 IIRC,通常使用a 2's complement identity 来节省代码大小,例如not ecx / dec ecx,但使用mov -2, eax / sub eax,ecx 返回strlen 会降低延迟)。
【解决方案2】:

十六进制的F 是二进制的1111,所以0ffffffffh 是一个DWORD,所有位都设置为1。 根据OR 的真值表,如果你OR1,你仍然得到1。 因此,无论之前持有什么ECX,在这个特定的OR 操作之后,它的所有位都将设置为1,即0ffffffffh。 Jose Manuel Abarca Rodriguez 在评论中提供的第一个链接对此进行了解释。

英特尔架构(以及大多数硬件架构)使用所谓的“二进制补码表示”来建立寄存器或内存中的数字和位模式之间的映射。在二进制补码表示中,-1 表示为0ffffffffh。 Jose Manuel Abarca Rodriguez 在评论中提供的第二个链接对此进行了解释。

【讨论】:

    猜你喜欢
    • 2015-11-01
    • 1970-01-01
    • 2021-03-25
    • 1970-01-01
    • 2013-07-26
    • 2013-06-14
    • 2014-11-21
    • 1970-01-01
    • 2016-11-11
    相关资源
    最近更新 更多