升级到伪代码:IR,SSA,从零开始构建
Type: Build
Languages: Python, C
Prerequisites: Phase 13 · 32 (ELF from scratch), Phase 13 · 33 (disassembly and control flow)
Time: ~150 minutes
学习目标
- 升降器将x86-64的指示解码到一个小的IR,明确的标志,负载,存储,分支,电话和回报.
- 将框架插槽转化为本地变量,系统V参数注册器转化为参数.
- 在统治边界上建立最小的SSA,然后替换,简化和删除死码.
- 恢复
if现在if随着else现在while其他do while控制流程图,后控制器和后边. - 解释乘以
0x66666667作为10的分数,证明身份. - 读取Ghidra伪代码,以排列一行,并给每个区别背后的指示或猜测.
问题
你问了 rea 的伪代码percent_change在ledger-x86_64-O1.o几达12.1.4在10秒钟内回答说:
cundefined1 [16] percent_change(int param_1,int param_2,ulong param_3)
{
long lVar1;
undefined1 auVar2 [16];
undefined1 auVar3 [16];
if (param_1 != 0) {
lVar1 = (long)((param_2 - param_1) * 100);
auVar3._0_8_ = lVar1 / (long)param_1 & 0xffffffff;
auVar3._8_8_ = lVar1 % (long)param_1 & 0xffffffff;
return auVar3;
}
auVar2._8_8_ = 0;
auVar2._0_8_ = param_3;
return auVar2 << 0x40;
}源长四行,需要两行.int值和回报int吉德拉报告了16字节返回值和源头没有的第三个参数.机器代码的16字节是正确的,吉德拉正确解码了它们.签名是猜测,文本中没有什么标志它为一个.
rea将此文本返回为procedure_pseudo_code根据该数据的数据,该数据的数据源和数据源都会被删除,并且将一个限制连接到记录中. 限制是"伪代码是Ghidra解译器输出,不是原始源,并不是Hopper输出的文本等等级".compare_functions工具不同于字体行.它的自己的维度记录说"组合是不透明的供应器文本",并且重新定位正常化是不可用的. rea不能看看文本内部,也不能只读取它的代理.
雷亚的DX-Ball记录显示了成本.FUN_00406400(void)电话__ftol()导向视图显示了堆输入和x87表达式. 代理只因为两个视图不同而继续运行.
您从第33课解码的56个指令开始,将它们提高到IR,将其改名为SSA,简化和结构.然后您将您的伪代码放在Ghidra的旁边.percent_change现在grade其他total测试将输出数据编译成Clang,然后通过输入网运行到源头.
固定物和神话
code/fixtures/ledger.c是该课程部分的共享设置:
cint percent_change(int before, int after) {
if (before == 0) return 0;
return (after - before) * 100 / before;
}
int grade(int score) {
switch (score / 10) {
case 10:
case 9: return 4;
case 8: return 3;
case 7: return 2;
case 6: return 1;
default: return 0;
}
}
int total(const int *values, int count) {
int sum = 0;
for (int i = 0; i < count; i++) sum += values[i];
return sum;
}grade_label现在,我们可以看到一个文件和一个对象中的一个函数,
项目是用clang -target x86_64-unknown-linux-gnu -O1 -fno-asynchronous-unwind-tables -c ledger.c现在,我们要去.-O0对于堆槽部分,对象坐在他旁边. code/fixtures/README.md列出了两个命令,每个对象的SHA-256和Ghidra固定器的 rea命令.
解码器的预言是llvm-objdump -d -r两项对象的文本是承诺的ledger-x86_64-O1.objdump.txt其他ledger-x86_64-O0.objdump.txt测试分析这些文件,并根据地址,字节和AT&T文本进行每个指令的比较.-O1在每一个地方,-O0什么时候?llvm-objdump测试后,第二次测试将其进行直播,然后再进行比较.
这里percent_change预言书所说:
text0000000000000000 <percent_change>:
0: 85 ff testl %edi, %edi
2: 74 09 je 0xd <percent_change+0xd>
4: 29 fe subl %edi, %esi
6: 6b c6 64 imull $0x64, %esi, %eax
9: 99 cltd
a: f7 ff idivl %edi
c: c3 retq
d: 31 c0 xorl %eax, %eax
f: c3 retq阶段1: 命令的字节
课33建立了解码器.这个课保持一个小副本,所以代码自动运行.副本完全覆盖了发射的光码.ledger.c在-O0其他-O1对于其他任何东西,它会提升DecodeError包含地址和字节字节.
解码器遵循英特尔SDM的字节顺序,卷 2,第2.1节.前置首先是opcode,然后是ModR/M,然后是SIB,然后是移动,然后是即时.0x40为了0x4F选择一个64位操作符,REX.R扩展了 reg根据 REX.X 扩展SIB指数,REX.B 扩展r/m根据2.1.3节,定义了三个ModR/M字段.mod现在reg其他r/m其他三种领域scale现在index其他base在64位模式下,mod = 00随着r/m = 101意思是32位移动与下一个指令相比 (第2.2.1.6节).
| Opcode bytes | Form | Fixture example |
|---|---|---|
01 29 31 39 85 89 | op Ev, Gv | 29 fe subl %edi, %esi |
03 2b 3b 8b 8d | op Gv, Ev | 03 04 97 addl (%rdi,%rdx,4), %eax |
63 with REX.W | movsxd Gv, Ev | 48 63 c7 movslq %edi, %rax |
69 and 6b | imul Gv, Ev, imm | 48 69 c0 67 66 66 66 |
83 /0 /5 /7 | add sub cmp Ev, imm8 | 83 c1 fa addl $-0x6, %ecx |
c1 /5 /7 | shr sar Ev, imm8 | 48 c1 f9 22 sarq $0x22, %rcx |
f7 /7 | idiv Ev | f7 ff idivl %edi |
ff /0 /4 | inc Ev, jmp Ev | 48 ff c2 incq %rdx |
70 to 7f, eb, e8 | jcc rel8, jmp rel8, call rel32 | 74 09 je 0xd |
99, c3, 90, 0f 1f | cltd, retq, nop | 66 0f 1f 44 00 00 nopw (%rax,%rax) |
其他leaq (%rip), %rcx在grade+0x23移动的速度是7字节,移动速度是0字节.R_X86_64_PC32.rodata+0x10在抵消0x36升降器在第三阶段解决了这个问题.
第二阶段:区块和主导者
基本块是一个列表,上面有一个输入,下一个输出.这是艾伦 (1970) 的定义,每个解译纸都会重复使用它.build_blocks()集合领导者:函数输入,函数内部的每个跳跃目标,以及跳跃或返回后的每个地址.每个块从最后的指示中获得继任者.一个条件跳跃有两个继任者和一个jmp没有一个.ret没有任何,而一个以普通命令结束的块, 传递给下一个领导者.
total在-O1具有五个区块:
| Block | Instructions | Successors |
|---|---|---|
0x40 | testl %esi, %esi then jle 0x5c | 0x5c, 0x44 |
0x44 | movl %esi, %ecx, xorl %edx, %edx, xorl %eax, %eax, nopw | 0x50 |
0x50 | addl (%rdi,%rdx,4), %eax, incq %rdx, cmpq %rdx, %rcx, jne 0x50 | 0x50, 0x5b |
0x5b | retq | exit |
0x5c | xorl %eax, %eax then retq | exit |
immediate_dominators()通过"反向后序"来完成区块,并切断处理前任的统治者,直到没有任何变化.在这样小的图表上,它在两个通过中完成.total这里写着0x44占据主导地位0x50它还说0x50通过后边来控制自己, 这就是第8阶段的循环.
两个更长的函数.dominance_frontiers()在5阶段使用SSA. post_dominators()通过虚拟出口节点运行在反向图表上,以找到8阶段的结合.
阶段3:升级到一个小 IR
IR有六种声明类型:Assign现在Store现在Branch现在Jump现在IndirectJump其他Return表达式是常数,变量,宽度二进制操作, (sext现在zext现在trunc五个形状还存在,但后来才被简化:Flags现在Cond现在Concat现在SignFill其他Symbolic现在,我们要去.
记录是以64位家族命名的变量.%eax是一个Assign为了rax宽32个,读数为%eax是一个读取的rax在64位模式下,32位写字扩展到完整的登记器中,因此以后的64位读取相同值为zext升降机不知道到达的定义,所以它记录了读数宽度,让SSA在5阶段插入.
| Instruction | IR statements |
|---|---|
subl %edi, %esi | flags = cmp(esi, edi) then esi = esi - edi |
testl %edi, %edi | flags = test(edi, edi) |
je 0xd | if je(flags) goto 0xd else goto 0x4 |
xorl %eax, %eax | eax = 0 |
cltd | edx = signfill(eax) |
idivl %edi | dividend = edx:eax, eax = dividend / edi, edx = dividend % edi |
movslq %edi, %rax | rax = sext64(edi) |
sarq $0x22, %rcx | rcx = rcx >>s 34 |
shrq $0x3f, %rax | rax = rax >>u 63 |
addl (%rdi,%rdx,4), %eax | flags = add(eax, load32(rdi + rdx * 4)) then eax = eax + load32(rdi + rdx * 4) |
leaq (%rip), %rcx with its relocation | rcx = &.rodata+0x14 |
pushq %rbp, movq %rsp, %rbp, subq $0x10, %rsp, popq %rbp | nothing, frame bookkeeping |
retq | return rax |
旗是变量
每个设置条件代码的指示都会写一个变量,命名为flags任何条件跳跃都会读到.cmp a, b写道cmp(a, b)其他test a, b写道test(a, b)其他sub写着同样的东西.cmp子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子jcc成为Branch(Cond(cc, flags), target, fallthrough)条件代码决定了比较运算器在6阶段遇到时:
| Code | After cmp(a, b) | After test(a, a) |
|---|---|---|
je | a == b | a == 0 |
jne | a != b | a != 0 |
jl, jge, jle, jg | signed <, >=, <=, > | signed compare with 0 |
jb, jae, jbe, ja | unsigned <, >=, <=, > | unsigned compare with 0 |
xorl %eax, %eax直接升到eax = 0它设置的旗写为add没有跳跃的标志.xor语言问题在grade吉德拉失去了它.
公司和公司
cltd填充%edx标签 %eax现在,我们要去.idivl %edi分开64位对%edx:%eax通过%edi输入了分数%eax其他部分%edx升降器将这对子写入一个临时的名称dividend剩下的句子,然后是旧的.%eax在SSA中,高半是signfill简化器将对减少到低半,分数变成32位./现在,我们要去.
堆积槽在 -O0
在-O0将每个局部人放在中.percent_change商店%edi为了-0x8(%rbp)其他%esi为了-0xc(%rbp)升降机看着pushq %rbp接着是movq %rsp, %rbp在前讲之后,一个记忆操作符和基础%rbp没有指数,一个移动变量变得一个命名的变量local_<hex>现在,我们要去.-0x8(%rbp)是local_8现在-0xc(%rbp)是local_c现在,我们要去.
框架指令本身没有产生IR,subq其他addq现在%rsp任何其他记忆操作符都保持一个Load或Store现在,我们要去.
转移
其他leaq (%rip), %rcx在grade移动在抵消的位置.0x36,,,R_X86_64_PC32.rodata+0x10链接器会写S + A - P进入场地.S是 .rodata现在A是添加剂0x10其他P命令然后计算next_ip + disp = (P + 4) + S + A - P = S + A + 4现在,我们要去.
升降器使用相同的算法,没有链接器.目标抵消是添加加加到指令结束的距离,0x10 + 4 = 0x14电气报说rcx = &.rodata+0x14现在,这张桌子里,有五个.int价值1, 2, 3, 4, 4现在,我们要去.
第四阶段:召开会议的参数
系统V AMD64 psABI 3.2.3 节通过INTEGER类参数%rdi现在%rsi现在%rdx现在%rcx现在%r8现在%r9结果返回在%rax然后%rdx3.2.1节说:%rbp现在%rbx其他%r12通过%r15其他一切都属于召唤者.
collect_params()应用一个规则:在函数中任何写入之前读取的参数注册表是一个参数.%rsi没有什么.%rdi现在还有两个参数.percent_change读书%edi其他%esi所以它已经param_1其他param_2现在,我们要去.grade只有读音%edi现在,我们要去.total读书%rdi作为64位的基础和%esi作为一个32位计数.
现在是Ghidra的param_3没有解释.idivl写道%rdx并且,psABI允许128位的INTEGER返回%rax:%rdx作为返回值,Ghidra 保存了两个登记器.
在零路线上,xorl %eax, %eax只有写作%eax吉德拉读%rdx函数的输入,并且该输入成为param_3指示是正确的,签名是猜测指示留下开放的部分.
阶段5:SSA,每定义一个名称
在升起后,grade写道rax两次和四次rcx没有什么. 赛特龙,费兰特,罗森,韦格曼和扎德克 (1991) 定义了静态单个任务形式. 每个变量都被分配一次,并将在接点上的phi函数从达到它们的定义中选择. 它们的建造有两个阶段. 5.1节将使用统治界限的phi函数, 5.2节将每个定义和使用重新命名,通过走过统治树.insert_phis()其他rename()按照这两个部分.
美国国家安全局列表total显示建筑物产生的产量:
textblock 0x44 -> 0x50
ecx_1 = esi_0
edx_1 = 0
eax_1 = 0
block 0x50 -> 0x50, 0x5b
eax_2 = phi(0x44: eax_1, 0x50: eax_3)
rdx_2 = phi(0x44: edx_1, 0x50: rdx_3)
flags_2 = phi(0x44: flags_1, 0x50: flags_5)
flags_3 = add(eax_2, load32(rdi_0 + rdx_2 * 4))
eax_3 = eax_2 + load32(rdi_0 + rdx_2 * 4)
flags_4 = inc(rdx_2, 1)
rdx_3 = rdx_2 + 1
flags_5 = cmp(zext64(ecx_1), rdx_3)
if jne(flags_5) goto 0x50 else goto 0x5b登记册的版本0是登记时的值. esi_0其他rdi_0它们是参数.0x50设置区块和它本身,所以每个定义的变量都得到了phi.flags_2鱼在抵达时已经死了.flags由于没有任何读取,所以第6阶段删除了它.
改名步骤还修复了宽度. edx_1 = 0是32位定义的rdx_3 = rdx_2 + 1现在,我们可以说是64位,所以rdx_2读取更广泛的.%rcx在cmpq %rdx, %rcx是64位的,而它的定义ecx_1 = esi_0现在,我们可以使用它.zext64(ecx_1)最后的伪代码中的每一个角色都来自这个规则.
简单的部分是SSA的退出.coalesce_webs()通过编译,它可以分成并行拷贝,其中两个phi在一个结合交换值,而课程不处理这种情况. 赛特龙的第7节描述了通过彩色的一般出口.
阶段6:简化,取代,删除已死代码
optimize()简单化器将表达式重写下面到上面,用以下规则:
| Before | After |
|---|---|
Cond(je, cmp(a, b)) | a == b, and the same for the other nine codes |
Cond(jle, test(a, a)) | a <= 0 |
x + (-6) | x - 6 |
trunc32(zext64(x)) where x is 32-bit | x |
trunc32(a + b) on 64-bit operands | trunc32(a) + trunc32(b) |
(signfill(x): x) / d | x / d at width 32 |
load32(base + idx * 4) | base[idx] |
trunc32(p >>s 34) + trunc32(p >>u 63) with p = sext64(x) * 0x66666667 | x / 10 |
转换将定义转换为唯一使用.规则有三个条件.使用在同一块中.使用是声明而不是phi.定义包含负载时,没有存储或调用在定义和使用之间.一个常数或部分地址在任何地方都被取代,除了为phi,因为phi参数必须保持为网络的变量.
移除死码删除了一个任务或一个 phi,其目的地没有用处,其表达没有调用.percent_change剩下的edx_2 = dividend_1 % edi_0现在,我们要把它放在第一位.edx_1 = signfill(eax_1)片的使用量为1次,因此被替换为dividend_1 = edx_1:eax_1现在,我们要去.
双子规则则则将减少signfill(eax_1):eax_1为了eax_1它们的分化是eax_1 / edi_0现在eax_1 = esi_1 * 100具有一个用途,esi_1 = esi_0 - edi_0两者都进入返回声明.flags它们的定义被取代为分支条件,并且消失.
creturn (param_2 - param_1) * 100 / param_1;乘以0x66666667
grade通过10分分分数, 声发出了不idiv它发射imulq $0x66666667, %rax, %rax现在sarq $0x22, %rcx现在,我可以写一本.shrq $0x3f, %rax格兰隆和蒙哥马利"乘法使用不变整数分数" (PLDI 1994),描述了该方法.编译器选择一个乘法.m,我也在.s对于每一个x在范围内,(x * m) >> s必须等于x / d现在,我们要去.
现在m = 0x66666667 = 1717986919其他s = 34算法证明:2^34 / 10 = 1717986918.4顶层是1717986919错误是m * 10 - 2^34 = 6让每一个32位的身份都能保持.x错误时间2^31必须在下面.2^34错误最大是8(x * m) >> 34相当于floor(x / 10)对于每一个非负面的int现在,我们要去.
对于负值x子从零转移,而C向零转移.shrq $0x3f产品的标志位,即1x增加它就会纠正圆的情况.
magic_divisor(m, s)测试一个候选对.d作为整数分数2^s通过m试图d其他d + 1它接受了d只有当m等于 2^s / d并且错误绑定保持.s = 33给出d = 5由于0x66666667的高峰值2^33 / 5检查身份与c_div(x, 10)包含两端的 2,406 值int距离.
随着我们的认可,grade读书ecx_1 = param_1 / 10 - 6;随着无识,演示程序打印了原始形式:
crax_1 = (long)param_1 * 0x66666667;
ecx_1 = (int)(rax_1 >> 34) + (int)((unsigned long)rax_1 >> 63) - 6;编译和计算的值都相同. 保持开关. 当你读到未经研究的编译器的代码时,原始形式是诚实的.
第七阶段:使用的类型
机器代码有宽度,没有类型.infer_types()网络的定义是32位宽的.int并且一个64位网络是long网络是元素访问的基础base[idx]具有4字节元素的int *标志型在复印和图像中传播,所以local_8 = param_1在-O0让他们都能int *返回类型是值的宽度在 return声明
结果是total是int total(int *param_1, int param_2)吉德拉写道int total(long param_1,uint param_2)提升员使用了量载荷作为指标证据. 吉德拉保持了登记器宽度,并写入为*(int *)(param_1 + uVar2 * 4)没有看到.const没有一个人能说int *其他unsigned *现在,我们要去.
李,阿弗杰诺斯和布鲁姆利 (TIE, NDSS 2011) 框架类型恢复作为一个网格上的约束解决.结果是一个顶部和下边界而不是一个猜测.诺南,洛基诺夫和科克 (Retypd,PLDI 2016) 增加了多形和复制性.他们报告了98%的回忆.const编译器留下来的语句中指标注释. 这一课使用了这两条最便宜的规则,并给了他们看不到的名称.
没有签名的算法在表达式中保存,而不是变量类型中. ja之后cmpl $0x4, %ecx图像(unsigned int)ecx_1 > 4其他shrq图像(unsigned long)rax_1 >> 63变量保持签名,以便读者看到注册表值和指令处理它未签名的唯一位置.
第八阶段:从图中构建
图表的区域与高层语言的形状相匹配. 循环首先出现 (6.6.1),然后是双向条件 (6.6.2),然后是 n 方向条件 (6.6.3). 雅克丹,埃施韦勒,格哈德斯-帕迪拉和史密斯 (NDSS 2015) 走了另一条道路. 它们从这些条件中得出每个节点执行的条件,并从这些条件中构建结构,没有固定的形状. 他们的动机是测量. 在一个样本上产生了1571个六射线goto它们的结构在1,530个核心函数中没有产生任何 49,514 个行中的声明,每32 个行的声明.
structure()需要从第二阶段开始的两个事实.Branch后边是一个控制其源的块的边缘.自然循环是连接到后边源的块的集合,而不会通过标题.
为了一个Branch合j其他:
- 如果
j发射if (!cond) { fallthrough region }继续下去j现在,我们要去. - 如果
j射,发射if (cond) { target region }继续下去j现在,我们要去. - 如果
j输出小臂,然后输出小臂.if (cond) {... }后面的另一只手臂,没有.else这是早期的回归.percent_change其他total现在,我们要去. - 否则发射
if (cond) { target } else { fallthrough }继续下去j这是percent_change在-O0两支武器都存储在哪里local_4我们会在幕上见面.
对于循环标题:
- 如果标题以一个
Branch发射while (cond) { body }现在,我们要去.total在-O0标题的形状是这样的:标题比较local_14随着local_c叶子jge现在,我们要去. - 如果标题也是后边的块,或者锁的出口,则发射
do { body } while (cond);现在,我们要去.total在-O1具有这样的形状:块0x50增加,增加,比较,然后跳到自己.
IndirectJump没有继任者.grade在-O0是一个真正的跳板桌,jmpq *%rax通过5个相对抵消.rodata+0x0现在,举起者打印了一张纸.0x76现在0x7f现在0x88其他0x91实验室补充说, 鱼只能通过间接跳跃而达到,switch现在,我们要去.
什么 - O1 对开关做了
在-O1没有留下一个跳桌grade它计算了score / 10 - 6然后它从一个五个输入表中加载了返回值.int在.rodata+0x14没有签名的比较是一个指令,涵盖了两者.score / 10 < 6其他score / 10 > 10负差是一个大未签字的值,所以它失败了相同的检查.
桌子可以住1, 2, 3, 4, 4对于6到10的定位数.switch五个return声明成为一个if电梯发射的就是这样,并给表命名.lookup_14在其部分的抵消之后.
桌子的长度是论的.table_values()从开始到下一个表.rodata任何一个.text在此对象中,下一个引用的偏移是 0x28其他类型grade_label现在,lookup_14没有(0x28 - 0x14) / 4 = 5没有签名的比较与4相符.Ghidra也不知道长度.它写道*(uint *)(&DAT_001000b4 + (ulong)uVar2 * 4)读者可以查看五个值.
格希德拉的身边
演示片每一对都打印了.code/fixtures/ghidra/*.json证据记录说,雷亚在2026年10-14日返回.npx -y rea-agents@6.5.0 decompile ledger-x86_64-O1.o <function> --provider ghidra --format json每张记录都包含confidence: observed其他authority: shipped-artifact文本中所述的说法是从文本中得到的,而记录中没有说明哪些.
百分比_变化
textlifter | ghidra
-----------------------------------------------+-----------------------------------------------
int percent_change(int param_1, int param_2) | undefined1 [16] percent_change(int param_1,int param_2,ulong param_3)
{ | {
if (param_1 == 0) { | long lVar1;
return 0; | undefined1 auVar2 [16];
} | undefined1 auVar3 [16];
return (param_2 - param_1) * 100 / param_1; | if (param_1 != 0) {
} | lVar1 = (long)((param_2 - param_1) * 100);
| auVar3._0_8_ = lVar1 / (long)param_1 & 0xffffffff;
| auVar3._8_8_ = lVar1 % (long)param_1 & 0xffffffff;
| return auVar3;
| }
| auVar2._8_8_ = 0;
| auVar2._0_8_ = param_3;
| return auVar2 << 0x40;
| }| Difference | Instruction or guess |
|---|---|
Return type undefined1 [16] against int | Guess. idivl writes %rax and %rdx, and no caller fixes the return width. Both registers were kept. |
param_3 | Guess that follows from the first. On the zero path only %eax is written, so %rdx is read as an input. |
lVar1 / (long)param_1 & 0xffffffff against / param_1 | Instruction. cltd plus idivl is a 64-bit division of a sign-extended 32-bit value, truncated to 32 bits. Both texts describe it. Ours reduced the pair with the signfill rule. |
lVar1 % (long)param_1 kept | Follows from the return type. The remainder in %rdx is dead in our analysis and alive in Ghidra's. |
if (param_1 != 0) with the zero case after it | Instruction order. je 0xd jumps to the zero return. Ghidra put the computed arm first, we put the smaller arm first. Both are correct. |
return auVar2 << 0x40 | Guess. A 16-byte value with param_3 in the low half, shifted into the high half, is how the decompiler expresses %rax = 0 with %rdx unknown. |
修复的版本在code/fixtures/ghidra/ledger-x86_64-O1.ghidra-repaired.c测试只编辑这个函数,并将其与81个输入对的源进行比较.
级别
textlifter | ghidra
-----------------------------------------------+-----------------------------------------------
static const int lookup_14[5] = {1, 2, 3, 4, 4}; | uint grade(int param_1)
int grade(int param_1) | {
{ | uint uVar1;
int eax_1; | uint uVar2;
int ecx_1; |
| uVar2 = param_1 / 10 - 6;
ecx_1 = param_1 / 10 - 6; | uVar1 = -(param_1 >> 0x1f) ^ (uint)((ulong)((long)param_1 * 0x66666667) >> 0x3f);
eax_1 = 0; | if (uVar2 < 5) {
if ((unsigned int)ecx_1 <= 4) { | uVar1 = *(uint *)(&DAT_001000b4 + (ulong)uVar2 * 4);
eax_1 = lookup_14[(unsigned long)(unsigned int)ecx_1]; | }
} | return uVar1;
return eax_1; | }
} || Difference | Instruction or guess |
|---|---|
param_1 / 10 - 6 in both | Instruction. Both recognized the 0x66666667 idiom. |
uVar1 = -(param_1 >> 0x1f) ^ (uint)(... >> 0x3f) against eax_1 = 0 | Instruction xorl %eax, %eax at grade+0x1a. The register is zero. Ghidra expressed the xor as the sign of param_1 exclusive-or the sign bit of the product, two spellings of the same bit, and did not cancel them. The value is 0 for every int, and the test proves it over 141 scores. A reader who trusts the text believes the default depends on the score. |
uVar2 < 5 against (unsigned int)ecx_1 <= 4 | Instruction cmpl $0x4, %ecx then ja. Same condition. Ghidra typed uVar2 as uint, we cast at the use. |
*(uint *)(&DAT_001000b4 + (ulong)uVar2 * 4) against lookup_14[...] | Instruction plus relocation. Ghidra loaded the object with .rodata at 0x001000a0, so 0x001000b4 is .rodata + 0x14. It applied the relocation and kept a byte address. We named the table and read its five values. |
uint return against int | Guess on both sides. The instructions only say 32 bits. |
总数
textlifter | ghidra
-----------------------------------------------+-----------------------------------------------
int total(int *param_1, int param_2) | int total(long param_1,uint param_2)
{ | {
int eax_1; | int iVar1;
int ecx_1; | ulong uVar2;
long rdx_1; |
| if (0 < (int)param_2) {
if (param_2 <= 0) { | uVar2 = 0;
return 0; | iVar1 = 0;
} | do {
ecx_1 = param_2; | iVar1 = iVar1 + *(int *)(param_1 + uVar2 * 4);
rdx_1 = 0; | uVar2 = uVar2 + 1;
eax_1 = 0; | } while (param_2 != uVar2);
do { | return iVar1;
eax_1 = eax_1 + param_1[rdx_1]; | }
rdx_1 = rdx_1 + 1; | return 0;
} while ((unsigned long)(unsigned int)ecx_1 != rdx_1); | }
return eax_1; |
} || Difference | Instruction or guess |
|---|---|
int *param_1 against long param_1 | Inference on our side from addl (%rdi,%rdx,4), %eax. Ghidra kept the width. |
uint param_2 and (int)param_2 against int param_2 | Ghidra chose unsigned because cmpq %rdx, %rcx compares the zero-extended count. We kept int and cast at the compare. |
do { } while in both | Instruction jne 0x50 from the block to itself. The compiler rotated the loop. The -O0 object keeps the while. |
ecx_1 = param_2 kept against param_2 != uVar2 | Our substitution rule stays inside one block. The copy movl %esi, %ecx is in the setup block and the compare is in the loop, so the variable stays. Ghidra propagated it. |
(unsigned long)(unsigned int)ecx_1 | Instruction. movl %esi, %ecx zero-extends, and the 64-bit compare reads the full register. |
行为测试编译了Ghidra文本grade其他total没有变化,增加了两个typedef线条和定义DAT_001000b4在20个字节中.rodata+0x14两个函数都与网格上的源相匹配.
研究对阅读伪代码的说法
根据"人价解"的测量,Ghidra仅可实现20.12%. 根据标准,每五个函数中就有一个重新编译并通过了与基准相关的声明. 通过使用6.7B模型来完善Ghidra的产量,将其提高到52.74%. 根据"2025年ACL发现"的规定,该公司运行了6个工业解译器和6个LLM方法,从130个程序中运行了23,400个功能. 士课程的读数更好,功能准确度也低了52%.
,拉夫和米辛斯基 (arXiv 2609.05370,2026年9月) 解释了这一差距. 他们使用了300个GitHub库功能和287个基于CVE的功能. 强大的精炼模式将Ghidra的构建率从75%提高到90%,而行为匹配率则从74%降至62%. 通过基准标准的每项测试的候选人仍然对模糊输入不同. 这在8个系统中的4.9%的病例中发生, 据了解,在此之前,
它们的原因很简单. 发明的字段,类型,字符号和守护者取代了解译器在文本中留下的可见未知.
这就是升降器印记的原因.(unsigned long)(unsigned int)ecx_1它们会打印一张纸条,而不是一张纸条.switch对于未解决的跳跃,lookup_14每一行都有一个可以读取的衍生值.code/main.py吉德拉的undefined1 [16]炼模型的清洁.int percent_change(int, int)同样的模型将在下一个函数上产生一个纯错误的签名.
建立它
运行演示:
bashcd phases/13-tools-and-protocols/34-lifting-to-pseudocode
python3 code/main.py演示程序解码了该系统的四个功能-O1根据这些指令,llvm-objdump为了percent_change现在grade其他total它打印了SSA前的IR,SSA列表与phi函数,以及Ghidra文本旁边的伪码.grade由于分区识别已关闭,-O0版本percent_change现在total其他grade_label总结:
json{
"functions": ["grade", "grade_label", "percent_change", "total"],
"instructions": 56,
"blocks": {"percent_change": 3, "grade": 3, "total": 5, "grade_label": 4},
"phis": {"percent_change": 0, "grade": 1, "total": 2, "grade_label": 1},
"objdumpDifferences": 0
}阅读code/main.py在此顺序:
read_object()阅读部分标题,符号表,以及.rela转移可用的ELF64对象的部分.decode_one()其他render_att()解码一个指示,然后打印它.llvm-objdump没有.build_blocks()现在immediate_dominators()其他dominance_frontiers()建立图形的数据.lift_insn()将一个指示转化为IR声明并解决迁移.insert_phis()其他rename()建立SSA并插入宽度.make_simplifier()现在substitute_once()其他remove_dead_code()走进里面optimize()现在,我们要去.collect_params()现在coalesce_webs()其他infer_types()给网格一个名称,一个字符.structure()随着post_dominators()其他natural_loops()发射C体.lift_function()结合各个阶段lift_all()提升一个整体的物体,用电话签名.
用它
为了从你自己的对象上提升一个函数,clang -target x86_64-unknown-linux-gnu -c打电话lift_all(read_object(path))当解码器升级时DecodeError读取信息中的opcode字节,在SDM的opcode地图中找到它.然后在文件顶部的表中添加一个输入.当升降器在mnemonic上升起时,添加一个规则lift_insn()结构者发射了goto打印图表IrPrinter并且决定哪个形状规则缺失. 每个加值都是小的,每个则是对比的.llvm-objdump现在的测试也没有.
在Ghidra安装后,运行rea在同一函数上并将两个文本放在一边.在相信任何文本之前填写区别表.背后有指示的区别是演示选择.背后没有指示的区别是猜测,猜测可以在任何一方.
互动实验室
实验室A:一个函数通过五个阶段
关于code开放字符串:
bashcd phases/13-tools-and-protocols/34-lifting-to-pseudocode/code
python3 -q运行:
pythonfrom main import *
obj = read_object(FIXTURES / "ledger-x86_64-O1.o")
lifted = lift_function(obj, "percent_change")
for insn in lifted.decoded: print(f"0x{insn.address:x}: {insn.raw.hex(' ')} {insn.att()}")
print(lifted.ir_text)
print(lifted.ssa_text)
print(lifted.pseudocode)追随%esi其他subl %edi, %esi为了esi_1 = esi_0 - edi_0为了(param_2 - param_1)字节fe是 电路的ModR/Msubl随着mod = 11现在reg = edi其他r/m = esi现在,我们要去.
实验室B:分类语法
pythonprint(lift_function(obj, "grade", recognize_division=False).pseudocode)
print(magic_divisor(0x66666667, 34), magic_divisor(0x66666667, 33), magic_divisor(0x66666667, 31))首先打印原始乘法和转移.10 5 None找出为什么同一乘法被5分,一个转变少,为什么转变31失败错误界限.
实验室C:一个新的对象停止了解码器
设置在-O2然后抬起它.
bashclang -target x86_64-unknown-linux-gnu -O2 -fno-asynchronous-unwind-tables -c fixtures/ledger.c -o "$TMPDIR/ledger-O2.o"
python3 -c 'from main import *; from pathlib import Path; import os; lift_all(read_object(Path(os.environ["TMPDIR"]) / "ledger-O2.o"))'解码器停在0x56随着opcode 81 is outside the fixture subset号码81命令是:andl $0x7ffffff8, %edx在向循环中使用了它total加入它旁边0x83在decode_one()单个字符串的输入量,然后再运行,并注意下一个字符号是什么.llvm-objdump现在,我们要去.
实验室D:Ghidra的标准值
pythonprint(ghidra_pseudocode("grade"))
for score in (-25, 0, 59, 60, 100, 115):
sign = -(score >> 31)
product_sign = ((score * 0x66666667) & ((1 << 64) - 1)) >> 63
print(score, sign ^ product_sign)每一行都印出了0个字符.uVar1字体是正确的,不可读的,xorl %eax, %eax两字节中写出来.
实践实验室
加入switch恢复grade在-O0现在,我们要去.
要求:
- 阅读五个搬迁情况
.rela.rodata其他ledger-x86_64-O0.o其他R_X86_64_PC32.text+offset根据表计算五个案例目标.rodata+0x0现在,我们要去. - 添加一个
Switch标签表,目标列表和默认的 IRja在餐桌装载之前. - 给开关块每一个目标一个继任者,以便主导者,后主导者和SSA继续工作.
- 发射
switch (local_10) { case 0:... }只有一个case目标和结合后的共享尾. - 留下任何间接跳跃的笔记,
- 通过语音编译结果,并与测试中分数格式的源相比较.
成功意味着grade在-O0其他grade在-O1电网上产出相同的输出,switch没有goto现在,我们要去.
艺术品
这一课产生了outputs/skill-lifting-to-pseudocode.md通过它将每个行的衍生函数升高,并将差异表与分解器的文本填写.该文件需要对每个不同行进行参考拆除,移动,调用公约,编译检查,行为比较和判决.
检查
运行演示和确定性套件:
bashcd phases/13-tools-and-protocols/34-lifting-to-pseudocode
python3 code/main.py
python3 -m unittest discover -s code/tests -v套房有25个测试.
- 解码器同意承诺
llvm-objdump通过直播,llvm-objdump装机时 - REX,ModR/M,SIB,以及移动场的抵消
leaq (%rip), %rcx按SDM描述的方式进行解码 - 在子集之外的 opcode 增加
DecodeError没有猜测 - 电脑电脑系统的读者列出了四个函数和三个函数.
.text迁移 total五个街区,0x44占据主导地位0x50其他0x50是一个循环头,它本身是身体%rdi其他%rsi成为param_1其他param_2其他param_1在total是int *test其他cmp随着je现在jle现在ja其他jge成为==现在<=没有签名>其他<- SSA 将两个直播fi函数放在 循环头上
total没有任何东西.percent_change - 编号码
percent_change现在grade其他total相当于预期的文本 grade读书param_1 / 10 - 6含有识别和含有识别的原料0x66666667换34分为10,换33分为5,身份保持在网格上- 上堆积的插槽
-O0成为local_4现在local_8现在local_c图指针从来没有出现 - 其他问题
.rodata+0x14读书1, 2, 3, 4, 4通过迁移 - 直接跳进
grade在-O0报告的四个未提起的块 - 达灯具包含16字节的返回,
param_3没有简单的xor其他long param_1 - 六个发射单位编译与
clang -std=c11 -Wall -Werror为了x86_64-unknown-linux-gnu - 提升的函数,修复的Ghidra文本和源头在输入网上打印相同的232行
生产失败模式
| Failure | What the weak reading reports | What the derivation must show |
|---|---|---|
| Decompiler widens the return value | "returns a 16-byte struct" | idivl writes %rdx, and no caller fixes the width |
| Decompiler adds a parameter | "takes three arguments" | The register is read on one path only because the return width was guessed |
| Idiom goes unrecognized | "multiplies by a magic constant" | The multiplier is the ceiling of 2^s / d and the error bound holds |
Idiom is recognized and a xor is lost | "default depends on the input" | xorl %eax, %eax writes zero, two spellings of one bit cancel |
| Pointer typed as integer | "takes a long" | A scaled 4-byte load from the register is pointer evidence |
| Table length unknown | "reads past the table" | The next referenced offset or the section end bounds it, and the range check agrees |
| Loop shape misread | "while loop" | The header has no exit, the latch jumps back, so it is do while |
| Indirect jump guessed | "switch with five cases" | The targets come from .rela.rodata, or the jump is reported as unresolved |
| Substitution across a store | "one expression" | A loaded value moved past a store or a call changes meaning |
| Refinement model edits the text | "compiles and looks right" | Build rate up, behaviour match down, invented guards replaced visible unknowns |
| Decoder guesses an unknown opcode | "disassembly looks fine" | DecodeError with the address and the byte, then one table entry checked against llvm-objdump |
运营规则
按照你没有看到的衍生法来处理伪代码.用可以阅读的升降器将其从说明中复制.将两条文本放在一边,并在每条不同行后写说明或猜测.
进一步阅读
- Intel SDM Volume 2, chapter 2, Instruction Format
- System V AMD64 psABI, section 3.2, Function Calling Sequence
- Cytron et al., Efficiently Computing Static Single Assignment Form and the Control Dependence Graph, TOPLAS 1991
- Cooper, Harvey, Kennedy, A Simple, Fast Dominance Algorithm, 2001
- Cifuentes, Reverse Compilation Techniques, PhD thesis, QUT 1994
- Yakdan et al., No More Gotos, NDSS 2015
- Granlund, Montgomery, Division by Invariant Integers using Multiplication, PLDI 1994
- Lee, Avgerinos, Brumley, TIE, NDSS 2011
- Noonan, Loginov, Cok, Polymorphic Type Inference for Machine Code, PLDI 2016
- LLM4Decompile, EMNLP 2024
- DecompileBench, Findings of ACL 2025
- Liu, Raff, Micinski, When LLM Decompilers Recompile More and Preserve Less, 2026
- rea 6.5.0 Ghidra provider notes
- 标签
3b79af2这一课描述了:src/domain/functionComparisonDimensions.ts现在website/evidence/dx-ball-sound-pan.md现在,我们要去.
This free lesson is part of the AI Engineering from Scratch curriculum. Read the full explanation, run the lesson code, and verify the result in the interactive reader or from the repository source.
Browse the complete course catalog or open this lesson on GitHub.