4.死代码消除、复制传播、常量传播

把他们放在一起,是因为这两个技术的特点很像:它们在编译器与反编译器中实现的方法一样。

编译器中的死代码消除

死代码是指程序中不会被执行到的代码,但仍然存在于程序中,浪费了空间和资源。为了提高程序的性能和可维护性,需要进行死代码消除。

在编译原理中,死代码消除是后端优化的一种方法,即在代码生成阶段对程序进行优化。通过分析程序的控制流图和数据流图,可以确定哪些代码是死代码,并将其从生成的代码中删除。

死代码消除通常是与其他优化技术结合使用的,例如常量传播、复写传播和循环优化等。这些技术可以进一步减少生成的代码中的死代码数量,提高程序的性能和效率。

对于反编译器,死代码消除的作用有限;想想也是,编译器为了节省资源,在高优化的级别下,很难说有多少“死代码”留给反编译器。并且“递归下降”这种反汇编的模式,不会对无法到达的块执行反编译。

我列举了一些在真实反编译器中能遇到的典型场景。

死代码消除-消除flag的副作用

最经典的用法就是消除指令对于flags指令”副作用”的削减。

例如,xor指令经常被用于寄存器清零;像eax=0这个代码往往在编译后就变为XOR eax,eax

这是在汇编汇中十分常见的一种做法(相比于 mov eax,0 xor eax,eax 指令更加的短)

但是,参考指令集:https://www.felixcloutier.com/x86/xor 中对亦或指令的定义:

“The OF and CF flags are cleared; the SF, ZF, and PF flags are set according to the result. The state of the AF flag is undefined.”

XOR指令还会影响CF、SF、ZF等标志位,因此在中间语言翻译的时候,需要把这部分内容也一起翻译了,因为那时候不知道这些标志位会不会被后面用到,我们拿IDA-micorocode作为例子

//汇编
xor     ebx, ebx

//IDA翻译后的中间语言-5条micorocode
0 mov    #0.4, ebx.4  
1 mov    #0.1, cf.1  
2 mov    #0.1, of.1  
3 setz   ebx.4, #0.4, zf.1  
4 setp   ebx.4, #0.4, pf.1 
5 sets   ebx.4, sf.1 

但是,太多的时候,我们只是为了把清零ebx,比如在for(int i = 0; i <10; i++) 这样的代码中。

对于flags设置的中间语言语句都是无用的,反编译器提供专门的”死代码消除”功能处理这一类问题。

死变量消除的方法太多了,下面这几个方案都能用:

  • 通过数据流分析构建各个对象的define-use关系,就可以消除死变量了。

  • 如果是SSA那就更好,只要把use=0的项直接去了就好

  • 构建变量的数据依赖图DDG(Data Dependence Graph),将图中无法连接上的节点消除

复制传播

一句话概括这个的优化技术:

因为:
ebx = eax
ecx = ebx
所以可以优化为:
ecx = eax 

下图就很好的展示了这种技术,可以直接把t3的值跳过x的拷贝

它和死代码传播很像,考虑到编译器的优化;在高级别的编译器优化选项开启时,高效的寄存器着色技术很难给反编译器留有什么优化的空间。

其中比较典型的场景:调用约定的寄存器使用,因为调用约定的寄存器使用顺序是“固定”的,因此代码生成时,会出现寄存器使用可以通过拷贝传播优化的场景。

看下面这个图,mov ebx, edi; 以及 mov [rax+4], ebx 这两条指令,通过复制传播,可以推断出函数的入参RDI放入了[rax+4]中。

常量传播

类似,还是一句话概括这个的优化技术:

因为:
eax = 32
ecx = eax
所以可以优化为:
ecx = 32

与复制传播类似,典型的场景就是函数调用

同理,还是同样的图,在malloc调用的时候,由于调用约定的要求,第一个参数通过RDI寄存器传入。通过常量传播技术,可以知道EDI=0x20,因此在最终的F5代码生成时,就能清楚的知道malloc的参数为常量0x20。

可能读到这里,有的朋友会感觉有点奇怪,因为,这里有个十分重要问题没有讨论:“对于反编译而言,什么是变量?”这部分内容将在后面的变量恢复章节进一步讨论。

Last updated