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

来源:互联网 发布:电子地图绘制软件 编辑:程序博客网 时间:2024/06/03 16:24

1.15. 循环

1.15.1. 简单例子

x86

x86中的LOOP指令,检查ECX中的值,如果不为0就不断跳到标签重复执行代码。LOOP并不是很方便,一般的编译器也不会用它,所以如果在代码中出现就一定是手写的。
对于for循环中的i值,在x86中,代码的逻辑就是,每次检查EAX的值是否是i的最大值,如果是,就跳出循环,否则不断跳回代码初始位置。

GCC的实现也差不多。上述都是未优化版本。

如果我们把/0x优化标签打开,则此时i不会存在栈中了,而是使用一个单独的寄存器ESI来存放i的值。这是MSVC的版本。

如果用GCC,要用-03参数来打开优化。简直了,GCC简单粗暴,就从2到9全部执行一遍,call printing_function写了八遍。这叫做loop unwinding。如果我们把i的上限提高到100,则GCC会老老实实用上述的循环方式,其中用EBX来分配i的值。

x86: OllyDbg

用ESI来表示i控制循环。ESI最大值为9。

CMP ESI, 0AJL xxxx

x86:tracer

tracer.exe可以手动追踪。

找到PUSH ESI的地址,然后开始追踪。在该地址设置断点,tracer会打印出寄存器的状态。tracer.log

还可以生成载入IDA的文件,查看反汇编和指令执行情况。

ARM

未优化的Keil(ARM模式)

i存在R4寄存器,MOV R4, #2初始化i。

MOV R0,R4BL printing_funcion

以上两条构成循环体

CMP R4,#0xABLT loc_35c

以上两句是比较和分支

优化的Keil(Thumb模式)

差不多,去掉了一些不必要的跳转。

优化的Xcode(LLVM)(Thumb-2模式)

直接把循环展开,还把f()函数内联了,直接调用其中的printf函数,展开了8次。在比较简单函数的情况下用这个比较好。

ARM64:优化的GCC

ARM64:未优化的GCC

这两个似乎没有任何区别。

MIPS

初始化i之后,先检查i的值,再执行循环体。

有些编译器在知道循环体肯定会执行的情况下,把检查i值和循环体执行对调。

1.15.2 内存复制步骤

实际中的内存复制每次迭代复制4或8字节,使用SIMD、向量化。我们给出了一个简单的my_memcpy函数实现,每次复制一个字节。

直接的实现

x64

RDI 目标地址RSI 源地址RDX 块大小mov cl, BYTE PTR [rsi+rax]mov BYTE PTR [rdi+rax]

加载到RSI+i,存储到RDI+i

ARM64

X0 目标地址
X1 源地址
X2 块大小
X3 存放i

Thumb模式

R0 目标地址
R1 源地址
R2 块大小

ARM的ARM模式

充分利用了条件后缀指令。
LDRBCC, STRBCC, ADDCC, BCC

MIPS

新指令:LBU, Load Byte Unsigned,SB,Store Byte。

1.15.3. 结论

ARM中通常用R4表示计数器变量。(类似x86中的ECX)

add [counter], 1 相当于MOV REG, [counter]INC REGMOV [counter], REG

如果循环次数不多,用EBX=[counter]

有时编译器会调动语句块位置,用跳转连接。

1.15.4. 练习

http://challenges.re/54
http://challenges.re/55
http://challenges.re/56
打印出从1到100。
http://challenges.re/57
打印1,4,7,10一直到…

1.16. 简单的C字符串处理

1.16.1. strlen()

其实现是用while()。

const char *eos=str;while(*eos++);return(eos-str-s);

这里利用了str以\0结尾的特性

x86

未优化的MSVC

第一个MOVSX从内存中一个地址读取byte放到32位寄存器。MOV with Sign-Extended,8-31位位数字,1-8为符号补位。对于有符号数的复制非常有用。

未优化的GCC

用MOVZX代替了MOVSX。拓展部分用0填充。

可用来替换指令对xor eax, eax/mov al, […]

SETNA:if ZF==0,设置AL为1

SETNZ al,接下来几条表明如果al不为0,跳转到loc_80483F0。

优化的MSVC

EDX存放指向字符串的指针。

EDX放到EAXCL = *EAXEAX++CL==0?

如果不是就继续循环,如果是,计算eax和edx的差,再减1即字符串长度。

优化的MSVC+OllyDbg

优化的GCC

NOT把所有位取反。

ARM

32位ARM

未优化的Xcode4.6.3(LLVM)(ARM模式)

两个变量:eos, str。先把两个变量存到栈上。循环从loc_2CB8开始,eos->R0,R0+1->eos,LDRSB与x86中的MOVSX相同,从内存地址读并拓展到32位。string->R0,然后是CMP和BEQ。

优化的Xcode4.6.3(LLVM)(Thumb模式)

不一样的是不需要栈了,直接把str放在R0,eos放在R1。

LDRB.W R2, [R1], #1   先将R1加载到R2,再R1+1MVNS类似x86中NOT,所有位取反。MVNS R0, R0ADD R0, R1

这两条达到str-eos-1效果。

优化的Keil(ARM模式)

与上面的基本差不多,不过计算str-eos-1的方法发生变化。

SUBEQ R0, R1, R0SUBEQ R0, R0, #1

ARM64

优化的GCC

未优化的GCCC

MIPS

没有NOT指令,有NOR,=OR+NOT。

1.17. 替换算术指令

ADD和SUB可以互换,LEA常用于简单算术运算。

1.17.1. 乘法

使用加法实现乘法

return a*8    用了3次加法实现add eax, eax   这条指令出现三次

使用移位实现乘法

a*4    shl eax, 2   左移两位即可

ARM:LSL r0, r0, #2

GCC:SLL r0,a0, 2

使用移位、减法、加法实现乘法

即使是乘7或17也能完成(不用乘法)

x86

a*7      EAX=EAX-ECX=ECX*8-ECX=ECX*7=A*7a*28     EAX=a*7,EAX<<2=(a*7)*4=a*28a*17     EAX=EAX<<4=EAX*16=a*16         EAX=EAX+a=a*16+a=a*17

ARM中也一样,与x86实现基本相同。

Thumb模式不太一样,a*28没法优化,要用MULS。

MIPS的a*7和a*17一样,a*8=a0<<5-a0<<2=a0*32-a0*4。

64位

x64

a*7=a*8-aa*28=RID<<5-RDI*4a*17-a<<4+a

ARM64

a*7=a<<3-aa*28=a<<5-a<<2a*17=a+a<<4

Booth乘法算法

使用加法和移位替换乘法

1.17.2. 除法

使用位移实现除法

a/4     shr eax, 2

ARM中是LSR r0, r0, #2

1.17.3. 练习

http://challenges.re/59
其实就是a*7的实现。

0 0
原创粉丝点击