跳转至内容

x86 反汇编/代码优化

来自维基教科书,开放的书籍,面向开放的世界

代码优化

[编辑 | 编辑源代码]

一个优化编译器可能是现存最复杂、最强大、最有趣的程序之一。本章将讨论优化,但不会包含常见优化的表格。

优化阶段

[编辑 | 编辑源代码]

编译器在两个时间点可以执行优化:首先,在中间表示中,其次,在代码生成期间。

中间表示优化

[编辑 | 编辑源代码]

在中间表示中,编译器可以执行各种优化,通常基于数据流分析技术。例如,考虑以下代码片段

 x = 5;
 if(x != 5)
 {
   //loop body
 }

优化编译器可能会注意到,在 "if (x != 5)" 的位置,x 的值始终是常量 "5"。这允许用 "5" 代替 x,得到 "5 != 5"。然后编译器注意到结果表达式完全在常量上操作,因此可以在现在计算其值,而不是在运行时计算,从而优化条件为 "if (false)"。最后,编译器看到这意味着 if 条件语句的语句体永远不会执行,因此可以完全省略 if 条件语句的整个语句体。

考虑相反的情况

 x = 5;
 if(x == 5)
 { 
    //loop body
 }

在这种情况下,优化编译器会注意到 IF 条件语句始终为真,甚至不会费心编写代码来测试 x。

控制流优化

[编辑 | 编辑源代码]

另一组优化可以在中间表示级别或代码生成级别执行,它们是控制流优化。大多数这些优化处理无用分支的消除。考虑以下代码

 if(A)
 {
    if(B)
    {
       C;
    }
    else
    {
       D;
    }
    end_B:
 }
 else
 {
    E;
 }
 end_A:

在这段代码中,一个简单的编译器会从 C 块生成一个跳转到 end_B,然后从 end_B 生成另一个跳转到 end_A(以绕过 E 语句)。显然,跳转到一个跳转是不高效的,因此优化编译器会从 C 块生成一个直接跳转到 end_A。

不幸的是,这会使代码更加混乱,并且会阻止很好地恢复原始代码。对于复杂的函数,可能需要考虑仅由 if()-goto; 序列组成的代码,而无法识别更高层次的语句,例如 if-else 或循环。

识别高层次语句层次结构的过程称为“代码结构化”。

代码生成优化

[编辑 | 编辑源代码]

一旦编译器筛选了代码中所有逻辑上的低效率,代码生成器就会接管。代码生成器通常会用更快的机器指令替换某些速度较慢的机器指令。

例如,指令

 beginning:
 ...
 loopnz beginning

运行速度比等效指令集

 beginning:
 ...
 dec ecx
 jne beginning

慢得多。那么,为什么编译器会使用 loopxx 指令呢?答案是大多数优化编译器永远不会使用 loopxx 指令,因此,作为逆向工程师,您可能永远不会在实际代码中看到它被使用。

那么指令

  
 mov eax, 0

怎么样?mov 指令相对较快,但处理器中较快的一部分是算术单元。因此,使用以下指令更有意义

 xor eax, eax

因为 xor 在很少的处理器周期内运行(并且同时节省了三个字节),因此比 "mov eax, 0" 快。xor 指令的唯一缺点是它会更改处理器标志,因此它不能用在比较指令和相应的条件跳转之间。

循环展开

[编辑 | 编辑源代码]

当循环需要运行固定的少量迭代时,通常最好展开循环,以减少执行的跳转指令数量,并且在许多情况下防止处理器的分支预测失败。考虑以下 C 循环,它调用函数 MyFunction() 5 次

for(x = 0; x < 5; x++) 
{
  MyFunction();
}

转换为汇编代码,我们看到它大致变为

mov eax, 0
loop_top:
cmp eax, 5
jge loop_end
call _MyFunction
inc eax
jmp loop_top
loop_end:

每次循环迭代都需要执行以下操作

  1. 将 eax(变量 "x")中的值与 5 进行比较,如果大于或等于 5,则跳转到末尾
  2. 递增 eax
  3. 跳转回循环顶部。

请注意,如果我们手动重复对 MyFunction() 的调用,我们将删除所有这些指令

call _MyFunction
call _MyFunction
call _MyFunction
call _MyFunction
call _MyFunction

这个新版本不仅占用的磁盘空间更小,因为它使用的指令更少,而且运行速度更快,因为它执行的指令更少。这个过程称为循环展开

内联函数

[编辑 | 编辑源代码]

C 和 C++ 语言允许定义 inline 类型的函数。内联函数是类似于宏的函数。在编译期间,对内联函数的调用将被该函数的语句体替换,而不是执行 call 指令。除了使用 inline 关键字声明内联函数之外,优化编译器还可以决定将其他函数也内联。

函数内联类似于循环展开,用于提高代码性能。非内联函数需要一个 call 指令,几个指令来创建堆栈帧,然后还需要几个指令来销毁堆栈帧并从函数返回。通过复制函数的语句体而不是进行调用,机器代码的大小会增加,但执行时间会减少

不一定要确定相同的代码部分最初是作为宏、内联函数创建的,还是仅仅是复制粘贴的。但是,在反汇编时,可以将这些代码块分离出来,将其视为独立的内联函数,这有助于使代码保持一致。

华夏公益教科书