您的位置:首页 > 编程语言

《Reverse Engineering for Beginners》 - 第1章 代码模式 - 笔记(1.14)

2016-12-19 08:55 453 查看

1.14. switch()/case/default

1.14.1. case数量较少

x86

未优化MSVC

case数量较少时,相当于数个if/else语句结合。光看汇编代码很难判断源代码是什么。

优化后的MSVC

一些优化很有趣:把a的值放在EAX中,用它减去0,用来检查a是否为0。如果ZF设置了(减法结果为0),则JE触发。接下来会分别减1、减2,即分别与1,2比较。

另一点是,调用printf()不是用CALL,而是用JMP。字符串指针放在a变量(某个内存地址),这段代码只是手动设置栈内容然后跳转。实际上是为了速度而优化。

OllyDbg

可以识别case语句。

ARM:优化Keil(ARM模式)

也是按if实现的。出现了比较多的ADRcc指令,在特定条件下加载字符串地址到R0,下一条Bxx跳转到某个位置,进行函数调用。

ARM:优化Keil(Thumb模式)

没有后缀形式,所以与x86差不多。

ARM64:未优化GCC

用w0保存输入而不是x0,因为是int类型。

用ADRP/ADD将字符串指针参数传给puts()。

ARM64:优化的GCC

用CBZ代替了复杂的指令,如果w0是0就跳转。直接跳转到puts()。

MIPS

jr $t9 跳转到puts()

LW指令后常有nop,因为load delay slot。

结论:

少量case的switch()与if/else基本没有区别。

1.14.2. 大量cases

如果case较多,则用大量JE/JNE指令是不合适的。

下面给出了一个5个case的例子。

x86

非优化的MSVC

如果a大于4,直接跳到$LN1@f(错误信息)。

如果a小于等于4,把a乘以4,再与LN11@f地址相加,即用一个jmpDWORDPTRLN11@f[ecx*4]。

比如a=2,2*4=8,LN11@ftable+8就是LN4@f,就会跳转到$LN4@f的位置执行。

存在一个跳转或分支表。

npad指令,用来对齐标签到4byte,64位的是16byte。

OllyDbg

input先放到EAX,比较是否大于4,若小于等于,则放到ECX进行ECX*4+address跳转。

非优化GCC

唯一的区别是,乘以4这个操作用左移2位替代。然后从ds:off_804855c[eax]取地址跳转。

然后JMP EAX。

ARM:优化的Keil(ARM模式)

ARM模式所有指令都是4bytes。

ADDCC PC, PC, R0, LSL#2


先用R0左移2位即R0*4,然后加上PC再赋值给PC,即PC<-PC+R0*4。

本来PC值是180,则跳到180+R0*4。注意ADDCC指令位置是178,但执行时PC+2。这实际上是间接取址。

ARM:优化Keil(Thumb模式)

也是用跳转表实现。

BL _ARM_common_switch8_thumb


LR作为指向跳转表的指针。

MIPS

逻辑是一样的,利用跳转表。

结论:

MOV REG, input
CMP REG, 4
JA default
SHL REG, 2
MOV REG, jump_table[REG]
JMP REG

case 1:
xxx
JMP exit
…
case 5:
xxx
JMP exit
default:
…
exit
…
jump_table dd case 1
dd case 2
dd case 3
dd case 4
dd case 5


跳转指令:

JMP jump_table[REG*4]


1.14.3. 一个块中有很多case

值不连续,而且多个case对应一个代码块。

MSVC

有两个表,LN10@f指向a值与某个值的对应,是一个索引。LN11@f是指向代码块的指针。

GCC

只用了一个指针表。

ARM64:优化GCC

把第一个序号列为1。也是用两个表实现的映射。

1.14.4 Fall-through

如果有一个块不加break,用类似if/else实现,只要把代码块按顺序放置即可,然后不加jmp。

ARM64

其实逻辑是一样的。

练习

把call_printf放在最后,一条语句就够了。

call_printf
add esp, 4
jmp SHORT $LN9@f


这一段重复了五次。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: