# 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作为例子

```c
//汇编
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），将图中无法连接上的节点消除

## 复制传播

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

```jsx
因为：
ebx = eax
ecx = ebx
所以可以优化为：
ecx = eax 
```

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

<figure><img src="https://2050902342-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FFIjush9EwySt5d8KqOoI%2Fuploads%2F144Qx0HncGEmgnZb6p64%2Fimage.png?alt=media&#x26;token=ce17e379-c5e2-4734-a565-62e279686b1a" alt=""><figcaption></figcaption></figure>

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

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

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

<figure><img src="https://2050902342-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FFIjush9EwySt5d8KqOoI%2Fuploads%2FJHSUGz4F2h8OGXAG12Rm%2Fimage.png?alt=media&#x26;token=39ea4955-f639-4d1e-afba-b5287da87aba" alt=""><figcaption></figcaption></figure>

## 常量传播

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

```jsx
因为：
eax = 32
ecx = eax
所以可以优化为：
ecx = 32
```

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

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

<figure><img src="https://2050902342-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FFIjush9EwySt5d8KqOoI%2Fuploads%2Fqfb4T0fW6FiZhbUaW7vB%2Fimage.png?alt=media&#x26;token=a10663aa-cbb1-4798-bc67-33cc80460920" alt=""><figcaption></figcaption></figure>

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