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

来源:互联网 发布:borderlands 2 mac 编辑:程序博客网 时间:2024/06/07 21:57

1.13. 条件跳转

1.13.1. 简单例子

两个函数,由一些if条件语句组成。没有then。

x86

x86+MSVC

f_signed()函数,接收int类型作为参数。先后用了eax、ecx、edx来完成三次条件的比较。第一个JLE表示如果小于等于就跳转。第二个JNE表示如果不等于就跳转。第三个JGE表示如果大于等于就跳转。

f_unsigned()函数,接收unsigned int作为参数。程序流程差不多,但是什么与上一个函数不一样呢?跳转指令不一样。这次用的是JBE(小于等于)、JAE(大于等于)。所以我们可以看出来,JGE和JLE用于有符号整数,而JBE和JAE用于无符号整数。

main()函数就没啥可说的了,都一样的。

x86+MSVC+OllyDbg

可以查看flag寄存器,当CMP指令完成的时候,flag寄存器会变化。比如JBE发生跳转的条件是CF=1或ZF=1。JNZ跳转的条件是ZF=0。JNB跳转的条件是CF=0。

JLE跳转的条件是ZF=1或者SF≠OF。JGE跳转的条件是SF=OF。

x86+MSVC+Hiew

试着patch可执行文件。要让a==b的代码永远都执行无论输入是什么。要确保第一个跳转一定执行。第二个跳转一定不执行,第三个跳转一定执行。

未优化的GCC

除了把printf()换成puts(),几乎是一样的。

优化的GCC

善于观察的人可能会问,既然flags有相同的结果,为什么还要用CMP指令多次比较?优化的GCC就只用了一次CMP,后面紧跟着三个JG, JE, JGE。并且,用JMP puts替代了CALL puts/RETN。

ARM

32位ARM

优化的Keil (ARM模式)

f_signed函数的情况:

ARM模式中的很多指令必须在指定标志寄存器值为1时才能执行。
ADDAL,加法指令,AL = Always,没有其他条件的执行。B是非条件跳转,如果加AL后缀也有永远执行之意。ADDGT的原型就是ADR,但要看之前的CMP结果为大于。BLGT原型就为BL,GT后缀表示CMP结果是大于。ADRGT的作用是把字符串“a>b\n”载入R0,BLGT调用printf()。鉴于前面的指令是CMP R0, R4,带GT的指令是否执行取决于R0是否大于R4。R0里面就是a,R4里面是b。

ADREQ和BLEQ的后缀EQ,是看CMP结果是否相等。

LDMGEFD原型是LDMFD,意味着大于或等于。a>=b会执行。条件不满足,转而执行下一条LDMFD SP’, {R4-R6, LR},恢复R4-R6和LR。

f_unsigned函数是一样的,用ADRHI, BLHI, CDMCSFD代替了之前的指令,即无符号版本。

x86中没有类似指令,只有CMOVcc,与MOV作用一样,但前提是前面的CMP是特定结果。

优化的Keil(Thumb模式)

Thumb模式中,只有B指令可以添加条件后缀。BLE、BNE、BGE含义都很容易理解。这是有符号版本。无符号版本为BLS(小于等于)和BCS(大于等于)。

ARM64:优化的GCC

其实差不多。其实就是加上了许多标签,利用标签进行跳转。这其中有很多死代码,不会执行到的。

练习

优化代码,去掉dead code。

MIPS

MIPS的一个特性是,没有flags寄存器。这样做是为了简化数据依赖分析。
与x86中SETcc类似的指令:SLT(如果小于就设置,有符号版本),SLTU是无符号版本。把目标寄存器设置为1如果条件成立,否则设置为0。然后这个目标会用BEQ来检查(相等时分支),BNE(不等时分支)。SLT和BEQ常成对使用。

BEQZ是IDA的省略,意为BEQ REG, $ZERO, LABEL,如果等于0就分支。

1.13.2 计算绝对值

简单的函数:

int my_abs(int i){    if (i<0)        return -i;    else        return i;}

ARM

优化的MSVC

与GCC基本相同。

优化的Keil:Thumb模式

没有取负值指令,用RSBS,比如放在r0中,用0-r0。

优化的Keil:ARM模式

RSBLT,当结束小于0时使用reverse subtract指令。

未优化GCC(ARM64)

ARM64里有NEG指令来取负。

MIPS

优化的GCC:BLTZ是一个新指令:如果小于等于0,就分支。
用NEGU,U表示整数溢出时不产生异常。

1.13.3. 三元条件操作符

C/C++中有这样的语法:expression ? expression : expression

例子

const char* f(int a){    return a == 0 ? “it is ten” : “it is not ten”;}

x86

比较老的编译器就像if/else一样生成代码。

新的编译器更精确:使用了CMOVcc指令,比如MSVC中的CMOVE rax,rdx,rax中放不是10的,rdx中放是10的,根据比较结果决定是否用rdx替换rax。

ARM

优化的Keil的ARM模式中使用了条件指令ADRcc。

Thumb模式需要使用条件跳转指令,因为没有支持条件flags的加载指令。

ARM64

优化的GCC使用了条件跳转。因为ARM64没有支持条件flag的加载指令,如ADRcc(32位ARM)或CMOVcc(x86)。

不过这种模式有条件选择语句CSEL,然而GCC编译并没有使用这个(不够聪明)。

MIPS

GCC的不太智能,基本还是if/else的形式。

结论

为什么优化的编译器倾向于不使用条件跳转?速度。

1.13.4. 获取最小最大值

min和max两个函数的区别在于JGE和JLE。还产生了一个多余的JMP指令。

不分支

ARM的Thumb模式:分支指令是BGT和BLT,因为使用后缀,代码更简洁。

ARM模式中用MOVcc在条件满足时执行。

x86用CMOVcc指令。

64位情况

不需要从栈中加载函数参数,因为已经在寄存器中的。ARM64有CSEL,就像ARM中的MOVcc和x86中的CMOVcc。

MIPS

GCC的优化不是很好。

1.13.5. 结论

x86

CMP register, register/valueJcc true  ;cc是跳转条件false:    xxxxJMP exittrue:    xxxxexit

ARM

CMP register, register/valueBcc truefalse:    xxxxJMP exittrue:    xxxxxxexit

MIPS

BEQZ REG, label         是否为0BLTZ REG, label         是否小于0BEQ REG1, REG2, label   相等BNE REG1, REG2, label   不相等SLT REG1, REG2, REG3    小于和大于,有符号BEQ REG1, label         SLTU REG1, REG2, REG3   小于和大于,无符号BEQ REG1, label

如果条件跳转的语句块比较短,会使用一些带条件后缀的指令,如 MOVcc(ARM模式)、CSEL(ARM64)、CMOVcc(x86)。

ARM模式中有一些指令可用后缀:
instr1_cc
instr2_cc
Thumb模式有IT指令,可在之后接指令:
CMP register, register/value
ITEEE EQ 用来设置后缀
instr1 条件为true
instr2 false
instr3 false
instr4 false

1.13.6. 练习

用CSEL替代条件跳转。

0 0
原创粉丝点击