Phase 13: Tools & Protocols

编译语法:优化对你的代码有什么影响以及如何回读它

优化编译器将源构件取代于更便宜的指令形状.每个形状都有衍生,这课程构建了向后运行衍生的识别器,并与源进行检查.

Type: Build

Languages: Python, C, x86-64 assembly, AArch64 assembly

Prerequisites: Phase 13 · 33 (disassembly and control flow), Phase 13 · 34 (lifting to pseudocode), Phase 13 · 38 (calling conventions)

Time: ~150 minutes

学习目标

  • 取出魔术数字,并通过任何恒定与格兰隆德-蒙特哥默的边界进行分离,并从发射的恒定中恢复分离器.
  • 举个说明LLVM降低a的五种方法switch阅读入口数,表位置,以及对象的入口.
  • 读取向量循环作为行径,积累器,每次代的元素,三次计数面具,减小和剩余循环.
  • 识别尾声,内线的呼叫,无枝的选择,以及成为的循环memset或memcpy现在,我们要去.
  • 报告每种语法,其证据说明和解码的源形式,在x86-64和AArch64上,在-O0通过-O3现在,我们要去.
  • 结果与Ghidra,Angr,Radar2,Rizin和Lldb的结果进行比较.

问题

你编译了idioms.c在-O2问一个代理人什么div7u经纪人读到imulq $0x24924925, %rax, %rax函数乘以613566757,变化,并加.每个字都是真的.函数计算x / 7现在,我们要去.

同一个物体持有一个switch七个案例,没有跳跃,switch五个案例,这是一个跳机,一个switch两种情况是sete其他cmovne,然后再.n两个中每次代的8个整数xmm后者在第二个循环中完成其余的.-O0产品来源相同divl七个进口跳机,一个圆.-O1七个案例的交换器成为一个值表,而向量循环还没有存在.

编译器可以从一个规则中发出每个形式,你可以在其源头中读取,每个形式都可以被解码到它来的构造中.解码是算术,算术可以检查.34课证明其中之一,乘以0x66666667这一课构建了所有它们的识别器, 运行了我们自己的18个代码, 并对C源进行了所有373个报告的验证.

语法是一种形状,其起源

语法是编译器发出一个源构造的指令序列.三个事情使其可识别.它具有固定形状,操作符带有导数预测的常数,形状与程序员手动写的不同.

标识器在code/main.py关于"llvm-objdump -d -r通过分析每个函数,它将其分为指令,字节,mnemonic,操作符和其后续的移动线.它只读取两个东西的对象文件:字节的.rodata值表的位置,.rela.rodata进入,跳表目标在连接之前居住.

每个发现都是一个Idiom记录一个kind解码的source形式,列表evidence其他地址detail字典,以及一个verified旗来自C源.parse_source()读书idioms.c找出每个函数,然后提取除分后的/其他%后的乘法*其他switch其他案件和它们的价值? :其他国家for循环和调用.一个分类语法在函数的分类列表中时被验证.一个搜索表在其输入与案例值匹配时被验证,直到一个常数.一个尾声调用被验证当调用在源中.演示报告以一个 结束unverified测试套件表示,所有18个构建的列表是空的.

解码需要名称.识别器在每个函数的基本块上运行一个小的象征评估器,以列表顺序,从课38中系统V和AAPCS64公约的参数注册表中取出的参数名称.-O0在每个区块入口时,前身的状态都会合并,并且在它们不同的地方,一个名称,一个更早的语法分配的胜利.cmovll %edx, %eax在digest现在,我们可以看到sum % 10 < 5 ? sum % 10 : 5循环语法命名为积累器sum分类语法命名为其分数.sum / 10模块语称之为结果sum % 10消息来源说rem > 5 ? 5 : rem两个表达式都给出最小的rem它们同意rem == 5现在,我们要去.

通过一个常数进行分离

格兰隆和蒙特哥马利"乘法使用不变整数分数" (PLDI 1994),取代n / d乘以一个和一个转变.s并且是乘法m = ceil(2^s / d)然后floor(n * m / 2^s)相当于floor(n / d)对于每一个n在错误时的范围e = m * d - 2^s对于未签名的32位n边界是e <= 2^(s - 32)签名的n随着|n| < 2^31边界是e <= 2^(s - 31)并且一个标志修复加上1n由于地板绕了错误的方向,

magic_numbers(d, bits, signed)在main.py搜索s搜索将在灯具中发出的每一个恒定的声音复制.

没有手印的10

2^35 / 10 = 3435973836.8顶层是3435973837 = 0xCCCCCCCD错误是0xCCCCCCCD * 10 - 2^35 = 2的s = 35是2^3 = 8在s = 34错误是6对4的边界,所以35是最小的转移.n = 123其他123 * 3435973837 = 422624781951其他422624781951 >> 35 = 12现在,我们要去.

声发射的正是为了div10u在-O2在 x86-64 上:

text       0: 89 f9                        	movl	%edi, %ecx
       2: b8 cd cc cc cc               	movl	$0xcccccccd, %eax
       7: 48 0f af c1                  	imulq	%rcx, %rax
       b: 48 c1 e8 23                  	shrq	$0x23, %rax

