逆向编译技术

原文： | Reverse Compilation Techniques
作者： | Cristina Cifuentes
下载： | http://www.itee.uq.edu.au/~cristina/dcc.html
翻译： | 月中人 【看雪论坛】
时间： | 2007-5-1

摘要

本论文提出逆向编译器或反编译器的编写技术。这些技术基于编译器和优化理论，并以独特的方式应用于反编译；这些技术以前从未被公开发表。

反编译器由几个阶段组成，即被组织成与语言或机器特征相关的几个模块。前端是一个机器依赖的模块，句法分析二进制程序、分析其指令的语义、并且生成该程序的低级中间表示法和每一子程序的控制流向图。通用的反编译机器是一个与语言和机器无关的模块，分析低级中间代码，将它转换成对任何高级语言都可接受的高级表示法，并且分析控制流向图的结构、把它们转换成用高级控制结构表现的图。最后，后端是一个目标语言依赖的模块，生成目标语言代码。

反编译的过程中要使用一些工具：把二进制程序装入内存，对这一程序做句法分析或反汇编，以及反编译或者分析该程序来生成高级语言程序。这个过程借助编译器和库的签名来识别特定的编译器和库子程序。只要在二进制程序中识别出编译器签名，就不去反编译这些编译器启动代码(start-up)和库子程序：对于前者，从最后的目标程序去掉启动代码的那些例程，反编译器从主(main)程序入口点开始分析；对于后者，那些子程序用其库函数名代替。

所提出的技术在一个适用于Intel i80286体系结构的反编译器(原型)样机上得以实现，该样机名为dcc，在DOS操作系统下运行，为输入源的.exe文件或.com文件产生目标C程序。在第9章，将反编译输出的程序同它最初的高级语言程序做了采样比较，并且对反编译结果做出一个分析。

第1章从编译器角度对反编译做一介绍，第2章从20世纪60年代早期反编译出现开始介绍它的历史概况，第3章介绍源二进制程序的静态二进制代码和在运行时间实现程序的动作之间的关系，第4章描述前端模块这个阶段，第5章详细说明用来分析中间代码的数据优化技术，把中间代码转换成一个更高级的表示法，第6章详细说明用来分析控制流向图结构的控制结构转换技术，把控制流向图转换成一个高级控制结构的图，第7章描述后端模块，第8章介绍反编译工具程序，第9章综述dcc的实现以及取得的成果，第10章给出结论以及这项研究的工作前景。

本论文有些部分已经公开发表或者提交给国际定期刊物。两篇文章在1993年出现在第19号《拉丁美洲信息会议》(XIX 'Conferencia Latinoamericana de Informatica')：“一个反编译方法学”[CG93] 和“反编译使用的一个结构化的算法”[Cif93]。前一篇文章提出反编译的阶段(如第1章第1.3节所述)、前端(第4章)、控制流分析阶段的初始工作(第6章)、以及dcc工作实现的说明。后一篇文章提出控制流分析阶段使用的结构化的算法(第6章)。一篇刊物文章“二进制程序的反编译”[CG94] 已经被《软件-实践与经验》(Software - Practice & Experience)接受发表；这篇文章概述建立一个反编译器所使用的技术(第4,5,6,7章的摘要)、在反编译过程中如何借助签名生成器工具(第8章第8.2节)、以及用dcc反编译的一个程序样本(第9章)。有两篇文章目前正考虑在国际刊物上发表。“子过程之间数据流的反编译”[Cif94a]被提交给《程序语言杂志》(the Journal of Programming Languages)，文中完整描述了数据流分析器的优化操作，把低级的中间代码转换成一个高级的表示法。“结构化反编译图”[Cif94b] 被提交给《计算机杂志》(The Computer Journal)，文中给出结构化控制流向图的最后的改进方法，以及用dcc反编译的一个程序样本。

本论文提出的技术更充分地拓展文献中前人的工作。关于为了确定寄存器参数和寄存器返回值所需要做的子过程寄存器分析、为了清除掉有关栈的指令(即push和pop)所需要的分析、以及控制结构类集之结构化，过去没有相关的反编译研究文献。这次研究做的创新性工作在第5,6,8章描述。第5章第5.2节、第5.4节举例并且描述九种不同类型的优化，将低级中间代码转换成高级表示法。这些优化考虑条件码、子程序调用(即，子过程之间的分析)和寄存器漏出(spill)，清除掉中间指令的所有低级特征(比如条件码和寄存器)，而且把高级概念之表达式引入中间表示法。第6章第6.2节、第6.6节举例并且描述将各种不同类型的循环和条件转移包括多分支条件(例如case语句)结构化的算法。在这个领域中前人的工作成果主要集中在循环的结构化，很少文章尝试二路(2-way)条件分支的结构化，而对于多路条件分支则没有研究说明。本论文提出一个完整的方法，基于一个预先确定的、一般化的高级控制结构集合(类集)，对所有类型的结构进行结构化。在第6章第6.4节给出一个确定一般化的控制结构的集合(类集)的标准。第8章描述反编译程序使用的全部工具，最重要工具是签名生成器(第8.2节)，它用于在操作系统不共享库的体系结构下确定编译器和库的签名，比如DOS操作系统。

致谢

在20世纪90年代早期，John Gough教授和Bill Caelliin教授提出为现代机器体系结构编写反编译器的可行性问题。这个问题看来可能是对图论和数据流理论领域的一次挑战，因此我决定把寻求二进制程序的逆向编译技术作为我的博士学位论文研究方向。本论文就是对于如何实现它的诸多疑问的回答；而且确实地给出一个反编译器原型。

我要感谢计算技术社区许多人提供时间和资源。John Gough教授提供许多关于数据流分析方面的讨论，而且为该论文每章草稿都提出意见。Sylvia Willie从她的实验室借给我一个办公室和一台PC机。Pete French提供在英国的一台Vax BSD 4.2机器上的一个账号，使我可以通过网络测试Vax反编译器。Jeff Ledermann重新编写了反汇编器。Michael Van Emmerik编写了库签名生成器程序，为一些PC编译器生成编译器和库的签名，把dcc移植到DOS环境下，而且为dcc编写交互式用户界面。Jinli Cao在QUT (QUEENSLAND UNIVERSITY OF TECHNOLOGY)学习期间把一篇反编译方面的中文文章翻译成英文。Geoff Olney校对每一章，指出矛盾之处，而且对论文的布局提出建议。我在攻读这个学位期间由澳大利亚研究生研究奖(APRA)奖学金资助。

Jeff Ledermann和Michael Van Emmerik受雇于澳大利亚研究理事会，ARC授权号No. A49130261。

本论文用LATEX文档预备系统撰写。所有图表使用xfig功能交互式制作生成。

Cristina Cifuentes

June 1994

作者承认这篇论文中使用的所有商标、注册名称或专利商品名是它们各自拥有者的合法财产。


第 1 章 反编译导言

编译器的编写技术在计算机界广为人知；而反编译器的编写技术还不这么被人熟悉。本论文说明了一个相当有趣的事实，即反编译器的编写技术乃是基于编译器的编写技术。这一章通过描述一个反编译器的组成和一个二进制程序的反编译环境，简单介绍一下反编译这个主题。

1.1 反编译器

反编译器是这样一个程序，它读入一个机器语言的程序 - 源语言 - 并把它翻译为一个等价的高级语言程序 - 目标语言 (见图1-1)。反编译器或反向编译器，尝试逆向一个编译器的过程：把一个高级语言程序翻译成一个二进制程序或可执行程序。

图 1-1: 反编译器

应用基本的反编译器技术，把多种多样的机器语言二进制程序反编译成各种高级语言。反编译器的结构是基于编译器的结构，而且应用类似的原理和技术进行程序分析。第一代反编译器诞生于1960年代早期，比它们的姐姐——编译器小十岁。相对第一代编译器，反编译大量地被用来翻译科学程序。第2章描述反编译的历史。

1.2 问题

在编写一个反编译器的时候，反编译器作者必须面对一些理论上和实际的问题。有些问题能够通过使用启发式(试探方法)解决，而另一些则不能被完全确定。由于这些限制，反编译器能够对某些源程序进行全自动的程序翻译，而对于另一些源程序只能进行半自动的程序翻译。编译器是不同的，它对所有源程序都进行全自动程序翻译。这一节讨论反编译器涉及的一些问题。

1.2.1 递归的不确定性

可计算性的一般理论试图解决判定问题，即，研究对于某一个完全的陈述类是否存在判定其对错的算法。如果问题有肯定的答案，那么必须给出一个算法；否则，就要证明这样的算法不存在。对于后者，我们称之为问题是不可解决的、不可判定的、或不可计算的。对于不可解决的问题，如果能给出这样一个算法——无穷循环的程序能够在任何时候停机，那么问题可能是部分可计算的。

在数学世界里，一个抽象的概念必须用数学定义描述和建模。算法的抽象化必须用图灵机 (一种可不受储存容量限制的假想计算机) 描述。图灵机是一个计算机器，它在一个两个方向都无限长的线性磁带上打印符号，拥有有限个数的状态，并基于它的当前内部配置和磁带上的当前符号、执行由四元组的含义指定的动作。图1-2显示一个图灵机表示法。

图 1-2: 图灵机表示法

图灵机Z的停机问题包括，对于任一给定的瞬间描述α，确定是否存在一个由α开始的Z计算。换句话说，我们正在尝试确定，如果把Z放在某一个初始状态中它是否会停机。已经证明这个问题是递归地不可解决的和部分可计算的 [Dav58,GL82]。

给定一个二进制程序，甚至可以假定在程序中不允许自修改代码之类的行为，从代码中区分出数据的问题等价于停机问题，因为一条特定指令是否会被运行一般来说是未知的 (例如，对于一个循环后面的代码)。这意味着该问题是部分可计算的，因而有些情况下能够写出一个从代码中区分出数据的算法，但不是所有情况下都能够做到。

1.2.2 冯·诺依曼体系结构

在冯·诺依曼机器中，内存里的数据和指令以同样的方式表现。这意味着，只有当一个给定字节从内存被读出放入一个寄存器作为数据或指令使用的时候，我们才知道它是数据还是指令(或两者都是)。即使是在分段的体系结构上，其中数据段里只有数据信息、代码段只有指令，数据仍然能够以表的形式被储存在一个代码段中 (例如，Intel架构的case表)，指令也能够以数据的形式被储存然后通过解释这些指令而运行。PC机的Modula-2编译器用这后一个方法解释一个抽象栈机器的中间代码。中间代码被储存为数据，es:di持有的偏移量指向特定的子程序 [GCC+92]。

1.2.3 自修改的代码

自修改代码指的是指令或者预置数据在程序运行期间被修改。一条指令的一个内存字节位置能够在程序运行期间被修改、表现成另一条指令或数据。这个方法曾经很长时间用于实现各种目的。在60年代和70年代，计算机内存很小，难以运行大程序。那时，计算机内存最多有32Kb和64Kb可用。由于空间有约束，所以必须尽量充分利用。其中一个办法就是在可运行程序中节省字节，重复使用数据位置作为指令，或反之亦可。这样，一个内存单元在某一时间持有指令，而在另一时间变成持有数据或另一指令。而且，在指令不被需要时其它指令修改它们，因此程序下次执行那部分代码的时候就会执行不同的代码。

现在的计算机在内存方面的限制少了，因此自修改代码不再经常使用。但是在编写加密程序或病毒代码的时候仍然有用(见第1.2.5节)。图1-3给出一个Intel架构的自修改代码样例。inst定义的数据字节被mov指令修改成E920。动作之后，inst被当作另一条指令，它现在是0E9h 20h；即，一条跳转偏移20h的无条件跳转指令。动作之前，inst内存位置上是9090，被作为两条nop指令来执行。

 | ... | ; other code
 | mov [inst], E920 | ; E9 == jmp, 20 == offset
Inst | db 9090 | ; 90 == nop

图 1-3: 自修改代码的样例

1.2.4 成语

成语或成语性表达是一个指令序列，它形成一个逻辑实体，作为整体表示一个含义，而这个含义无法从各组成指令的基本含义被推导出来 [Gai65]。

例如，乘以或除以2的N次方是一个普遍已知的成语：乘法是通过左移来执行，而除法是通过右移来执行。另一个成语是long变量相加的方式。如果机器的字长是2字节，则一个long变量有4字节。若要相加两个long变量，先相加低两字节，然后在相加高两字节时计入第一次相加的进位。这些成语及其含义在图1-4举例说明。大多数成语在计算机界已知，但遗憾的是，并非所有的成语都被普遍了解。

shl ax, 2 |  | add ax, [bp-4] adc dx, [bp-2]
 | ↓ |
mul ax, 4 |  | add dx:ax, [bp-2]:[bp-4]

图 1-4: 成语样例

1.2.5 病毒和木马的“诡计”

病毒程序不仅含有产生恶性后果的代码，而且还设法隐藏这个代码。病毒使用各种方法隐藏其恶意代码，包括自修改和加密技术。图1-5以Azusa病毒为例说明，它在栈中存放一个值作为一个子程序新的返回地址。如图示，病毒代码的段和偏移地址先入栈，随后一条远返回指令将控制交给病毒代码。在反汇编代码的时候，大多数反汇编器会误认为子程序已经结束了，因而在远返回指令上停止对该子程序的反汇编；然而实际上它还没有结束。

 | ... | ; other code, ax holds segment SEG value
SEG:00C4 | push ax | ; set up segment
SEG:00C5 | mov ax, 0CAh | ; ax holds an offset
SEG:00C8 | push ax | ; set up offset
SEG:00C9 | retf | ; jump to virus code at SEG:00CA
SEG:00CA | ... | ; virus code is here

图 1-5: 修改返回地址

使用自修改代码来修改一条无条件跳转指令的目标地址偏移是一个常用技巧，该偏移量事先已定义为数据。图1-6举例说明Cia病毒在运行前的有关代码。如图示，cont和conta分别定义数据项0E9h和0h。在这个程序的运行期间，procX用病毒代码的偏移修改conta的内容，并在子过程返回后，数据当作指令，执行 jmp virusOffset (0E9h virusOffset) 指令。

start: |  |
 | call procX | ; invoke procedure
cont | db 0E9h | ; opcode for jmp
conta | dw 0 |
procX: |  |
 | mov cs:[conta],virusOffset |
 | ret |
virus: | ... | ; virus code
end. |  |

图 1-6: 自修改代码的病毒

病毒代码会以加密的形式出现，而且这个代码仅在需要的时候才进行解密。一个简单的加密/解密机制是用异或函数做的，因为一个字节与同样的常数两次异或的结果等于原字节。因此，加密过程是对其代码运用一次异或操作，解密过程则是将代码异或相同的常数值。图1-7举例说明这个病毒，它是LeprosyB病毒的一部分。

encrypt_decrypt: |  |
 | mov bx, offset virus_code | ; get address of start encrypt/decrypt
xor_loop: |  |
 | mov ah, [bx] | ; get the current byte
 | xor ah, encrypt_val | ; encrypt/decrypt with xor
 | mov [bx], ah | ; put it back where we got it from
 | inc bx | ; bx points to the next byte
 | cmp bx, offset virus_code+virus_size | ; are we at the end?
 | jle xor_loop | ; if not, do another cycle
 | Ret |

图 1-7: 自加密的病毒

最近，多态变形被用来加密病毒。这类病毒主要关键是基于指令集的规则性自己生成代码段。图1-8举例说明Nuke病毒的加密引擎。这里，加密循环中每轮使用一个不同的键(ax)，并使用一条异或指令加密。

 | Encryption_Engine: |
07AB |  | mov cx,770h
07AE |  | mov ax,7E2Ch
07B1 | encryption_loop: |
07B1 |  | xor cs:[si],ax
07B4 |  | inc si
07B5 |  | dec ah
07B7 |  | inc ax
07B8 |  | loop encryption_loop
07BA |  | retn

图 1-8: 自生成的病毒

总之，病毒程序利用机器语言集的任何缺陷、自修改代码、自加密代码和无文档操作系统函数。这类代码难以自动反汇编，因为对指令/数据的修改大部分在程序运行期间进行。在这些情况下，需要人工介入。

1.2.6 体系结构依赖的限制

大多数现代计算机体系结构使用预取缓冲区在处理器执行指令的时候预取指令。这意味着所预取的指令被存放在一个不同于它原先在主存里位置的地方。当一个程序利用自修改代码手段、试图修改一条内存里的指令时，如果指令已经被预取，那么它的内存副本被修改而在流水线缓冲区里的指令并没有得到修改；因此，被执行的将是原始未修改的指令。这个例子可以见图1-9。在该例子中，jmpDef数据定义的确实是一条指令，jmp codeExecuted。这一定义看起来似乎被前面的指令‘mov[jumpDef],ax’修改了，它把jmpDef的定义换成两条nop指令。这就意味着codeNotExecuted处的代码被执行，显示“Hello world!”然后退出。而在i80386机器上运行这个程序的时候，它显示“Share and Enjoy!”。因为i80386有一个4字节的预取缓冲区，jmpDef定义已经被预取，因而没有被修改，所以执行的是跳转到codeExecuted处，亦即显示“Share and Enjoy!”。使用一般的直线单步调试器我们无法确定这类代码的执行情况，除非对机器做一个完全的仿真。

1.2.7 由编译器和链接器引用的子程序

另一个反编译问题来自被编译器引进的大量子程序和由链接器链接的一些例程。编译器总是引进启动子程序(start-up)建立它的环境、以及所需要的运行时支持例程。这些例程通常是用汇编语言编写的，而且大多数情况下无法翻译成高级表示法。另外，大多数操作系统不提供共享库机制，因此，二进制程序是自我包含的，库例程被封装入每一个二进制映像之内。库例程或是用编译器的编写语言或是用汇编语言编写。这意味着一个二进制程序不仅包含由程序设计者编写的例程，也通过链接器链接了很多其它例程。例如，一个显示“hello world”的C程序在PC机上编译成二进制程序以后有25个以上不同的子程序。而使用Pascal语言编写类似的程序在PC上编译会在可执行程序中产生40个以上子程序。逆向工程师通常只对所有这些例程中的一个初始子程序感兴趣：主程序。

 | mov ax, 9090 | ; 90 == nop
 | mov [jumpDef], ax |
 | jmpDef db 0EBh 09h | ; jmp codeExecuted
codeNotExecuted: |  |
 | mov dx, helloStr |
 | mov ah,09 |
 | Int 21 | ; display string
 | Int 20 | ; exit
codeExecuted: |  |
 | mov dx, shareStr |
 | mov ah, 09 |
 | Int 21 | ; display string
 | Int 20 | ; exit
shareStr | db "Share and Enjoy!", 0Dh, 0Ah, "$" |
helloStr | db "Hello World!", 0Dh, 0Ah, "$" |

图 1-9: 体系结构依赖的问题

1.3 反编译器的阶段

在概念上，一个反编译器的构成与编译器是相似的，通过一系列阶段把源机器程序从一个表示法转换成另一个表示法。反编译器的典型阶段见图1-10所示。这些阶段反映了反编译器的逻辑组织。实际上有些阶段可以结合，见第1.4节。

请注意的一点是，在反编译器中没有词汇分析阶段或扫描阶段。这是因为机器语言太简单：所有的语言符号(记号)都用一些字节或一字节的位来表示。任给一字节，不可能确定该字节是不是一个新记号的开始；例如，字节50可能表示一条push ax指令操作码，也可能表示一个立即常数或者一个数据单元的偏移量。

图 1-10: 反编译器的阶段

1.3.1 句法分析

语法分析器或句法分析程序把源程序的字节组织成源机器语言的语法短语(或语句)。这些短语用一个语法分析树表示。表达式sub cx, 50在语义上等价于cx := cx- 50。后一种表达可以用如图1-11所示的一个语法分析树表现。这个表达式有两个短语：cx-50和cx:=<exp>。这些短语形成一个层次结构，但是由于机器语言的单纯天性，因此总是最多两层。

图 1-11: cx := cx- 50 语法分析树

句法分析程序的主要问题是确定哪些是数据和哪些是指令。例如，由于冯·诺依曼机器的体系结构，一个case表可以放在代码段，而反编译器并不知道这个表是数据而非指令。象这样的情况，我们不能想当然地认为下一字节总是一条指令从而循序地分析指令。确定指令的正确顺序需要使用机器依赖的启发式。句法分析在第4章讨论。

1.3.2 语义分析

语义分析阶段检查源程序一组指令的语义含义，收集类型信息并且向整个子程序传递这个类型。对于任何一个编译器生成的二进制程序，只要程序能够运行，其机器语言语义一定是正确的。一般来说，编译器不会生成错误的代码导致二进制程序不能运行。因此，在源程序中不会有语义错误，除非句法分析程序对某一条指令做了不正确的分析或者把指令当作数据分析。

为了检查一组指令的语义含义，就要寻找成语。图1-4的成语可以被转换成语义等价的指令如：第一例ax乘以4，和第二例long变量的加法。在那个特定的子程序中，[bp-2]:[bp-4]表示一个long变量，而dx:ax在这个子程序中临时保存一个long变量。dx:ax这两个寄存器不必在整个子程序中都被当作一个long寄存器使用，只有在需要的时候才这样。

通过短语表达式新发现的类型被类型传播到整个图。例如，在图1-4中，子程序的两个栈存储单元已知作为一个long变量使用。因此，无论这两个单元在哪里被独立地使用或定义都必须被转换成一个long变量的使用或定义。如果下面两个语句是该子程序代码的一部分

asgn [bp-2], 0

asgn [bp-4], 14h

那么，[bp-2]和[bp-4]的long类型传递会将这两个语句合并成一个语句来表示那些标识符为long变量，如下

asgn [bp-2]:[bp-4 ], 14h

最后，语义错误通常不是由编译器生成代码的时候造成的，但是当可执行程序在一个更先进的体系结构上运行的时候会发现语义错误。例如，假设我们要反编译i80286体系结构的二进制程序。新的i80386和i80486体系结构是以这个i80286体系结构为基础，而且它们的二进制程序储存方式是相同的。这些新的体系结构在机器语言方面不同的是，使用更多的寄存器和指令。如果给我们一条指令

add ebx, 20

寄存器识别符ebx是一个32位寄存器，这在旧的体系结构是没有的。因此，尽管该指令在语法上是正确的，但是对于我们正在反编译的机器语言而言它在语义上是错误的，因而就要报告一个错误。第4章在这方面做了一些分析。

1.3.3 中间代码生成

反编译器分析程序需要一个中间表示法来明确地表示源程序。它必须容易从源程序中生成，而且还必须适合用来表示目标语言。第1.3.1节举例的语义等价表示法用来实现这个目标是很理想的：它是一个三地址代码表示法，一条指令最多能有三个操作元。这些操作元全部都是机器语言的标识符，但是能够容易地把它展开成表达式以表现高级语言表达式 (亦即，一个标识符就是一个表达式)。这样，使用一个三地址的表示法，则一条指令最多能有三个表达式。第4章描述反编译器使用的中间代码。

1.3.4 控制流向图生成

源程序中每个子程序的控制流向图也是反编译器分析程序所必需的。这个表示法适合用于确定程序里的高级控制结构。它也用于清除由于机器语言的条件跳转有偏移量限制因而由编译器产生的中间跳转(intermediate jump)。在下面的代码中

 | ... | ; other code
 | jne x | ; x <= maximum offset allowed for jne
 | ... | ; other code
x: | jmp y | ; intermediate jump
 | ... | ; other code
y: | ... | ; final target address

标签x是条件跳转jne x的目标地址。这条指令受到该机器体系结构最大允许偏移量的限制，因此不能够只用一条指令执行到y的一个条件跳转；只得使用一条中间跳转指令。在控制流向图中，到x的条件跳转直接使用到y的最后目标跳转替换。

1.3.5 数据流分析

数据流分析阶段试图改善中间代码，以便能够得到高级语言表达式。在这个分析期间，临时寄存器的使用和条件标志被清除掉，因为在高级语言里面没有这些概念。如下一系列的中间语言指令

asgn ax, [bp-0Eh]

asgn bx, [bp-0Ch]

asgn bx, bx * 2

asgn ax, ax + bx

asgn [bp-0Eh], ax

最后的输出应该是一个高级表达式的形式

asgn [bp-0Eh], [bp-0Eh] + [bp-0Ch] * 2

第一组指令使用寄存器、栈变量和常数；表达式用标识符组成，而且表达式树最多2层次。在分析之后，最后输出的指令使用栈变量标识符，[bp-0Eh]、[bp-0Ch]、和一个3层的表达式树，[bp-0Eh] := [bp-0Eh] + [bp-0Ch] * 2。被机器语言用来计算这个高级表达式的临时寄存器，ax和bx，连同对这些寄存器的读取和写入，都已经被清除掉。第5章给出一个算法实现这个分析，以及清除其它中间语言指令，比如push和pop。

1.3.6 控制流分析

控制流分析器阶段试图将程序每个子程序的控制流向图结构化成一个一般化的高级语言构造集合。这个一般化的结构集必须包含大多数语言适用的控制指令；比如控制的循环和条件转移。不应允许使用语言特定的构造。图1-12展示了两个控制流向图样例：一个if..then..else和一个while()。第6章给出把任意的控制流向图结构化的算法。

图 1-12: 一般化的构造

1.3.7 代码生成

反编译器的最后阶段是在每个子程序的中间代码和控制流向图的基础上生成目标高级语言代码。为所有的局部栈、参数和寄存器变量标识符选择变量名称。也为在程序中出现的各个例程指定各自的子程序名称。控制结构和中间指令被翻译成高级语言语句。

以第1.3.5节的例子来说，局部栈标识符[bp-0Eh]和[bp-0Ch]分别被赋予任意的名称loc2和loc1，然后那条指令被翻译成C语言如下

loc2 = loc2 + (loc1 * 2);

代码生成在第7章讨论。

1.4 阶段的组合

反编译器的实际实现通常是把第1.3节列出的几个反编译器阶段组合在一起。正如图1-13所示，分成三个不同的模块：前端、UDM和后端。

图 1-13: 反编译器的模块

前端由那些机器依赖的和机器语言依赖的阶段组成。这些阶段包括词汇的、句法的和语义的分析，以及中间代码生成和控制流向图生成。总而言之，这些阶段产生一个中间的、与机器无关的程序表示法。

UDM即universal decompiling machine（通用反编译机器）；它是一个完全独立于机器和语言的中间模块，而且它是进行反编译分析的核心。这个模块包括两个部分：数据流分析器和控制流分析器。

最后，后端由那些高级语言依赖的或目标语言依赖的阶段组成。这个模块是代码生成器。

在编译器理论中，阶段的组合是编译器作者为不同机器、不同语言建造编译器的机制。如果为不同的机器重新编写一个编译器后端，那么该机器的新编译器使用原来的前端构成。类似地，可以为别的高级语言定义编写一个新前端，然后跟原来的后端一起使用。这个方法的实现实际上会由于它固有选择的中间代码表示法而受到一定限制。

理论上，反编译器的阶段组合使得为各种不同机器和语言编写反编译器变得容易些：为不同机器编写不同的前端，为不同的目标语言编写不同的后端。在实际应用中，其成果总是受制于它所采用的中间语言的普遍适用程度。

1.5 反编译器的上下文环境

在实践中，有一些程序可以配合反编译器一起创建目标高级语言程序。一般来说，源二进制程序有一个重定位地址表，当程序被装入内存的时候，将在那些地址上进行重定位。这个任务由装载程序完成。然后，已重定位的或绝对的机器码被反汇编产生该程序的汇编表示法。反汇编器可以借助编译器和库的签名去掉编译器启动代码(start-up)和库例程的反汇编。然后，汇编语言程序作为反编译器的输入，产生一个高级语言目标程序。需要对目标程序做进一步的处理，比如对while()循环做一转换以便后期处理器处理。图1-14展示了“反编译过程”的几个典型步骤。使用者也可能作为一个信息提供者，尤其是在确定库例程以及区分数据和指令的时候。如果可能的话，这比使用自动工具更可靠。反编译器辅助工具在第8章讨论。这一节简单说明它们的任务。

图 1-14: 反编译系统

装载器

装载器是一个程序，它把二进制程序装入内存、而且如果程序是可重定位的那么就重定位机器码。在重定位期间，指令被改变然后放回内存。

签名生成器

签名生成器是一个自动确定编译器和库的签名的程序：唯一地标识每个编译器和库子程序的二进制标本。这些签名的使用试图反向进行链接器的工作——链接器把库和编译器启动代码链接到程序。这样，被分析的程序只包括用户子程序：从用户最初的高级语言程序编译那些。

例如，显示“hello world”的C程序编译以后，在二进制程序中有超过25个不同子程序，其中16个子程序是被编译器增加来设置它的环境，9个例程是被链接器加入来实现printf()，1个子程序来自最初的C程序。

签名生成器的使用不仅减少了需要分析的子程序个数，也由于使用库函数名称代替任意的子程序名称从而增加了目标程序的可读性。

原型生成器

原型生成器是一个自动确定库子程序参数类型以及函数返回值类型的程序。这些原型来自于函数库的头文件，被反编译器用来确定库子程序的参数以及参数个数。

反汇编器

反汇编器是一个把机器语言转换成汇编语言的程序。有些反编译器把汇编语言程序转换成一个更高级的表示法(见第2章)。在这些事例中，反汇编器输出的汇编程序原来是在汇编器中编写的或者是在编译器中编写并编译成汇编程序。

库绑定

假如产生的目标代码中使用库函数名称 (亦即，能检测到库签名)，如果反编译器的目标语言不是最初编译二进制源程序的同一种语言，那么由于两种语言使用不同的库例程，所以即使这个程序是正确的也不能再用目标语言编译它了。解决这个问题的办法是使用库绑定——在两种语言的库例程之间建立关联。

后期处理器

后期处理器是一个程序，它把一个高级语言程序转换成同种语言的一个语义等价的高级程序。例如，假如目标语言是C语言，以下代码

loc1 = 1;

while (loc1 < 50) {

/* some code in C */

loc1 = loc1 + 1;

}

可能被后期处理器转换成

for (loc1 = 1; loc1 < 50; loc1++) {

/* some code in C */

}

这是一个语义等价的程序，使用C语言适用的控制结构，而不是反编译器反编译结果的一般化结构。

1.6 反编译的用途

反编译是计算机专业人士的一个工具。反编译主要应用在两个领域：软件维护与安全性。在前一个领域，反编译被用来恢复丢失的或者无法获取的源程序代码，将一个使用过时语言编写的代码翻译成更新的语言，将一个以非结构化方式编写的旧程序(即，层次代码) 变成一个结构化程序，将应用程序移植到一个新的硬件平台，以及调试已知有缺陷但是源代码不可得的二进制程序。在后一领域，反编译在软件关键的系统中被用来验证一个编译器产生的目标代码，因为在这些系统中我们不能绝对信赖编译器，另外，反编译也被用来检查是否存在恶意代码比如病毒。

1.6.1 法律方面

关于反编译的合法性问题去年已经被提出来了。反编译支持者和反对者之间的争论目前正在进行，支持方主张反编译工具的使用有利于公平竞争，反对方主张反编译侵犯了版权。各个不同国家正在修改法律以确定在哪些情况下反编译是法律许可的。目前，商业软件的销售随同软件协议书禁止用户反汇编或反编译其产品。例如，Lotus软件协议部分这样写道：

你不可以使用任何方式包括反汇编或反编译来改变、合并、修改或者改编这个软件。

探讨反编译的合法性相关问题不是本论文的目的。本论文不对该主题做更进一步讨论。


第 2 章 反编译 - 已经实现的

在过去20年中已经为编写反编译器做过各种不同的尝试。由于在编译过程中丢失了大量信息，为了能够再生成高级语言代码，所有这些实验的反编译器需要做一些限制，包括——汇编文件的反编译[Hou73, Fri74, Wor78, Hop78, Bri81]，有或没有符号调试信息的目标文件的反编译[Reu88, PW93]，简单化的高级语言[Hou73]，以及对编译器规约(compiler specification)的要求 [BB91,BB93]。汇编程序有符号化文本形式的有用信息，比如数据段、数据和类型信息、子程序入口点、和子程序出口语句。所有的这些信息能够从符号表中收集，如此反编译器便不需要处理区分指令和数据，或者变量和子程序的命名问题。带有调试信息的目标文件包含编译器构造的程序符号表。假如有符号表，因为存放数据的内存单元确定以后，就很容易确定哪些内存单元是指令了。一般来说，目标文件包含的信息比二进制文件更多。最后，利用编译器规约是不实际的，因为编译器厂商通常不公开这些规约。

2.1 前人的工作

自从在1960年代首次使用以后，反编译器一直被认为是一个有用的软件工具。那时，反编译器被用来帮助从第二代计算机到第三代计算机的程序转换；如此免除了费时费力的为第三代机器重写程序的任务。在70年代和80年代期间，反编译器在程序的可移植性、文档编写、调试、遗失的源程序代码的重建、和现有二进制程序的修改等领域得到应用。在90年代，反编译器已经成为一个逆向工程工具，能够帮助使用者完成许多任务，诸如检查软件有没有非法代码、检验编译器是否生成正确的代码、以及把一个机器上二进制程序翻译成另一个机器上的。注意，反编译并没有被用于软件盗版或侵犯版权，因为这个方法还没有彻底成功，只能在一个具体任务中充当辅助工具。

下列描述举例说明最著名的反编译器以及个人或公司在反编译器方面的研究：

D-Neliac decompiler, 1960.

正如Halstead在[Hal62]中所言，Donnelly-Neliac (D-Neliac) 反编译器是在1960年由J.K.Donnelly和H.Englander在海军电子实验室(NEL)制造。Neliac是NEL在1955年开发的一个Algol类型语言。D-Neliac反编译器从机器代码程序产生Neliac代码；为雷明顿兰德公司的 Univac M-460 伯爵夫人计算机和控制数据公司的1604计算机编写了不同版本。

事实证明，D-Neliac可应用于把非Neliac编译的程序转换为Neliac程序，以及在原来的高级程序中检测逻辑错误。这个反编译器证明了编写反编译器的可行性。

W.Sassaman, 1966.

Sassaman在TRW公司开发了一个反编译器，帮助从第二代计算机到第三代计算机的程序转换过程。这个反编译器以IBM 7000序列的符号化汇编程序做为输入并产生Fortran程序。因为符号化汇编程序中的信息更有用，所以不选择二进制代码作为输入语言。Fortran在1960年代是一个标准语言，而且第二代和第三代计算机上都在使用它。被编译的程序类型是涉及代数运算的工程应用。使用者需要为子程序的识别定义规则。反编译器达到90%正确率，需要一些人工干预 [Sas66]。

这是第一个使用汇编程序而非纯二进制代码作为输入的反编译器。汇编程序包含名字、宏、数据和指令形式的有用信息，在二进制程序或者可运行程序中是没有这些信息的，因此避免了在反编译器的语法分析阶段区分数据和指令的问题。

M.Halstead, 1967.

洛克希德导弹与空间公司(LMSC)在海军电子实验室开发的Neliac编译器上做了一些增强，以照顾反编译的需要 [Hal67]。LMSC Neliac反编译器以IBM 7094上的机器代码作为输入，产生Univac 1108机器的Neliac语言代码。它证明成功地反编译90%以上的指令，让程序员去反编译剩下的10%。这个反编译器在LMSC使用，遵守美国和加拿大的消费者合同 [Hal70]。

Halstead分析，如果要把正确反编译的指令比例从一半提高到100%则需要在实现上做更多工作，而且发现它和已经付出的努力近乎相等 [Hal70]。这是因为那里的反编译器只处理直接明了的情况，而较难的情况被留给程序员去考虑。为了处理更多的情况，需要花更多的时间为这些特殊情况编写编译器代码，而且跟简单情况相比编写代码所需时间也成比例地增加。

Autocoder to Cobol Conversion Aid Program, 1967.

Housel详细描述了在IBM上开发的一套商业反编译器，把自动编码语言(Autocoder)的程序——定位于商业数据处理——翻译成Cobol。其转化是一对一映射，因而需要手工优化。最后程序的尺寸是原来程序核心存储的2.1%倍。

这个反编译器确实是一种语言到另一种语言的翻译工具。没有尝试分析程序和减少生成的指令数。一般来说生成的代码执行效率比较低。

C.R.Hollander, 1973.

Hollander的博士学位论文 [Hol73] 描述一个反编译器，它的设计围绕一个规则化的面向语法的元语言，而且有5个相互合作的顺序过程；初始化器、扫描器、语法分析器、构造器和生成器；各自实现为一个元规则组的解释程序。该反编译器是一个元系统，通过实现解释程序来定义它的操作。

初始化器把程序装入而且把它转换成一个内部表示法。当发现一个指令区域的时候，扫描器跟初始化器交互；当匹配源程序代码模板与指令的时候，扫描器跟语法分析器交互。语法分析器在源语言的语法短语和它们语义对等的目标语言之间建立对应。最后，构造器和生成器生成最后的程序代码。

实现了一个实验的反编译器将一个IBM的System/360汇编子集翻译为一个类Algol的目标语言。这个反编译器用斯坦福大学开发的一个编译器Algol-W编写，而且在10个测试程序上正确地工作。

这个工作提出一个新奇的反编译方式，使用一个拘泥规则的面向语法的元语言，但它的主要缺点也正是这个方法学，相当于从汇编指令到高级指令的模式匹配操作。因为需要按照特定的顺序识别一个指令是否属于某一个模式，所以限制了能够被反编译的汇编指令数量；没有考虑中间指令、各种不同的控制流模式或者优化代码。为了使面向语法的反编译器能够工作，需要列举每个不同编译器每一个高级指令的所有可能模式集合。另一方法是为一个特定的编译器编写一个反编译器，而且利用其编译器规约；仅当编译器作者愿意公开他的编译器规约的时候，这个方法才有可能实现。显然，Hollander的反编译器可以这样做，因为Algol-W编译器是在他做这项研究时所在的大学研制的，他知道这个编译器规约。这样，由任何一个Algol-W指令生成的汇编指令组是已知的。

B.C.Housel, 1973.

Housel的博士学位论文 [Hou73] 描述一个有条理的反编译方法，它借用编译器、图、和优化理论的概念。他的反编译器包括三个主要的步骤：部分汇编，分析器和代码生成。

部分汇编阶段从指令中区分数据，建立一个控制流向图，而且产生一个程序的中间表示法。分析器分析该程序以便发现程序循环并且清除掉不必要的中间指令。最后，代码生成器优化算术表达式的翻译，而且为目标语言生成代码。

编写了一个实验的反编译器，针对Knuth的MIX汇编器(MIXAL)，产生IBM 370机器适用的PL/1代码。测试了6个程序，88%的指令是正确的，而其余12%的指令需要人工干预 [HH73]。

这个反编译器证明，通过使用已知的编译器和图的方法，可以实现生成良好高级代码的反编译器。中间表示法的使用使得分析完全不依赖机器。这个方法学的主要缺陷在于源语言的选择，MIX汇编语言，在这些程序中不仅带有大量可用信息，而且它是一个简单化的、非现实的汇编语言。

The Piler System, 1974.

Barbe的Piler系统尝试成为一个通用的反编译器，在许多不同类别的源-目标语言对之间进行翻译，以帮助计算机程序的自动化转换。Piler系统由三部分组成：解释、分析和转换。这样，可以为不同的源机器语言编写不同的解释程序，而且可以为不同的目标高级语言编写不同的转换器，使得编写不同的源-目标语言对的反编译器比较容易。这个反编译器也被用于文档编写、调试辅助以及评估编译器生成的代码。

在解释期间，源的机器程序被装入内存储器，经过语法分析，转换成一个三地址微型表示法。也就是说，每一个机器指令需要一个或多个微型指令。分析器使用数据流分析确定程序的逻辑结构，而且修改其微型表示法成为一个中间表示法。经过这一分析以后使用者得到该程序的一个流程图，而且如果其中有什么错误，他们甚至能代表反编译器修改流程图。最后，转换器生成目标高级语言的代码 [Bar74]。

虽然Piler系统尝试成为一个一般化的反编译器，但是只编写了一个用于GE/Honeywell 600计算机机器语言的解释程序，以及Univac 1108机器Fortran和Cobol语言的程序轮廓转换器。这个项目主要致力于分析器。

Piler系统为较多类别的源和目标语言编写通用反编译器的第一个尝试。它的主要问题是尝试使用一个甚至比一个汇编类型表示法更低级的微型表示法实现相当程度的一般化。

F.L.Friedman, 1974.

Friedman的博士学位论文描述一个反编译器，用于在相同体系结构等级内的小型计算机操作系统的迁移 [Fri74]。描述了四个主要部分：前期处理器、反编译器、代码生成器、和编译器。

前期处理器把汇编代码转换成一个标准形式 (描述的汇编语言)。反编译器读入标准的汇编语言形式进行分析，并且把它反编译成一个内部表示法，然后由代码生成器从中产生FRECL代码。最后，一个FRECL编译器把这个程序编译成另一个机器上的机器代码。FRECL是一个用于程序迁移和开发的高级语言；它是Friedman开发的，而且也为它编写了一个编译器。在这个工程中使用的反编译器是Housel反编译器的一个改写版 [Hou73]。

进行过两个实验；第一个实验把IBM 1130 Disk Monitor System一个自包含的小部分迁移到Microdata 1600/21；在输入的汇编程序上需要最高达到33%的人工干涉。总体来说，为迁移系统准备输入代码需要做太多的工作，无法在合理的时间量上完成；因此，做了第二个实验。第二实验反编译Microdata 1621操作系统程序成FRECL程序，并且把它们再编译成Microdata 1621机器代码。产生的一些程序被重新插入操作系统内并进行测试。平均上，只有2%的输入汇编指令需要人工干预，但是最后机器程序的机器指令数有194%的增长。

这个论文是反编译操作系统代码的第一个尝试，而且它例证了在反编译机器依赖的代码时反编译器面对的困难。这个迁移系统需要对输入程序做大量格式化工作，而且看起来，最后产生的程序由于包含更多机器指令造成在空间利用和执行时间上效率都很低。

Ultrasystems, 1974.

Hopwood 详细描述了Ultrasystems公司的一个反编译工程，他是该公司的一个系统设计顾问 [Hop78]。这个反编译器作为Trident(三叉戟)潜艇射击控制软件系统的一个文档编写工具。它以Trident汇编程序作为输入，而且产生这个公司开发的Trident高级语言(THLL)程序。分成四个主要阶段：规格化、分析、表达式凝聚(expression condensation)和代码生成。

输入的汇编程序经过规格化以便区别数据区域和伪指令。产生一个中间表示法，并分析数据。在表达式凝聚过程期间建立算术表达式和逻辑表达式，最后通过和THLL匹配控制结构生成输出的高级语言程序。

这个工程尝试通过把汇编程序转换成高级语言来为其编写文档。事实上，由于该工程的时间原因，没有为表达式凝聚阶段编写代码，因而一个单一的表达式需要几个指令，以致输出的程序很难读懂。

V.Schneider and G.Winiger, 1974.

Schneider和Winiger提出指定高级语言的编译与反编译规格这样一个观念。通过为编译过程定义一个无语境(context-free)语法 (即，描述从表达式和赋值产生的所有可能的二地址目标代码(2-address object code))，文章说明怎样通过反转这个语法来把目标代码反编译成最初的源程序 [SW74]。甚至，一个语义含糊的编译语法会产生最理想的目标代码，而且会产生一个语义不含糊的反编译语法。一个案例研究说明，用Algol 60构造产生的目标代码无法被反编译出确定的结果。这个工作是一个未最终完成的反编译器，但是在文献中没有发现关于它的更多信息。

这个工作提出以另一种方式实现的一个面向语法的反编译器 [Hol73]；亦即，反编译器使用一系列目标指令的模式匹配来重建最初的源程序。在这个方式中，为了反转语法并产生一个反编译语法，必须已经了解编译语法。注意，除非已把它定义为编译语法，否则不可能做最优化。

Decompilation of Polish code, 1977, 1981, 1988.

在这个文献中发现两篇反编译领域的文章，是关于波兰代码(Polish code)转化成Basic代码。问题的出现与高度交互式系统有关，这类系统要求快速响应用户的每个输入。用户的程序保存为一个中间形式，然后要每次“反编译”一个命令。它给出一个从逆波兰表示法到表达式的翻译算法 [BP79]。

第二篇文章提出反编译过程分两步骤：需要将机器码转换成波兰表示法，和波兰代码到源形式的转换。该文专注于在反编译问题的第二个步骤，但还是声称使用一个波兰表示法的无语境语法和一个左到右或者右到左的语法分析模式，把波兰代码反编译成Basic代码 [BP81]。

这个技术最近被用于一个把逆波兰代码转换成电子表格表达式的反编译器 [May88]。在这个例子中，一个含有类似电子表格组件的产品的程序员为了让他的产品提速，希望把用户的表达式储存为一个编译的形式——在这个例子中就是逆波兰表示法，并且每当用户想看或修改它们的时候它就反编译这些表达式。圆括号被留下作为逆波兰表示法的一部分以便重建与用户输入系统的精确相同的表达式。

从这个意义上讲，用反编译这个词是不正确地使用术语了。在这些文章中提到的一切是重建或逆语法分析最初的表示 (用Basic编写或电子表格表达式)——提供一个程序的中间波兰表示法——的一个方法。在波兰到Basic翻译器这个例子中，没有说明关于如何得到这样一个提供一个机器程序的中间表示法。

G.L.Hopwood, 1978.

Hopwood的博士学位论文 [Hop78] 描述了为可迁移性和文档编写设计的一个7步反编译器。它宣称反编译过程可以借助人工干涉或其他外部信息。

反编译器的输入程序用一个预处理程序格式化，然后装入内存，而且建立该程序的一个控制流向图。这个图的结点代表一条指令。在构造该图之后，控制模式被识别，而且通过结点分割法或人造变量的引入把产生goto语句的指令清除掉。然后源程序被翻译为一个中间机器独立代码，而且为了寻找表达式并通过一个向前代入的方法清除不必要的变量，在这个表示法上做变量使用(variable usage)分析。最后，为每一个中间指令生成代码，目标语言不支持的操作则用函数实现，而且提供注解。需要人工介入准备输入数据，在翻译过程当中给反编译器提供它需要的额外信息，和对目标程序做修改。

为Varian Data机器620/i编写了一个实验的反编译器。它把汇编程序反编译成MOL620，这是M.D.Hopwood和该作者在Irvine市的加州大学开发的一个面向机器的语言。反编译器用一个大的调试器程序Isadora进行测试，该程序是用汇编语言编写的。所生成的反编译程序被手工修改以再编译成机器代码，因为其中有调用中断服务例程、自修改代码、和为了子程序调用而使用的额外寄存器。最后的程序比原来的汇编程序有更好的文档说明。

这项研究的主要缺点是控制流向图的粒度和在最后的目标程序中寄存器的使用。关于前者，是指Hopwood选择了建立每个指令一个结点的控制流向图；这就是说，大程序的控制流向图尺寸相当大，而且跟使用基本块结点 (即，结点的大小依赖控制流改变的次数) 相比没有任何优越性。关于后者，MOL620语言允许机器寄存器的使用，而且在Hopwood的论文中举例的样本代码显示，寄存器被用作表达式的一部分和子程序调用参数。寄存器的概念并非在高级语言中可用的高级概念，而且如果想要生成高级的代码的话不应该使用它。

D.A.Workman, 1978.

这项成果描述了在为实时训练装置系统设计适用的高级语言时反编译的应用，尤其在F4教练机 [Wor78]。F4的操作系统是用汇编语言编写的，因此这个反编译器的输入语言是汇编语言。由于这个工程的目标是用于设计，因此没有确定输出语言，没有实现代码生成。

反编译器的两个阶段被实现：第一个阶段，把汇编程序映射为一个中间语言并收集关于源程序的统计信息，而第二个阶段产生基本块的控制流向图，依据它们的可能类型来划分指令，并且分析控制流以确定高级控制结构。结果表明高级语言需要能够处理位元串，支持循环和条件控制结构，而且不需要动态的数据结构或递归。

这项成果提出一种新奇的反编译技术应用，尽管输入语言是汇编语言而非机器码。通过把指令分类来做简单的数据分析，但是没有尝试完全地分析它们，因为这里不需要生成高级代码。控制流分析是完全的而且考虑8个不同的循环类别和二路条件语句。

Zebra, 1981.

在海军水下系统中心开发的Zebra样机试图实现汇编程序的可移植性。Zebra把ULTRA/32汇编语言一个名叫AN/UYK-7的子集作为输入语言，而且产生PDP11/70的汇编程序。D.L.Brinkley在[Bri81]中描述了这一工程。

Zebra反编译器由3趟组成：词汇和流分析，这一趟解析程序而且在基本块的图中做控制流分析。第二趟是关于把程序翻译为中间形式，而第三趟通过清除多余的读写(loads and stores)使中间表示法简单化，跟Housel描述的方法大同小异 [Hou73,HH73]。

总的来说，获取程序的语义很难，而且这个反编译不经济实用，不过在迁移过程中它能提供一定的帮助。

这个工程利用已知技术来开发一个汇编程序反编译器。这项研究没有引入新的概念，但是它提出一个观点，认为反编译应该作为一个工具帮助解决某个问题，而不是完全解决该问题的工具，因为假如反编译器不可能达到100%正确。

Decompilation of DML programs, 1982.

数据库代码的反编译器被设计用来把Codasyl DML程序的一个子集——用程序性操作编写——转换成一个带有非程序性查询规格(nonprocedural query specification)的关系系统。一个存取路径模型(Access Path Model)被引进来解释该程序所执行的语义上的存取。为了确定FIND操作如何实现语义上的存取，在控制流向图上做一个全局数据流延伸分析，而且把操作匹配到模板。最后的图结构被再映射成一个关系结构。这个方法依赖对象的逻辑顺序和DML语句的一个标准顺序 [KW82]。

在参考文献[DS82]中提议另一个数据库代码反编译器，把精心编码的应用程序反编译成该文中提议的一个语义表示法。这项成果主要是通过改变一个数据库管理系统(DBMS)的应用需求，其中应用程序用Cobol-DML语言编写。一个Cobol-DML程序反编译器用来分析和把应用程序转换成一个与模型和(数据库)模式无关的表示法。这个表示法稍后被修改或重新结构化以说明数据库改变。语言模板被用来匹配一个Cobol-DML程序的关键指令。

对于数据库环境而言，反编译是组织一系列语句的过程，将一个查询用另一个 (非程序性的) 规格来表现。需要数据流分析，但是没有为这个类型的应用实现反编译器的所有其它阶段。

Forth Decompiler, 1982, 1984.

一个递归的Forth语言反编译器是扫描一个编译字典条目而且把单词反编译成原语和地址的一个工具 [Dud82]。这样的反编译器被认为Forth语言工具箱中最有用的工具之一 [HM84]。该反编译器实现一个递归降序的语法分析器以便反编译的单词能够以一个递归方式被反编译。

这些成果提出一个逆语法分析工具而不是一个真正的反编译器。该工具递归地扫描一个字典表并且返回与给定单词有关的原语或地址。

Software Transport System, 1985.

C.W.Yoo描述了一个自动化软件迁移系统(STS)，它把汇编语言代码从一个机器移到另一个机器。该过程包括把机器m1的一个汇编程序反编译成一个高级语言，和在机器m2上把这个程序编译成汇编程序。一个实验的反编译器在Intel 8080体系结构上开发；它以汇编程序作为输入并且产生PL/M程序。再编译的PL/M程序比它们原来的汇编程序提高效率达23%。一个实验的STS被用于为Z-80处理器开发一个C语言交叉编译器(C cross-compiler)。该工程遇到在STS中缺乏数据类型的问题 [Yoo85]。

STS把机器m1的一个汇编程序和机器m2的一个汇编语法作为输入，而且为机器m2产生一个汇编程序。输入语法经过语法分析所产生的表被抽象语法树语法分析器用来解析该输入汇编程序，而且生成该程序的一个抽象语法树(AST)。这个AST是反编译器的输入，然后执行控制流分析和数据流分析，跟 Hollander [Hol73]、Friedman [Fri74] 和Barbe [Bar74] 描述的方法大同小异，而且最后生成高级代码。然后为机器m2编译该高级语言。

这项成果没有提出任何新的反编译领域研究，但是提出一个新颖的汇编程序迁移方法——它使用一个描述目标体系结构汇编指令的语法。

Decomp, 1988.J.

Reuter编写decomp，一个用于Vax BSD 4.2的反编译器，把带有符号信息的目标文件作为输入并且产生类似C的程序。这个反编译器的性质是在没有可用源代码的情况下把帝国游戏(Empire)移植到VMS环境。该反编译器可从因特网上免费获取 [Reu88]。

Decomp使用符号表来寻找函数的入口点，确定在程序中被使用的数据，和那个数据的名字。按以下方式，一次反编译一个子程序：基本块的一个控制流向图被建立并且最优化——去掉通向中间无条件分支的弧。在该图中做控制流分析，寻找高级控制构造，把控制流向图转换成一个一般化构造的树。这个分析使用的算法来自struct程序，这是一个把从Fortran程序产生的图进行结构化的程序，该算法是基于B.Baker在[Bak77]中描述的结构化算法。最后，在树里的一般化构造被转换成C语言特有的构造，而且产生代码。最后的输出程序需要手工修改，把参数放在子过程的参数列表上，以及确定一个子程序是否返回一个数值(亦即，一个函数)。这个反编译器大约花了5人月编写完成的 [Reu91]。

样本程序是在一个Vax BSD 4.2机器上用C编写并编译的，感谢Pete French的协作 [Fre91]，他给我提供了在一台Vax BSD 4.2机器上的一个账户。产生的C语言程序需要经过一些手工编辑才可以再编译。该程序有正确的控制结构——归功于他实现的结构化算法，也有变量的正确数据类型——归功于在目标代码中有嵌入的符号表。库例程和子过程的名字，以及用户程序入口点也都从符号表可知；因此，没有反编译外来的子过程 (例如，编译器启动代码、库例程)。数据流分析阶段是十分必要的，即使，既没有表达式和实际参数也没有函数返回值被确定。子过程间的数据流分析可以为再编译输出程序节省许多必需的手工编辑。

exe2c, 1990.

Austin Code Works赞助exe2c反编译器的开发，目标是运行DOS操作系统的PC兼容系列计算机 [Wor91]。该工程在1990年4月正式公告 [Gut90]，经过大约20人测试，并决定它在反编译成C语言代码上需要做更多工作。一年后，该工程达到一个β操作级[Gut91a]，但是再没有完成 [Gut91b]。我是它的一个beta版测试者。

exe2c是一个多趟反编译器，由三个程序组成：e2a、a2aparse和e2c。e2a是反汇编器。它将可运行文件转换成汇编程序，而且也产生一个有注解的汇编列表。a2aparse是汇编语言到C语言的前端处理器，它分析由e2a产生的汇编程序文件，而且生成.cod文件和.glb文件。最后，e2c程序翻译由a2aparse准备的文件并且生成伪C语言程序(pseudo-C)。还提供一个集成环境envmnu。

exe2c反编译的程序使用一个定义寄存器、类型和宏的头文件。输出的C程序依靠寄存器和条件码 (表现为布尔变量)，所以不易理解。通常，一个机器指令被反编译成一个或多个需要对寄存器进行操作和设置条件码(如果指令需要的话)的C指令。没有确定表达式和子程序参数，而且最后的C程序使用一个局部栈。从这个输出代码看出，显然在exe2c中没有实现数据流分析。这个反编译器已经实现一个控制流分析阶段；可以应用循环构造和条件构造。尽管case表没有被正确地检测，控制结构的选择一般来说还可以的。被反编译的子过程的类型和数目说明，在程序中发现的所有库例程以及编译器启动代码和运行时支持例程都被反编译。这些例程的性质通常是低级的，因为它们通常是用汇编语言编写的。大多数情况下，这些例程很难被反编译成对应的高级代码 (除非低级类型的C代码)。

这个反编译器是在许多年中反编译可运行文件的第一个努力。其成果说明，为了产生更好的C代码需要做一个数据流分析和启发式。而且，建立一个忽略所有由编译器引进的外来代码和发现库子程序的机制会很有帮助。

PLM-80 Decompiler, 1991.

澳大利亚国防部的信息技术司研究反编译的国防应用，比如废弃代码的维护、科技情报产品、以及安全或保密风险的系统评估。S.T. Hood在[Hoo91]中描述了这个工作。

它描述了建造反编译器的技术，在一个Prolog环境中使用有限子句语法(definite-clause grammar)——一个扩展的无语境语法。一个Prolog数据库被用来储存最初的汇编代码和该语法的公开句法结构。反编译器样机是用Prolog语言编写的，它适用于由PLM-80编译器编译的Intel 8085汇编程序。该反编译器产生Small-C语言的目标程序，它是C语言的一个子集。在这个报告中给出的有限子句语法能够识别if..then类型结构，和while()循环，以及简单类型 (即，字符型、整型和长整型) 的静态 (全局) 变量和自动 (局部) 变量。有一个图形化用户界面用于显示汇编程序和伪C程序，而且让使用者能够赋予变量名字和加注解。这个界面也向使用者询问主程序的入口点，并且允许他选择要识别的控制构造。

这个反编译器所做的分析局限于控制结构和简单数据类型的识别。没有对寄存器的使用做分析或提及。自动变量用一个索引变量表示——它表现栈。图形化界面帮助用户以注解和有意的变量名字的形式给反编译的程序增加说明。这个分析不支持被最优化的代码。

Decompiler compiler, 1991-1994.

反编译器编译器是一个工具，以一个编译器规约和目标代码的相应部分作为输入，并且返回一个反编译器的代码；即，它是一个制造反编译器的自动方法，跟yacc制造编译器的方法有很多相同之处 [BBL91, BB91, BB94]。

描述了产生这个反编译器编译器的两种方法：一个逻辑程序设计方式和一个函数型程序设计方式。前一方式利用逻辑程序设计语言比如Prolog语言的双向性，而且逆推编译器规约以获得一个反编译器 [BBL91, BB91, BBL93]。理论上，这是正确的，但是在实践中这个方法中受制于Prolog解释程序的实现，因而遇到严密性和可反转性问题 [BB92,BB93]。后一方式基于逻辑方式但是lazy函数型程序设计语言如Haskell语言，产生一个比较有效率的反编译器 [BBL91, BB91, BBL93]。即便使用一个non-lazy函数型语言，也可以用对象的形式模拟lazy而不是用列表的形式。

由一个反编译器编译器产生的反编译器将以目标代码作为输入，并且返回可以被编译成给定目标代码的一个源代码列表。为了实现它，需要针对一个任意的遗传属性语法 (inherited attribute grammar) 枚举所有可能的源代码。事实证明，这样的一个枚举等价于停机问题 [BB92, BB93]，因而是不可计算的。更有甚者，没有一个可计算的方法读入一个属性语法描述并且决定，对于该属性的一个给定值，被编译的代码是否将给出一个有尽的枚举 [BB92, BB93]，所以关于哪些语法可以使用并不是直接明了的。因此，这个方法可接受的语法类别必须局限于那些能够产生一个完全枚举的语法，比如非左递归的语法。

这个方法的首次实现是使用一个函数型程序设计语言、为一个类似Occam语言的子集。反编译器语法是一个遗传属性语法，把有意选择的目标代码作为一个参数 [BB92,BB93]。也基于编译器规约描述了一个Prolog语言反编译器。这个反编译器以一个选择性的和顺序的方式应用编译器的条款(clause)，以免遭遇不终止的问题，而且只返回源代码程序的一个子集 (而非一个无穷的列表) [Bow91, Bow93]。由于函数型方式和逻辑方式的低效率，这个方法最近使用命令式程序设计语言C++了。在这个样机中，C++对象被当作lazy列表使用，而且编写了一组库函数来实现所使用的中间表示法的操作码 [BB94]。关于优化代码的问题已经被检测。

正如这项研究举例说明的，只要知道编译器规约集合和为规约的每个条款产生的目标代码，反编译器编译器就能够自动地被建造。一般来说事情没这么简单，因为编译器作者不公开他们的编译器规约。只有用户定义的编译器和反编译器可以用这个方法建造。也要注意，这个方法不处理被编译器最优化阶段产生的最优化，而且用这个方法建造的反编译器不能反编译真实的可执行程序。没有解决数据和指令的区分问题，以及确定在可运行程序中所使用的变量的数据类型的问题。总之，如果已经了解某个编译器产生的目标代码，那么就能够自动生成它的反编译器编译器，但是所生成的反编译器不能反编译任意的可执行程序。

8086 C Decompiling System, 1991-1993.

这个反编译器以来自DOS环境的可运行文件作为输入，而且产生C语言程序。输入文件必须是用Microsoft C 5.0版本小存储器模型编译的 [FZL93]。描述了五个阶段：库函数的识别、符号执行、数据类型识别、程序转换和C语言代码生成。库函数的识别和中间语言在[FZ91,HZY91]中有更多描述。

Microsoft C的库函数的识别用来清除属于库的子程序，如此只为使用者例程产生C语言代码。反编译系统有一个内建的C语言库函数表。为每一个库函数，储存它的名字、特征代码 (该函数中区别于其它函数的指令序列)、在特征代码中的指令数目，以及识别该函数的方法。这是反编译器作者手工制作的。符号执行将机器指令翻译成中间指令，而且用它的符号内容表示每个指令。数据类型的识别是通过一组对于不同数据类型的信息收集规则和用于确定数据类型的分析规则完成的。程序转换把存储计算转变成地址表达式，例如数组寻址。最后，C语言代码生成器通过发现控制结构而转换程序结构，并且生成C语言代码。

这个反编译系统利用库函数识别产生更可读的C语言程序。库函数通过手工识别，因此，如果用编译器的其它版本、其它存储器模型或其它编译器编译最初的程序，识别工作效率较低。数据类型的识别是对数组、指针和结构类型识别的第一次尝试，但是这篇文章里没有详细说明。没有描述如何在中间代码中构成一个地址表达式，而且没有举例说明最后C程序的质量。

Alpha AXP Migration Tools, 1993.

在数字装备公司(Digital Equipment Corporation)设计Alpha AXP体系结构的时候，AXP团队参与一个工程项目——在新的Alpha AXP计算机上运行现有的VAX和MIPS代码。他们选择了一个二进制翻译器，能把旧体系结构的指令序列转换成新体系结构的指令序列。此过程需要完全自动化，而且需要照顾到在执行期间创建或修改代码的需要。移值过程定义为两个部分：二进制翻译和运行时环境 [SCK+93]。

二进制翻译阶段读入二进制程序并且把它们翻译成AXP操作码。它使用反编译技术了解机器指令的潜在含义。进行条件码使用分析，因为这些条件在Alpha体系结构上不存在。并通过分析代码，确定函数返回值以及寻找bug (例如，未初始化的变量)。MIPS有标准的库例程——嵌入在二进制程序中。对于这种情况，一个模式匹配算法被用来检测例程是否库例程——这些例程不被分析而是用它们的名字代替。寻找成语并且用一个最理想的指令序列代替它。最后，代码以AXP操作码的形式生成。新的二进制文件包含新代码和旧代码二者。

运行时环境执行翻译的代码而且做为在新旧操作系统之间的一个桥梁 (例如，不同的调用标准、异常处理)。它有一个内建的旧代码解释程序，用来运行在翻译时没有发现或不存在的旧代码。这是可以的，因为旧代码也作为新的二进制文件的一部分被保存。

编写了两个二进制程序的翻译器：VEST从OpenVMS VAX系统翻译到OpenVMS AXP系统，和mx将ULTRIX MIPS映像翻译成DEC OSF/1 AXP映像。这些翻译器的运行时环境分别是TIE和mxr。

这个工程举例说明在一个现代翻译系统中反编译技术的使用。证明对于众多类别的二进制程序来说它是成功的。一些无法翻译的程序是在技术上不可翻译的程序，比如使用特权操作码的程序或者以超级用户特权运行的程序。

Source/PROM Comparator, 1993.

这是英国核电子股票上市公司开发的一个工具，论证源代码和PROM内容的等价，用于验证从PL/M-86程序到PROM程序的正确翻译，这些程序要在安全关键的计算机控制系统上运行 [PW93]。

可以被看成三阶段：从PROM文件再组织目标代码文件，借助从源代码建立的一个名字表把目标代码反汇编成一个类似汇编形式，以及汇编程序的反编译并且与最初源代码进行比较。注意，在反编译阶段必须清除中间跳转、寄存器和栈操作，识别子过程参数，解决结构、数组和指针的索引，而且将表达式转换成一个规格形式。为了比较最初的程序和反编译的程序，使用了一个中间语言。使用一个商业产品把源程序翻译成这个语言，而且反编译阶段的输出也是用相同的语言编写。该工程证明，对于被分析的这个特定的安全系统而言，它是验证翻译代码正确性以及论证那些用来创建程序的工具 (编译器、链接器、优化器) 行为可靠性的一个实际方法。

这个工程描述了反编译技术的一个应用，帮助论证在一个安全关键的系统中高级代码和低级代码的等效。借助从最初源程序构造的一个符号表，反编译阶段进行许多分析。通过对编译高级程序所使用的编译器的了解，任务得以简单化。

在去年内，商业的特定厂商反编译器已经被制造出来。这些反编译器针对数据库语言，生二进制文件的反编译，比如Clipper和FoxPro。它们的厂商没有提供用于反编译这些程序的技术信息。下面列表提到其中一些商业的反编译器：

Valkyrie, 1993.

用于 Clipper Summer '87 的可视化反编译器，CodeWorks 制造 [Val93]。

OutFox, 1993.

用于加密的FoxBASE+程序的反编译器 [Out93]。

ReFox, 1993.

反编译加密的FoxPro文件，Xitech公司制造 [HHB+93]。

DOC, 1993.

用于 AS/400 和 System/38 的COBOL反编译器。把目标文件程序转换成COBOL源程序，然后可以由程序员修改。Harman Resources 制造 [Cob93].

Uniclip, 1993.

用于 Clipper Summer '87 EXE 文件的反编译器，Stro Ware 制造 [Unc93]。

Clipback, 1993.

用于Summer '87 可执行文件的反编译器，Intelligent Information Systems 制造 [Unc93]。

Brillig, 1993.

用于 Clipper 5.X .exe文件和.obj文件的反编译器，APTware 制造 [Bri93]。


第 3 章 运行时环境

在考虑反编译之前，先讨论一下程序的静态二进制代码和在运行时间实现它的动作二者之间的关系。二进制程序的对象表示法因编译器不同而不同；基本数据类型比如整数型、字符型、和实数型，常常用机器上一个等价的数据对象表示 (即，其尺寸字节数固定)，而聚集对象比如数组、字符串和结构的表示则各不相同。

这个论文通篇把子程序(subroutine)这个词作为一个有普遍意义的词，意指一个子过程或函数；后面这两个词只在能够确定子程序是什么的时候才使用，也就是说，一个返回数值的子程序是函数(function)，而一个不返回数值的子程序是子过程(procedure)。

3.1 存储组织

一个高级语言程序由一或多个子程序组成，这些都是用户子程序。对应的二进制程序由用户子程序、被用户程序调用的库例程、和被链接器链接的其他子程序 (给编译器提供运行时支持) 组成。一个程序的二进制代码一般格式见图3-1所示。该程序的启动是通过调用编译器启动子程序建立它的运行环境；随后是用户主程序，它调用被链接器链接的库例程；并且在程序终止之前是由一系列编译器子程序恢复机器状态而完成。

启动代码

用户程序

(包括库子程序)

出口代码

图 3-1: 二进制程序的一般格式

例如，一个“hello world”C程序，用Borland Turbo C v2.01编译以后，有超过25个不同的子程序。启动代码调用多达16个不同的子程序来建立编译器的环境。用户的主程序是由一个子过程组成。这个子过程调用printf()子过程，进而调用多达8个不同的子程序来显示格式化的字符串。最后，出口代码调用3个子程序恢复环境并且退回DOS。这个程序的代码构架样本见图3-2所示。

helloc | proc far |
 | mov dx,DGROUP | ; dx == GROUP segment adr
 | mov cs:DGROUP@@,dx |
 | ; save several vectors and install default divide by zero handler |
 | call SaveVectors |
 | ; calculate environment size, determine amount of memory needed, |
 | ; check size of the stack, return to DOS memory allocated in excess, |
 | ; set far heap and program stack, reset uninitialized data area, |
 | ; install floating point emulator |
 | push cs |
 | call ds:[__emu1st] |
 | ; prepare main arguments |
 | call _setargv@ |
 | call _setenvp@ |
 | ; initialize window sizes |
 | call ds:[__crt1st] |
 | ; invoke main(argc,argv,envp) |
 | push word ptr environ@ |
 | push word ptr _argv@ |
 | push word ptr _argc@ |
 | call main@ | ; user's main() program
 | ; flush and close streams and files |
 | push ax |
 | call exit@ |
helloc | endp |

图 3-2: 一个“hello world”程序的代码构架

在一个二进制程序中，子程序是用它们的入口地址来标识的；子程序序没有与之关联的名称，而且在对这些子程序所定义和使用的寄存器做数据流分析之前，我们并不知道某个子程序是一个子过程还是一个函数。我们说，调用另一个子程序的子程序是调用者(caller)，而被调用的子程序是被调用者(callee)。

3.1.1 栈框架

每一个子程序在运行时期间都关联一个栈框架。栈框架是参数、局部变量和返回地址 (调用者子程序中的地址) 的集合，如图3-3所示。在栈框架中参数表现子程序的一次特定调用的实际参数：在二进制文件中没有储存子程序形式参数的有关信息。栈标记(stack mark)表示调用者里的返回地址 (以便在被调用者完成以后把控制传递给调用者)，而调用者的框架指针 (在Intel体系结构中是寄存器bp) 是在栈框架里面各偏移量的参照点。局部变量表现当子程序取得控制以后由它分配的空间；只有对于活动的 (即，未终止的) 子程序来说这个空间才是可用的。

图 3-3: 栈框架

当框架指针 (即寄存器bp) 已经被设置以后，从框架指针起的正偏移存取参数和栈标记，而负偏移存取局部变量。在与栈架框有关的图中使用如下约定：在Intel体系结构里，栈从高内存向下往低内存增长。

栈框架也可能包含其它域，如图3-4所示。这些域不被所有语言或所有编译器使用的 [ASU86a]。返回值域在一些语言中给被调用者使用，以便函数返回数值给调用者；为了效率，更多地是在寄存器中返回这些数值。控制链指向调用者的栈框架，而存取链指向一个正在关闭的子程序的栈框架，其中保存它可以存取的非局部数据。

3.2 数据类型

数据对象通常被储存在连续相邻的内存单元里。基本的数据类型，比如字符型、整型、和长整型，可以被放在寄存器中来操作。聚集数据类型，比如数组、字符串和记录，则不能将其整体放在寄存器中，因为它们的大小通常超过寄存器的大小，所以通过一个指向它们起始地址的指针来存取它们更容易。

返回值

参数

控制链

存取链

栈标记

局部变量

图 3-4: 栈框架

关于i80286体系结构的不同数据类型的大小见图3-5所示。它的机器字长是16位。大小以8位字节为单位。

数据类型 | 大小(字节)
字符型 | 1
整型 | 2
长整型 | 4
实型 | 4
长实型 | 8
近指针 | 2
远指针 | 4
其它类型 | >=1

图 3-5: 在i80286中各种数据类型的大小

3.2.1 高级语言的数据管理

对于聚集数据类型，不同的编译器有不同的管理方式。这一节依据参考文献[Mic87]，描述C、Pascal、Fortran和Basic语言编译器所使用的不同格式。

数组

数组是连续相邻的一片内存，它保存一个或多个特定类型的数据项。根据特定语言所规定的顺序，数组在内存里用一些行或列来实现：

l 行为主顺序：多维数组的元素按行顺序存储；即，一行接一行。C语言和Pascal语言编译器使用这种顺序。

l 列为主顺序：多维数组的元素以列顺序存储，而不是按照行顺序。Fortran语言和Basic语言编译器使用这种顺序。有些Basic语言编译器有一个使用以行为主顺序的编译选项。

对于大多数语言，数组的大小在编译时是已知的；比如C、Pascal和Fortran就是这样的。Basic允许运行时声明数据大小，因此一个数组需要有一个数组描述符，用来保存数组的大小和一个指针指向数组储存在内存里的实际位置。

字符串

字符串是一个字符序列。不同语言使用不同的表示法表现一个字符串，如下：

l C语言格式：一个字符串是一个以空字符(null, 即0)结束的字节数组。

l Fortran语言格式：一个字符串是在一个固定不变的存储单元上的字节序列，因此在字符串结束处不需要定界符。

l Pascal语言格式：一般的Pascal编译器有两种类型的字符串：STRING和LSTRING。前者是一个固定长度字符串，与Fortran格式一样。后者是一个长度可变的字符串，是一个字符数组，该数组的第一个字节保存该字符串的长度。标准的Pascal语言没有STRING或LSTRING类型。

l Basic语言格式：一个字符串用一个4字节的字符串描述符实现；开头两个字节保存字符串的长度，后两个字节是一个默认数据区的相对偏移量，在那里保存字符串。这个区域由Basic的字符串空间管理例程分配，因而其位置在内存里是不固定的。

记录

记录是连续相邻的一片内存，它保存一个或多个数据类型的相关数据项。在不同的语言里记录有不同的名称：在C语言里叫struct，在Pascal语言里叫record，在Basic语言里叫用户定义类型。默认地，C语言和Pascal语言储存结构体的方式除了字节单位尺寸的对象以及这些对象的数组，而且是无压缩存储、字对齐(word-aligned)。Basic语言和某些C语言以及Pascal语言的编译器储存结构体的方式是压缩存储。

复数

Fortran复数数据类型按照以下方式存储浮点数：

l COMPLEX*8：4个字节表现实数部分，其它4个字节表现虚数部分的浮点数。

l COMPLEX*16：8个字节表现实数部分，其它8个字节表现虚数部分。

布尔值

Fortran的LOGICAL数据类型按照以下方式存储布尔信息：

l LOGICAL*2：一个字节保存布尔值(0或1)，另一个字节保留未使用。

l LOGICAL*4：一个字节保存布尔值，其它三个字节保留未使用。

3.3 高级语言界面

高级语言的编译器为了允许混合语言编程而使用一些约定，以便一个程序的某些子程序可以用某一种语言编写，而另一些子程序则可以用另一种不同的语言编写，而且所有这些子程序都被链接到同一个程序中。这一序列的约定关系到栈框架的建立方式以及调用子程序的调用约定。

3.3.1 栈框架

栈标记包含有调用者里的返回地址和框架指针。根据对被调用者的调用是近调用还是远调用，返回地址的尺寸是不同的。近调用是在相同的段里面，因此可以通过一个当前段基地址的相对偏移量引用。远调用是在不同的段里面，所以两个段和被调用者的相对偏移都被储存。对于机器字长是两字节的体系结构来说，近调用以两字节储存调用者的相对偏移，而远调用以四字节储存段和调用者的相对偏移。寄存器bp被用作为框架指针，而调用者的框架指针内容被推入栈中，以便可以在子程序结束时恢复它。

进入子程序

先把寄存器bp的值推入栈顶 (即，把调用者的框架指针储存在栈上)，然后把当前堆栈指针寄存器(sp)复制给bp，这样bp就被设置为框架指针了。下面的代码在i80286体系结构中被使用：

push bp | ; save old copy of bp
mov bp, sp | ; bp == frame pointer

分配局部数据

子程序可能在栈上给局部变量预留空间。其做法是，把栈寄存器sp的值减去一个恰好的字节数。例如，为了给两个整型变量分配空间，在栈上预留4字节：

sub sp, 4

保存寄存器值

对于DOS编译器，调用约定普遍要求子程序应该总是保存一些寄存器si、di、ss、ds和bp的数值。即使其中任何一个寄存器在被调用者子程序中被使用，因为它们的值都已被入栈了，所以能够在子程序返回之前恢复它们。举例说明，假如一个子程序使用了si和di，下面的代码会在局部数据分配之后出现：

push si

push di

存取参数

参数是位于相对框架指针寄存器bp所指位置的正偏移。若要存取某参数n，那么它相对bp所指位置的偏移就等于是将栈标记的尺寸加上在bp所指位置和参数n之间那些参数的尺寸和，再加上参数n的尺寸即可。

返回数值

使用寄存器返回数值的函数根据返回值的尺寸使用不同的寄存器。一字节的数据值在al寄存器中返回，两字节的在ax寄存器中返回，而四字节的在dx:ax寄存器中返回，如图3-6所示。

数据大小(字节) | 寄存器
1 | AL
2 | AX
4 | DX = high byte, AX = low byte

图 3-6: 用于返回数值的寄存器约定

更大的数据数值使用下面的约定返回：

l 被C语言调用的函数：被调用者必须从堆中为返回值分配空间，并且将其地址放入dx:ax。

l 被Pascal、Fortran或Basic调用的函数：调用者在栈段中为返回数值预留空间，并且把所分配空间的偏移地址入栈作为最后一个参数。因此，对于远调用，返回值的偏移地址是在bp+6，而对于近调用则是在bp+4，如图3-7所示。

退出子程序

恢复栈框架即，弹出那些在子程序入口处保存的任何寄存器、释放为局部变量预留的任何空间、恢复旧框架指针(bp)，并且依照使用的约定返回。

l C语言约定：由调用者为入栈的任何参数平衡栈。结束子程序只须一条ret指令即可。

l Pascal、Fortran、Basic约定：由被调用者平衡栈，即从栈上削减参数所需字节数。使用一条ret n指令即可，其中n是参数的字节数。

图 3-7: 返回数值约定

例如，下面的代码从栈中恢复寄存器di和si，通过复制bp给sp来释放局部变量的空间，通过从栈中弹出bp来恢复框架指针，并且使用C语言约定返回：

pop di | ; restore registers
pop si |
mov sp, bp | ; deallocate local variables
pop bp | ; restore bp
ret |

3.3.2 参数传递

在Intel体系结构上的DOS操作系统下，传递参数有三个不同的方法：C语言调用约定、Pascal语言调用约定和寄存器调用约定。在其它操作系统和体系结构中可能混合使用这些调用约定。例如，在OS/2里，标准调用是按照C语言的参数传递次序，但是由被调用者从系统调用的栈上削减参数。

C语言调用约定

调用者负责将参数入栈，并且负责在被调用者返回以后恢复它们。参数是按照从右到左的次序入栈，以便向被调用者传递可变数目的参数。例如，对于一个C语言函数原型 void procX (int, char, long)，以及一个调用procX()子过程的调用者子过程：

procN()

{ int i; /* bp - 8 */

char c; /* bp - 6 */

long l; /* bp - 4 */

procX (i, c, l);

}

将会产生下面的汇编代码：

push word ptr [bp-2] | ; high word of l
push word ptr [bp-4] | ; low word of l
push [bp-6] | ; c
push word ptr [bp-8] | ; i
call procX | ; call function
add sp, 8 | ; restore the stack

注意，由于字对齐，尽管字符c的尺寸只有一字节，储存它还是占了栈上的两个字节。

Pascal语言调用约定

调用者负责把参数入栈，而被调用者在返回之前平衡栈。参数按照从左到右的次序入栈，因此在这个约定里使用固定数目的参数。对于前面的例子，在Pascal语言约定里，procX (i, c, l)的调用产生如下汇编代码：

push word ptr [bp-8] | ; i
push [bp-6] | ; c
push word ptr [bp-2] | ; high word of l
push word ptr [bp-4] | ; low word of l
call procX | ; call procX (procX restores stack)

寄存器调用约定

这个约定不把参数入栈，而是用寄存器传递它们，因此所生成的代码执行速度比较快。在子程序之间使用预定的寄存器来传递参数，而按参数的尺寸不同使用不同的寄存器。图3-8是Borland Turbo C++ [Bor92] 所使用的寄存器集；最多三个参数可以用寄存器传递。远指针、联合(union)、结构和实数则被入栈。

参数类型 | 寄存器
字符型 | al, dl, bl
整型 | ax, dx, bx
长整型 | dx:ax
近指针 | ax, dx, bx

图 3-8: 寄存器参数传递约定

3.4 符号表

反编译器用符号表存储在程序中使用的变量的有关信息。在二进制程序中变量是用变量的地址来标识；变量没有与之关联的名称。全局变量是有实际内存地址的变量；通过它们的段和偏移量来存取。相对于框架指针，位于负偏移上的变量是与这个栈框架对应的子程序的局部变量，位于正偏移上的变量是该子程序的实际参数。由于编译器为了效率的缘故使用寄存器变量，因此起初先把所有寄存器看作变量；再对寄存器做进一步分析以确定它们是否果真代表寄存器变量 (见第5章第5.2.9节)。在代码生成期间赋予变量唯一的名称，见第7章说明。

符号表必须能够有效地提供一个项目的有关信息，并且管理数目可变化的变量；因此，符号表可在必要时动态生长。符号表的性能测量是通过存取一个项目和插入一个新的数据项到表中。

3.4.1 数据结构

符号表用多种数据结构表现。有些实现是以编写更多的代码为代价来获得更高的效能。为了通过例子说明各种数据结构之间的差别，我们假设要把下列数据项放进符号表：

cs:01F8 | ; 全局变量
bp + 4 | ; 参数
bp – 6 | ; 局部变量
ax | ; 寄存器ax
bp – 2 | ; 局部变量

无顺序列表

无顺序列表是一个数据项链表或者是一个数据项数组。数据项被储存在一个基于先入的列表 (即，放在下一个可用位置)。数组实现受数组尺寸的限制；链表实现则没有这些限制。这个符号表是一个有n个数据项的列表，它的存取时间复杂度是O(n)。图3-9是我们例子的列表。

图 3-9: 无顺序列表表示法

有顺序列表

有顺序列表比较容易存取，因为确定一个数据项是否已经在列表中不需要检查该列表的全部数据项。有顺序列表可以使用二分法搜索，其存取时间复杂度是O(log n)。但是，插入一个数据项花费代价较大，因为列表需要保持有顺序。

因为在一个二进制程序中有不同类型的变量，而这些数据项是根据其类型以不同的方式来识别，同一种类型数据项的排序是可以的，但是四个不同类型就必须独立存取。图3-10是我们例子的有顺序列表表示法：首先用一个记录指定所使用数据项的数据类型，然后每一个数据类型关联一个有顺序的列表。

图 3-10: 有顺序列表表示法

散列表

散列表是在一个表的固定数目位置和可能很大数目的变量之间的映射。这个映射是通过一个散列函数 (为所有可能的变量定义这个函数) 完成的，能够快速地计算，为所有变量提供一致的概率，而且把相似的变量映射到表中不同的随机位置。

在打开的散列中，散列表表现为一个固定大小的数组而且每个数组位置(bucket)附接着一个链表。链表保存那些经过散列产生相同bucket的不同变量。图3-11是我们例子的散列表；对于有顺序列表，首先用一个记录指定变量的类型，然后每个不同的变量类型关联一个散列表。

图 3-11: 散列表表示法

适用于反编译的符号表表示法

为了适合反编译目的，将上述方法配合使用。根据不同类型的变量定义符号表：全局的、局部的、参数和寄存器。其中每一种类型均以不同方式实现。对于全局变量，因为它们的地址范围大，所以用散列表实现最合适。对于局部变量和参数，由于这些变量是相对框架指针的偏移量，而且总是以顺序方式分配的 (即，在栈框架中没有“间隙”)，所以它们的实现是一个关于偏移量的顺序列表；不需要把寄存器bp存入，因为它总是相同的。最后，因为寄存器数目是固定的，所以寄存器的实现是一个以寄存器编号作为索引的数组；数组位置有一个关联的数据项，表示在符号表中所定义的寄存器。这个表示法见图3-12所示。

图 3-12: 符号表表示法

有关讨论符号表的文献非常多，有关从编译器角度讨论符号表的资料可参考[ASU86a, FJ88a, Gou88]。


第 4 章 前端 - 句法分析

第 4 章 前端

前端是一个机器依赖的模块，它把二进制源程序作为输入，对程序进行句法分析，然后产生该程序的控制流向图和中间语言表示法作为输出。前端的几个阶段如图4-1所示。

图 4-1: 前端的几个阶段

与编译器不同，在反编译器的前端里没有词汇分析这个阶段。词汇分析或扫描程序是把输入流字符组合成语言符号(token)的过程。已有专门化工具比如lex和scanGen用来帮助编译器扫描程序自动化处理句法结构 [FJ88a]。由于机器语言的简单性，不需要通过扫描输入字节来识别这种语言拥有的各种单词；所有信息是按照字节或位存储的，而且如果离开上下文，就不可能确定某一个字节表示什么 (即，操作码、寄存器、偏移量)。句法分析程序基于机器语言的语言描述确定一个字节序列所代表的东西。

4.1 句法分析

句法分析程序是反编译器的第一个阶段。它所扮演的角色是把一个字节序列组织成该语言的一个短语或语句。检查这个字节序列的句法结构，即该语言拥有的字符串。有效的字符串用一个语法分析树表示出来，做为下一个阶段 (语义分析程序)的输入。句法分析程序与语义分析程序之间的关系见图4-2所示。语法分析器(parser)即句法分析程序(syntax ananlyzer)。

图 4-2: 语法分析器和语义分析程序之间的交互

机器语言的句法能够用语法准确地加以规定。在机器语言中，只规定了指令或语句；而没有象高级语言那样规定控制结构。一般来说，任何语言规定都是通过语法做出准确的注解。

反编译器语法分析器的主要困难是区分代码和数据，亦即，确定内存里哪些字节是代码、哪些字节是数据。这个问题源自冯·诺依曼体系结构的先天不足，因此需要用启发式 (试探方法) 来解决。

句法错误

二进制程序中几乎不会出现语法错误，编译器总是为被编译的程序生成能够在机器上正确运行的代码。但是，假如机器体系结构升级产生新的机器——它支持所有前任机器，新体系结构的机器指令集是旧体系结构机器指令集的一个扩展。i80486就是如此，它支持所有前任i8086、i80186、i80286和i80386的指令集。因此，如果为i8086编写一个语法分析器，那么它不能识别所有新的机器指令从而必定造成一个错误。反之，如果语法分析器是为最新的机器i80486编写的，那么所有的指令应该都会被识别，可能不会遇到语法错误的情况。

4.1.1 有限状态自动机

有限状态自动机(FSA)是一个语言识别程序。输入一个字符串，如果对于该语言是有效的它回答yes，否则回答no。字符串即是某个给定字母表的一个符号序列。任意给定一个字符串，FSA能够确定它是不是该语言的有效字符串。

定义1

一个有限状态自动机是一个数学模型，它由以下组成：

l 一个有限状态集S

l 一个初始状态s0

l 一个最终状态或接受状态集F

l 一个输入符号字母表∑

l 一个转变函数T：state×symbol → state

FSA可以用转变图(transition diagram)来图形化地表现。这些图的组成如图4-3所示。字母表符号标示了状态的转变。在转变图中没有显式地表现错误的转变，而是以任何非有效符号标签隐含地表现从一个状态到一个错误状态的转变。

图 4-3: FSA转变图的组成

通配符语言是一个元语言，用于在一个语言中指定通配符条件 [Gou88]。两个元符号是‘*’和‘%’。元符号‘*’代表任何由零个或者更多个字母表∑的符号组成的序列，‘%’代表任何一个单一的字母表符号。

例子 1

令 ∑=f {a, b, c}。以通配符表达式a*说明，该语言接受任何以a开头的字符串。用一个FSA表示如下：

非确定性的有限状态自动机

每当一个状态有两个或多个转变是用相同的符号标示的，或者当空字符串(ε)标示一个转变的时候，那么FSA就是非确定性的 (NFSA)。此时，无法用一个 (state, symbol) 元组唯一地标识下一个状态。

确定性的有限状态自动机

一个确定性的有限状态自动机 (DFSA) 是一个没有用ε字符串标示其转变的FSA，能够唯一地标识或确定一个 (state, symbol) 元组的下一个状态。

任何NFSA可以通过构造子集的方法转换成一个等价DFSA。这个方法在文献中已经有说明，详细请看参考文献[ASU86b, Gou88, FJ88a]。另外，构造一个最小状态DFSA的方法见参考文献[ASU86b]。

4.1.2 有限状态自动机和语法分析器

任何机器语言都能用一个FSA (即，接受或拒绝任意字符串的识别程序) 表现。字母表∑是一个十六进制数字00..FF的有限集 (即，数字用一个字节表示)，一个字符串就是一个字节序列。那些它能够认识的机器语言指令就是该语言的有效字符串。

例子 2

对于一条i80286机器指令

83E950 ; sub cx, 50

识别它的FSA首先需要确定83是一个操作码(sub)、这个操作码有两个字节或更多字节的操作数。第二个字节编码目标寄存器操作数 (较低的3位)，以及指明在这个字节之后用多少字节表示其它信息：如果较高两位是0或2，那么在第二字节后面的两个字节也是目标操作数的一部分；如果这两位的值是1，那么在第二字节后面的一个字节是目标操作数的一部分；否则，该目标操作数没有使用更多字节。在我们的例子中，较低的三位等于1，即是寄存器cx，而较高两位是3，说明目标操作数没有使用更多字节。最后，最末尾的一个字节是立即常数操作数，在这个例子里是50。这个例子的FSA见图4-4所示。

图 4-4 : FSA例子

机器语言也可以用一个无语境语法(CFG)来描述；如，正则表达式是无语境语法的一个子集。在参考文献[ASU86a]中提出把NFSA机械地转换成CFG的一个算法。CFGs被用来描述原本有递归结构的高级构造，但由于机器语言没有使用递归构造，所以用CFGs定义递归不是必要的。

4.1.3 代码和数据的区分

句法分析程序的功能就是，从一个程序的入口点开始解析机器指令，跟随该程序所有可能的路径(路线)。语法分析器面临的主要问题是在一个冯·诺依曼机器里面数据和代码看上去没什么不同，因此不容易确定某一条指令后面的一些字节是属于另一条指令抑或是数据。这一节讨论从代码中确定数据所要用到的启发式。

当源二进制程序被装入内存以后，装载器返回该程序在内存里最初的开始地址。这个地址是完全的二进制程序的启动地址，因为程序要从这里开始运行，所以它一定是一条指令的地址。此外，如果已经从二进制程序查出编译器签名，就可以把程序main函数入口点作为最初的启动地址；亦即，略过所有的编译器启动代码，直接从相应源码的主程序开始地址开始。图4-5举例说明一个“hello world”程序的样本代码。由装载器返回的入口点是CS:0000，即是完整程序的入口点 (包括编译器启动代码)。由编译器签名分析器给出的入口点是CS:01FA，即主程序的开始地址。在这篇论文中，为了保持普遍性，我们都将假定入口点为编译器签名分析器给出的那个地址。这里叙述的方法对二者都适用，但是举例比较偏重后者。编译器签名的生成技术和检测技术请看第8章。

 | helloc | proc far |
CS:0000 | start: mov | dx,* |
CS:0003 | mov | cs:*,dx |
... | ... |  | ; start-up code
CS:011A | call | _main |
CS:011D | ... |  | ; exit code
 | helloc | endp |
... | ... |  |
 | _main | proc near |
CS:01FA | push | bp |
CS:01FB | mov | bp,sp |
CS:01FD | mov | ax,194h |
CS:0200 | push | ax |
CS:0201 | call | _printf |
CS:0204 | pop | cx |
CS:0205 | pop | bp |
CS:0206 | ret |  |
 | _main | endp |

图 4-5: 一个“hello world”程序的样本代码

R.N. Horspool和N. Marovac的一篇文章重点讨论了代码和数据的区分问题。该文提到这个问题等价于停机问题，因为冯·诺依曼体系结构是在运行时计算数据地址和分支目标地址，所以不可能区分数据和指令 [HM79]。该文中提出一个取得指令保存单元最大集的算法。在最初的问题上应用分支界限方法(branch and bound)做些修改，使之等价于从所有候选树中寻找一个最大树集的组合问题。这次实践发现该算法完全不适用。

实践证明，在密集机器指令集 (比如Intel体系结构) 中，由于几乎任何字节组合都是一个有效的机器指令，该算法不能工作，因而很难知道哪里是不是数据，所以很难确定代码边界。这个算法一个简单的反面例子可通过存放在代码段的一个case表来说明 (见图4-6)。在CS:0DDB上这条变址跳转指令之后，通过变址进入case表内，该表本身被定义从CS:0DE0开始，但是却由于它包含有效指令字节，而被该算法当作代码看待。在这个i80286代码例子中，0E相当于push CS，而2B相当于sub，它会把另一个字节当成变元，即这个例子中的0E，结果造成sub ax,[bp]，等等。因此所产生的代码是错误的。

CS:0DDB | jmp CS:0DE0[bx] |
CS:0DE0 | 0E2B | ; push CS
CS:0DE2 | 0E13 | ; sub ax,[bp]
 | ... |

图 4-6: 反面例子

这一节将提出一个不同方法，从一个已经装入内存的二进制程序指令确定代码。它提供启发式方法解决在代码段之间发现数据的特殊情况。

处理

如前述，从代码确定数据的过程是建立在程序的初始入口点是一条指令这个基础之上。从这条指令开始，沿着这条路径循序地解析出指令，直至控制流改变或者到达路径终点。在前一情况下，目标地址(es)成为新的内存入口点，因为在该地址上一定是一条有效指令使程序可以继续运行。在后一情况下，当前路径结束了，由于我们不能确定后面那些字节是代码还是数据，所以不再沿着这条路径扫描指令。

控制流的改变是由于跳转和子过程调用。条件跳转把控制流向一分为二：若条件为真，就跟随目标分支地址，否则跟随那个条件分支后面的地址。为了得到所有可能执行代码，两条路径都要被语法分析器跟随。无条件跳转把控制流向转移到目标地址；语法分析器只需要跟随这唯一的一条路径。子过程调用把控制流向转移到被调用的子过程，而且在它返回以后，再分析这个子过程调用的下一条指令。对于子过程没有返回的情况，这条子过程调用指令后面的字节不被分析，因为不能确定这些字节是什么 (代码或数据)。

每当遇到一个子过程返回指令或程序结束，也就到达路径终点了。程序的结束通常为一个规定的指令序列，它们让操作系统终止当前进程 (或者说，该程序)。这个指令序列因操作系统而不同，所以对于不同的源机器，这部分代码是不同的。不过，确定是否遇到程序结束 (即，程序完成或者停机) 不是等价于解决停机问题，因为所跟随的这条路径必然不是可执行程序会跟随的，也就是说，去往这条路径的分支条件永远不可能在程序运行期间变成真；例如，在无限循环中的程序。

例子 3

在Intel体系结构上，程序的结束是通过中断指令指明。终止一个程序有不同的方法，其中有一些方法利用程序段前缀，通常叫作PSP；关于PSP的更多信息请参考附录B。在DOS下面有7个不同方法终止一个程序：

1. 用返回代码终止进程：int 21h，功能号4Ch。这是在.exe文件中最普遍使用的方法。

2. 终止进程：int 20h。代码段cs必须指向PSP。这个方法通常用于.com文件，因为cs已经指向PSP段。

3. 热启动/终止向量：在PSP的偏移00h处包含int 20h指令。寄存器cs必须指向PSP段。

4. 返回指令:：在程序启动之前将返回地址入栈。当程序即将终结的时候，它返回在栈上这个地址。这个方法在CP/M操作系统中使用，因为热启动向量的地址在栈上。初始化DOS的.com程序利用这个技术。

5. 终止进程功能：int 21h，功能号00h。寄存器cs必须指向PSP。

6. 终止并且驻留：int 27h。寄存器cs必须指向PSP。

7. 终止并且驻留功能：int 21h，功能号31h。

确定一个子过程是否返回 (亦即，完成或停机) 很困难，因为该子过程可能利用自修改代码或者把数据当作代码来执行、并且在这个数据里面的某一条指令上终止。一般来说，我们对正常情况下的解决方案感兴趣，因为不正常的情况需要通过单步调试和用户输入来解决问题。如果一个子过程到达程序终点或者调用了另一个到达程序终点的子过程，那么它是不返回的 (例如，一个调用exit(.)的C语言子过程)。有可能通过模拟程序终止指令序列的有关寄存器值来确定一个子过程是否已经到达程序终点。在例子3这些情况下，就要跟踪寄存器ah而且多数情况还要跟踪寄存器cs。

最初的区分代码和数据的算法见图4-7所示。为了掌握寄存器信息，用一个 machState 记录寄存器值。使用一个 state 变量 (这个记录类型的变量) 保存寄存器的当前值 (亦即，机器的当前状态)。使用一个位映像图(点阵)储存每一个被装入内存的字节的有关信息，每一个内存字节用两个位元表现：

0：代表一个未知的数值 (亦即，内存单元还没有被分析)。

1：代表一个代码字节。

2：代表一个数据字节。

3：代表一个字节，该字节既用作数据也作为代码。

该算法以递归方式实现。每当需要跟随一个非向下直通的(non fall-through)路径的时候，就复制一份当前 state 的拷贝并且用这个状态拷贝来递归调用parse子过程、继续跟随该路径。

procedure parse (machState *state)

done = FALSE;

while (! done)

getNextInst (state, &inst);

if (alreadyParsed (inst)) /* check if instruction already parsed */

done = TRUE;

break;

end if

setBitmap (CODE, inst);

case (inst.opcode) of

conditional jump:

*stateCopy = *state;

parse (stateCopy); /* fall-through */

state->ip = targetAdr (inst); /* target branch address */

if (hasBeenParsed (state->ip)) /* check if code already parsed */

done = TRUE;

end if

unconditional jump:

state->ip = targetAdr (inst); /* target branch address */

if (hasBeenParsed (state->ip)) /* check if code already parsed */

done = TRUE;

end if

procedure call:

/* Process non-library procedures only */

if (! isLibrary (targetAdr (inst)))

*stateCopy = *state;

stateCopy->ip = targetAdr (inst);

parse (stateCopy); /* process target procedure */

end if

procedure return:

done = TRUE; /* end of procedure */

move:

if (destination operand is a register)

updateState (state, inst.sourceOp, inst.destOp);

end if

interrupt:

if (end of program via interrupt)

done = TRUE; /* end of program */

end if

end case

end while

end procedure

图 4-7: 最初的句法分析算法

间接寻址模式

间接寻址模式利用一个寄存器或者内存单元的值确定一条指令的目标地址 (该指令使用这个寻址模式)。间接寻址模式能够用于无条件跳转 (例如，实现变址的case表) 和子过程调用指令。这个寻址模式的主要问题是内存值可以在程序运行期间被改变，因而对程序做静态分析不能确定内存单元是否已经被修改从而无法求出正确的值。对于寄存器值也是同样的，除非一直模仿寄存器的内容，然而很可能在某个循环中使用寄存器，因此需要虚拟执行循环。

在i80286中，一条间接的指令可能是在段之内或者跨段之间。在前一情况下，用寄存器或内存单元保存一个16位偏移地址，后一情况则是一个32位地址 (即，段和偏移)。

在高级语言 (如C语言) 中，函数调用指针的实现就是间接的子过程调用。考虑下面的C程序：

typedef char (*tfunc)();

tfunc func[2] = {func1, func2};

char func1() {/* some code here */}

char func2() {/* some code here */}

main()

{

func[0]();

func[1]();

}

在主程序里，对函数func1()和func2()的调用是通过一个函数指针和这些函数数组的一个索引。这个程序的反汇编代码如下：

CS:0094 | B604 |  |  | ; address of proc1 (04B6)
CS:0098 | C704 |  |  | ; address of proc2 (04C7)
 | ... |  |  |
 | proc_1 | PROC FAR |  |
CS:04B6 | 55 | push | bp |
... | ... |  |  |
CS:04C6 | CB | retf |  |
 | proc_1 | ENDP |  |
 | proc_2 | PROC FAR |  |
CS:04C7 | 55 | push | bp |
... | ... |  |  |
CS:04D7 | CB | retf |  |
 | proc_2 | ENDP |  |
 | main | PROC FAR |  |
CS:04D8 | 55 | push | bp |
CS:04D9 | 8BEC | mov | bp,sp |
CS:04DB | FF1E9400 | call | 0094 | ; intra-segment indirect call
CS:04DF | FF1E9800 | call | 0098 | ; intra-segment indirect call
CS:04E3 | 5D | pop | bp |
CS:04E4 | CB | retf |  |
 | main | ENDP |  |

用每个子过程地址的内存偏移地址代替函数指针 (即，分别是04B6和04C7)。如果这些地址在程序运行期间没有被修改，我们可以从这些内存单元的内容得到那些函数的目标地址。我们的实现就是，函数的目标地址用子过程调用指令代替，把它看作一个普通的子过程调用，反编译C程序的结果如下：

void proc_1() {/* some code */}

void proc_2() {/* some code */}

void main()

{

proc_1();

proc_2();

}

Case语句

高级语言通过一个叫作case语句的高级构造来实现多路 (或n路) 分支。在这个构造中，有n个不同的路径可能被执行。这个构造没有相应的低级机器指令，因此不同的编译器作者可能用不同的方法定义一个case表。

如果case的数目不是太多 (即，少于10个)，一个case用一序列的条件跳转来实现，每个检测一个单独的数值并且把控制转移到相应的代码。考虑下面的汇编代码片断

 | cmp | al,8 | ; start of case
 | je | lab1 |
 | cmp | al,7Fh |
 | je | lab2 |
 | cmp | al,4 |
 | je | lab3 |
 | cmp | al,18h |
 | je | lab4 |
 | cmp | al,1Bh |
 | je | lab5 |
 | jmp | endCase |
lab1: | ... |  |
... |  |  |
lab5: | ... |  |
... |  |  |
endCase: | ... |  | ; end of case

在这个代码片断中，寄存器al跟5个不同的字节数值做比较，如果结果是相等的，就无条件跳转到处理该case的标签。如果寄存器跟5个选项中任何一个都不相等，程序就无条件跳转到这组case结束处。

实现case语句更紧凑的一个办法是，使用一个索引表，其中保存n个目标标签地址：每个地址对应那n种情况其中之一。用一个变址的跳转指令进入该表。先对表索引值检查该表的上下限，以避免错误的索引号。在确定索引值不越界以后，再执行变址的跳转指令。考虑下面的代码片断：

cs:0DCF |  | cmp | ax,17h | ; 17h == 24
cs:0DD2 |  | jbe | startCase |
cs:0DD4 |  | jmp | endCase |
cs:0DD7 | startCase: |  |  |
 |  | mov | bx,ax |
cs:0DD9 |  | shl | bx,1 |
cs:0DDB |  | jmp | word ptr cs:0DE0[bx] | ; indexed jump
cs:0DE0 |  | 0E13 | ; dw lab1 | ; start of indexed table
cs:0DE2 |  | 0E1F | ; dw lab2 |
 |  | ... |  |
cs:0EOE |  | 11F4 | ; dw lab24 | ; end of indexed table
cs:0E10 | lab1: |  |  |
 |  | ... |  |
cs:11C7 | lab24: |  |  |
 |  | ... |  |
cs:11F4 | endCase: |  | ; end of case |
 |  | ... |  |

case表在代码段中被定义为数据，而且紧跟在变址的跳转之后和任何目标分支标签之前。寄存器ax保存进入该表的索引值。这个寄存器跟上限24做比较。如果寄存器大于24，将不执行这组指令序列的其它部分并且把控制转移给最后一个标签，即这组case结束后的第一条指令。另一方面，如果寄存器小于或等于24，就到第一个标签，并且寄存器bx被设置为表内偏移。因为字长是2，case表标签偏移是2，所以原来的表内索引被乘以2求出在2字节表内的正确偏移。然后，变址的跳转指令确定该case表是在cs段中偏移0DE0处 (对于这个例子，也就是在内存中下一个字节)。因此，目标跳转地址是这个表的24个不同可选地址中任何一个。

另一个非常相似的实现case语句方法是，把case表放置在子过程的末尾，进入该表的变址寄存器就是保存表内偏移的寄存器 (在下面代码片断中寄存器bx)：

cs:0BE7 |  | cmp | bx,17h | ; 17h == 24
cs:0BEA |  | jbe | startCase |
cs:0BEC |  | jmp | jumpEnd |
cs:0BEF | startCase: |  |  |
 |  | shl | bx,1 |
cs:0BF1 |  | jmp | word ptr cs:0FB8[bx] | ; indexed jump
cs:0BF6 | jumpEnd: |  |  |
 |  | jmp | endCase |
cs:0BF9 | lab1: |  |  |
 |  | ... |  |
cs:0F4C | lab24: |  |  |
 |  | ... |  |
cs:0F88 | endCase: |  |  | ; end of case
 |  | ... |  |
cs:0FB5 |  | ret |  | ; end of procedure
cs:0FB8 |  | 0BF9 | ; dw lab1 | ; start of indexed table
cs:0FBA |  | 0C04 | ; dw lab2 |
 |  | ... |  |
cs:0FE6 |  | 0F4C | ; dw lab24 | ; end of indexed table

实现case语句的第三个方法是把所有变址分支放在case表后面。在这个方法中，代码跳过所有的目标跳转地址，检查索引表的上限 (在下面的代码片断中是31)，校准作为表内索引的寄存器，然后分支转移到那个位置：

cs:0C65 |  | jmp | startCase |
cs:0C68 | lab5: |  |  |
... |  | ... |  |
cs:1356 | lab31: |  |  |
... |  | ... |  |
cs:1383 | lab2: |  |  |
... |  | ... |  |
cs:13B8 |  | 1403 | ; dw endCase | ; Start of indexed table
cs:13BA |  | 1383 | ; dw lab2 |
... |  | ... |  |
cs:13F4 |  | 1356 | ; dw lab31 | ; End of indexed table
cs:13F6 | startCase: |  |  |
 |  | cmp | ax,1Fh | ; 1Fh == 31
cs:13F9 |  | jae | endCase |
cs:13FB |  | xchg | ax,bx |
cs:13FC |  | shl | bx,1 |
cs:13FE |  | jmp | word ptr cs:13B8[bx] | ; indexed jump
cs:1403 | endCase: |  |  |
 |  | ... |  |
cs:1444 |  | ret |  |

case语句一个不同的实现是，使用一串字符选项代为编号。考虑下面的代码片断：

cs:246A | 4C6C68464E6F 785875646973 ; db 'LlhFNoxXudiscpneEfgG%'
cs:2476 | 63706E654566 674725
cs:247F |  | 256C | ; dw lab1 | ; start of table
cs:2481 |  | 2573 | ; dw lab2 |
... |  | ... |  |
cs:24A7 |  | 24DF | ; dw lab21 |
... |  | ... |  |
cs:24C4 | procStart: |  |  |
 |  | push | bp |
... |  | ... |  |
cs:2555 |  | mov | di,cs |
cs:2557 |  | mov | es,di | ; es = cs
cs:2559 |  | mov | di,246Ah | ; di = start of string
cs:255C |  | mov | cx,15h | ; cx = upper bound
cs:255F |  | repne | scasb |
cs:2561 |  | sub | di,246Bh |
cs:2565 |  | shl | di,1 |
cs:2567 |  | jmp | word ptr cs:247F[di] | ; indexed jump
cs:256C | lab1: |  |  |
... |  | ... |  |
cs:26FF | lab12: |  |  |
... |  | ... |  |
cs:2714 |  | ret |  |

字符选项字符串被放置于cs:246A。寄存器al保存所要核对的当前字符选择项，es:di指向作为比较的内存字符串，repne scasb指令在es:di所指向的字符串里面寻找与寄存器al的第一个匹配。执行以后，寄存器di就指向那个匹配字符。然后将这个寄存器减去字符串的开始地址和一，就是在变址跳转表内的索引值，该表被放置于代码段上该子过程之前。这个方法紧凑而且精彩。

不幸的是，case语句没有固定的表示法，因此需要首先手动检查二进制代码以确定它是如何实现case语句的。不同的编译器使用不同的实现，但是通常一个特定厂商的编译器只使用一两种不同case表表示法。case表的确定是一个启发式方法，它操作一组预先定义的一般化实现。反编译器能处理的实现方法越多，它所能产生的输出就越好。在使用启发式方法的时候，需要首先满足预备条件：即，如果遇到索引表而索引表的上下限无法确定，就不能应用所建议的启发式方法。

最后的算法

最后区分数据/代码的算法见图4-8所示。该算法在图4-7的算法的基础上，增加处理变址跳转和间接的跳转与调用。

procedure parse (machState *state)

done = FALSE;

while (! done)

getNextInst (state, &inst);

if (alreadyParsed (inst)) /* check if instruction already parsed */

done = TRUE; break;

end if

setBitmap (CODE, inst);

case (inst.opcode) of

conditional jump:

*stateCopy = *state;

parse (stateCopy); /* fall-through */

state->ip = targetAdr (inst); /* target branch address */

if (hasBeenParsed(state->ip)) /* check if code already parsed */

done = TRUE;

end if

unconditional jump:

if (direct jump)

state->ip = targetAdr(inst); /* target branch address */

if (hasBeenParsed(state->ip)) /* check if code already parsed */

done = TRUE;

end if

else /* indirect jump */

check for case table, if found, determine bounds of the table.

if (bounds determined)

for (all entries i in the table)

*stateCopy = *state;

stateCopy->ip = targetAdr(targetAdr(table[i]));

parse (stateCopy);

end for

else /* cannot continue along this path */

done = TRUE;

end if

end if

procedure call: /* Process non-library procedures only */

if (! isLibrary (targetAdr (inst)))

*stateCopy = *state;

if (direct call)

stateCopy->ip = targetAdr(inst);

else /* indirect call */

stateCopy->ip = targetAdr(targetAdr(inst));

end if

parse (stateCopy); /* process target procedure */

end if

/* other cases (procedure return, move, interrupt) remain the same */

end case

end while

end procedure

图 4-8: 最后的语法分析器算法


第 4 章 前端 - 语义分析

4.2 语义分析

语义分析阶段确定一组机器指令的含义，收集子程序各个指令单独的信息，并且在子程序所有指令中传播这个信息。这样，把基本数据类型 (比如整型和长整型) 传播到整个子程序。这个阶段与句法分析程序和中间代码生成器之间的关系见图4-9所示。

图 4-9: 语义分析程序的交互

定义 2

标识符 (<ident>) 可以是一个寄存器、局部变量 (在栈上负偏移)、参数 (在栈上正偏移)、或者全局变量 (位于内存中)。

4.2.1 成语

一个指令序列的语义含义有时成为一个成语。可能一个指令序列表现一个高级指令。

定义 3

成语是一个有逻辑含义的指令序列，但是其含义无法从所有单独的指令含义推导出来。

大多数成语在编译器社区是广为人知的，这样的一个指令序列是以一个唯一有效的或者比使用其它指令更为有效的方式执行一个操作。下面各节举例说明一些最著名的成语。

子程序成语

当进入一个子程序的时候，通过复制栈指什(sp)的值给基址寄存器bp，把bp设置为框架指针。框架指针用于在那个子程序里面存取保存在栈上的参数和局部数据。这个指令序列见图4-10所示。高级语言子程序的序言把寄存器bp设置为指向当前栈指针，而且可选择地在栈上为局部静态变量分配空间——通过把栈指针sp的值减去所需要的字节数目。这个成语可以用一条enter指令表示，它将一些字节预留作为局部存储。

push bp

mov bp, sp

[sub sp, immed]

↓

enter immed, 0

图 4-10: 高级子程序的序

在子程序的序言中任何入栈的寄存器即表示它的值要在这个子程序期间受保护。这些寄存器可能在当前子程序中充当寄存器变量 (即，局部变量)，因而把它们标记为可能是寄存器变量。图4-11是寄存器si和di开始被压入栈。

push si

push di

图 4-11: 寄存器变量

最后，为了退出子程序，任何保存在栈上的寄存器都要被弹出，任何被分配的数据空间都要被释放，bp需要被恢复为指向旧的框架指针，然后子程序用一条近返回指令或远返回指令返回。图4-12展示了尾代码的样本。

pop di | ; Restore registers
pop si |
mov sp,bp | ; Restore sp
pop bp | ; Restore bp
ret(f) | ; Return

图 4-12: 子程序的尾代码

调用约定

C语言调用约定也即是C语言的参数传递次序。在这个约定中，调用者把参数推入栈的次序跟它们在源代码中出现的次序相反 (即，从右到左的次序)，然后调用子过程。在子过程返回之后，调用者或通过将参数出栈、或通过将栈指针增加参数字节数来恢复栈。对于这两种情况，参数所使用的字节总数都可以知道，储存起来以备后用。从代码中清除掉与栈恢复相关的指令。在传递可变个数的参数的时候使用的是C语言调用约定，被调用者不负责恢复栈。图4-13是使用出栈指令恢复栈的情况。字节总数通过pop指令个数乘以2求得。

call(f) proc_X

pop reg

[pop reg] reg in {ax, bx, cx, dx}

↓

proc_X.numArgBytes = 2 * numPops

sets CALL_C flag

图 4-13: C语言调用约定 - 使用pop

图4-14是通过给栈增加参数字节数来恢复栈的情况。这个数值储存起来以备后用，并且把恢复栈的指令清除掉以便进一步的分析。已经发现，当参数只使用2或4字节的时候，是通过从栈上弹出这些字节来恢复栈。这是由于两个不同操作需要花费不同的时钟周期数。每条pop reg指令占1字节，而一条add sp,immed指令占3字节。在只需要一两条pop reg指令的情况下，二进制代码很可能为空间优化而不为速度优化，毕竟在i8086上一条pop reg指令需要8个时钟周期，而一条add sp,immed指令只需要4个时钟周期。

call(f) proc_X

add sp, immed

↓

proc_X.numArgBytes = immed

sets CALL_C flag

图 4-14: C语言调用约定 - 使用add

Pascal语言调用约定也即是Pascal语言的参数传递次序。在这个约定中，调用者把参数推入栈的次序跟它们在源代码中出现的次序相同 (即，从左到右的次序)，然后调用被调用者子过程，而且由被调用者负责在返回之前平衡栈。因此被调用者必须知道有多少参数字节被传递过来，由此推出，它不能使用变参参数。

ret(f) immed

↓

proc_X.numArgBytes = immed

sets CALL_PASCAL flag

图 4-15: Pascal语言调用约定

长整型变量的运算

长整型变量在内存中用两个连续内存单元或栈单元存储。当对这些变量进行简单的加减法运算时，它们通常就加上长整型变量标识符。一般使用指令简单明了的成语处理这些运算。

图4-16是长整型加法所用指令序列。长整型变量的低字部分用一条add指令相加，如果有溢出就会设置进位标志。然后它们的高字部分带进位相加，相当于在低字部分有一个1溢出，这个1需要被加到高字部分。因此，需要用一条adc指令 (带进位加) 来做高字部分相加。

add ax, [bp-4]

adc dx, [bp-2]

↓

dx:ax = dx:ax + [bp-2]:[bp-4]

图 4-16: 长整型的加法

长整型减法的做法与之类似。低字部分先用一条sub指令相减。如果有借位就设置借位标志。如果下溢，那么在高字部分相减的时候计入，相当于在低字部分有一个溢出，借位需要从源的高字部分减去。因此，使用sbb指令 (带借位减)。见图4-17所示。

sub ax, [bp-4]

sbb dx, [bp-2]

↓

dx:ax = dx:ax - [bp-2]:[bp-4]

图 4-17: 长整型的减法

长整型变量的求负用一个三指令序列来完成：高字部分求反，然后低字部分求反，最后高字部分带借位减零以防止在低字部分的求反有下溢。这个成语见图4-18所示。

neg regH

neg regL

sbb regH, 0

↓

regH:regL = - regH:regL

图 4-18: 长整型的求负

长整型移1位通常使用进位标志，而且在移位结果的高字或低字部分循环该标志。左移不受长整型操作数的符号影响，而且通常包括低字左移(shl)，低字部分的高位会移到进位标志中。然后高字部分左移，不同的是它要把进位标志放在结果高字部分的最低位，因此要使用rcl指令 (带进位左循环移位)。这个成语见图4-19所示。

shl regL, 1

rcl regH, 1

↓

regH:regL = regH:regL << 1

图 4-19: 长整型变量左移1位

长整型右移1位需要保持长整型操作数的符号，因此两个不同的成语各用于有符号操作数和无符号操作数。图4-20是用于有符号长整型操作数的成语。长整型操作数的高字部分右移1位，是使用一条算术右移指令(sar)，所以这个数是作为有符号数看待。高字部分的较低位被放在进位标志上。然后操作数的低字部分带进位右移，因此使用一条带进位循环右移指令(rcr)。

sar regH, 1

rcr regL, 1

↓

regH:regL = regH:regL >> 1 (regH:regL is signed long)

图 4-20: 带符号长整型变量右移1位

无符号长整型操作数右移1位与之类似。在这种情况下，高字部分右移把较低位移到进位标志，然后用一条带进位循环右移指令移入低字部分。见图4-21。

shr regH, 1

rcr regL, 1

↓

regH:regL = regH:regL >> 1 (regH:regL is unsigned long)

图 4-21: 无符号长整型变量右移1位

各种成语

给一个变量赋零是一个常见的机器成语。不是使用一条mov指令，而是使用xor指令：只要一个变量跟它本身异或，结果便是零。这个机器成语使用更少的机器周期和占用更少的字节，见图4-22所示。

xor reg/stackOff, reg/stackOff

↓

reg/stackOff = 0

图 4-22: 赋零

不同的机器体系结构对于一条移位指令移的位数有不同限制。在i8086中，移位指令只允许一条指令移1位，因此为了移2位或更多位不得不编写若干条移位指令。图4-23是这个成语。一般来说，移n位 (n是常数) 可以用n条移1位指令完成。

shl reg, 1 --\

[...] | n times

shl reg, 1 --/

↓

reg = reg << n

图 4-23: 左移n位

整型/字型变量的按位变反实现见图4-24所示。这个成语将寄存器求反 (二进制补码)，然后用借位减法从中减去自身，以防止该寄存器最初的求反下溢，最后该寄存器增量1得到答案是0或1。

neg reg

sbb reg, reg

inc reg

↓

reg = !reg

图 4-24: 位反

4.2.2 简单类型的传播

基本数据类型比如字节型和整型，它们的符号容易通过在比较一个操作数时所使用的条件跳转的类型来确定。这样的技术也被用来确定更复杂的基本数据类型的符号，比如长整型和实型。下面几节举例说明一些技术，用来确定一个字尺寸操作数是有符号整型还是无符号整型、一个双字尺寸操作数是有符号长整型还是无符号长整型。这些技术容易推广到其它基本数据类型。

整型的传播

一个字尺寸操作数可能是一个有符号整数也可能是一个无符号整数。大多数处理字尺寸操作数指令对于有符号和无符号不作任何的区别；条件跳转指令是一个例外。对于大多数相关操作，有不同类型的条件跳转，例如，下面的代码：

cmp [bp-0Ah], 28h

jg X

检查在bp-0Ah处的字操作数是否大于28h。下面的代码：

cmp [bp-0Ah], 28h

ja X

检查在bp-0Ah处的字操作数是否超出28h。这后一个条件跳转测试无符号字操作数，而前一个条件跳转测试有符号操作数；因此，对于前一种情况在bp-0Ah处的局部变量是一个有符号整数，而后一情况下则是一个无符号整数。把这个信息作为该局部变量的一个属性储存在符号表中。

同样地，如果一个条件跳转的操作数是寄存器，就可以确定该寄存器是一个有符号或无符号整数寄存器，然后这个信息在寄存器所属的基本程序块(basic block)上向后传播，直至该寄存器的定义(赋值)。考虑下面的代码：

1 | mov | ax, [bp-0Ch]
2 | cmp | ax, 28h
3 | ja | X

通过第3条指令，条件跳转的操作数被确定是无符号整数；因此，寄存器ax和常数28h是无符号整数操作数。因为寄存器ax不是一个局部变量，所以把这个信息向后传播，直至找到ax的定义。在这个例子中，第1条指令用局部变量bp-0Ch定义ax，因此，这个局部变量代表一个无符号整数，把这个属性储存在符号表的bp-0Ch项目里。

用来区别有符号整数和无符号整数的条件跳转指令集见图4-25所示。这些条件跳转适用于Intel体系结构。

带符号条件的 | 无符号条件的
jg | ja
jge | jae
jl | jb
jle | jbe

图 4-25: 根据条件跳转来确定有无符号

长整型变量的传播

长整型变量的最初识别是通过第4.2.1节所描述的成语分析来确定。一旦已知一对标识符是一个长整型变量，就必须改变对这些标识符的所有引用以反映它们是一个长整型变量的组成部分 (亦即，长整型变量的高字或低字部份)。而且，能够把处理长整型变量高字和低字部分的一对指令合并成一条指令。考虑下面的代码：

108 | mov | dx,[bp-12h]
109 | mov | ax,[bp-14h]
111 | add | dx:ax,[bp-0Ah]:[bp-0Ch]
112 | mov | [bp-0Eh],dx
113 | mov | [bp-10h],ax

通过成语分析，指令110和指令111被合并成一条add指令，继而标识符bp-0Ah和bp-0Ch以及寄存器dx:ax变成一个长整型变量。除了寄存器之外的标识符被传播到整个子程序中间代码，在这个例子中，对bp-0Ah没有其它引用。寄存器在使用它们的基本块里面传播，通过向后传播直至找到寄存器定义，而且向前传播直至遇到对该寄存器重新定义。在这个例子中，通过dx:ax的向后传播，我们得到以下代码：

109 | mov | dx:ax,[bp-12h]:[bp-14h]
111 | add | dx:ax,[bp-0Ah]:[bp-0Ch]
112 | mov | [bp-0Eh],dx
113 | mov | [bp-10h],ax

上面已把指令108和指令109合并成一条mov指令。同时，由这个合并可以确定局部标识符bp-12h和bp-14h是一个长整型变量，并且因此把这个信息储存在符号表中。通过在基本块中向前传播dx:ax，我们得到以下代码：

109 | mov | dx:ax,[bp-12h]:[bp-14h]
111 | add | dx:ax,[bp-0Ah]:[bp-0Ch]
113 | mov | [bp-0Eh]:[bp-10h],dx:ax

上面已把指令112和113合并成一条mov指令。此时，局部标识符bp-0Eh和bp-10h被确定是一个长整型变量，而且也把这个信息储存在符号表中并且传播。

长整型变量的跨条件跳转传播用两步或更多步完成。长整型识别符的高字和低字部分在不同的基本块中相互比较。基本块的概念很简单：只有一个入口点和一个出口点的指令序列；这个概念在第4.4.3节做更详细解释。考虑以下代码：

115 | mov | dx:ax,[bp-0Eh]:[bp-10h]
116 | cmp | dx,[bp-0Ah]
117 | jl | L21
118 | jg | L22
119 | cmp | ax,[bp-0Ch]
120 | jbe | L21

在指令115上寄存器dx:ax被确定是一个长整型寄存器，因此在指令116上的cmp操作码只是在检查这个长整型寄存器的高字部分，稍后有一条指令(119)检查长整型寄存器的低字部分。通过分析该指令我们了解到，如果dx:ax少于或等于标识符[bp-0Ah]:[bp-0Ch]，就到标签L21；否则就到标签L22。这三个基本块能够被转换成一个包含这个条件的唯一基本块，如下：

115 | mov | dx:ax,[bp-0Eh]:[bp-10h]
116 | cmp | dx:ax,[bp-0Ah]:[bp-0Ch]
117 | jle | L21

若该条件为真，这个基本块分支转移到标签L21，若该条件为假，就转移到标签L22。在这些指令中没有显式地出现标签L22，但是被这个基本块的出边(out-edge)隐含地指出。

一般来讲，长整型条件分支通过它们的图结构来识别。图4-26展示出五个图。其中四个代表六个不同的条件。图(a)和图(b)表现相同的条件。这些图代表不同的长整型条件，取决于这些图的结点所对应的指令，条件是：<=, <, >, 和>=。图(c)和图(d)表现长整型变量的相等和不相等。当满足以下条件的时候，这四个图被转化成图(e)：

³ 图(a)和(b)：

l 基本块x是一个条件结点，它比较长整型标识符的高字部分。

l 基本块y是一个有一条指令的条件结点；一个条件跳转而且有一个来自基本块x的入边(in-edge)。

l 基本块z是一个有两条指令的条件结点；长整型标识符低字部分的比较和一个条件跳转。

³ 图(c)和(d)：

l 基本块x是一个条件结点，它比较长整型标识符的高字部分。

l 基本块y是一个有两条指令的条件结点；长整型标识符低字部份的比较，和一个条件跳转，而且有一个来自基本块x的入边。

图 4-26: 长整型条件分支图

图4-27展示的代码样本对应图4-26的图(c)和图(d)：长整型标识符的相等和不相等。这个代码适用于Intel i80286体系结构。

结点x | 结点y | 布尔条件
cmp dx, offHi jne t | cmp ax, offLow jne t | !=
cmp dx, offHi jne e | cmp ax, offLow je t | ==

图 4-27: 长整型相等的布尔条件码

图4-26之图(a)的结点对应的代码样本见图4-28。每个结点所对应的代码表现不同的不相等布尔条件，即，不多于 (少于或等于)、少于、大于、和不少于 (大于等于)。图(b)对应的代码与之相似，表现的布尔条件完全相同。这个代码适用于Intel i80286体系结构。

结点x | 结点y | 结点z | 布尔条件
cmp dx, offHi jl t | jg e | cmp ax, offLow jbe t | <=
cmp dx, offHi jl t | jne e | cmp ax, offLow jb t | <
cmp dx, offHi jg t | jne e | cmp ax, offLow ja t | >
cmp dx, offHi jg t | jl e | cmp ax, offLow jae t | >=

图 4-28: 长整型不相等的布尔条件码


第 4 章 前端 - 中间代码生成

4.3 中间代码生成

在一个反编译器中，前端将机器语言源代码翻译成适合给通用反编译机器做分析的一个中间表示法。图4-29是这个阶段与语义分程序和前端最后阶段——控制流向图生成器——的关系图。采用一个与目标语言无关的表示法，使得我们只须为不同的目标语言编写一个后端附加到反编译器上就可以产生各种不同的反编译结果。

图 4-29: 中间代码生成器的交互

在反编译这个阶段采用两步法：首先用低级的中间表示法来表现机器语言程序。在这个表示法里面可以进行成语分析和类型传播，并从中产生汇编代码 (即，这是适合反汇编器使用的一个中间代码，因为它不对代码做高级分析)。然后把这个表示法转换成一个高级的中间表示法，它适合用来生成高级语言。该表示法需要具有足够的一般性，以便将来可以生成任何高级语言的代码。

4.3.1 低级中间代码

低级的中间表示法跟目标机器的汇编语言很相近，很适合用来表现低级中间代码，这样我们可以对代码进行语义分析并且从中生成汇编语言程序。对于每一条完整的机器语言指令，中间代码必须有对应的指令。复合的机器指令也必须用一条中间指令表现。例如，在图4-30中，机器指令B720对应的中间指令是mov bh,20。机器指令2E及其后的FFEFC006 (即一条用CS段前缀覆盖的jmp指令) 用一条jmp指令代替，显式地使用寄存器。而最后，复合的机器指令F3A4等同于汇编指令rep和movs di,si。这两条指令只用一条中间指令rep_movs表示，把这个移动的目的寄存器和源寄存器都清楚地表示出来了。

 | 2E | F3
B720 | FFEFC006 | A4
 | ↓ |
mov bh,20 | jmp cs:06C0[bx] | rep_movs di,si

图 4-30: 低级中间指令 - 例子

低级中间代码的实现

低级中间表示法用一个四元组实现一条指令，显式地表现它所使用的操作数，如图4-31所示。opcode字段保存低级的中间操作码，dest字段保存目的操作数 (即，一个标识符)，src1字段和src2字段保存指令的源操作数。有些指令不使用两个源操作数，只使用src1字段。

opcode

dest

src2

src1

图 4-31: 四元组的一般表示法

例子 4

一条add bx, 3机器指令用四元组表示如下：

add

bx

bx

3

其中寄存器bx是源操作数和目的操作数，常数3是第二个源操作数。

例子 5

一条push cx机器指令表示如下：

push

sp

cx

\

其中寄存器cx是源操作数，寄存器sp是目的操作数。

4.3.2 高级中间代码

三地址代码是三地址机器汇编代码的一般形式。由于三地址代码是程序的抽象句法树(AST: abstract syntax tree)的线性化表示法，所以这个中间代码最适合在反编译器中使用。这样，我们能够在数据流分析期间重建程序完整的AST。三地址指令的一般形式如下：

x := y op z

其中x 、y和z是标识符，op是一个算术运算符或逻辑运算符。结果地址是x，两个操作数地址是是y和z。

三地址语句的类型

三地址语句与高级语言语句很相似。假如数据流分析要重建程序的AST，三地址指令不仅提供单个标识符，而且提供表达式。一个标识符可以看作是一个表达式的最小形式。各种指令类型如下：

1. asgn <exp>,<arithExp>

赋值指令，把一个算术表达式赋给一个标识符或表达式 (即，标识符用一个表达式表现，比如对一个数组的索引)。这个语句表现三种不同类型的高级赋值指令：

l x := y op z 其中x、y和z是标识符，op是一个二进制算术运算符。

l x := op y 其中x和y是标识符，op是一个一元的算术运算符。

l x := y 其中x和y是标识符。

在数据流分析之后，算术表达式不仅表现一个二进制运算，而且是一个由算术运算符和标识符组成的、完整的分析树。这个转换在第5章讲述。

在这个意义上讲，一个有返回值的子程序 (即，函数) 也被看成一个标识符，因为对它的调用返回一个结果被赋给另一个标识符 (例如，a := sum(b,c) )。

2. jmp

无条件跳转指令，除了跳转目标目的地址之外不关联任何表达式。这条指令使控制转到目标地址。因为该地址会通过含有这条指令的基本块的出边予以说明，所以没有显式地作为该指令的一部分。这条指令等价于如下高级指令：

goto L

其中L是跳转的目标地址。

3. jcond <boolExp>

条件跳转指令，带有一个与之关联的布尔表达式，由它判定分支是否转移。该布尔表达式的形式为x relop y，其中x和y是标识符，relop是一个关系运算符，比如<、≥、=。这个语句等价于如下高级语句：

if x relop y goto L

在这个中间指令中，目标分支地址(L)和直通的地址 (即，下一条指令的地址) 不是该指令的一部分，因为在控制流向图中它们会通过含有这条指令的基本块的出边予以说明。

4. call <procId> <actual parameters>

调用指令，表现一个子程序调用。子过程标识符(<procId>)是一个指针，它指向被调用子过程的流向图。其中，实际参数的列表要等到数据流分析期间才被建立。如果所调用的子程序是一个函数，它还定义了持有返回值的寄存器。在这个情况下，该指令等价于asgn <regs>, <procId> <actual parameters>。

5. ret [<arithExp>]

返回指令，确定一个子过程在一条路径上的终点。如果子程序是一个子过程，就没有返回值；如果是函数就有返回值。

还有两个伪高级中间指令，在数据流分析中用作中间指令，但是在分析结束时被清除掉。它们是：

1. push <arithExp>

进栈指令，把有关算术表达式放在一个临时栈上。

2. pop <ident>

退栈指令，取出临时栈顶端的表达式或者标识符、而且将它赋给标识符ident。

高级中间代码的实现

高级的中间表示法用三元组实现。在一个三元组中，有两个显式的表达式和指令操作码，如图4-32所示。result和arg字段是指向一个表达式的指针，它的最小形式是一个标识符，指向符号表。

op

result

arg

图 4-32: 三元组的一般表示法

赋值语句 x := y op z 用三元组表示如下：op字段是asgn操作码，result字段用一个指针指向标识符x (而x也用一个指针指向在符号表中的标识符)，arg字段有对一个二进制的表达式的一个指针；这个表达式用一个抽象句法树表现如下，指针指向符号表项目y和z：

类似地，条件跳转语句if a relop b用一个三元组表示如下：op字段是jcond操作码，result字段用一个指针指向一个测试关系的抽象句法树，arg字段未使用，如下：

无条件跳转语句goto L不使用result或arg字段。op字段被设置为jmp，其它段未使用，如下：

JMP

\

\

子过程调用语句 procX(a,b) 用op字段存放call操作码，result字段指向保存在符号表中的子过程名字，arg字段用于子过程参数，它是一连串指向符号表的参数，如下：

子过程返回语句ret a用op字段存放ret操作码，result字段用于标识符/表达式，arg字段未使用，如下：

伪高级指令push a用三元组储存，用op字段存放push操作码，arg字段用于要入栈的标识符，result字段未使用，如下：

类似地，pop a指令用三元组存储，用op字段存放pop操作码，result字段用于标识符，而arg字段最初留空未使用，最后在数据分析期间用它要弹出的表达式填充。其三元组表示法如下：


第 4 章 前端 - 控制流向图生成

4.4 控制流向图生成

控制流向图生成阶段为源程序构造一个调用图，并且为程序的每个子程序构造一个基本块控制流向图。这些图在通用反编译机器模块(UDM)中用于分析程序。这个阶段与中间代码生成器和udm的交互见图4-33所示。

图 4-33: 控制流向图生成器的交互

4.4.1 基本概念

这一节描述数学和图论的定义。在这里定义这些术语以避免叙述用词混乱。

定义 4

图 G是一个 (V, E, h) 元组，其中V是一组结点，E是一组边，h是图的根。一个边是一对结点(v, w)，这里v, w ∈V。

定义 5

有向图 G=(N, E, h) 是一个由有向边组成的图；亦即，每组(ni, nj)∈E有一个方向，而且表示为ni → nj。

定义 6

在图 G(N, E, h) 中一条路径从n1到nm，表示为n1 → *nn，是一个边的序列 (n1, n2), (n2, n3), ..., (nn-1, nm)∈N, m≥1。

定义 7

如果 G=(V, E, h) 是一个图，∃!h∈V，而且E=∅，那么G叫做一个平凡图。

注：∅ 表示空集

定义 8

如果 G=(N, E, h) 是一个图，而且∀n∈N, h → *n，那么G是一个连通图。

一个连通图是这样一个图，其中所有结点可以从头结点出发到达。图4-34给出一个有向连通图的样例。

图 4-34: 有向连通图样例

图表示法

图 G=(V, E, h) 以几个不同方式表现，包括关联矩阵、邻接矩阵和前趋后继表。

定义 9

图 G=(V, E, h) 的关联矩阵是v×e矩阵M(G)=[mij]，其中mij是顶点vi和边ej的关联次数 (0, 1或2)。

定义 10

图 G=(V, E, h) 的邻接矩阵是v×v矩阵A(G)=[aij]，其中aij是连接顶点vi和vj的边数。

定义 11

图 G=(V, E, h) 的前趋后继表是v×2表T(G)=[ti1, t i2]，其中t i1是顶点vi的前趋顶点列表，ti2是顶点vi的后继顶点列表。

例子 6

图4-34中的图用下列矩阵表示：

³ 关联矩阵：

 | e1 | e2 | e3 | e4 | e5 | e6 | e7 | e8
v1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0
v2 | 1 | 0 | 0 | 1 | 1 | 1 | 0 | 0
v3 | 0 | 1 | 0 | 1 | 0 | 0 | 1 | 2
v4 | 0 | 0 | 1 | 0 | 1 | 1 | 1 | 0

³ 邻接矩阵：

 | v1 | v2 | v3 | v4
v1 | 0 | 1 | 1 | 1
v2 | 1 | 0 | 1 | 2
v3 | 1 | 1 | 1 | 1
v4 | 1 | 2 | 1 | 0

³ 前趋后继表：

 | Predecessor | Successor
v1 | ∅ | {v2, v3, v4}
v2 | {v1, v4} | {v3, v4}
v3 | {v1, v2, v3} | {v3, v4}
v4 | {v1, v2, v3} | {v2}

4.4.2 基本块

在这一节中，我们给基本块下一个正式的定义。为了表明它的特征，我们需要给程序结构下更多的定义。我们从一个程序的组成部分 (即，数据和结构) 开始。注意：这个定义与内存里具体的结构或数据无关。

定义 12

令

l P是一个程序

l I = {i1, ..., in} 是P的指令

l D = {d1, ..., dm} 是P的数据

那么 P = I∪D

为了便于研究，约定程序没有包含自修改代码、而且没有利用数据作为指令、也没有利用指令作为数据 (即，I(P)∩D(P) = ∅)。一个指令序列是指在内存里连续存放的一组指令。

定义 13

令

l P是一个程序

l I = {i1, ..., in} 是P的指令

那么对于ij, ∀1≤j≤k-1，当且仅当 S = [ij, ij+1, ..., ij+k] · 1 ≤ j < j+k ≤ n ∧ ij+1 是在一块连续不断的内存单元里面时，S是一个指令序列。

为了生成控制流向图，我们将中间指令分成两类：

³ 转移指令(TI)：一些指令的集合，这些指令把控制流转移到在内存里不同于下一条指令地址的某个地址。它们是：

l 无条件跳转：把控制流转移到跳转目标地址。

l 条件跳转 ：如果条件为真，把控制流转移到跳动目标地址，否则把控制转移到在指令序列中的下一条指令。

l 变址的跳转：把控制流转移到一些目标地址中的某一个。

l 子程序调用：把控制流转移到被调用的子程序。

l 子程序返回：用返回指令把控制流转移到调用子程序的子程序。

l 程序终点 ：程序结束。

³ 非转移指令(NTI)：把控制转移到指令序列中下一条指令的指令的集合，即所有不属于TI集合的指令。

对中间指令进行分类以后，基本块根据它的指令被定义为：

定义 14

基本块 b = [i1, ..., in-1, in], n≥1 是一个满足下列条件的指令序列：

1. [i1, ..., in-1]∈NTI

2. in∈NTI

或

1. [i1, ..., in-1, in]∈NTI

2. in+1是另一个基本块的第一条指令。

一个基本块是只有一个入口点和一个出口点的一个指令序列。如果基本块的一条指令被执行，那么所有其它指令也会被执行。

一个程序的指令集合能够从程序的入口点开始被唯一地分成一组相互不重叠的基本块。

定义 15

令

l I是程序P的指令

l h是P的入口点

那么∃B = {b1, ..., bn} · b1∩b2...∩bn = ∅ ∧ I = b1∪b2∪...∪bn ∧ b1 的入口点 = h。

4.4.3 控制流向图

控制流向图是一个表现程序控制流的有向图，因此它只表现出该程序的指令。图的结点代表该程序的基本块，图的边代表结点之间的控制流。更正式地下一个定义，

定义 16

程序P的控制流向图 G=(N, E, h) 是一个满足以下条件的有向连通图：

l ∀n∈N，n 代表P的一个基本块。

l ∀e=(ni, nj)∈E，e代表从一个基本块到另一个基本块的控制流，而且ni, nj∈N。

l ∃f : B → N · ∀bi∈B, f(bi) = nk 对于某些 nk∈N ∧ ∄bj∈B · f(bj) = nk

为了生成控制流向图(cfg)，我们根据基本块的最后一条指令，将基本块分成不同类型。基本块可以有以下几种类型：

l 单路基本块：基本块的最后一条指令是一个无条件跳转。这种块有一个出边。

l 两路基本块：最后一条指令是一个有条件跳转，因此这种块有两个出边。

l 多路基本块：最后一条指令是一个变址的跳转。那么位于case表的分支变成这个基本块结点的n路出边。

l 调用基本块：最后一条指令是对某个子程序的一个调用。从这种块出发有两个出边：一个到子程序调用指令的下一条指令 (如果子程序返回的话)，另一条到被调用的子程序。

l 返回基本块：最后一条指令是一个子过程返回或者程序终止。这种基本块没有出边。

l 直通基本块：下一条指令是一条分支转移指令的目标地址 (亦即，下一条指令有一个标签)。这个结点被当作向下直通下一个结点的结点看待，因此只有一个出边。

在控制流向图中，不同类型的基本块在它对应的结点旁边标出它的类型名称，如图4-35所示。在一个图中，如果某个结点没有标出类型名称，则说明该基本块的类型与我们的主题无关，或者可以从上下文明显地看出来 (亦即，在图中明确地标示了出边的准确数目)。

图 4-35: 不同类型基本块的结点表示法

例子 7

考虑下面的代码片断：

0 | PUSH | bp
1 | MOV | bp, sp
2 | SUB | sp, 4
3 | MOV | ax, 0Ah
4 | MOV | [bp-2], ax
5 | MOV | [bp-4], ax
6 | LEA | ax, [bp-4]
7 | PUSH | ax
8 | CALL | near prt proc_1
9 | POP | cx
10 | L1: MOV | ax, [bp-4]
11 | CMP | ax, [bp-2]
12 | JNE | L2
13 | PUSH | word prt [bp-4]
14 | MOV | ax, 0AAh
15 | PUSH | ax
16 | CALL | near prt printf
17 | POP | cx
18 | POP | cx
19 | MOV | sp, bp
20 | POP | bp
21 | RET |
22 | L2: MOV | ax, [bp-4]
23 | CMP | ax, [bp-2]
24 | JGE | L1
25 | LEA | ax, [bp-2]
26 | PUSH | ax
27 | CALL | near prt proc_1
28 | POP | cx
29 | JMP | L1

这个代码有以下基本块：

基本块类型 | 指令范围
call | 0 to 8
fall | 9
2w | 10 to 12
call | 13 to 16
ret | 17..21
2w | 22..24
call | 25..27
1w | 28, 29

表现这些指令的控制流向图见图4-36所示。

图 4-36: 例子10的控制流向图

以后如果没有特别说明，我们就用图这个词代表一个控制流向图，而且用结点这个词代表一个基本块。

实现

控制流向图平均每个结点有两个出边，因此使用矩阵表示法 (例如，关联矩阵和连接矩阵) 将会是非常稀疏的、内存利用率很低 (即，矩阵大部分用零填充)。因此用前趋后继表中实现控制流向图会更好，在这种关系表中只表现图中存在的边。注意，后继是完整地表现一个图所必需的；前趋是为了便于在图的不同遍历当中存取图。

如果图的大小是未知的 (亦即，结点数目不固定)，可能动态地构造该图，指向基本块的指针带有一组前趋和一组后继。前趋和后继也是指向基本块结点的指针；这样，对于一个基本块我们只须表现一次。任何一个允许内存动态分配的高级语言似乎都可以接受这种表示法。考虑在图4-37中基本块的C语言定义。BB结构定义一个基本块结点，numInEdges和numOutEdges保存前趋结点数和后继结点数。

分别地，**inEdges是前趋基本块指针的动态指针数组，**outEdges是后继基本块指针的动态指针数组。在这个表示法中，图是用一个指向头基本块的指针 (即，一个PBB) 来表示。

typedef struct _BB{

byte nodeType; /* Type of node */

int numInEdges; /* Number of in edges */

struct _BB **inEdges; /* Array of pointers to predecessors */

int numOutEdges; /* Number of out-edges */

struct _BB **outEdges; /* Array of pointers to successors */

/* other fields go here */

} BB;

typedef BB *PBB; /* Pointer to a basic block */

图 4-37: 基本块的C语言定义

图的优化

经由编译器产生的机器代码使用多余或不必要的跳转，其形式有到跳转的跳转、到跳转的条件跳转、以及到条件跳转的跳转。这些不必要的跳转可以通过在控制流方面做窥孔优化来去掉。尽管这个优化不总是被使用。

窥孔优化(peephole)方法是通过检查一个短的目标指令序列 (名叫窥孔) 并且使用一个更短或更快的指令序列替换这些指令，从而改善目标程序的性能。窥孔是目标代码中一个活动的小窗口；在窥孔中的代码不需要是连续存放的。通过窥孔优化的每一次改善有可能造就更多的附加改善，因此，对代码进行多次反复处理是必需的。

通过该方法把多余的跳转清除掉达到控制流向的优化。对于反编译，我们感兴趣的是清除掉所有到跳转的跳转、以及到跳转的条件跳转，因为目标跳转持有目标分支的地址而且使用一个可以从图中去掉的中间基本块。并不希望去掉那些到条件跳转的跳转，因为这涉及若干指令的重新排列，而不仅仅是对一个目标分支地址的修改而已。

下列跳转序列跳转到标签Lx然后跳到标签Ly，在两次跳转之间没有执行任何其它指令：

 | jmp | Lx
 | ... | ; other code here
Lx: | jmp | Ly

这个序列用下面的序列替换

 | jmp | Ly
 | ... | ; other code here
Lx: | jmp | Ly

其中第一个跳转分支的目标是Ly标签，而非中间的Lx标签。让开始于Lx的基本块的前趋数目被递减1，并且让开始于Ly的基本块的前趋数目被递增一，如此在图中反映边的变化。只要那个开始于标签Lx的基本块的前趋数目变成零，它的结点就从图中去掉，因为现在它是无法到达的结点，首先就没有必要了。

类似地，象下面这样跳到一个无条件跳转的跳转

 | jZ | Lx
 | ... | ; other code here
Lx: | jmp | Ly

用下面的代码序列替换

 | jZ | Ly
 | ... | ; other code here
Lx: | jmp | Ly

调用图

调用图是用数学方法来表现一个程序的子程序。每个结点代表一个子程序，每个边代表对另一个子程序的一个调用。更正式的定义如下，

定义 17

令 P = {p1, p2, ...} 是一个程序的子过程的有限集合。调用图C是一个 (N, E, h) 元组，其中：N是子过程的集合，ni∈N代表一个且只有一个pi∈P；E是边的集合，(ni, nj)∈E代表现pi对pj的一个或多个引用；h是main子过程。

如果被调用的子程序静态绑定子程序常数，也就是说，在程序中不使用子过程参数或子过程变量来调用子程序，那么构造一个调用图会很简单。递归调用在调用图中表现为环。文献[Ryd79]提出在出现子过程参数的情况下构造调用图的一个有效算法，适用于没有递归构造的语言。这个方法后来被扩展成支持递归，详见参考文献[CCHK90]所述。最后，文献[HK92]提出一个方法，处理子过程参数和有限制类型的子过程变量。

应该注意，这个方法无法重新构造出所有图的最初形式，因为编译器优化可能会把一个隐含的调用指令改变成一条无条件跳转指令。在这些情况下，调用图 (因而反编译器) 会把两个子程序的代码看作是同一个子程序的代码，除非还有其它地方也通过一个隐含的调用指令调用这个被调用的子程序。


第 5 章 数据流分析

由前端生成的低级中间代码是一个汇编类型的表示法，它使用寄存器和条件码。这个表示法能够被转换成一个不使用这些低级概念而是重新生成高级概念之表达式的、更高级的表示法。从低级中间代码到高级中间代码的转换是通过程序转换来完成，传统上称之为优化。对低级的中间代码应用这些转换，把它转变成第4章第4.3.2节所描述的高级中间代码。这个阶段跟前端以及控制流分析阶段的关系见图5-1所示。

图 5-1: 数据流分析阶段的上下文

数据流分析阶段需要的转换类型包括，无用指令的清除、条件码的清除、寄存器参数和函数返回寄存器(组)的确定、通过再生表达式来清除寄存器和中间指令、实际参数的确定、以及在跨子程序调用之间传播数据类型。这些转换大多数是为了改善低级中间代码的质量所必需要的，也是为了重建一些在编译过程中丢失的信息所必需要的。就无用指令的清除来说 (请看第5.2.1节的例子)，对于优化的编译器，当有的机器指令一次执行多于一个功能的时候，这一步骤可以说是很必要的。

传统的数据流分析是这样一个过程，它收集关于变量在程序中被如何使用的信息、并且以集合的形式做出统计。这个信息被反编译器用于转换和改善中间代码的质量。为了改善代码所做的转换要求具备如下几个性质 [ASU86b]：

1. 转换必须无损程序的含义。

2. 转换必须是值得做的。

这一章讨论中间代码的反编译优化技术。首先通过例子说明是什么样的转换，然后给出每一种优化的算法。

5.1 前人的工作

在反编译器的数据流分析领域中还有许多问题有待解决，主要是由于文献里提出的反编译器有诸多限制；汇编源文件的反编译 [Hou73、Fri74、Wor78、Bri81]、带符号调试信息的目标文件(.obj)的反编译 [Reu88]、以及建造一个反编译器的编译器规约需求 [BB91, Bow93, BB93]。在反编译纯粹的二进制文件的时候，数据流分析是必要的，因为没有关于数据的类型及其使用的附加信息。下面几节概述在这个领域中已经做过的工作。

5.1.1 条件码的清除

Marshall和Zobrist提出一个翻译程序，把微处理器目标代码 (i8085) 翻译成一个行为上等价的PL/1程序，而且被用于电子系统模拟 [MZBR85]。最后的PL/1程序包含大量定义标志的语句，即使后面的程序没有使用或引用这些标志。这促使DeJean和Zobrist在文献[DZ89]中阐述，使用一个延伸算法来最优化标志定义。这个方法清除掉翻译过程中50%以上的标志定义，生成的PL/1程序只定义后面条件必需要的标志。

本论文所提出的方法比这个标志定义最优化更加优越，这里不仅确定哪些标志定义是与主题无关因而是不必要的，而且还利用定义和使用该标志的指令组合集确定其布尔条件表示式。通过这样做，使目标高级语言程序(HLL)不依赖标志的概念及使用，就象任何真正的HLL程序一样没有这些东西。

5.1.2 清除多余的读写

Housel在文献[Hou73]中提出一个文本压缩方法，用来清除中间的读写寄存器指令(loads and stores)。这个方法工作在程序的三地址中间表示法上，而且由两个步骤组成：向前代入和向后代入。前一个步骤把一条赋值指令的源操作数代入一条使用相同结果操作数的后续指令 (如果发现结果在相同基本块中还没有被使用的话，即not busy)。后一步骤把一条赋值指令的结果操作数代入一条把该赋值指令源操作数定义为结果操作数的前面指令 (不是赋值指令)。在Knuth的MIXAL编译器编译生成的汇编代码中应用这个方法时指令缩减率最高达到40%。

Hopwood在文献[Hop78]中描述了一个表达式凝聚(expression condensation)的方法，使用向前代入(forward substitution)把两个或更多个中间指令结合成一个等价的表达式。这个方法指出变量或寄存器向前代入的5个必要条件和6个充分条件。这个方法基于变量使用分析(variable usage analysis)。他选择的这种控制流向图——每条中间指令为一个结点，而非每个基本块一个结点——必然地受到诸多条件的限制。这意味着变量跨结点边界向前代入，导致整个处理不必要地更加复杂。

本论文中提出的子过程间数据流分析定义了两个充分条件，在此条件下可以用某些指令使用的寄存器代入或代替到另一条指令里面，这些指令包括如push和pop这样的中间指令。这个方法不仅通过清除掉中间的寄存器和指令定义来发现表达式，而且还确定了子程序的实际参数、函数返回值，并去除伪高级指令。该方法是基于二进制程序最初的高级中间表示法——在语义上等价于低级中间表示法，并将它转换成一个HLL表示法。


第 5 章 数据流分析 - 优化的类型

5.2 优化的类型

这一节提出一个反编译器使用的代码改善转换。用来实现这些转换的技术在第5.3节和5.4节解释。在这一节中提出的优化以图5-2的流向图为例，其中基本块B1...B4属于主程序main，而B5...B7属于子程序_aNlshl (一个运行时支持例程)。在主程序中，寄存器si和di被用作寄存器变量，而且如果可能的话已经由语法分析器做了标志 (见第4章第4.2.1节)。

这些最优化的目标是清除掉低级语言概念之条件码、寄存器和中间过渡的指令，并引进多于两个操作数的表达式高级概念。出于这个目的，请注意现代编译器对push指令的多种用法。参数传递是这个指令最通常的使用，在子程序调用之前按照其调用约定规定的次序把参数入栈。如果在计算一个表达式时编译器用光了寄存器，就使用寄存器泄漏出(spill)。push和pop也用于跨子过程调用保存寄存器内容，以及用于把数值复制到寄存器。

图 5-2: 流向图样本

5.2.1 死寄存器清除

如果一个变量的数值在它定义之后没有被使用，那么在程序中该标识符此时是死的(无用的)。可以认为，定义一个死的标识符的指令是无用的，因此可以从代码中被清除掉。考虑下面来自图5-2中基本块B1的代码：

6 | ax = tmp / di
7 | dx = tmp % di
8 | dx = 3
9 | dx:ax = ax * dx
10 | si = ax

指令6定义寄存器ax，指令7定义寄存器dx，而指令8重新定义寄存器dx。在指令7定义和指令8定义之间没有使用寄存器dx，因此指令7对寄存器dx的定义是死的，而且由于这条指令的功能只是定义寄存器dx，所以这条指令是无用的。前面的指令序列被下面的代码代替：

6 | ax = tmp / di
8 | dx = 3
9 | dx:ax = ax * dx
10 | si = ax

在指令8上寄存器dx的定义被指令9的乘法使用，其中把该寄存器和寄存器ax重新定义。指令10使用寄存器ax，而在指令13对寄存器dx重新定义之前没有再使用寄存器dx，因此，最后的这个dx定义是死的，必须被清除掉。因为指令9不仅定义dx也定义ax，而ax不是死的，所以这条指令不是无用的，因为它还定义了一个活的(有用的)寄存器；因此，对该指令做些修改使之反映只有寄存器ax被定义这个事实，如下：

6 | ax = tmp / di
8 | dx = 3
9 | ax = ax * dx
10 | si = ax

5.2.2 死条件码清除

类似于死寄存器清除，如果一个条件码的值在重新定义之前没有被使用，那么在程序中这个条件码此时是死的。在这种情况下，该条件码的定义是无用的、不需要的，但是，如果该指令定义的标识符不是死的，那么定义这个条件码的指令还是有用的，因此，指令本身不一定要被清除掉。考虑下面来自图5-2中基本块B1的代码：

14 | cmp [bp-6]:[bp-8], dx:ax | ; cc-def = ZF, CF, SF
15 | jg B2 | ; cc-use = SF

指令14定义条件码为零(ZF)、进位(CF)和符号(SF)。指令15使用符号条件码。在进位条件码以及零条件码被重新定义之前，后面两个基本块都没有使用这些条件码，因此，在指令14中对这些条件码的定义是无用的、可以被清除掉。我们将指令14所持有的信息替换如下：

14

cmp [bp-6]:[bp-8], dx:ax

; cc-def = SF

5.2.3 条件码传播

条件码是一些标志，它们被机器用来指明一个条件的具体值。一般来说，有一些机器指令设置这些标志，一条指令设置1个到3个不同的标志，而且较少的指令使用那些标志，它们只使用1个或2个标志。在死条件码清除之后，条件码的多余定义被去掉了，因此，所有剩余的标志被后续指令使用。考虑来自图5-2中基本块B1的代码经过死条件码清除以后如下：

14 | cmp [bp-6]:[bp-8], dx:ax | ; cc-def = SF
15 | jg B2 | ; cc-use = SF

指令14通过比较两个操作数定义符号标志，指令15使用这个标志确定前面指令的第一个操作数是不是比第二个操作数更大。这两条指令在功能上等价于一条高级条件跳转指令——检查一个操作数是否大于另一个操作数。该指令可以被替换为：

15

jcond ([bp-6]:[bp-8] > dx:ax) B2

如此，清除掉指令14以及对条件码的所有引用。

5.2.4 寄存器参数

子程序使用寄存器参数来加速对那些参数的存取，而且免除在子程序调用之前将参数入栈带来的系统开销。许多运行时支持例程以及使用寄存器调用约定 (在有些编译器中可用) 编译生成的用户例程使用寄存器参数。考虑下面在图5-2中基本块B2的代码：

19 | cx = 4 | ; def = {cx}
20 | dx:ax = [bp-6]:[bp-8] | ; def = {dx, ax}
21 | call _aNlshl |

指令19定义寄存器cx，指令20定义寄存器dx:ax，指令21调用子程序_aNlshl。在图5-2中，子程序_aNlshl的第一个基本块B5，在定义寄存器cx的高字节部分 (即寄存器ch) 之后使用这个寄存器。因此，这个寄存器的低字节部分 (即寄存器cl) 的值是在子程序被调用之前的。类似地，基本块B6在寄存器dx:ax被子程序中定义之前使用了它们，因此，被使用的是这些寄存器在子程序调用之前的值。这三个寄存器在子程序中被定义之前被使用，它们是由调用者定义的，因此，它们是被传给这个_aNlshl子程序的寄存器参数。修改这个子程序的形式参数列表以反映这个事实：

formal_arguments(_aNlshl) = {arg1 = dx:ax, arg2 = cl}

在子程序里面，这些寄存器被它们的形式参数名称所代替。

5.2.5 函数返回寄存器(组)

返回一个数值的子程序叫做函数。函数通常在寄存器中返回数值，然后调用者子程序使用这些寄存器。考虑下面来自在图5-2中基本块B2和B3的代码：

20 | dx:ax = [bp-6]:[bp-8] | ; def = {dx, ax} use = {}
21 | call _aNlshl | ; def = {} use = {dx, ax, cl}
22 | [bp-6]:[bp-8] = dx:ax | ; def = {} use = {dx, ax}

指令21调用子程序_aNlshl。在子程序返回之后，指令22使用寄存器dx:ax。这些寄存器已经被前面基本块的指令20定义，但是因为在这两条指令之间有一个子程序调用，所以需要检查该子程序有没有对寄存器dx:ax做过任何修改。考虑在图5-2中基本块B6的代码经过死寄存器清除以后如下：

35 | dx:ax = dx:ax << 1
36 | cx = cx – 1
37 | jcond (cx <> 0) B6

记得在第5.2.4节dx:ax是寄存器参数。这些寄存器在指令35中被左移1位而修改了。实际上它们属于一个循环的部分，即，如果寄存器cx不等于零，则指令37跳回到最初的指令35。在循环结束之后，控制流被传给基本块B7——从这个子程序返回。在指令22上对寄存器dx:ax的引用是在这些寄存器经过修改以后。我们可以认为子程序_aNlshl是一个返回这些寄存器的函数，因此在指令21中对函数_aNlshl的调用被替换为：

21

dx:ax = call _aNlshl

; def = {dx, ax} use = {dx, ax, cl}

指令22使用在指令21中定义的两个寄存器，因此通过寄存器拷贝传播，我们得到以下代码：

22

[bp-6]:[bp-8] = call _aNlshl

函数_aNlshl的返回指令 (指令38) 被修改为返回寄存器dx:ax，成为以下代码：

38

ret dx:ax

5.2.6 寄存器拷贝传播

如果一个指令定义一个寄存器数值只被它的后续指令唯一地使用，那么这个指令是中间过渡的。在机器语言中，中间过渡的指令被用来将操作数的内容移入寄存器内，将一条指令的操作数移入被某个特定指令使用的寄存器之内，以及将放在寄存器里的计算结果储存到一个局部变量。考虑下面来自图5-2中基本块B2的代码：

16 | dx:ax = [bp-6]:[bp-8] | ; def = {dx, ax} use = {}
17 | dx:ax = dx:ax - [bp-2]:[bp-4] | ; def = {dx, ax} use = {dx, ax}
18 | [bp-6]:[bp-8] = dx:ax | ; def = {} use = {dx, ax}

指令16通过复制在bp-6处的长整型局部变量来定义长整型寄存器dx:ax。然后这个长整型寄存器在指令17被当作一个减法操作数使用。结果被放在相同的长整型寄存器，然后这个长整型寄存器在指令18被复制到在bp-6处的长整型局部变量。如上所见，指令16定义临时的长整型寄存器dx:ax在指令17被使用，而这条指令又重新定义寄存器，然后在指令18被复制到最后的局部变量。这些中间过渡的寄存器可以通过用局部变量代替它们而被清除掉，因此，在指令17中寄存器dx:ax被bp-6处的长整型局部变量替换——前面那条指令是用该变量定义这些寄存器：

17

dx:ax = [bp-6]:[bp-8] - [bp-2]:[bp-4]

于是指令16被去掉了。类似地，用指令17得到的结果代替在指令18中的长整型寄存器dx:ax，变成如下代码：

18

[bp-6]:[bp-8] = [bp-6]:[bp-8] - [bp-2]:[bp-4]

于是指令17被去掉了。最后的指令18是原来高级表达式的一个重建。

高级语言表达式表现为由一个或多个操作数组成的语法分析树，而机器语言表达式只允许最多两个操作数。在大多数情况下，这些操作数中必须有一个是一寄存器(组)，而结果也被放在一个寄存器(组)中。然后，最后的结果被复制到适当的标识符 (即，局部变量、参数、全局变量)。考虑下面来自图5-2中基本块B1的代码经过死寄存器清除以后如下：

3 | ax = si | ; def = {ax} use = {}
4 | dx:ax = ax | ; def = {dx, ax} use = {ax}
5 | tmp = dx:ax | ; def = {tmp} use = {dx, ax}
6 | ax = tmp / di | ; def = {ax} use = {tmp}
8 | dx = 3 | ; def = {dx} use = {}
9 | ax = ax * dx | ; def = {ax} use = {ax, dx}
10 | si = ax | ; def = {} use = {ax}

指令3通过复制整数寄存器变量si的内容来定义寄存器ax。在这个上下文中，寄存器变量被当作局部变量、而不是看成寄存器。指令4通过寄存器ax的符号扩展来用寄存器ax定义寄存器dx。然后指令5使用这些符号扩展的寄存器，把它们复制给寄存器tmp——它在指令6中被当做除法指令的被除数使用。局部的整数寄存器变量di被当做除数使用，而结果被放在寄存器ax上。在指令9中用这个结果和寄存器dx相乘，并重新定义寄存器ax。最后，结果被放在局部的寄存器变量si上。如上所见，这些指令能够在大部分被折叠入一个后续指令之后被清除，如下：指令3被代入指令4，得到：

4

dx:ax = si

于是指令3被去掉了。指令4被代入指令5，得到：

5

tmp = si

于是指令4被去掉了。指令5被代入指令6，得到：

6

ax = si / di

于是指令5被去掉了。指令6被代入指令9，得到：

9

ax = (si / di) * dx

于是指令6被去掉了。指令7被代入指令9，得到：

9

ax = (si / di) * 3

于是指令7被去掉了。最后，指令9被代入指令10，得到如下最后的代码：

10

si = (si / di) * 3

最后这条指令10代替前面的指令3至指令10全部。

5.2.7 实际参数

在子程序被调用之前，给子程序调用的实际参数或者被入栈，或者被放到寄存器上 (为寄存器参数)。这些参数能与子程序的形式参数列表相对应，可以把它们放在调用指令的实际参数列表中。考虑下面来自图5-2中基本块B4的代码经过寄存器拷贝传播以后如下：

24 | push [bp-6]:[bp-8]
28 | push (si * 5)
30 | push 66
31 | call printf

经过语法分析之后，printf的形式参数列表有一个尺寸为2字节的固定参数、和个数可变的其它参数。这个子过程使用的是C语言调用约定。指令31还保存了关于在子程序调用后出栈字节数目的信息：在这个例子里是8字节，因此，这个子程序的实际参数有8个字节；开头两个字节是固定参数的。指令24把4字节入栈，指令28把2字节入栈，指令30把另外2字节入栈，在这个实例中printf要求的参数字节总数为8字节。根据C语言调用约定 (即，最后一个入栈指令推入的是在参数列表中的第一个参数)，可以按照反向次序把这些标识符代入指令31的printf的实际参数列表。修改后得到以下代码：

31

call printf (66, si * 5, [bp-6]:[bp-8])

于是指令24, 28和30被去掉了。

类似地，把寄存器参数放到被调用子程序的实际参数列表上。考虑下面在图5-2中基本块B2的代码经过寄存器参数及函数返回寄存器检测和死寄存器清除之后如下：

19 | cl = 4 | ; def = {cl}
20 | dx:ax = [bp-6]:[bp-8] | ; def = {dx, ax}
22 | [bp-6]:[bp-8] = call _aNlshl | ; use = {dx, ax, cl}

指令19和20定义了给函数_aNlshl使用的寄存器参数，有关的寄存器定义被放在该函数的实际参数列表，如下：

22

[bp-6]:[bp-8] = call _aNlshl ([bp-6]:[bp-8], 4)

去掉了指令19和20、以及中间过渡的寄存器dx、ax和cl。

5.2.8 跨子过程调用的数据类型传播

子程序实际参数的类型必须与形式参数的类型相同。对于库子程序的情况，形式参数的类型是确实知道的，因而要求实际参数的类型匹配这些类型。如果有任何的不同，则要把形参的类型传播给实参。考虑下面来自在图5-2中基本块B4的代码在寄存器拷贝传播和实际参数的检测之后如下：

31

call printf (66, si * 5, [bp-6]:[bp-8])

printf的第一个形式参数类型是一个字符串 (即，C语言里的一个char*)。在机器语言中，字符串是作为数据常量储存在数据段或代码段中。对这些字符串的引用是通过存取它们所在的段和该段中的一个偏移。在我们的例子中，66是一个常数，而且因为它是给printf的第一个参数，所以它确实是指向位于数据段中的字符串的一个偏移量。把字符串类型传播给此第一个参数，在内存中找到该字符串，然后把它代入实际参数列表中，得到以下代码：

31

call printf ("c * 5 = %d, a = %ld"n", si * 5, [bp-6]:[bp-8])

给printf的所有其它参数的类型不能从形式参数列表中得到确定，所以只能信赖实际参数的类型 (即，在调用者中使用的类型)，不作修改。

5.2.9 寄存器变量清除

寄存器拷贝传播优化发现高级表达式、并且通过清除在表达式计算中使用的大部分中间过渡寄存器而去掉中间过渡的指令，正如第5.2.6节所见。在应用这个优化以后，在中间代码中只剩下不多几个寄存器 (如果还有的话)。这些残余的寄存器表现为寄存器变量或者共同的子表达式——编译器或优化器用它们来加速存取时间。这些寄存器等价于高级程序中的局部变量，因此在使用它们的相应子程序中使用新的局部变量代替。考虑下面来自图5-2中基本块B1的代码经过寄存器拷贝传播以后如下：

1 | si = 20
2 | di = 80
10 | si = si / di * 3

在这个子过程中，寄存器si和di被当做寄存器变量使用。这些寄存器在指令1和2中被初始化，而且稍后在指令10的表达式中被使用。让我们重新命名寄存器si为loc1、寄存器di为loc2，这样先前的代码就变成：

1 | loc1 = 20
2 | loc2 = 80
10 | loc1 = loc1 / loc2 * 3

于是所有对寄存器的引用被去掉了。

在应用前面讲到的所有转换之后，对应图5-2的最后的中间代码见图5-3所示。

图 5-3: 代码优化以后的流向图


第 5 章 数据流分析 - 全局的数据流分析

5.3 全局的数据流分析

为了在中间代码上进行改善代码的转换，反编译器需要在整个程序中收集关于寄存器和条件码的信息，并且跨不同的基本块传播该信息。该信息的收集是通过一个数据流分析过程，即求解方程式系统——与该程序不同点上的信息相关联的方程式。这一节定义在文献中可以找到的数据流问题和方程式；更多信息请参考文献[All72, AC76, ASU86b, FJ88b]。

5.3.1 数据流分析定义

定义 18

定义一个寄存器就是修改该寄存器的内容 (即，赋予它一个新的数值)。类似地，定义一个标志就是用一条指令修改这个标志。

定义 19

使用一个寄存器就是引用一个寄存器 (即，该寄存器的值被使用)。类似地，使用一个标志就是用一条指令引用这个标志。

定义 20

在基本块Bi中局部可用的定义d就是在Bi中的上一个d定义。

定义 21

在基本块Bi中局部地向后影响的使用u是一个先前不曾在Bi中定义过的使用。

（注：upwards exposed 向后影响、向上影响）

定义 22

在基本块Bi中一个定义d 延伸到基本块Bj，只要满足以下三个条件 [1]

注[1] 在这一章中使用的符号 → 表示一个路径。这个符号在第6章第6.3.1节中被定义。

1. d是来自Bi的一个局部可用的定义。

2. ∃Bi → Bj.

3. ∃Bi → Bj·∀Bk∈(Bi → Bj), k≠i ∧ k≠j, Bk不重新定义d。

定义 23

在基本块Bi中一个寄存器/标志的任何定义被认为终止所有延伸到Bi的该寄存器/标志的定义。

定义 24

如果d没有在基本块Bi中被重新定义，那么d定义在Bi中被保留(保持)。

定义 25

在基本块Bi的出口上可用的定义是以下二者之一：

1. 寄存器/标志的局部可用定义。

2. 延伸到Bi的寄存器/标志定义。

定义 26

寄存器/标志的一个使用u在基本块Bi中是向后影响的，只要满足以下二者之一：

1. u是从Bi局部地向后影响的。

2. ∃Bi → Bk · u是从Bk局部地向后影响的、而且不存在包含一个u定义的Bj (i≤j<k)。

(原文：∃Bi → Bk · u is locally upwards exposed from Bk ∧ ∄Bj, i≤j<k, which contains a definition of u.)

定义 27

定义d在基本块Bi中是活的(live)或活动的(active)

只要：

1. d延伸到Bi

2. 在Bi中d有一个向后影响的使用。

定义 28

如果在沿着从Bi出发的所有路径上d在重新定义之前被使用，那么在基本块Bi中的定义d是忙的(busy, 有时叫做非常忙 very busy)。

定义 29

如果在沿着从Bi出发所有路径上d在重新定义之前没有被使用，那么在基本块Bi中的定义d是死的(dead 无用的，亦即d不是忙的或者不是活的)。

定义 30

对于在指令i上的一个定义d，其定义-使用链(du-chain)是指令集j (亦即，能够被d影响的那些指令)，在其中d可能在重新定义之前被使用。

定义 31

对于在指令j上的一个使用u，其使用-定义链(ud-chain)是指令集i (亦即，能够影响u的那些声明)，在其中u被定义了。

定义 32

如果沿着某一个路径没有d的定义，那么这个路径是 d-clear 的。

5.3.2 数据流问题的分类

数据流问题就是一序列的方程式求解，使用在每个基本块中收集到的信息，并向整个完整的控制流向图传播它。在一个子过程的流向图里面传播的信息叫做子过程内的数据流分析(intraprocedural)，而跨子过程调用传播的信息叫做子过程间的数据流分析(interprocedural)。

从基本块中以集合的形式 (例如，gen()和kill() ) 收集关于被定义的或被终止的寄存器信息，然后在基本块入口和出口以集合的形式 (例如，in()和out()集合) 做统计。对于基本块Bi，一个典型的数据流方程式有以下形式：

out(Bi) = gen(Bi) ∪ (in(Bi) - kill(Bi))

它表示“在基本块Bi出口的信息或者是在Bi上生成的信息、或者是进入该基本块而且没有在该基本块内被终止的信息”。统计信息in()是从该图的前导结点收集，其方程式形式如下：

in(Bi) = ∪p∈Pred(Bi)out(p)

它收集在任何前导结点的出口可用的信息。因为从前导结点收集的信息是来自任何一个路径 (亦即，并非所有的路径需要有相同的信息)，所以这个数据流问题被归类为一个任何路径(any-path)问题。任何路径问题是前导结点与后续结点联合表现的方程式，具体取决于问题本身。

类似地，所有路径(all-paths)问题是用一个方程式给予说明的数据流问题，这个方程式在所有的路径中——从当前基本块到后继结点或前导结点——收集可用的信息，具体取决于问题的类型。

定义 33

把一个数据流问题认为是向前流，只要

1. out()集合是根据在相同基本块里的in()集合计算的。

2. in()集合是从前导基本块的out()集合计算的。

定义 34

把一个数据流问题认为是向后流，只要

1. in()集合根据在相同基本块里的out()集合计算的。

2. out()集合是从前导基本块的in()集合计算的。

数据流问题的分类是由在图5-4中所示的分类法导出。对于每个向前流和向后流问题，所有路径方程式和任何路径方程式的定义是根据后继结点和前导结点。这个表是从文献[FJ88b]拿来的。

 | 向前流 | 向后流
任何路径 | Out(Bi) = Gen(Bi) ∪ (In(Bi) - Kill(Bi)) In(Bi) = ∪p∈Pred(Bi)Out(p) | In(Bi) = Gen(Bi) ∪ (Out(Bi) - Kill(Bi)) Out(Bi) = ∪s∈Succ(Bi)In(s)
所有路径 | Out(Bi) = Gen(Bi) ∪ (In(Bi) - Kill(Bi)) In(Bi) = ∩p∈Pred(Bi)Out(p) | In(Bi) = Gen(Bi) ∪ (Out(Bi) - Kill(Bi)) Out(Bi) = ∩s∈Succ(Bi)In(s)

图 5-4: 数据流分析方程式

数据流方程式

一般来说，数据流方程式没有唯一解；但是在数据流问题中，我们关心的解是满足方程式的最小或最大的固定点解。对于向前流问题，找到这个解是通过把头基本块的in(B)集合的初始值设定为一个边界条件，而对于向后流问题，找到这个解是通过把出口基本块的out(B)集合的值设定为一个边界条件。根据该问题的说明，这些边界条件集合被初始化为空集或全集 (即，所有可能的值)。

子过程内的数据流问题只求解与一个子程序有关的方程式，而不考虑被其它子程序使用或定义的值。由于这些问题是流向不敏感的(flow insensitive)，因此为所有最初结点 (对于向前流问题) 或所有出口结点 (对于向后流问题) 设定边界条件。子过程间的数据流问题则求解与一个程序的子程序们有关的方程式，它考虑到由被调用子程序使用或定义的值。信息在调用图的子程序之间流动。这些流向敏感的(flow sensitive)问题只为程序调用图的main子程序设定边界条件，而所有其它子程序都是从调用图中所有的前导结点 (对于向前流问题) 或所有的后继结点 (对于向后流问题) 统计信息。这一节讲述，延伸寄存器、活寄存器、可用寄存器和忙寄存器的数据流方程式求解。

延伸寄存器定义分析确定哪些寄存器沿着一些路径延伸到某个特定基本块，因此在下面，向前流-任何路径方程式被使用：

定义 35

令

l Bi是一个基本块

l ReachIn(Bi) 是延伸到Bi入口的寄存器的集合

l ReachOut(Bi) 是延伸到Bi出口的寄存器的集合

l Kill(Bi) 是在Bi中被终止的寄存器的集合

l Def(Bi) 是在Bi中被定义的寄存器的集合

则有

ReachIn(Bi) = ∪p∈Pred(Bi)ReachOut(p) 如果Bi不是头结点的话；

否则为空集

ReachOut(Bi) = Def(Bi) ∪ (ReachIn(Bi) - Kill(Bi))

活寄存器分析确定一个寄存器是否在沿着一些路径上被使用，因此在下面，向后流-任何路径方程式被使用：

定义 36

令

l Bi是一个基本块

l LiveIn(Bi) 是在Bi入口上活的寄存器的集合

l LiveOut(Bi) 是在Bi出口上活的寄存器的集合

l Use(Bi) 是在Bi中被使用的寄存器的集合

l Def(Bi) 是在Bi中被定义的寄存器的集合

则有

LiveOut(Bi) = ∪s∈Succ(Bi)LiveIn(s) 如果Bi不是一个返回结点的话；

否则为空集

LiveIn(Bi) = Use(Bi) ∪ (LiveOut(Bi) - Def(Bi))

可用寄存器分析确定哪些寄存器在沿着图的所有路径上是可用的，因此在下面，向前流-所有路径方程式被使用：

定义 37

令

l Bi是一个基本块

l AvailIn(Bi) 是在Bi入口上可用的寄存器的集合

l AvailOut(Bi) 是在Bi出口上可用的寄存器的集合

l Compute(Bi) 在Bi中被计算而且没有被终止的寄存器的集合

l Kill(Bi) 是在Bi中因一个赋值指令而被终止的寄存器的集合

则有

AvailIn(Bi) = ∩p∈Pred(Bi)AvailOut(p) 如果Bi不是头结点的话；

否则为空集

AvailOut(Bi) = Compute(Bi) ∪ (AvailIn(Bi) - Kill(Bi))

忙寄存器分析确定哪些寄存器在沿着图的所有路径上是忙的，因此在下面，向后流-所有路径方程式被使用：

定义 38

令

l Bi是一个基本块

l BusyIn(Bi) 是在Bi入口上忙的寄存器的集合

l BusyOut(Bi) 是在Bi出口上忙的寄存器的集合

l Use(Bi) 是在Bi中被终止之前被使用的寄存器的集合

l Kill(Bi) 是在Bi中被使用之前被终止的寄存器的集合

则有

BusyOut(Bi) = ∩s∈Succ(Bi)BusyIn(s) 如果Bi不是一个返回结点的话；

否则为空集

BusyIn(Bi) = Use(Bi) ∪ (BusyOut(Bi) - Kill(Bi))

寻找一个寄存器定义的使用的问题，也就是一个du-chain问题，是求解一个向后流-任何路径问题。类似地，寻找一个寄存器的一个使用的所有定义的问题，也就是一个ud-chain问题，是求解一个向前流-任何路径问题。前面的数据流问题概括为图5-5的表。

 | 向前流 | 向后流
任何路径 | Reach ud-chains | Live du-chains
所有路径 | Available Copy propagation | Busy Dead

图 5-5: 数据流问题 - 概述

最近，精确的子过程间活变量的方程式在链接时系统(link-time system)上作为代码最优化的一部分被提出来 [SW93]。使用两个步骤方式来去掉在无关子程序 (调用相同的其它子程序) 上的信息传播。在调用图中每个调用结点有两个结点：call结点——有一个去往被调用者子程序头结点的出边，和ret_call结点——有一个来自被调用者子程序返回结点的入边。在第一个步骤内，信息流动只经过普通结点和调用边；从调用图中把返回边去掉了。在第二个步骤内，信息流动只经过普通结点和返回边；从调用图中把调用边去掉了。这个步骤使用在第一个步骤中计算的统计信息。因为该信息从调用者流向被调用者，或者反过来，所以这个方法提供的信息比起文献中的其它方法提供信息更加精确。

定义39提出用于精确的子过程间寄存器分析的方程式。第一个步骤中求解活寄存器方程式和死寄存器方程式，并且对调用图中每个子程序做统计放在PUse()和PDef()集合中。因为第二个步骤中也求解活寄存器方程式，所以这些方程式用步骤编号加以区别 (例如，LiveIn1()是第一个步骤的，而LiveIn2()是第二个步骤的)。call基本块和ret_call基本块有各自单独的方程式。对于活的方程式和死的方程式，初始边界条件是空集。

定义 39

令

l Bi是一个非call和ret_call类型的基本块

l LiveIn1(Bj) 是步骤1期间在Bj的入口上活的寄存器的集合

l LiveOut1(Bj) 是在步骤1期间在Bj的出口上活的寄存器的集合

l DeadIn(Bj) 是在Bj的入口上已经被终止的寄存器的集合

l DeadOut(Bj) 是在Bj的出口上已经被终止的寄存器的集合

l Use(Bj) 是在Bj中被使用的寄存器的集合

l Def(Bj) 是在Bj中被定义的寄存器的集合

l LiveIn2(Bj) 是步骤2期间在Bj的入口上活的寄存器的集合

l LiveOut2(Bj) 是步骤2期间在Bj的出口上活的寄存器的集合

那么，精确的子过程间的活寄存器分析被计算如下：

l 步骤 1：

LiveOut1(Bi) = ∪s∈Succ(Bi)LiveIn1(s) 如果Bi不是一个返回结点的话；

否则为空集

LiveIn1(Bi) = Use(Bi) ∪ (LiveOut1(Bi) - Def(Bi))

DeadOut(Bi) = ∩s∈Succ(Bi)DeadIn(s) 如果Bi不是一个返回结点的话；

否则为空集

DeadIn(Bi) = Def(Bi) ∪ (DeadOut(Bi) - Use(Bi))

LiveOut1(ret_call) = ∪s∈Succ(ret_call)LiveIn1(s)

LiveIn1(ret_call) = LiveOut1(ret_call)

LiveOut1(call) = LiveIn1(entry) ∪ (LiveOut1(ret_call) - DeadIn(entry))

LiveIn1(call) = LiveOut1(call)

DeadOut(ret_call) = ∩s∈Succ(ret_call)DeadIn(s)

DeadIn(ret_call) = DeadOut(ret_call)

DeadOut(call) = DeadIn(entry) ∪ (DeadOut(ret_call) - LiveIn1(entry))

DeadIn(call) = DeadOut(call)

l 子程序统计：∀p子程序，有

PUse(p) = LiveIn1(entry)

PDef(p) = DeadIn1(entry)

l 步骤 2：

LiveOut2(Bi) = ∪s∈Succ(Bi)LiveIn2(s) 如果Bi不是main的返回结点的话；

否则为空集

LiveIn2(Bi) = Use(Bi) ∪ (LiveOut2(Bi) - Def(Bi))

LiveOut2(ret_call) = ∪s∈Succ(ret_call)LiveIn2(s)

LiveIn2(ret_call) = LiveOut2(ret_call)

LiveOut2(call) = PUse(p) ∪ (LiveOut2(ret_call) - PDef(p))

LiveIn2(call) = LiveOut2(call)

图 5-6: 活寄存器例子图

例子 8

考虑图5-6的调用图。这个程序有一个main子过程和两个子程序。子过程间的活寄存器分析，正如定义39所解释的，提供了以下关于各结点的统计信息：

³ 步骤 1：

子程序 | 结点 | 定义(Def) | 使用(Use) | LiveIn1 | LiveOut1 | DeadIn | DeadOut
P1 | 12 | ∅ | ∅ | ∅ | ∅ | ∅ | ∅
 | 11 | ∅ | {cx} | {cx} | ∅ | ∅ | ∅
 | 10 | {ax,cx} | ∅ | ∅ | {cx} | {ax,cx} | ∅
 | 9 | {ax} | ∅ | ∅ | ∅ | {ax} | ∅
 | 8 | ∅ | {dx} | {dx} | ∅ | {ax} | {ax}
P2 | 16 | ∅ | {ax} | {ax} | ∅ | ∅ | ∅
 | 15 | ∅ | ∅ | {ax} | {ax} | ∅ | ∅
 | 14 | ∅ | ∅ | {dx} | {dx} | {ax} | {ax}
 | 13 | {dx} | ∅ | ∅ | {dx} | {ax,dx} | {ax}
main | 7 | ∅ | ∅ | ∅ | ∅ | ∅ | ∅
 | 6 | ∅ | ∅ | ∅ | ∅ | ∅ | ∅
 | 5 | ∅ | ∅ | ∅ | ∅ | {ax,dx} | {ax,dx}
 | 4 | ∅ | {ax,bx} | {ax,bx} | ∅ | {dx} | {ax,dx}
 | 3 | ∅ | ∅ | {ax,bx} | {ax,bx} | {dx} | {dx}
 | 2 | ∅ | ∅ | {bx,dx} | {bx,dx} | {ax} | {ax}
 | 1 | {bx,dx} | ∅ | ∅ | {bx,dx} | {ax,bx,dx} | {ax}

³ 子程序统计：

子程序 | 使用(PUse) | 定义(PDef)
P1 | {dx} | {ax}
P2 | ∅ | {ax,dx}
main | ∅ | {ax,bx,dx}

³ 步骤 2：

子程序 | 结点 | 定义(Def) | 使用(Use) | LiveIn2 | LiveOut2
P1 | 12 | ∅ | ∅ | {ax,bx} | {ax,bx}
 | 11 | ∅ | {cx} | {ax,bx,cx} | {ax,bx}
 | 10 | {ax,cx} | ∅ | {bx} | {ax,bx,cx}
 | 9 | {ax} | ∅ | {bx} | {ax,bx}
 | 8 | ∅ | {dx} | {bx,dx} | {bx}
P2 | 16 | ∅ | {ax} | {ax} | ∅
 | 15 | ∅ | ∅ | {ax} | {ax}
 | 14 | ∅ | ∅ | {dx} | {dx}
 | 13 | {dx} | ∅ | ∅ | {dx}
main | 7 | ∅ | ∅ | ∅ | ∅
 | 6 | ∅ | ∅ | ∅ | ∅
 | 5 | ∅ | ∅ | ∅ | ∅
 | 4 | ∅ | {ax,bx} | {ax,bx} | ∅
 | 3 | ∅ | ∅ | {ax,bx} | {ax,bx}
 | 2 | ∅ | ∅ | {bx,dx} | {bx,dx}
 | 1 | {bx,dx} | ∅ | ∅ | {bx,dx}

其它类型的数据流方程式也被用来解决数据流问题。考虑寻找所有根据定义22延伸到某个基本块Bi的寄存器定义的问题。在这个定义中，延伸问题是根据可用问题来定义；如果一个寄存器在沿着从一个前导结点到当前结点的某路径上是可用的，那么说该寄存器延伸到与当前结点相应的基本块。这个问题等价于寻找ReachIn()集合。下面的方程式被用来解这个问题：

定义 40

令

1. Bi是一个基本块

2. Reach(Bi) 是延伸到Bi的寄存器的集合

3. Avail(Bi) 是来自Bi的可用寄存器的集合

则有

Reach(Bi) = ∪p∈Pred(Bi)Avail(p)

从一个基本块里面寻找可用寄存器的问题是根据局部地可用的和延伸的定义 (见定义25) 来定义的。这个问题等价于寻找AvailOut()集合。下面的方程式被使用：

定义 41

令

1. Bi是一个基本块

2. Avail(Bi) 是来自Bi的可用寄存器的集合

3. Reach(Bi) 是延伸到Bi的寄存器的集合

4. Propagate(Bi) 是在整个Bi中被传播的寄存器的集合

5. Def(Bi) 是在Bi中局部地可用的定义的集合

则有

Avail(Bi) = Def(Bi) ∪ (Reach(Bi) ∩ Propagate(Bi))

最后，根据延伸定义和向后影响使用，定义27定义了活寄存器问题。这个问题等价于求解LiveIn()集合的方程式。下面的方程式被使用：

定义 42

令

1. Bi是一个基本块

2. Live(Bi) 是在Bi的入口上活的寄存器的集合

3. Reach(Bi) 是延伸到Bi的寄存器的集合

4. UpwardExp(Bi) 是在Bi中向后影响的寄存器的集合

则有

Live(Bi) = Reach(Bi) ∩ UpwardExp(Bi)

5.3.3 数据流方程式求解

给定一个子程序的控制流向图，数据流方程式可以用两个方法求解：迭代方法，用一个解反复计算直至遇到一个固定点；区间方法，为某一个区间建立一个解，然后向那个区间里的所有结点传播这个解。这些方程式没有唯一解，但是我们把最小的解作为答案。迭代算法在文献[ASU86b]中讲述，区间算法在文献[All72, AC76]中给出。


第 5 章 数据流分析 - 代码改善优化

5.4 代码改善优化

这一节描述如何将数据流信息应用于反编译器进行代码改善的优化。这些最优化的目标是清除掉所有对条件码和寄存器的引用，因为在高级语言中不存在这些概念，另外就是在反编译产生的程序中重新生成可用的高级表达式。这一节引用最初的图5-2，为方便读者把它复制到图5-7。

图 5-7: 优化前的流向图

5.4.1 死寄存器清除

如果一个寄存器用一条指令定义了、而且在它被后来的指令重新定义之前没有被使用，那么这个寄存器是死的。如果定义死寄存器的那条指令只定义了这一个寄存器，我们认为该指令是无用的，因而把它清除掉。另一方面，如果该指令还定义了其它寄存器(组)，那么该指令仍然是有用的，但是不应该再让它定义死寄存器了。在这种情况下，我们把该指令修改成反映该事实的。死寄存器分析是利用有关寄存器的定义-使用链来求解，因为定义-使用链说明了哪些指令使用了该定义的寄存器；如果没有指令使用这个寄存器，那么该寄存器是死的。考虑下面来自在图5-7中基本块B1的代码，以及关于所有被定义的寄存器的定义-使用(du)链。注意，寄存器变量没有du-chain，因为它们表示局部变量而非临时寄存器。

6 | ax = tmp / di | ; du(ax) = {9}
7 | dx = tmp % di | ; du(dx) = {}
8 | dx = 3 | ; du(dx) = {9}
9 | dx:ax = ax * dx | ; du(ax) = {10} du(dx) = {}
10 | si = ax |

从上面可以看出，寄存器dx在指令7和指令9上被定义了，但是都没有在重新定义之前被使用，因此它在这两条指令中是死的。指令7只定义这一个寄存器，所以指令7是多余的、可以被清除掉。指令9还定义了寄存器ax，所以指令9被修改以反映事实——在该指令中不再定义dx。结果代码如下：

6 | ax = tmp / di | ; du(ax) = {9}
8 | dx = 3 | ; du(dx) = {9}
9 | ax = ax * dx | ; du(ax) = {10}
10 | si = ax |

图5-8的算法寻找所有的死寄存器并且把它们从代码中去掉。

procedure DeadRegElim

/* Pre: du-chains on registers have been computed for all instructions.

* Post: dead registers and instructions are eliminated */

for (each basic block b) do

for (each instruction i in b) do

for (each register r defined in i) do

if (du(r) = {}) then

if (i defines only register r) then

eliminate instruction i

else

modify instruction i not to define register r

def(i) = def(i) - {r}

end if

end if

end for

end for

end for

end procedure

图 5-8: 死寄存器清除算法

在整个反编译优化过程中稍后还会用到du-chains，因此du-chains必须被更新以反映有些指令已被清除：如果某一条指令i之所以被清除是由于死寄存器定义r——这个r是用其它寄存器定义的 (即，r=f(r1, ..., rn), n≥1)，那么这些寄存器在指令i中的使用不再存在，因而，与定义这些寄存器 (在指令i上使用的) 的指令相对应的du-chains将被修改成不再包括指令i。这个问题的求解开始于检查指令i的使用-定义链——它说明哪一条指令j定义了在指令i中所使用的寄存器。再考虑来自基本块B1的代码片断以及与那些寄存器有关的du链和ud链 (使用-定义链)：

5 | tmp = dx:ax | ; du(tmp) = {6,7} | ; ud(dx) = {4} ud(ax) = {4}
6 | ax = tmp / di | ; du(ax) = {9} | ; ud(tmp) = {5}
7 | dx = tmp % di | ; du(dx) = {} | ; ud(tmp) = {5}
8 | dx = 3 | ; du(dx) = {9} |
9 | dx:ax = ax * dx | ; du(ax) = {10} du(dx)={} | ; ud(ax) = {6} ud(dx) = {8}
10 | si = ax | ; | ; ud(ax) = {9}

当指令7被发现是多余的时候，检查它的ud-chain，看看是哪些指令定义了与死寄存器dx计算有关的寄存器(组)。从上面看出，寄存器tmp在指令7中被使用，而且它是在指令5中被定义 (ud(tmp) = {5})，指令5的du-chain里面有指令6和指令7。因为指令7要被清除，所以指令5的du-chain必须被更新以使之只延伸到指令6，在死寄存器清除和du-chain更新之后我们得到以下代码：

5 | tmp = dx:ax | ; du(tmp) = {6} | ; ud(dx) = {4} ud(ax) = {4}
6 | ax = tmp / di | ; du(ax) = {9} | ; ud(tmp) = {5}
8 | dx = 3 | ; du(dx) = {9} |
9 | ax = ax * dx | ; du(ax) = {10} | ; ud(ax) = {6} ud(dx) = {8}
10 | si = ax | ; | ; ud(ax) = {9}

图5-9中的算法解决在进行死寄存器清除的时候更新du-chain这个问题。一旦发现某一条指令是多余的，在它被清除之前，应该由deadRegElim调用这个算法。注意，一个特定寄存器的du-chain可能变成空的，这导致更多的死寄存器被递归地从代码中清除。

procedure UpdateDuChain (i: instructionNumber)

/* Pre: ud and du-chains on registers have been computed for all instructions.

* instruction i is to be eliminated.

* Post: no du-chain references instruction i any more */

for (each register r used in instruction i) do

for (each instruction j in ud(r)) do

if (i in du(r) at instruction j) then

du(r) = du(r) - {i}

if (du(r) = {}) then

if (j defines only register r) then

updateDuChain (j)

eliminate instruction j

else

modify instruction j not to define register r

def(j) = def(j) - {r}

end if

end if

end if

end for

end for

end procedure

图 5-9: du-chains的更新

5.4.2 死条件码清除

如果一个条件码(或标志)用一条指令定义了而且在重新定义之前没有被使用，那么它是死的。因为一个条件码的定义是某一条指令的边缘效应 (副作用，亦即该指令还有其它功能)，所以清除死标志不造成某一条指令多余，因此指令不因为死标志而被清除。在这个分析中，一旦确定某一个条件码是死的，就不再需要让那一条指令来定义它了，因此，把这个信息从那条指令中去掉。关于条件码的信息以集合的形式保持在一条指令中：一个被定义的条件的集合和一个被使用的条件的集合(即，置位)。用来寻找死条件码的分析类似于死寄存器分析——使用du-chains。但是这一回不需要ud-chains，因为没有指令被清除。考虑下面来自在图5-7中基本块B1的代码以及与条件码有关的du-chains：

14 | cmp [bp-6]:[bp-8],dx:ax | ; def={ZF,CF,SF} | ; du(SF)={15} du(CF,ZF)={}
15 | jg B2 | ; use={SF} |

指令14定义条件码ZF (零标志)、CF (进位标志)和SF (符号标志)。通过检查这些条件的du-chains，我们发现以后只有SF标志被使用，因此，在这个定义之后其它标志没有被使用，也就是死的。把这些标志的定义从与指令14关联的条件码中去掉，得到以下代码：

14 | cmp [bp-6]:[bp-8],dx:ax | ; def = {SF} | ; du(SF)={15}
15 | jg B2 | ; use = {SF} |

图5-10中的算法寻找所有的死条件码并且清除它们。

procedure DeadCCElim

/* Pre: du-chains on condition codes have been computed for all instructions.

* Post: dead condition codes are eliminated */

for (each basic block b) do

for (each instruction i in b) do

for (each condition code c in def(i)) do

if (du(c) = {}) then

def(i) = def(i) - {c}

end if

end for

end for

end for

end procedure

图 5-10: 死条件码清除算法

5.4.3 条件码的传播

死条件码清除去掉了所有在程序中没有被使用的条件码的定义。剩下的所有条件码定义在其定义后面的指令中有一个使用，而且将在弄懂这些条件的本质以后把它们清除。该问题能够用条件码的du-chains或者ud-chains解决；不论用此二者中哪一个办法都得到相等的解。考虑下湎来自图5-7中基本块B1的代码以及条件码的ud-chains：

14 | cmp [bp-6]:[bp-8],dx:ax | ; def = {SF} |
15 | jg B2 | ; use = {SF} | ; ud(SF) = {14}

对于一个特定的标志(组)使用，我们寻找定义该标志(组)的指令，而且根据在使用该标志的指令中隐含的布尔条件合并这两条指令。指令15使用了标志SF，而且隐含检查布尔条件——大于。指令14定义了在指令15中被使用的标志，而且比较第一个标识符 ([bp-6]:[bp-8]) 和第二个标识符 (dx:ax)。如果第一个标识符比第二个标识符更大，则把SF置位。原来由这条指令设置的其它标志已经通过死条件码清除去掉了，所以不作考虑。从这两条指令的功能明显地看出，该条件的传播——给使用这个条件的指令设置SF (即，比较两个标识符)——将清除掉定义该条件的指令，而且将为使用该条件的指令生成一个布尔条件式。在我们的例子中，SF的传播产生以下代码：

15

jcond ([bp-6]:[bp-8] > dx:ax) B2

于是，清除了对标志的所有引用。

在扩展的基本块里面使用条件码

定义 43

一个扩展的基本块是一个基本块序列B1...Bn，如此，对于1≤ i <n，Bi是基本块Bi+1的唯一前导，而对于1< i ≤n，Bi只是一个条件跳转指令。

在大多数程序中，标志定义和标志使用出现在相同基本块中。在一些标准的情况中，标志定义不与标志使用在相同基本块里面，而在相同的扩展基本块里面，正如大下面的代码中：

1 | cmp ax, dx | ; def = {SF,ZF} | ; du(SF) = {2} du(ZF) = {3}
2 | jg Bx | ; use = {SF} | ; ud(SF) = {1}
3 | je By | ; use = {ZF} | ; ud(ZF) = {1}

在这个例子中，指令1定义两个标志：SF和ZF。符号标志被指令2 (在相同基本块里面) 使用，而零标志被指令3 (在一个不同基本块中但是在相同的扩展基本块里面) 使用。来自指令1的符号条件被传播给指令2，指令2检查‘大于’布尔条件，所以指令2被替换成：

1 | cmp ax, dx | ; def = {ZF} | ; du(ZF) = {3}
2 | jcond (ax > dx) Bx |  |
3 | je By | ; use = {ZF} | ; ud(ZF) = {1}

因为指令1还定义了零标志，该标志在指令3中被使用，所以还不能把指令1去掉，因为我们需要知道组成布尔条件这部分的标识符。接着分析，当指令3被分析的时候，指令1中的零标志定义被传播到在指令3中该标志的使用，然后生成一个检查两个寄存器相等的布尔条件。因为在指令1中没有其它条件码定义，所以这个指令现在安全地被清除了，得到以下代码：

2 | jcond (ax > dx) Bx
3 | jcond (ax = dx) By

该算法可以被推广为传播在两个或更多基本块中被定义的条件码 (亦即，通过单个布尔条件的与运算)，但是实践上还没有这个需要，因为对于优化良好的编译器，试图跨基本块边界跟踪标志定义几乎是空白 [Gou93]。图5-11中的算法在一个扩展的基本块里面传播条件代码。

procedure CondCodeProp

/* Pre: dead-condition code elimination has been performed.

* the sets of defined and used flags has been computed for all

* instructions.

* ud-chains on condition codes have been computed for all instructions.

* Post: all references to condition codes have been eliminated */

for (all basic blocks b in postorder)

for (all instructions i in b in last to first order)

if (use(i) <> {}) then /* check for a flag use */

for (all flags f in use(i)) do

j = ud(f)

def(j) = def(j) - {f} /* remove it from the set */

propagate identifiers from instruction j to the Boolean

condition in instruction i (do not store repetitions).

if (def(j) = {}) then

eliminate instruction j.

end if

end for

end if

end for

end for

end procedure

图 5-11: 条件码传播算法

从这个分析得到的布尔条件表达式生成用图5-12中BNF (巴科斯范式Backus normal form) 描述的表达式。这些表达式被保存为中间高级表示法的语法分析树。

Cond | ::= (Cond ∧ RelTerm) | (Cond|RelTerm) | RelTerm
RelTerm | ::= Factor op Factor
Factor | ::= register | localVar | literal | parameter | global
op | ::= ≤ | < | = | > | ≥ | <>

图 5-12: 条件表达式的BNF

5.4.4 寄存器参数

寄存器调用约定是编译器用来加速子程序调用。在大多数现代编译器中这是一个可用的选项，而且也在编译器运行时支持例程所使用。给定一个子程序，寄存器参数转化成在子程序中要使用的寄存器——在该子程序定义这些寄存器之前；也就是说，在整个子程序中向后影响寄存器的使用。考虑来自在图5-7中子程序_aNlshl的基本块B5和B6的代码经过条件码清除以后如下：

33 | ch = 0 |
34 | jcond (cx = 0) B7 | ; ud(ch) = {33} ud(cl) = {}
35 | dx:ax = dx:ax << 1 | ; ud(dx:ax) = {}

指令34使用寄存器cx，在这个子程序中它还没有被完整地定义：高字节部份，寄存器ch在指令33中被定义，但是低字节部分根本被定义过。在指令35中有类似状况：寄存器dx:ax在它被使用之前没有在该子程序中被定义过。寄存器在被定义之前被使用的有关信息由子过程内的活寄存器分析做了统计：一个寄存器在使用它的基本块的入口上是活的。这个分析是通过求解定义36的子过程内的活寄存器方程式，或者求解精确的子过程间的活寄存器分析第一个步骤的方程式 (定义39)。在子程序_aNlshl上进行活寄存器分析得到以下LiveIn集合和LiveOut集合：

基本块 | LiveIn | LiveOut
B5 | {dx,ax,cl} | {dx,ax}
B6 | {dx,ax} | {}
B7 | {} | {}

头基本块B5的LiveIn寄存器集合的统计是被该子程序使用的寄存器参数的集合；在这个例子中就是dx、ax和cl。这个子程序的形式参数列表被更新以反映这两个参数：

formal_arguments(_aNlshl) = (arg1 = dx:ax, arg2 = cl)

不妨认为_aNlshl子程序使用这些寄存器。一般来说，任何使用寄存器参数的子程序使用这些寄存器，因此，对其中某一个子程序的调用 (即，一个call指令) 也就是使用那些寄存器，正如下面的指令：

21

call _aNlshl

; use = {dx, ax, cl}

图5-13中的算法寻找给一个子程序的寄存器参数(如果有的话)的集合。

procedure FindRegArgs (s: subroutineRecord)

/* Pre: intraprocedural live register analysis has been performed on

* subroutine s.

* Post: uses(s) is the set of register arguments of subroutine s. */

if (LiveIn(headerNode(s)) <> {}) then

uses(s) = LiveIn(headerNode(s))

else

uses(s) = {}

end if

end procedure

图 5-13: 寄存器参数算法

5.4.5 函数返回寄存器(组)

函数在寄存器返回结果，而没有机器指令表明哪些寄存器被函数返回。在函数返回之后，调用者在重新定义它们之前使用函数返回的寄存器 (即，这些寄存器在函数调用后跟着的基本块入口上是活的)。我们跨子程序边界传播这个寄存器信息，并且用一个延伸的活的寄存器分析求解。考虑下面来自图5-7中基本块B2和B3的代码：

20 | dx:ax = [bp-6]:[bp-8] | ; def = {dx, ax} use = {}
21 | call _aNlshl | ; def = {} use = {dx, ax, cl}
22 | [bp-6]:[bp-8] = dx:ax | ; def = {} use = {dx, ax}

指令22使用寄存器dx:ax；这些寄存器在指令20中被定义，但是在定义和使用之间出现一个子程序调用。因为它不知道这个子程序是一个子过程还是一个函数，所以假定在指令20中的定义延伸到指令22的使用是不安全的。需要根据统计信息确定哪些定义延伸到指令22。在子程序_aNlshl上执行一个子过程内的延伸寄存器分析，我们得到以下ReachIn和ReachOut集合：

基本块 | ReachIn | ReachOut
B5 | {} | {ch}
B6 | {ch} | {cx,dx,ax}
B7 | {cx,dx,ax} | {cx,dx,ax}

这个分析表明寄存器cx、dx和ax的最后一个定义延伸到子程序结束 (即，基本块B7的ReachOut集合)。调用者子程序只使用其中一些延伸的寄存器，因此有必要确定哪些寄存器向后影响子程序调用的后继基本块(组)。通过求解定义36的子过程间的活寄存器方程式、或者通过精确的子过程间的活寄存器分析的第二个步骤 (定义39)，我们计算出这个信息。因为信息必须是准确的，活寄存器分析方程式以优化方式求解；亦即，如果一个寄存器的使用在一个后继结点中出现，那么该寄存器是活的。以下LiveIn和LiveOut集合是从图5-7的例子计算而来：

基本块 | LiveIn | LiveOut
B1 | {} | {}
B2 | {} | {dx,ax}
B3 | {dx,ax} | {}
B4 | {} | {}
B5 | {dx,ax,cl} | {dx,ax}
B6 | {dx,ax} | {dx,ax}
B7 | {dx,ax} | {dx,ax}

在延伸到基本块B3的这三个寄存器当中，只有其中两个寄存器被使用 (即，属于B3的LiveIn)：dx:ax，因此一旦被调用的子程序已经完成，这两个寄存器是该函数返回的寄存器，也是我们唯一关心的寄存器。检查返回的寄存器的条件是：

ReachOut(B7) ∩ LiveIn(B3) = {dx,ax}

一般来说，一个子程序可以有一个或多个返回结点，因此该子程序的ReachOut()集合必须包含有全部延伸到每一个出口的寄存器。以下方程式统计某个子程序s的ReachOut信息：

ReachOut(s) = ∩Bi=returnReachOut(Bi)

一旦某一个子程序已经被确定是一个函数、而且该函数返回的寄存器(组)也已经被确定，就把这个信息传播到两个地方：在该函数里的返回指令(组)，和调用这个函数的指令。对于前者，所有的返回基本块有一条ret指令；我们把这个指令修改成返回寄存器 (被函数返回的那些)。在我们的例子中，图5-7中的基本块B7的指令38被修改成以下代码：

38

ret dx:ax

对于后者，任何函数调用指令 (即call指令) 用一条asgn赋值指令替换——把被定义的寄存器(组)作为该指令左部，而把函数调用作为右部，如下代码：

21

dx:ax = call _aNlshl

; def = {dx,ax} use = {dx, ax, cl}

该指令被转换成一个asgn指令，而且在指令左部(lhs)定义该寄存器。

图5-14中的算法确定哪些子程序是函数 (即，在一个寄存器(组)中返回数值)。千万注意，对于返回寄存器(组)没有被使用的库函数来说，该调用不转换成一个asgn指令而是保持作为一个call指令。

procedure FindRetRegs

/* Pre: interprocedural live register analysis has been performed.

* intraprocedural reaching register definition has been performed.

* Post: def(f) is the set of registers returned by a function f.

* call instruction to functions are modified to asgn instructions.

* ret instructions of functions return the function return registers.*/

for (all subroutines s) do

for (all basic blocks b in postorder) do

for (all instructions i in b) do

if (i is a call instruction to subroutine f) then

if (function(f) == False) then /* f is not a function so far */

def(i) = LiveIn(succ(b)) intersect ReachOut(f)

if (def(i) <> {}) then /* it is a function */

def(f) = def(i)

function(f) = True

rhs(i) = i /* convert i into an asgn inst */

lhs(i) = def(f)

opcode(i) = asgn

for (all ret instructions j of function f) do

exp(j) = def(f) /* propagate return register(s) */

end for

end if

else /* f is a function */

rhs(i) = i /* convert i into an asgn inst */

lhs(i) = def(f)

opcode(i) = asgn

def(i) = def(f) /* registers defined by i */

end if

end if

end for

end for

end for

end procedure

图 5-14: 函数返回寄存器(组)

5.4.6 寄存器拷贝传播

寄存器拷贝传播方法就是，对于一个在一条赋值指令中被定义的寄存器 (以ax=cx为例)，在引用或使用它的后继指令(组)中，如果在这条赋值指令之后两个寄存器都没有被修改 (即，重新定义) (也就是说，ax和cx都没有被修改)，那么就把它(ax)代换。就这个例子来讲，对寄存器ax的引用被替换成对寄存器cx的引用，而且，如果ax的所有使用被cx代替，那么ax变成死的并且赋值指令被清除。如果ax=cx是延伸到那个ax使用的唯一的ax定义、而且在指令ax=cx之后没有给cx赋值的话，那么一个ax使用可以用一个cx使用代入。前一条件是用寄存器的ud-chains来检查。后一条件是用一个r-clear条件来检查 (即，一个向前流-所有路径分析)。考虑下面来自图5-7中基本块B2的代码以及ud-chains和du-chains：

16 | dx:ax = [bp-6]:[bp-8] | ; du(dx:ax) = {17}
17 | dx:ax = dx:ax - [bp-2]:[bp-4] | ; ud(dx:ax) = {16} du(dx:ax) = {18}
18 | [bp-6]:[bp-8] = dx:ax | ; ud(dx:ax) = {17}

从这些指令的ud-chains我们知道，指令17使用了在指令16中所定义的寄存器dx:ax。因为在指令16和指令17之间这些寄存器还没有被重新定义，所以在指令17的右部中这些寄存器的使用被替换：

17

dx:ax = [bp-6]:[bp-8] - [bp-2]:[bp-4]

; du(dx:ax)={18}

因为在指令16上只有这些寄存器的一个使用 (即，du(dx:ax)=17)，所以这些寄存器现在是死的并且因此，该指令被清除。类似地，指令18使用在指令17中被定义的寄存器dx:ax。因为在这两条指令之间这些寄存器还没有被重新定义，所以把指令17的右部代入指令18的寄存器使用，得到：

18

[bp-6]:[bp-8] = [bp-6]:[bp-8] - [bp-2]:[bp-4]

因为在指令上只有这些寄存器定义的一个使用，所以这些寄存器变成死的，而且该指令被清除。在这个例子我们看到，指令i右部可以代替后来的指令i左部的使用，在一个赋值指令的右部建立表达式。

考虑来自图5-7中基本块B1的另一个例子，在死寄存器清除之后，以及寄存器 (不包括寄存器变量) 的ud-chains和du-chains：

3 | ax = si | ; | ; du(ax) = {4}
4 | dx:ax = ax | ; ud(ax) = {3} | ; du(dx:ax) = {5}
5 | tmp = dx:ax | ; ud(dx:ax) = {4} | ; du(tmp) = {6}
6 | ax = tmp / di | ; ud(tmp) = {5} | ; du(ax) = {9}
8 | dx = 3 | ; | ; du(dx) = {8}
9 | ax = ax * dx | ; ud(ax) = {6} ud(dx) = {8} | ; du(ax) = {10}
10 | si = ax | ; ud(ax) = {9} |

在指令4中寄存器ax的使用被一个寄存器变量si的使用代替，造成在指令3中的ax定义是死的。在指令5中dx:ax的使用被 (来自指令4) 一个si使用代替，造成dx:ax定义是死的。在指令6中tmp的使用被 (来自指令5) 一个si使用代替，造成在指令5中tmp定义是死的。在指令9中ax的使用被来自指令6的一个 (si/di) 使用代替，造成ax定义是死的。在相同的指令中dx的使用被来自指令8的一个常数3的使用代替，造成在指令8中dx定义是死的。最后，在指令10中ax的使用被来自指令9的一个 (si/di)*3 的使用代替，造成在指令9中ax定义是死的。因为在指令3到指令9中定义的寄存器(组)只被使用一次，而所有这些寄存器都变成死的，所以这些指令被清除，得到最后的代码：

10

si = (si / di) * 3

在向所有赋值指令传播寄存器的时候，寄存器必然被定义成一个使用其它寄存器、局部变量、参数和常数的表达式。因为这些标识符中任何一个 (除了常数之外) 都能够被重新定义，所以有必要在沿着从定义该寄存器的指令到使用它的指令的路径上检查这些标识符都没有被重新定义。因此，我们检查寄存器拷贝传播的下列必要条件：

1. 对于一个寄存器使用而言的寄存器定义唯一性：在重新定义之前被使用的寄存器转化成临时寄存器——为该机器保存一个中间结果。这个条件是通过在一个指令中被使用的寄存器的ud-chains来检查。

2. rhs-clear路径：在一个定义寄存器r的表达式 (即，指令的右部) 中——这个r满足条件1，我们检查表达式中那些标识符x，必须有一个x-clear路径到使用寄存器r的指令。寄存器r唯一地在指令i中被定义了，使用r的指令j的rhs-clear条件正式定义如下：

rhs-clear i → j | = | ∩ | x-clear i → j
 |  | x∈rhs(i) |

其中rhs(i) = 指令i的右部

和 x = 一个属于rhs(i)的标识符

和 x-clear i → j = 为真，只要在沿着路径i → j上没有x的定义

否则为假

图5-15中的算法在赋值指令上执行寄存器拷贝传播。对于这个分析，那些既能作为字寄存器使用的又能作为字节寄存器使用的寄存器 (例如，ax,ah,al) 在活寄存器分析中被看作是不同的寄存器。只要寄存器ax被定义，它也定义了寄存器ah和al，但是，如果寄存器al被定义，它仅仅定义了寄存器al和ax，而没有定义寄存器ah。必须如此，以便能够发现寄存器的部分使用 (如，高部或低部)、并且把它当成一个字节操作数而非一个整数操作数看待。

procedure RegCopyProp

/* Pre: dead-register elimination has been performed.

* ud-chains and du-chains have been computed for all instructions.

* Post: most references to registers have been eliminated.

* high-level language expression have been found. */

for (all basic blocks b in postorder) do

for (all instructions j in basic block b) do

for (all registers r used by instruction j) do

if (ud(r) = {i}) then /* r is uniquely defined at instruction i */

prop = True

for (all identifiers x in rhs(i)) do /* compute rhs-clear */

if (not x-clear(i, j)) then

prop = False

end if

end for

if (prop == True) then /* propagate rhs(i) */

replace the use of r in instruction j with rhs(i)

du(r) = du(r) - {j} /* at instruction i */

if (du(r) = {}) then

if (i defines only register r) then

eliminate i

else

modify instruction i not to define register r

def(i) = def(i) - {r}

end if

end if

end if /* end propagate */

end if

end for

end for

end for

end procedure

图 5-15: 寄存器拷贝传播算法

推广到非赋值使用寄存器的指令

对于传播在非赋值的指令中被使用的寄存器 (比如push、call、jcond指令) 来说，图5-15给出的算法基本上可以满足要求。考虑来自图5-7中基本块B1的代码经过条件码传播以后如下：

13 | dx:ax = [bp-2]:[bp-4] | ; du(dx:ax) = {15}
15 | jcond ([bp-6]:[bp-8] > dx:ax) B2 | ; ud(dx:ax) = {13}

指令15使用寄存器dx:ax，它们唯一地在指令13中被定义。指令13的右部被传播到这些寄存器的使用，导致指令13的清除。最后的代码如下：

15

jcond ([bp-6]:[bp-8] > [bp-2]:[bp-4]) B2

类似地，在一个push指令中的一个寄存器使用被替换成定义该寄存器的指令的一个右部使用，正如下来自图 5-7中基本块B4的代码经过死寄存器清除以后：

25 | ax = si | ; du(ax) = {27}
26 | dx = 5 | ; du(dx) = {27}
27 | ax = ax * dx | ; ud(dx) = {26} du(ax) = {28}
28 | push ax | ; ud(ax) = {27}

应用寄存器拷贝传播算法我们得到以下代码：

28

push (si * 5)

而且指令25、26和27被清除。

当一个call指令调用函数而非调用子过程，该指令被修改成一个asgn指令，也有可能要对它做寄存器拷贝传播。考虑下列在函数返回寄存器确定之后的代码：

21 | dx:ax = call _aNlshl | ; ud(dx:ax) = {20} ud(cl) = {19} ; du(dx:ax) = {22}
22 | [bp-6]:[bp-8] = dx:ax | ; ud(dx:ax) = {21}

函数_aNlshl在寄存器dx:ax中返回一个值。这些寄存器在当前基本块后跟的基本块的第一个指令中被使用，而且最后被复制到在偏移-6上的局部长整型变量。执行拷贝传播得到以下代码：

22

[bp-6]:[bp-8] = call _aNlshl

当dx:ax变成死的时候清除指令21。

5.4.7 实际参数

传给一个子程序的实际参数通常是在对子程序调用之前入栈。因为大多数语言允许嵌套的子程序调用，被推入栈中的参数可以是两个或多个子程序的参数，因此，有必要确定哪些参数属于哪一个子程序。为了搞清楚，我们使用一个表达式栈来储存与push指令关联的表达式。每当遇到一个call指令，所需要的参数个数从栈中被弹出。考虑来自图5-7中基本块B4的代码，经过死寄存器清除和寄存器拷贝传播以后如下：

24 | push [bp-6]:[bp-8]
28 | push (si * 5)
30 | push 66
31 | call printf

指令24,28和30把与之关联的表达式推入栈中，如图5-16所示。在到达printf调用的时候，我们检查这个函数的有关信息以确定该函数调用需要多少字节参数。在我们的例子中，是8字节。从栈中弹出表达式，检查表达式的类型以确定每个表达式要多少字节。第一个表达式是一个用2字节的整型常数，第二个表达式是一个用2字节的整型表达式，而第三个表达式是一个用4字节的长整型变量；这个函数调用总共需要8字节。从栈中弹出表达式并且依照子程序所使用的调用约定把它们放在被调用子程序的实际参数列表上。在我们的例子中，库函数printf使用C语言调用约定，所以得到如下代码：

31

call printf (66, si * 5, [bp-6]:[bp-8])

当它们被放在栈上的时候，指令24、28和30从中间代码中被清除。

图 5-16: 表达式栈

寄存器参数不入栈，但是已经定义在使用它们的子程序的use集合中。这种情况，把一个子程序的实际参数放到其实际参数列表中，是对寄存器拷贝传播算法的一个扩展应用。考虑来自图5-7中基本块B2和B3的代码，经过死寄存器清除和寄存器参数检测之后如下：

19 | cl = 4 | ; du(cl) = {21}
20 | dx:ax = [bp-6]:[bp-8] | ; du(dx:ax) = {21}
21 | dx:ax = call _aNlshl | ; ud(dx:ax) = {20} ud(cl) = {19}

指令21使用在指令20中定义的寄存器dx:ax，也使用在指令19中定义的寄存器cl。这些使用被相应指令的右部代替，按照_aNlshl的形式参数列表之次序被放在它的实际参数列表上，得到以下代码：

21

dx:ax = call _aNlshl ([bp-6]:[bp-8], 4)

指令19和指令20被清除，因为现在它们定义的是死寄存器。

5.4.8 跨子过程调用的数据类型传播

在用实际参数实例化形式参数的期间，这些参数的数据类型需要被验证，因为如果两个数据类型是不同的，那么其中一个需要被修改。考虑来自图5-7中基本块B4的代码，经过前面所有的优化以后如下：

31

call printf (66, si * 5, [bp-6]:[bp-8])

其中，实际参数列表有下列数据类型：整型常数、整型变量和长整型变量。printf的形式参数列表把一个指向一个字符串的指针作为第一个参数。因为只有关于第一个参数的信息，所以第一个实际参数被检查，而且发现它的数据类型与对应的形参不同。假定库子程序使用的数据类型一定是正确的 (亦即，它们被信赖)，那么可以安全地认为实际的整型常数必须是一个在内存里面的偏移量——指向一个字符串。通过检查内存，发现在单元DS:0066上有一个字符串；因此，整型常数被用字符串本身代替。后两个参数的形参类型未知，所以由调用者给予的类型被信赖，得到以下代码：

31

call printf ("c * 5 = %d, a = %ld\n", si * 5, [bp-6]:[bp-8])

类型传播的其它情况包括：把两个整数转换成一个长整型变量 (亦即，被调用者已经确定其参数之一是一个长整型变量，但是调用者到目前为止还把该实际参数作为两个独立的整数)。

5.4.9 寄存器变量清除

寄存器变量转化成一个高级语言程序中的局部变量。这些寄存器用新的局部变量名字代替。这个名字替换可以在数据流分析期间进行，或者由代码生成器完成。在我们的例子中，如果寄存器si和di用局部名字loc1和loc2代替，从图5-7中基本块B1某部将会得出以下代码片断：

1 | loc1 = 20
2 | loc2 = 80
9 | loc1 = (loc1 / loc2) * 3

5.4.10 扩展的寄存器拷贝传播算法

对于寄存器拷贝传播、实际参数检测和数据类型跨子过程调用的传播，在它们把寄存器信息传播到其它指令 (包括参数)期间可以做一些相应的优化。图5-17列出定义和使用寄存器的各种不同高级指令。

只有3个指令能定义寄存器：一是asgn，通过第5.2.6节讲述的寄存器拷贝传播而被清除；一是函数call，被转化成一个等价的asgn指令并且通过寄存器拷贝传播方法被清除；一是pop指令，还没有被解决。

定义 | 使用
asgn (lhs) | asgn (rhs)
call (function) | call (register arguments)
pop | jcond
 | ret (function return registers)
 | push

图 5-17: 定义和使用寄存器的潜在高级指令

pop指令总是用栈顶的值定义关联的寄存器。假设在一个子程序调用之后或者在子程序返回期间用于恢复栈的pop指令已经在成语分析期间 (见第4章第4.2.1节和4.2.1节) 从中间代码中被清除。唯一剩下的pop指令使用是从栈中把前面push指令推入的最后一个数值 (即，从寄存器泄漏出的数值) 取出。因为在实际参数的检测 (见第5.4.7节) 当中，与push指令关联的表达式是被推入一个表达式栈上，只要遇到一个pop指令，在栈顶的就是与该pop指令的寄存器相关联的表达式，我们就把它转换成一个asgn指令。考虑来自一个矩阵加法子过程的代码——在指令27和指令38把部分计算的答案泄漏到栈上，代码经过死寄存器清除以后如下。在这个例子中，三个数组已经作为参数传给子过程：由bp+4和bp+6指向的数组是两个数组操作数，由bp+8指向的数组是结果数组。这三个数组是整数数组 (即，2字节)：

18 | ax = si | ; ud(ax) = {20}
19 | dx = 14h | ; ud(dx) = {20}
20 | ax = ax * dx | ; ud(ax) = {21}
21 | bx = ax | ; ud(bx) = {22}
22 | bx = bx + [bp+4] | ; ud(bx) = {25}
23 | ax = di | ; ud(ax) = {24}
24 | ax = ax << 1 | ; ud(ax) = {25}
25 | bx = bx + ax | ; ud(bx) = {26}
26 | ax = [bx] | ; ud(ax) = {27}
27 | push ax | ; spill ax
28 | ax = si | ; ud(ax) = {30}
29 | dx = 14h | ; ud(dx) = {30}
30 | ax = ax * dx | ; ud(ax) = {31}
31 | bx = ax | ; ud(bx) = {32}
32 | bx = bx + [bp+6] | ; ud(bx) = {35}
33 | ax = di | ; ud(ax) = {34}
34 | ax = ax << 1 | ; ud(ax) = {35}
35 | bx = bx + ax | ; ud(bx) = {37}
36 | pop ax | ; ud(ax) = {37}
37 | ax = ax + [bx] | ; ud(ax) = {38}
38 | push ax | ; spill ax
39 | ax = si | ; ud(ax) = {41}
40 | dx = 14h | ; ud(dx) = {41}
41 | ax = ax * dx | ; ud(ax) = {42}
42 | bx = ax | ; ud(bx) = {43}
43 | bx = bx + [bp+8] | ; ud(bx) = {46}
44 | ax = di | ; ud(ax) = {45}
45 | ax = ax << 1 | ; ud(ax) = {46}
46 | bx = bx + ax | ; ud(bx) = {48}
47 | pop ax | ; ud(ax) = {48}
48 | [bx] = ax |

在从指令18到指令27上寄存器拷贝传播之后，指令27保存由相对si和di (行偏移和列偏移) 的bp+4偏移指向的数组内容，用下面的表达式表示：

27

push [(si*20) + [bp+4] + (di*2)]

这个表达式被推入栈，寄存器ax在下一条指令中被重新定义。随着扩展的寄存器拷贝传播，指令36弹出栈上的表达式，把它修改成下面的asgn指令：

36

ax = [(si*20) + [bp+4] + (di*2)]

; ud(ax) = {37}

这个指令被代入指令37，而且在指令38上寄存器ax被泄漏到栈上——此时寄存器ax的值正是在偏移si和di上两个数组内容之和，用表达式表示如下：

38

push [(si*20) + [bp+4] + (di*2)] + [(si*20) + [bp+6] + (di*2)]

最后，这个表达式在指令47被弹出，用下面的asgn指令代替pop指令：

47

ax = [(si*20) + [bp+4] + (di*2)] + [(si*20) + [bp+6] + (di*2)]

寄存器bx持有相对结果数组在偏移si和di上的偏移。指令48的寄存器用指令46和指令47计算的表达式替换，得到以下代码：

48

[(si*20) + [bp+8] + (di*2)] = [(si*20) + [bp+4] + (di*2)] +

[(si*20) + [bp+6] + (di*2)]

注意：这个指令没有定义任何的寄存器，仅仅使用他们，因此，不能将这个指令代入任何后来的指令，从这个意义上讲它是最后结果。从上面看出，右部和左部持有的表达式是计算一个数组的一地址。这些表达式可以被进一步分析以确定它们计算的是一个数组偏移，因而，被传给这个子程序的参数是指向数组的指针；这个信息可以进一步被传播给调用者子程序。

图5-18是扩展的寄存器拷贝传播所使用的一个最后算法描述。

procedure ExtRegCopyProp (p: subroutineRecord)

/* Pre: dead-register analysis has been performed.

* dead-condition code analysis has been performed.

* register arguments have been detected.

* function return registers have been detected.

* Post: temporary registers are removed from the intermediate code. */

initExpStk().

for (all basic blocks b of subroutine p in postorder) do

for (all instructions j in b) do

for (all registers r used by instruction j) do

if (ud(r) = {i}) then /* uniquely defined at instruction i */

case (opcode(i))

asgn: if (rhsClear (i, j))

case (opcode(j))

asgn: propagate (r, rhs(i), rhs(j)).

jcond, push, ret: propagate (r, rhs(i), exp(j)).

call: newRegArg (r, actArgList(j)).

end case

end if

pop: exp = popExpStk().

case (opcode(j))

asgn: propagate (r, exp, rhs(j)).

jcond, push, ret: propagate (r, exp, exp(j)).

call: newRegArg (exp, actArgList(j)).

end case

call: case (opcode(j))

asgn: rhs(j) = i.

push, ret, jcond: exp(j) = i.

call: newRegArg (i, actArgList(j)).

end case

end case

end if

end for

if (opcode(i) == push) then

pushExpStk (exp(i)).

elsif (opcode(i) == call) and (invoked routine uses stack arguments) then

pop arguments from the stack.

place arguments on actual argument list.

propagate argument type.

end if

end for

end for

end procedure

图 5-18: 扩展的寄存器拷贝传播算法


第 5 章 数据流分析 - 进一步的数据类型传播

5.5 进一步的数据类型传播

一旦所有的程序表达式已经找到以后，就可以对数据类型做进一步的确定，因为如数组之类的数据类型利用地址计算引用数组中的对象。这个地址计算表现为一个表达式，需要把它简化，以便得到一个高级语言表达式。考虑第5.4.10节的数组表示法：

48

[(si*20) + [bp+8] + (di*2)] = [(si*20) + [bp+4] + (di*2)] +

[(si*20) + [bp+6] + (di*2)]

用一个启发式方法能够确定，在bp+8处整数指针是一个用两个偏移量表达式计算地址的2维数组。偏移量di*2用数组元素类型的尺寸来调节索引器di (在这里是整数型的尺寸2)，而偏移量si*20用行的尺寸乘以数组元素的尺寸来调节索引器si (即，在一行中有20/2 = 10个元素，或者说该数组的列数)；因此，该表达式可以被修改成以下代码：

48

[bp+8][si][di] = [bp+4][si][di] + [bp+6][si][di]

而且，参数的类型被修改为数组 (即，一个指向一整数数组的指针)。为了确定数组的上下限，需要更多的启发式介入。前面的启发式确定了在数组的一行中元素的数目，如果该数组在一个循环中或者在任何能够给出有关行数信息的其它结构中使用，那么数组的行数也能够被确定。考虑在图5-19中的矩阵加法子程序。

图 5-19: 矩阵加法子程序

这个子程序有两个循环，一个对于行，一个对于列。通过检查所有引用索引si的条件跳转，可以确定行数的上限。在基本块B2中，si与5做比较；如果si大于或等于5，则循环不被执行 (即，没有对数组索引)；因此，我们能够假定这就是行的上限。同样地，列数的检查可以通过找出那些使用寄存器di的条件跳转指令。在这个例子中，基本块B5把这个寄存器跟10做比较；如果寄存器大于或等于这个常数，内循环不被执行 (即，没有对数组索引)。因此，这个常数能够被用作列数的上限。注意，从确定一个数组地址计算的启发式已经了解到的数字跟这个数字是相同的，因此我们假定这个数目是正确的。于是我们得到以下形式参数声明：

formal_arguments (arg1: array[5][10] = [bp+4],

arg2: array[5][10] = [bp+6],

arg3: array[5][10] = [bp+8])

然后该信息被传播到调用者子程序。

如果代码有经过最优化，一般来讲很难确定数组的上下限。例如，如果已经对下标计算应用强度削弱(strength reduction)，或者代码移动(code motion)已经把部分下标计算移到该循环之外，或者如果归纳变量(induction variable)清除已经替换了循环索引，那么无法应用前面的启发式方法。在这种情况下，反编译器或者保留数组边界未知，或者通过交互式对话向使用者询问解决方案。


第 6 章 控制流分析

由前端构造的控制流向图(control flow graph)没有关于高级语言控制结构的信息，比如if..then..elses和while()循环。这样图通过一个结构化算法能够被转换成一个结构化的高级语言图。在图中检测高级控制结构，并且在图中粘贴控制结构子图。这个阶段与数据流分析阶段和后端的关系见图6-1所示。

图 6-1: 控制流分析阶段的上下文

一个一般化的高级控制结构集合(类集)被用于把图结构化。这个集合应该具有足够的一般性，以迎合普遍使用的语言中可能的各种不同的控制结构，比如C、Pascal、Modula-2和Fortran。这类结构应该包括不同类型的循环和条件。因为图所隐含的结构没有被修改，所以这个方法无损其功能和语义。

6.1 前人的工作

过去大部分结构化算法关心如何以引进新布尔变量、代码重复、使用多级出口循环或使用一组常用语言中没有的高级结构为代价从控制流向图中去掉goto语句。一个图转换系统也曾经被提出，它的目标是不去掉全部goto语句而识别潜在控制结构。下列几节概述在这个领域中做过的工作。

6.1.1 布尔变量的介绍

Bohm和Jacopini [BJ66] 证明，利用新布尔变量的引进以及给这些变量赋值，任何程序的流图(flowgraph)可以表现为另一个流图——它可分解为π(结点序列)、φ(后测试的循环)和Δ(2路条件码)。Cooper [Coo67] 指出，如果新变量可以被引进到原来的程序，任何程序能用最多带一个φ的一个结点表示；因此，从实践的观点看，该定理没有意义 [Knu74]。

Ashcroft和Manna [AM71] 论证，如果没有引进新变量，goto程序不能被转换成while()程序，并提出一个利用引进新的布尔变量的这些程序转换算法。转换保持原来流程图程序的拓扑学，但是按照不同顺序进行计算。

Williams和Ossher [WO78] 提出一个迭代算法，通过引进一个布尔变量和每一个循环一个计数器整型变量，把一个多出口循环转换成一个单出口循环。

Baker和Zweben [BZ80] 提出利用新布尔变量的引进把多出口循环结构化。多出口循环的结构化被认为是一个控制流复杂性问题而且在这篇文章中被量化。

Williams和Chen [WG85] 提出从Pascal语言程序中清除goto语句的转换。Goto语句根据目标标签的位置来分类为：在与对应标签相同的层次上、离开一个结构的分支、从外面一个标签转入一个结构、以及子程序的不正常出口。这些转换需要一个或多个布尔变量的引进，以及必要的赋值语句和测试语句来检查一个布尔变量的值。该算法在一个PDP11/34机器上的Prolog语言中被实现。

Erosa和Hendren [EH93] 提出一个算法把所有的goto语句从C程序中去除掉。该方法利用goto-elimination(清除)和goto-movement(移动)的转换，而且为每goto引进一个新的布尔变量。为每一个新布尔变量的测试平均引进三条新指令，而且不同的循环和if条件被修改以包括新布尔变量。这个方法作为McCAT并行化反编译器的组成部分被实现。

新的(布尔)变量的引进修改了潜在程序(underlying program)的语义，因为这些变量不是原来程序的组成部分。合成的程序在功能上等价于原来的程序，因此程序产生同样的结果。

6.1.2 代码重复

Knuth和Floyd [KF71] 提出各种不同的方法——不使用新变量的引进而消除goto语句的使用。他们给出四个方法：递归的引进、新的子过程的引进、结点分割法和一个repeat..until()构造的使用。结点分割法技术的使用在最后的程序中复制代码。也证明了，如果不引进新的子过程调用，存在有些程序它的goto语句无法被清除掉。

Williams [Wil77] 提出导致非结构化图的五个子图：不正常选择路径、多出口循环、多入口循环、重叠循环和并行循环。为了把这些子图转换成结构化的图，要进行代码复制。

Williams和Ossher [WO78] 提出一个用单入口while()循环代替多入口循环的算法。该方法利用所有可能被从非正常入口进入该循环而到达的结点的代码复制。

Baker和Zweben [BZ80] 提出使用结点分割法技术，通过复制流图的一个或多个结点，生成在执行上等价的流图。结点分割法被认为是一个控制流复杂性问题，而且被量化。

Oulsnam [Oul82] 提出转换法——把六种非结构化图转换成等价的结构化图。该方法学利用结点复制而非函数复制。证明，因结点复制带来的时间开销，其时间增幅因子是至少一个路径加3。

代码复制由于复制代码/结点一次或多次而修改了原来的程序/图，因此，最后的程序/图是功能上等价于原来的程序/图，但是它的语义和结构已经被修改。

6.1.3 多级出口循环和其它结构

Baker [Bak77] 提出一个算法利用下列控制结构把流图等价地结构化：if..then..else、多级break、多级next、和无穷循环。每当图无法用前面这些结构来结构化，就使用Gotos。该算法也被推广到不可归约图(irreducible graph)。它说明该算法生成格式良好而且适当地嵌套的程序，而且在最后形成的图中任何goto语句都是向前跳转。这个算法在运行Unix的一台PDP11/54机器上被struct程序实现。它被用来把Fortran程序重写成Ratfor，一个使用控制结构的扩展的Fortran语言。该struct程序后来被J.Reuter在decomp反编译器中用来从带符号信息的目标文件(.obj)中建立结构化图。

Sharir [Sha80] 提出一个算法在一个流向图中发现潜在控制结构。这个算法检测正常的条件构造和循环构造，但是也检测适当的和不适当的强连通区间、以及适当的和不适当的最远区间。最后的流向图表现为一个分级的流结构。

Ramshaw [Ram88] 提出一个方法，使用向前清除规则和向后清除规则，从程序中清除所有的goto语句。 合成的程序是一个结构上等价的程序——它使用无穷的、命名的循环的多级出口。这个算法被用来把Knuth的TEX编译器的Pascal语言版本移植到使用Mesa语言的PARC/CSL。这两个语言都允许使用goto语句，不过在Mesa中不允许向外的goto语句。

多级出口或高级构造的使用在大多数语言中不可用，这限制了该结构化方法的普遍性以及编写程序的结构化版本可以使用的语言数目。目前，大多数第三代语言 (例如，Pascal、Modula-2、C) 不使用多级出口；只有Ada语言允许。

6.1.4 图转换系统

Lichtblau [Lic85] 提出通过标识出表现高级控制结构的子图把一个控制流向图转换成一个平凡图的一序列转换规则；高级控制结构如，2路条件结构、顺序结构、循环结构和多出口循环结构。每当没有任何规则适用于该图的时候，就从该图去掉一个边并且在它的位置上生成一个goto语句。这个转换系统被证明是有限的Church-Rosser，因此该转换能被以任何顺序应用而且得到相同的最后答案。

Lichtblau 通过引进无语境的流图语法——使用把一个图转换成另一个图的生成规则定义的无语境语法——使转换系统定式化 [Lic91]。他证明假如有一个固定的无语境流图语法GG，就有可能确定能不能从GG衍生出一个流图g。他提供了一个算法以多项式时间复杂度解决这个问题。

通过图转换来做的控制结构检测不更改潜在程序的语义或功能性，因此转换系统提供一个生成语义等价的图的方法。Lichtblau的方法在图上使用一序列图转换，把该图转换/变换成一个等价的结构化图 (如果可能的话)。这些转换不考虑从缩短评价语言生成的图，在这类语言中一个复合布尔条件的操作数不需要全部被评价，因而依照这个方法学将会生成非结构化的图。

相反地，在这篇论文中提出的结构化算法把一个任意的控制流向图转换成一个功能和语义等价的流向图——它根据一个在普遍使用的高级语言中可用的一般化的控制结构集合(类集)来结构化，而且每当图无法用普遍化的结构来结构化的时候，就使用goto跳转。这些算法考虑由缩短评价生成的图，因而不为这些图生成不必要的goto跳转。


第 6 章 控制流分析 - 图的结构化

6.2 图的结构化

以非正式方式给出一个控制流向图样本的结构化。图的结构化算法在第6.6节讲述。图6-2的控制流向图是一个包含有若干控制结构的样本程序。中间代码已经经过数据流分析阶段分析，而且所有变量都已被命名。

反编译适用的结构化算法的目标是，基于一个预先定义的高级控制结构集合，确定一个控制流向图所有潜在的控制结构。如果该图不能用预先定义的结构集合来结构化，就使用goto跳转。这些条件确保在原来的图和最后的图之间的功能等价和语义等价。

图 6-2: 控制流向图样例

6.2.1 结构化循环

在图中，循环是根据有回边的存在而被发现；亦即，一个边从一个“下游的”结点到一个“上游的”结点。下游(lower)和上游(higher)的概念还不是正式定义，但是不妨就当作在图表中较低和较高的结点 (对于从顶端开始画的图)。在图6-2的图中，有两个回边：(B14,B13) 和 (B15,B6)。这些回边表示两个不同的循环的范围。

循环的类型检测是通过检查循环的头结点和最后一个结点。循环(B14,B13)在其头结点上没有条件检查，但是循环的最后一个结点测试该循环是否应该再次被执行；因此，这是一个后测试循环，比如在C语言里的do..while()、或者在Modula-2里的repeat..until()。表现这个循环的子图能够在逻辑上被转换成图6-3的循环子图，这个循环子图被用一个结点代替，该结点容纳它所有的中间代码指令和关于循环类型的信息。

图 6-3: 后测试的循环

循环 (B15,B6) 有一个条件头结点决定该循环是否被执行。这个循环的最后一个结点是一个单路结点把控制转移回到该循环的头。这个循环无疑是一个先测试的循环，比如在很多种语言里的一个while()循环。这个循环的子图能够在逻辑上被转换成图6-4的子图，这个循环子图被一个结点代替，该结点容纳关于该循环的所有信息及其指令。

图 6-4: 先测试的循环

6.2.2 结构化的条件

如果条件 (loc3*2) <= loc4 为真，二路条件结点B2分支转移控制到结点B4，否则它分支转移到结点B3。这两个结点的后面都是跟随结点B5，换句话说，开始于结点B2的条件分支在结点B5上结束。这个图无疑是一个 if()then..else 结构，而且能够在逻辑上被转换成图6-5的子图，其中结点表示基本块B2、B3和B4。注意，在相同基本块中条件跳转之前的所有指令都不被修改。

图 6-5: 二路条件分支

如果条件 loc3 >= loc4 为真，二路条件结点B1转移控制到结点B5，否则它转移控制到结点B2。从我们前面的例子，结点B2已经与结点B3、结点B4合并，而且转换成一个带有一个去往结点B5的出边的等价结点；因此从结点B2到B5有一个路径。因为B5是在结点B1上的条件的目标分支结点之一，而且它被该条件的其它分支到达，这个二路结点表示一个单分支条件 (亦即，一个if()then)。这个子图能够被转换成图6-6的结点，其中在结点B1上的条件已经被求否，因为‘假’分支是组成这个if的单一分支。

图 6-6: 单分支条件

二路条件结点B7和结点B8不是平凡结构化的，因为，如果把结点B8看作是 if..then..else (结束于结点10) 的头、把结点B7看作是 if..then 的头，那么我们不是在入口点进入以B8为头的子图，而是在条件分支的子句之一里面进入。如果我们先把结点B7结构化成一个 if..then，那么结点B8就要通过另一个不同于出口结点B9的结点从以B7为头的子图分支出去；这样，该图无法用 if..then 和 if..then..else 结构来结构化。但是因为B7和B8都只有一个条件分支线指令，这两个条件能合并成一个组合条件如下：只要结点B7的条件为真的时候，或者B7的条件为假而且B8的条件也为假的时候，结点B9就被到达。结点B10总是被到达，每当结点B7的条件为假而且B8的条件为真的时候，或者是通过一个来自结点B9的路径。这就是说，只要当结点B7的条件为真或者结点B8的条件为假，结点B9就被到达，而且最后的结束结点是基本块B10。最后的组合条件以及转换后的子图见图6-7所示。

图 6-7: 组合条件分支


第 6 章 控制流分析 - 控制流分析

6.3 控制流分析

通过对程序的图进行控制流分析可以得到一个程序的控制结构有关信息。从图的各个不同结点收集信息——无论结点属于循环结构、条件结构，或者不属于任何结构。这一节定义了文献中使用的控制流术语；更多相关信息请参考文献[All72, Tar72, Tar74, HU75, Hec77, ASU86b]。

6.3.1 控制流分析的定义

下列定义是控制流分析中使用的基本概念。这些定义使用一个有向图 G=(N, E, h)。

定义 44

从n1到nv的路径; n1,nv∈N，用n1→nv表示，是一个边的序列 (n1, n2), (n2, n3), ..., (nv-1, nv) 满足 (ni, ni+1)∈E, ∀1≤ i <v, v≥1。

定义 45

一个闭合路径或环是一个从n1到nv的路径，其中n1=nv。

定义 46

ni∈N的后继结点是{nj∈N | ni→nj} (即，从ni出发可到达的所有结点)。

ni∈N的立即后继结点是{nj∈N | (ni, nj)∈E}。

定义 47

nj∈N的前导结点是{ni∈N | ni→nj} (即，所有到达nj的结点)。

nj∈N的立即前导结点是{ni∈N | (ni, nj)∈E}。

定义 48

结点ni∈N 向后支配或者预先支配结点nk∈N，只要结点ni在每一个h→nk路径上。叫作，ni支配 nk。

定义 49

结点ni∈N 立即向后支配 nk∈N，只要不存在nj向后支配nk、而且ni向后支配nk (即，ni是nk最接近的向后支配者)。叫做，ni是nk的立即支配者。

(原文：A node ni∈N immediately back dominates nk∈N if ∄nj·nj back dominates nk∧ni back dominates nj (i.e. ni is the closest back dominator to nk). It is said that ni is the immediate dominator of nk.)

定义 50

强连通区域(SCR)是一个子图S = (NS, ES, hS) 满足以下条件 ∀ni, nj∈NS · ∃ni→nj ∧ nj→ni。

定义 51

G的强连通组成是一个满足以下条件的子图 S = (NS, ES, hS)

l S是一个强连通的区域。

l 在G中不存在包含S的强连通区域S2。

(原文：∄S2 strongly connected region of G · S⊂S2.)

定义 52

深度优先搜索(DFS)是一个遍历方法——从最近被访问的、还有未被访问边的结点出发选择下一个将要遍历的边。

DFS算法定义了G的结点的一个部分有序。反向后根次序(reverse postorder)是在它们上一次访问期间结点的编号方式；编号从图中结点的最大号开始，而且在第1号上结束。在这一章中，所有的有编号的图使用反向后序编号模式。

定义 53

流向图G的一个深度优先生成树(DFST)是由一个DFS算法生成的，G的一个有向的、有根的、有序的生成树。一个DFST (用T表示) 可以把在G中的边划分成三组：

1. 回边 = {(v, w): w→v∈T}.

2. 向前边 = {(v, w): v→w∈T}.

3. 交叉边 = {(v, w): ∄(v→w 或 w→v) 而且 w≤v 按先根次序}。

6.3.2 关系

定义 54

令R为在一个集合S上的一个关系，那么xRy表示 (x,y)∈R。

定义 55

令R为在一个集合S上的一个关系，

那么

l R的自反闭包是 Rα = R∪{(x, x)|x∈S}

l R的传递闭包是 Rβ = R1∪R2∪..., 其中 R1=R 而且当i≥2有Ri=RRi-1

l R的自反传递闭包是 R* = Rα∪Rβ

l R的补集是 Ȓ = {(x, y)∈S×S | xR*y ∧ ∄z∈S·yRz}。

定义 56

令R为在一个集合S上的一个关系，那么 (S,R) 是有限的 Church-Rosser (fcr) 当且仅当：

1. R是有限的，即 ∀p∈S·∃kp·pRiq ⇒ i ≤ kp。

2. Ȓ是一个函数，即 pȒq ∧ pȒr ⇒ q = r。

6.3.3 区间理论

区间是J.Cocke在文献[Coc70]中定义的一个图理论构造，而且被F.Allen在控制流分析 [All70, AC72] 和数据流分析 [All72, All74, AC76] 中广泛地使用。以下几节概述区间理论概念。

定义 57

给定一个结点h，区间I(h)是单一入口的极大子图，在其中h是唯一的入口结点，而且所有闭合的路径包含h。这个唯一的区间结点h叫做区间头结点，或简称头结点。

通过选择正确的头结点集合，G能够被划分成一个唯一的不相交区间集合I = {I(h1), I(h2), ..., I(hn)}，这里n≥1。寻找一个图的唯一区间集合的算法在图6-8中描述。这个算法使用下列变量：H(头结点的集合)，I(i)(区间i的结点集合)，和I(图G的区间列表)，以及函数immedPred(n)，它返回n的下一个立即前导结点。

procedure intervals (G=(N,E,h))

/* Pre: G is a graph.

* Post: the intervals of G are contained in the listI. */

I:={}.

H:={h}.

for (all unprocessed n∈H) do

I( n) := {n}.

repeat

I(n) := I(n) + {m∈N | ∀p∈immedPred(m)·p∈I(n)}.

until

no more nodes can be added to I(n).

H := H + {m∈N | m∉62 H∧m∉I(n)∧(∃p∈immedPred(m)·p∈I(n))}.

I := I + I(n).

end for

end procedure

图 6-8: 区间算法

在图6-9中的例子表示一个图G，用点框表示它的区间。这个图有两个区间，I(1)和I(2)。区间I(2)包含一个循环，这个循环的范围由回边 (4,2) 决定。

图 6-9: 图的区间

定义 58

区间顺序被定义为在一个区间列表中结点的顺序，这个区间列表由图6-8的区间算法给出的。

几个区间性质：

1. 头结点向后支配区间里的每一个结点。

2. 在区间中每一个强连通的区域必须包含头结点。

3. 区间顺序满足，如果所有的结点以给定顺序被处理，那么给定一个结点，从头结点沿着无循环路径可以到达的它所有的区间前导结点将在这个结点之前被处理。

定义 59

一个关闭结点是在区间中任何一个以头结点为立即后继结点的结点。

派生序列构造

图的派生序列，G1...Gn，被F.Allen在文献[All70, All72]中基于图G的区间做过描述。通过一个反复地把区间折叠成结点的方法来构造这些图。G是第一个顺序图，用G1表示。第二个顺序图G2，是通过把G1中的每一个区间折叠成一个结点，从G1派生。折叠结点 (意即，将某个区间折叠而产生的结点) 的立即前导结点是原来头结点的立即前导结点——它(们)不是该区间的组成部分。其立即后继结点是原来出口结点所有不属于该区间的立即后继结点。属于G2的那些区间由区间算法计算出来，而且图构造过程重复进行直至得到一个极限的流向图Gn。Gn具有一个平凡图 (即，单一结点) 或不可归约图的特性。图6-10描述了这个算法。

定义 60

图G的第 n个顺序图或极限流向图Gn，被定义为由图6-10的派生序列算法构造的图Gi-1, i≥1，满足Gi-1=Gi。

定义 61

如果图G的第n个顺序图Gn是平凡的，那么图G是可归约的。

图 6-10: 派生序列算法

派生序列的构造在图6-11中举例说明。图G1是最初的控制流向图G。G1有两个区间，在前面的图6-9已描述。图G2把G1的区间用结点表示。G2在它的唯一区间中有一个循环。这个循环表示那个由回边 (5,1) 决定范围的循环。最后，G3没有循环而且是一个平凡图。

图 6-11: 图的派生序列

实现补偿

为了计算一个图G的区间，G需要根据前导结点和后继结点来定义 (即，一个邻接类型的图表示法)。借助额外的数据结构，Hecht提出一个寻找区间的优化算法 [Hec77]，复杂度 O(e), |E|=e。

6.3.4 不可归约流向图

不可归约流向图即这样一个图要满足条件——它的第n个顺序图不是一个平凡图 (通过区间归约)。不可归约流向图是以存在一个不允许存在的规范的不可归约图(canonical irreducible graph)为特征 [HU72, HU74, Hec77]。在一个流向图中不存在这样的图是该图可归约的充分条件。规范的不可归约图见图6-12所示。

图 6-12: (标准)规范的不可归约图

定理 1

一个流向图是不可归约的，当且仅当它有一个形式规范的不可归约图的子图。


第 6 章 控制流分析 - 高级语言控制结构

6.4 高级语言控制结构

不同高级语言使用不同的控制结构，但是一般来说，没有高级语言使用所有可用的各种控制结构。这一节举例说明各种不同的控制结构，划出一个分类，并且分析在普遍使用的高级语言中可用的结构，比如C、Pascal和Modula-2语言。

6.4.1 控制结构 - 分类

控制结构根据其复杂性被分成不同的类。一个最初的分类由Kosaraju在文献[Kos74]中提出，而且根据其分类来确定哪些类可以简化成其它哪些类。这个分类系统被Ledgard和Marcotty在文献[LM75]中予以扩展，并据此提出一个受到语义可归约性约束的控制结构的分类等级。

图6-13显示，在这个分类系统中所有值得考虑的各种控制结构；这些结构是：

图 6-13: 高级控制结构

1. 动作：一个单一的基本块结点是一个动作。

2. 复合结构：一个由两个结构组成的序列是一个复合结构。

3. 条件结构：一个有 if p then s1 else s2 形式的结构，其中p是一个断言(判定)、而s1, s2是结构，这是一个条件结构。

4. 先测试循环：一个有 while p do s 形式的循环，其中p是一个断言、而s是一个结构，这是一个先测试循环结构。

5. 单一分支条件结构：一个有 if p then s 形式的条件结构，其中p是一个断言、而s是一个结构，这是一个单一分支条件结构。

6. n路(n-way)条件结构：一个有以下形式的条件结构

case p of

1 : s1

2 : s2

...

n : sn

end case

其中p是一个断言、而s1..sn是结构，这是一个n路条件结构。

7. 后测试循环：一个有 repeat s until p 形式的循环，其中s是一个结构、而p是一个断言，这是一个后测试循环结构。

8. 多出口循环：一个有以下形式的循环

while p1 do

s1

if p2 then exit

s2

if p3 then exit

...

if pn then exit

sn

end while

其中s1..sn是结构、而p1..pn是断言，这是一个多出口循环结构。每个exit语句从循环分支转移到该循环之后的第一个语句/基本块。

9. 无穷循环：一个 loop s end 形式的循环是一个无穷循环，其中s是一个结构。

10. 多级出口：一个exit(i)语句导致封闭无穷循环i的终止。

11. 多级循环：一个cycle(i)语句导致第i个封闭的无穷循环被重新执行。

12. Goto：一个goto语句转移控制给任何其它基本块，不管唯一入口条件。

基于这12个不同结构，控制结构被划分成下列类别：

l D结构：D为Dijkstra缩略。D = {1,2,3,4}

l D'结构：D结构的扩展。D' = {1,2,3,4,5,6,7}

l BJn结构：BJ为Bohm和Jacopini的缩略，n为在一个多出口循环中断言的最大数目。BJn = {1,2,3,8}

l REn结构：RE为Repeat-End (重复-结束)，n为最大出口层数。REn = {1,2,3,9,10}

l RECn结构：REC为带有cycle(i)的Repeat-End结构，n为层数。RECn = {1,2,3,9,10,11}

l DREn结构：DRE为Repeat-End和Do-while循环，n为从封闭层退出的最大数目。DREn = {1,2,3,4,9,10}

l DRECn结构：DREC为Repeat-End、Do-while和cycle(i)结构，n为封闭的无穷循环的最大数目。DRECn = {1,2,3,4,9,10,11}

l GPn结构：任何有‘一进一出’子结构的结构，它最多有n个不同的断言。GPn = {1..7,9}

l L结构：任何形状良好的结构。对于断言、动作和控制转移的数目没有限制；因此，goto语句被允许。L = {1..12}

定义 62

令s1和s2为两个结构，则s1是s2的一个语义转换，当且仅当

l 对于每一个输入，s2计算与s1相同的函数。

l s2的动作原语和断言正好是s1的那些动作原语和断言。

换句话说，这个转换不允许有新的语义，比如变量、动作或断言。

基于语义的转换，控制结构的类别形成一个等级，如图6-14所示。在该等级中较高的类别是较低类别的一个语义转换。

图 6-14: 控制结构的分类等级

6.4.2 第三代语言的控制结构

在这一节中，各种不同的高级语言被分析并且根据它们的控制结构来分类。所选择的语言广泛使用于许多应用程序，包括系统编程、数值计算或科学应用程序、以及多用途的应用程序；这些语言是：Modula-2、Pascal、C、Fortran和Ada语言。

Modula-2语言 [Wir85, PLA91] 不允许goto语句的使用，因此，从这种语言生成的控制流是结构化的和可归约的。Modula-2有所有的D'型结构：二路条件(IF p THEN s1 {ELSE s2})、n路条件(CASE p OF ... END, pre-tested loop (WHILE p DO)、后测试循环(REPEAT s UNTIL p)和无限循环(LOOP s END)。一个无穷循环能够通过在该循环的语句序列体中由一个或多个EXIT语句终止。这个构造可以被用来模拟其它循环结构，比如一个带有n个判定 (BJn结构) 的多出口循环。一个EXIT语句停止执行直接封闭无穷循环的语句，而且在该循环终点后面跟随的语句上恢复运行。如果一个EXIT出现在一个嵌套在一个无穷循环里面的先测试循环或后测试循环中，那么内循环和封闭的无穷循环都被终止；因此，一个EXIT语句等价于一个exit(1)语句，而且属于RE1类结构。

Pascal语言 [Coo83] 对于goto语句的使用没有象Modula-2那样严格。所有的D'型结构都被允许：二路条件(if p then s1 [else s2])、n路条件(case p of ... end)、先测试循环(while p do)、后测试循环(repeat s until p)、以及无穷循环通过一个带true条件的while()来模拟(while (True) do)。Gotos能够被用来模拟多出口和多级循环，但是也能在一个非结构化方式中被用来从结构的中部进入；因此，这个语言允许L类结构。

C语言 [KR88] 允许结构化的和非结构化的控制转移。D'结构表现为以下语句：二路条件(if (p) s1 [else s2])、n路条件(switch (p) {...})、先测试循环(while (p) {s}、后测试循环(do s while (p))、和无穷循环(for (;;;) 或者 while (1) {s})。允许通过使用break语句的一级出口控制，允许通过使用continue语句的一级循环转移控制；因此，C语言包含RE1类和REC1类结构。使用goto语句能够模拟任何DRECn类的结构，但是也会产生非结构化的图；因此C语言允许L类结构。

Fortran语言 [Col81] 有不同类型的条件结构，包括：二路条件(IF (p) s1,s2)、算术条件语句或3路条件(IF (p) s1,s2,s3)、以及计算转向语句或n路条件(GOTO (s1,s2,...,sn) p)。先测试循环、后测试循环和无穷循环全都使用DO语句模拟；因此，Fortran允许所有的D'型结构。最后，goto语句是允许的，产生结构化的或非结构化的控制转移，允许L型结构。

Ada语言 [DoD83] 允许大多数D'型结构，包括：二路条件(if p then s1 [else s2])、n路条件(case p is ... end)、先测试循环(while p 和 for 循环)、以及无穷循环(loop s end 循环)。Ada语言也允许使用exit语句从指定的无穷循环里面退出；因此，使用这个指令可以终止几个嵌套循环 (即REn分类类型结构)。Goto语句被允许受限制地使用；它们只能转移控制给一个封闭的语句序列中一个语句，而不能用它们从中跳出来。而且，禁止用它转移控制进入一个case语句或者一个if..then..else语句的选择项。在goto使用上的这些限制造成它们可以模拟多级出口和多级继续、但是不允许非结构化的控制转移；因此，依靠DRECn型结构能够在这个语言中被建立。

图6-15汇总在若干著名语言中可用的、不同类型分类的结构。必须指出，所有这些语言都使用D'型结构，加上一个或多个属于不同类型分类的结构。非结构化的语言允许goto的非结构化使用，如Pascal语言和Fortran语言。goto的结构化使用，比如在Ada语言中，可能产生结构化控制流向图的构造，因为依靠DRECn型结构能够用这些gotos模拟。

语言 | 控制结构分类
Modula-2 | D' + BJn + RE1
Pascal | D' + L
C | D' + BJn + DREC1 + L
Fortran | D' + L
Ada | D' + DRECn

图 6-15: 高级语言的控制结构分类

6.4.3 控制结构的类集

为了把一个图结构化，需要选出一组一般化的控制结构。这个集合必须是够一般以迎合在多种语言里普遍使用的结构。从前一节几个第三代语言的回顾，清楚地说明大多数语言有D'类结构，加上几种结构化的或者非结构化的控制转移 (即，多级出口或gotos)。所有REn类、RECn类、DREn类和DRECn类的结构都能够使用一个goto语句的结构化的控制转移来模仿。因为大多数语言允许goto的使用，而不是所有语言有相同的多级出口结构或多级继续结构，所以选择goto作为一般化构造比选择exit(i)或cycle(i)更好。因此，我们希望用下列一般化结构来结构化一个控制流向图：

l 动作

l 复合结构

l 条件结构

l 先测试循环结构

l 单分支条件结构

l n 路条件结构

l 后测试循环结构

l 无穷循环结构

l Goto

换句话说，控制结构的类集有全部的D'和L类结构。


第 6 章 控制流分析 - 结构化的图和非结构化的图

6.5 结构化的图和非结构化的图

结构化控制流向图是从使用DRECn类结构的程序生成的一个图；亦即，这一个图可分解成带有一个入口和一个或多个出口的子图。如果是以一个结构化的方式用gotos来转移控制 (即，转移控制到一个结构的开始或结束)，允许使用goto的语言仍然能够生成结构化的图。由于goto语句的非结构化的控制转移，非结构化图被生成，也就是说，在结构化图中的一个控制转移把先前的结构化图打破成一个非结构化图——因为现在这个子图有多于一个入口。在编译器最优化阶段之前做的代码移动 (即，代码被移动)，也能带来非结构化。

6.5.1 循环

循环是一个强连通区域——在该有向子图的任何两个结点之间都有一个路径。这就是说，必须至少有一个到该循环头结点的回边。

结构化的循环是这样一个子图，它有一个入口、一个回边、以及可能的一个或多个转移控制到相同结点的出口点。结构化的循环包括所有自然循环 (先测试循环和后测试循环)、无穷循环和多出口循环。这些循环见图6-16所示。

图 6-16: 结构化的循环

非结构化的循环是这样一个子图，它有一个或多个回边、一个或多个入口点、以及一个或多个到不同结点的出口点。图6-17举例说明非结构化循环的四种不同类型：

l 多入口循环：一个循环有两个或多个入口。

l 并行循环：一个循环有两个或多个到相同头结点的回边。

l 重叠的循环：两个循环在相同的强连通区域中重叠。

l 多出口循环：一个循环有两个或多个到不同结点的出口。

图 6-17: 非结构化循环样例

结构化循环或非结构化循环的后随结点是从循环的出口到达的第一个结点。就非结构化循环而言，一个结点被认为是该循环出口结点，那么跟随其后的第一个结点是该循环的后随结点。

6.5.2 条件

结构化的二路条件结构是一个有向子图——它有一个二路条件头结点、一个入口点、两个或多个分支结点、和一个共同终结点 (从两个分支结点最后都到达该结点)。这个最后的共同终结点被称为后随结点，而且具有被头结点立即支配的特性。

在一个 if..then 条件结构中，头结点的两个分支结点之一是该子图的后随结点。在一个 if..then..else 条件结构中，两个分支结点都不是后随结点，但是它们都聚合到一个共同的终结点。图6-18显示这两个一般化的构造，带有头结点的出边的值：真或假。就一个 if..then 而言，要么‘真’的边去往后随结点，要么‘假’的边去往后随结点，因此，有两个不同的图表现一个结构；而对于一个 if..then..else 来说，图表示法是唯一的。

类似地，一个结构化的n路条件结构是一个有向子图——它有一个n路入口头结点 (即，该头结点有n个后继结点)，和一个共同终结点 (由n个后继结点都到达该结点)。这个共同终结点被称为后随结点，而且具有被该结构头结点支配的特性。一个4路条件结构样例见图6-19所示。

非结构化的二路条件结构是二路头结点子图——进入头结点的分支有两个或多个入口，或者头结点的分支有两个或多个出口。这些图表现为非正常选择路径图，见图6-20(a)所示。 从图的结构可知，可以分别从结点1和结点2开始一个 if..then..else 子图，从而产生图6-20(b)和(c)中的两个子图。图(b)假定一个二路条件结构从结点2开始，在结点5上有一个非正常入口。图(c)假定一个二路条件结构从结点1开始，从结点2有一个非正常出口。

图 6-18: 结构化的二路条件结构

图 6-19: 结构化的4路条件结构

图 6-20: 非正常选择路径

类似地，非结构化的n路条件结构允许有两个或多个入口去往n路头结点的一个或多个分支，以及两个或多个出口来自n路头结点的一个或多个分支。图6-21显示，非结构化的3路图的四种不同情况：图(a)其中一个分支有一个非正常的向前出边，图(b)其中一个分支有一非正常的向后出边，图(c)其中一个分支有一个非正常的向前入边，图(d)其中一个分支有一个非正常的向后入边。

图 6-21: 非结构化的3路条件结构

6.5.3 结构化图和可归约性

结构化的图是由结构化的子图组成的——这些子图属于由DRECn结构生成的图类。给出一个非正式的论证，证明DRECn类的所有结构化的图是可归约的。考虑在图6-22中给出的非正式的图语法。有11个生成式规则，每个规则定义一个不同的结构化的子图S。每个生成式规则指出，一个结构化的子图能够通过把结点S用与生成式相关联的右部子图代替而产生。

图 6-22: DRECn结构类的图语法

定理 2

DRECn类的图是可归约的。

证明：

DRECn类的图由图6-22非正式的图语法来定义。在生成式右部的所有子图具有共同特性：一个入口点、一个或多个到一个共同目标终结点的出口点；这样，控制的转移是以一种结构化的方式进行。由定理1可知，一个图是不可归约的，当且仅当它有一个标准不可归约图形状的子图 (见图6-12)。这个标准不可归约图由两个子图组成：一个条件分支图和一个循环。前一子图有一个入口点，后一子图有两个 (或多个) 入口点；这造成该图是不可归约的。因为图语法的生成式都不产生有多个入口的子图，这个图语法不会生成一个不可归约图；因此，属于DRECn类的图是可归约的。


第 6 章 控制流分析 - 结构化算法

6.6 结构化算法

在反编译中，结构化算法的目标是确定一个任意图的潜在控制结构，从而把它转换成一个功能和语义等价的图。任意图代表任何控制流向图；可归约的或不可归约的，来自一个结构化的或非结构化的语言。因为不知道最初的程序是用什么语言编写的、用什么编译器编译的 (例如，打开哪些最优化了)，所以必须允许使用goto跳转，以防止无法把图结构化成一个一般化的高级结构集合。第6.4.3节的控制结构类集对于本节提出的结构化算法是适用的。

6.6.1 结构化的循环

为了结构化循环，首先需要使用图表示法来定义一个循环。这个表示法不能仅仅确定循环的范围，必须也提供循环的嵌套顺序。正如Hecht在文献[Hec77]中指出的，使用环的循环表示法太精细，因为循环并非必然被适当地嵌套或者不相交。使用强连通组成(strongly connected components)对循环来说太粗糙，因为没有提供嵌套顺序。使用强连通区域来表现则不能提供图的唯一覆盖、而且不覆盖整个图。最后，使用区间表示法确实可以满足以上条件：每个区间一个循环，而且由图的派生序列提供一个嵌套顺序。

图 6-23: 图6-2的控制流向图的区间

给定一个头结点hj的区间I(hj)，如果有一个从关闭结点nk∈I(hj)到头结点hj的回边，就有一个根为hj的循环。考虑在图6-23中的图，该图与图6-2中的图是相同的——没有中间指令信息、而且用虚线分隔区间。有3个区间：I1根在基本块B1，I2根在结点B6，以及I3根在结点B13。

在这个图中，区间I3完全包含循环 (B14, B13)，区间I2包含循环 (B15, B6) 的头，而它的关闭结点是在区间I3中。如果每个区间被折叠成单独的结点，新图的区间被建立，那么现在区间I3和I2之间的循环一定属于相同的区间。考虑图6-24中派生序列图G2...G4。在图G2中，在结点I3和I2之间的循环完全在区间I5中。这个循环表示与原始图中结点 (B15, B6) 相对应的循环。注意，在这些图中没有更多的循环，而且因为从这个过程派生了平凡图G4，所以原始图是可归约的。注意，派生序列的长度与原始图中循环的最大嵌套深度成比例。

图 6-24: 派生序列图G2...G4

一旦发现一个循环，循环的类型 (例如先测试、后测试、无穷) 就根据头结点和关闭结点的类型确定了。而且，属于该循环的结点被打上这样的标记，以防止出现一个结点属于两个不同循环的情况，比如重叠的或多入口的循环。这些方法在下面几节讲解，至于现在，我们假定有决定循环类型的两个子过程，而且把属于该循环的结点做了标记。

给定一个带有区间信息的控制流G=G1，G的派生序列图G1, ..., Gn，以及这些图的区间集合I1...In，那么寻找循环的算法如下：检查G1的一个区间的每一个头结点，看看有没有一个来自关闭结点 (属于相同区间) 的回边。如果有这个回边，就找到一个循环了，因此它的类型就被确定，而且属于它的结点都被做上记号。下一步，检查G2的区间I2有没有循环，而且重复这个过程直至检查了区间In以后。只要有一个潜在的循环 (即，一个区间的头结点有一个带有回边的前导结点) 它的头结点或关闭结点已经被另一个循环做了记号，那么对该循环不予处理，因为它属于一个非结构化的循环。在代码生成期间，对于这些循环，它总是产生goto跳转。在这个算法中没有确定goto跳转和目标标签。在图6-25中给出完整的算法。这个算法按照恰当的嵌套层顺序——从最内层到最外层——建立循环。

procedure loopStruct (G=(N, E, h))

/* Pre: G1...Gn has been constructed.

* I1...In has been determined.

* Post: all nodes of G that belong to a loop are marked.

* all loop header nodes have information on the type of loop and the latching node. */

for (Gi := G1...Gn)

for (Ii(hj) := I1(h1)...Im(hm))

if((∃x∈Ni·( x, hj)∈Ei) ∧ (inLoop( x) == False))

for (all n∈loop(x, hj))

inLoop(n) = True

end for

loopType(hj) = findLoopType ((x, hj)).

loopFollow(hj) = findLoopFollow ((x, hj)).

end if

end for

end for

end procedure

图 6-25: 循环结构化算法

寻找属于一个循环的结点

给定一个由 (y, x), y∈I(x) 导出的循环，注意两个不同的循环，它们是图6-23中的程序样例的一部分，满足以下条件：

∀n∈loop(y, x)·n∈{x...y}

换句话说，该循环由x和y之间所有的结点按照结点编号组成。可是，确定这些结点属于一个循环并不是简单的事。考虑图6-26中的多出口图，其中每个循环有一个非正常出口，而且每个不同的图有一个不同类型的边被用于潜在的DFST中。我们知道，带有向前边、回边、或交叉边的循环满足上述条件。尽管带有树边(tree edge)的图包括更多的结点，但是，结点4和结点5并非循环的一部分，只不过它们的编号是在结点2和结点6 (循环的上下限) 之间。在这种情况下，需要满足一个额外条件，即，那些结点应该属于相同的区间，因为区间头结点 (即x) 支配该区间所有的结点，而在一个循环中，循环头结点支配循环的所有结点。如果一个结点属于一个不同的区间，它不被循环头结点支配，因此它不能属于相同循环。

换句话说，需要满足下列条件：

∀n∈loop(y, x)·n∈I(x)

给定一个区间I(x)，它带有一个由 (y, x), y∈I(x) 导出的循环，属于这个循环的结点满足两个条件：换句话说，如果一个结点n属于相同区间 (即，它被x支配)，而且它的编号 (即，反向后序编号) 比头结点大而且比关闭结点小 (即，它是循环“中部的”一个结点)，那么它属于由 (y, x) 导出的循环。这些条件可以简化成下列表达式：

n∈loop(y, x) ⇔ n∈(I(x)∩{x...y})

图 6-26: 多出口循环 - 4种情况

图6-23的循环有下列结点：循环 (9,8) 只有那两个结点，而循环 (10,6) 有结点6和结点10之间属于G2的区间15 (图6-4) 的所有结点。这些结点如下：

l Loop (9,8) = {8,9}

l Loop (10,6) = {6..10}

图6-27的算法寻找属于一个由一个回边导出的循环的所有结点。把这些结点的循环头(loop head)设置为循环的头结点，以此作为这些结点的记号——意即，该结点也属于一个嵌套的循环，因而它的loopHead字段不变。这样，属于一个循环(s)的所有结点用它们所属于的最内层嵌套循环的头结点做了记号。

确定循环的类型

循环的类型是由循环的头结点和关闭结点决定的。在一个先测试循环中，二路头结点决定该循环是否被执行，而单路关闭结点将控制转移回到头结点。一个后测试循环的特点是，有一个二路关闭结点决定分支转移回到该循环的头结点还是转移到循环外面，还有一个任何类型的头结点。最后，一个无穷循环有一个单路关闭结点转移控制回到头结点，还有一个任何类型的头结点。

图6-23的两个循环的类型如下：循环 (9,8) 有一个二路关闭结点和一个调用头结点，因此该循环是一个后测试循环 (即，一个 repeat..until() 循环)。循环 (10,6) 有一个单路关闭结点和一个二路头结点，因此该循环是一个先测试循环 (即，一个 while() 循环)。

procedure markNodesInLoop (G=(N, E, h), (y, x))

/* Pre: (y, x) is a back-edge.

* Post: the nodes that belong to the loop (y, x) are marked. */

nodesInLoop = {x}

loopHead(x) = x

for (all nodes n∈{x+1 ... y})

if (n∈I(x))

nodesInLoop = nodesInLoop∪{n}

if (loopHead(n) == No_Node)

loopHead(n) = x.

end if

end if

end for

end procedure

图 6-27: 把属于一个由 (y,x) 导出的循环的所有结点做记号的算法

在这个例子中，repeat..until() 循环有一个调用头结点，因此毫无疑问这个循环的确是一个后测试循环。当头结点和关闭结点二者都是二路条件结点的时候，就有问题了，因为无法仅凭头结点确定二路结点分支转移是进入循环还是离开循环；亦即，对于进入循环的情况，该循环将是一个非正常循环，而对于离开循环的情况，该循环则是一个后测试循环。因此有必要检查头结点分支的结点是否属于该循环，如果不是，那么该循环可以作为一个带有一个从关闭结点非正常出口的while()循环来编写代码。图6-28给出一个算法，基于图6-27的算法建立的nodesInLoop设置，确定循环的类型。

procedure loopType ( G=(N,E,h), (y,x), nodesInLoop)

/* Pre: (y, x) induces a loop.

* nodesInLoop is the set of all nodes that belong to the loop (y, x).

* Post: loopType(x) has the type of loop induced by (y, x). */

if (nodeType(y) == 2-way)

if (nodeType(x) == 2w)

if (outEdge(x,1)∈nodesInLoop∧outEdge(x,2)∈nodesInLoop)

loopType(x) = Post_Tested.

else

loopType(x) = Pre_Tested.

end if

else

loopType(x) = Post_Tested.

end if

else /* 1-way latching node */

if (nodeType(x) == 2-way)

loopType(x) = Pre_Tested.

else

loopType(x) = Endless.

end if

end if

end procedure

图 6-28: 确定循环的类型的算法

寻找循环的后随结点

循环的后随结点是在循环终止以后第一个被到达的结点。在自然循环的情况中，在循环终止以后只有一个结点被到达，但是在多出口循环和多级出口循环的情况中，能够有多于一个出口，因此，不止一个结点能够在循环之后被到达。因为结构化算法仅仅把自然循环结构化，因此把所有的多出口循环被结构化成带有一个“真正的”出口和一个或多个非正常出口。在循环中间有出口的无穷循环的情况中，在不同的出口之后有几个结点能够被到达。这个算法的目的是只寻找一个后随结点。

在一个先测试循环中，后随结点是循环头结点的后继结点，它不属于该循环。类似地，后测试循环的后随结点是该循环关闭结点的后继结点，它不属于该循环。在无穷循环中，最初并没有后随结点，因为头结点和关闭结点都不跳出循环。但是，由于一个无穷循环可以在该循环的中间中一个离开循环的跳转 (例如，在C语言中的一个break)，所以它也能有一个后随结点。因为后随结点是在循环结束之后被到达的第一个结点，所以我们要找的是在一个exit执行之后从该循环到达的‘最靠近的结点’。最靠近的结点就是有最小的反向后序编号的那个结点；即，(在编号顺序上)最靠近该循环的一个结点。从该循环到达的任何其它结点都可以从最靠近的结点到达 (因为那些结点的反向后序编号一定更大)，因此，最靠近的结点被认为是无穷循环的后随结点。

例子 9

图6-23的循环有以下后随结点：

l Follow (loop (9,8)) = 10

l Follow (loop (10,6)) = 11

图6-29给出一个算法，基于在图6-27的算法中所确定的nodesInLoop设置，确定一个由 (y,x) 导出的循环的后随结点。

procedure loopFollow ( G=(N,E,h), (y,x), nodesInLoop)

/* Pre: (y, x) induces a loop.

* nodesInLoop is the set of all nodes that belong to the loop (y, x).

* Post: loopFollow(x) is the follow node to the loop induced by (y, x). */

if (loopType(x) == Pre_Tested)

if (outEdges(x,1)∈nodesInLoop)

loopFollow(x) = outEdges(x,2).

else

loopFollow(x) = outEdges(x,1).

end if

else if (loopType(x) == Post_Tested)

if (outEdges(y,1)∈nodesInLoop)

loopFollow(x) = outEdges(y,2).

else

loopFollow(x) = outEdges(y,1).

end if

else /* endless loop */

fol = Max /* a large constant */

for (all 2-way nodes n∈nodesInLoop)

if ((outEdges(x,1)∉nodesInLoop) ∧ (outEdges(x,1) <fol))

fol = outEdges(x,1).

else if ((outEdges(x,2)∉nodesInLoop) ∧ (outEdges(x,2) <fol))

fol = outEdges(x,2).

end if

end for

if (fol≠Max)

loopFollow(x) = fol.

end if

end if

end procedure

图 6-29: 确定一个循环的后随结点的算法

6.6.2 结构化的二路条件结构

单分支条件结构 (即，if..then) 的子图和一个条件结构 (即，if..then..else) 的子图都有一个共同的终结点，从这里开始我们都把它叫作‘后随结点’(the follow node)，它具有被二路头结点立即支配的特性。如果这些子图是被嵌套的，它们可以有不同的后随结点或者共享相同的共同后随结点。考虑在图6-30中的图，它是图6-2中相同的图，没有中间指令信息，有立即支配者信息。结点按照反向后序被编号。

图 6-30: 带有立即支配者信息的控制流向图

在这个图中，有6个二路结点，即，结点1, 2, 6, 9, 11, 和12。正如循环结构化期间所见 (第6.6.1节)，属于一个循环的头结点或关闭结点的二路结点被做了记号，而且假如它已经属于另一个结构，那么一定不能在二路条件结构的结构化期间处理它。因此，图6-30中的结点6和结点9在这个分析不予考虑。当有两个或多个条件结构嵌套的时候，首先要分析的总是最内层嵌套的条件结构，然后是较外的一层。就在结点1和结点2上的条件结构而言，结点2必须先于结点1被分析，因为它是被嵌套在以结点1为头的子图中；换句话说，具有更大的反向后序编号的结点需要先被分析，因为首先在深度优先搜索遍历中它是最后一个被访问的。在这个例子中，两个子图都共享共同的后随结点5；因此，没有被结点2 (即，内层的条件结构)立即支配的结点，但是结点5被结点1 (即，外层的条件结构) 立即支配，而且这个结点是两个条件结构一起的后随结点。一旦后随结点已经被确定以后，通过检查二路头结点的分支之一是不是后随结点，就能够知道条件结构的类型，对于是的情况，该子图是一个单分支条件结构，否则它是一个 if..then..else 结构。就结点11和结点12的情况而言，结点12先被分析，因为没有结点以它作为立即支配者，所以没有后随结点。这个结点被遗留在一个未解决结点列表中，因为它能在另一个条件结构中被嵌套。在结点11被分析的时候，结点12,13和14都可以作为后随结点，因为结点12和结点13到达结点14，所以把结点14当作后随结点 (即，在一个子图中编号最靠近最大编号的结点，最大结点)。在未解决后随结点列表中的结点12也被打上有一个后随结点14这样的记号。这两个条件结构没有被完全地嵌套，而且在代码生成期间会使用一个goto跳转。

将这个例子一般化，我们就得到条件结构的结构化算法。算法的要点是确定哪些结点是条件结构的头结点，以及哪些结点是这些条件结构的后随结点。在找到后随结点之后，通过检查头结点的分支之一是否等同于后随结点，条件结构的类型能够被确定。先检查内层条件结构，然后较外的一层，如此做一个降序的反向后序遍历 (即，从编号较大的结点到较小的结点)。在整个处理过程中保持着一个未解决条件结构后随结点的集合。这个集合保存所有的没有后随结点的二路头结点。对于每一个不是循环的头结点或关闭结点的二路结点，其后随结点应该是把它作为一个立即支配者而且有两个或多个入边的结点 (因为它必须从头结点通过至少两个不同路径到达)。如果有多于一个这样的结点，就选择最靠近结点的最大编号的那一个结点 (即，有最大编号的那个结点)。如果没有找到这样的一个结点，这个二路头结点就被放在未解决集合上。每当一个后随结点被发现，属于未解决结点集合的所有结点被设置成有相同后随结点——刚刚找到的那个后随结点 (即，它们是到达这个结点的嵌套的条件结构或者非结构化的条件结构)。完整的算法见图6-31所示。

procedure struct2Way (G=(N,E,h))

/* Pre: G is a graph.

* Post: 2-way conditionals are marked in G.

* the follow node for all 2-way conditionals is determined. */

unresolved = {}

for (all nodes min descending order)

if ((nodeType(m) == 2-way) ∧ (inHeadLatch(m) == False))

if (∃n·n = max{i | immedDom(i) = m∧#inEdges(i)≥2})

follow(m) = n

for (all x∈unresolved)

follow(x) = n

unresolved = unresolved - {x}

end for

else

unresolved = unresolved∪{m}

end if

end if

end for

end procedure

图 6-31: 二路条件结构的结构化算法

复合条件结构

在反编译中把图结构化的时候，不仅要考虑潜在构造的结构，也要考虑潜在的中间指令信息。大多数高级语言允许复合布尔条件 (即，在条件中含有逻辑与或运算and和or) 的短路评估(short-circuit evaluation)。在这些语言中，为这些条件表达式生成的控制流向图是非结构化的，因为只要检查足够的条件、确定该表达作为一个整体为真或为假，就可以执行一个出口。例如，如果表达式 x and y 是用短路评估来编译的，那么，如果表达式x为假，整个表达式就成为假的，因而表达式y没有被评估。类似地，如果表达式x为真，那么一个 x or y 表达式也只有部分被评估。图6-32显示，从复合条件结构形成的四个不同子图。顶上的图表示我们正在考虑的逻辑条件，而底下的图表示关于每一个复合条件的短路评估图。

图 6-32: 复合条件结构图

在反编译期间，只要发现一个具有短路评估图形式的子图，就检查它有没有下列特性：

1. 结点x和结点y是二路结点。

2. 结点y有一个入边。

3. 结点y有一个唯一的指令，一个条件跳转(jcond)高级指令。

4. 结点x和结点y必须分支转移到一个共同结点t或e。

一个与图6-32底部的子图同构的子图，则必须具有第一、第二和第四个特性，而第三个特性用于确定该图是表现一个复合的条件结构、而非一个非正常条件结构图。考虑在图6-33中带有中间指令信息的图6-2子图。结点11和结点12是二路结点，结点12有一个入边，结点12有一个唯一的指令 (一个jcond)，而且结点11的‘真’分支和结点12的‘假’分支都到达结点13；即，这个子图是图6-32中的¬x∧y形式。

图 6-33: 带有中间指令信息的图6-2的子图

结构化复合条件结构的算法利用对该图从顶向下的一个遍历，因为在一个复合条件表达式中的第一个条件是该图中较高处 (即，它最先被测试)。对于所有的二路结点，为一个二路条件检查then结点和else结点。如果这两个结点中任何一个表现一个高级条件指令(jcond)、而且该结点没有其它入口 (即，这个结点的唯一入边来自头结点二路结点)、而且该结点形成在图6-32中被举例的四个子图之一，那么这两个结点被合并成一个唯一的结点——与复合条件有等价的语义含义 (即，取决于该子图的结构)，而且该结点从图上被去掉。重复这个处理过程，直至再也找不到复合的条件结构 (即，可能有3个或多个复合的‘与’和‘或’，因此用相同头结点重复这个处理直至再也没有条件结构)。最后的算法见图6-34所示。

procedure structCompConds (G=(N,E,h))

/* Pre: G is a graph.

* 2-way, n-way, and loops have been structured in G.

* Post: compound conditionals are structured in G. */

change = True

while (change)

change = False

for (all nodes n in postorder)

if (nodeType(n) = 2-way)

t= succ[n, 1]

e= succ[n, 2]

if ((nodeType(t) = 2-way) ∧ (numInst(t) = 1) ∧ (numInEdges(t) = 1))

if (succ[t, 1] = e)

modifyGraph (¬n∧t)

change = True

else if (succ[ t, 2] = e)

modifyGraph (n∨t)

change = True

end if

else if ((nodeType(e) = 2-way) ∧ (numInst(e) = 1) ∧ (numInEdges(e) = 1))

if (succ[e, 1] = t)

modifyGraph (n∧e)

change = True

else if (succ[e, 2] = t)

modifyGraph (¬n∨e)

change = True

end if

end if

end if

end for

end while

end procedure

图 6-34: 复合条件结构的结构化算法

6.6.3 结构化的n路条件结构

N路条件结构(n-way conditional)的结构化跟二路条件结构的类似。为了先寻找内层嵌套的n路条件再寻找外层的，要从图的底部向上遍历结点。对于每一个n路结点，一个后随结点被确定。最理想地，这个结点有来自n路头结点(n-way header node)的n个后继结点的n个入边，而且这个结点被这些头结点立即支配。

在一个非结构化的n路条件子图中，后随结点的确定利用上述后随结点特性的修改版。考虑在图6-35中的非结构化图，它有一个来自n路条件子图的非正常出口。所有以头结点1作为立即支配者、而且不是这个结点的后继结点的结点都是候选的后随结点，因此，结点5和结点6是候选的后随结点。结点5有三个来自从头结点开始路径的入边，结点6有两个来自从头结点开始路径的入边。因为结点5有更多来自从头结点而来的路径到达它，所以这个结点被认为是完整子图的后随结点。

不幸的是，上述方法没有覆盖那些进入一个n路子图的非正常入口。考虑图6-36中的图，它有一个非正常入口——进入头结点n路结点的分支之一。在这个例子中，由于非正常入口 (1,2) 的存在，结点6以结点1作为立即支配者，而不是结点2 (n路头结点)。换句话说，后随结点把结点3的所有入边的共同支配者作为立即支配者；即结点1。在这个例子中，为了找到一个把结点1作为立即支配者的后随结点，需要确定那个实现该子图的一个非正常入口的结点。完整的算法见图6-37所示。

图 6-35: 带有非正常出口的非结构化的n路子图

图 6-36: 带有非正常入口的非结构化的n路子图

procedure structNWay (G = (N,E,h))

/* Pre: G is a graph.

* Post: n-way conditionals are structured in G.

the follow node is determined for all n-way subgraphs. */

unresolved ={}

for (all nodes m∈N in postorder)

if (nodeType(m) == n-way)

if (∃s: succ(m)·immedDom(s)≠m)

n= commonImmedDom({s | s = succ(m)})

else

n= m

end if

if (∃j·#inEdges(j) =

max {i | immedDom(i) = n∧#inEdges(i)≥2·#inEdges(i)})

follow(m) = j

for (all i∈unresolved)

follow(i) = j

unresolved = unresolved - {i}

end for

else

unresolved = unresolved∪{m}

end if

end if

end for

end procedure

图 6-37: n路条件结构的结构化算法

6.6.4 应用顺序

在前面三节提出的结构化算法确定表现高级循环、n路结构和二路结构的子图的入口结点和出口结点 (即，头结点和后随结点)。这些算法不能够以任意的顺序应用，因为它们不形成一个有限的Church-Rosser系统。考虑在图6-38中的图，它由于非正常入口和非正常出口而有循环子图。图(a)有一个来自n路子图的非正常出口，而且整个图属于相同的循环。如果这个图应该首先被循环结构化，回边 (3,1) 会被发现，从而得到循环{1,2,3}。然后结构化n路条件，发现结点2是一个n路子图的一个头结点，但是因为这个以结点2为根的子图只有两个结点属于该循环，我们确定该子图无法被结构化为一个n路子图，而是该循环有几个非正常出口。另一方面，如果该图应该先被n路子图结构化，那么子图{2,3,4,5,6}会被结构化为一个带有后随结点6的n路子图。然后应用循环算法，回边 (3,1) 的结点被发现属于不同的结构 (即，结点3属于一个以结点2为头的结构，而结点1到目前为止不属于任何结构)，因此，存在一个从一个结构到另一个结构的非正常出口，而且该循环不同样地被结构化。在图(b)的情况，这个图是一个不可归约图，因此，通过首先进行循环结构化，将发现一个带有来自n路子图的结点的非正常出口的多出口循环 (由于非正常出口的存在，该循环没有同样地被结构化)。另一方面，如果这个图先被结构化为一个n路子图，循环不同样地被结构化，而是成为一个goto跳转。

这些例子说明，前面几节提出的结构化算法系列不是有限的Church-Rosser。这说明需要按顺序来做，而且：先做结构化n路条件结构，接下来做循环结构化，最后做二路条件结构化。循环比二路条件先做结构化，以确定形成先测试循环或后测试循环的布尔条件是该循环的一部分，而非一个二路条件子图的头结点。一旦一个二路条件已经被标记为一个循环的头结点或关闭结点，就不对它做进一步结构化了。

图 6-38: 非结构化图

不可归约图的情况

在这一章中到目前为止给出的例子都是可归约图。回忆第6.3.4节，如果一个图包含一个有标准不可归约流图形状的子图，那么它是不可归约的。基本上，如果一个图有两个或多个入口 (即，一个多入口循环)，至少两个入口由相同的共同结点支配，而且这个共同结点支配该循环的入口结点，那么一个图是不可归约为。考虑在图6-39中的多入口图。这些图表现不同分类的多入口图——根据在图的一个深度优先树中潜在的边。从上面看出，有一个树边(tree-edge)、交叉边和向前边的图是不可归约的，但是带有进入循环的回边的图不是不可归约的，因为没有支配循环所有入口的共同结点。这后一个循环更加等同于一个重叠的循环，而不是作为一个带有一个离开循环的回边的多出口循环 (图6-26的图(d) )。

图 6-39: 多入口循环 - 4种情况

因为一个反编译器结构化算法的目标是不修改控制流向图的语义和功能，结点分割法没有用来结构化不可归约图，因为新结点的加入会修改程序的语义。因此期望不使用结点复制而把该图结构化，亦即，保持该图为一个有goto跳转的不可归约图。考虑在图7-14中的带有立即支配者信息的图。因为图是不可归约的，没有完全包含在一个区间中的循环，因此，循环结构化算法确定它同样没有自然循环。当结构化二路条件结构的时候，在结点1上的条件结构被确定为有后随结点3，因为这个结点从来自头结点的路径到达而且其编号比结点2更大。这意味着，该图被结构化成一个带有后随结点3而且没有自然循环的二路子图。在代码生成期间，goto跳转被用来模拟循环和多入口 (见第7章第7.1.3节)。

图 6-40: 带有立即支配者信息的标准不可归约图


第 7 章 后端

数据流分析程序生成的高级中间代码和控制流分析程序生成的结构化控制流向图，是后端的输入。这个模块完全由代码生成器组成，产生目标高级语言的代码。这个关系见图7-1所示。

图 7-1: 代码生成器与UDM的关系

7.1 代码生成

代码生成器为一个预先定义的目标高级语言产生代码。下列例子把C语言作为目标语言，而且该例子是建立于带有结构化信息摘要以后的第6章图6-2之控制流向图样例。

7.1.1 生成一个基本块的代码

在数据流分析之后，基本块里面的中间指令全部都是高级指令；伪高级指令必须在这之前已经从代码中清除掉。考虑图7-2中经过数据流分析和控制流分析之后的控制流向图。对于每个基本块，基本块里的指令对应到目标语言的一个等价指令。控制转移的指令(即，jcond指令和jmp指令)取决于图的结构(即，它们属于一个循环或一个条件跳转(二路和n路)，或者等价于一个goto)，因而，要依据控制流信息为它们生成代码，详见下一节(第7.1.2节)描述。这一节举例说明如何为一个基本块的所有其它指令生成代码。

图 7-2: 经过数据流分析和控制流分析以后的控制流向图样例

为asgn指令生成代码

asgn指令把一个算术表达式或标识符赋值给另一个标识符。反编译器将表达式储存为抽象语法树的形式，因此，使用一个树遍历程序来为它们生成代码。考虑图7-2基本块B1的第一个指令：

asgn loc3, 5

左部是局部标识符loc3，而右部是常数标识符5。因为两个表达式都是标识符，所以毫不奇怪地代码被翻译成：

loc3 = 5;

图7-2基本块B9的第一个指令在它的右部使用一个表达式：

asgn loc3, (loc3 + loc4) - 10

这个指令用图7-3的抽象语法树表示；只有该指令的右部以抽象语法树格式储存 (中间表示法三元组的arg字段 (见第4章图4-32) )。从这个树可见，右部等价于表达式 (loc3 + loc4) - 10，对应这个指令的C语言代码如下：

loc3 = (loc3 + loc4) - 10;

图 7-3: B9第一个指令的抽象语法树

从一个抽象语法树生成代码是根据操作符类型用递归方式求得；二元操作符或一元操作符。对于二元操作符，树的左分支先被遍历，随后是操作符，再遍历右部。对于一元操作符，先输出操作符，随后是它的子树表达式。两种情况都是当遇到一个标识符 (即，树的叶子) 的时候结束递归。

例子 10

表达式用中间语言定义，有以下表达式类型：

l 二元表达式：所有使用二元操作符的表达式。二元操作符及其C语言表示如下：

³ 小于或等于 (<=)

³ 小于 (<)

³ 等于 (==)

³ 不等于 (!=)

³ 大于 (>)

³ 大于或等于 (>=)

³ 位逻辑与 (&)

³ 位逻辑或 (|)

³ 位逻辑异或 (^)

³ 求反 (二进制反码) (~)

³ 加 (+)

³ 减 (-)

³ 乘 (*)

³ 除 (/)

³ 求余 (%)

³ 右移 (>>)

³ 左移 (<<)

³ 组合与 (&&)

³ 组合或 (||)

l 一元表达式：所有使用一元操作符的表达式。一元操作符及其C语言表示如下：

³ 表达式非 (!)

³ 求地址 (&)

³ 解除参照 (*)

³ 后加和先加 (++)

³ 后减和先减 (--)

l 标识符：一个标识符是最小类型的表达式。标识符根据它在内存以及寄存器中的位置分类，如下：

³ 全局变量

³ 局部变量 (相对栈框架负的偏移)

³ 形式参数 (相对栈框架正的偏移)

³ 常数

³ 寄存器

³ 函数 (函数名字和实际参数列表)

图7-4的算法通过递归方式走遍表达式树，为一个使用上列操作符类型的表达式生成代码。

procedure walkCondExp (e: expression)

/* Pre: e points to an expression tree (abstract syntax tree).

* Post: the code for the expression tree pointed to by e is written. */

case (expressionType(e))

Boolean: write ("(%s %s %s)", walkCondExp (lhs(e)), operatorType(e),

walkCondExp (rhs(e))).

Unary: write ("%s (%s)", operatorType(e), walkCondExp (exp(e))).

Identifier: write ("%s", identifierName(e)).

end case

end procedure

图 7-4: 从一个表达式树生成代码的算法

identifierName(e) 函数返回在标识符结点e中的标识符名字；这个名字从适当的符号表 (即，全局的、局部的或参数) 中取得。如果该标识符是一个寄存器，则产生一个新的局部变量来唯一地命名；在局部变量序列中的下一序号。新的变量被放在子程序的局部变量定义的末尾。

为call指令生成代码

call指令用实际参数列表调用一个子过程。这个列表被储存在arg字段中，而且它是一个有顺序的表达式列表 (即，算术表达式以及标识符)。先输出子过程的名字，其后跟随使用图7-4的树遍历算法产生的实际参数。

为ret指令生成代码

ret指令返回一个函数中的一个表达式/标识符。如果返回指令不带任何参数，该子过程在那个语句上结束。带一个表达式的返回是可选的。

为一个基本块 (除了转移指令之外) 生成代码的完整算法见图7-5所示。在这个算法中使用indent() 函数；这个函数根据缩进层次 (每一缩进层三个空格) 决定返回一个或多个空格。

procedure writeBB (BB: basicBlock, indLevel: integer)

/* Pre: BB is a basic block.

* indLevel is the indentation level to be used in this basic block.

* Post: the code for all instructions, except transfer instructions, is displayed. */

for (all high-level instructions i of BB) do

case (instType(i))

asgn: write ("%s%s = %s;"n", indent(indLevel), walkCondExp (lhs(i)),

walkCondExp (rhs(i))).

call: fa = "".

for (all actual arguments f2formalArgList(i)) do

append (fa, "%s,", walkCondExp (f)).

end for

write ("%s%s (%s);"n", indent (indLevel), invokedProc (i), fa).

ret: write ("%sreturn (%s);"n", indent(indLevel), exp(i)).

end case

end for

end procedure

图 7-5: 从一个基本块生成代码的算法

7.1.2 从控制流向图生成代码

在图的控制流分析期间所收集的信息在代码生成中用于确定为该图生成代码的顺序。考虑图7-6中带结构化信息的图。这个图跟图7-2中没有中间指令信息的图同属一个图；结点按照反向后根顺序编号。

图 7-6: 带结构化信息的控制流向图

从一个图生成代码可以被看成——为根结点及其可能有的后继结点(属于以这个根结点为根的结构) 递归地生成代码、然后在该结构的后随结点上继续代码的生成过程。回忆第6章，后随结点是从一个结构出来到达的第一个结点 (即，该结构完成后马上就被执行的第一个结点)。循环、二路条件和n路条件结构的后随结点是在控制流分析阶段中计算出。因为其它控制转移结点 (即，单路的、直通的、调用的) 将控制转移到唯一的后继结点，所以它们的后继结点就是后随结点，而终止结点 (即，返回) 是该图隐含的深度优先搜索树的叶子，所以沿着那个路径进行的代码生成到此为止。

这一节描述为一个子过程生成代码的算法writeCode()的各算法组件。为了使讲解更容易，我们将假定这个例程已存在；因此，我们只专心讨论某一个特定结构对应代码的生成，而且使用writeCode()例程为该结构的各组成部分生成代码。在各部分的生成算法都讲完以后，最后再给出writeCode()的算法。

为循环生成代码

给定一个根在一个循环头结点上的子图，这个循环的代码是基于循环的类型生成的。不管循环是什么类型，所有的循环都有相同的结构：循环头、循环体和循环尾(trailer)。循环头和尾的生成都取决于循环的类型，循环体的生成是通过为根在循环体第一个结点上的子图生成代码。考虑在图7-6的图中的循环。根在结点6上的循环是一个先测试循环，根在结点8上的循环是一个后测试循环。

对于先测试循环，当循环条件为真的时候 (即，在结点6中的jcond布尔条件)，循环体被执行。如果进入循环之内的分支是‘假’分支，那么循环条件必须被求否，因为当条件为假的时候循环被执行。循环体由writeCode()例程生成，循环尾仅仅由一个循环支架结束组成 (对于C语言)。一旦这个代码被生成以后，就调用writeCode()例程来为循环后随结点生成代码。下面的构架被使用：

write ("%s while (loc1 < 10) {\n", indent(indLevel))

writeCode (7, indLevel + 1, 10, ifFollow, nFollow)

write ("%s }\n", indent(indLevel))

writeCode (11, indLevel, latchNode, ifFollow, nFollow)

其中第一个指令生成循环头的代码，第二个指令生成循环体的代码：根在结点7上而且有一个关闭结点10 (latching node)，第三个指令生成循环尾的代码，第四个指令为根在结点11上的其余部分生成代码。

在后测试循环中，循环头结点是循环条件为真的分支。下列的构架被使用：

write ("%s do {\n", indent(indLevel))

writeBB (8, indLevel + 1)

writeCode (9, indLevel + 1, 9, ifFollow, nFollow)

write ("%s } while (loc2 < 5); \n", indent(indLevel))

writeCode (10, indLevel, latchNode, ifFollow, nFollow)

其中第一个指令生成循环头的代码，第二个指令为根结点的指令生成代码，第三个指令为根在结点9上而且在循环关闭结点9上结束的循环体生成代码，第四个指令生成循环尾，第五个指令为根在结点10上的其余部分生成代码。为无穷循环生成代码的办法与之相似，不同之处在于它可能有或可能没有一个循环后随结点。

通常，先测试循环的头结点只有一个与之关联的指令，但是对于允许在一个物理指令中编码几个逻辑指令的语言，比如C语言，这些指令将是在头结点中，但是它们并不全部都是循环条件的组成部份。例如，在下面的C语言循环中：

while ((a += 11) > 50)

{

printf ("greater than 50\n");

a = a - b;

}

while()语句有两个目的：使变量a加11，而且在这个赋值以后检查a是否大于50。因为我们选择的中间代码只允许在一个中间指令中储存一个指令，而赋值和比较是两个不同的指令，如下列的中间代码所示：

B3:

asgn a, a + 11

jcond (a <= 50) B5

B4:

call printf ("greater than 50\n")

asgn a, a - b

jmp B3

B5:

/* other code */

因此对于这种情况，有两个解决方案：通过在循环结束处额外重复头结点基本块的指令而保持while()循环结构，或者把while()循环转换成for(;;)无限循环，并且当while()所关联的布尔条件为假的时候中断跳出循环。在我们的例子中，前一方案得到下列C语言代码：

a = a + 11;

while (a > 50) {

printf ("greater than 50\n");

a = a - b;

a = a + 11;

}

后一方案得到下列C语言代码：

for (;;) {

a = a + 11;

if (a <= 50)

break;

printf ("greater than 50\n");

a = a - b;

}

这两种方法都为该图产生正确的代码；前一方法复制代码 (如果有的话，通常复制几个指令) 而且保持while()结构，后一方法不复制代码而是修改该循环原来的结构。在本论文中更喜欢使用前一方法，因为这个解决方案提供的代码比后一方案的更容易理解。

当为循环体或循环后随结点生成代码的时候，如果代码生成器已经遍历了目标结点，就是说已经由另一个路径到达过该结点，因此，只需要生成一个goto标签以备转移控制到这个目标代码。在图7-7中的算法为一个根在循环头结点的图生成代码。这个算法生成C语言代码，而且它假定存在函数invExp()——它返回一个表达式的逆 (即，为表达式求否)，以及假定存在子过程emitGotoLabel()——它产生一个唯一的标签，产生一个到该标签的goto，而且把标签放在最后的C语言代码中适当的位置。

procedure writeLoop (BB: basicBlock; i, latchNode, ifFollow, nFollow: Integer)

/* Pre: BB is a pointer to the header basic block of a loop.

* i is the indentation level used for this basic block.

* latchNode is the number of the latching node of the enclosing loop (if any).

* ifFollow is the number of the follow node of the enclosing if structure (if any).

* nFollow is the number of the follow node of the enclosing n-way structure (if any).

* Post: code for the graph rooted at BB is generated. */

traversedNode(BB) = True.

case (loopType(BB)) /* Write loop header */

Pre_Tested:

writeBB (BB, i).

if (succ (BB, Else) == loopFollow(BB)) then

write ("%s while (%s){\n", indent(i), walkCondExp (loopExp(BB))).

else

write ("%s while (%s){\n", indent(i), walkCondExp (invExp(loopExp(BB)))).

end if

Post_Tested: write ("%s do\n{", indent(i)).

writeBB (BB, i+1).

Endless: write ("%s for (;;){\n", indent(i)).

writeBB (BB, i+1).

end case

if ((nodeType(BB) == Return)∨(revPostorder(BB) == latchNode)) then return.

if (latchNode(BB) ≠ BB) then /* Loop is several basic blocks */

for (all successors s of BB) do

if (loopType(BB) ≠ PreTested)∨(s ≠ loopFollow(BB)) then

if (traversedNode(BB) == False) then

writeCode (s, i+1, latchNode (BB), ifFollow, nFollow).

else /* has been traversed */

emitGotoLabel (firstInst(s)).

end if

end if

end for

end if

case (loopType(BB)) /* Write loop trailer */

Pre_Tested: writeBB (BB, i+1).

write ("%s}\n", indent(i)).

PostTested: write ("%s}while (%s);\n", indent(i), walkCondExp (loopExp(BB))).

Endless: write ("%s}\n", indent(i)).

end case

if (traversedNode(loopFollow(BB)) == False) then /* Continue with follow */

writeCode (loopFollow(BB), i, latchNode, ifFollow, nFollow).

else

emitGotoLabel (firstInst(loopFollow(BB))).

end if

end procedure

图 7-7: 为一个根在循环头的图生成代码的算法

为根在二路结点上的图生成代码

给定一个根在一个二路结点上的图——而且该结点不是一个循环条件表示式的组成部分，为这个图生成代码需要先确定该结点是一个if..then条件的还是if..then..else条件的头结点。对于前一情况，为if条件生成代码，之后生成then子句的代码，最后以生成if后随子图的代码结束。对于后一情况，为if条件生成代码，之后生成then子句和else子句的代码，最后以生成后随结点的代码结束。考虑中图7-6中两个二路结点，它们不是循环表达式的组成部分；结点1、结点2和结点11。

结点1是一个if..then结构的根，因为后随结点 (结点5) 是结点1的立即后继结点之一。另一个立即后继结点2是then子句，当结点1的条件为假的时候它被到达；即，需要将条件否定，代码如下：

write ("%s if (loc3 < loc4) {\n", indent(indLevel))

writeCode (2, indLevel+1, latchNode, 5, nFollow)

write ("%s }\n", indent(indLevel))

writeCode (5, indLevel, latchNode, ifFollow, nFollow)

其中第一个指令为否定的if条件生成代码，第二个指令为根在结点2上而且有结点5作为一个后随结点的then子句子图生成代码，第三个指令生成if的尾，最后一个指令为根在结点5上的后随子图生成代码。

结点2是一个if..then..else结构的根。对于这一情况，头结点的两个立即后继结点都不等同于后随结点。当条件为真的时候‘真’分支被到达，当条件为假的时候‘假’分支线被到达，得到下列代码：

write ("%s if ((loc3 * 4) <= loc4) {\n", indent(indLevel))

writeCode (3, indLevel+1, latchNode, 5, nFollow)

write ("%s }\n else {"n", indent(indLevel))

writeCode (4, indLevel+1, latchNode, 5, nFollow)

write ("%s }\n", indent(indLevel))

其中第一个指令为if条件生成代码，第二个指令为then子句生成代码，第三个指令生成else，第四个指令生成else子句的代码，最后一个指令生成if尾。在这个情况下，后随结点的代码不被生成，因为这个条件结构被嵌套在另一个也把结点5作为后随结点的条件结构中。只须检查ifFollow参数即可知道——该参数告诉关闭(enclose)这个if的后随结点是哪一个结点，如果是相同的结点，就不为这个结点生成代码。

以类似的方式，为根在结点11的子图生成代码。在这一情况，‘真’分支去往后随结点，因此，与这个if关联的布尔条件必须被否定，而且‘假’分支变成then子句。下面的代码构架被使用：

write ("%s if ((loc3<loc4) || ((loc4*2)>loc3)) {\n", indent(indLevel))

writeCode (13, indLevel+1, latchNode, 14, nFollow)

write ("%s }\n", indent(indLevel))

writeCode (14, indLevel, latchNode, ifFollow, nFollow)

对于有循环的情况，如果图中某结点已经在当前子图访问它们之前被访问过，就生成goto跳转。在这一情况，只要一个二路结点的分支已经被访问，就生成一个去往这些分支的goto。而且，只要一个根在二路结点上的子图没有后随结点，就意味着该图的两个分支不是去往一个共同结点，因为在相遇之前分支就结束了 (即，遇到一个返回结点)。在这一情况，为两个分支都生成代码，而且路径的终点会确保让递归结束。在图7-8中的算法为一个根在一个二路结点的图生成代码——这个二路结点不是一个循环布尔表达式的组成部分。

procedure write2way (BB: basicBlock; i, latchNode, ifFollow, nFollow: Integer)

/* Pre: BB is a 2-way basic block.

* i is the indentation level.

* latchNode is the latching node of the enclosing loop (if any).

* ifFollow is the follow node of the enclosing 2-way structure (if any).

* nFollow is the number of the follow node of the enclosing n-way structure (if any).

* Post: the code for the tree rooted at BB is generated. */

if (ifFollow(BB) ≠ MAX) then

emptyThen = False.

if (traversedNode(succ(BB,Then)) == False) then /* Process then clause */

if (succ(BB,Then) ≠ ifFollow(BB)) then

write ("\n %s if (%s)\n{", indent(i+1), walkCondExp (ifExp(BB)).

writeCode (succ(BB,Then), i+1, latchNode, ifFollow(BB), nFollow).

else /* empty then clause; negate else clause */

write ("\n %s if (%s)\n{", indent(i+1), walkCondExp (invExp(ifExp(BB))).

writeCode (succ(BB,Else), i+1, latchNode, ifFollow(BB), nFollow).

emptyThen = True.

end if

else

emitGotoLabel (firstInst(succ(BB,Then))).

end if

if (traversedNode(succ(BB,Else)) == False) then /* Process else clause */

if (succ(BB,Else) ≠ ifFollow(BB)) then

write ("%s}\n %s else\n{", indent(i), indent(i)).

writeCode (succ(BB,Else), i+1, latchNode, ifFollow(BB), nFollow).

end if

else if (emptyThen == False)

write ("%s}\n %s else\n{", indent(i), indent(i)).

emitGotoLabel (firstInst(succ(BB,Else))).

end if

write ("%s}\n", indent(i)).

if (traversedNode(ifFollow(BB)) == False)) then

writeCode (ifFollow(BB), i, latchNode, ifFollow, nFollow).

end if

else /* No follow, emit if..then..else */

write ("\n %s if (%s){\n", indent(i), walkCondExp(ifExp(BB)).

writeCode (succ(BB,Then), i, latchNode, ifFollow, nFollow).

write ("%s}\n %s else\n{", indent(i), indent(i)).

writeCode (succ(BB,Else), i, latchNode, ifFollow, nFollow).

write ("%s}\n", indent(i)).

end if

end procedure

图 7-8: 为一个根在二路结点的图生成代码的算法

为根在n路结点上的图生成代码

给定一个根在一个n路结点的图，该图的代码用以下方式生成：为n路头结点产生代码 (在C语言中是一个switch())，为头结点的每个后继结点相应地生成n路选择项 (在C语言中用一个case)，随后生成以那些后继结点为根而且在n路后随结点上结束的子图的代码。一旦所有的后继结点的代码已经被产生以后，就生成n路尾代码，而且该图其余部分的代码生成就是为把n路头结点的后随结点作为根的那个图生成代码。每当为分支之一或者n路结构的后随结点生成代码的时候，如果目标结点已经被遍历过，就生成一个goto跳转用于把控制转移到与该目标结点相关的代码。

图7-9中的算法为一个根在一个n路结点的图生成代码。

procedure writeNway (BB: basicBlock; i, latchNode, ifFollow, nFollow: Integer)

/* Pre: BB is an n-way basic block.

* i is the indentation level.

* latchNode is the number of the enclosing loop latching node (if any).

* ifFollow is the number of the enclosing if terminating node (if any).

* nFollow is the number of the enclosing n-way terminating node (if any).

* Post: code is generated for the graph rooted at BB. */

write ("%s switch (%s){\n", indent(i), nwayExp(BB)).

for (all successors s of BB) do /* Generate Code for each Branch */

if (traversedNode(s) == False) then

write ("%s case %s:\n", indent(i+1), index(s)).

writeCode (s, i+2, latchNode, ifFollow, nwayFollow(BB)).

write ("%s break;\n", indent(i+2)).

else

emitGotoLabel (firstInst(s)).

end if

end for

if (traversedNode(nwayFollow(BB)) == False) /* Generate code for the follow node */

writeCode (nwayFollow(BB), i, latchNode, ifFollow, nFollow).

else

emitGotoLabel (firstInst(nwayFollow(BB))).

end if

end procedure

图 7-9: 为一个根在n路结点的图生成代码的算法

为根在单路的、直通的、和调用的结点上的图生成代码

给定一个根在一个单路的、直通或调用的结点上的图，先为该基本块生成代码，随后是这个结点的唯一后继结点。尽管调用结点有两个后继结点，其中一个后继结点指向被这条指令调用的子程序；但是，因为代码的生成是以子程序为单位的，所以在这个意义上讲可以不管这个分支，只把该结点当成是只有一个唯一后继结点的。

图7-10中的算法为那些有一个唯一后继结点的结点生成代码。如果已经为唯一的后随结点生成代码，就意味着沿着另一个路径到达过该图，因而要生成一个 goto 跳转以转移控制到与那个子图相关的代码。

procedure write1way (BB: basicBlock; i, latchNode, ifFollow, nFollow: Integer)

/* Pre: BB is a pointer to a 1-way, call, or fall-through basic block.

* i is the indentation level used for this basic block.

* latchNode is the number of the latching node of the enclosing loop (if any).

* ifFollow is the number of the follow node of the enclosing 2-way structure (if any).

* nFollow is the number of the follow node of the enclosing n-way structure (if any).

* Post: code for the graph rooted at BB is generated. */

writeBB (BB, i).

if (traversedNode(succ(BB,1)) == False) then

writeCode (succ(BB,1), i, latchNode, ifFollow, nFollow).

else

emitGotoLabel (firstInst(succ(BB,1))).

end if

end procedure

图 7-10: 为根在单路的、调用的、和直通的结点上的图生成代码的算法

一个完整的算法

从一个子程序的图生成C语言代码的最后算法见图7-11所示。writeCode()子过程的参数有：一个指向基本块的指针，所使用的缩进层次，关闭一个循环的关闭结点 (如果有的话)，以及关闭二路条件和n路条件的后随结点 (如果有的话)。最初，基本块指针指向该子程序的图的开始处，缩进层次是1，而且没有用于检查的关闭结点或后随结点 (这些值被设置成一个预先确定的数值)。每当遇到一个后随结点的时候，沿着那个路径上就不再有代码被生成，而且子过程返回到调用它的子过程——它会负责后随结点的代码生成。这样使得一个条件的尾代码在后随该条件的代码之前被生成。对于循环的情况，关闭结点是沿着一个路径生成代码的最后一个结点，之后该路径的递归停止，而被调用的子过程负责循环尾代码的生成和循环后随结点的继续处理。

procedure writeCode (BB: basicBlock; i, latchNode, ifFollow, nFollow: Integer)

/* Pre: BB is a pointer to a basic block. Initially it points to the head of the graph.

* i is the indentation level used for this basic block.

* latchNode is the number of the latching node of the enclosing loop (if any).

* ifFollow is the number of the follow node of the enclosing 2-way structure (if any).

* nFollow is the number of the follow node of the enclosing n-way structure (if any).

* Post: code for the graph rooted at BB is generated. */

if ((revPostorder(BB) == (ifFollow∨nFollow))∨(traversedNode(BB) == True)) then

return.

end if

traversedNode(BB) = True.

if (isLoopHeader(BB)) then /* ... for loops */

writeLoop (BB, i, latchNode, ifFollow).

else /* ... for other nodes */

case (nodeType(BB))

2-way: write2way (BB, i, latchNode, ifFollow, nFollow).

n-way: writeNway (BB, i, latchNode, ifFollow, nFollow).

default: write1way (BB, i, latchNode, ifFollow, nFollow).

end case

end if

end procedure

图 7-11: 从一个控制流向图生成代码的算法

子过程代码的生成顺序是由该程序的调用图决定的。我们喜欢先为被嵌套的子过程生成代码，随后再生成调用它们的那些子过程的代码；因此，对调用图做一个深度优先遍历，每当一个子过程的图已经过代码生成的处理，就给它打上已经被遍历的标记。每个子程序的C语言代码生成，首先是子程序的头结点，随后是局部变量定义和子程序的主体。图7-12中的算法显示所使用的顺序，和子程序代码的生成。在这个算法中，使用isLib()函数确定一个子程序是不是一个库例程；对于被第8章签名方法发现的库例程将不生成它们的代码。writeComments()子过程写入从子程序分析中收集的信息，比如所使用参数的类型 (栈参数或寄存器参数)、在子程序中有没有检测到高级序言(prologue)、子程序使用的参数个数、子程序是否产生一个不可归约图、等等。

procedure writeProc (p: procedure)

/* Pre: p is a procedure pointer; initially the start node of the call graph.

* Post: C code is written for the program rooted at p in a depth-first fashion. */

if (traversedProc(p)∨isLib(p)) then

return.

end if

traversedProc(p) = True.

for (all successors s∈succ(p)) do /* Dfs on Successors */

writeProc (s).

end for

/* Generate code for this procedure */

if (isFunction(p)) then /* Generate Subroutine Header */

write ("%s %s (%s)\n{", returnType(p), funcName(p), formalArgList(p)).

else

write ("void %s (%s)\n{", procName(p), formalArgList(p)).

end if

writeComments(p). /* Generate Subroutine Comments */

for (all local variables v∈localStkFrame(p)) do /* Local Variable Definitions */

write ("%s %s;\n", varType(v), genUniqueName(v)).

end for

if (isHighLevel(p)) then /* Generate Code for Subroutine */

writeCode (controlFlowGraph(p), 1, Max, Max, Max).

else /* low-level subroutine, generate assembler */

disassemble(p).

end if

write ("}\n").

end procedure

图 7-12: 从一个调用图生成代码的算法

使用这一节描述的算法，为图7-2中的图生成的C语言代码见图7-13所示。局部变量按照从1开始连续地顺序命名，而且使用前缀loc (表示局部的)。

void main ()

/* Takes no parameters.

* High-level language prologue code.

*/

{

int loc1;

int loc2;

int loc3;

int loc4;

loc3 = 5;

loc4 = (loc3 * 5);

if (loc3 < loc4) {

loc3 = (loc3 * loc4);

if ((loc3 << 2) > loc4) {

loc3 = (loc3 << 3);

}

else {

loc4 = (loc4 << 3);

}

}

loc1 = 0;

while ((loc1 < 10)) {

loc2 = loc1;

do {

loc2 = (loc2 + 1);

printf ("i = %d, j = %d\n", loc1, loc2);

} while ((loc2 < 5));

loc1 = (loc1 + 1);

}

if ((loc3 >= loc4) && ((loc4 << 1) <= loc3)) {

loc3 = ((loc3 + loc4) - 10);

loc4 = (loc4 / 2);

}

printf ("a = %d, b = %d\n", loc3, loc4);

}

图 7-13: 为图7.2的图生成的最后代码

7.1.3 不可归约图的情况

正如第6章第6.6.4节所指出的，不可归约图的循环没有被结构化成自然循环，因为该循环的结点不组成一个完全的区间。考虑图7-14的带结构化信息的标准不可归约的流向图。

图 7-14: 带结构化信息的标准不可归约图

在代码生成期间，结点1对应的那个if代码首先被生成，因为‘真’分支引向该if的后随结点(结点3)，结点1的‘假’分支是then子句 (与结点1关联的布尔条件之否定)。then子句对应的代码被生成，而且这个if..then后面跟随为结点3产生的代码。因为结点3转移控制到结点2——已经在代码生成期间访问过，所以生成一个goto跳转去结点2所关联的代码。这个goto模拟循环，而且通过转移控制给这个if的then子句，形成一个非结构化的if..then结构。这个图的C语言代码构架如下：

if (! 1) {

L: 2;

}

3;

goto L;

其中，数字表示基本块及其相应的由writeBB()子过程生成的代码


第 8 章 反编译工具

反编译器工具是帮助反编译器生成目标高级语言程序的一系列程序。给定一个二进制文件，装载器确定其二进制映像从哪里开始，以及该文件有没有任何重定位信息。二进制映像被装入内存以后 (而且可能经过重定位)，就可以用反汇编器对二进制映像做语法分析并且产生该程序的一个反汇编形式。语法分析过程可以借助一个编译器和库的签名识别器——它根据事先由另一程序生成的一组预先定义的签名，确定某一个子程序是不是一个库子程序。如此，反汇编和反编译只输出用户自己编写的最初代码。反汇编器可以是反编译器的可选部分，只用于语法分析二进制映像(即，前端模块的一个部分)。程序经过语法分析以后，就可以使用第4,5,6章和第7章的方法进行反编译，产生目标高级程序。最后，一个后期处理程序能够改善高级代码的质量。图8-1显示在一个反编译系统中涉及的不同阶段。

图 8-1: 反编译系统

8.1 装载器

装载器是一个操作系统程序，它把一个可执行文件或二进制程序装入内存，如果有足够的自由内存给该程序装入和运行的话。大多数二进制程序包含关于运行该程序所需要的内存大小、重定位地址、和最初的段寄存器值等信息。该程序被装入内存以后，装载器通过设置代码与指令段把控制转移给二进制程序。

不同操作系统的二进制程序结构是不同的，因此，二进制程序的装载依赖它的运行平台——操作系统和机器。二进制程序的最简单形式只包含该程序的二进制映像，亦即，按程序原样把一个完全链接的映像装入内存，没有对二进制映像做任何改变。在DOS操作系统下的.com就是使用这个二进制结构。而大多数二进制程序不仅包含二进制映像，也包含决定二进制程序类型 (即，对于相同的操作系统或者相同机器上的不同操作系统，有不同类型的可执行程序) 和初始寄存器值的头部信息，以及包含一个重定位表保存相对于二进制映像开始处的字偏移地址——在这些位置上需要根据程序装入到内存中的位置重新定位。在DOS和Windows下使用的exe文件就是这一类型的二进制文件。二进制程序的一般格式见图8-2所示。

头部

二进制映像

重定位表

图 8-2: 二进制程序的一般格式

将一个程序装入内存的算法如下：先确定二进制文件的类型 (对于允许不同类型二进制文件的系统)，如果该文件本身是一个二进制映像，需要分配的内存大小等于文件大小，因此，分配一个文件大小的内存块，把该文件按原样装入该内存块，不做任何的修改，然后设置默认的段寄存器。对于二进制文件带有头部信息和重定位表信息的情况，先读入头部以确定需要用多少内存来装入程序、重定位表的位置、并取得其它用来设置寄存器的信息。根据头部信息指定的大小分配一个内存块，然后将该文件的二进制映像装入内存块，把重定位表里的各元素在内存中重新定位，并根据头部里的信息设置段寄存器。算法见图8-3所示。

procedure loader (name: fileName)

/* Pre: name is the name of a binary file.

* Post: the binary program name has been loaded into memory. */

determine type of binary program.

if (only binary image) then

S = size of the binary file.

allocate free block of memory of size S.

load file into allocated memory block, at a predefined offset.

setup default segment registers.

else

read header information.

S = size of the binary image (from the header information).

allocate free block of memory of size S.

load binary image into allocated memory block.

relocate all items from the relocation table in memory.

setup segment registers with information from the header.

end if

end procedure

图 8-3: 装载器算法

8.2 签名生成器

签名生成器是一个为输入文件自动生成签名的程序。签名即是用来识别病毒、编译器和库子程序的二进制标本。在反编译中使用签名的目的是撤消链接器执行的处理过程，也就是说，确定哪些子程序是库例程和编译器启动代码，并且用它们的名字 (对于前者) 代替它们或者从目标输出代码去掉它们 (对于后者)。这用于某些不共享库而是将库子程序的目标代码绑入程序二进制映像的操作系统。在二进制程序中没有储存子程序的名字或参数，因此，没有办法区别它们和用户编写的子程序，不可能从其它子程序区分出它们。而对于共享库子程序的操作系统，这些子程序并不成为二进制程序的组成部分，而是在程序中有对它们的引用，因此，子程序的名字被储存在二进制文件中 (最有可能在头部节中)。在这一节中提出的方法针对那些不共享库子程序而是在二进制程序中包括它们的操作系统。

为一组库子程序产生一个签名文件以后，调用一个接口过程根据库签名检查一个特定的子程序是否匹配库签名，以便决定要不要由反编译器/反汇编器对它进行语法分析。如果一个子程序匹配其中一个签名，那么子程序用它的名字 (即，子程序在库中的名字，比如printf) 代替并且做上记号——不再需要对它做任何分析。这样，需要被分析的子程序数目减少了，但是更大的意义在于，由于那些子程序调用使用真正的库函数名而非无意义的任意名字，使得目标高级程序的质量(可读性)得到相当大的改善。而且，由于对性能或者低级机器访问的要求，某些库子程序是用汇编语言编写的，这些例程大都没有相应的高级表示法，因此无法反编译而只能被反汇编；库签名识别方法的应用使我们可以不必分析这一类子程序，从而产生更好的目标代码。

在本节和下一节 (第8.2节和第8.3节) 提出的这个想法被Michael Van Emmerik在昆士兰技术大学工作的时候加以发展。在参考文献[Emm94]中表达了这些想法。经原作者许可，我复制了图8-4。

8.2.1 库子程序签名

一个标准库文件是一个可重定位的目标文件，实现一个特定语言/编译器为用户提供的不同子程序。一个库子程序签名是一个二进制标本，唯一地标识该库中的一个子程序，区别于在相同库中的其它任何子程序。因为所有子程序执行不同的功能，一个包含有子程序完整二进制映像的签名能够从其它任何子程序唯一地标识该子程序。这个方法的主要问题在于签名的尺寸及创建它造成的开销。因此理想的做法是只检查子程序中最少必须的字节，所以签名要尽可能小。对于有大量子程序的库，不难知道，有些子程序的签名需要使用n字节唯一地标识它们，但是n可能大于那些小的子程序的完全尺寸；因此，对于小的子程序，剩余字节需要用一个预先确定的数值填补，以免把属于另一个库子程序的字节也拿来了。例如，如果n为23，库函数cos只有10字节，而且cos后面跟随的是库函数strcpy；cos的10个字节组成它的签名，而剩下13字节用预定值填补；否则，这13字节将会来自strcpy的一部分。

对于任意一个子程序的开头n字节，其中有些机器指令——可能无法确定其操作数是常数还是偏移量，或者其操作数取决于模块装入什么地址——应当是可变字节，所谓可变字节，即是在库文件中和在包含这个子程序的二进制程序中这些字节的值可能是不一样的。因此，为了产生一个地址无关的签名，必须把这些可变字节用通配符代替。考虑图8-4中库例程fseek()的代码。指令108的call有一个偏移地址指向所调用的子程序。被调用的子程序不总是被链接到相同的位置，因此这个偏移地址是可变的；所以用通配符代替它。指令110的mov把一个常数或偏移操作数作为它的参数之一；因为不知道这个位置是不是恒定的 (即，一个常数做为地址)，所以也用通配符。通配符值的选择取决于机器汇编器。在机器上几乎不被使用的字节，比如这个例子中的halt指令 (操作码 F4)，或者在机器的反汇编中不被使用的字节，都是不错的通配符选择。对于在签名太小时使用的填充字节，其数值的选择要求与此相似。在这个例子中，用00作为通配符。

0100 | 55 | push bp |
0101 | 8BEC | mov bp, sp |
0103 | 56 | push si |
0104 | 8B7604 | mov si, [bp+4] |
0107 | 56 | push si |
0108 | E8F4F4 | call **** | ; destination wildcarded
010B | 59 | pop cx |
010C | 0BC0 | or ax, ax |
010E | 7405 | jz 0115 |
0110 | B8F4F4 | mov ax, **** | ; operand wildcarded
0113 | EB4C | jmp 0161 |
0115 | 0000 | ; padding |

图 8-4: 库函数fseek()的部分反汇编

注意，在这个例子中，虽然在函数fseek()的映像中有较多的字节，但是由于指令114的无条件跳转，签名只截取前21字节。之所以这样做，是因为不知道跟随这条无条件跳转指令的字节是不是相同库子程序的组成部分。一般来说，对于库签名而言，只要遇到一个返回指令或(无)条件跳转指令，就认为该子程序结束而填补其余不足字节。这个例子最后的签名见图8-5。应该注意，这个方法有一定的出错小概率，因为不同的子程序从它开始处到第一个(无)条件控制转移可能有相同的起始代码。

558BEC568B760456E8F4F4590BC07405B8F4F4EB4C0000

图 8-5: 库函数fseek()的签名

自动生成库子程序签名的算法见图8-6所示。这个算法的参数有：一个标准库文件、输出签名文件的名字、和签名的尺寸(以字节为单位)——事先设立的经验值。

因为编译器厂商为使用不同存储器模型的机器提供不同的库文件，所以需要为每个存储器模型产生不同的签名文件。理想的做法是，使用一个命名约定来确定签名库的编译器厂商和存储器模型，如此，就不必在签名文件里保存额外的头部信息。

procedure genLibrarySignature (lib:libraryFile, signLib:file, n:integer)

/* Pre: lib is a standard library file.

* signLib is the name of the output library signature file.

* n is the size of the signature in bytes.

* Post: the signLib file is created, and contains all library subroutine signatures. */

openFile (signLib).

for (all subroutines s∈lib) do

if ( shas nconsecutive bytes) then

sign[1..n] = first nbytes of s.

else /* shas only m < nbytes */

sign[1..m] = first mbytes of s.

sign[m+1..n] = paddingValue.

end if

for (all variant bytes b∈sign[1..n]) do

sign[b] = wildcardValue.

end for

write (name( s), sign[1..n]) to the file signLib.

end for

closeFile (signLib).

end procedure

图 8-6: 签名算法

库签名和反编译器的整合

给定一个子程序的入口点，语法分析器从入口点跟随所有路径反汇编指令。如果知道当前正在分析的源二进制程序是用什么样的编译器编译的，语法分析器能够检查子程序是不是来自某个库 (对于这个特定的编译器)。如果是的，因为知道调用了哪一个子程序，所以就不需要对这个代码做语法分析，而仅仅使用该子程序的名字。

由于在一个库中有大量的子程序，用线性搜索在一个文件中检查所有可能的签名会很低效率的。对此，散列是一个有用技术，如果在一个库中每个子程序的签名是唯一的，可以使用理想散列(perfect hashing)，效果甚至更好而且有一个固定的尺寸。理想散列信息可以储存在库签名文件的头部，每当需要确定一个子程序是否属于该库的时候，供语法分析器使用。

8.2.2 编译器签名

为了确定对一个二进制程序使用哪一个库签名文件，需要先确定原来的用户程序是使用什么编译器来编译的。因为不同的编译器厂商在编译器启动代码中使用不同的二进制映像，所以我们可以手工调查这些映像并且储存为一个应用通配符的签名，其做法和库子程序签名一样。对于相同的编译器，不同的存储器模型会产生不同的编译器签名，而且很可能，相同编译器的不同版本有不同的签名，因此，为它们每一组合 (编译器厂商、存储器模型、编译器版本) 储存一个不同的签名。可以再次使用一命名系统来区别不同的编译器签名。

确定主程序

由装载器给予的入口点是编译器启动代码的入口——在调用该程序的主要子程序之前，至少调用一打的子程序设置它的环境；主要子程序即，任何C语言程序中的main，或者某个Modula-2程序中的BEGIN。对于一个用我们预先规定的编译器编译的程序，它的主入口点是通过手工调查启动代码确定的。在所有的C语言编译器中，在主函数被调用之前，先把要传给main()函数的参数入栈 (即argv,argc,envp)；因此，确定主入口点不是很难。为了便于移值性，大多数C语言编译器提供它们启动代码的源程序代码，因此，主入口点的检测也可以利用这个东西来做。一旦知道了如何确定主入口点，就可以在编译器签名文件中为那个特定的编译器储存这个方法。

适用于反编译器的编译器签名整合

语法分析器开始分析(由装载器给出的)入口点上任何指令之前，先调用一个接口过程检查不同的编译器签名。这个过程确定所装入程序的开头字节是不是与一个已知的编译器签名等同的，如果相同，那么编译器厂商、编译器版本和存储器模型即可确定，并且把这些信息储存在一个全局结构中。当这一步完成之后，主入口点根据这个签名就可以确定了，而且该入口点将作为语法分析器的出发点。此后，可以针对确定的编译器厂商、编译器版本和存储器模型应用适当的库签名文件，检查该程序调用的任何子程序。

8.2.3 签名的手工生成

签名自动生成是理想的解决方案，但是，找出所有在库中能够唯一标识不同子程序的二进制标本不是那么容易。实验结果已经显示，在一个标准库文件中重复签名总数的最低比例5.3%而最高比例29.7% [Emm94]。大多数重复的签名是由于一些函数有不同的名字却有相同的实现，或者由于开头几个字节里过早出现无条件跳转迫使其签名截取得过于短小。

在参考文献[FZ91]中描述了一个手工生成签名的方法，而且在一个8086 C语言反编译系统中被使用 [FZL93]。通过手工调查，分析微软C语言5.0版使用的一个库文件中每个函数并且储存每个函数的以下信息：函数名字、该函数完整的二进制映像(包括可变字节)、以及用来确定一个任意子程序是否与之相配的匹配方法。匹配方法是一个指令序列，它确定在该函数的二进制标本中从某一个偏移地址开始有多少固定的信息字节，以及该函数都调用了什么子程序。每当无法确定某一个操作数是一个偏移量还是常数的时候，那些字节就被跳过 (即，由于它们是可变字节，所以不与二进制标本的对应字节做比较)，而且当遇到一个子程序调用的时候，对子程序的偏移地址不做测试，但是要对所调用的例程做匹配；依次，把被调用例程跟它的签名标本做匹配。这样，子程序的所有路径都被跟随并且被检查签名。

签名人工生成的缺点在于生成它们需要的时间；典型地，一个库有300个以上的子程序，而对于面向对象语言，这个数字增加到1300以上。为这样的一个库手工生成签名需要花费数天，对于大的库文件则长达一周。而且，当编译器的一个新版本出来的时候，必须重新手工分析其签名，因此时间开销很大。使用一个自动签名生成器会使得为一个完全的库生成签名所花费的大量时间，减少到几秒 (少于一分钟)，尽管有一定比例的重复签名这一点点缺憾。但是，如果有必要，可以通过手工检查那些有重复签名的函数并为它们生成唯一的签名。

8.3 库原型生成器

库原型生成器是一个自动生成库子程序原型有关信息的程序；即，子程序使用的参数的类型，和函数的返回值类型。确定库子程序的原型信息帮助反编译器检查正确的参数类型和数目，并且传播、更正曾经由于在分析过程中缺乏信息而错误判断的任何类型信息。考虑下列代码：

mov ax, 42

push ax

call printf

在数据流分析期间，这个代码经过扩展的寄存器拷贝传播以后被转换成下面的代码：

call printf (42)

在不知道printf的参数类型的情况下，姑且认为常数参数42是这个函数调用的正确参数。但是，如果有这个函数的原型信息，该函数的形式参数列表有一个固定字符指针参数 (即，C语言的一个字符串) 和未知类型的数目可变的其他参数。因此，可以确定常数42是数据段的相对偏移而非一个常数，于是可以确定它是一个偏移量。这个方法给反编译器提供下列改善后的代码：

printf (``Hello world"n'');

该程序的反汇编版本经过改善可能变成：

mov ax, offset szHelloWorld

push ax

call printf

其中szHelloWorld是在数据段中的偏移42，它指向空结束字符串。

因此反编译器应该利用库原型。与库签名不同，对于每个高级语言我们只需要一个库原型文件 (即，同一种语言的标准函数一定都有相同的原型)。编译器依赖的库需要额外的原型文件。比如C语言和Modula-2语言使用头文件定义所有的库原型。这些原型能够容易地用一个程序解析并且以一个预先确定的格式储存到一个文件中。有些语言比如Pascal在它们的库中储存库原型信息，因此，需要用一个特别的语法分析器读这些文件。

运行时支持例程的注解

编译器运行时支持例程是被编译器用来执行特定任务的那些子程序。这些子程序被储存在库文件中，但是没有给用户提供函数原型 (即，它们不在库的头文件中)，因此它们只被编译器使用而且遵循高级调用约定。大多数运行时子程序使用寄存器参数、而且是在寄存器中返回结果。因为没有这些子程序的原型，所以为了确定它们使用的寄存器参数(组)和返回寄存器(组) (如果有的话)，反编译器必须对这些子程序进行分析。

通过检查库原型来区别运行时支持例程和其它任何库例程：如果一个子程序属于某个库、但是没有原型，那么它就是一个运行时例程。这些例程有一个名字 (例如LXMUL)，但是参数(组)和返回值的类型是未知的。在反编译期间，这些子程序被分析，而且根据它们在库文件里的名字给予命名。根据它们的寄存器参数确定其形式参数。

库原型和那反编译器的整合

只要利用编译器签名确定编译最初源程序所使用的编译器的类型，那么也就知道编译该程序所使用的语言类型了；因此，能够使用适当的库原型文件来确定在程序中使用的库子程序的更多信息。在程序的语法分析期间，如果确定一个子程序是在库签名文件中的那个，就读取那个语言的原型文件，而且把这个信息连同子程序的名字一起储存在该子程序的摘要信息记录中。这一过程给数据流分析程序提供完整确定的库子程序的参数类型，因此，这些类型可以向后传播给调用者子程序——只要发现跟原型里的参数类型不一样。而且，如果该子程序有一个返回类型定义，那就是说该子程序的确是一个函数，因而应该被作为函数看待。

8.4 反汇编器

反汇编器是一个把源二进制程序转换成目标汇编程序的程序。汇编代码使用助记符表现机器操作码；一个或多个的机器操作码被映射到相同的助记符 (例如，所有把两个操作数相加的机器指令被映射到add助记符)。

反汇编器作为一个工具，用来修改没有可用源码的现有二进制文件、弄懂无文档的代码、重建丢失的源程序代码、以及了解一个程序如何工作 [Flu89,Com91]。近几年来，反汇编器被用作调试工具，帮助确定在一个二进制文件中是否存在病毒代码，以及获取这个病毒的反汇编码；选择性的反汇编技术被用来检测潜在的恶意代码 [Gar88]。

反汇编器由两个阶段组成：二进制程序的语法分析和汇编代码的生成。前一阶段跟反编译器的语法分析阶段相同 (见第4章第4.1节)，然后代码生成器直接产生或者从二进制程序的一个中间表示法产生汇编代码。为了声明数据段里的所有字符串和常数，符号表信息也被储存。

大多数通用的DOS反汇编器 [Zan85, GD83, Cal, Mak90, Sof88, Chr80] 只在二进制映像上进行一趟，而没有构造该程序的控制流向图。语法分析错误多数是由于把一些数据的内存单元假定认为代码引起的。其中有些反汇编器给各种不同的DOS中断加上注解，而且不仅能够反汇编二进制文件，也能反汇编内存块和系统文件。商业的反汇编器如Sourcer [Com91] 对二进制映像进行多趟扫描，每一趟重新定义符号表，而且确保较好地区分数据和代码。使用一个内部的模拟器通过跟踪寄存器内容来解决变址的跳转和调用问题。这个反汇编器也收集交叉引用信息。

在反编译中，反汇编器可以是反编译器的可选部分，通过增加一个额外的汇编代码生成器阶段，比如图8-7中所示，或者可以作为一个附加工具，把它生成的汇编程序作为反编译器的源输入程序，比如最初的图8-1中所示。

图 8-7: 反汇编器作为反编译器的组成部分

8.5 语言独立绑定

反编译器为其特定的目标语言产生代码。利用编译器和库的签名来反编译二进制程序，产生目标语言程序——在其中使用在库签名文件中定义的库例程的名字。如果二进制程序原来的编写语言跟反编译器的目标语言是不同的，则不能够用这个语言再编译目标程序，因为它使用的库例程是在别的语言/编译器中定义的。考虑下列反编译的C语言代码片断：

WriteString ("Hello Pascal");

CrLf();

这两个语句调用Pascal语言库例程实现原来Pascal语句功能 writeln ("Hello Pascal"); 第一个例程显示字符串而第二个例程执行一个回车换行。换句话说，因为在Pascal语言的库中没有writeln这个库例程，所以这个调用是以WriteString和CrLf的调用代替。反编译的代码是正确的，但是由于目标语言是C语言，因此，假如WriteString和CrLf不是C语言的库例程，那么这个代码就无法被再编译。

上述问题可以通过在Pascal库函数和C语言库函数之间建立联系(绑定)来解决。用这个办法，不生成上面那两个语句，而是使用一个对printf的调用，如下：

printf ("Hello Pascal\n");

ISO第11工作组SC22委员会的目标是为服务设施的语言独立访问建立标准。其工作成果能够被用来为一些语言定义语言独立绑定，比如C和Modula-2。可以把关于库绑定的信息放在一个文件里，而反编译器的代码生成器可以利用它产生使用目标语言库例程的目标代码。

8.6 后期处理器

反编译器生成的目标高级语言程序可以通过后期处理器改善其质量，用语言特有的结构代替一般化的控制结构。在第6章的结构化分析中对语言特有的结构不予考虑，因为这些构造不具有跨多种语言的普遍性。

对于C语言作为目标语言的情况，for()循环是用一个检查终止条件的while()循环实现。归纳变量(induction variable)在循环之前被初始化，而且在每轮循环中在while()的最后一条语句上被更新。考虑下列反编译的C语言代码：

1 loc1 = 0;

2 while (loc1 < 8)

{

3 if (loc1 != 4)

{

4 printf ("%d", loc1);

}

5 loc1 = loc1 + 1;

}

在语句2上的while()循环对照常数8检查局部变量loc1。这个变量在语句1被初始化，而且也在循环的最后一个语句 (即，语句5) 被更新；因此，这个变量是一个归纳变量，而且该while()循环可以用一个for循环代替，得到以下代码：

2 for (loc1 = 0; loc1 < 8; loc1 = loc1 + 1)

{

3 if (loc1 != 4)

{

4 printf ("%d", loc1);

}

}

其中用指令2替换掉指令1和指令5。在C语言中也可以使用先加指令和后加指令，因此，上面代码能够被改善成下面的：

2 for (loc1 = 0; loc1 < 8; loc1++)

{

3 if (loc1 != 4)

{

4 printf ("%d", loc1);

}

}

在C语言中一个break语句结束当前循环的执行，分支转移控制给该循环后随的第一个指令。考虑以下反编译的代码：

1 loc1= 0;

2 while (loc1 < 8)

{

3 printf ("%d", loc1);

4 if (loc1 == 4)

5 goto L1;

6 loc1 = loc1 + 1;

}

7 L1:

指令4检查比较局部变量loc1和4，如果它们相等就执行一个goto跳转，转移控制给标签L1；该循环后的第一个指令。这个控制转移等价于一个break，这可省掉标签和goto。而且，该循环被转换成一个for循环，得到下列代码：

2 for (loc1 = 0; loc1 < 8; loc1++)

{

3 printf ("%d", loc1);

4 if (loc1 == 4)

5 break;

}

类似地，可以在代码中使用continue语句。如果目标语言是Ada语言，则允许使用标签的多出口循环。当循环中使用一些goto跳转退出循环的時候，反编译器会生成这种结构。检查这些goto跳转的目标语句是否为关闭循环标签，用适当的exit loopName语句代替。

一般来说，任何语言特有的构造能够用在第6章第6.4.3节结构化算法里的一般化构造集表现；可以用后期处理器替换掉这些构造，但是这不是非常有必要的，因为这些构造在功能上是等价的。


第 9 章 dcc

dcc是用C语言编写的一个适用于DOS操作系统的原型反编译器(样机)。dcc最初在一台运行Ultrix的DecStation 3000上开发，后来被移植到运行DOS的PC机体系结构上。dcc把Intel i80286体系结构的.exe文件和.com文件作为输入，并且产生目标C语言和汇编语言程序。这个反编译器使用在这篇论文中 (第4,5,6,7章和第8章) 所描述的技术，它是由图9-1显示的几个部分组成。从图中可知，该反编译器有一个内建的装载器和反汇编器，而没有后期处理阶段。以下几节详细描述关于dcc的几个方面，而且在第9.7节给出一系列反编译的程序。

图 9-1: dcc反编译器的构成

反编译器主程序见图9-2所示，它有五个相互联系的主要模块：initArgs()从命令行argv[]读入用户选择项并且把它们放在一个全局的程序选项变量中；Loader()读入二进制程序并把它装入内存；FrontEnd()解析该程序建立一个调用图；udm()分析该程序的控制流和数据流；和BackEnd()为在调用图中的各个不同例程生成C代码。

Int main(int argc, char *argv[])

{ char *filename; /* Binary file name */

CALL_GRAPH *callGraph; /* Pointer to the program's call graph */

filename = initArgs(argc, argv);

/* Read a .exe or .com file and load it into memory */

Loader (filename);

/* Parse the program, generate Icode while building the call graph */

FrontEnd (filename, &callGraph);

/* Universal Decompiling Machine: process the Icode and call graph */

udm (callGraph);

/* Generates C for each subroutine in the call graph */

BackEnd(filename, callGraph);

}

图 9-2: 反编译器主程序

DOS操作系统使用一个分段的机器表示法。为这个体系结构编写的编译器使用6个不同的存储器模型：极小的、小的、中等的、紧凑的、大的、和极大的。存储器模型来自于代码和数据是16位指针或是32位指针。附录A提供关于i80286体系结构的信息，附录B提供关于PSP的信息。这一章假定读者已经熟悉这个结构了。

反编译器选择项

dcc是通过指定要反编译的二进制文件从命令行运行的。例如，若要反编译文件test.exe则使用下列命令：

dcc test .exe

这个命令产生test.b文件，这是目标C语言文件。使用者可以使用一些选择项以获得关于程序的更多信息。这些选择项是：

l a1: 在语法分析后产生一个汇编程序文件 (即，在图的最优化之前)。

l a2: 在图的最佳化之后产生一个汇编程序文件。

l o <fileName>: 指定输出的汇编程序文件名字。

l m: 产生该程序的一个内存映像图。

l s: 统计在为每个子程序的控制流向图做最优化之前和之后的基本块数目。

l v: 冗长模式选择项，显示关于被装入的程序 (缺省的寄存器值、映像大小等等)，每个子程序图的基本块，每个指令、liveIn、liveOut、liveUse的已定义的寄存器和被使用的寄存器，以及每个基本块的已定义的寄存器集合，等信息。

l V: 极冗长模式选择项，除了显示冗长模式选项所显示的信息，还加上关于该程序重定位表(如果有的话)，在图最优化之前每个子程序的控制流向图的基本块，和每个子程序的派生序列图，等信息。

l i: dcc的文本用户界面。这个界面是Michael Van Emmerik使用curses库编写的。它允许用户单步(浏览)程序，包括子程序调用。右光标键被用来跟随跳转和子程序调用，左光标键被用来回到在你使用右光标键之前的地方，上下光标键被用来上/下移动光标一次一行，向上翻页键在向下翻页键被用来上卷一页或下卷一页，而ctrl-X被用来退出交互式界面。

9.1 装载器

DOS装载器是一个名叫exec的操作系统程序。Exec检查有没有足够的可用内存来装入程序，分配一个内存块，在其开头建立PSP，把程序读入到所分配的内存块中PSP之后，设置段寄存器和堆栈，而且把控制转移给该程序 [Dun88a]。

因为反编译器需要对程序有控制，所以不使用exec程序，而是编写一个装载程序执行相似的任务。对于.exe程序，从程序头部获得关于所需要的内存量和重定位表的位置的信息，根据映像大小所需字节数动态分配内存，然后把该程序装入内存而且重定位。对于.com程序，所需要的内存量就是文件大小，动态分配内存并且把程序按照原样装入内存。在附录C给出这些文件的格式。

在dcc中，内存用一个字节数组表示；只要确定程序的映像大小之后，即可动态分配一个足够大的数组。由于历史原因，.com程序被装入在偏移0100h。该装载器也把与该程序有关的信息储存在一个PROG记录中，见图9-3定义。这个记录不仅储存在二进制文件上的信息，而且也有关于内存映像图和该程序的装入地址(段、偏移量)(这个地址是固定的，但依赖于二进制程序的类型)。

typedef struct {

int16 initCS; /* Initial CS register value */

int16 initIP; /* Initial IP register value */

int16 initSS; /* Initial SS register value */

int16 initSP; /* Initial SP register value */

boolT fCOM; /* Flag set if COM program (else EXE) */

Int cReloc; /* # of relocation table entries */

dword *relocTable; /* Pointer to relocation table */

Int cProcs; /* Number of subroutines */

Int offMain; /* The offset of the main() proc */

word segMain; /* The segment of the main() proc */

boolT libSigs; /* True if library signatures loaded */

Int cbImage; /* Length of image in bytes */

byte *Image; /* Entire program image */

byte *map; /* Memory bitmap pointer */

} PROG;

图 9-3: 程序信息记录

9.2 编译器和库签名

DOS操作系统不提供共享库的方法，因此库例程被连接到程序的映像。由于这个原因，我们为几个编译器生成编译器和库的签名；第9.3.1节解释它们如何在dcc中使用。

一个自动的签名生成器被编写来为标准.lib文件生成库签名，详见第8章第8.2.1节的描述。签名的长度被设定为23字节，实验结果证明这个值是一个合理大小。通配符字节是F4 (是HALT指令的操作码)，因为这个操作码极少被用，而填充字节被设定为00。下列C语言编译器生成了库签名：Microsoft C 5.1，Microsoft Visual C++ V1.00，Turbo C 2.01，和Borland C V3.0。为编译器厂商、存储器模型和编译器版本的每一个三元组合生成一个单独的库签名文件。签名在几秒钟之内被生成。

由于使用自动的签名生成，所以发现有重复的签名。重复的数目从最低Turbo C 2.01的5.3%，到最高Microsoft Visual C++ V1.00的29.7%不等。在Turbo C 2.01的情况，在357个例程中有19重复的签名。主要原因是因为有同一表现的例程有不同的名字，比如spawnvp, spawnvpe, spawnve, spawnv, spawnlp, 和spawnl。少数签名的相同是由于相似的函数，比如tolower和toupper。只有一对不相关的函数有相同的签名；它们是brk和atoi。在Microsoft Visual C++ V1.00的情况，在1327例程中有440个相同的签名。其中大多数重复是由于用户不能访问的内部公共名字，比如_CIcosh和_CIfabs。其它一些重复的签名是因为不同的名字被相同例程使用，尤其是为了不同的存储器模型使用的命名约定 (即，相同例程工作在不同的存储器模型中) [Emm94]。

Pascal语言编译器不使用标准的库文件。在Borland Pascal语言编译器的情况，所有的库信息被储存在一个.tpl文件，其中有关于库例程和原型的信息。一个修改的签名生成器用于.tpl文件，而且为Turbo Pascal 4.0版和5.0版生成了签名。

库签名文件平均占用50Kb磁盘空间，相对其中储存的库例程信息量来说这个尺寸是适中的。

用于上述编译器的编译器签名是手工生成的而且作为dcc的一部分被储存。在语法分析器第一次被调用的时候检查这些签名。签名生成器和原型生成器的实现归功于Michael Van Emmerik在昆士兰技术大学工作的时候。这个成果在[Emm94]中详述。

9.2.1 库原型

编写了一个名叫parsehdr的程序用来解析C语言库的头文件，抽出原型，而且把关于参数类型和返回类型的信息储存到一个文件。生成的原型适用于C语言标准库。

至于Pascal语言，原型信息是作为.tpl库文件的组成部分储存的。由于缺乏该原型信息准确结构的有关资料，没有生成这些原型。

9.3 前端

在语法分析已装入内存的程序时，前端为该程序构造一个调用图。每个子程序的中间代码和控制流向图隶属于调用图中的子程序结点；因此，语法分析、中间代码生成和流向图的构成是在扫描该程序映像同一趟中完成。数据信息被储存在全局的和局部的符号表中。如果使用者要求反汇编，则一个汇编器文件被输出写入一个带扩展名.a1的文件；如果使用者要求交互式界面，则显示一个交互式窗口，而且使用者能够单步浏览指令、跟随程序。最后，进行语义分析，接着显示位映像图(bitmap)(如果使用者要求)。图9-4显示FrontEnd()子过程的代码。

9.3.1 语法分析器

语法分析器确定从装载器提供的入口点开始的代码是不是跟它储存的编译器签名之一等同，如果相同，就可以确定该程序的main并且把它作为这个分析的入口点。只要识别出一个编译器签名，就可以装入与之相关联的库签名文件。如果没有检测出编译器签名，语法分析过程也丝毫不受影响。此时，从装载器提供的入口点开始的所有代码都要被反编译，而且不做库例程识别。指出很重要一点是，有些编译器有set-up例程，由于它们使用间接跳转，所以很难对它们做语法分析；在这种情况下，无法对完整的代码进行语法分析，反编译将陷入困境。

void FrontEnd (char *filename, CALL_GRAPH *callGraph)

{

/* Parse image while building call graph and generating Icode */

parse (callGraph);

/* Transform basic block list into a graph */

constructGraph (callGraph);

/* Check for bytes used as both data and code */

checkDataCode (callGraph);

if (option.asm1) /* disassembly of the program */

{

printf ("dcc: writing assembler file %s.a1\n", filename);

disassemble (1, callGraph, filename);

}

if (option.Interact) /* interactive option, display window */

interactWin (callGraph);

/* Idiom analysis */

semAnalyzer (callGraph);

/* Remove redundancies from the graph */

compressCFG (callGraph);

if (option.stats) /* statistics on the basic blocks */

displayStats (callGraph);

if (option.asm2) /* disassembly after graph compression */

disassemble (2, callGraph, filename);

if (option.map) /* display memory bitmap */

displayMemMap();

}

图 9-4: 前端子过程

根据一个子程序的入口点，语法分析器实现把数据/指令分离的算法，见第4章图4-7的描述。这个算法递归地跟随从入口点开始的所有路径，而且模拟写入寄存器 (只要有可能)。当遇到一个子程序调用的时候，子程序的入口地址变成一个新的入口点，通过递归的方式对它进行分析，把子程序信息放在调用图里。寄存器内容被模拟以便检测某些情况，比如程序利用中断结束——这将依赖一个或多个寄存器的内容。对于编译器签名已经被识别的程序，已知它是通过在主程序结束之后执行的一个例程来终止的，因此，在这个例子中模拟寄存器的内容不是必需的。这个语法分析器不尝试识别外层寻址(up-level addressing)。

图9-5显示PROC记录的定义，它储存一个子程序的有关信息。注意，在语法分析期间并非所有的字段都被填充信息；有些是稍后由通用的反编译机器进行填充。

typedef struct _proc {

Int procEntry; /* label number */

char name[SYMLEN]; /* Meaningful name for this proc */

STATE state; /* Entry state */

flags32 flg; /* Combination of Icode & Procedure flags */

int16 cbParam; /* Probable no. of bytes of parameters */

STKFRAME args; /* Array of formal arguments */

LOCAL_ID localId; /* Local symbol table */

ID retVal; /* Return value type (for functions) */

/* Icodes and control flow graph */

ICODE_REC Icode; /* Record of ICODE records */

PBB cfg; /* Pointer to control flow graph (cfg) */

PBB *dfsLast; /* Array of pointers to BBs in revPostorder */

Int numBBs; /* Number of basic blocks in the graph cfg */

boolT hasCase; /* Boolean: subroutine has an n-way node */

/* For interprocedural live analysis */

dword liveIn; /* Registers used before defined */

dword liveOut; /* Registers that may be used in successors */

boolT liveAnal; /* Procedure has been analysed already */

} PROC;

图 9-5: 子过程记录

语法分析器后面跟随一个checkDataCode()子过程，它检查在位映像图里的每个字节，看有没有两个标志：数据，和代码；当字节位置被标志为是数据和代码，那么相应的子程序被标志为潜在地使用自修改代码。

9.3.2 中间代码

在dcc中使用的中间代码叫做Icode，它有两种类型：低级的和高级的。低级Icode是i80286机器指令到汇编助记符的一个映射，确保每个Icode指令只执行一个逻辑指令。例如，指令：

DIV bx

把dx:ax除以bx所得的商赋值给ax，而且把前面这个商的余数赋值给dx；因此，DIV机器指令是执行了两个逻辑指令。在Icode指令中，DIV被分割成两个不同的指令：iDIV和iMOD。前者执行操作数的除法，而后者执行操作数的求余。因为这两个指令都使用被指令结果重写的寄存器 (即，这个例子中的dx和ax)，所以在指令被执行之前这些寄存器需要被放在一个临时寄存器中。dcc把寄存器tmp做为临时寄存器使用。这个寄存器被向前代入到另一个指令而且在数据流分析期间被清除掉。上述机器指令被翻译成三个Icode指令，如下：

iMOV tmp, dx:ax ; tmp = dx:ax

iDIV bx ; ax = tmp / bx

iMOD bx ; dx = tmp % bx

其中iDIV和iMOD的被除数都被设置为tmp寄存器而非dx:ax。图9-6显示不同的机器指令，它们用多于一个Icode指令表示。每个指令给出一个例子。

机器指令 | Icode指令 | 含义
DIV cl | iMOV tmp, ax | tmp = ax
 | iDIV cl | al = tmp / cl
 | iMOD cl | ah = tmp % cl
LOOP L | iSUB cx, 1 | cx = cx - 1
 | iJNCXZ L | cx<>0 goto L
LOOPE L | iSUB cx, 1 | cx = cx - 1
 | iCMP cx, 0 | cx == 0?
 | iJZ L | zeroFlag == 1 goto L
 | iJNCXZ L | if cx<>0 goto L
LOOPNE L | iSUB cx, 1 | cx = cx = 1
 | iCMP cx, 0 | cx == 0?
 | iJNE L | zeroFlag == 0 goto L
 | iJNCXZ L | if cx<>0 goto L
XCHG cx, bx | iMOV tmp, cx | tmp = cx
 | iMOV cx, bx | cx = bx
 | iMOV bx, tmp | bx = tmp

图 9-6: 用多于一个Icode指令表示的机器指令

复合的指令，比如rep movsb被表现为两个不同的机器指令，但它只是执行一个逻辑的字符串功能；在这个例子中就是当字符串未结束时重复。这些指令就用一个Icode指令表示；在这个例子中是iREP_MOVS。

执行低级任务的机器指令，比如从一个端口输入和输出，最有可能是由编译器在编译高级语言代码的时候产生的 (即，嵌入的汇编代码能够使用这些指令但是高级代码不生成这些指令)。这些指令在Icode中被标志为非高级的，而且使用这些指令的子程序也被这样做了标记，以便只为该子程序生成汇编程序。下列指令被认为不是由编译器生成的；被标记一个星号的指令有时是非高级的，取决于所使用的寄存器操作数：

AAA, AAD, AAM, AAS, CLI, DAA, DAS, *DEC, HLT, IN, *INC, INS,

INT, INTO, IRET, JO, JNO, JP, JNP, LAHF, LOCK, *MOV, OUT, OUTS,

*POP, POPA, POPF, *PUSH, PUSHA, PUSHF, SAHF, STI, *XCHG, XLAT

Icode指令有一组与之关联的Icode标志用来确认在指令的语法分析期间发现的特性。下列标志被使用：

l B：字节操作数 (缺省是字操作数)。

l I：立即的(常数)源操作数。

l No_Src：没有源操作数。

l No_Ops：没有操作数。

l Src_B：源操作数是字节，目的操作数是字。

l Im_Ops：隐含的操作数。

l Im_Src：隐含的源操作数。

l Im_Dst：隐含的目的操作数。

l Seg_Immed：指令有一个重定位的段数值。

l Not_Hll：非高级的指令。

l Data_Code：指令修改数据。

l Word_Off：指令有一个字偏移量 (即，可能是一个地址)。

l Terminates：指令终止该程序。

l Target：指令是一个跳转指令的目标。

l Switch：当前的间接跳转决定一个n路语句的开始。

l Synthetic：指令是人工合成的 (即，在二进制文件中不存在的)。

l Float_Op：下一个指令是一个浮点指令。

dcc使用一个由机器指令索引的静态表，实现i80286机器指令到低级Icodes的映射，该表包括的信息有相关联的Icode，被使用的和被定义的条件码、标志，以及确定源操作数和目的操作数的子过程 (不同的子过程被用于不同的操作数类型，因此，相同的子过程被几个不同的指令使用)。机器指令到Icode指令的映射把250个指令转换成108个Icode指令。这个映射见图9-7所示。

低级指令 | 机器指令(组)
iAAA | 37
iAAD | D5
iAAM | D4
iAAS | 3F
iADC | 10..15, (80..83)(50..57,90..97,D0..D7)
iADD | 00..05, (80..83)(40..47,80..87,C0..C7)
iAND | 20..25, (80..83)(60..67,A0..A7,E0..E7)
iBOUND | 62
iCALL | E8, FE50..FE57, FE90..FE9F, FED0..FED7, FF50..FF57, FF90..FF9F, FFD0..FFD7
iCALLF | 9A, FE58..FE5F, FE98..FE9F, FED8..FEDF, FF58..FF5F, FF98..FF9F, FFD8..FFDF
iCLC | F8
iCLD | FC
iCLI | FA
iCMC | F5
iCMP | 38..3D, (80..83)(78..7F,B8..BF,F8..FF)
iCMPS | A6, A7
iREPNE_CMPS | F2A6, F2A7
iREPE_CMPS | F3A6, F3A7
iDAA | 27
iDAS | 2F
iDEC | 48..4F, FE48..FE4F, FE88..FE8F, FEC8..FECF, FF48..FF4F, FF88..FF8F, FFC8..FFCF
iDIV | F670..F677, F6A0..F6A7, F6F0..F6F7, F770..F777, F7A0..F7A7, F7F0..F7F7
iMOD | F670..F677, F6A0..F6A7, F6F0..F6F7, F770..F777, F7A0..F7A7, F7F0..F7F7, F678..F67F, F6A8..F6AF, F6F8..F6FF, F778..F77F, F7A8..F7AF, F7F8..F7FF
iENTER | C8
iESC | D8..DF
iHLT | F4
iIDIV | F678..F67F, F6A8..F6AF, F6F8..F6FF, F778..F77F, F7A8..F7AF, F7F8..F7FF
iIMUL | 69, 6B, F668..F66F, F6A8..F6AF, F6E8..F6EF, F768..F76F, F7A8..F7AF, F7E8..F7EF
iIN | E4, E5, EC, ED
iINC | 40..47, FE40..FE47, FE80..FE87, FEC0..FEC7, FF40..FF47, FF80..FF87, FFC0..FFC7
iINS | 6C, 6D
iREP_INS | F36C, F36D
iINT | CC, CD
iINTO | CE
iIRET | CF
iJB | 72
iJBE | 76
iJAE | 73
iJA | 77
iJE | 74
iJNE | 75
iJL | 7C
iJGE | 7D
iJLE | 7E
iJG | 7F
iJS | 78
iJNS | 79
iJO | 70
iJNO | 71
iJP | 7A
iJNP | 7B
iJCXZ | E3
iJNCXZ | E0..E2
iJMP | E9, EB, FE60..FE67, FEA0..FEA7, FEE0..FEE7, FF60..FF67, FFA0..FF A7, FFE0..FFE7
iJMPF | EA, FE68..FE6F, FEA8..FEAF, FEE8..FEEF, FF68..FF6F, FFA8..FF AF, FFE8..FFEF
iLAHF | 9F
iLDS | C5
iLEA | 8D
iLEAVE | C9
iLES | C4
iLOCK | F0
iLODS | AC, AD
iREP_LODS | F3AC, F3AD
iMOV | 88..8C, 8E, A0..A3, B0..BF, C6, C7
iMOVS | A4, A5
iREP_MOVS | F3A4, F3A5
iMUL | F660..F667, F6A0..F6A7, F6E0..F6E7, F760..F767, F7A0..F7A7, F7E0..F7E7
iNEG | F658..F65F, F698..F69F, F6D8..F6DF, F758..F75F, F798..F79F, F7D8..F7DF
iNOT | F650..F657, F690..F697, F6D0..F6D7, F750..F757, F790..F797, F7D0..F7D7
iNOP | 90
iOR | 08..0D, (80..83)(48..4F,88..8F,C8..CF)
iOUT | E6, E7, EE, EF
iOUTS | 6E, 6F
iREP_OUTS | F36E, F36F
iPOP | 07, 17, 1F, 58..5F, 8F
iPOPA | 61
iPOPF | 9D
iPUSH | 06, 0E, 16, 1E, 50..57, 68, 6A, FE70..FE77, FEB0..FEB7, FEF0..FEF7, FF70..FF77, FFB0..FFB7, FFF0..FFF7
iPUSHA | 60
iPUSHF | 9C
iRCL | (C0,C1,D0..D3)(50..57,90..97,D0..D7)
iRCR | (C0,C1,D0..D3)(58..5F,98..9F,D8..DF)
iREPE | F3
iREPNE | F2
iRET | C2, C3
iRETF | CA, CB
iROL | (C0,C1,DO..D3)(40..47,80..87,C0..C7)
iROR | (C0,C1,D0..D3)(48..4F,88..8F,C8..CF)
iSAHF | 9E
iSAR | (C0,C1,D0..D3)(78..7F,B8..BF,F8..FF)
iSHL | (C0,C1,D0..D3)(60..67,A0..A7,E0..E7)
iSHR | (C0,C1,D0..D3)(68..6F,A8..AF,E8..EF)
iSBB | 18..1D, (80..83)(58..5F,98..9F,D8..DF)
iSCAS | AE, AF
iREPE_SCAS | F3AE, F3AF
iREPNE_SCAS | F2AE, F2AF
iSIGNEX | 98, 99
iSTC | F9
iSTD | FD
iSTI | FB
iSTOS | AA, AB
iREP_STOS | F3AA, F3AB
iSUB | 28..2D, (80..83)(68..6F,A8..AF,E8..EF)
iTEST | 84, 85, A8, A9, F640..F647, F680..F687, F6C0..F6C7, F740..F747, F780..F787, F7C0..F7C7
iWAIT | 9B
iXCHG | 86, 87, 91..97
iXLAT | D7
iXOR | 30..35, (80..83)(70..77,B0..B7,F0..F7)

图 9-7: 适用于i80286的低级中间代码

9.3.3 控制流向图生成器

dcc通过把基本块放在一个列表上然后把那个表转换成一个适当的图，为每个子程序实现控制流向图的构造。在语法分析的时候，只要遇到一个基本块的结束指令，该基本块就构造完成了，而且在该子程序的开始指令和结束指令在Icode数组里的索引号被记录储存。如果遇到无法确定它把控制转移到哪里的指令 (即，变址跳转假如未被识别为一个已知的n路结构头结点的，间接调用，等等) 那么应该让这个基本块结束了，因为沿着含有该指令的路径上已经没有更多指令需要解析了。这些结点在dcc中被叫做迷途结点(no-where)。其他类型的基本块有标准单路的、二路的、n路的、直通的、调用的、和返回结点。基本块的定义记录见图9-8所示。这个信息的大部分稍后由通用的反编译机器填写。

typedef struct _BB {

byte nodeType; /* Type of node */

Int start; /* First instruction offset */

Int finish; /* Last instruction in this BB */

flags32 flg; /* BB flags */

Int numHlIcodes; /* # of high-level Icodes */

/* In edges and out edges */

Int numInEdges; /* Number of in edges */

struct _BB **inEdges; /* Array of pointers to in-edges */

Int numOutEdges; /* Number of out edges */

union typeAdr {

dword ip; /* Out edge Icode address */

struct _BB *BBptr; /* Out edge pointer to successor BB */

interval *intPtr; /* Out edge pointer to next interval */

} *outEdges; /* Array of pointers to out-edges */

/* For interval and derived sequence construction */

Int beenOnH; /* #times been on header list H */

Int inEdgeCount; /* #inEdges (to find intervals) */

struct _BB *reachingInt; /* Reaching interval header */

interval *inInterval; /* Node's interval */

interval *correspInt; /* Corresponding interval in Gi-1 */

/* For live register analysis */

dword liveUse; /* LiveUse(b) */

dword def; /* Def(b) */

dword liveIn; /* LiveIn(b) */

dword liveOut; /* LiveOut(b) */

/* For structuring analysis */

Int preorder; /* DFS #: first visit of the node */

Int revPostorder; /* DFS #: last visit of the node */

Int immedDom; /* Immediate dominator (revPostorder) */

Int ifFollow; /* follow node (if node is 2-way) */

Int loopType; /* Type of loop (if any) */

Int latchNode; /* latching node of the loop */

Int numBackEdges; /* # of back edges */

Int loopFollow; /* node that follows the loop */

Int caseFollow; /* follow node for n-way node */

/* Other fields */

Int traversed; /* Boolean: traversed yet? */

struct _BB *next; /* Next (initial list link) */

} BB;

图 9-8: 基本块记录

每个子程序的控制流向图由控制流优化(flow-of-control optimizations)进行最优化——去掉多余的到跳转的跳转、和到跳转的条件跳转。这些最优化有可能从该图中移除一些基本块，因此直到所有可能的结点从图中移除以后才对该图进行编号。同时，每个基本块的前导结点被确定而且被放在*inEdges[]数组内。

9.3.4 语义分析程序

dcc的语义分析程序确定成语而且用其它的Icode指令(组)代替它们。在dcc中所检查的成语都在第4章第4.2.1节中有描述，被分成下列类别：子程序成语、调用约定、长整型变量运算、以及其它各种成语。

有一些的成语只在C语言中可用。在C语言中，一个变量可以被先加和后加，以及先减和后减。表现这些指令的机器代码使用一个额外的寄存器保存那个被先加/先减或后加/后减的变量的值，用于对比某数值/变量的检查。这个额外的寄存器可以通过利用某个成语把该指令组转换成一个使用‘先/后 加/减’操作数的指令而被清除掉。

对于在一个条件跳转中的一个后加/后减变量，该变量的值先被复制到一个寄存器，然后该变量被增量或减量，最后，保存最初的变量副本(即，在加或减之前)的寄存器跟其它标识符相比较。额外寄存器的使用可以利用C语言中的后加/后减操作符而清除掉。因此，仅当生成C语言代码时才能检查这些成语。图9-9显示这个情况。

mov reg, var |  | mov reg, var
inc var | 或者 | dec var
cmp var, Y |  | cmp var, Y
jX label |  | jX label
 | ↓ |
jcond (var++ X Y) |  | jcond (var-- X Y)

图 9-9: 在一个条件跳转中的后加或后减

类似地，先加/先减利用一个中间的寄存器。该变量先被增量/减量，然后它被移入一个寄存器，跟另一个标识符相比较，然后条件跳转发生。在这个情况中，中间寄存器的使用是因为在比较指令中不能使用除了寄存器之外的标识符。这个中间寄存器能够通过使用一个先加/先减操作符而去掉，如图9-10所示。

inc var |  | dec var
mov reg, var | 或者 | mov reg, var
cmp reg, Y |  | cmp reg, Y
jX lab |  | jX lab
 | ↓ |
jcond (++var X Y) |  | jcond (--var X Y)

图 9-10: 在条件跳转中的先加/先减

在dcc中实现了C语言依赖的成语。从这些成语的一般格式可知，几个低级Icode指令被一个高级的jcond指令代替。这个指令被标志为一个高级指令，如此它不再被数据流分析程序处理。而且，所有参与这些成语的其它指令被标志为不表现高级指令。

在成语识别之后，对有符号整型、有符号字节、和长整型变量做简单类型传播。在跨条件结构传播长整型变量的时候，传播通过从图中移除一个结点而修改控制流向图，正如第4章第4.2.2节所描述。因为高级条件是从图的类型来确定的，所以写入对应的高级jcond指令而且那个指令被标志为是一个高级指令。

9.4 反汇编器

dcc实现一个内建的反汇编器，它产生汇编程序文件。汇编文件只包含关于该程序的汇编语言助记符 (也就是代码段) 而不显示与数据有关的任何信息。反汇编器使用的所有信息是由语法分析器和反编译器的中间代码阶段收集的；因为基本上从低级Icodes到汇编助记符存在一个1:1映射，所以汇编代码生成器主要在于输出格式化。

反汇编器一次处理一个子程序；给予一个调用图，该图被以一个深度优先搜索遍历，首先为被嵌套的子程序生成汇编程序。对于生成汇编程序文件，使用者有两个选择项：一是在语法分析阶段之后直接生成汇编程序，一是在图的最优化之后生成汇编程序。前者产生的汇编程序跟二进制映像尽可能地接近；后者可能丢失某些被图的最优化器认为多余的跳转指令。反编译器在产生目标C语言代码的时候也使用反汇编器；如果一个子程序被标志为一个非高级子程序的，那么在生成该子程序的C语言头文件和注解以后只为该子程序生成汇编代码。

9.5 通用的反编译机器

通用的反编译机器(udm)由两个阶段组成；数据流分析阶段把低级Icode转换成一个最理想的高级Icode表示法，和控制流分析阶段遍历每个子程序的图以确定条件结构和循环的范围；这些边界稍后被代码生成器使用。图9-11显示udm()子过程的代码。

9.5.1 数据流分析

数据流分析的第一部分是条件码的去除。条件码被分成两组：可能由编译器产生的条件码 (HLCC组)，和可能手工汇编制作的的条件码 (NHLCC组)。在Intel i80286 [Int86](溢出、方向、中断准许、陷阱、符号、零、辅助进位、对等和进位)的9个条件码中，只有4个标志可能是高级的；它们是进位、方向、零和符号。这些标志被用指令修改，这些指令可能是高级指令 (即，没有被打上非高级语言的标志)，因此对这一组做条件码去除的分析。在可能的高级指令中，30个指令定义HLCC组的标志；一个指令定义1个标志到3个标志不等，而且25个指令使用标志；通常，每个指令使用一个或两个标志。dcc实现了死条件码清除和条件码传播，详见第5章第5.4.2节和第5.4.3节的描述。这些最优化去掉所有对条件码的引用并且创建有一布尔条件表达式与之关联的jcond指令。这个分析与所有其它低级Icodes到高级Icodes的、用寄存器表达的最初映射互相重叠。最初的Icodes映射见附录D说明。

void udm (CALL_GRAPH *callGraph)

{ derSeq *derivedG;

/* Data flow analysis - optimizations on Icode */

dataFlow (callGraph);

/* Control flow analysis -- structure the graphs */

/* Build derived sequences for each subroutine */

buildDerivedSeq (callGraph, &derivedG);

if (option.VeryVerbose) /* display derived sequence for each subroutine */

displayDerivedSeq (derivedG);

/* Graph structuring */

structure (callGraph, derivedG);

}

图 9-11: 通用的反编译机器子过程

这个分析的第二部分是关于该图中的基本块和Icode指令操作数的统计信息生成。为每个子程序进行一个定义-使用和使用-定义分析(definition-use and use-definition analysis)；为每个指令建立与之关联的链(chain)。在建立这些链的过程当中，进行死寄存器清除，详见第5章第5.4.1节的描述。下一步，为每个子程序做一个子过程内的活寄存器分析，以确定被该子程序使用的任何寄存器参数。这个分析在第5章第5.4.4节中描述。最后，接着做一个子过程间的活寄存器分析，以确定被函数返回的寄存器；分析在第5章第5.4.5节中描述。

死寄存器清除确定DIV机器指令的目的——即，这个指令是用来求操作数的商和余数这两个东西。下面的中间代码

1 | asgn tmp, dx:ax | ; ud(tmp) = {2,3}
2 | asgn ax, tmp / bx | ; ud(ax) = {}
3 | asgn dx, tmp % bx | ; ud(dx) = {4}
4 | asgn [bp-2], dx | /* no further use of ax before redefinition */

确定寄存器ax在重新定义之前不被使用，因为在指令2中它的使用-定义链是空的。因为这个定义是死的，所以把该指令去掉——两个操作数的除法运算，并得到以下代码：

1 | asgn tmp, dx:ax | ; ud(tmp) = {2,3}
3 | asgn dx, tmp % bx | ; ud(dx) = {4}
4 | asgn [bp-2], dx |

所有那些在其使用-定义链中有指令2的指令需要被更新，以反映该寄存器不再被使用的这个事实，因为指令2是被用来定义一个死的寄存器；因此，在这个例子中，指令1的ud()链被更新，得到最后的代码：

1 | asgn tmp, dx:ax | ; ud(tmp) = {3}
3 | asgn dx, tmp % bx | ; ud(dx) = {4}
4 | asgn [bp-2], dx |

该分析的第三部分和最后部分是通过应用寄存器的使用-定义链来进行扩展的寄存器拷贝传播，详见第5章第5.4.10节的描述。这个分析去掉多余的寄存器引用，确定高级表达式，把实际参数放在子程序的列表上，而且跨子程序调用传播参数类型。在该分析中始终使用一个临时的表达式栈来清除中间伪高级指令push和pop。

在前面的例子中，通过向前代入，确定了最初的DIV指令只是用来计算在两个操作数之间的模 (在这个例子中，这两个操作数被放在寄存器dx:ax和bx)：

4

asgn [bp-2], dx:ax % bx

9.5.2 控制流分析

控制流分析程序有两个部分：第一部分为调用图中的每个子程序构造一个派生图序列而且计算区间。这个序列被结构化算法用来确定循环的边界和这些循环的嵌套层次。一旦为这个子程序建立派生图序列以后，就测试该图的可归约性；如果极限第n个顺序图不是一个平凡图，那么该子程序就是不可归约的。

该分析第二部分是程序的控制流向图的结构化。结构化算法确定循环和条件结构 (二路和n路结构) 的边界；这些边界稍后在代码生成期间被使用。循环被使用区间来结构化，而且它们的嵌套层次是根据它们在派生图序列中的建立顺序来确定，详见第6章第6.6.1节的描述。通过这个算法确定先测试循环、后测试循环和无穷循环。条件结构的结构化是依靠深度优先搜索树的一个反向遍历；这样，被嵌套的条件结构首先被发现。结构化二路条件和n路条件的方法在第6章第6.6.2节和第6.6.3节描述。这个方法考虑到组合的布尔条件，而且通过把两个或多个结点的布尔条件信息储存在这一个结点而从该图中移除一些结点。

9.6 后端

后端的全部就是C代码生成器。这个模块打开输出文件，给予扩展名.b (b表示beta)，为它编写程序头，然后调用代码生成器。一旦为整个的图生成全部代码以后，就把文件关闭。图9-12显示后端子过程的代码。

void BackEnd (char *fileName, CALL_GRAPH *callGraph)

{ FILE *fp; /* Output C file */

/* Open output file with extension .b */

openFile (fp, filename, ".b", "wt");

printf ("dcc: Writing C beta file %s.b\n", fileName);

/* Header information */

writeHeader (fp, fileName);

/* Process each procedure at a time */

writeCallGraph (fileName, callGraph, fp);

/* Close output file */

fclose (fp);

printf ("dcc: Finished writing C beta file\n");

}

图 9-12: 后端子过程

9.6.1 代码生成

dcc实现第7章第7.1节描述的C代码生成器。程序的调用图以深度优先方式遍历，首先为叶子代表的子程序生成C代码 (亦即，如果该图是可归约的，就按照逆调用顺序)。根据每个子程序的控制流向图的结构，为它生成相应的代码；条件结构和循环的边界已经由结构化阶段在图中做了标记。代码是通过递归方式生成的，所以，在递归中如果一个结点不是第一次被到达，就用一个goto跳转以转移控制到与这个结点相关联的代码。

由于从表达式叶子里发现的寄存器在代码生成期间才给予命名 (即，在所有局部变量都已经被定义在局部变量定义区之后)，而且已经生成代码的那些指令可能会有一个与之关联的标签，以便作为后面可能产生的某个goto跳转目标，所以代码不能被直接生成并输出一个文件，而是需要先储存在一个中间的数据结构中直至一个完整的子程序代码全部已经生成；然后它可以被复制到目标输出文件，而且该结构给调用图中的下一个子程序重复使用。dcc用来管理子程序声明和代码的数据结构名叫bundle (包裹)。一个bundle由两个行数组(array of lines)组成，一个用于子程序声明，另一个用于子程序代码。子程序声明不仅包括子程序头，而且包括注解和局部变量定义。如果最初分配的数组尺寸太小，行数组可以动态增长。数据结构bundle的定义见图9-13所示。

typedef struct {

Int numLines; /* Number of lines in the table */

Int allocLines; /* Number of lines allocated in the table */

char **str; /* Table of strings */

} strTable;

typedef struct {

strTable decl; /* Declarations */

strTable code; /* C code */

} bundle;

图 9-13: 数据结构bundle的定义

dcc所显示的注解和错误信息在附录E列表。

9.7 结果

这一节给出用dcc反编译的一些程序。最初的程序是用C语言编写而且用DOS下的Borland Turbo C编译。这些程序使用基本类型变量 (即，字节、整型和长整型)，而且通过例子说明反编译过程的不同方面。利用批处理模式反编译这些程序，产生反汇编文件.a2、C语言文件.b、该程序的调用图和关于中间代码指令的统计。对于产生C代码的所有子程序，该统计反映中间指令缩减比例；被翻译成汇编程序的子程序不在此统计之列。为每个程序给出一个低级指令和高级指令的合计，以及一个总的缩减比例。

前三个程序举例说明三个不同基本类型的运算。最初的C程序有相同的代码，只不过它们的变量被定义为不同的类型。其后四个程序是来自Plum-Hall基准测试(benchmark)套装的基准程序。这些程序是Eric S. Raymond编写的而且在网上可免费获得 [Ray89]。这些程序被修改成使用scanf()询问程序的参数，而不在argv[]命令行数组中扫描参数，因为dcc不支持数组。最后，余下的三个程序：计算斐波纳契数(Fibonacci number)，计算一个字符的循环冗余校验(CRC)，和两个矩阵相乘。最后这一个程序是用来展示如何从低级中间代码推导出数组表示法。


第 9 章 dcc - Intops.exe

9.7.1 Intops.exe

Intops这个程序在两个整型变量上执行不同的运算，并且显示这些变量的最后结果。反汇编的C程序在图9-14中显示，反编译的C程序在图9-15，最初的C程序在图9-16。该程序有下列调用图：

main

printf

从该程序的反汇编可知，第二个变量被放在寄存器si里，而第一个变量被放在栈的偏移-2上。语法分析器为IDIV机器指令产生人工合成的指令(Synthetic inst)；这个指令一次被用来做除法，另一次用来求模。中间代码使用临时寄存器tmp，详见前面的第9.3.2节说明；这个寄存器在数据流分析期间被清除掉。对于每个运算，该指令的操作数被移到寄存器，在寄存器上执行运算，而结果被放回到变量上。该程序中没有控制结构。成语和数据流分析减少的中间指令数目达77.78%，如图9-17所示。

 | main | PROC NEAR |  |  |
000 | 0002FA | 55 | PUSH | bp |
001 | 0002FB | 8BEC | MOV | bp,sp |
002 | 0002FD | 83EC02 | SUB | sp,2 |
003 | 000300 | 56 | PUSH | si |
004 | 000301 | C746FEFF00 | MOV | word ptr [bp-2],0FFh |
005 | 000306 | BE8F00 | MOV | si,8Fh |
006 | 000309 | 8B46FE | MOV | ax,[bp-2] |
007 | 00030C | 03C6 | ADD | ax,si |
008 | 00030E | 8BF0 | MOV | si,ax |
009 | 000310 | 8B46FE | MOV | ax,[bp-2] |
010 | 000313 | 2BC6 | SUB | ax,si |
011 | 000315 | 8946FE | MOV | [bp-2],ax |
012 | 000318 | 8B46FE | MOV | ax,[bp-2] |
013 | 00031B | F7E6 | MUL | si |
014 | 00031D | 8946FE | MOV | [bp-2],ax |
015 | 000320 | 8BC6 | MOV | ax,si |
016 | 000322 | 99 | CWD |  |
017 |  |  | MOV | tmp, dx:ax | ;Synthetic inst
018 | 000323 | F77EFE | IDIV | word ptr [bp-2] |
019 |  |  | MOD | word ptr [bp-2] | ;Synthetic inst
020 | 000326 | 8BF0 | MOV | si,ax |
021 | 000328 | 8BC6 | MOV | ax,si |
022 | 00032A | 99 | CWD |  |
023 |  |  | MOV | tmp,dx:ax | ;Synthetic inst
024 | 00032B | F77EFE | IDIV | word ptr [bp-2] |
025 |  |  | MOD | word ptr [bp-2] | ;Synthetic inst
026 | 00032E | 8BF2 | MOV | si,dx |
027 | 000330 | 8B46FE | MOV | ax,[bp-2] |
028 | 000333 | B105 | MOV | cl,5 |
029 | 000335 | D3E0 | SHL | ax,cl |
030 | 000337 | 8946FE | MOV | [bp-2],ax |
031 | 00033A | 8BC6 | MOV | ax,si |
032 | 00033C | 8A4EFE | MOV | cl,[bp-2] |
033 | 00033F | D3F8 | SAR | ax,cl |
034 | 000341 | 8BF0 | MOV | si,ax |
035 | 000343 | 56 | PUSH | si |
036 | 000344 | FF76FE | PUSH | word ptr [bp-2] |
037 | 000347 | B89401 | MOV | ax,194h |
038 | 00034A | 50 | PUSH | ax |
039 | 00034B | E8AC06 | CALL | near ptr printf |
040 | 00034E | 83C406 | ADD | sp,6 |
041 | 000351 | 5E | POP | si |
042 | 000352 | 8BE5 | MOV | sp,bp |
043 | 000354 | 5D | POP | bp |
044 | 000355 | C3 | RET |  |
 | main | ENDP |  |  |

图 9-14: Intops.a2

/*

* Input file : intops.exe

* File type : EXE

*/

#include "dcc.h"

void main ()

/* Takes no parameters.

* High-level language prologue code.

*/

{

int loc1;

int loc2;

loc1 = 255;

loc2 = 143;

loc2 = (loc1 + loc2);

loc1 = (loc1 - loc2);

loc1 = (loc1 * loc2);

loc2 = (loc2 / loc1);

loc2 = (loc2 % loc1);

loc1 = (loc1 << 5);

loc2 = (loc2 >> loc1);

printf ("a = %d, b = %d\n", loc1, loc2);

}

图 9-15: Intops.b

#define TYPE int

main()

{ TYPE a, b;

a = 255;

b = 143;

b = a + b;

a = a - b;

a = a * b;

b = b / a;

b = b % a;

a = a << 5;

b = b >> a;

printf ("a = %d, b = %d\n", a, b);

}

图 9-16: Intops.c

子程序 | 低级 | 高级 | % 缩减率
main | 45 | 10 | 77.78
合计 | 45 | 10 | 77.78

图 9-17: Intops统计


第 9 章 dcc - Byteops.exe

9.7.2 Byteops.exe

Byteops是一个与intops类似的程序，差别在于这两个变量是字节并非整型数。反汇编的程序见图9-18所示，反编译的C版本在图9-19，最初的C程序在图9-20。该程序有下列调用图：

main

printf

从程序的反汇编可知，局部变量被放在栈的偏移-1和-2上。和intops.a2程序相比，这个程序多出22.41%的指令，因为有些机器指令比如IDIV取字寄存器作为操作数而不是用字节寄存器；因此，字节寄存器或被填补或被符号扩展以形成一个字寄存器。两个程序中最后的高级指令数目是相同的，因此在这个程序里面中间指令数目的缩减率比较大。它达到82.76%，如图9-21所示。

 | main | PROC NEAR |  |  |
000 | 0002FA | 55 | PUSH | bp |
001 | 0002FB | 8BEC | MOV | bp,sp |
002 | 0002FD | 83EC02 | SUB | sp,2 |
003 | 000300 | C646FEFF | MOV | byte ptr [bp-2],0FFh |
004 | 000304 | C646FF8F | MOV | byte ptr [bp-1],8Fh |
005 | 000308 | 8A46FE | MOV | al,[bp-2] |
006 | 00030B | 0246FF | ADD | al,[bp-1] |
007 | 00030E | 8846FF | MOV | [bp-1],al |
008 | 000311 | 8A46FE | MOV | al,[bp-2] |
009 | 000314 | 2A46FF | SUB | al,[bp-1] |
010 | 000317 | 8846FE | MOV | [bp-2],al |
011 | 00031A | 8A46FE | MOV | al,[bp-2] |
012 | 00031D | B400 | MOV | ah,0 |
013 | 00031F | 8A56FF | MOV | dl,[bp-1] |
014 | 000322 | B600 | MOV | dh,0 |
015 | 000324 | F7E2 | MUL | dx |
016 | 000326 | 8846FE | MOV | [bp-2],al |
017 | 000329 | 8A46FF | MOV | al,[bp-1] |
018 | 00032C | B400 | MOV | ah,0 |
019 | 00032E | 8A56FE | MOV | dl,[bp-2] |
020 | 000331 | B600 | MOV | dh,0 |
021 | 000333 | 8BDA | MOV | bx,dx |
022 | 000335 | 99 | CWD |  |
023 |  |  | MOV | tmp,dx:ax | ;Synthetic inst
024 | 000336 | F7FB | IDIV | bx |
025 |  |  | MOD | bx | ;Synthetic inst
026 | 000338 | 8846FF | MOV | [bp-1],al |
027 | 00033B | 8A46FF | MOV | al,[bp-1] |
028 | 00033E | B400 | MOV | ah,0 |
029 | 000340 | 8A56FE | MOV | dl,[bp-2] |
030 | 000343 | B600 | MOV | dh,0 |
031 | 000345 | 8BDA | MOV | bx,dx |
032 | 000347 | 99 | CWD |  |
033 |  |  | MOV | tmp,dx:ax | ;Synthetic inst
034 | 000348 | F7FB | IDIV | bx |
035 |  |  | MOD | bx | ;Synthetic inst
036 | 00034A | 8856FF | MOV | [bp-1],dl |
037 | 00034D | 8A46FE | MOV | al,[bp-2] |
038 | 000350 | B105 | MOV | cl,5 |
039 | 000352 | D2E0 | SHL | al,cl |
040 | 000354 | 8846FE | MOV | [bp-2],al |
041 | 000357 | 8A46FF | MOV | al,[bp-1] |
042 | 00035A | 8A4EFE | MOV | cl,[bp-2] |
043 | 00035D | D2E8 | SHR | al,cl |
044 | 00035F | 8846FF | MOV | [bp-1],al |
045 | 000362 | 8A46FF | MOV | al,[bp-1] |
046 | 000365 | B400 | MOV | ah,0 |
047 | 000367 | 50 | PUSH | ax |
048 | 000368 | 8A46FE | MOV | al,[bp-2] |
049 | 00036B | B400 | MOV | ah,0 |
050 | 00036D | 50 | PUSH | ax |
051 | 00036E | B89401 | MOV | ax,194h |
052 | 000371 | 50 | PUSH | ax |
053 | 000372 | E8AB06 | CALL | near ptr printf |
054 | 000375 | 83C406 | ADD | sp,6 |
055 | 000378 | 8BE5 | MOV | sp,bp |
056 | 00037A | 5D | POP | bp |
057 | 00037B | C3 | RET |  |
 | main | ENDP |  |  |

图 9-18: Byteops.a2

/*

* Input file : byteops.exe

* File type : EXE

*/

#include "dcc.h"

void main ()

/* Takes no parameters.

* High-level language prologue code.

*/

{

int loc1;

int loc2;

loc1 = 255;

loc2 = 143;

loc2 = (loc1 + loc2);

loc1 = (loc1 - loc2);

loc1 = (loc1 * loc2);

loc2 = (loc2 / loc1);

loc2 = (loc2 % loc1);

loc1 = (loc1 << 5);

loc2 = (loc2 >> loc1);

printf ("a = %d, b = %d\n", loc1, loc2);

}

图 9-19: Byteops.b

#define TYPE unsigned char

main()

{ TYPE a, b;

a = 255;

b = 143;

b = a + b;

a = a - b;

a = a * b;

b = b / a;

b = b % a;

a = a << 5;

b = b >> a;

printf ("a = %d, b = %d\n", a, b);

}

图 9-20: Byteops.c

子程序 | 低级 | 高级 | % 缩减率
main | 58 | 10 | 82.76
合计 | 58 | 10 | 82.76

图 9-21: Byteops统计


第 9 章 dcc - Longops.exe

9.7.3 Longops.exe

longops程序跟intops和byteops程序类似，不过它使用两个长整型变量。反汇编的程序见图9-22所示，反编译的C程序在图9-23，最初的C程序在图9-24。程序有下列调用图：

main

LXMUL@

LDIV@

LMOD@

LXLSH@

LXRSH@

printf

长整型变量的运算使用成语和编译器的运行时支持例程。在这个程序中，长整型的加法和减法是由第4章第4.2.1节的成语执行，而运行时例程LXMUL@, LDIV@, LMOD@, LXLSH@, 和LXRSH@各被用于长整型的乘法、除法、求余、左移位、和右移位。从这些运行时例程，长整型乘法、左移位、和右移位是可以被翻译成C语言的；在某些情况下使用宏来存取一个变量的低字部分或高字部分。除法和求模例程无法被翻译成C语言，所以为它们生成汇编程序。长整型变量被放在栈中偏移量-4和-8上 (见main子程序)。主程序比intops程序多出28.57%的指令、而比byteops程序多出7.9%的指令。指令数目增加的原因有两个：首先，长整型变量到寄存器的传送要用两个指令而不是一个指令 (即，高字部分和低字部份被传送到不同的寄存器)，其次，对运行时支持例程的子程序调用指令。由于中间指令数目的缩减率84.13%，最后反编译的主程序仍然产生与前面两个程序相同数目的高级指令，如图9-25所示。总的指令数目缩减率是58.97%，这么低是归功于被翻译成C语言的运行时例程，它们没有做大量的寄存器移动，因为参数已在寄存器中而且这些例程最初是用汇编语言编写。

 | LXRSH@ | PROC FAR |  |  |
000 | 001269 | 80F910 | CMP | cl,10h |
001 | 00126C | 7310 | JAE | L1 |
002 | 00126E | 8BDA | MOV | bx,dx |
003 | 001270 | D3E8 | SHR | ax,cl |
004 | 001272 | D3FA | SAR | dx,cl |
005 | 001274 | F6D9 | NEG | cl |
006 | 001276 | 80C110 | ADD | cl,10h |
007 | 001279 | D3E3 | SHL | bx,cl |
008 | 00127B | 0BC3 | OR | ax,bx |
009 | 00127D | CB | RETF |  |
010 | 00127E | 80E910 | L1: SUB | cl,10h |
011 | 001281 | 8BC2 | MOV | ax,dx |
012 | 001283 | 99 | CWD |  |
013 | 001284 | D3F8 | SAR | ax,cl |
014 | 001286 | CB | RETF |  |
 | LXRSH@ | ENDP |  |  |
 | LXLSH@ | PROC FAR |  |  |
000 | 001287 | 80F910 | CMP | cl,10h |
001 | 00128A | 7310 | JAE | L2 |
002 | 00128C | 8BD8 | MOV | bx,ax |
003 | 00128E | D3E0 | SHL | ax,cl |
004 | 001290 | D3E2 | SHL | dx,cl |
005 | 001292 | F6D9 | NEG | cl |
006 | 001294 | 80C110 | ADD | cl,10h |
007 | 001297 | D3EB | SHR | bx,cl |
008 | 001299 | 0BD3 | OR | dx,bx |
009 | 00129B | CB | RETF |  |
010 | 00129C | 80E910 | L2: SUB | cl,10h |
011 | 00129F | 8BD0 | MOV | dx,ax |
012 | 0012A1 | 33C0 | XOR | ax,ax |
013 | 0012A3 | D3E2 | SHL | dx,cl |
014 | 0012A5 | CB | RETF |  |
 | LXLSH@ | ENDP |  |  |
 | LMOD@ | PROC FAR |  |  |
000 | 0011CF | B90200 | MOV | cx,2 |
002 | 0011D7 | 55 | PUSH | bp |
003 | 0011D8 | 56 | PUSH | si |
004 | 0011D9 | 57 | PUSH | di |
005 | 0011DA | 8BEC | MOV | bp,sp |
006 | 0011DC | 8BF9 | MOV | di,cx |
007 | 0011DE | 8B460A | MOV | ax,[bp+0Ah] |
008 | 0011E1 | 8B560C | MOV | dx,[bp+0Ch] |
009 | 0011E4 | 8B5E0E | MOV | bx,[bp+0Eh] |
010 | 0011E7 | 8B4E10 | MOV | cx,[bp+10h] |
011 | 0011EA | 0BC9 | OR | cx,cx |
012 | 0011EC | 7508 | JNE | L3 |
013 | 0011EE | 0BD2 | OR | dx,dx |
014 | 0011F0 | 7469 | JE | L4 |
015 | 0011F2 | 0BDB | OR | bx,bx |
016 | 0011F4 | 7465 | JE | L4 |
017 | 0011F6 | F7C70100 | L3: TEST | di,1 |
018 | 0011FA | 751C | JNE | L5 |
019 | 0011FC | 0BD2 | OR | dx,dx |
020 | 0011FE | 790A | JNS | L6 |
021 | 001200 | F7DA | NEG | dx |
022 | 001202 | F7D8 | NEG | ax |
023 | 001204 | 83DA00 | SBB | dx,0 |
024 | 001207 | 83CF0C | OR | di,0Ch |
025 | 00120A | 0BC9 | L6: OR | cx,cx |
026 | 00120C | 790A | JNS | L5 |
027 | 00120E | F7D9 | NEG | cx |
028 | 001210 | F7DB | NEG | bx |
029 | 001212 | 83D900 | SBB | cx,0 |
030 | 001215 | 83F704 | XOR | di,4 |
031 | 001218 | 8BE9 | L5: MOV | bp,cx |
032 | 00121A | B92000 | MOV | cx,20h |
033 | 00121D | 57 | PUSH | di |
034 | 00121E | 33FF | XOR | di,di |
035 | 001220 | 33F6 | XOR | si,si |
036 | 001222 | D1E0 | L7: SHL | ax,1 |
037 | 001224 | D1D2 | RCL | dx,1 |
038 | 001226 | D1D6 | RCL | si,1 |
039 | 001228 | D1D7 | RCL | di,1 |
040 | 00122A | 3BFD | CMP | di,bp |
041 | 00122C | 720B | JB | L8 |
042 | 00122E | 7704 | JA | L9 |
043 | 001230 | 3BF3 | CMP | si,bx |
044 | 001232 | 7205 | JB | L8 |
045 | 001234 | 2BF3 | L9: SUB | si,bx |
046 | 001236 | 1BFD | SBB | di,bp |
047 | 001238 | 40 | INC | ax |
048 | 001239 | E2E7 | L8: LOOP | L7 |
049 | 00123B | 5B | POP | bx |
050 | 00123C | F7C30200 | TEST | bx,2 |
051 | 001240 | 7406 | JE | L10 |
052 | 001242 | 8BC6 | MOV | ax,si |
053 | 001244 | 8BD7 | MOV | dx,di |
054 | 001246 | D1EB | SHR | bx,1 |
055 | 001248 | F7C30400 | L10: TEST | bx,4 |
056 | 00124C | 7407 | JE | L11 |
057 | 00124E | F7DA | NEG | dx |
058 | 001250 | F7D8 | NEG | ax |
059 | 001252 | 83DA00 | SBB | dx,0 |
060 | 001255 | 5F | L11: POP | di |
061 | 001256 | 5E | POP | si |
062 | 001257 | 5D | POP | bp |
063 | 001258 | CA0800 | RETF | 8 |
064 |  |  | L4: MOV | tmp,dx:ax | ;Synthetic inst
065 | 00125B | F7F3 | DIV | bx |
066 |  |  | MOD | bx | ;Synthetic inst
067 | 00125D | F7C70200 | TEST | di,2 |
068 | 001261 | 7402 | JE | L12 |
069 | 001263 | 8BC2 | MOV | ax,dx |
070 | 001265 | 33D2 | L12: XOR | dx,dx |
071 | 001267 | EBEC | JMP | L11 |
 | LMOD@ | ENDP |  |  |
 | LDIV@ | PROC FAR |  |  |
000 | 0011C6 | 33C9 | XOR | cx,cx |
002 | 0011D7 | 55 | PUSH | bp |
003 | 0011D8 | 56 | PUSH | si |
004 | 0011D9 | 57 | PUSH | di |
005 | 0011DA | 8BEC | MOV | bp,sp |
006 | 0011DC | 8BF9 | MOV | di,cx |
007 | 0011DE | 8B460A | MOV | ax,[bp+0Ah] |
008 | 0011E1 | 8B560C | MOV | dx,[bp+0Ch] |
009 | 0011E4 | 8B5E0E | MOV | bx,[bp+0Eh] |
010 | 0011E7 | 8B4E10 | MOV | cx,[bp+10h] |
011 | 0011EA | 0BC9 | OR | cx,cx |
012 | 0011EC | 7508 | JNE | L13 |
013 | 0011EE | 0BD2 | OR | dx,dx |
014 | 0011F0 | 7469 | JE | L14 |
015 | 0011F2 | 0BDB | OR | bx,bx |
016 | 0011F4 | 7465 | JE | L14 |
017 | 0011F6 | F7C70100 | L13: TEST | di,1 |
018 | 0011FA | 751C | JNE | L15 |
019 | 0011FC | 0BD2 | OR | dx,dx |
020 | 0011FE | 790A | JNS | L16 |
021 | 001200 | F7DA | NEG | dx |
022 | 001202 | F7D8 | NEG | ax |
023 | 001204 | 83DA00 | SBB | dx,0 |
024 | 001207 | 83CF0C | OR | di,0Ch |
025 | 00120A | 0BC9 | L16: OR | cx,cx |
026 | 00120C | 790A | JNS | L15 |
027 | 00120E | F7D9 | NEG | cx |
028 | 001210 | F7DB | NEG | bx |
029 | 001212 | 83D900 | SBB | cx,0 |
030 | 001215 | 83F704 | XOR | di,4 |
031 | 001218 | 8BE9 | L15: MOV | bp,cx |
032 | 00121A | B92000 | MOV | cx,20h |
033 | 00121D | 57 | PUSH | di |
034 | 00121E | 33FF | XOR | di,di |
035 | 001220 | 33F6 | XOR | si,si |
036 | 001222 | D1E0 | L17: SHL | ax,1 |
037 | 001224 | D1D2 | RCL | dx,1 |
038 | 001226 | D1D6 | RCL | si,1 |
039 | 001228 | D1D7 | RCL | di,1 |
040 | 00122A | 3BFD | CMP | di,bp |
041 | 00122C | 720B | JB | L18 |
042 | 00122E | 7704 | JA | L19 |
043 | 001230 | 3BF3 | CMP | si,bx |
044 | 001232 | 7205 | JB | L18 |
045 | 001234 | 2BF3 | L19: SUB | si,bx |
046 | 001236 | 1BFD | SBB | di,bp |
047 | 001238 | 40 | INC | ax |
048 | 001239 | E2E7 | L18: LOOP | L17 |
049 | 00123B | 5B | POP | bx |
050 | 00123C | F7C30200 | TEST | bx,2 |
051 | 001240 | 7406 | JE | L20 |
052 | 001242 | 8BC6 | MOV | ax,si |
053 | 001244 | 8BD7 | MOV | dx,di |
054 | 001246 | D1EB | SHR | bx,1 |
055 | 001248 | F7C30400 | L20: TEST | bx,4 |
056 | 00124C | 7407 | JE | L21 |
057 | 00124E | F7DA | NEG | dx |
058 | 001250 | F7D8 | NEG | ax |
059 | 001252 | 83DA00 | SBB | dx,0 |
060 | 001255 | 5F | L21: POP | di |
061 | 001256 | 5E | POP | si |
062 | 001257 | 5D | POP | bp |
063 | 001258 | CA0800 | RETF | 8 |
064 |  |  | L14: MOV | tmp,dx:ax | ;Synthetic inst
065 | 00125B | F7F3 | DIV | bx |
066 |  |  | MOD | bx | ;Synthetic inst
067 | 00125D | F7C70200 | TEST | di,2 |
068 | 001261 | 7402 | JE | L22 |
069 | 001263 | 8BC2 | MOV | ax,dx |
070 | 001265 | 33D2 | L22: XOR | dx,dx |
071 | 001267 | EBEC | JMP | L21 |
 | LDIV@ | ENDP |  |  |
 | LXMUL@ | PROC FAR |  |  |
000 | 0009C3 | 56 | PUSH | si |
001 |  |  | MOV | tmp,ax | ;Synthetic inst
002 |  |  | MOV | ax,si | ;Synthetic inst
003 |  |  | MOV | si,tmp | ;Synthetic inst
004 |  |  | MOV | tmp,ax | ;Synthetic inst
005 |  |  | MOV | ax,dx | ;Synthetic inst
006 |  |  | MOV | dx,tmp | ;Synthetic inst
007 | 0009C6 | 85C0 | TEST | ax,ax |
008 | 0009C8 | 7402 | JE | L23 |
009 | 0009CA | F7E3 | MUL | bx |
010 |  |  | L23: MOV | tmp,ax | ;Synthetic inst
011 |  |  | MOV | ax,cx | ;Synthetic inst
012 |  |  | MOV | cx,tmp | ;Synthetic inst
013 | 0009CD | 85C0 | TEST | ax,ax |
014 | 0009CF | 7404 | JE | L24 |
015 | 0009D1 | F7E6 | MUL | si |
016 | 0009D3 | 03C8 | ADD | cx,ax |
017 |  |  | L24: MOV | tmp,ax | ;Synthetic inst
018 |  |  | MOV | ax,si | ;Synthetic inst
019 |  |  | MOV | si,tmp | ;Synthetic inst
020 | 0009D6 | F7E3 | MUL | bx |
021 | 0009D8 | 03D1 | ADD | dx,cx |
022 | 0009DA | 5E | POP | si |
023 | 0009DB | CB | RETF |  |
 | LXMUL@ | ENDP |  |  |
 | main | PROC NEAR |  |  |
000 | 0002FA | 55 | PUSH | bp |
001 | 0002FB | 8BEC | MOV | bp,sp |
002 | 0002FD | 83EC08 | SUB | sp,8 |
003 | 000300 | C746FA0000 | MOV | word ptr [bp-6],0 |
004 | 000305 | C746F8FF00 | MOV | word ptr [bp-8],0FFh |
005 | 00030A | C746FE0000 | MOV | word ptr [bp-2],0 |
006 | 00030F | C746FC8F00 | MOV | word ptr [bp-4],8Fh |
007 | 000314 | 8B56FA | MOV | dx,[bp-6] |
008 | 000317 | 8B46F8 | MOV | ax,[bp-8] |
009 | 00031A | 0346FC | ADD | ax,[bp-4] |
010 | 00031D | 1356FE | ADC | dx,[bp-2] |
011 | 000320 | 8956FE | MOV | [bp-2],dx |
012 | 000323 | 8946FC | MOV | [bp-4],ax |
013 | 000326 | 8B56FA | MOV | dx,[bp-6] |
014 | 000329 | 8B46F8 | MOV | ax,[bp-8] |
015 | 00032C | 2B46FC | SUB | ax,[bp-4] |
016 | 00032F | 1B56FE | SBB | dx,[bp-2] |
017 | 000332 | 8956FA | MOV | [bp-6],dx |
018 | 000335 | 8946F8 | MOV | [bp-8],ax |
019 | 000338 | 8B56FA | MOV | dx,[bp-6] |
020 | 00033B | 8B46F8 | MOV | ax,[bp-8] |
021 | 00033E | 8B4EFE | MOV | cx,[bp-2] |
022 | 000341 | 8B5EFC | MOV | bx,[bp-4] |
023 | 000344 | 9AC3081000 | CALL | far ptr LXMUL@ |
024 | 000349 | 8956FA | MOV | [bp-6],dx |
025 | 00034C | 8946F8 | MOV | [bp-8],ax |
026 | 00034F | FF76FA | PUSH | word ptr [bp-6] |
027 | 000352 | FF76F8 | PUSH | word ptr [bp-8] |
028 | 000355 | FF76FE | PUSH | word ptr [bp-2] |
029 | 000358 | FF76FC | PUSH | word ptr [bp-4] |
030 | 00035B | 9AC6101000 | CALL | far ptr LDIV@ |
031 | 000360 | 8956FE | MOV | [bp-2],dx |
032 | 000363 | 8946FC | MOV | [bp-4],ax |
033 | 000366 | FF76FA | PUSH | word ptr [bp-6] |
034 | 000369 | FF76F8 | PUSH | word ptr [bp-8] |
035 | 00036C | FF76FE | PUSH | word ptr [bp-2] |
036 | 00036F | FF76FC | PUSH | word ptr [bp-4] |
037 | 000372 | 9ACF101000 | CALL | far ptr LMOD@ |
038 | 000377 | 8956FE | MOV | [bp-2],dx |
039 | 00037A | 8946FC | MOV | [bp-4],ax |
040 | 00037D | 8B56FA | MOV | dx,[bp-6] |
041 | 000380 | 8B46F8 | MOV | ax,[bp-8] |
042 | 000383 | B105 | MOV | cl,5 |
043 | 000385 | 9A87111000 | CALL | far ptr LXLSH@ |
044 | 00038A | 8956FA | MOV | [bp-6],dx |
045 | 00038D | 8946F8 | MOV | [bp-8],ax |
046 | 000390 | 8B56FE | MOV | dx,[bp-2] |
047 | 000393 | 8B46FC | MOV | ax,[bp-4] |
048 | 000396 | 8A4EF8 | MOV | cl,[bp-8] |
049 | 000399 | 9A69111000 | CALL | far ptr LXRSH@ |
050 | 00039E | 8956FE | MOV | [bp-2],dx |
051 | 0003A1 | 8946FC | MOV | [bp-4],ax |
052 | 0003A4 | FF76FE | PUSH | word ptr [bp-2] |
053 | 0003A7 | FF76FC | PUSH | word ptr [bp-4] |
054 | 0003AA | FF76FA | PUSH | word ptr [bp-6] |
055 | 0003AD | FF76F8 | PUSH | word ptr [bp-8] |
056 | 0003B0 | B89401 | MOV | ax,194h |
057 | 0003B3 | 50 | PUSH | ax |
058 | 0003B4 | E8C406 | CALL | near ptr printf |
059 | 0003B7 | 83C40A | ADD | sp,0Ah |
060 | 0003BA | 8BE5 | MOV | sp,bp |
061 | 0003BC | 5D | POP | bp |
062 | 0003BD | C3 | RET |  |
 | main | ENDP |  |  |

图 9-22: Longops.a2

/*

* Input file : longops.exe

* File type : EXE

*/

#include "dcc.h"

long LXMUL@ (long arg0, long arg1)

/* Uses register arguments:

* arg0 = dx:ax.

* arg1 = cx:bx.

* Runtime support routine of the compiler.

*/

{

int loc1;

int loc2; /* tmp */

loc2 = LO(arg0);

LO(arg0) = loc1;

loc1 = loc2;

loc2 = LO(arg0);

LO(arg0) = HI(arg0);

if ((LO(arg0) & LO(arg0)) != 0) {

LO(arg0) = (LO(arg0) * LO(arg1));

}

loc2 = LO(arg0);

LO(arg0) = HI(arg1);

HI(arg1) = loc2;

if ((LO(arg0) & LO(arg0)) != 0) {

LO(arg0) = (LO(arg0) * loc1);

HI(arg1) = (HI(arg1) + LO(arg0));

}

loc2 = LO(arg0);

LO(arg0) = loc1;

loc1 = loc2;

arg0 = (LO(arg0) * LO(arg1));

HI(arg0) = (HI(arg0) + HI(arg1));

return (arg0);

}

long LDIV@ (long arg0, long arg2)

/* Takes 8 bytes of parameters.

* Runtime support routine of the compiler.

* Untranslatable routine. Assembler provided.

* Return value in registers dx:ax.

* Pascal calling convention.

*/

{

/* disassembly code here */

}

long LMOD@ (long arg0, long arg2)

/* Takes 8 bytes of parameters.

* Runtime support routine of the compiler.

* Untranslatable routine. Assembler provided.

* Return value in registers dx:ax.

* Pascal calling convention.

*/

{

/* disassembly code here */

}

long LXLSH@ (long arg0, char arg1)

/* Uses register arguments:

* arg0 = dx:ax.

* arg1 = cl.

* Runtime support routine of the compiler.

*/

{

int loc1; /* bx */

if (arg1 < 16) {

loc1 = LO(arg0);

LO(arg0) = (LO(arg0) << arg1);

HI(arg0) = (HI(arg0) << arg1);

HI(arg0) = (HI(arg0) | (loc1 >> (!arg1 + 16)));

return (arg0);

}

else {

HI(arg0) = LO(arg0);

LO(arg0) = 0;

HI(arg0) = (HI(arg0) << (arg1 - 16));

return (arg0);

}

}

long LXRSH@ (long arg0, char arg1)

/* Uses register arguments:

* arg0 = dx:ax.

* arg1 = cl.

* Runtime support routine of the compiler.

*/

{

int loc1; /* bx */

if (arg1 < 16) {

loc1 = HI(arg0);

LO(arg0) = (LO(arg0) >> arg1);

HI(arg0) = (HI(arg0) >> arg1);

LO(arg0) = (LO(arg0) | (loc1 << (!arg1 + 16)));

return (arg0);

}

else {

arg0 = HI(arg0);

LO(arg0) = (LO(arg0) >> (arg1 - 16));

return (arg0);

}

}

void main ()

/* Takes no parameters.

* High-level language prologue code.

*/

{

long loc1;

long loc2;

loc2 = 255;

loc1 = 143;

loc1 = (loc2 + loc1);

loc2 = (loc2 - loc1);

loc2 = LXMUL@ (loc2, loc1);

loc1 = LDIV@ (loc1, loc2);

loc1 = LMOD@ (loc1, loc2);

loc2 = LXLSH@ (loc2, 5);

loc1 = LXRSH@ (loc1, loc1);

printf ("a = %ld, b = %ld\n", loc2, loc1);

}

图 9-23: Longops.b

#define TYPE long

main()

{ TYPE a, b;

a = 255;

b = 143;

b = a + b;

a = a - b;

a = a * b;

b = b / a;

b = b % a;

a = a << 5;

b = b >> a;

printf ("a = %ld, b = %ld\n", a, b);

}

图 9-24: Longops.c

子程序 | 低级 | 高级 | % 缩减率
LXMUL@ | 24 | 19 | 20.83
LDIV@ | 72 | - | -
LMOD@ | 72 | - | -
LXLSH@ | 15 | 10 | 33.33
LXRSH@ | 15 | 94 | 0.00
main | 63 | 10 | 84.13
合计 | 117 | 48 | 58.97

图 9-25: Longops统计


第 9 章 dcc - Benchsho.exe

9.7.4 Benchsho.exe

Benchsho是来自Plum-Hall基准程序套装的一个程序，测试短整型数。该程序使用两个长整型变量反复经过循环，以及三个(短)整型变量执行1000个运算。反汇编的程序在图9-27被显示，反编译的C程序在图9-28，最初的C程序在图9-29。该程序有下列调用图：

main

scanf

printf

从该程序的反汇编可知，长整型变量位于栈上偏移-4和-8处，而整型变量位于偏移-14、-12、和-10处。最后的C代码使用整型变量loc6保存一个布尔表达式的结果 (即，0或1) 而且把它赋给对应的变量。这个布尔变量是一个寄存器变量 (寄存器ax) 而且可能被更进一步的控制流向图分析从代码中清除掉，类似于组合条件的结构化方法。

图 9-26: 布尔赋值的控制流向图

例如，如果满足下列条件，图9-26中的图(a)能够被缩减为图(b)：

1. 结点1是一个二路结点。

2. 结点2和结点3只有一个来自结点1的入边，而且通向一个共同的结点4。

3. 结点2和结点3只有一个指令。这个指令分别地对一个寄存器赋值0和1。

4. 结点4把结点2和结点3的寄存器赋给一个局部变量。在该程序中，该寄存器没有在重新定义之前再被使用。

因为该寄存器只被使用一次储存一个布尔表达式评估的中间结果，所以它被从把布尔表达式赋给目标变量的最后代码中清除掉。这个转换不仅去掉了有关的寄存器，而且也去掉了赋值给它的那两个结点 (即，在图9-26的图中结点2和结点3)。

显然，图9-28的两个布尔赋值能够被转换成下列代码：

loc1 = (loc2 == loc3);

/* other code */

loc1 = (loc2 > loc3);

这会使最后的C程序成为最初的C程序的一个准确反编译。没有这个转换，所产生的C代码跟最初的C代码在功能上是等价的，而且在结构上跟反编译的图是等价的。因为一个布尔赋值的图天生是结构化的，如果不实现这个转换无论如何也不会生成非结构化的代码，不像组合条件的情况——它是天生非结构化的图被转换成结构化的图。

没有图的最优化，由dcc生成的最后的反编译代码产生75.25%的中间指令数目缩减率，如图9-30所示。对于最初C代码的每个布尔赋值，由于一个临时局部变量 (在这个例子中是loc6) 的使用而造成有三个额外的指令。

 | main | PROC NEAR |  |  |
000 | 0002FA | 55 | PUSH | bp |
001 | 0002FB | 8BEC | MOV | bp,sp |
002 | 0002FD | 83EC0E | SUB | sp,0Eh |
003 | 000300 | 8D46FC | LEA | ax,[bp-4] |
004 | 000303 | 50 | PUSH | ax |
005 | 000304 | B89401 | MOV | ax,194h |
006 | 000307 | 50 | PUSH | ax |
007 | 000308 | E8E914 | CALL | near ptr scanf |
008 | 00030B | 59 | POP | cx |
009 | 00030C | 59 | POP | cx |
010 | 00030D | FF76FE | PUSH | word ptr [bp-2] |
011 | 000310 | FF76FC | PUSH | word ptr [bp-4] |
012 | 000313 | B89801 | MOV | ax,198h |
013 | 000316 | 50 | PUSH | ax |
014 | 000317 | E8510C | CALL | near ptr printf |
015 | 00031A | 83C406 | ADD | sp,6 |
016 | 00031D | 8D46F2 | LEA | ax,[bp-0Eh] |
017 | 000320 | 50 | PUSH | ax |
018 | 000321 | B8B201 | MOV | ax,1B2h |
019 | 000324 | 50 | PUSH | ax |
020 | 000325 | E8CC14 | CALL | near ptr scanf |
021 | 000328 | 59 | POP | cx |
022 | 000329 | 59 | POP | cx |
023 | 00032A | 8D46F4 | LEA | ax,[bp-0Ch] |
024 | 00032D | 50 | PUSH | ax |
025 | 00032E | B8B601 | MOV | ax,1B6h |
026 | 000331 | 50 | PUSH | ax |
027 | 000332 | E8BF14 | CALL | near ptr scanf |
028 | 000335 | 59 | POP | cx |
029 | 000336 | 59 | POP | cx |
030 | 000337 | C746FA0000 | MOV | word ptr [bp-6],0 |
031 | 00033C | C746F80100 | MOV | word ptr [bp-8],1 |
033 | 0003BD | 8B56FA | L1: MOV | dx,[bp-6] |
034 | 0003C0 | 8B46F8 | MOV | ax,[bp-8] |
035 | 0003C3 | 3B56FE | CMP | dx,[bp-2] |
036 | 0003C6 | 7D03 | JGE | L2 |
038 | 000344 | C746F60100 | L3: MOV | word ptr [bp-0Ah],1 |
040 | 0003AF | 837EF628 | L4: CMP | word ptr [bp-0Ah],28h |
041 | 0003B3 | 7E96 | JLE | L5 |
042 | 0003B5 | 8346F801 | ADD | word ptr [bp-8],1 |
043 | 0003B9 | 8356FA00 | ADC | word ptr [bp-6],0 |
044 |  |  | JMP | L1 | ;Synthetic inst
045 | 00034B | 8B46F2 | L5: MOV | ax,[bp-0Eh] |
046 | 00034E | 0346F4 | ADD | ax,[bp-0Ch] |
047 | 000351 | 0346F6 | ADD | ax,[bp-0Ah] |
048 | 000354 | 8946F2 | MOV | [bp-0Eh],ax |
049 | 000357 | 8B46F2 | MOV | ax,[bp-0Eh] |
050 | 00035A | D1F8 | SAR | ax,1 |
051 | 00035C | 8946F4 | MOV | [bp-0Ch],ax |
052 | 00035F | 8B46F4 | MOV | ax,[bp-0Ch] |
053 | 000362 | BB0A00 | MOV | bx,0Ah |
054 | 000365 | 99 | CWD |  |
055 |  |  | MOV | tmp,dx:ax | ;Synthetic inst
056 | 000366 | F7FB | IDIV | bx |
057 |  |  | MOD | bx | ;Synthetic inst
058 | 000368 | 8956F2 | MOV | [bp-0Eh],dx |
059 | 00036B | 8B46F4 | MOV | ax,[bp-0Ch] |
060 | 00036E | 3B46F6 | CMP | ax,[bp-0Ah] |
061 | 000371 | 7505 | JNE | L6 |
062 | 000373 | B80100 | MOV | ax,1 |
064 | 00037A | 8946F2 | L7: MOV | [bp-0Eh],ax |
065 | 00037D | 8B46F2 | MOV | ax,[bp-0Eh] |
066 | 000380 | 0B46F6 | OR | ax,[bp-0Ah] |
067 | 000383 | 8946F4 | MOV | [bp-0Ch],ax |
068 | 000386 | 8B46F4 | MOV | ax,[bp-0Ch] |
069 | 000389 | F7D8 | NEG | ax |
070 | 00038B | 1BC0 | SBB | ax,ax |
071 | 00038D | 40 | INC | ax |
072 | 00038E | 8946F2 | MOV | [bp-0Eh],ax |
073 | 000391 | 8B46F2 | MOV | ax,[bp-0Eh] |
074 | 000394 | 0346F6 | ADD | ax,[bp-0Ah] |
075 | 000397 | 8946F4 | MOV | [bp-0Ch],ax |
076 | 00039A | 8B46F4 | MOV | ax,[bp-0Ch] |
077 | 00039D | 3B46F6 | CMP | ax,[bp-0Ah] |
078 | 0003A0 | 7E05 | JLE | L8 |
079 | 0003A2 | B80100 | MOV | ax,1 |
081 | 0003A9 | 8946F2 | L9: MOV | [bp-0Eh],ax |
082 | 0003AC | FF46F6 | INC | word ptr [bp-0Ah] |
083 |  |  | JMP | L4 | ;Synthetic inst
084 | 0003A7 | 33C0 | L8: XOR | ax,ax |
085 |  |  | JMP | L9 | ;Synthetic inst
086 | 000378 | 33C0 | L6: XOR | ax,ax |
087 |  |  | JMP | L7 | ;Synthetic inst
088 | 0003CB | 7F08 | L2: JG | L10 |
089 | 0003CD | 3B46FC | CMP | ax,[bp-4] |
090 | 0003D0 | 7703 | JA | L10 |
092 | 0003D5 | FF76F2 | L10: PUSH | word ptr [bp-0Eh] |
093 | 0003D8 | B8BA01 | MOV | ax,1BAh |
094 | 0003DB | 50 | PUSH | ax |
095 | 0003DC | E88C0B | CALL | near ptr printf |
096 | 0003DF | 59 | POP | cx |
097 | 0003E0 | 59 | POP | cx |
098 | 0003E1 | 8BE5 | MOV | sp,bp |
099 | 0003E3 | 5D | POP | bp |
100 | 0003E4 | C3 | RET |  |
 | main | ENDP |  |  |

图 9-27: Benchsho.a2

/*

* Input file : benchsho.exe

* File type : EXE

*/

#include "dcc.h"

void main ()

/* Takes no parameters.

* High-level language prologue code.

*/

{ int loc1; int loc2; int loc3;

long loc4; long loc5; int loc6; /* ax */

scanf ("%ld", &loc5);

printf ("executing %ld iterations\n", loc5);

scanf ("%ld", &loc1);

scanf ("%ld", &loc2);

loc4 = 1;

while ((loc4 <= loc5)) {

loc3 = 1;

while ((loc3 <= 40)) {

loc1 = ((loc1 + loc2) + loc3);

loc2 = (loc1 >> 1);

loc1 = (loc2 % 10);

if (loc2 == loc3) {

loc6 = 1;

}

else {

loc6 = 0;

}

loc1 = loc6;

loc2 = (loc1 | loc3);

loc1 = !loc2;

loc2 = (loc1 + loc3);

if (loc2 > loc3) {

loc6 = 1;

}

else {

loc6 = 0;

}

loc1 = loc6;

loc3 = (loc3 + 1);

}

loc4 = (loc4 + 1);

}

printf ("a=%d\n", loc1);

}

图 9-28: Benchsho.b

/* benchsho - benchmark for short integers

* Thomas Plum, Plum Hall Inc, 609-927-3770

* If machine traps overflow, use an unsigned type

* Let T be the execution time in milliseconds

* Then average time per operator = T/major usec

* (Because the inner loop has exactly 1000 operations)

*/

#define STOR_CL auto

#define TYPE short

#include <stdio.h>

main (int ac, char *av[])

{ STOR_CL TYPE a, b, c;

long d, major;

scanf ("%ld", &major);

printf("executing %ld iterations\n", major);

scanf ("%ld", &a);

scanf ("%ld", &b);

for (d = 1; d <= major; ++d)

{

/* inner loop executes 1000 selected operations */

for (c = 1; c <= 40; ++c)

{

a = a + b + c;

b = a >> 1;

a = b % 10;

a = b == c;

b = a | c;

a = !b;

b = a + c;

a = b > c;

}

}

printf("a=%d\n", a);

}

图 9-29: Benchsho.c

子程序 | 低级 | 高级 | % 缩减率
main | 101 | 25 | 75.25
合计 | 101 | 25 | 75.25

图 9-30: Benchsho统计


第 9 章 dcc - Benchlng.exe

9.7.5 Benchlng.exe

Benchlng是来自Plum-Hall基准程序套装的一个程序，测试长整型变量。该程序跟benchsho.exe程序完全相同，除了它使用长整型变量而非短整型数。反汇编的程序在图9-31被显示，反编译的C程序在图9-32，最初C程序在图9-33。该程序有下列调用图：

main

scanf

printf

LMOD@

从该程序的反汇编可知，长整型变量是位于栈上偏移-4、-20、-16、-8、和-12处。最后的C反编译代码使用五个长整型变量和一个整型变量loc6。后者这个变量被当做一个布尔变量使用，保存布尔表达式评估的内容。在最后的C代码中三个布尔表达式评估如下：

loc1 == loc2

LO(loc1) | HI(loc1)

loc1 > loc2

这些表达式全都能够使用前面章节描述的转换来转换成布尔赋值。所生成代码应该如下：

loc4 = (loc1 == loc2);

/* other code here */

loc4 = (LO(loc1) | HI(loc1));

/* other code here */

loc4 = (loc1 > loc2);

第二个布尔表达式检查一个长整型变量的低字部分和高字部分以及它们的‘或’；这等价于该长整型变量的一个逻辑非，得到下列最后的代码：

loc4 = !loc1;

benchlng程序跟benchsho程序相比，在main程序中多27.34%低级指令 (LMOD@子程序计算长整型变量的模而且它是无法翻译成高级语言的)，在main的高级表示法中多三个指令 (归因于长整型变量的逻辑非，它使用临时的布尔变量loc6)，而且79.86%的指令缩减率如图9-34所示。

 | LMOD@ | PROC FAR |  |  |
000 | 001EEB | B90200 | MOV | cx,2 |
002 | 001EF3 | 55 | PUSH | bp |
003 | 001EF4 | 56 | PUSH | si |
004 | 001EF5 | 57 | PUSH | di |
005 | 001EF6 | 8BEC | MOV | bp,sp |
006 | 001EF8 | 8BF9 | MOV | di,cx |
007 | 001EFA | 8B460A | MOV | ax,[bp+0Ah] |
008 | 001EFD | 8B560C | MOV | dx,[bp+0Ch] |
009 | 001F00 | 8B5E0E | MOV | bx,[bp+0Eh] |
010 | 001F03 | 8B4E10 | MOV | cx,[bp+10h] |
011 | 001F06 | 0BC9 | OR | cx,cx |
012 | 001F08 | 7508 | JNE | L1 |
013 | 001F0A | 0BD2 | OR | dx,dx |
014 | 001F0C | 7469 | JE | L2 |
015 | 001F0E | 0BDB | OR | bx,bx |
016 | 001F10 | 7465 | JE | L2 |
017 | 001F12 | F7C70100 | L1: TEST | di,1 |
018 | 001F16 | 751C | JNE | L3 |
019 | 001F18 | 0BD2 | OR | dx,dx |
020 | 001F1A | 790A | JNS | L4 |
021 | 001F1C | F7DA | NEG | dx |
022 | 001F1E | F7D8 | NEG | ax |
023 | 001F20 | 83DA00 | SBB | dx,0 |
024 | 001F23 | 83CF0C | OR | di,0Ch |
025 | 001F26 | 0BC9 | L4: OR | cx,cx |
026 | 001F28 | 790A | JNS | L3 |
027 | 001F2A | F7D9 | NEG | cx |
028 | 001F2C | F7DB | NEG | bx |
029 | 001F2E | 83D900 | SBB | cx,0 |
030 | 001F31 | 83F704 | XOR | di,4 |
031 | 001F34 | 8BE9 | L3: MOV | bp,cx |
032 | 001F36 | B92000 | MOV | cx,20h |
033 | 001F39 | 57 | PUSH | di |
034 | 001F3A | 33FF | XOR | di,di |
035 | 001F3C | 33F6 | XOR | si,si |
036 | 001F3E | D1E0 | L5: SHL | ax,1 |
037 | 001F40 | D1D2 | RCL | dx,1 |
038 | 001F42 | D1D6 | RCL | si,1 |
039 | 001F44 | D1D7 | RCL | di,1 |
040 | 001F46 | 3BFD | CMP | di,bp |
041 | 001F48 | 720B | JB | L6 |
042 | 001F4A | 7704 | JA | L7 |
043 | 001F4C | 3BF3 | CMP | si,bx |
044 | 001F4E | 7205 | JB | L6 |
045 | 001F50 | 2BF3 | L7: SUB | si,bx |
046 | 001F52 | 1BFD | SBB | di,bp |
047 | 001F54 | 40 | INC | ax |
048 | 001F55 | E2E7 | L6: LOOP | L5 |
049 | 001F57 | 5B | POP | bx |
050 | 001F58 | F7C30200 | TEST | bx,2 |
051 | 001F5C | 7406 | JE | L8 |
052 | 001F5E | 8BC6 | MOV | ax,si |
053 | 001F60 | 8BD7 | MOV | dx,di |
054 | 001F62 | D1EB | SHR | bx,1 |
055 | 001F64 | F7C30400 | L8: TEST | bx,4 |
056 | 001F68 | 7407 | JE | L9 |
057 | 001F6A | F7DA | NEG | dx |
058 | 001F6C | F7D8 | NEG | ax |
059 | 001F6E | 83DA00 | SBB | dx,0 |
060 | 001F71 | 5F | L9: POP | di |
061 | 001F72 | 5E | POP | si |
062 | 001F73 | 5D | POP | bp |
063 | 001F74 | CA0800 | RETF | 8 |
064 |  |  | L2: MOV | tmp,dx:ax | ;Synthetic inst
065 | 001F77 | F7F3 | DIV | bx |
066 |  |  | MOD | bx | ;Synthetic inst
067 | 001F79 | F7C70200 | TEST | di,2 |
068 | 001F7D | 7402 | JE | L10 |
069 | 001F7F | 8BC2 | MOV | ax,dx |
070 | 001F81 | 33D2 | L10: XOR | dx,dx |
071 | 001F83 | EBEC | JMP | L9 |
 | LMOD@ | ENDP |  |  |
 | main | PROC NEAR |  |  |
000 | 0002FA | 55 | PUSH | bp |
001 | 0002FB | 8BEC | MOV | bp,sp |
002 | 0002FD | 83EC14 | SUB | sp,14h |
003 | 000300 | 8D46FC | LEA | ax,[bp-4] |
004 | 000303 | 50 | PUSH | ax |
005 | 000304 | B89401 | MOV | ax,194h |
006 | 000307 | 50 | PUSH | ax |
007 | 000308 | E85D15 | CALL | near ptr scanf |
008 | 00030B | 59 | POP | cx |
009 | 00030C | 59 | POP | cx |
010 | 00030D | FF76FE | PUSH | word ptr [bp-2] |
011 | 000310 | FF76FC | PUSH | word ptr [bp-4] |
012 | 000313 | B89801 | MOV | ax,198h |
013 | 000316 | 50 | PUSH | ax |
014 | 000317 | E8C50C | CALL | near ptr printf |
015 | 00031A | 83C406 | ADD | sp,6 |
016 | 00031D | 8D46EC | LEA | ax,[bp-14h] |
017 | 000320 | 50 | PUSH | ax |
018 | 000321 | B8B201 | MOV | ax,1B2h |
019 | 000324 | 50 | PUSH | ax |
020 | 000325 | E84015 | CALL | near ptr scanf |
021 | 000328 | 59 | POP | cx |
022 | 000329 | 59 | POP | cx |
023 | 00032A | 8D46F0 | LEA | ax,[bp-10h] |
024 | 00032D | 50 | PUSH | ax |
025 | 00032E | B8B601 | MOV | ax,1B6h |
026 | 000331 | 50 | PUSH | ax |
027 | 000332 | E83315 | CALL | near ptr scanf |
028 | 000335 | 59 | POP | cx |
029 | 000336 | 59 | POP | cx |
030 | 000337 | C746FA0000 | MOV | word ptr [bp-6],0 |
031 | 00033C | C746F80100 | MOV | word ptr [bp-8],1 |
033 | 00042D | 8B56FA | L11: MOV | dx,[bp-6] |
034 | 000430 | 8B46F8 | MOV | ax,[bp-8] |
035 | 000433 | 3B56FE | CMP | dx,[bp-2] |
036 | 000436 | 7D03 | JGE | L12 |
038 | 000344 | C746F60000 | L13: MOV | word ptr [bp-0Ah],0 |
039 | 000349 | C746F40100 | MOV | word ptr [bp-0Ch],1 |
041 | 000411 | 837EF600 | L14: CMP | word ptr [bp-0Ah],0 |
042 | 000415 | 7D03 | JGE | L15 |
044 | 000351 | 8B56EE | L16: MOV | dx,[bp-12h] |
045 | 000354 | 8B46EC | MOV | ax,[bp-14h] |
046 | 000357 | 0346F0 | ADD | ax,[bp-10h] |
047 | 00035A | 1356F2 | ADC | dx,[bp-0Eh] |
048 | 00035D | 0346F4 | ADD | ax,[bp-0Ch] |
049 | 000360 | 1356F6 | ADC | dx,[bp-0Ah] |
050 | 000363 | 8956EE | MOV | [bp-12h],dx |
051 | 000366 | 8946EC | MOV | [bp-14h],ax |
052 | 000369 | 8B56EE | MOV | dx,[bp-12h] |
053 | 00036C | 8B46EC | MOV | ax,[bp-14h] |
054 | 00036F | D1FA | SAR | dx,1 |
055 | 000371 | D1D8 | RCR | ax,1 |
056 | 000373 | 8956F2 | MOV | [bp-0Eh],dx |
057 | 000376 | 8946F0 | MOV | [bp-10h],ax |
058 | 000379 | 33D2 | XOR | dx,dx |
059 | 00037B | B80A00 | MOV | ax,0Ah |
060 | 00037E | 52 | PUSH | dx |
061 | 00037F | 50 | PUSH | ax |
062 | 000380 | FF76F2 | PUSH | word ptr [bp-0Eh] |
063 | 000383 | FF76F0 | PUSH | word ptr [bp-10h] |
064 | 000386 | 9AEB1D1000 | CALL | far ptr LMOD@ |
065 | 00038B | 8956EE | MOV | [bp-12h],dx |
066 | 00038E | 8946EC | MOV | [bp-14h],ax |
067 | 000391 | 8B56F2 | MOV | dx,[bp-0Eh] |
068 | 000394 | 8B46F0 | MOV | ax,[bp-10h] |
069 | 000397 | 3B56F6 | CMP | dx,[bp-0Ah] |
070 | 00039A | 750A | JNE | L17 |
071 | 00039C | 3B46F4 | CMP | ax,[bp-0Ch] |
072 | 00039F | 7505 | JNE | L17 |
073 | 0003A1 | B80100 | MOV | ax,1 |
075 | 0003A8 | 99 | L18: CWD |  |
076 | 0003A9 | 8956EE | MOV | [bp-12h],dx |
077 | 0003AC | 8946EC | MOV | [bp-14h],ax |
078 | 0003AF | 8B56EE | MOV | dx,[bp-12h] |
079 | 0003B2 | 8B46EC | MOV | ax,[bp-14h] |
080 | 0003B5 | 0B46F4 | OR | ax,[bp-0Ch] |
081 | 0003B8 | 0B56F6 | OR | dx,[bp-0Ah] |
082 | 0003BB | 8956F2 | MOV | [bp-0Eh],dx |
083 | 0003BE | 8946F0 | MOV | [bp-10h],ax |
084 | 0003C1 | 8B46F0 | MOV | ax,[bp-10h] |
085 | 0003C4 | 0B46F2 | OR | ax,[bp-0Eh] |
086 | 0003C7 | 7505 | JNE | L19 |
087 | 0003C9 | B80100 | MOV | ax,1 |
089 | 0003D0 | 99 | L20: CWD |  |
090 | 0003D1 | 8956EE | MOV | [bp-12h],dx |
091 | 0003D4 | 8946EC | MOV | [bp-14h],ax |
092 | 0003D7 | 8B56EE | MOV | dx,[bp-12h] |
093 | 0003DA | 8B46EC | MOV | ax,[bp-14h] |
094 | 0003DD | 0346F4 | ADD | ax,[bp-0Ch] |
095 | 0003E0 | 1356F6 | ADC | dx,[bp-0Ah] |
096 | 0003E3 | 8956F2 | MOV | [bp-0Eh],dx |
097 | 0003E6 | 8946F0 | MOV | [bp-10h],ax |
098 | 0003E9 | 8B56F2 | MOV | dx,[bp-0Eh] |
099 | 0003EC | 8B46F0 | MOV | ax,[bp-10h] |
100 | 0003EF | 3B56F6 | CMP | dx,[bp-0Ah] |
101 | 0003F2 | 7C0C | JL | L21 |
102 | 0003F4 | 7F05 | JG | L22 |
103 | 0003F6 | 3B46F4 | CMP | ax,[bp-0Ch] |
104 | 0003F9 | 7605 | JBE | L21 |
105 | 0003FB | B80100 | L22: MOV | ax,1 |
107 | 000402 | 99 | L23: CWD |  |
108 | 000403 | 8956EE | MOV | [bp-12h],dx |
109 | 000406 | 8946EC | MOV | [bp-14h],ax |
110 | 000409 | 8346F401 | ADD | word ptr [bp-0Ch],1 |
111 | 00040D | 8356F600 | ADC | word ptr [bp-0Ah],0 |
112 |  |  | JMP | L14 | ;Synthetic inst
113 | 000400 | 33C0 | L21: XOR | ax,ax |
114 |  |  | JMP | L23 | ;Synthetic inst
115 | 0003CE | 33C0 | L19: XOR | ax,ax |
116 |  |  | JMP | L20 | ;Synthetic inst
117 | 0003A6 | 33C0 | L17: XOR | ax,ax |
118 |  |  | JMP | L18 | ;Synthetic inst
119 | 00041A | 7F09 | L15: JG | L24 |
120 | 00041C | 837EF428 | CMP | word ptr [bp-0Ch],28h |
121 | 000420 | 7703 | JA | L24 |
123 | 000425 | 8346F801 | L24: ADD | word ptr [bp-8],1 |
124 | 000429 | 8356FA00 | ADC | word ptr [bp-6],0 |
125 |  |  | JMP | L11 | ;Synthetic inst
126 | 00043B | 7F08 | L12: JG | L25 |
127 | 00043D | 3B46FC | CMP | ax,[bp-4] |
128 | 000440 | 7703 | JA | L25 |
130 | 000445 | FF76EE | L25: PUSH | word ptr [bp-12h] |
131 | 000448 | FF76EC | PUSH | word ptr [bp-14h] |
132 | 00044B | B8BA01 | MOV | ax,1BAh |
133 | 00044E | 50 | PUSH | ax |
134 | 00044F | E88D0B | CALL | near ptr printf |
135 | 000452 | 83C406 | ADD | sp,6 |
136 | 000455 | 8BE5 | MOV | sp,bp |
137 | 000457 | 5D | POP | bp |
138 | 000458 | C3 | RET |  |
 | main | ENDP |  |  |

图 9-31: Benchlng.a2

/*

* Input file : benchlng.exe

* File type : EXE

*/

#include "dcc.h"

long LMOD@ (long arg0, long arg2)

/* Takes 8 bytes of parameters.

* Runtime support routine of the compiler.

* Untranslatable routine. Assembler provided.

* Return value in registers dx:ax.

* Pascal calling convention.

*/

{

/* disassembly code here */

}

void main ()

/* Takes no parameters.

* High-level language prologue code.

*/

{

long loc1;

long loc2;

long loc3;

long loc4;

long loc5;

int loc6; /* ax */

scanf ("%ld", &loc5);

printf ("executing %ld iterations\n", loc5);

scanf ("%ld", &loc2);

scanf ("%ld", &loc4);

loc3 = 1;

while ((loc3 <= loc5)) {

loc2 = 1;

while ((loc2 <= 40)) {

loc4 = ((loc4 + loc1) + loc2);

loc1 = (loc4 >> 1);

loc4 = LMOD@ (loc1, 10);

if (loc1 == loc2) {

loc6 = 1;

}

else {

loc6 = 0;

}

loc4 = loc6;

loc1 = (loc4 | loc2);

if ((LO(loc1) | HI(loc1)) == 0) {

loc6 = 1;

}

else {

loc6 = 0;

}

loc4 = loc6;

loc1 = (loc4 + loc2);

if (loc1 > loc2) {

loc6 = 1;

}

else {

loc6 = 0;

}

loc4 = loc6;

loc2 = (loc2 + 1);

}

loc3 = (loc3 + 1);

}

printf ("a=%d\n", loc4);

}

图 9-32: Benchlng.b

/* benchlng - benchmark for long integers

* Thomas Plum, Plum Hall Inc, 609-927-3770

* If machine traps overflow, use an unsigned type

* Let T be the execution time in milliseconds

* Then average time per operator = T/major usec

* (Because the inner loop has exactly 1000 operations)

*/

#define TYPE long

#include <stdio.h>

main (int ac, char *av[])

{ TYPE a, b, c;

long d, major;

scanf ("%ld", &major);

printf("executing %ld iterations\n", major);

scanf ("%ld", &a);

scanf ("%ld", &b);

for (d = 1; d <= major; ++d)

{

/* inner loop executes 1000 selected operations */

for (c = 1; c <= 40; ++c)

{

a = a + b + c;

b = a >> 1;

a = b % 10;

a = b == c;

b = a | c;

a = !b;

b = a + c;

a = b > c;

}

}

printf("a=%d\n", a);

}

图 9-33: Benchlng.c

子程序 | 低级 | 高级 | % 缩减率
LMOD@ | 72 | - | -
main | 139 | 28 | 79.86
合计 | 139 | 28 | 79.86

图 9-34: Benchlng Statistics


第 9 章 dcc - Benchmul.exe

9.7.6 Benchmul.exe

Benchmul是另一个来自Plum-Hall基准程序套装的程序。这个程序测试在一个循环中执行1000次相乘的整数乘法。反汇编的程序在图9-35被显示，反编译的C程序在图9-36，最初的C程序图9-37。这个程序有下列调用图：

main

scanf

printf

Benchmul使用两个长整型变量经过程序许多次循环，和三个执行该运算的整型变量；这些变量其中一个实际上在该程序中没有被使用。从反汇编的代码可知，长整型变量位于栈上偏移-4和-8处，而整型变量在偏移-12、-10和寄存器变量si上。最后的C代码和最初的C代码是相同的，而且这个程序达到86.36%的指令缩减率，如图9-38所示。

 | main | PROC NEAR |  |  |
000 | 0002FA | 55 | PUSH | bp |
001 | 0002FB | 8BEC | MOV | bp,sp |
002 | 0002FD | 83EC0C | SUB | sp,0Ch |
003 | 000300 | 56 | PUSH | si |
004 | 000301 | 8D46FC | LEA | ax,[bp-4] |
005 | 000304 | 50 | PUSH | ax |
006 | 000305 | B89401 | MOV | ax,194h |
007 | 000308 | 50 | PUSH | ax |
008 | 000309 | E8D014 | CALL | near ptr scanf |
009 | 00030C | 59 | POP | cx |
010 | 00030D | 59 | POP | cx |
011 | 00030E | FF76FE | PUSH | word ptr [bp-2] |
012 | 000311 | FF76FC | PUSH | word ptr [bp-4] |
013 | 000314 | B89801 | MOV | ax,198h |
014 | 000317 | 50 | PUSH | ax |
015 | 000318 | E8380C | CALL | near ptr printf |
016 | 00031B | 83C406 | ADD | sp,6 |
017 | 00031E | 8D46F4 | LEA | ax,[bp-0Ch] |
018 | 000321 | 50 | PUSH | ax |
019 | 000322 | B8B201 | MOV | ax,1B2h |
020 | 000325 | 50 | PUSH | ax |
021 | 000326 | E8B314 | CALL | near ptr scanf |
022 | 000329 | 59 | POP | cx |
023 | 00032A | 59 | POP | cx |
024 | 00032B | 8D46F6 | LEA | ax,[bp-0Ah] |
025 | 00032E | 50 | PUSH | ax |
026 | 00032F | B8B501 | MOV | ax,1B5h |
027 | 000332 | 50 | PUSH | ax |
028 | 000333 | E8A614 | CALL | near ptr scanf |
029 | 000336 | 59 | POP | cx |
030 | 000337 | 59 | POP | cx |
031 | 000338 | C746FA0000 | MOV | word ptr [bp-6],0 |
032 | 00033D | C746F80100 | MOV | word ptr [bp-8],1 |
034 | 0003AA | 8B56FA | L1: MOV | dx,[bp-6] |
035 | 0003AD | 8B46F8 | MOV | ax,[bp-8] |
036 | 0003B0 | 3B56FE | CMP | dx,[bp-2] |
037 | 0003B3 | 7C8F | JL | L2 |
038 | 0003B5 | 7F05 | JG | L3 |
039 | 0003B7 | 3B46FC | CMP | ax,[bp-4] |
040 | 0003BA | 7688 | JBE | L2 |
041 | 0003BC | FF76F4 | L3: PUSH | word ptr [bp-0Ch] |
042 | 0003BF | B8B801 | MOV | ax,1B8h |
043 | 0003C2 | 50 | PUSH | ax |
044 | 0003C3 | E88D0B | CALL | near ptr printf |
045 | 0003C6 | 59 | POP | cx |
046 | 0003C7 | 59 | POP | cx |
047 | 0003C8 | 5E | POP | si |
048 | 0003C9 | 8BE5 | MOV | sp,bp |
049 | 0003CB | 5D | POP | bp |
050 | 0003CC | C3 | RET |  |
051 | 000344 | BE0100 | L2: MOV | si,1 |
053 | 00039D | 83FE28 | L4: CMP | si,28h |
054 | 0003A0 | 7EA7 | JLE | L5 |
055 | 0003A2 | 8346F801 | ADD | word ptr [bp-8],1 |
056 | 0003A6 | 8356FA00 | ADC | word ptr [bp-6],0 |
057 |  |  | JMP | L1 | ;Synthetic inst
058 | 000349 | 8B46F4 | L5: MOV | ax,[bp-0Ch] |
059 | 00034C | F766F4 | MUL | word ptr [bp-0Ch] |
060 | 00034F | F766F4 | MUL | word ptr [bp-0Ch] |
061 | 000352 | F766F4 | MUL | word ptr [bp-0Ch] |
062 | 000355 | F766F4 | MUL | word ptr [bp-0Ch] |
063 | 000358 | F766F4 | MUL | word ptr [bp-0Ch] |
064 | 00035B | F766F4 | MUL | word ptr [bp-0Ch] |
065 | 00035E | F766F4 | MUL | word ptr [bp-0Ch] |
066 | 000361 | F766F4 | MUL | word ptr [bp-0Ch] |
067 | 000364 | F766F4 | MUL | word ptr [bp-0Ch] |
068 | 000367 | F766F4 | MUL | word ptr [bp-0Ch] |
069 | 00036A | F766F4 | MUL | word ptr [bp-0Ch] |
070 | 00036D | F766F4 | MUL | word ptr [bp-0Ch] |
071 | 000370 | F766F4 | MUL | word ptr [bp-0Ch] |
072 | 000373 | F766F4 | MUL | word ptr [bp-0Ch] |
073 | 000376 | F766F4 | MUL | word ptr [bp-0Ch] |
074 | 000379 | F766F4 | MUL | word ptr [bp-0Ch] |
075 | 00037C | F766F4 | MUL | word ptr [bp-0Ch] |
076 | 00037F | F766F4 | MUL | word ptr [bp-0Ch] |
077 | 000382 | F766F4 | MUL | word ptr [bp-0Ch] |
078 | 000385 | F766F4 | MUL | word ptr [bp-0Ch] |
079 | 000388 | F766F4 | MUL | word ptr [bp-0Ch] |
080 | 00038B | F766F4 | MUL | word ptr [bp-0Ch] |
081 | 00038E | F766F4 | MUL | word ptr [bp-0Ch] |
082 | 000391 | F766F4 | MUL | word ptr [bp-0Ch] |
083 | 000394 | BA0300 | MOV | dx,3 |
084 | 000397 | F7E2 | MUL | dx |
085 | 000399 | 8946F4 | MOV | [bp-0Ch],ax |
086 | 00039C | 46 | INC | si |
087 |  |  | JMP | L4 | ;Synthetic inst
 | main | ENDP |  |  |

图 9-35: Benchmul.a2

/*

* Input file : benchmul.exe

* File type : EXE

*/

#include "dcc.h"

void main ()

/* Takes no parameters.

* High-level language prologue code.

*/

{

int loc1;

int loc2;

long loc3;

long loc4;

int loc5;

scanf ("%ld", &loc4);

printf ("executing %ld iterations\n", loc4);

scanf ("%d", &loc1);

scanf ("%d", &loc2);

loc3 = 1;

while ((loc3 <= loc4)) {

loc5 = 1;

while ((loc5 <= 40)) {

loc1 = (((((((((((((((((((((((((loc1 * loc1) * loc1) * loc1)

* loc1) * loc1) * loc1) * loc1) * loc1) * loc1) *

loc1) * loc1) * loc1) * loc1) * loc1) * loc1) * loc1)

* loc1) * loc1) * loc1) * loc1) * loc1) * loc1) *

loc1) * loc1) * 3);

loc5 = (loc5 + 1);

}

loc3 = (loc3 + 1);

}

printf ("a=%d\n", loc1);

}

图 9-36: Benchmul.b

/* benchmul - benchmark for int multiply

* Thomas Plum, Plum Hall Inc, 609-927-3770

* If machine traps overflow, use an unsigned type

* Let T be the execution time in milliseconds

* Then average time per operator = T/major usec

* (Because the inner loop has exactly 1000 operations)

*/

#define STOR_CL auto

#define TYPE int

#include <stdio.h>

main (int ac, char *av[])

{ STOR_CL TYPE a, b, c;

long d, major;

scanf ("%ld", &major);

printf("executing %ld iterations\n", major);

scanf ("%d", &a);

scanf ("%d", &b);

for (d = 1; d <= major; ++d)

{

/* inner loop executes 1000 selected operations */

for (c = 1; c <= 40; ++c)

{

a = 3 *a*a*a*a*a*a*a*a * a*a*a*a*a*a*a*a *

a*a*a*a*a*a*a*a * a; /* 25 * */

}

}

printf("a=%d\n", a);

}

图 9-37: Benchmul.c

子程序 | 低级 | 高级 | % 缩减率
main | 88 | 12 | 86.36
合计 | 88 | 12 | 86.36

图 9-38: Benchmul统计


第 9 章 dcc - Benchfn.exe

9.7.7 Benchfn.exe

Benchfn是来自Plum-Hall基准程序套装的一个程序，测试函数调用；每轮循环完成1000个子程序调用。反汇编的程序在图9-39被显示，反编译的C程序在图9-40，最初的C程序在图9-41。这个程序有下列调用图：

main

scanf

printf

proc_1

proc_2

proc_3

proc_4

Benchfn有四个子过程和一个主程序。四个子过程其中三个调用其它子过程，而第四个子过程是空的。这个程序的中间指令数目的缩减比例没有前面几个程序那么高，因为在该程序中没有许多表达式 (通常在高级程序中不是这种情况)。从这个程序的统计可知 (见图9-42)，空的子过程有100%的缩减率，因为该子过程的序言和尾部低级指令在C程序中被清除；另外三个子过程对于它们执行的29个子过程调用平均有29.30%的指令缩减率，而main程序有81.08%的指令缩减率，因为在这个子过程中有使用表达式和赋值。该程序的总平均是比较低的，56.10%，这是因为这个程序较少有赋值语句。

 | proc_4 | PROC NEAR |  |  |
000 | 0002FA | 55 | PUSH | bp |
001 | 0002FB | 8BEC | MOV | bp,sp |
002 | 0002FD | 5D | POP | bp |
003 | 0002FE | C3 | RET |  |
 | proc_4 | ENDP |  |  |
 | proc_3 | PROC NEAR |  |  |
000 | 0002FF | 55 | PUSH | bp |
001 | 000300 | 8BEC | MOV | bp,sp |
002 | 000302 | E8F5FF | CALL | near ptr proc_4 |
003 | 000305 | E8F2FF | CALL | near ptr proc_4 |
004 | 000308 | E8EFFF | CALL | near ptr proc_4 |
005 | 00030B | E8ECFF | CALL | near ptr proc_4 |
006 | 00030E | E8E9FF | CALL | near ptr proc_4 |
007 | 000311 | E8E6FF | CALL | near ptr proc_4 |
008 | 000314 | E8E3FF | CALL | near ptr proc_4 |
009 | 000317 | E8E0FF | CALL | near ptr proc_4 |
010 | 00031A | E8DDFF | CALL | near ptr proc_4 |
011 | 00031D | E8DAFF | CALL | near ptr proc_4 |
012 | 000320 | 5D | POP | bp |
013 | 000321 | C3 | RET |  |
 | proc_3 | ENDP |  |  |
 | proc_2 | PROC NEAR |  |  |
000 | 000322 | 55 | PUSH | bp |
001 | 000323 | 8BEC | MOV | bp,sp |
002 | 000325 | E8D7FF | CALL | near ptr proc_3 |
003 | 000328 | E8D4FF | CALL | near ptr proc_3 |
004 | 00032B | E8D1FF | CALL | near ptr proc_3 |
005 | 00032E | E8CEFF | CALL | near ptr proc_3 |
006 | 000331 | E8CBFF | CALL | near ptr proc_3 |
007 | 000334 | E8C8FF | CALL | near ptr proc_3 |
008 | 000337 | E8C5FF | CALL | near ptr proc_3 |
009 | 00033A | E8C2FF | CALL | near ptr proc_3 |
010 | 00033D | E8BFFF | CALL | near ptr proc_3 |
011 | 000340 | E8BCFF | CALL | near ptr proc_3 |
012 | 000343 | 5D | POP | bp |
013 | 000344 | C3 | RET |  |
 | proc_2 | ENDP |  |  |
 | proc_1 | PROC NEAR |  |  |
000 | 000345 | 55 | PUSH | bp |
001 | 000346 | 8BEC | MOV | bp,sp |
002 | 000348 | E8D7FF | CALL | near ptr proc_2 |
003 | 00034B | E8D4FF | CALL | near ptr proc_2 |
004 | 00034E | E8D1FF | CALL | near ptr proc_2 |
005 | 000351 | E8CEFF | CALL | near ptr proc_2 |
006 | 000354 | E8CBFF | CALL | near ptr proc_2 |
007 | 000357 | E8C8FF | CALL | near ptr proc_2 |
008 | 00035A | E8C5FF | CALL | near ptr proc_2 |
009 | 00035D | E8C2FF | CALL | near ptr proc_2 |
010 | 000360 | E8BFFF | CALL | near ptr proc_2 |
011 | 000363 | 5D | POP | bp |
012 | 000364 | C3 | RET |  |
 | proc_1 | ENDP |  |  |
 | main | PROC NEAR |  |  |
000 | 000365 | 55 | PUSH | bp |
001 | 000366 | 8BEC | MOV | bp,sp |
002 | 000368 | 83EC08 | SUB | sp,8 |
003 | 00036B | 8D46FC | LEA | ax,[bp-4] |
004 | 00036E | 50 | PUSH | ax |
005 | 00036F | B89401 | MOV | ax,194h |
006 | 000372 | 50 | PUSH | ax |
007 | 000373 | E85614 | CALL | near ptr scanf |
008 | 000376 | 59 | POP | cx |
009 | 000377 | 59 | POP | cx |
010 | 000378 | FF76FE | PUSH | word ptr [bp-2] |
011 | 00037B | FF76FC | PUSH | word ptr [bp-4] |
012 | 00037E | B89801 | MOV | ax,198h |
013 | 000381 | 50 | PUSH | ax |
014 | 000382 | E8BE0B | CALL | near ptr printf |
015 | 000385 | 83C406 | ADD | sp,6 |
016 | 000388 | C746FA0000 | MOV | word ptr [bp-6],0 |
017 | 00038D | C746F80100 | MOV | word ptr [bp-8],1 |
019 | 00039F | 8B56FA | L1: MOV | dx,[bp-6] |
020 | 0003A2 | 8B46F8 | MOV | ax,[bp-8] |
021 | 0003A5 | 3B56FE | CMP | dx,[bp-2] |
022 | 0003A8 | 7CEA | JL | L2 |
023 | 0003AA | 7F05 | JG | L3 |
024 | 0003AC | 3B46FC | CMP | ax,[bp-4] |
025 | 0003AF | 76E3 | JBE | L2 |
026 | 0003B1 | B8B201 | L3: MOV | ax,1B2h |
027 | 0003B4 | 50 | PUSH | ax |
028 | 0003B5 | E88B0B | CALL | near ptr printf |
029 | 0003B8 | 59 | POP | cx |
030 | 0003B9 | 8BE5 | MOV | sp,bp |
031 | 0003BB | 5D | POP | bp |
032 | 0003BC | C3 | RET |  |
033 | 000394 | E8AEFF | L2: CALL | near ptr proc_1 |
034 | 000397 | 8346F801 | ADD | word ptr [bp-8],1 |
035 | 00039B | 8356FA00 | ADC | word ptr [bp-6],0 |
036 |  |  | JMP | L1 | ;Synthetic inst
 | main | ENDP |  |  |

图 9-39: Benchfn.a2

/*

* Input file : benchfn.exe

* File type : EXE

*/

#include "dcc.h"

void proc_4 ()

/* Takes no parameters.

* High-level language prologue code.

*/

{

}

void proc_3 ()

/* Takes no parameters.

* High-level language prologue code.

*/

{

proc_4 ();

proc_4 ();

proc_4 ();

proc_4 ();

proc_4 ();

proc_4 ();

proc_4 ();

proc_4 ();

proc_4 ();

proc_4 ();

}

void proc_2 ()

/* Takes no parameters.

* High-level language prologue code.

*/

{

proc_3 ();

proc_3 ();

proc_3 ();

proc_3 ();

proc_3 ();

proc_3 ();

proc_3 ();

proc_3 ();

proc_3 ();

proc_3 ();

}

void proc_1 ()

/* Takes no parameters.

* High-level language prologue code.

*/

{

proc_2 ();

proc_2 ();

proc_2 ();

proc_2 ();

proc_2 ();

proc_2 ();

proc_2 ();

proc_2 ();

proc_2 ();

}

void main ()

/* Takes no parameters.

* High-level language prologue code.

*/

{

long loc1;

long loc2;

scanf ("%ld", &loc2);

printf ("executing %ld iterations\n", loc2);

loc1 = 1;

while ((loc1 <= loc2)) {

proc_1 ();

loc1 = (loc1 + 1);

}

printf ("finished\n");

}

图 9-40: Benchfn.b

/* benchfn - benchmark for function calls

* Thomas Plum, Plum Hall Inc, 609-927-3770

* Let T be the execution time in milliseconds

* Then average time per operator = T/major usec

* (Because the inner loop has exactly 1000 operations)

*/

#include <stdio.h>

f3() { ;}

f2() { f3();f3();f3();f3();f3();f3();f3();f3();f3();f3();} /* 10 */

f1() { f2();f2();f2();f2();f2();f2();f2();f2();f2();f2();} /* 10 */

f0() { f1();f1();f1();f1();f1();f1();f1();f1();f1();} /* 9 */

main (int ac, char *av[])

{ long d, major;

scanf ("%ld", &major);

printf("executing %ld iterations\n", major);

for (d = 1; d <= major; ++d)

f0(); /* executes 1000 calls */

printf ("finished\n");

}

图 9-41: Benchfn.c

子程序 | 低级 | 高级 | % 缩减率
proc4 | 4 | 0 | 100.00
proc3 | 14 | 10 | 28.57
proc2 | 14 | 10 | 28.57
proc1 | 13 | 9 | 30.77
main | 37 | 7 | 81.08
合计 | 82 | 36 | 56.10

图 9-42: Benchfn统计


第 9 章 dcc - Fibo.exe

9.7.8 Fibo.exe

Fibo是计算输入数字的斐波纳契数列的程序。斐波纳契数(Fibonacci number)的计算用一个递归的函数执行 (两个递归被使用)。反汇编的程序见图9-43所示，反编译的C程序在图9-44，最初的C程序在图9-45。Fibo有下列的调用图：

main

scanf

printf

exit

proc_1

proc_1

反编译的C程序的main跟最初的C程序有相同的指令数目；for()循环被表示为一个while()循环。递归的Fibonacci函数，即反编译的程序中的proc_1，使用五个指令，它们对应最初的代码中的三个指令。额外的这些指令是由于把参数复制到一个局部变量(loc1 = arg0;)，以及在返回数值之前沿着两个不同的路径把结果放在一个寄存器变量中(即，有两个不同的可能结果)。在所有的方式中代码都跟最初的代码是功能等价的。注意，proc_1的第二个递归调用上，实际参数表达式是(loc1 + -2)；即等于(loc1 - 2)。前一个表达式来自该程序的反汇编——使用一个局部变量和一个负数的相加，而不是减去一个正数。从该程序的统计可知 (见图9-46)，单独的和总计的中间指令数目缩减率均是80.77%。

 | proc_1 | PROC NEAR |  |  |
000 | 00035B | 55 | PUSH | bp |
001 | 00035C | 8BEC | MOV | bp,sp |
002 | 00035E | 56 | PUSH | si |
003 | 00035F | 8B7604 | MOV | si,[bp+4] |
004 | 000362 | 83FE02 | CMP | si,2 |
005 | 000365 | 7E1C | JLE | L1 |
006 | 000367 | 8BC6 | MOV | ax,si |
007 | 000369 | 48 | DEC | ax |
008 | 00036A | 50 | PUSH | ax |
009 | 00036B | E8EDFF | CALL | near ptr proc_1 |
010 | 00036E | 59 | POP | cx |
011 | 00036F | 50 | PUSH | ax |
012 | 000370 | 8BC6 | MOV | ax,si |
013 | 000372 | 05FEFF | ADD | ax,0FFFEh |
014 | 000375 | 50 | PUSH | ax |
015 | 000376 | E8E2FF | CALL | near ptr proc_1 |
016 | 000379 | 59 | POP | cx |
017 | 00037A | 8BD0 | MOV | dx,ax |
018 | 00037C | 58 | POP | ax |
019 | 00037D | 03C2 | ADD | ax,dx |
021 | 000388 | 5E | L2: POP | si |
022 | 000389 | 5D | POP | bp |
023 | 00038A | C3 | RET |  |
024 | 000383 | B80100 | L1: MOV | ax,1 |
025 | 000386 | EB00 | JMP | L2 |
 | proc_1 | ENDP |  |  |
 | main | PROC NEAR |  |  |
000 | 0002FA | 55 | PUSH | bp |
001 | 0002FB | 8BEC | MOV | bp,sp |
002 | 0002FD | 83EC04 | SUB | sp,4 |
003 | 000300 | 56 | PUSH | si |
004 | 000301 | 57 | PUSH | di |
005 | 000302 | B89401 | MOV | ax,194h |
006 | 000305 | 50 | PUSH | ax |
007 | 000306 | E8080C | CALL | near ptr printf |
008 | 000309 | 59 | POP | cx |
009 | 00030A | 8D46FC | LEA | ax,[bp-4] |
010 | 00030D | 50 | PUSH | ax |
011 | 00030E | B8B101 | MOV | ax,1B1h |
012 | 000311 | 50 | PUSH | ax |
013 | 000312 | E88514 | CALL | near ptr scanf |
014 | 000315 | 59 | POP | cx |
015 | 000316 | 59 | POP | cx |
016 | 000317 | BE0100 | MOV | si,1 |
018 | 000349 | 3B76FC | L3: CMP | si,[bp-4] |
019 | 00034C | 7ECE | JLE | L4 |
020 | 00034E | 33C0 | XOR | ax,ax |
021 | 000350 | 50 | PUSH | ax |
022 | 000351 | E87300 | CALL | near ptr exit |
023 | 000354 | 59 | POP | cx |
024 | 000355 | 5F | POP | di |
025 | 000356 | 5E | POP | si |
026 | 000357 | 8BE5 | MOV | sp,bp |
027 | 000359 | 5D | POP | bp |
028 | 00035A | C3 | RET |  |
029 | 00031C | B8B401 | L4: MOV | ax,1B4h |
030 | 00031F | 50 | PUSH | ax |
031 | 000320 | E8EE0B | CALL | near ptr printf |
032 | 000323 | 59 | POP | cx |
033 | 000324 | 8D46FE | LEA | ax,[bp-2] |
034 | 000327 | 50 | PUSH | ax |
035 | 000328 | B8C301 | MOV | ax,1C3h |
036 | 00032B | 50 | PUSH | ax |
037 | 00032C | E86B14 | CALL | near ptr scanf |
038 | 00032F | 59 | POP | cx |
039 | 000330 | 59 | POP | cx |
040 | 000331 | FF76FE | PUSH | word ptr [bp-2] |
041 | 000334 | E82400 | CALL | near ptr proc_1 |
042 | 000337 | 59 | POP | cx |
043 | 000338 | 8BF8 | MOV | di,ax |
044 | 00033A | 57 | PUSH | di |
045 | 00033B | FF76FE | PUSH | word ptr [bp-2] |
046 | 00033E | B8C601 | MOV | ax,1C6h |
047 | 000341 | 50 | PUSH | ax |
048 | 000342 | E8CC0B | CALL | near ptr printf |
049 | 000345 | 83C406 | ADD | sp,6 |
050 | 000348 | 46 | INC | si |
051 |  |  | JMP | L3 | ;Synthetic inst
 | main | ENDP |  |  |

图 9-43: Fibo.a2

/*

* Input file : fibo.exe

* File type : EXE

*/

#include "dcc.h"

int proc_1 (int arg0)

/* Takes 2 bytes of parameters.

* High-level language prologue code.

* C calling convention.

*/

{

int loc1;

int loc2; /* ax */

loc1 = arg0;

if (loc1 > 2) {

loc2 = (proc_1 ((loc1 - 1)) + proc_1 ((loc1 + -2)));

}

else {

loc2 = 1;

}

return (loc2);

}

void main ()

/* Takes no parameters.

* High-level language prologue code.

*/

{

int loc1; int loc2;

int loc3; int loc4;

printf ("Input number of iterations: ");

scanf ("%d", &loc1);

loc3 = 1;

while ((loc3 <= loc1)) {

printf ("Input number: ");

scanf ("%d", &loc2);

loc4 = proc_1 (loc2);

printf ("fibonacci(%d) = %u\n", loc2, loc4);

loc3 = (loc3 + 1);

}

exit (0);

}

图 9-44: Fibo.b

#include <stdio.h>

int main()

{ int i, numtimes, number;

unsigned value, fib();

printf("Input number of iterations: ");

scanf ("%d", &numtimes);

for (i = 1; i <= numtimes; i++)

{

printf ("Input number: ");

scanf ("%d", &number);

value = fib(number);

printf("fibonacci(%d) = %u\n", number, value);

}

exit(0);

}

unsigned fib(x) /* compute fibonacci number recursively */

int x;

{

if (x > 2)

return (fib(x - 1) + fib(x - 2));

else

return (1);

}

图 9-45: Fibo.c

子程序 | 低级 | 高级 | % 缩减率
proc_1 | 26 | 5 | 80.77
main | 52 | 10 | 80.77
合计 | 78 | 15 | 80.77

图 9-46: Fibo统计


第 9 章 dcc - Crc.exe

9.7.9 Crc.exe

Crc这个程序计算单字符(1-character)消息块的循环冗余校验(CRC)，然后把求得的CRC送回到CRC函数，检查“收到的”单字符消息和CRC是否正确的。反汇编程序见图9-47所示，反编译的C程序在图9-48，最初的C程序在图9-49。Crc有如下调用图：

main

proc_1

proc_2

LXLSH@

LXRSH@

proc_3

proc_2

printf

从最初的C程序可知，crc有三个函数和一个主要子过程。该程序的反编译版本有五个函数和一个主程序；两个额外的函数是支持长整型右移位和左移位的运行时支持例程 (分别是LXRSH@和LXLSH@)。这两个例程最初是用汇编语言编写的，而且被翻译成C语言——通过存取长整型参数的低字部分和高字部分。从该程序的统计可知 (见图9-50)，用户函数有中间指令缩减率超过80%。这些函数有跟最初程序相同数目的高级指令。函数proc_1是返回零的crc_clear函数。这个函数有中间指令83.33%的缩减率，归因于子过程序言和尾代码带来的开销。函数proc_2是crc_update函数，它依照CCITT推荐的CRC生成器函数来为输入参数计算CRC。这个函数使用32位来计算结果，而且返回较低的16位作为该函数的值。这个函数的反编译版本传播以下事实——被调用的运行时例程LXRSH@的结果只使用其中16位，因此这个函数只返回一个整型数(16位)而非一个长整型；其代码比相应的LXLSH@ (它返回一个长整型) 简单得多。指令数目的缩减率达84.62%。函数proc_3是crc_finish函数，它返回最后的两个CRC字符——在块结束时传输。这个函数调用crc_update函数两次；其中一个作为另一个的参数。指令数目的缩减率是很高的(93.75%)，因为所有16个低级指令被转换成1个高级返回指令。最后，主程序以正确的顺序调用函数；达到82.09%的缩减率。在这个程序中使用整型数而非字符型，因为这样的字符没有使用字符型变量 (即，一个无符号的字符产生相同的代码)。该程序总的中间指令缩减率是77.78%，这是由于运行时例程而少于80%。

 | proc_3 | PROC NEAR |  |  |
000 | 000385 | 55 | PUSH | bp |
001 | 000386 | 8BEC | MOV | bp,sp |
002 | 000388 | 33C0 | XOR | ax,ax |
003 | 00038A | 50 | PUSH | ax |
004 | 00038B | 33C0 | XOR | ax,ax |
005 | 00038D | 50 | PUSH | ax |
006 | 00038E | FF7604 | PUSH | word ptr [bp+4] |
007 | 000391 | E86FFF | CALL | near ptr proc_2 |
008 | 000394 | 59 | POP | cx |
009 | 000395 | 59 | POP | cx |
010 | 000396 | 50 | PUSH | ax |
011 | 000397 | E869FF | CALL | near ptr proc_2 |
012 | 00039A | 8BE5 | MOV | sp,bp |
014 | 00039E | 5D | POP | bp |
015 | 00039F | C3 | RET |  |
 | proc_3 | ENDP |  |  |
 | LXRSH@ | PROC FAR |  |  |
000 | 001585 | 80F910 | CMP | cl,10h |
001 | 001588 | 7310 | JAE | L1 |
002 | 00158A | 8BDA | MOV | bx,dx |
003 | 00158C | D3E8 | SHR | ax,cl |
004 | 00158E | D3FA | SAR | dx,cl |
005 | 001590 | F6D9 | NEG | cl |
006 | 001592 | 80C110 | ADD | cl,10h |
007 | 001595 | D3E3 | SHL | bx,cl |
008 | 001597 | 0BC3 | OR | ax,bx |
009 | 001599 | CB | RETF |  |
010 | 00159A | 80E910 | L1: SUB | cl,10h |
011 | 00159D | 8BC2 | MOV | ax,dx |
012 | 00159F | 99 | CWD |  |
013 | 0015A0 | D3F8 | SAR | ax,cl |
014 | 0015A2 | CB | RETF |  |
 | LXRSH@ | ENDP |  |  |
 | proc_1 | PROC NEAR |  |  |
000 | 0002FA | 55 | PUSH | bp |
001 | 0002FB | 8BEC | MOV | bp,sp |
002 | 0002FD | 33C0 | XOR | ax,ax |
004 | 000301 | 5D | POP | bp |
005 | 000302 | C3 | RET |  |
 | proc_1 | ENDP |  |  |
 | LXLSH@ | PROC FAR |  |  |
000 | 0015A3 | 80F910 | CMP | cl,10h |
001 | 0015A6 | 7310 | JAE | L2 |
002 | 0015A8 | 8BD8 | MOV | bx,ax |
003 | 0015AA | D3E0 | SHL | ax,cl |
004 | 0015AC | D3E2 | SHL | dx,cl |
005 | 0015AE | F6D9 | NEG | cl |
006 | 0015B0 | 80C110 | ADD | cl,10h |
007 | 0015B3 | D3EB | SHR | bx,cl |
008 | 0015B5 | 0BD3 | OR | dx,bx |
009 | 0015B7 | CB | RETF |  |
010 | 0015B8 | 80E910 | L2: SUB | cl,10h |
011 | 0015BB | 8BD0 | MOV | dx,ax |
012 | 0015BD | 33C0 | XOR | ax,ax |
013 | 0015BF | D3E2 | SHL | dx,cl |
014 | 0015C1 | CB | RETF |  |
 | LXLSH@ | ENDP |  |  |
 | proc_2 | PROC NEAR |  |  |
000 | 000303 | 55 | PUSH | bp |
001 | 000304 | 8BEC | MOV | bp,sp |
002 | 000306 | 83EC06 | SUB | sp,6 |
003 | 000309 | 8B4604 | MOV | ax,[bp+4] |
004 | 00030C | 99 | CWD |  |
005 | 00030D | B108 | MOV | cl,8 |
006 | 00030F | 9AA3141000 | CALL | far ptr LXLSH@ |
007 | 000314 | 52 | PUSH | dx |
008 | 000315 | 50 | PUSH | ax |
009 | 000316 | 8A4606 | MOV | al,[bp+6] |
010 | 000319 | 98 | CWD |  |
011 | 00031A | 99 | CWD |  |
012 | 00031B | 5B | POP | bx |
013 | 00031C | 59 | POP | cx |
014 | 00031D | 03D8 | ADD | bx,ax |
015 | 00031F | 13CA | ADC | cx,dx |
016 | 000321 | 894EFC | MOV | [bp-4],cx |
017 | 000324 | 895EFA | MOV | [bp-6],bx |
018 | 000327 | C746FE0000 | MOV | word ptr [bp-2],0 |
020 | 000365 | 837EFE08 | L3: CMP | word ptr [bp-2],8 |
021 | 000369 | 7CC3 | JL | L4 |
022 | 00036B | 8B56FC | MOV | dx,[bp-4] |
023 | 00036E | 8B46FA | MOV | ax,[bp-6] |
024 | 000371 | 2500FF | AND | ax,0FF00h |
025 | 000374 | 81E2FF00 | AND | dx,0FFh |
026 | 000378 | B108 | MOV | cl,8 |
027 | 00037A | 9A85141000 | CALL | far ptr LXRSH@ |
029 | 000381 | 8BE5 | MOV | sp,bp |
030 | 000383 | 5D | POP | bp |
031 | 000384 | C3 | RET |  |
032 | 00032E | 8B56FC | L4: MOV | dx,[bp-4] |
033 | 000331 | 8B46FA | MOV | ax,[bp-6] |
034 | 000334 | D1E0 | SHL | ax,1 |
035 | 000336 | D1D2 | RCL | dx,1 |
036 | 000338 | 8956FC | MOV | [bp-4],dx |
037 | 00033B | 8946FA | MOV | [bp-6],ax |
038 | 00033E | 8B56FC | MOV | dx,[bp-4] |
039 | 000341 | 8B46FA | MOV | ax,[bp-6] |
040 | 000344 | 250000 | AND | ax,0 |
041 | 000347 | 81E20001 | AND | dx,100h |
042 | 00034B | 0BD0 | OR | dx,ax |
043 | 00034D | 7413 | JE | L5 |
044 | 00034F | 8B56FC | MOV | dx,[bp-4] |
045 | 000352 | 8B46FA | MOV | ax,[bp-6] |
046 | 000355 | 350021 | XOR | ax,2100h |
047 | 000358 | 81F21001 | XOR | dx,110h |
048 | 00035C | 8956FC | MOV | [bp-4],dx |
049 | 00035F | 8946FA | MOV | [bp-6],ax |
050 | 000362 | FF46FE | L5: INC | word ptr [bp-2] |
051 |  |  | JMP | L3 | ;Synthetic inst
 | proc_2 | ENDP |  |  |
 | main | PROC NEAR |  |  |
000 | 0003A0 | 55 | PUSH | bp |
001 | 0003A1 | 8BEC | MOV | bp,sp |
002 | 0003A3 | 83EC06 | SUB | sp,6 |
003 | 0003A6 | C646FD41 | MOV | byte ptr [bp-3],41h |
004 | 0003AA | E84DFF | CALL | near ptr proc_1 |
005 | 0003AD | 8946FA | MOV | [bp-6],ax |
006 | 0003B0 | 8A46FD | MOV | al,[bp-3] |
007 | 0003B3 | 98 | CWD |  |
008 | 0003B4 | 50 | PUSH | ax |
009 | 0003B5 | FF76FA | PUSH | word ptr [bp-6] |
010 | 0003B8 | E848FF | CALL | near ptr proc_2 |
011 | 0003BB | 59 | POP | cx |
012 | 0003BC | 59 | POP | cx |
013 | 0003BD | 8946FA | MOV | [bp-6],ax |
014 | 0003C0 | FF76FA | PUSH | word ptr [bp-6] |
015 | 0003C3 | E8BFFF | CALL | near ptr proc_3 |
016 | 0003C6 | 59 | POP | cx |
017 | 0003C7 | 8946FA | MOV | [bp-6],ax |
018 | 0003CA | 8B46FA | MOV | ax,[bp-6] |
019 | 0003CD | 2500FF | AND | ax,0FF00h |
020 | 0003D0 | B108 | MOV | cl,8 |
021 | 0003D2 | D3E8 | SHR | ax,cl |
022 | 0003D4 | 8846FE | MOV | [bp-2],al |
023 | 0003D7 | 8A46FA | MOV | al,[bp-6] |
024 | 0003DA | 24FF | AND | al,0FFh |
025 | 0003DC | 8846FF | MOV | [bp-1],al |
026 | 0003DF | FF76FA | PUSH | word ptr [bp-6] |
027 | 0003E2 | B89401 | MOV | ax,194h |
028 | 0003E5 | 50 | PUSH | ax |
029 | 0003E6 | E8FC08 | CALL | near ptr printf |
030 | 0003E9 | 59 | POP | cx |
031 | 0003EA | 59 | POP | cx |
032 | 0003EB | E80CFF | CALL | near ptr proc_1 |
033 | 0003EE | 8946FA | MOV | [bp-6],ax |
034 | 0003F1 | 8A46FD | MOV | al,[bp-3] |
035 | 0003F4 | 98 | CWD |  |
036 | 0003F5 | 50 | PUSH | ax |
037 | 0003F6 | FF76FA | PUSH | word ptr [bp-6] |
038 | 0003F9 | E807FF | CALL | near ptr proc_2 |
039 | 0003FC | 59 | POP | cx |
040 | 0003FD | 59 | POP | cx |
041 | 0003FE | 8946FA | MOV | [bp-6],ax |
042 | 000401 | 8A46FE | MOV | al,[bp-2] |
043 | 000404 | 98 | CWD |  |
044 | 000405 | 50 | PUSH | ax |
045 | 000406 | FF76FA | PUSH | word ptr [bp-6] |
046 | 000409 | E8F7FE | CALL | near ptr proc_2 |
047 | 00040C | 59 | POP | cx |
048 | 00040D | 59 | POP | cx |
049 | 00040E | 8946FA | MOV | [bp-6],ax |
050 | 000411 | 8A46FF | MOV | al,[bp-1] |
051 | 000414 | 98 | CWD |  |
052 | 000415 | 50 | PUSH | ax |
053 | 000416 | FF76FA | PUSH | word ptr [bp-6] |
054 | 000419 | E8E7FE | CALL | near ptr proc_2 |
055 | 00041C | 59 | POP | cx |
056 | 00041D | 59 | POP | cx |
057 | 00041E | 8946FA | MOV | [bp-6],ax |
058 | 000421 | FF76FA | PUSH | word ptr [bp-6] |
059 | 000424 | B89A01 | MOV | ax,19Ah |
060 | 000427 | 50 | PUSH | ax |
061 | 000428 | E8BA08 | CALL | near ptr printf |
062 | 00042B | 59 | POP | cx |
063 | 00042C | 59 | POP | cx |
064 | 00042D | 8BE5 | MOV | sp,bp |
065 | 00042F | 5D | POP | bp |
066 | 000430 | C3 | RET |  |
 | main | ENDP |  |  |

图 9-47: Crc.a2

/*

* Input file : crc.exe

* File type : EXE

*/

#include "dcc.h"

int proc_1 ()

/* Takes no parameters.

* High-level language prologue code.

*/

{

return (0);

}

long LXLSH@ (long arg0, char arg1)

/* Uses register arguments:

* arg0 = dx:ax.

* arg1 = cl.

* Runtime support routine of the compiler.

*/

{

int loc1; /* bx */

if (arg1 < 16) {

loc1 = LO(arg0);

LO(arg0) = (LO(arg0) << arg1);

HI(arg0) = (HI(arg0) << arg1);

HI(arg0) = (HI(arg0) | (loc1 >> (!arg1 + 16)));

return (arg0);

}

else {

HI(arg0) = LO(arg0);

LO(arg0) = 0;

HI(arg0) = (HI(arg0) << (arg1 - 16));

return (arg0);

}

}

int LXRSH@ (long arg0, char arg1)

/* Uses register arguments:

* arg0 = dx:ax.

* arg1 = cl.

* Runtime support routine of the compiler.

*/

{

int loc1; /* bx */

if (arg1 < 16) {

loc1 = HI(arg0);

LO(arg0) = (LO(arg0) >> arg1);

HI(arg0) = (HI(arg0) >> arg1);

return ((LO(arg0) | (loc1 << (!arg1 + 16))));

}

else {

return ((HI(arg0) >> (arg1 - 16)));

}

}

int proc_2 (int arg0, unsigned char arg1)

/* Takes 4 bytes of parameters.

* High-level language prologue code.

* C calling convention.

*/

{

int loc1;

long loc2;

loc2 = (LXLSH@ (arg0, 8) + arg1);

loc1 = 0;

while ((loc1 < 8)) {

loc2 = (loc2 << 1);

if ((loc2 & 0x1000000) != 0) {

loc2 = (loc2 ^ 0x1102100);

}

loc1 = (loc1 + 1);

}

return (LXRSH@ ((loc2 & 0xFFFF00), 8));

}

int proc_3 (int arg0)

/* Takes 2 bytes of parameters.

* High-level language prologue code.

* C calling convention.

*/

{

return (proc_2 (proc_2 (arg0, 0), 0));

}

void main ()

/* Takes no parameters.

* High-level language prologue code.

*/

{

int loc1;

int loc2;

int loc3;

int loc4;

loc1 = 65;

loc2 = proc_1 ();

loc2 = proc_2 (loc2, loc1);

loc2 = proc_3 (loc2);

loc3 = ((loc2 & 0xFF00) >> 8);

loc4 = (loc2 & 255);

printf ("%04x\n", loc2);

loc2 = proc_1 ();

loc2 = proc_2 (loc2, loc1);

loc2 = proc_2 (loc2, loc3);

loc2 = proc_2 (loc2, loc4);

printf ("%04x\n", loc2);

}

图 9-48: Crc.b

/*

* crc_clear:

* This function clears the CRC to zero. It should be called prior to

* the start of the processing of a block for both received messages,

* and messages to be transmitted.

*

* Calling sequence:

*

* short crc;

* crc = crc_clear();

*/

short crc_clear()

{

return(0);

}

/*

* crc_update:

* this function must be called once for each character which is

* to be included in the CRC for messages to be transmitted.

* This function is called once for each character which is included

* in the CRC of a received message, AND once for each of the two CRC

* characters at the end of the received message. If the resulting

* CRC is zero, then the message has been correctly received.

*

* Calling sequence:

*

* crc = crc_update(crc,next_char);

*/

short crc_update(crc,crc_char)

short crc;

char crc_char;

{

long x;

short i;

/* "x" will contain the character to be processed in bits 0-7 and the CRC */

/* in bits 8-23. Bit 24 will be used to test for overflow, and then cleared */

/* to prevent the sign bit of "x" from being set to 1. Bits 25-31 are not */

/* used. ("x" is treated as though it is a 32 bit register). */

x = ((long)crc << 8) + crc_char; /* Get the CRC and the character */

/* Repeat the following loop 8 times (for the 8 bits of the character). */

for(i = 0;i < 8;i++)

{

/* Shift the high-order bit of the character into the low-order bit of the */

/* CRC, and shift the high-order bit of the CRC into bit 24. */

x = x << 1; /* Shift "x" left one bit */

/* Test to see if the old high-order bit of the CRC was a 1. */

if(x & 0x01000000) /* Test bit 24 of "x" */

/* If the old high-order bit of the CRC was a 1, exclusive-or it with a one */

/* to set it to 0, and exclusive-or the CRC with hex 1021 to produce the */

/* CCITT-recommended CRC generator of: X**16 + X**12 + X**5 + 1. To produce */

/* the CRC generator of: X**16 + X**15 + X**2 + 1, change the constant from */

/* 0x01102100 to 0x01800500. This will exclusive-or the CRC with hex 8005 */

/* and produce the same CRC that IBM uses for their synchronous transmission */

/* protocols. */

x = x ^ 0x01102100; /* Exclusive-or "x" with a...*/

/* ...constant of hex 01102100 */

/* And repeat 8 times. */

} /* End of "for" loop */

/* Return the CRC as the 16 low-order bits of this function's value. */

return(((x & 0x00ffff00) >> 8)); /* AND off the unneeded bits and... */

/* ...shift the result 8 bits to the right */

}

/*

* crc_finish:

* This function must be called once after all the characters in a block

* have been processed for a message which is to be TRANSMITTED. It

* returns the calculated CRC bytes, which should be transmitted as the

* two characters following the block. The first of these 2 bytes

* must be taken from the high-order byte of the CRC, and the second

* must be taken from the low-order byte of the CRC. This routine is NOT

* called for a message which has been RECEIVED.

*

* Calling sequence:

*

* crc = crc_finish(crc);

*/

short crc_finish(crc)

short crc;

{

/* Call crc_update twice, passing it a character of hex 00 each time, to */

/* flush out the last 16 bits from the CRC calculation, and return the */

/* result as the value of this function. */

return(crc_update(crc_update(crc,'\0'),'\0'));

}

/*

* This is a sample of the use of the CRC functions, which calculates the

* CRC for a 1-character message block, and then passes the resulting CRC back

* into the CRC functions to see if the "received" 1-character message and CRC

* are correct.

*/

main()

{

short crc; /* The calculated CRC */

char crc_char; /* The 1-character message */

char x, y; /* 2 places to hold the 2 "received" CRC bytes */

crc_char = 'A'; /* Define the 1-character message */

crc = crc_clear(); /* Reset the CRC to "transmit" a new message */

crc = crc_update(crc,crc_char); /* Update the CRC for the first... */

/* ...(and only) character of the message */

crc = crc_finish(crc); /* Finish the transmission calculation */

x = (char)((crc & 0xff00) >> 8); /* Extract the high-order CRC byte */

y = (char)(crc & 0x00ff); /* And extract the low-order byte */

printf("%04x\n",crc); /* Print the results */

crc = crc_clear(); /* Prepare to "receive" a message */

crc = crc_update(crc,crc_char); /* Update the CRC for the first... */

/* ...(and only) character of the message */

crc = crc_update(crc,x); /* Pass both bytes of the "received"... */

crc = crc_update(crc,y); /* ...CRC through crc_update, too */

printf("%04x\n",crc); /* If the result was 0, then the message... */

/* ...was received without error */

}

图 9-49: Crc.c

子程序 | 低级 | 高级 | % 缩减率
proc_1 | 6 | 1 | 83.33
LXLSH@ | 15 | 10 | 33.33
LXRSH@ | 15 | 6 | 60.00
proc_2 | 52 | 8 | 84.62
proc_3 | 16 | 1 | 93.75
main | 67 | 12 | 82.09
合计 | 171 | 38 | 77.78

图 9-50: Crc统计


第 9 章 dcc - Matrixmu

9.7.10 Matrixmu.exe

Matrixmu是一个矩阵相乘的程序。这个程序在某种意义上是不完整的，它没有初始化矩阵，但是把它反编译了用以说明第5章第5.4.10节的向前代入法能够发现数组表示法。dcc没有实现从这个表示法到数组的转换，但是在第5章第5.5节有详细解释。反汇编的程序在图9-51中显示，反编译的C程序在图9-52，最初的C程序在图9-53。这个程序的调用图如下：

main

proc_1

两个用户子过程都以相同的高级指令数被反编译；矩阵乘法子过程有10个，主程序有1个。指令数目的缩减率超过85%，归因于在一个数组偏移的计算中涉及大量低级指令。在程序的反汇编版本中，在子程序proc_1第026行到第069行上的基本块有44个指令，它们被转换成两个高级指令；中间指令缩减率95.45%。整体来说，这个程序的中间指令有86.90%的缩减率，如图9-54所示。

 | proc_1 | PROC NEAR |  |  |
000 | 0002FA | 55 | PUSH | bp |
001 | 0002FB | 8BEC | MOV | bp,sp |
002 | 0002FD | 83EC02 | SUB | sp,2 |
003 | 000300 | 56 | PUSH | si |
004 | 000301 | 57 | PUSH | di |
005 | 000302 | 33F6 | XOR | si,si |
007 | 000378 | 83FE05 | L1: CMP | si,5 |
008 | 00037B | 7C89 | JL | L2 |
009 | 00037D | 5F | POP | di |
010 | 00037E | 5E | POP | si |
011 | 00037F | 8BE5 | MOV | sp,bp |
012 | 000381 | 5D | POP | bp |
013 | 000382 | C3 | RET |  |
014 | 000306 | 33FF | L2: XOR | di,di |
016 | 000372 | 83FF04 | L3: CMP | di,4 |
017 | 000375 | 7C93 | JL | L4 |
018 | 000377 | 46 | INC | si |
019 |  |  | JMP | L1 | ;Synthetic inst
020 | 00030A | C746FE0000 | L4: MOV | word ptr [bp-2],0 |
022 | 00036B | 837EFE04 | L5: CMP | word ptr [bp-2],4 |
023 | 00036F | 7CA0 | JL | L6 |
024 | 000371 | 47 | INC | di |
025 |  |  | JMP | L3 | ;Synthetic inst
026 | 000311 | 8BDE | L6: MOV | bx,si |
027 | 000313 | D1E3 | SHL | bx,1 |
028 | 000315 | D1E3 | SHL | bx,1 |
029 | 000317 | D1E3 | SHL | bx,1 |
030 | 000319 | 035E04 | ADD | bx,[bp+4] |
031 | 00031C | 8B46FE | MOV | ax,[bp-2] |
032 | 00031F | D1E0 | SHL | ax,1 |
033 | 000321 | 03D8 | ADD | bx,ax |
034 | 000323 | 8B07 | MOV | ax,[bx] |
035 | 000325 | 50 | PUSH | ax |
036 | 000326 | 8B46FE | MOV | ax,[bp-2] |
037 | 000329 | BA0A00 | MOV | dx,0Ah |
038 | 00032C | F7E2 | MUL | dx |
039 | 00032E | 8BD8 | MOV | bx,ax |
040 | 000330 | 035E06 | ADD | bx,[bp+6] |
041 | 000333 | 8BC7 | MOV | ax,di |
042 | 000335 | D1E0 | SHL | ax,1 |
043 | 000337 | 03D8 | ADD | bx,ax |
044 | 000339 | 58 | POP | ax |
045 | 00033A | F727 | MUL | word ptr [bx] |
046 | 00033C | 50 | PUSH | ax |
047 | 00033D | 8BC6 | MOV | ax,si |
048 | 00033F | BA0A00 | MOV | dx,0Ah |
049 | 000342 | F7E2 | MUL | dx |
050 | 000344 | 8BD8 | MOV | bx,ax |
051 | 000346 | 035E08 | ADD | bx,[bp+8] |
052 | 000349 | 8BC7 | MOV | ax,di |
053 | 00034B | D1E0 | SHL | ax,1 |
054 | 00034D | 03D8 | ADD | bx,ax |
055 | 00034F | 58 | POP | ax |
056 | 000350 | 0307 | ADD | ax,[bx] |
057 | 000352 | 50 | PUSH | ax |
058 | 000353 | 8BC6 | MOV | ax,si |
059 | 000355 | BA0A00 | MOV | dx,0Ah |
060 | 000358 | F7E2 | MUL | dx |
061 | 00035A | 8BD8 | MOV | bx,ax |
062 | 00035C | 035E08 | ADD | bx,[bp+8] |
063 | 00035F | 8BC7 | MOV | ax,di |
064 | 000361 | D1E0 | SHL | ax,1 |
065 | 000363 | 03D8 | ADD | bx,ax |
066 | 000365 | 58 | POP | ax |
067 | 000366 | 8907 | MOV | [bx],ax |
068 | 000368 | FF46FE | INC | word ptr [bp-2] |
069 |  |  | JMP | L5 | ;Synthetic inst
 | proc_1 | ENDP |  |  |
 | main | PROC NEAR |  |  |
000 | 000383 | 55 | PUSH | bp |
001 | 000384 | 8BEC | MOV | bp,sp |
002 | 000386 | 83EC78 | SUB | sp,78h |
003 | 000389 | 8D46D8 | LEA | ax,[bp-28h] |
004 | 00038C | 50 | PUSH | ax |
005 | 00038D | 8D46B0 | LEA | ax,[bp-50h] |
006 | 000390 | 50 | PUSH | ax |
007 | 000391 | 8D4688 | LEA | ax,[bp-78h] |
008 | 000394 | 50 | PUSH | ax |
009 | 000395 | E862FF | CALL | near ptr proc_1 |
010 | 000398 | 83C406 | ADD | sp,6 |
011 | 00039B | 8BE5 | MOV | sp,bp |
012 | 00039D | 5D | POP | bp |
013 | 00039E | C3 | RET |  |
 | main | ENDP |  |  |

图 9-51: Matrixmu.a2

/*

* Input file : matrixmu.exe

* File type : EXE

*/

#include "dcc.h"

void proc_1 (int arg0, int arg1, int arg2)

/* Takes 6 bytes of parameters.

* High-level language prologue code.

* C calling convention.

*/

{

int loc1;

int loc2;

int loc3;

loc2 = 0;

while ((loc2 < 5)) {

loc3 = 0;

while ((loc3 < 4)) {

loc1 = 0;

while ((loc1 < 4)) {

*((((loc2 * 10) + arg2) + (loc3 << 1))) =

((*((((loc2 << 3) + arg0) + (loc1 << 1))) *

*((((loc1 * 10) + arg1) + (loc3 << 1)))) +

*((((loc2 * 10) + arg2) + (loc3 << 1))));

loc1 = (loc1 + 1);

}

loc3 = (loc3 + 1);

}

loc2 = (loc2 + 1);

}

}

void main ()

/* Takes no parameters.

* High-level language prologue code.

*/

{

int loc1;

int loc2;

int loc3;

proc_1 (&loc3, &loc2, &loc1);

}

图 9-52: Matrixmu.b

#define n 5

#define m 4

static void multMatrix (int a[n][m], int b[m][n], int c[n][n])

{ int i,j,k;

for (i=0; i<n; i++)

for (j=0; j<m; j++)

for (k=0; k<m; k++)

c[i][j] = a[i][k] * b[k][j] + c[i][j];

}

main()

{ int a[n][m], b[n][m], c[n][m];

multMatrix (a, b, c);

}

图 9-53: Matrixmu.c

子程序 | 低级 | 高级 | % 缩减率
proc_1 | 70 | 10 | 85.71
main | 14 | 1 | 92.86
合计 | 84 | 11 | 86.90

图 9-54: Matrixmu Statistics


第 9 章 dcc - 结果汇总

9.7.11 结果汇总

前面几节10个程序的统计结果在图9-55中给出。低级中间指令的总数是963，与最后的306个高级指令相比，指令的缩减比例是76.25%。指令的缩减主要归功于数据流分析期间所做的最优化，尤其是扩展的寄存器拷贝传播 (第5章第5.4.10节)。在低级代码中成语的识别减少了指令的数目，而且帮助数据类型的确定比如长整型。反编译的程序除了拥有全部相同数目的用户子程序，还加上该程序中使用的任何运行时支持例程。其中后者这些例程有时可以被翻译成一个高级表示法；要是无法翻译的时候就为它们产生汇编程序。

程序 | 低级 | 高级 | % 缩减率
intops | 45 | 10 | 77.78
byteops | 58 | 10 | 82.76
longops | 117 | 48 | 58.97
benchsho | 101 | 25 | 75.25
benchlng | 139 | 28 | 79.86
benchmul | 88 | 12 | 86.36
benchfn | 82 | 36 | 56.10
fibo | 78 | 15 | 80.77
crc | 171 | 38 | 77.78
matrixmu | 84 | 11 | 86.90
合计 | 963 | 306 | 76.25

图 9-55: 所测试程序的结果


第 10 章 结论

这篇论文论述了二进制程序的逆向编译或反编译技术，并且给出反编译器各个不同阶段的实现算法。而且，在DOS和Unix下运行测试用该方法学实现的一个反编译器样机dcc。

反编译器应用和编译器相似的原理和技术。一个反编译器有七个不同阶段，相当于混合编译器和优化阶段。由于源机器语言的简单性，它没有词汇分析阶段。语法分析阶段解析做为源输入的二进制程序，区分代码和数据，并且把数据引用存放在符号表。区分代码和数据的主要困难是它们在冯·诺依曼机器中以相同的方式表现。中间代码生成阶段产生该程序的低级中间表示法。语义分析阶段检查低级指令组(成语)的语义含义，收集类型信息，并且向整个中间表示法传播这些信息。控制流向图生成阶段产生该程序每个子程序的控制流向图，并且把中间表示法信息附加到图的结点上。数据流分析阶段分析低级中间代码，并且把它转换成一个任何高级语言均可接受的高级中间表示法。指令转换清除掉所有低级引用之条件码和寄存器，并且引进高级的表达式概念。标示出无法用高级语言表现的子程序。在控制流分析阶段分析程序的结构，这个阶段把程序中每个子程序的控制流图结构化。最后，代码生成阶段在高级中间表示法和每个子程序的结构化图的基础上生成高级代码。

若要完整地反编译一个程序，不仅要使用反编译器也要使用其它有关工具：装载器、签名生成器、原型生成器、反汇编器和后期处理器。装载器把做为源输入的二进制程序装入内存，签名生成器为已知的编译器和它们的库生成签名(如果需要的话)，原型生成器确定库子程序形式参数的类型，反汇编器语法分析该程序并且产生一个汇编输出文件，反编译器利用签名信息以减少需要反编译的子程序数目 (即，反编译器不尝试反编译已通过签名或装载器得到识别的库例程)，后期处理器把反编译输出的高级程序转换成一个语义等价的程序——使用目标语言明确的控制结构。实际上，反编译器可以把一个二进制程序或者一个汇编程序作为输入，并产生一个高级语言程序输出。大多数反编译器文献使用后一种方法：用一个汇编语言源程序。这篇论文专注于把二进制程序作为源输入，而它提供的信息远比汇编程序少得多。

在这篇论文中所描述的技术一般来讲足够为不同的机器体系结构建造反编译器。几个阶段被组合成3个不同模块，将机器和语言依赖的特征分离：前端是机器依赖的模块，它对作为源输入的二进制程序做语法分析，并且产生该程序的低级中间表示法以及每个子程序的控制流图；通用的反编译机器是一个机器和语言独立的模块，它分析中间代码和图的结构，并且生成该程序的一个高级中间表示法和结构化的图；后端是目标语言依赖的模块，它从中间表示法和图的结构生成高级目标代码。这样，如果要给一个不同的机器建造一个反编译器只须为该机器编写一个新的前端即可，而如果要为一个不同的目标高级语言做一个反编译器也可以通过为该目标语言编写一个新的后端来实现。这个方法实际上会由于所选择的低级中间语言表示法而受到一定限制。

这篇论文的重要贡献是在通用的反编译机器中所进行的两类分析：数据流分析和控制流分析，它们把低级中间代码(类似机器码)转换成一个高级中间表示法(类似HLL)。数据流分析程序描述了基于编译器优化原理的优化技术——清除低级概念之条件码和寄存器，而且引进高级概念之表达式。这些技术包括有子过程间分析、寄存器泄漏和类型传播。控制流分析程序描述了用于确定程序隐含的高级控制结构的结构化算法。这些算法根据一组预先定义的、一般化的控制结构 (适用于大多数常用语言) 把图结构化。

本文所提出的这些技术在反编译器样机dcc中的实现充分说明其可行性。dcc是一个以DOS操作系统和Intel i80286机器体系结构为目标的反编译器，它生成目标C语言程序。这个反编译器在Unix操作系统下的DecStation 3100机器上以及在DOS下的Intel机器上运行。dcc利用编译器和库的签名识别，只反编译用户例程(如果可以反编译的话)，不反编译那些编译器启动例程和库例程。如果编译器签名没有得到确定，那么在源二进制程序中所有的子程序都要被反编译；有一些库例程和编译器启动例程无法被翻译成高级语言表示法，故而仅仅被反汇编。dcc为每个子程序提供注解，而且可以通过命令开关让它生成该程序的位映像图、调用图、汇编文件输出、在每个子程序中高级指令和低级指令的数目统计、以及关于每个子程序的控制流图的信息。

反编译主要在计算机科学的两个领域中使用：软件维护与安全。在软件维护方面，反编译器被用来重新获得丢失的或无法获取的源程序代码，把用陈旧过时语言编写的一个代码翻译成一个更新的语言，把一个非结构化方式编写的旧代码 (即spaghetti code) 转化成结构化方式，把应用程序移植到一个新的硬件平台，以及调试已知有bug的二进制程序。在安全性方面，反编译器被用来为安全关键的系统验证二进制程序和编译器生成的代码的正确性；这里不能完全信赖编译器一定会为你生成正确的代码；以及检查有无恶意代码，比如病毒。

反编译研究的未来工作可以在两个领域中进行：代码与数据的区分，和数据类型的确定，比如数组、记录和指针。前一领域需要一个强健的方法来确定n路分支语句(即，变址的跳转)和间接子程序调用。后一领域需要用启发式方法来识别不同的复合数据类型并且传播它们的数值。这些算法的有效实现将会让我们的反编译器跑得更快，尽管在通常情况下一个程序只须被反编译一次，因此对反编译速度的要求并不是十分关键。


附录 A i8086 - i80286 体系结构

附录 A

i8086 - i80286 体系结构

Intel iAPX 8086, 8088, 80186 和 80286 机器体系结构有相同的寄存器类型、内存储器结构和输入/输出端口结构 [Int86,Int87]。这些体系结构是向下兼容的，因此80286支持被8086结构所支持的全部机器指令。这些16位字长机器的寄存器按照其用途分成五组不同类别：数据、指针、索引、控制和段寄存器；这个分类见图A-1所示。

类型 | 寄存器 | 功能
数据 | ax | 累加器
 | bx | 在某些寻址模式中作为基址寄存器
 | cx | 计数器
 | dx | 一般用途
指针 | sp | 堆栈指针
 | bp | 基址指针
索引 | si | 源
 | di | 目的
控制 | ip | 指令指针
 | flags | 标志或状态字
段 | cs | 代码段
 | ds | 数据段
 | ss | 堆栈段
 | es | 附加段

图 A-1: 寄存器分类

数据寄存器或通用寄存器能够被当作字寄存器或字节寄存器来存取。每个寄存器有一个高字节和一个低字节，遵循以下命名约定：用一个h代替寄存器名字中的x表示存取该寄存器的高字节；用一个l代替寄存器名字中的x表示存取该寄存器的低字节。标志寄存器是一个专用寄存器，它保持跟踪由各种不同指令设置的条件码。这个寄存器的结构见图A-2所示。从中可见，并非所有的位都被使用，未使用的位被Intel保留。

内存的结构是一个8位字节(8-bit bytes)数组，按照小头约定进行存储 (little-endian 即，一个字中最重要的字节存放在最高的内存地址上)。内存被划分成几个段存储体，每个段是一个线性的64K字节序列；因此内存寻址是通过一对的段和偏移量。

图 A-2: 标志寄存器的结构

输入/输出端口结构由最多64Kb的8位端口或32Kb的16位端口组成，位于一个和内存空间分开的地址空间里。

A.1 指令格式

一个80286指令的长度从1字节到6字节不等。操作码有两种类型：单字节操作码和复合操作码。单字节操作码用一个指令的第一个字节保存操作码，其后依次是域字节、最多两字节的位移、和最多两字节的数据。域字节包含关于寄存器、立即操作数以及位移数据的信息。复合操作码在指令的第一个字节中存放操作码的一部分，而其余部分放在该指令第二个字节其中的3位(见图A-3)。第一个字节决定指令属于哪一个组表(group table)，而第二个字节的3位操作码决定在该表中的索引 (即，表中有8项)。第二个字节的其余位被用作域字节。指令其它部分的结构跟单字节操作码一样[LG86]。

图 A-3: 复合操作码的第2字节

在80286 中，几乎所有的字节组合都是有效的操作码。有229个单字节操作码，29个复合操作码和6个前缀指令。机器语言指令、助记符和操作数的一个完整列表在A.2节。

域字节被用来计算操作数的有效地址(EA)。这个字节由3个字段组成：reg字段三位决定采取哪一个寄存器的值，r/m字段三位指示第二寄存器或者内存操作数，mod字段两位决定位移字节数(DISP)，无论r/m被当作一个寄存器或内存操作数来使用，或者指令的有效地址既不是变址的也不是基址变址的。这个字节的结构见图A-4所示。一个解释域字节的算法见图A-5所示。

图 A-4: 域字节

case (mod) of {

0: if (r/m == 6) /* get 2 bytes displacement */

EA = dispHi:dispLo;

else /* no extra bytes */

DISP = 0;

1: /* get 1 byte displacement */

DISP = dispLo sign-extended to 16 bits;

2: /* get 2 bytes displacement */

DISP = dispHi:dispLo;

3: /* Indexed */

r/m is treated as a register field;

}

图 A-5: 解释域字节的算法

变址和基址变址操作数的EA是根据r/m字段来计算；每个值被映射为一个变址寄存器、或者是一个变址寄存器与基址寄存器的组合，如图A-6所示。

r/m的值 | 变址寄存器(组)
0 | bx + si
1 | bx + di
2 | bp + si
3 | bp + di
4 | si
5 | di
6 | bp
7 | bx

图 A-6: r/m字段的映射

最后的有效地址是位移(DISP)和由r/m位指定的寄存器(组)的相加计算结果。

mod值与r/m值的每一个组合使用一个默认的段寄存器来定址，这些默认段见图A-7所示。一个操作数的有效地址是由mod字段与r/m字段共同决定的，但最后的实际地址是EA再加上乘以16的默认段寄存器的内容。通常，当使用bp寄存器的时候，默认段是ss，否则默认段是ds。

mod r / m | 0 | 1 | 2
0 | DS | DS | DS
1 | DS | DS | DS
2 | SS | SS | SS
3 | SS | SS | SS
4 | DS | DS | DS
5 | DS | DS | DS
6 | DS | SS | SS
7 | DS | DS | DS

图 A-7: 默认段

段覆盖前缀是一个单字节操作码，允许下一个指令例外地不使用默认的段寄存器(即，它只对跟随它后面的那一个指令是有效的)。段由这个前缀字节的两个位域(位3和位4)确定。其余所有域是固定值，如图A-8示例。

图 A-8: 段覆盖前缀

重复前缀操作码有两个：repne和repe。当寄存器cx不等于0或等于0的时候，这些操作码重复下一个指令的执行。它们通常与字符串指令一起使用，比如movs和ins，以字符串未结束作为重复条件。

A.2 指令集

i80286指令集依照机器操作码、汇编助记符和汇编操作数来描述指令，遵循下列约定：

l reg8: 8位寄存器。

l reg16: 16位寄存器。

l mem8: 8位内存数值。

l mem16: 16位内存数值。

l immed8: 8位立即数值。

l immed16: 16位立即数值。

l immed32: 32位立即数值。

l segReg: 16位段寄存器。

图A-9显示全部的单字节操作码。复合操作码通过一个表的索引参照，每个表有8个可能值。这些表在图A-10、图A-11、图A-12和图A-13中显示。它们是摘录自参考文献[Int86,Int87]中的图。

机器操作码 | 汇编助记符和操作数
00 | ADD reg8/mem8,reg8
01 | ADD reg16/mem16,reg16
02 | ADD reg8,reg8/mem8
03 | ADD reg16,reg16/mem16
04 | ADD AL,immed8
05 | ADD AX,immed16
06 | PUSH es
07 | POP es
08 | OR reg8/mem8,reg8
09 | OR reg16/mem16,reg16
0A | OR reg8,reg8/mem8
0B | OR reg16,reg16/mem16
0C | OR al,immed8
0D | OR ax,immed16
0E | PUSH cs
0F | Not used
10 | ADC reg8/mem8,reg8
11 | ADC reg16/mem16,reg16
12 | ADC reg8,reg8/mem8
13 | ADC reg16,reg16/mem16
14 | ADC al,immed8
15 | ADC ax,immed16
16 | PUSH ss
17 | POP ss
18 | SBB reg8/mem8,reg8
19 | SBB reg16/mem16,reg16
1A | SBB reg8,reg8/mem8
1B | SBB reg16,reg16/mem16
1C | SBB al,immed8
1D | SBB ax,immed16
1E | PUSH ds
1F | POP ds
20 | AND reg8/mem8,reg8
21 | AND reg16/mem16,reg16
22 | AND reg8,reg8/mem8
23 | AND reg16,reg16/mem16
24 | AND al,immed8
25 | AND ax,immed16
26 | Segment override
27 | DAA
28 | SUB reg8/mem8,reg8
29 | SUB reg16/mem16,reg16
2A | SUB reg8,reg8/mem8
2B | SUB reg16,reg16/mem16
2C | SUB al,immed8
2D | SUB ax,immed16
2E | Segment override
2F | DAS
30 | XOR reg8/mem8,reg8
31 | XOR reg16/mem16,reg16
32 | XOR reg8,reg8/mem8
33 | XOR reg16,reg16/mem16
34 | XOR al,immed8
35 | XOR ax,immed16
36 | Segment override
37 | AAA
38 | CMP reg8/mem8,reg8
39 | CMP reg16/mem16,reg16
3A | CMP reg8,reg8/mem8
3B | CMP reg16,reg16/mem16
3C | CMP al,immed8
3D | CMP ax,immed16
3E | Segment override
3F | AAS
40 | INC ax
41 | INC cx
42 | INC dx
43 | INC bx
44 | INC sp
45 | INC bp
46 | INC si
47 | INC di
48 | DEC ax
49 | DEC cx
4A | DEC dx
4B | DEC bx
4C | DEC sp
4D | DEC bp
4E | DEC si
4F | DEC di
50 | PUSH ax
51 | PUSH cx
52 | PUSH dx
53 | PUSH bx
54 | PUSH sp
55 | PUSH bp
56 | PUSH si
57 | PUSH di
58 | POP ax
59 | POP cx
5A | POP dx
5B | POP bx
5C | POP sp
5D | POP bp
5E | POP si
5F | POP di
60 | PUSHA
61 | POPA
62 | BOUND reg16/mem16,reg16
63 | Not used
64 | Not used
65 | Not used
66 | Not used
67 | Not used
68 | PUSH immed16
69 | IMUL reg16/mem16,immed16
6A | PUSH immed8
6B | IMUL reg8/mem8,immed8
6C | INSB
6D | INSW
6E | OUTSB
6F | OUTSW
70 | JO immed8
71 | JNO immed8
72 | JB immed8
73 | JNB immed8
74 | JZ immed8
75 | JNZ immed8
76 | JBE immed8
77 | JA immed8
78 | JS immed8
79 | JNS immed8
7A | JP immed8
7B | JNP immed8
7C | JL immed8
7D | JNL immed8
7E | JLE immed8
7F | JG immed8
80 | Table2 reg8
81 | Table2 reg16
82 | Table2 reg8
83 | Table2 reg8, reg16
84 | TEST reg8/mem8,reg8
85 | TEST reg16/mem16,reg16
86 | XCHG reg8,reg8
87 | XCHG reg16,reg16
88 | MOV reg8/mem8,reg8
89 | MOV reg16/mem16,reg16
8A | MOV reg8,reg8/mem8
8B | MOV reg16,reg16/mem16
8C | MOV reg16/mem16,segReg
8D | LEA reg16,reg16/mem16
8E | MOV segReg,reg16/mem16
8F | POP reg16/mem16
90 | NOP
91 | XCHG ax,cx
92 | XCHG ax,dx
93 | XCHG ax,bx
94 | XCHG ax,sp
95 | XCHG ax,bp
96 | XCHG ax,si
97 | XCHG ax,di
98 | CBW 99CWD
9A | CALL immed32
9B | WAIT
9C | PUSHF
9D | POPF
9E | SAHF
9F | LAHF
A0 | MOV al,[mem8]
A1 | MOV ax,[mem16]
A2 | MOV [mem8],al
A3 | MOV [mem16],ax
A4 | MOVSB
A5 | MOVSW
A6 | CMPSB
A7 | CMPSW
A8 | TEST al,[mem8]
A9 | TEST ax,[mem16]
AA | STOSB
AB | STOSW
AC | LODSB
AD | LODSW
AE | SCASB
AF | SCASW
B0 | MOV al,immed8
B1 | MOV cl,immed8
B2 | MOV dl,immed8
B3 | MOV bl,immed8
B4 | MOV ah,immed8
B5 | MOV ch,immed8
B6 | MOV dh,immed8
B7 | MOV bh,immed8
B8 | MOV ax,immed16
B9 | MOV cx,immed16
BA | MOV dx,immed16
BB | MOV bx,immed16
BC | MOV sp,immed16
BD | MOV bp,immed16
BE | MOV si,immed16
BF | MOV di,immed16
C0 | Table1 reg8
C1 | Table1 reg8, reg16
C2 | RET immed16
C3 | RET
C4 | LES reg16/mem16,mem16
C5 | LDS reg16/mem16,mem16
C6 | MOV reg8/mem8,immed8
C7 | MOV reg16/mem16,immed16
C8 | ENTER immed16, immed8
C9 | LEAVE
CA | RET immed16
CB | RET
CC | INT 3
CD | INT immed8
CE | INTO
CF | IRET
D0 | Table1 reg8
D1 | Table1 reg16
D2 | Table1 reg8
D3 | Table1 reg16
D4 | AAM
D5 | AAD
D6 | Not used
D7 | XLAT [bx]
D8 | ESC immed8
D9 | ESC immed8
DA | ESC immed8
DB | ESC immed8
DC | ESC immed8
DD | ESC immed8
DE | ESC immed8
DF | ESC immed8
E0 | LOOPNE immed8
E1 | LOOPE immed8
E2 | LOOP immed8
E3 | JCXZ immed8
E4 | IN al,immed8
E5 | IN ax,immed16
E6 | OUT al,immed8
E7 | OUT ax,immed16
E8 | CALL immed16
E9 | JMP immed16
EA | JMP immed32
EB | JMP immed8
EC | IN al,dx
ED | IN ax,dx
EE | OUT al,dx
EF | OUT ax,dx
F0 | LOCK
F1 | Not used
F2 | REPNE
F3 | REP
F4 | HLT
F5 | CMC
F6 | Table3 reg8
F7 | Table3 reg16
F8 | CLC
F9 | STC
FA | CLI
FB | STI
FC | CLD
FD | STD
FE | Table4 reg8
FF | Table4 reg16

图 A-9: 单字节操作码

索引 | 汇编助记符
0 | ROL
1 | ROR
2 | RCL
3 | RCR
4 | SHL
5 | SHR
6 | Not used
7 | SAR

图 A-10: 表1操作码

索引 | 汇编助记符
0 | ADD
1 | OR
2 | ADC
3 | SBB
4 | AND
5 | SUB
6 | XOR
7 | CMP

图 A-11: 表2操作码

索引 | 汇编助记符
0 | TEST
1 | Not used
2 | NOT
3 | NEG
4 | MUL
5 | IMUL
6 | DIV
7 | IDIV

图 A-12: 表3操作码

索引 | 汇编助记符
0 | INC
1 | DEC
2 | CALL
3 | CALL
4 | JMP
5 | JMP
6 | PUSH
7 | Not used

图 A-13: 表4操作码


附录 B 程序段前缀

附录 B

程序段前缀

程序段前缀或PSP是一个256字节块的信息，显然有CP/M操作系统的余风，以便把CP/M程序移植到DOS环境 [Dun88b]。当一个程序被装入内存的时候，在该分配的内存块开头256字节上建立PSP。PSP的域见图B-1所示。

段偏移量 | 描述
00h | 终止向量：中断20h (移植到DOS)
02h | 最后分配的段
04h | 保留
05h | 调用向量功能：对DOS的功能请求处理者的远调用
0Ah | 程序终止处理者向量的继承拷贝
0Eh | Ctrl-C / Ctrl-Break处理者向量的继承拷贝
12h | 标准错误处理者向量的继承拷贝
16H | 保留
2Ch | DOS环境的第一个节(paragraph)的地址
2Eh | 保留
50h | 中断21h, 返回远(retf)指令
53h | 保留
5Ch | 命令行中第一个参数
6Ch | 命令行中第二个参数
80h | 命令尾；用作缓冲区

图 B-1: PSP的域

终止向量(PSP的偏移00h)相当于过去在CP/M下的热启动/终止(WBOOT)向量。调用向量功能(PSP的偏移05h)相当于过去在CP/M下的基本磁盘操作系统(BDOS)向量。


附录 C 可执行文件格式

附录 C

可执行文件格式

DOS操作系统支持两种不同类型的可执行文件：.exe文件和.com文件。前者允许大的程序和在内存中使用多个段，后者用于安装到一个段里面(即，最大64Kb)的小程序 [Dun88b]。

C.1 .exe文件

.exe文件由一个头部和一个装入模块组成，如图C-1所示。文件头部有28字节的固定格式区和一个大小可变的重定位表。装入模块是一个完全链接(fully linked)的程序映像；没有关于如何从模块中分割段的信息，因为DOS不管程序如何分段。

图 C-1: .exe文件的结构

头部格式区的结构见图C-2所示。一页的大小是512字节，一节(paragraph)的大小是16字节。程序映像的大小用格式区的值计算出来，即文件大小和头部大小之差。文件大小由文件页数(只入不舍)和最后一页所用字节尺寸给出。

重定位表是一个字指针列表，每个指针指向装入模块里面需要调整的一个字。对这些字的调整就是把它们加上该程序要被装入到的起始段地址。这个表里的指针被储存为相对装入模块的两个字。

字节 | 描述
00-01h | .exe签名(4Dh, 5Ah)
02-03h | 在最后页中的字节数
04-05h | 页数(只入不舍)
06-07h | 在重定位表中的项目数
08-09h | 在头部中的节数
0A-0Bh | 数据和堆栈要求的最小节数
0C-0Dh | 最大内存节数
0E-0Fh | 预先重定位的最初ss值
10-11h | 最初的sp值(绝对数值)
12-13h | 补足的校验和(二进制反码)
14-15h | 最初的ip值
16-17h | 预先重定位的cs最初值
18-19h | 重定位表偏移量
1A-1B | 覆盖数(默认：0000h)

图 C-2: 固定格式区

C.2 .com文件

.com文件是一个没有头部的映像程序(即，相当于一个.exe文件的装入模块)，因此该程序照“原样”被装入内存。与.exe程序相比，.com程序只能使用一个段(最大64Kb)。这些程序被设计用于把CP/M程序移植到DOS环境。


附录 D 低级Icode到高级Icode的映射

附录 D

低级Icode到高级Icode的映射

在低级和高级Icode (中间代码)之间的映射见图D-1所示。在高级Icode栏中，一个短划线(-)表示低级Icode没有与之对应的高级Icode，一个星号(*)表示仅当该低级Icode在一个成语中的时候是某一个高级指令的组成部分，一个f表示一个Icode标志被设置而且该指令不再被考虑，一个cc表示该低级指令设置一个条件码，它没有对应的高级中间代码，而且将通过条件码传播把它去除，一个n表示该低级Icode指令在这个分析中不予考虑。被标记一个n的指令涉及处理机器字符串指令，dcc执行这个分析时对此不作考虑。

最初的低级到高级的Icode映射是用寄存器表达。对Icode做更进一步的数据流分析把这些指令转换成不使用临时寄存器的表达式，只使用变量和寄存器变量(如果有的话)。

低级Icode | 高级Icode
iAAA | -
iAAD | -
iAAM | -
iAAS | -
iADC | *
iADD | asgn(+)
iAND | asgn(&)
iBOUND | f
iCALL | call
iCALLF | call
iCLC | cc
iCLD | cc
iCLI | -
iCMC | cc
iCMP | cc
iCMPS | n
iREPNE_CMPS | n
iREPE_CMPS | n
iDAA | -
iDAS | -
iDEC | asgn(- 1)
iDIV | asgn(/)
iENTER | f
iESC | f
iHLT | -
iIDIV | asgn(/)
iIMUL | asgn(*)
iIN | -
iINC | asgn(+ 1)
iINS | -
iREP_INS | -
iINT | -
iINTO | -
iIRET | -
iJB | jcond(<)
iJBE | jcond(<=)
iJAE | jcond(>=)
iJA | jcond(>)
iJE | jcond(==)
iJNE | jcond(<>)
iJL | jcond(<)
iJGE | jcond(>=)
iJLE | jcond(<=)
iJG | jcond(>)
iJS | jcond(> 0)
iJNS | jcond(< 0)
iJO | -
iJNO | -
iJP | -
iJNP | -
iJCXZ | jcond(cx == 0)
iJNCXZ | jcond(cx <> 0)
iJMP | jmp
iJMPF | jmp
iLAHF | -
iLDS | asgn(far pointer)
iLEA | asgn(near pointer)
iLEAVE | ret
iLES | asgn(far pointer)
iLOCK | -
iLODS | n
iREP_LODS | n
iMOV | asgn(=)
iMOVS | n
iREP_MOVS | n
iMOD | asgn(%)
iMUL | asgn(*)
iNEG | asgn(-)
iNOT | !
iNOP | -
iOR | asgn(|)
iOUT | -
iOUTS | -
iREP_OUTS | -
iPOP | pop
iPOPA | -
iPOPF | -
iPUSH | push
iPUSHA | -
iPUSHF | -
iRCL | *
iRCR | *
iREPE | n
iREPNE | n
iRET | ret
iRETF | ret
iROL | *
iROR | *
iSAHF | -
iSAR | *
iSHL | asgn(<<)
iSHR | asgn(>>)
iSBB | *
iSCAS | n
iREPNE_SCAS | n
iREPE_SCAS | n
iSIGNEX | asgn(=)
iSTC | cc
iSTD | cc
iSTI | -
iSTOS | n
iREP_STOS | n
iSUB | asgn(-)
iTEST | cc
iWAIT | f
iXCHG | asgn(uses tmp)
iXLAT | -
iXOR | asgn(^)

图 D-1: Icode操作码


附录 E dcc显示的注解和错误信息

附录 E

dcc显示的注解和错误信息

dcc在输出的C语言文件和汇编语言文件中加入一系列注解，是在每一个子程序的分析期间所收集的信息。这个信息在每个子程序前面显示。dcc支持下列注解：

l "Takes %d bytes of parameters."

参数共计 %d 字节。

l "Uses register arguments:" (and lists the registers and the formal argument name).

使用寄存器参数：(而且列出寄存器和形式参数名字)。

l "Takes no parameters."

没有参数。

l "Runtime support routine of the compiler."

编译器的运行时支持例程。

l "High-level language prologue code."

高级语言的序言代码。

l "Untranslatable routine. Assembler provided."

无法翻译的例程。只提供汇编代码。

l "Return value in register %s." (register(s) provided).

在寄存器 %s 中返回数值。(给出寄存器(组))

l "Pascal calling convention."

Pascal语言调用约定。

l "C calling convention."

C语言调用约定。

l "Unknown calling convention."

未知调用约定。

l "Incomplete due to an untranslatable opcode"

由于一个无法翻译的操作码而未完成

l "Incomplete due to an indirect jump"

由于一个间接跳转而未完成

l "Indirect call procedure."

间接调用子过程。

l "Contains self-modifying code."

包含自修改代码。

l "Contains coprocessor instructions."

包含协处理器指令。

l "Irreducible control flow graph."

不可归约的控制流向图。

汇编子程序连同所有的DOS内核服务也被加注；中断20h到2Fh。附录F含有一个dcc支持的所有DOS中断列表。

dcc还显示两种不同的错误类型：致命错误和非致命错误。致命错误终止dcc的运行，显示的错误信息足够让使用者了解已发生的事。非致命错误不导致dcc终止，作为给使用者的警告。

dcc支持的致命错误是：

l "Invalid option -%c."

无效选项 -%c。

l "Usage: dcc [-a1a2mpsvV][-o asmfile] DOS_executable"

用法：dcc [-a1a2mpsvV][-o asmfile] DOS_executable

l "New EXE format not supported."

新的EXE格式不被支持。

l "Cannot open file %s."

无法打开文件 %s。

l "Error while reading file %s."

在读文件时出错 %s。

l "Invalid instruction %02X at location %06lX."

无效指令%02X在%06lX位置上。

l "Don't understand 80386 instruction %02X at location %06lX."

不理解在%06lX位置上的80386指令%02X。

l "Instruction at location %06lX goes beyond loaded image."

在%06lX位置上的指令超出所装入的映像。

l "malloc of %ld bytes failed."

分配%ld字节失败。

l "Failed to find a basic block for jump to %ld in subroutine %s."

没有在子程序%s中发现跳转到%ld的基本块。

l "Basic Block is a synthetic jump."

基本块是一个人工合成的跳转。

l "Failed to find a basic block for interval."

没有发现区间的基本块。

l "Definition not found for condition code usage at opcode %d."

在操作码%d上的条件码使用没有定义。

dcc支持的非致命错误是：

l "Segment override with no memory operand at location %06lX."

在%06lX位置上段覆盖没有内存操作数。

l "REP prefix without a string instruction at location %06lX."

在%06lX位置上REP前缀缺少一个字符串指令。

l "Conditional jump use, definition not supported at opcode %d."

条件跳转使用，在操作码 %d 上定义不被支持

l "Definition-use not supported. Definition opcode = %d, use opcode = %d."

定义-使用不被支持。定义操作码 = %d，使用操作码 = %d。

l "Failed to construct do..while() condition."

没有构造 do..while() 条件。

l "Failed to construct while() condition."

没有构造while()条件。


附录 F DOS中断

附录 F

DOS 中断

DOS内核通过软件中断20h..2Fh为应用程序提供服务。中断21h负责字符输入/输出、文件、记录、目录操作、磁盘、进程、内存管理、网络功能以及其它各种的系统功能；功能号保存在寄存器ah中。图F-1列出由DOS提供的各个不同中断[Dun88a]。在产生一个子程序的反汇编的时候，dcc给这些中断加上注解。

中断 | 功能 | 功能名称
20h |  | 终止进程
21h | 0h | 终止进程
21h | 1h | 带回显的字符输入
21h | 2h | 字符输出
21h | 3h | 异步通讯口输入
21h | 4h | 异步通讯口输出
21h | 5h | 打印机输出
21h | 6h | 直接控制台输入/输出
21h | 7h | 无回显的无过滤字符输入
21h | 8h | 无回显的字符输入
21h | 9h | 显示字符串
21h | Ah | 有缓冲区的键盘输入
21h | Bh | 检查输入状态
21h | Ch | 刷新输入缓冲区然后输入
21h | Dh | 重置磁盘
21h | Eh | 选择磁盘
21h | Fh | 打开文件
21h | 10h | 关闭文件
21h | 11h | 查找第一个文件
21h | 12h | 查找下一个文件
21h | 13h | 删除文件
21h | 14h | 顺序读
21h | 15h | 顺序写
21h | 16h | 创建文件
21h | 17h | 更改文件名
21h | 18h | 保留
21h | 19h | 取得当前磁盘
21h | 1Ah | 设置磁盘传送地址(DTA)
21h | 1Bh | 取得默认磁盘驱动器数据
21h | 1Ch | 取得磁盘驱动器数据
21h | 1Dh | 保留
21h | 1Eh | 保留
21h | 1Fh | 保留
21h | 20h | 保留
21h | 21h | 随机读
21h | 22h | 随机写
21h | 23h | 取得文件大小
21h | 24h | 设置相对记录字段
21h | 25h | 设置中断向量
21h | 26h | 创建新的程序段前缀(PSP)
21h | 27h | 随机块读
21h | 28h | 随机块写
21h | 29h | 解析文件名(即，建立文件控制块FCB)
21h | 2Ah | 取得日期
21h | 2Bh | 设置日期
21h | 2Ch | 取得时间
21h | 2Dh | 设置时间
21h | 2Eh | 设置校验标志
21h | 2Fh | 取得DTA地址
21h | 30h | 取得DOS版本号
21h | 31h | 终止并且驻留
21h | 32h | 保留
21h | 33h | 取得或设置Break标志
21h | 34h | 保留
21h | 35h | 取得中断向量
21h | 36h | 取得磁盘驱动器的分配信息
21h | 37h | 保留
21h | 38h | 取得或设置国家信息
21h | 39h | 创建目录
21h | 3Ah | 删除目录
21h | 3Bh | 设置当前目录
21h | 3Ch | 创建文件
21h | 3Dh | 打开文件
21h | 3Eh | 关闭文件
21h | 3Fh | 读文件或设备
21h | 40h | 写文件或设备
21h | 41h | 删除文件
21h | 42h | 设置文件指针
21h | 43h | 取得或设置文件属性
21h | 44h | IOCTL (输入/输出控制)
21h | 45h | 复制句柄
21h | 46h | 重定向句柄
21h | 47h | 取得当前目录
21h | 48h | 分配内存块
21h | 49h | 释放内存块
21h | 4Ah | 修改已分配的内存块大小
21h | 4Bh | 运行程序(exec)
21h | 4Ch | 终止进程并返回调用者
21h | 4Dh | 取得返回代码
21h | 4Eh | 查找第一个文件
21h | 4Fh | 查找下一个文件
21h | 50h | 保留
21h | 51h | 保留
21h | 52h | 保留
21h | 53h | 保留
21h | 54h | 取得校验标志
21h | 55h | 保留
21h | 56h | 更改文件名
21h | 57h | 取得或设置文件日期和时间
21h | 58h | 取得或设置分配策略
21h | 59h | 取得扩展的错误信息
21h | 5Ah | 创建临时文件
21h | 5Bh | 创建新文件
21h | 5Ch | 锁定或解锁文件区域
21h | 5Dh | 保留
21h | 5Eh | 取得机器名
21h | 5Fh | 设备重定向
21h | 60h | 保留
21h | 61h | 保留
21h | 62h | 取得PSP地址
21h | 63h | 取得DBCS(数据库控制系统)引导字节表
21h | 64h | 保留
21h | 65h | 取得扩展的国家信息
21h | 66h | 取得或设置代码页
21h | 67h | 设置句柄数量
21h | 68h | 提交文件
21h | 69h | 保留
21h | 6Ah | 保留
21h | 6Bh | 保留
21h | 6Ch | 扩展的打开文件
22h |  | 终止处理者的地址
23h |  | Ctrl-C处理者的地址
24h |  | 标准错误处理者的地址
25h |  | 绝对磁盘读取
26h |  | 绝对磁盘写入
27h |  | 终止并且驻留内存
28h |  | 保留
29h |  | 保留
2Ah |  | 保留
2Bh |  | 保留
2Ch |  | 保留
2Dh |  | 保留
2Eh |  | 保留
2Fh | 1h | 打印缓冲池
2Fh | 2h | 重定向盘符
2Fh | 10h | 共享
2Fh | B7h | 设置搜索路径

图 F-1: DOS中断


参考书目

[AC72] | F.E. Allen and J. Cocke. Graph theoretic constructs for program control flow analysis. Technical Report RC 3923 (No. 17789), IBM, Thomas J. Watson Research Center, Yorktown Heights, New York, July 1972.
[AC76] | F.E. Allen and J. Cocke. A program data flow analysis procedure. Communications of the ACM, 19(3):137-147, March 1976.
[All70] | F.E. Allen. Control flow analysis. SIGPLAN Notices, 5(7):1-19, July 1970.
[All72] | F.E. Allen. A basis for program optimization. In Proc. IFIP Congress, pages 385-390, Amsterdam, Holland, 1972. North-Holland Pub.Co.
[All74] | F.E. Allen. Interprocedural data flow analysis. In Proc. IFIP Congress, pages 398-402, Amsterdam, Holland, 1974. North-Holland Pub.Co.
[AM71] | E. Ashcroft and Z. Manna. The translation of 'go to' programs to 'while' programs. Technical report, Stanford University, Department of Computer Science, 1971.
[ASU86a] | A.V. Aho, R. Sethi, and J.D. Ullman. Compilers: Principles, Techniques, and Tools. Addison-Wesley Publishing Company, 1986.
[ASU86b] | A.V. Aho, R. Sethi, and J.D. Ullman. Compilers: Principles, Techniques, and Tools, chapter 10, pages 585-722. Addison-Wesley Publishing Company, 1986.
[Bak77] | B.S. Baker. An algorithm for structuring flowgraphs. Journal of the ACM, 24(1):98-120, January 1977.
[Bar74] | P. Barbe. The Piler system of computer program translation. Technical report, Probe Consultants Inc., September 1974.
[BB91] | J. Bowen and P. Breuer. Decompilation techniques. Internal to ESPRIT REDO project no. 2487 2487-TN-PRG-1065 Version 1.2, Oxford University Computing Laboratory, 11 Keble Road, Oxford OX1 3QD, March 1991.
[BB92] | P.T. Breuer and J.P. Bowen. Decompilation: The enumeration of types and grammars. Technical Report PRG-TR-11-92, Oxford University Computing Laboratory, 11 Keble Road, Oxford OX1 3QD, 1992.
[BB93] | P.T. Breuer and J.P. Bowen. Decompilation: the enumeration of types and grammars. To appear in Transaction of Programming Languages and Systems, 1993.
[BB94] | P.T. Breuer and J.P. Bowen. Generating decompilers. To appear in Information and Software Technology Journal, 1994.
[BBL91] | J.P. Bowen, P.T. Breuer, and K.C. Lano. The REDO project: Final report. Technical Report PRG-TR-23-91, Oxford University Computing Laboratory, 11 Keble Road, Oxford OX1 3QD, December 1991.
[BBL93] | J. Bowen, P. Breuer, and K. Lano. A compendium of formal techniques for software maintenance. Software Engineering Journal, pages 253-262, September 1993.
[BJ66] | C. Bohm and G. Jacopini. Flow diagrams, Turing machines and languages with only two formation rules. Communications of the ACM, 9(5):366-371, May 1966.
[Bor92] | Borland. Borland C++ Version 3.1 - User's Guide. Borland International, 1800 Green Hills Road, Scotts Valley, CA 95066, 1992.
[Bow91] | J. Bowen. From programs to object code using logic and logic programming. In R. Giegerich and S.L. Graham, editors, Code Generation - Concepts, Tools, Techniques, Workshops in Computing, pages 173-192, Dagstuhl, Germany, May 1991. Springer Verlag.
[Bow93] | J. Bowen. From programs to object code and back again using logic programming: Compilation and decompilation. Journal of Software Maintenance: Research and Practice, 5(4):205-234, 1993.
[BP79] | D. Balbinot and L. Petrone. Decompilation of Polish code in Basic. Rivista di Informatica, 9(4):329-335, October 1979.
[BP81] | M.N. Bert and L. Petrone. Decompiling context-free languages from their Polish-like representations. pages 35-57, 1981.
[Bri81] | D.L. Brinkley. Intercomputer transportation of assembly language software through decompilation. Technical report, Naval Underwater Systems Center, October 1981.
[Bri93] | Brillo brillig. EXE, 8(5):6, October 1993.
[BZ80] | A.L. Baker and S.H. Zweben. A comparison of measures of control flow complexity. IEEE Transactions on Software Engineering, SE-6(6):506-512, November 1980.
[Cal] | C.A. Calkins. Masterful disassembler. Public domain software. Anonymous ftp oak.oakland.edu:SimTel/msdos/disasm/md86.zip.
[CCHK90] | D. Callahan, A. Carle, M.W. Hall, and K. Kennedy. Constructing the procedure call multigraph. IEEE Transactions on Software Engineering, 16(4):483-487, April 1990.
[CG93] | C. Cifuentes and K.J. Gough. A methodology for decompilation. In Proceedings of the XIX Conferencia Latinoamericana de Informatica, pages 257-266, Buenos Aires, Argentina, 2-6 August 1993. Centro Latinoamericano de Estudios en Informatica.
[CG94] | C. Cifuentes and K.J Gough. Decompilation of binary programs. Technical Report 3/94, Faculty of Information Technology, Queensland University of Technology, GPO Box 2434, Brisbane 4001, Australia, April 1994. (To appear in 1995, Software - Practice & Experience).
[Chr80] | W. Christensen. Resource for the 8086. Public domain software. Anonymous ftp oak.oakland.edu:SimTel/msdos/disasm/res86.zip, 1980. Translated to the 8086 by Larry Etienne.
[Cif93] | C. Cifuentes. A structuring algorithm for decompilation. In Proceedings of the XIX Conferencia Latinoamericana de Informatica, pages 267-276, Buenos Aires, Argentina, 2-6 August 1993. Centro Latinoamericano de Estudios en Informatica.
[Cif94a] | C. Cifuentes. Interprocedural data flow decompilation. Technical Report 4/94, Faculty of Information Technology, Queensland University of Technology, GPO Box 2434, Brisbane 4001, Australia, April 1994. (Submitted to Journal of Programming Languages).
[Cif94b] | C. Cifuentes. Structuring decompiled graphs. Technical Report 5/94, Faculty of Information Technology, Queensland University of Technology, GPO Box 2434, Brisbane 4001, Australia, April 1994. (Submitted to The Computer Journal).
[Cob93] | Cobol decompiler. Midrange Systems, 6(13):66, July 1993.
[Coc70] | J. Cocke. Global common subexpression elimination. SIGPLAN Notices, 5(7):20-25, July 1970.
[Col81] | J.W. Perry Cole. ANSI Fortran IV, A structured programming approach. WM.C.Brown, 2460 Kerper Boulevard, Dubuque, Iowa 52001, 8 edition, 1981.
[Com91] | V Communications. Sourcer - Commenting Disassembler, 8088 to 80486 Instruction Set Support. V Communications, Inc, 4320 Stevens Creek Blvd., Suite 275, San Jose, CA 95129, 1991.
[Coo67] | D.C. Cooper. Bohm and Jacopini's reduction of flow charts. Communications of the ACM, 10(8):463,473, August 1967.
[Coo83] | D. Cooper. Standard Pascal, User Reference Manual. W.W.Norton & Company, 500 Fifth Avenue, New York, N.Y. 10110, 1 edition, 1983.
[Dav58] | M. Davis. Computability and Unsolvability, chapter 5, pages 69-70. McGraw-Hill, 1958.
[DoD83] | American National Standards Institute, Inc., Department of Defense, OUSD(R&E), Washington, DC 20301, USA. Reference Manual for the Ada Programming Language, ANSI/MIL-STD-1815A-1983, February 1983.
[DS82] | L.M. Dorsey and S.Y. Su. The decompilation of COBOL-DML programs for the purpose of program conversion. In Proceedings of COMPSAC 82. IEEE Computer Society's Sixth International Computer Software and Applications Conference, pages 495-504, Chicago, USA, November 1982. IEEE.
[Dud82] | R. Dudley. A recursive decompiler. FORTH Dimensions, 4(2):28, Jul-Aug 1982.
[Dun88a] | R. Duncan. Advanced MSDOS programming. Microsoft Press, 16011 NE 36th Way, Box 97017, Redmond, Washington 98073-9717, 2 edition, 1988.
[Dun88b] | R. Duncan. The MSDOS Encyclopedia, chapter 4, pages 107-147. Microsoft Press, 1988.
[DZ89] | D.M. Dejean and G.W. Zobrist. A definition optimization technique used in a code translation algorithm. Communications of the ACM, 32(1):94-104, January 1989.
[EH93] | A.M. Erosa and L.J. Hendren. Taming control flow: A structured approach to eliminating goto statements. Technical Report ACAPS Technical Memo 76, School of Computer Science, McGill University, 3480 University St, Montreal, Canada H3A 2A7, September 1993.
[Emm94] | M. Van Emmerik. Signatures for library functions in executable files. Technical Report 2/94, Faculty of Information Technology, Queensland University of Technology, GPO Box 2434, Brisbane 4001, Australia, April 1994.
[FJ88a] | C.N. Fischer and R.J. LeBlanc Jr. Crafting a Compiler. Benjamin Cummings, 2727 Sand Hill Road, Menlo Park, California 94025, 1988.
[FJ88b] | C.N. Fischer and R.J. LeBlanc Jr. Crafting a Compiler, chapter 16, pages 609-680. Benjamin Cummings, 2727 Sand Hill Road, Menlo Park, California 94025, 1988.
[Flu89] | B.H. Flusche. The art of disassembly; getting to the source of the problem when the object's the data. Micro Cornucopia, (46):8-14, March - April 1989.
[Fre91] | P. French. Private communication. Email, 1991.
[Fri74] | F.L. Friedman. Decompilation and the Transfer of Mini-Computer Operating Systems. PhD dissertation, Purdue University, Computer Science, August 1974.
[FZ91] | C. Fuan and L. Zongtian. C function recognition technique and its implementation in 8086 C decompiling system. Mini-Micro Systems, 12(11):33-40,47, 1991.
[FZL93] | C. Fuan, L. Zongtian, and L. Li. Design and implementation techniques of the 8086 C decompiling system. Mini-Micro Systems, 14(4):10-18,31, 1993.
[Gai65] | R.S. Gaines. On the translation of machine language programs. Communications of the ACM, 8(12):737-741, December 1965.
[Gar88] | P.D. Garnett. Selective disassembly: a first step towareds developing a virus filter. Fourth Aerospace Computing Security Appl Conference, pages 2-6, December 1988.
[GCC+92] | K.J. Gough, C. Cifuentes, D. Corney, J. Hynd, and P. Kolb. An experiment in mixed compilation/interpretation. In G.K.Gupta and C.D.Keen, editors, Proceedings of the Fifteenth Australian Computer Science Conference (ACSC-15), pages 315-327, University of Tasmania, Hobart, Australia, 29-31 January 1992. Australian Computer Society.
[GD83] | J. Gersbach and J. Damke. Asmgen, version 2.01. Public domain software. Anonymous ftp oak.oakland.edu:SimTel/msdos/disasm/asmgen3.zip, 1983.
[GL82] | L. Goldschlager and A. Lister. Computer Science: A modern introduction. Prentice-Hall International, 1982.
[Gou88] | K.J. Gough. Syntax Analysis and Software Tools. Addison Wesley Publishing Company, Reading, U.K., 1988.
[Gou93] | K.J. Gough. Private communication, 1993.
[Gut90] | S. Guthery. exe2c. News item in comp.compilers USENET newsgroup, 30 Apr 1990.
[Gut91a] | S. Guthery. exe2c. News item in comp.compilers USENET newsgroup, 23 Apr 1991.
[Gut91b] | S. Guthery. Private communication. Austin Code Works, 11100 Leafwood Lane, Austin, TX 78750-3587, 14 Dec 1991.
[Hal62] | M.H. Halstead. Machine-independent computer programming, chapter 11, pages 143-150. Spartan Books, 1962.
[Hal67] | M.H. Halstead. Machine independence and third generation computers. In Proceedings SJCC (Sprint Joint Computer Conference), pages 587-592, 1967.
[Hal70] | M.H. Halstead. Using the computer for program conversion. Datamation, pages 125-129, May 1970.
[Hec77] | M.S. Hecht. Flow Analysis of Computer Programs. Elsevier North-Holland, Inc, 52 Vanderbilt Avenue, New York, New York 10017, 1977.
[HH73] | B.C. Housel and M.H. Halstead. A methodology for machine language decompilation. Technical Report RJ 1316 (#20557), Purdue University, Department of Computer Science, December 1973.
[HHB+93] | P. Heiser, D. Hanson, C. Berntson, K. Everett, and A. Schwartz. Feedback. Data Based Advisor, 11(6):14-15, June 1993.
[HK92] | M.W. Hall and K. Kennedy. Efficient call graph analysis. Letters on Programming Languages and Systems, 1(3):227-242, September 1992.
[HM79] | R.N. Horspool and N. Marovac. An approach to the problem of detranslation of computer programs. The Computer Journal, 23(3):223-229, 1979.
[HM84] | N.L. Hills and D. Moines. Revisited: Recursive decompiler. FORTH Dimensions, 5(6):16-18, Mar-Apr 1984.
[Hol73] | C.R. Hollander. Decompilation of Object Programs. PhD dissertation, Stanford University, Computer Science, January 1973.
[Hoo91] | S.T. Hood. Decompiling with definite clause grammars. Technical Report ERL-0571-RR, Electronics Research Laboratory, DSTO Australia, PO Box 1600, Salisbury, South Australia 5108, September 1991.
[Hop78] | G.L. Hopwood. Decompilation. PhD dissertation, University of California, Irvine, Computer Science, 1978.
[Hou73] | B.C. Housel. A Study of Decompiling Machine Languages into High-Level Machine Independent Languages. PhD dissertation, Purdue University, Computer Science, August 1973.
[HU72] | M.S. Hecht and J.D. Ullman. Flow graph reducibility. SIAM Journal of Computing, 1(2):188-202, June 1972.
[HU74] | M.S. Hecht and J.D. Ullman. Characterizations of reducible flow graphs. Journal of the ACM, 21(3):367-375, July 1974.
[HU75] | M. Hecht and J. Ullman. A simple algorithm for global data flow analysis problems. SIAM Journal of Computing, 4(4):519-532, December 1975.
[HZY91] | L. Hungmong, L. Zongtian, and Z. Yifen. Design and implementation of the intermediate language in a PC decompiler system. Mini-Micro Systems, 12(2):23-28,46, 1991.
[Int86] | Intel. iAPX 86/88, 186/188 User's Manual. Intel Corporation, 3065 Bowers Avenue, Santa Clara, CA 95051, 1986.
[Int87] | Intel. 80286 and 80287 Programmer's Reference Manual. Intel Corporation, 3065 Bowers Avenue, Santa Clara, CA 95051, 1987.
[KF71] | D.E. Knuth and R.W. Floyd. Notes on avoiding go to statements. Information Processing Letters, 1(1):23-31, 1971.
[Knu74] | D.E. Knuth. Structured programming with go to statements. Computing Surveys, 6(4):261-301, December 1974.
[Kos74] | S.R. Kosaraju. Analysis of structured programs. Journal of Computer and System Sciences, 9(3):232-255, 1974.
[KR88] | B.W. Kernighan and D.M. Ritchie. The C Programming Language. Prentice-Hall, Inc, Englewood Cliffs, N.J., 2 edition, 1988.
[KW82] | R.H. Katz and E. Wong. Decompiling CODASYL DML into relational queries. ACM Transactions on Database Systems, 7(1):1-23, March 1982.
[LG86] | Y.C. Liu and G.A. Gibson. Microcomputer Systems: the 8086/8088 family. Prentice-Hall International, New Jersey, 2 edition, 1986.
[Lic85] | U. Lichtblau. Decompilation of control structures by means of graph transformations. In Proceedings of the International Joint Conference on Theory and Practice of Software Development (TAPSOFT), Berlin, 1985.
[Lic91] | U. Lichtblau. Recognizing rooted context-free flowgraph languages in polynomial time. In G. Rozenberg H. Ehrig, H.J. Kreowski, editor, Graph Grammars and their application to Computer Science, number 532 in Lecture Notes in Computer Science, pages 538-548. Springer-Verlag, 1991.
[LM75] | H.F. Ledgard and M. Marcotty. A genealogy of control structures. Communications of the ACM, 18(11):629-639, November 1975.
[Mak90] | O.J. Makela. Intelligent disassembler, version 1.2. Public domain software. Anonymous ftp oak.oakland.edu:SimTel/msdos/disasm/id12.zip, 1990.
[May88] | W. May. A simple decompiler. Dr.Dobb's Journal, pages 50-52, June 1988.
[Mic87] | Microsoft. Mixed-Language Programming Guide. Microsoft Corporation, 16011 NE 36th Way, Box 97017, Redmond, WA 98073-9717, 1987.
[MZBR85] | W.K. Marshall, G.W. Zobrist, W. Bach, and A. Richardson. A functional mapping for a microprocessor system simulation. In Proceedings of the 1985 IEEE Microprocessor Forum (Atlantic City, N.J., Apr.2-4). IEEE Piscataway, N.J. pp.37-39, 1985.
[Oul82] | G. Oulsnam. Unravelling unstructured programs. The Computer Journal, 25(3):379-387, 1982.
[Out93] | Tools and utilities. DBMS, 6(7):63-92, June 1993.
[PLA91] | Programming Languages and Systems Group - Queensland University of Technology, GPO Box 2434, Brisbane, QLD 4001, Australia. Gardens Point Modula-2, 1991.
[PW93] | D.J. Pavey and L.A. Winsborrow. Demonstrating equivalence of source code and PROM contents. The Computer Language, 36(7):654-667, 1993.
[Ram88] | L. Ramshaw. Eliminating go to's while preserving program structure. Journal of the ACM, 35(4):893-920, October 1988.
[Ray89] | E.S. Raymond. Plum-hall benchmarks. URL: ftp//plaza.aarnet.edu.au/usenet/comp.sources.unix/volume20/plum-benchmarks.gz, 1989.
[Reu88] | J. Reuter. URL: ftp//cs.washington.edu/ pub/decomp.tar.z. Public domain software, 1988.
[Reu91] | J. Reuter. Private communication. Email, 1991.
[Ryd79] | B.G. Ryder. Constructing the call graph of a program. IEEE Transactions on Software Engineering, 5(3):216-226, May 1979.
[Sas66] | W.A. Sassaman. A computer program to translate machine language into fortran. In Proceedings SJCC, pages 235-239, 1966.
[SCK+93] | R.L. Sites, A. Chernoff, M.B. Kirk, M.P. Marks, and S.G. Robinson. Binary translation. Communications of the ACM, 36(2):69-81, February 1993.
[Sha80] | M. Sharir. Structural analysis: A new approach to flow analysis in optimizing compilers. Computer Languages, 5:141-153, 1980.
[Sof88] | RH Factor Software. Bubble chamber installation. Public domain software, beta release. Anonymous ftp oak.oakland.edu:SimTel/msdos/disasm/bubble.zip, 1988.
[SW74] | V. Schneider and G. Winiger. Translation grammars for compilation and decompilation. BIT, 14:78-86, 1974.
[SW93] | A. Srivastava and D.W. Wall. A practical system for intermodule code optimization at link-time. Journal of Programming Languages, 1(1):1-18, March 1993.
[Tar72] | R.E. Tarjan. Depth-first search and linear graph algorithms. SIAM Journal of Computing, 1(2):146-160, June 1972.
[Tar74] | R.E. Tarjan. Testing flow graph reducibility. Journal of Computer and System Sciences, pages 355-365, September 1974.
[Unc93] | Your guide to clipper add-ons. Data Based Advisor, 11(8):47-67, August 1993.
[Val93] | Slicing thru source code. Data Based Advisor, 11(3):28, March 1993.
[WG85] | M.H. Williams and G.Chen. Restructuring pascal programs containing goto statements. The Computer Journal, 28(2):134-137, 1985.
[Wil77] | M.H. Williams. Generating structured flow diagrams: the nature of unstructuredness. The Computer Journal, 20(1):45-50, 1977.
[Wir85] | N. Wirth. Programming in Modula-2. Springer-Verlag, Berlin Heidelberg, 3 edition, 1985.
[WO78] | M.H. Williams and H.L. Ossher. Conversion of unstructured flow diagrams to structured form. The Computer Journal, 21(2):161-167, 1978.
[Wor78] | D.A. Workman. Language design using decompilation. Technical report, University of Central Florida, December 1978.
[Wor91] | Austin Code Works. exe2c.firelease, 1991. Email: info@acw.com.
[Yoo85] | C.W. Yoo. An approach to the transportation of computer software. Information Processing Letters, 21:153-157, September 1985.
[Zan85] | J.R. Van Zandt. Interactive 8086 disassembler, version 2.21. Public domain software. Anonymous ftp oak.oakland.edu:SimTel/msdos/disasm/dis86221.zip, 1985.