首页
社区
课程
招聘
[原创]Ghidra反编译器的设计与实现(2)
发表于: 2026-3-16 19:37 1593

[原创]Ghidra反编译器的设计与实现(2)

2026-3-16 19:37
1593

前文介绍了Ghidra反编译器的调试环境搭建,相信看到上篇文章的读者都已经把环境搭建起来了,当然要是没有搭建起来也是可以看本篇文章的,不过少了些动手的乐趣,让我们开始吧。


本文会介绍

Ghidra如何实现反汇编?

中间代码PCode如何生成?

C语言的If语句如何生成?


Ghidra如何实现反汇编?

本文以一个最小exe文件来分析反编译器的流程,IDA载入该exe文件,可以看到在0x140000300的程序开头处是一条比较语句,Ghidra如何处理这条指令呢?

   

打开安装有Ghidra的Ubuntu虚拟机,

用VS Code远程打开Ghidra/Features/Decompiler/src/decompile/cpp目录,

F5开始调试。

首先载入要分析的exe文件,指令是

在sleigh.cc文件

下好断点,然后执行

不出意外程序会停在下好的断点处。

进入这个函数,可以看到

  

首先需要分析的是resolve()函数,这个函数完成的是反汇编工作。

在resolve()函数中首先的重点是

resolve(walker)函数内部执行了很复杂的匹配工作,主要根据cmp eax,3的机器码83 F8 03得到ct,这个匹配很复杂,好在我们可以直接忽略这个函数,不影响我们的分析。

得到ct后会根据特定脚本文件的内容建立内存中的树状结构,这牵涉到Ghidra反汇编器的基本机制。

在Ghidra\Processors\x86\data\languages文件夹下有一个脚本文件ia.sinc,这个文件是x86_64架构cpu指令集的描述文件,之所以是这个文件会被采用是因为我们载入的exe是这个指令集。

对于比较语句“cmp eax,3”首先会对应到ia.sinc的3062行

这个脚本首先是

这代表这这条指令的构成方式,在resolve()函数接下来的程序执行中会解析这个脚本文件,首先得到rm32的信息,这在ia.sinc的1487行

rm32又依赖Rmr32,程序继续分析脚本文件会引用ia.sinc的848行内容

至此“cmp eax,3”的第一个操作数确定下来了。接下来程序分析第二个操作数simm8_32,这会引用ia.sinc的924行

这样“cmp eax,3”这条指令就被转换为了内存中的数状结构以便接下来的程序继续处理。反汇编结构就分析到这,接下来是resolveHandles()函数,这个函数中会继续分析“cmp eax,3”汇编语句机器码83 F8 03的后两个字节,对汇编比较熟悉的朋友知道F8字节用于确定cmp指令的第一个操作数是哪个寄存器,在ia.sinc的3062行首先可以看到

刚好“cmp eax,3”的机器码的第一个字节是0x83,所以“cmp eax,3”确实会绑定到ia.sinc的3062行。继续看脚本的3062行,接下来是

搜索ia.sinc可以在536行可以看见

这代表这reg_opcode表示一个字节中的3,4,5三个比特位。由于

所以比较指令“cmp eax,3”的第二字节只能表示为:xx111xxx,x表示不定。

继续查看上面的这条

其中mod为3,而mod在ia.sinc的535行

mod为3就表示一个字节中的第6,7比特位全为1,所以比较指令“cmp eax,3”的第二字节只能表示为:11111xxx,x表示不定。

现在只差最低位的三个比特没有确定了,继续看上面的这条

这条中的r32在ia.sinc的560行

这代表r32是一个字节中的最低3位。比较指令“cmp eax,3”中的不同寄存器就被编码为不同的这3个比特,由于只有3个比特所以最多可以表示8个不同的寄存器。看ia.sinc的690行

这就确定了r32的值对应的寄存器,对于“cmp eax,3”,由于是寄存器eax,所以比特位为000,比较指令的第二字节为11111000,刚好是0xF8。

分析完了“cmp eax,3”的机器码83 F8 03的第二个字节,resolveHandles()函数接下来会将exe文件中的0x140000300处的第三个字节03提取出来,以便后用。