0x23现在,我们要做什么?movl %edi, %ecx识别器读取从 字符中乘数.movl $imm这种食物给两的子.imulq读取班次,打电话divisor_from_magic(0xCCCCCCCD, 35)这种函数试着2^35 // m = 10并且接受10因为magic_numbers(10)结果是相同的乘法,然后转向后.

在AArch64上,相同的函数是mov w8, #0xcccd; movk w8, #0xcccc, lsl #16; umull x8, w0, w8; lsr x0, x8, #35常数在两个16位半部分中,乘法是umull识别器将这些半部分组合在一起并报告相同的分数.

无签名7手动,附加补充

2^35 / 7 = 4908534052.57顶层是4908534053 = 0x124924925错误是3对8的边界,所以转移是35,没有更小的转移工作.一个32位乘法不能保持乘法,所以编译器乘以低32位0x24924925按一下,将股息按一下加回:

text      32: 48 69 c0 25 49 92 24         	imulq	$0x24924925, %rax, %rax
      39: 48 c1 e8 20                  	shrq	$0x20, %rax
      3d: 29 c7                        	subl	%eax, %edi
      3f: d1 ef                        	shrl	%edi
      41: 01 f8                        	addl	%edi, %eax
      43: c1 e8 02                     	shrl	$0x2, %eax

阅读如下t = (n * 0x24924925) >> 32现在q = (((n - t) >> 1) + t) >> 2总转变是32 + 1 + 2 = 35随着n = 100其他t = 14现在(100 - 14) >> 1 = 43现在43 + 14 = 57现在57 >> 2 = 14识别器检测到sub现在shr现在add现在shr后尾 32 位转换,添加了 2^32返回乘法,然后恢复到7.evaluate_division()测试对其进行了测试.n // 7根据AArch64的形式,sub w9, w0, w8; add w8, w8, w9, lsr #1; lsr w0, w8, #2由于转移的加算是一个指令.

双手签名3

2^32 / 3 = 1431655765.33顶1431655766 = 0x55555556错误2号,并签署了s = 32是2^1 = 2局没有空间,所以转变为32个,shrq $0x20随着n = -7: 产品是-10021590362算数转移为32 给出-3现在,我们可以将这个符号设置为1,所以,-2现在,-7 / 3在C.签署的灯具div10s其他div3s两者都携带了装.shrq $0x3f在产品副本上,其次是:addl现在,我们要去.

常数是什么?

乘法本身就没有分数.0x66666667随着转变34是10.同样的乘法随着转变33是5,如第34课所示.因此,识别器总是对乘法与总转变,clangMatchesAlgorithm两项的权力从来没有使用乘法.magic_numbers()由于编译器发出了一个信号偏差的转变,mod8s在下一节中,我们将看到这个形状.

语法属于指令选项

在-O0现在div10u是movl $0xa, %ecx; xorl %edx, %edx; divl %ecx现在,我们要去.div10s是cltd; idivl %ecx在同一个物体中,digest通过 imulq $0x66666667一个旗解释了差异.idioms-x86_64-nofastisel-O0.o造的-O0 -mllvm -fast-isel=false在其中,每个分数是乘法,包括div10u随着movl $0xcccccccd, %ecx; imulq %rcx, %rax; shrq $0x23硬件分区是快速指令选项的选择-O0乘以是选择DAG的降低,这是LLVM的UnsignedDivisionByConstantInfo其他SignedDivisionByConstantInfo产品.digest区块的分区也包含一个switch识别器报告了两种表格,hardware idivl其他magic multiply图表显示混合-O0功能.

模块 遵循 率数

x % d是x - (x / d) * d编译器计算了乘法,乘以回,然后减去.在x86-64上,乘以10是addl %ecx, %ecx; leal (%rcx,%rcx,4), %ecx现在,2 * q * 5在AArch64上,这是一个指令.msub w0, w8, w9, w0计算的w0 - w8 * w9随着w9 = 10识别器通过复印来追踪分数登记器,通过 追踪积累的分数.add现在lea现在shl其他imul接受了sub如果因子等于分数,

当同一个分数也给出了另一个表达式时,两个语法都会留在报告中.-O1编译器计算sum / 10一次一次digest为了switch在x86-64上,它甚至将分数结合两次,leal (%rax,%rsi), %edi对于模块和addl %esi, %eax识别器报告一个分数和一个模块,并标记分数为共享.switch索引不可读.

通过两的功率签署的模块具有自己的形状,因为分数从来没有计算:

text      92: 8d 48 07                     	leal	0x7(%rax), %ecx
      95: 85 ff                        	testl	%edi, %edi
      97: 0f 49 cf                     	cmovnsl	%edi, %ecx
      9a: 83 e1 f8                     	andl	$-0x8, %ecx
      9d: 29 c8                        	subl	%ecx, %eax

对于一个负面的x密码在掩盖之前偏向7 ,所以切割轮向零,正如C所要求.x缩小到8的倍数,减去的页面x % 8 64 号可以使用negs w8, w0; and w9, w0, #0x7; and w8, w8, #0x7; csneg w0, w9, w8, mi面具:x其他-x吉德拉以此命名了它的规则.RuleSignMod2nOpt简单的"转换INT_SREM表格: (V + (sign >> (64-n)) & (2^n-1)) - (sign >> (64-n)) => V s% 2^n"我知道.

在-O0图为"Aarch64"写为sdiv w9, w8, w10; mul w9, w9, w10; subs w0, w8, w9认证器也接受了该表格,并报告了hardware sdiv + mul + subs现在,我们要去.

五种方法来降低开关

体的形状是LLVM决定的switch在两个地方.SimplifyCFG::simplifySwitchLookup运行在-O1并且把所有情况下都产生常数的交换器变成一个值表,当至少有三个情况下,目标可以构建查找表.SingleValueKind现在LinearMapKind现在BitMapKind其他ArrayKind现在,我们要去.-O0管道从来没有经过过这个通道.SwitchLowering::findJumpTables在指令选择中,该指令构建了一个跳板表,当集群至少有min-jump-table-entries输入,默认4个,并且足够密集: NumCases * 100 >= Range * MinDensity随着jump-table-density正常功能10个,正常功能40个-Os剩下的东西变成了对案例常数进行二进制搜索,splitWorkItem灯具的每个结果都具有一个功能.

FunctionSource-O0 x86-64-O2 x86-64-O2 AArch64
linear_switchcases 0 to 6 return 10 to 70jump table, 7 entrieslinear map 10*k + 10, default -1madd and csinv
table_switchcases 0 to 6 return 10 to 60 then 71jump table, 7 entrieslookup table at .rodata+0x28lookup table at .rodata+0x0
call_switchcases 0 to 4 call one of five pointersjump table, 5 entriesjump table, 5 entriescomparison tree, pivot 1
sparse_switchcases 1, 100, 10000, 1000000, 100000000compare chaincomparison tree, pivot 9999comparison tree, pivot 9999
two_casescases 3 and 4 return 7 and 9compare chainselect chain k != 3 ? (k == 4)*9 : 7two csel

查找表,从对象中读取

table_switch在-O2其他:

text      d0: b8 ff ff ff ff               	movl	$0xffffffff, %eax
      d5: 83 ff 06                     	cmpl	$0x6, %edi
      d8: 77 0c                        	ja	0xe6 <table_switch+0x16>
      da: 89 f8                        	movl	%edi, %eax
      dc: 48 8d 0d 00 00 00 00         	leaq	(%rip), %rcx
		00000000000000df:  R_X86_64_PC32	.rodata+0x24
      e3: 8b 04 81                     	movl	(%rcx,%rax,4), %eax
      e6: c3                           	retq

检查边界cmpl $6; ja移动补充0x24加上 RIP 相对场的4个字节,将表放在 .rodata+0x28它们的字节是0a 00 00 00 14 00 00 00 ...现在,我们要做什么?[10, 20, 30, 40, 50, 60, 71]组件组件命名为.Lswitch.table.table_switch在.s文件,附有评论@switch.table.table_switch在对象中只有移动和字节.没有跳跃,所以控制流图有三个块.

文件表中的地址与 adrp x8其他add x8, x8, #0x0其他地方的地区.rodata子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,子,ldr w0, [x8, w0, uxtw #2]识别器读取了从add转移.

线性地图,没有表格

linear_switch收益10 * (k + 1)为了k在0到6simplifySwitchLookup检测到相似关系并发出算术:

text      b0: 83 ff 07                     	cmpl	$0x7, %edi
      b3: 8d 04 bf                     	leal	(%rdi,%rdi,4), %eax
      b6: 8d 4c 00 0a                  	leal	0xa(%rax,%rax), %ecx
      ba: b8 ff ff ff ff               	movl	$0xffffffff, %eax
      bf: 0f 42 c1                     	cmovbl	%ecx, %eax

评价者读到leal (%rdi,%rdi,4)作为5*k现在leal 0xa(%rax,%rax)作为10*k + 10其他cmovb之后cmpl $7作为k < 7 ? 10*k + 10 : -1识别器检查公式与每个源头案例,并标记验证的语法.madd w8, w0, w8, w9其他csinv w0, w8, wzr, lo在哪里csinv随着wzr产量-1在默认路径上.

跳跃表,从搬迁读取

call_switch在-O2现在,我们可以看到一个grade在-O0其他cmpl $4; ja其他leaq为了.rodata-0x4其他movslq具有4字节的输入,addq其他jmpq *%rax五个项目是搬迁..rela.rodata其他target = addend - 4k给出0x10c, 0x107, 0x10e, 0x110, 0x109它们的每个尾巴都通过一个函数指针跳过.dispatch设置将此扩展到16个案例:在x86-64上16个迁移输入,在AArch64上16个单字节的压缩表,ldrb w11, [x9, x8]通过4 缩小add x10, x10, x11, lsl #2根据adr识别器解码[0x24, 0x74, 0x54, ...]通过字节表和报告compressed: true现在,我们要去.