Ghidra反汇编的总结:

Ghidra通过编写自己内部定义的脚本语言文件来实现机器码到汇编语言的转换,ia.sinc就是按照这个脚本语言规则编写的CPU指令集描述,要想自己扩展反编译对不同CPU指令集的支持,就需要自己去编写脚本语言文件。Ghidra会将编写的脚本语言文件读入后编码,然后将编码后的文件输出为后缀名为sla的文件,在Ghidra反编译器初始化过程中会读入对应CPU指令集的sla文件,然后解码sla文件的内容生成内存结构。在反汇编阶段,其实主要就是用exe文件的机器码去Ghidra中查询得到对应的内存结构,再做进一步处理。

脚本语言除了能够实现机器码到汇编语言的转换,还可以进一步实现汇编语言到PCode的转换,不管是哪种CPU的机器码,Ghidra都会根据对应脚本文件将机器码转换为PCode,这可以认为是反编译器的前端,反编译之后的动作全部都围绕PCode进行,通过这样的方式,Ghidra实现了对多种CPU指令集的支持。

脚本语言的解析

在编写完脚本语言后,Ghidra会调用相应程序去解析脚本语言文件,这些解析代码不是直接用C++写的,而是用工具生成的,脚本语言的词法分析阶段是在slghscan.I中描述的,语法解析阶段实在slghparse.y文件中描述的。在make编译阶段,slghscan.I和slghparse.y都会被工具处理为可编译的C++文件,工具为bison/flex。


PCode的生成

还是以“cmp eax,3”为例来介绍PCode的相关内容

在大括号内部就是这条比较指令会转换为哪条PCode的描述。

在执行完前面我们所下断点处的代码处后会继续往下执行,来到PCode的处理函数。

首先

这条指令就会取出前面辛辛苦苦resolve得到的ct,然后调用了它的getTempl()函数,这个模板就是解析脚本文件所得到的PCode的模板。

接下来进入build函数中,主要看它的dump()函数,该函数在一个for循环中,会将解析得到的PCode一一处理。

首先dump()函数处理的PCode是CPUI_COPY指令,我们结合脚本语言中的描述来看看到底有没有这条PCode

可见“cmp eax,3”脚本语言中大括号的第一个部分的确有复制的行为,所以这样脚本语言中的内容就被处理了。

接下来继续调试,dump()函数处理的PCode是CPUI_INT_LESS,接着看大括号内部的语句

在ia.sinc的2164行

看到了吗,首先的确是在做比较操作,而且是小于的比较,这正好是PCode的CPUI_INT_LESS中的LESS。

继续调试,dump()函数处理的PCode是CPUI_INT_SBORROW,一眼可见这是正确的。

继续调试,dump()函数处理的PCode是CPUI_INT_SUB,

可见的确是在做减法。

继续调试,dump()函数处理的PCode是CPUI_INT_SLESS

在ia.sinc的2142行

可见,首先是比较。

继续调试,dump()函数处理的PCode是CPUI_INT_EQUAL,这是等于判断,也对的上。

继续调试,dump()函数处理的PCode是CPUI_INT_AND,这是与操作,对应的是&。

继续调试,dump()函数处理的PCode是CPUI_POPCOUNT。

继续调试,dump()函数处理的PCode是CPUI_INT_AND,这是与操作,对应的是&。

继续调试,dump()函数处理的PCode是CPUI_INT_EQUAL,转换结束。

这样一条“cmp eax,3”汇编就被转换为多条PCode了。

在dump()函数中,还会将前面的resolveHandles()函数中提取的信息,如“cmp eax,3”中的3利用起来。

接下来是

这个函数中会将生成的PCode做进一步封装。PCode被封装到PcodeOp类型对象中,同时“cmp eax,3”中的3被封装到Varnode类型对象中,并将Varnode设置为PcodeOp类型对象的输入。这样一条扁平的“cmp eax,3”就被表示为立体的内存结构了。


If语句的生成


冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。

最后于 2026-3-17 18:54 被zhzhz编辑 ,原因:
上传的附件:
收藏
点赞 ・2
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回