在四个层面上,同一个开关

digest保持开关sum / 10在 x86-64 上,这是一个跳转表.-O0查找表-O1包含条目[16, 32, 64, 128, 128]现在,我们还在上跳桌.-O2其他-O3他们的四个案子区块存储0x10现在0x20现在0x40现在0x80从-O1乘以16的乘法已经进入了这些案例.-O0对于 -O1查找表识别器将输入与源值进行比较,找到一个常数比率,并报告foldedFactor: 16在Aarch64上-O2形式是一个比较树,轴心在8,6,7和8的等式测试,以及一个范围测试.sub w9, w9, #9; cmp w9, #2; b.hs报告中包括9号和10号案例.rangeTests: [[9, 10]]现在,我们要去.

两支旗移动了这些边界.-fno-jump-tables转table_switch现在call_switch其他digest在 x86-64 上,转换为比较树,为7个案例函数的2和4个旋转点.-mllvm -min-jump-table-entries=4叶子call_switch作为一个比较树,因此在该目标上,入口数量不是五个间接调用的决定性输入.idioms-arm64-jt4-O2.o记录的运行.

墨西哥湾委员会将两项决定都纳入tree-switch-conversion.cc随着课程jump_table_cluster现在bit_test_cluster现在switch_decision_tree其他switch_conversion现在,我们要去.-ftree-switch-conversion现在,-O2现在-fjump-tables其他-fbit-tests在-O1跳板值来自case-values-threshold没有GCC安装在这个机器上,所以这些名称来自GCC源和手册,

循环: 向,解卷或取代

路向量仪在线-O2现在-O3其他-Os现在,我们要去.-O0其他-O1根据2014年1月的LLVM3.4的规则.sum_n在-O2显示形状的每个部分:

text     1f6: 83 fe 07                     	cmpl	$0x7, %esi
     1f9: 77 09                        	ja	0x204 <sum_n+0x14>
     206: 81 e2 f8 ff ff 7f            	andl	$0x7ffffff8, %edx
     230: f3 0f 6f 14 37               	movdqu	(%rdi,%rsi), %xmm2
     235: 66 0f fe c2                  	paddd	%xmm2, %xmm0
     239: f3 0f 6f 54 37 10            	movdqu	0x10(%rdi,%rsi), %xmm2
     23f: 66 0f fe ca                  	paddd	%xmm2, %xmm1
     243: 48 83 c6 20                  	addq	$0x20, %rsi
     24a: 75 e4                        	jne	0x230 <sum_n+0x40>
     24c: 66 0f fe c8                  	paddd	%xmm0, %xmm1
     250: 66 0f 70 c1 ee               	pshufd	$0xee, %xmm1, %xmm0
     262: 66 0f 7e c8                  	movd	%xmm1, %eax
     270: 03 04 97                     	addl	(%rdi,%rdx,4), %eax
     279: 75 f5                        	jne	0x270 <sum_n+0x80>

警卫cmpl $7; ja通过轮,将旅行数量降低到8以下.andl $0x7ffffff8乘以8的乘数, 身体每次重复的32字节分为2个.xmm积累器,每个有4个32位行径,所以向量化因子为4和交差因子为2.paddd两个pshufd另外两个paddd其他movd缩小8条车道到一个整数.0x270识别器从后边找到循环,看到SIMD注册,从 读取路径数量paddd计算不同的积累器, 分开步骤0x20报告4 lanes x 2 accumulators = 8 elements per iteration,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,ldp q2, q3两个add v.4s现在addv s0, v0.4s其他fmov w8, s0现在,我们要去.

固定的旅行数量消除了循环.sum4是一个movdqu减少,movd没有后边.poly8无法向量化,因为每个回复取决于前一个积累器,所以编译器将其打开8次:8次负载在 0 到 之间0x1c其他%rdi后面是5的转移和减值,acc * 31识别仪从一个基地计算连续的负载,并报告loop unrolled by 8, no back edge在Aarch64上,同样的载荷抵达了四个.ldp两对.

关闭向量表仪-fno-vectorize -fno-slp-vectorize其他sum_n变得一个由8解开的尺度循环,addl k(%rdi,%rdx,4), %eax其他addq $8另外还有一个剩余的循环.sum4识别器报告unrolled loop随着copies: 8其他indexStep: 8对于第一位和copies: 4现在,我还没有说过.

课33发现了两个后边total在-O2它们是两个循环,这个课程给它们命名:一个向量体和一个源循环的 skalar 余分.一个后边是只有当目标能够再次达到它时的循环.is_loop()检查,因为digest在AArch64上包含非循环的共享默认区块的倒向分支,并且识别器的第一个版本将它们计算为向量循环.

成为图书馆通话的循环

LoopIdiomRecognize将存储零的循环转换为调用到memset根据-O1升.fill在-O2是testl %esi, %esi; jle; movl %esi, %edx; xorl %esi, %esi; jmp memset@PLT调用是搬迁的后续跳跃R_X86_64_PLT32 memset-0x4并且函数是retq识别器报告说store loop recognized as memset(...)并且对源循环进行验证,因为源头从来没有命名memset现在,我们要去.

copy16电话memcpy随着16的常数, x86的后端将其扩展为movups (%rsi), %xmm0; movups %xmm0, (%rdi)只有8家店铺memcpy在-O0同样的调用是两个8字节movq两对,与-fno-builtin电话仍然是电话,jmp memcpy@PLT随着movl $0x10, %edx其他fill它们的循环保持在循环中,然后向量化器将其转化为两个16字节的循环.movups认出器报告vectorized store loop: 2 x 16-byte stores = 32 bytes per iteration为了这座建筑.

消失或变成跳跃的电话

tail_caller收益tail_target(x + 1)在-O0现在是addl $0x1, %edi; callq tail_target@PLT; addq $0x10, %rsp; popq %rbp; retq在-O2现在是incl %edi; jmp tail_target@PLT现在,我们在Aarch64上跳跃,我们可以看到一个叫做"Aarch64"的字幕.b tail_target随着R_AARCH64_JUMP26识别器处理一个jmp或b课程33的递归下降必须把它视为函数出口,因为目标是函数之外的.

twice_sum电话leaf_add在-O0呼叫就在那里,callq leaf_add@PLT从-O1公司的leaf_add现在leal (%rdi,%rsi), %eax现在,它在里面出现了.twice_sum没有任何电话.-O0标记每个函数optnone其他noinline识别器从来源知道twice_sum电话leaf_add电话号码,找不到电话号码或尾声跳到它,然后在电话号码器中寻找电话号码的体形:add w8, w1, w0在AArch64调用器匹配中add w0, w1, w0一个匹配是证据.

dispatch结合了两者:一个16个进口跳机,每个案例都是movl %esi, %edi; jmp fN@PLT识别器报告了一个开关和16次尾声调用,所有这些都与源头的16次调用进行了验证.

成为数据的分支移动

clamp_max是movl %esi, %eax; cmpl %esi, %edi; cmovgl %edi, %eax评价者名字%edi作为a其他%esi作为b记录了比较,并读了cmovg作为a > b ? a : b根据本文的规定,abs_int是movl %edi, %eax; negl %eax; cmovsl %edi, %eax字母是-x < 0 ? x : -x据Aarch64写为cmp w0, #0; cneg w0, w0, mi字母是x < 0 ? -x : x现在,我们要去.is_pos是xorl %eax, %eax; testl %edi, %edi; setg %al现在,我们要做什么?(x > 0)这些是-fif-conversion根据GCC的说法,cmov在LLVM中选择.X86CmovConversion通过可以转换一个cmov由于同一个来源可以显示一个循环中的任何一个形状.

在-O0没有一个.clamp_max是一个比较,jle认知器没有报道任何信息.-O0其他branchless select其他-O1图片显示的这一点.

mul9是一个leal (%rdi,%rdi,8), %eax认证器报告strength reduction随着x * 9由于源乘以9 ,它只有当源有因数时才会这样做;lea只有一个表的指数,

两个看起来像语的硬化特征

堆保护器是一个编译功能,它留下了一个形式, 代理将其它读取为程序逻辑. canary.c编译为-fstack-protector-strong对于x86-64 Linux 提供:

text       7: 64 48 8b 04 25 28 00 00 00   	movq	%fs:0x28, %rax
      10: 48 89 44 24 40               	movq	%rax, 0x40(%rsp)
      26: 64 48 8b 0c 25 28 00 00 00   	movq	%fs:0x28, %rcx
      2f: 48 3b 4c 24 40               	cmpq	0x40(%rsp), %rcx
      34: 75 05                        	jne	0x3b <with_canary+0x3b>
      3b: e8 00 00 00 00               	callq	0x40 <plain_leaf>
		000000000000003c:  R_X86_64_PLT32	__stack_chk_fail-0x4

%fs:0x28是stack_guard在Glibc的tcbhead_t位的位,getIRStackGuard序言复制缓冲器上方的卫报,表尾比较,失败路径调用__stack_chk_fail在AArch64 Linux上,卫兵来自全球.__stack_chk_guard通过 GOT adrp其他ldr没有成功.bl __stack_chk_fail识别器报告了警卫源,插槽,比较,和失败电话.plain_leaf没有,没有鱼.strong通过配列保护函数,alloca调查说明还记录了MSVC表格在/GS饼干与%rsp经过检查__security_check_cookie;该机器上没有MSVC连接器,因此这种形状仅从编译器组装输出中描述.

-fcf-protection=full子endbr64字节f3 0f 1e fa它是对处理器的间接分支的降落台,nop基德拉的x86-64函数启动模式包括endbr64预定变体push rbp; mov rbp, rsp鉴定器报告它为cet landing pad函数开始于四个不寻常字节,

碎者所折叠的东西

作为重写规则,Ghidra的解译器将它们命名为ruleaction.cc现在,我们要去.RuleDivOpt是"将INT_MULT转换为INT_DIV或INT_SDIV",其中包含了表格sub( (zext(V)*c), d) >> e => V / (2^n/(c-1)) where n = d*8 + e签署的变体sext其他s>>现在,我们要去.RuleModOpt是"简化优化INT_REM和INT_SREM的表达式". RuleDivTermAdd处理补充补充,RuleSignDiv2转(V + -1*(V s>> 31)) s>> 1进入V s/ 2其他RulePositiveDiv课程37捕获了第33课的Ghidra 12.1.4 课程33grade在-O1输出结果是uVar2 = param_1 / 10 - 6;部门已经被折叠了.

文文的pypcode4.0.1使用了Ghidra使用的SLEIGH规格,并且code/fixtures/idioms-x86_64-O2.lifts.json具有原始P码div10s: 112条说明书,其中7条是IMARK开启每一个指令的标记,其中包括RAX = RAX * unique[95a00:8]之后unique[95a00:8] = sext(0x66666667)现在RCX = RCX >> unique[d1a00:4]其他RAX = RAX s>> unique[c5c00:4]现在,我们要去.shrq执行41项行动,sarq它们大部分都计算着旗.RuleDivOpt通过将死码删除,将没有任何东西读取的标志删除.

发动了10.0.3的攻击.-O2标题idioms-x86_64-O2.angr.json文件的解译器输出为20个函数.div10u是return a0 / 10;没有折叠签署的表格:div10s是v1 = a0 * 1717986919; return ((long long)(v1) >> 0x22) + (v1 >> 63);其他mod10s是a0 - (...) * 2 * 5为了div7u它打印了(a0 / 7 + ((a0 * 6 / 7 & 0xffffffff) >> 1 & 0xffffffff) & 0xffffffff) >> 2它们可以被编译成数值,mul9为了a0 * 9现在linear_switch为了(a0 < 7 ? a0 * 10 + 10 : 0xffffffff)现在clamp_max为了((int)a1 < (int)a0 ? a0 : a1)现在is_pos为了a0 > 0其他fill为了return memset(a0, 0, a1);它打印了table_switch作为一个负担*(int *)(4 * a0 + 4195400)没有价值,sum_n随着_mm_add_epi32其他_mm_shuffle_epi32基本的基础知识,tail_caller作为tail_target(); return;没有任何证据.digest跳板的表变得goto *((void *)(...))通过表格算法写出.

雷达雷达2.2.4和里津0.9.1从源入研究缓存,pdc印记div10s作为rax = rax * 0x66666667; rcx >>>= 0x3f; rax >>= 0x22; eax += ecx无折叠的注册级伪C,table_switch作为eax = dword [rcx + rax*4]具有一个RELOC 32 .rodata评论lea子没有内置的分解器;aoi打印了该 号的 RzILimul作为一个128位产品. lldb-1703.0.236.21 载入ELF对象及其disassemble -n div10s打印了7个指令llvm-objdump由于两个都使用LLVM的拆除器. Capstone 5.0.7解码相同的字节,并报告如ret其他mode64它们的文档描述了这种重写,微码优化和MLIL到HLIL的阶段,这本课程没有关于它们折叠的语言的权利的权利.

编译器 Explorer 是向前方向的工具.idioms.c选择"x86-64 clang 18.1.0"或"ARM64 GCC 14.2.0"等编译器输入,设置旗.服务列出了约1050个C编译器输入,并有一个API:GET /api/compilers/c返回标识符和POST /api/compiler/<id>/compile通过这个工具来进行分类,人口计数,链条条件和总结语法.使用它来生成GCC输出,这台机器无法产生,并比较GCC的跳跃表门和-O2通过电源输出,GCC 12中的向量化默认设置.

建立它

运行演示:

bashcd phases/13-tools-and-protocols/39-compiler-idioms-and-optimization
python3 code/main.py

输出是一个JSON报告.对于每个18个构建中,它列出了每个函数及其语法,包括类型,源形式,证据地址,验证和细节字段.它以每个构建的类型矩阵结束,-O2其他-O3建筑物和建筑物之间的比较unverified列表.

预期的形状:

json{
  "builds": {
    "idioms-x86_64-O2": {
      "arch": "x86_64", "level": "O2", "flags": "x86_64",
      "functions": {
        "div7u": [{"kind": "division", "source": "x / 7", "evidence": ["0x30", "0x32", "0x39", "0x3d", "0x3f", "0x41", "0x43"], "verified": true, "form": "magic multiply with add fix-up", "magic": "0x24924925", "effectiveMagic": "0x124924925", "shift": 35, "...": "..."}],
        "table_switch": [{"kind": "switch", "source": "switch (k) as lookup table", "table": ".rodata+0x28", "entries": 7, "values": [10, 20, 30, 40, 50, 60, 71], "...": "..."}],
        "sum_n": [{"kind": "vector loop", "lanes": 4, "accumulators": 2, "elementsPerIteration": 8, "...": "..."}]
      }
    }
  },
  "kindMatrix": {"idioms-x86_64-O2": {"division": 5, "modulo": 3, "switch": 6, "vector loop": 3, "...": "..."}},
  "sameCodeAtO2AndO3": {"x86_64": {"identical": true}, "arm64": {"identical": false, "differences": ["..."]}},
  "unverified": []
}

阅读code/main.py在此顺序:

  1. parse_listing()转换了llvm-objdump -d -r文本将其分为两个架构的函数,指令和附加位置.
  2. ElfFile阅读部分,符号,.rela.*报名,以及words()阅读表格的输入.
  3. magic_numbers()现在divisor_from_magic()其他evaluate_division()作为指令序列,我们可以将这些指令列为
  4. parse_source()从C文件中提取了验证需求的构成.
  5. Evaluator是一个区块对区块的象征性通行,
  6. find_divisions()其他find_modulos()它们可以通过乘法,转移,固定,反乘法和两种形式进行.
  7. find_switches()类别类别:跳跃表,查找表,线性地图,选择链,比较树和比较链.
  8. find_vector_loops()现在find_unrolled_loops()现在find_tail_calls()现在find_inlined_calls()现在find_selects()现在find_memory_builtins()现在find_stack_protector()现在find_cet()其他find_strength_reduction()覆盖其余的.
  9. analyze_function()通过测试,将每个语法的结果名称输入到下一个评估符.

现在还有三个命令.python3 code/main.py scan FILE.objdump.txt [FUNCTION]列出一个列表,并将对象和相匹配的C文件找到在旁边. python3 code/main.py magic DIVISOR [signed]打印乘法,转移,错误,以及身份是否保留边缘值. python3 code/main.py figure-data打印了图像嵌入的数据.

用它

在你建造的新物体上使用识别器.

  1. 捕获llvm-objdump -d -r识别器从列表标题中读取架构.
  2. 跑步scan在列表上,阅读unverified它们都是源解析器未见的构建,或者识别器误读的形状,
  3. 对于每一个部门,阅读algorithm什么时候?clangMatchesAlgorithm如果是错误的,那么编译器有理由你还不知道;写下常数.
  4. 对于每一个开关,lowering成为树,或树成为表,标志着一个可以从LLVM来源命名的门.
  5. 阅读每一个循环elementsPerIteration其他scalarRemainderLoop后面两边的课33报告是一个源循环.
  6. 运行一个解译器,引用其文本,并记录它折叠的.

互动实验室

实验室A:取出一个魔术数字,然后找到它

关于code开放字符串:

bashcd phases/13-tools-and-protocols/39-compiler-idioms-and-optimization/code
python3 -q

运行:

pythonfrom main import *
magic_numbers(10), magic_numbers(10, signed=True)
magic_numbers(7), magic_numbers(7, signed=True)
divisor_from_magic(0x66666667, 34, signed=True), divisor_from_magic(0x66666667, 33, signed=True)

计算ceil(2^35 / 10)在第一次电话之前,在纸上.unsigned div13(unsigned x) { return x / 13; }转载到idioms.c打造它-O2随着命令在build_fixtures.sh查看列表中的常数magic_numbers(13)现在,我们要去.

实验室B:桌子和树

pythonlisting = load_listing("idioms-x86_64-O2.objdump.txt")
elf = load_elf("idioms-x86_64-O2.o")
sources = load_source("idioms.c")
[i.to_dict() for i in analyze_function(listing, "table_switch", elf, sources)]
[i.to_dict() for i in analyze_function(listing, "sparse_switch", elf, sources)]
elf.words(".rodata", 0x28, 7)

改变71回到70在本文中table_switch修建在-O2换一个案例值为1000,然后看它保持为ArrayKind桌子.

实验室C:循环在三个设置

pythonfor name in ("idioms-x86_64-O1", "idioms-x86_64-O2", "idioms-x86_64-novec-O2"):
    print(name, [i.source for i in analyze_function(load_listing(name + ".objdump.txt"), "sum_n", load_elf(name + ".o"), sources)])

绘制控制流图sum_n在-O2按手指标标记保护器,面具,向量体,减速和剩余循环,并与detail其他地方

实验室D:共享分数

pythonfor name in ("idioms-x86_64-O1", "idioms-arm64-O1"):
    print([(i.kind, i.source, i.detail.get("sharedQuotient")) for i in analyze_function(load_listing(name + ".objdump.txt"), "digest", load_elf(name + ".o"), sources)])

阅读-O1列出digest然后解释为什么AArch64构建只需要一个.

实践实验室

增加一些小技巧,扩展识别器.

要求:

  • 写下popcount现在ctz其他rotl它们可以在GCC的结构和转换或旋转-O2随着和没有-mpopcnt -mbmi它们的列表.
  • 认识一个0x55555555现在0x33333333现在0x0f0f0f0f现在0x01010101人口数量序列TargetLowering::expandCTPOP发射,并报告为popcount作为证据,
  • 认识rep bsfl作为tzcnt- 或是...bsf编码和roll %cl它们都被转换成转换器,然后将它们解码到构建器.
  • 添加新的类型parse_source()检查,所以一个popcount语言只有当源头打电话时才会得到验证__builtin_popcount现在,我们要去.
  • 添加一个每种语法测试,读取器件列表中的常数.

成功意味着python3 code/main.py报告了三种新型verified: true在两个构建中,套房仍然报告一个空白unverified列表.

教训艺术品

这一课包括outputs/skill-compiler-idioms-and-optimization.md通过它将列表,对象和其源头转化为一个验证的语法报告,并进行解译器比较.它需要每个索赔的证据地址,每个分数的魔术数字衍生,每个交换器的降低类型和输入源,每个语法的引用解译器线.

检查

运行演示和确定性套件:

bashcd phases/13-tools-and-protocols/39-compiler-idioms-and-optimization
python3 code/main.py
python3 -m unittest discover -s code/tests -v

验证应证明:

  • 列表解析器读取25个函数-O2它们是对象,保持字节,并附加R_X86_64_PC32 .rodata+0x24对于leaq在table_switch
  • 电脑电脑阅读器返回[10, 20, 30, 40, 50, 60, 71]其他.rodata+0x28其他地方.rela.rodata
  • magic_numbers收益0xcccccccd随着班350x66666667其他0x124924925附35号和附加表格,0x55555556含32个,0xaaaaaaab具有33
  • 认同对每一个从2到64的分数的32位边值保持,而不是两次的权力,签名和未签名
  • divisor_from_magic能从对子中恢复10,5,7和3的情况,然后拒绝错误的对子
  • 增加固定序列计算n // 7确切地
  • 四个x86-64分区报告右乘法,转移,分数和形式,四个AArch64分区解码mov其他movk
  • -O0使用divl其他idivl现在-O0 -fast-isel=false使用乘法,digest在-O0混合了两种
  • 模块是被认可为复制回来,msub两级电力sdiv另外mul另外subs其他设备idivl
  • 共享的分数保持分数和模块的值-O1
  • 五个降低分类为-O2在两个架构上,配方,值,目标,转点和选项表达式
  • -O0交换机是跳机在x86-64和比较链在AArch64
  • 其他digest交换机是跳转表,然后是折叠因子16的查找表,然后是x86-64上有四个不同的目标的跳转表,以及AArch64上具有9到10的范围测试的比较树
  • -fno-jump-tables它们的结果是相比树,dispatch读取其目标从x86-64上的转移和AArch64上的压缩字节表
  • 矢量循环报告4条条路,2个积累器,每次回复的8个元素,面具,保护器,减速和剩余循环
  • 固定路环完全向量化或完全解,-fno-vectorize截至8
  • -O1没有向量化-O2其他-O3在x86-64上,它们是相同的,在AArch64上,它们只有在call_switch在哪里-O3包含17个指令,-O2只有18个
  • -fno-builtin转换了商店循环成向量化商店循环,并保持memcpy一个电话
  • 后尾呼叫,memset换取,直线16字节的副本,-O0两动副本被识别
  • 的是从-O1根据其体状
  • 选择解码到a > b ? a : b现在-x < 0 ? x : -x现在(x > 0)其他sum % 10 < 5 ? sum % 10 : 5
  • 堆保护器报告%fs:0x28子,子,__stack_chk_fail其他endbr64报告CET构建的每个功能
  • 源解析器读取函数指标参数,开关情况,调用和蓄积器名称
  • 每个构建中的每种语法都得到验证,
  • 互动图像包含分析产生的相同数据

生产失败模式

FailureWhat the weak tool reportsWhat the analysis must prove
Multiply by a large odd constant"multiplies by 1717986919"The constant is ceil(2^s / d) for one d, the error satisfies the bound, and the shift is the smallest valid one
Add fix-up after a 32-bit shiftFive arithmetic instructionsThe multiplier is 33 bits wide and the tail is the add form of the same method
Same multiplier, different shiftThe same divisorThe shift selects the divisor: 34 is 10, 33 is 5
Power-of-two signed modulo"adds 7, masks, subtracts"The bias rounds toward zero and the result is x % 8
Indexed load from .rodata"a jump table"A lookup table of values, with entries read from the bytes at addend plus 4
Affine arithmetic and a cmov"computes 10k + 10"A switch lowered as a linear map, verified against every source case
Compare chain with ordered splits"a sequence of ifs"A binary search over the case constants with the pivots named
Backward branch to a default block"a loop"The target leaves the region with an unconditional jump; it is a shared tail
Two back edges in one function"two loops"One source loop: the vector body and its scalar remainder
jmp memset@PLT with no ret"the function has no return"A tail call that returns through the callee, replacing a store loop
Callee missing from the call graph"never called"Inlined: the callee's body shape appears in the caller
%fs:0x28 load and compare"reads thread-local data"The stack protector canary, with the fail path to __stack_chk_fail
Four odd bytes at function start"data before the function"endbr64, the CET landing pad
Decompiler shows raw arithmetic"the tool is wrong"The tool did not apply a fold; the derivation gives the source form and the quoted output records the gap

运营规则

根据其形状,运行后方衍生,并写出证明它的常数. 报告每种语法,其证据地址和解码的源形式, 根据你编译的源, 验证每一个, 并引用每个解译器印发的相同字节.

进一步阅读

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.