《Reverse Engineering for Beginners》 - 第1章 代码模式 - 笔记(1.7)
2016-12-05 10:41
260 查看
1.7. 带有参数的printf()
示例代码:#include <stdio.h> int main() { printf(“a=%d; b=%d; c=%d”, 1, 2, 3); return 0; }
1.7.1. x86
x86: 3个参数
MSVC
参数是以3,2,1的顺序入栈的。因为参数类型是int,是32bits大小,所以一个参数占4byte。除了参数,还要入栈字符串的地址,所以是一共4个阐述,在栈中占据了16byte。所以,我们看看函数调用之后的ADD ESP, X指令,用X除以4就可以得到参数的个数。以上针对cdecl调用约定和32位环境。注意有的时候编译器会在好几个函数调用完毕之后一起恢复栈。也就是很多个CALL之后才有ADD ESP, X指令,X的大小是此前所有调用函数参数大小总和。
MSVC和OllyDbg
发现printf()执行后ECX和EDX也有变化,说明函数内部机制用了这两个寄存器。如果不执行下一步,我们发现ESP和栈中内容都没有改变。这是因为cdecl调用约定,被调函数不恢复ESP,调用函数需要恢复ESP。继续执行下一条ADD ESP, 16指令,会发现ESP改变了,但栈中内容依然没变。GCC
用GCC编译后,我们可以看到与MSVC的不同之处。GCC编译出的汇编代码,并没有使用PUSH/POP来将参数入栈,而是先设置好esp的值(一个基址),再使用mov [esp+10h+var_4], 3这样的指令,将参数放在esp加偏移指示的内存地址,实际上就是把值放在了栈中。GCC和GDB
在printf()处下断点,然后查看esp指向的地址,可以证明该处放的是返回地址。xchg %ax, %ax等同于nop。
紧跟着返回地址之后存放的就是要打印的字符串。(地址递增)再后面放的就是1,2,3三个参数值。
然后执行到结束。查看寄存器值,printf()返回值放在EAX里,为13(打印字符串长度)。
x64:8个参数
输出1-8这8个数字。加上字符串一共是9个参数。MSVC
在Win64中,前4个参数放在RCX, RDX, R8, R9这4个寄存器中,其他的放在栈中。入栈的指令并不是PUSH,而是MOV。在32位中,4byte是一个基本单位。而在64位中,8byte是一个基本单位。
GCC
前6个参数用RDI,RSI,RDX,RCX,R8,R9传递,其他的用栈传递。字符串指针用EDI存储而不是RDI。在printf()调用前会把EAX清零。因为返回值即打印字符个数要放在EAX中。GCC+GDB
在printf()处断点。可以看到rdi的值就是字符串的地址。栈中第一个元素就是返回地址。然后余下的3个参数放在栈中。1.7.2. ARM
ARM: 3个参数
前4个参数用R0-R3传递。余下的用栈传递。这种方式与fastcall调用约定以及win64相似。32位ARM
Non-optimizing Keil (ARM模式)字符串放在R0中,1,2,3放在R1-R3中。调用printf函数后将R0中的值变为0。
Optimizing Keil (Thumb模式)
与ARM模式的不同就是把开头的STMFD指令换成PUSH,把结尾的LDMFD指令换成POP。
Optimizing Keil (ARM模式) + 去掉返回值的情况
在源代码中去掉return语句。发现生成汇编的结果完全不一样了。前面的STMFD和后面的将R0清零和LDMFD指令都没有了。调用printf的指令BL变成了B。B指令类似x86中的JMP,直接跳转到一条指令,而不处理LR。
这块代码为什么行得通?1)没有改变栈的内容和SP的值。2)调用printf是最后一条指令,下面就没有指令了,所以不需要返回。
这种类型的优化通常运用在最后一条指令是调用函数指令的情况。
ARM64
Non-optimizing GCC第一条指令STP会把FP(X29)和LR(X30)存在栈里。第二条指令ADD X29, SP, 0形成了栈帧。也就是把SP的值写入X29。接下来的ADRP/ADD指令对形成了指向字符串的指针,其中的lo12表示低12位,链接器会把LC1地址(即字符串地址)的低12位写入ADD指令的操作码。然后会把1,2,3这三个数放在w1,w2,w3,即32位。
优化的GCC会产生一样的代码。
ARM: 8个参数
加上字符串是一共9个参数。Optimizing Keil (ARM模式)
函数前言部分:STR LR, [SP, #VAR_4]!指令将LR存放在栈上,因为我们会在调用printf的时候使用这个寄存器。首先SP会减少4,然后LR会被放在SP指示的地址。与PUSH有点像。第二个语句SUB SP, SP, #0x14减少SP以在栈上分配20bytes的空间。因为我们需要通过栈传递5个32位的参数,每个参数4bytes,总共就是20bytes。其他4个32位参数用寄存器传递。将值5,6,7,8用栈传递。先存储在R0-R3里,然后用ADD R12, SP, #0x18+var_14指令把这4个值要存放的栈地址放到R12里。var_14是IDA定义的相当于-0x14,所以相当于把SP+4放在R12寄存器。下一条指令STMIA R12, R0-R3将R0-R3寄存器的内容写入R12指向的内存。STMIA是Store Multiple Increment After的缩写。
然后用栈传递值4。先存到R0,再存入栈中。
用寄存器传递数值1,2,3。分别放在R1-R3中。R0放字符串。
然后就可以调用printf()函数了。
函数结尾部分:ADD SP, SP, #0x14指令将SP恢复为原来的值,即清栈。LDR PC, [SP+4+var_4], #4将已存的LR从栈中加载到PC寄存器,让函数可以退出返回。与x86里的POP PC有点像。
Optimizing Keil: Thumb模式
与ARM模式差不多,还是换成了PUSH和POP指令,以及传参顺序不一样:先8,然后5,6,7,然后是4。Optimizing Xcode (LLVM): ARM模式
基本与之前的差不多,就是STMFA指令,与STMIB相同。这个指令增加SP中的值,然后将下一个寄存器值写入内存,而不是以相反的顺序。还有一个不同就是指令的分布是随机化的。编译器会为了效率做一些布置。编译器会把一些能够并行执行的指令放在一起。
Optimizing Xcode (LLVM): Thumb-2模式
与上一个例子几乎差不多,只是换成了Thumb模式特有的指令,比如PUSH和POP。ARM64
Non-optimizing GCC
前8个参数都被传递到X或W开头的寄存器。字符串指针需要64位寄存器,所以传递到X0。其他的参数都是int类型,所以传递到W开头的32位寄存器。第9个参数即数字8用栈传递。不能用寄存器传递大量参数,因为寄存器就那么几个。优化的GCC形成的代码差不多。
1.7.3. MIPS
3个参数
Optimizing GCC
这里与HelloWorld例子不同的是,调用了printf()而不是puts()。多出来的3个参数用寄存器5…7(A0…A2)。这些寄存器以A为前缀就表明了这些寄存器用来传递函数参数。如果用IDA加载的话,可以看到把LUI和ADDIU指令合并为LA。这个指令的作用是,加载字符串的地址并设置printf的第一个调用参数。
Non-optimizing GCC
没有优化的情况下有点冗长。总之步骤都是一样的:函数前言、加载字符串地址并设置第一个参数、设置余下的参数、加载printf地址、调用printf函数、设置返回值、函数结尾(通常恢复栈和设置指令指针)。8个参数
Optimizing GCC
前4个参数会被传递到A0…A3寄存器。剩下的通过栈传递。这种调用约定称为O32,是MIPS中最常见的一种。另一种N32会把寄存器用作不同的目标。SW即Store Word的简称(从寄存器存储到内存)。MIPS不能直接将值保存到内存,所以需要LI/SW指令对,即先将数值放入一个寄存器,再从寄存器把值复制到内存。可以发现传递参数的顺序很乱。Non-optimizing GCC
冗长一点。1.7.4. 结论
x86就是PUSH所有的参数到栈里,然后调用函数,最后恢复栈。x64如果用MSVC编译,会用RCX,RDX,R8,R9传递前4个参数,剩下的用栈传递。然后调用函数,最后恢复栈。如果用GCC编译,会用RDI,RSI,RDX,RCX,R8,R9传递前6个参数,余下的用栈传递,然后调用函数,最后恢复栈。
ARM用R0-R3传递前4个参数,后面的用栈传递。用BL调用函数,最后恢复栈。
ARM64用X0-X7传递前8个参数,余下的放在栈中。BL CALL调用函数,最后恢复栈。
MIPS的O32调用约定是用4−7即A0−A3传递参数,剩下的放在栈中。然后使用指令LW temp_reg, address of function和JALR temp_reg指令调用函数。
1.7.5. BTW
CPU对调用约定毫无洞察!它根本不管你用什么调用约定,它只管执行汇编指令。你完全可以使用任何一个寄存器,或者根本不用栈。
相关文章推荐
- 《Reverse Engineering for Beginners》 - 第1章 代码模式 - 笔记(1.8-1.9)
- 《Reverse Engineering for Beginners》 - 第1章 代码模式 - 笔记(1.18)
- 《Reverse Engineering for Beginners》 - 第1章 代码模式 - 笔记(1.10-1.12)
- 《Reverse Engineering for Beginners》 - 第1章 代码模式 - 笔记(1.4)
- 《Reverse Engineering for Beginners》 - 第1章 代码模式 - 笔记(1.15-1.17)
- 《Reverse Engineering for Beginners》 - 第1章 代码模式 - 笔记(1.14)
- 《Reverse Engineering for Beginners》 - 第1章 代码模式 - 笔记(1.1-1.3)
- 《Reverse Engineering for Beginners》 - 第1章 代码模式 - 笔记(1.5-1.6)
- 逆向基础8:循环结构-Reverse Engineering for Beginners
- (译)Cocos2d_for_iPhone_1_Game_Development_Cookbook:1.7使用Retina模式
- android-apktool - A tool for reverse engineering Android apk files - Google Project Hosting
- 设计模式笔记:单例模式(C++代码)
- 《大话设计模式》之--第1章 代码无错就是优?----简单工厂模式
- 创建型模式之 工厂、简单工厂、抽象工厂 简单图析和代码分析 笔记
- 通用代码学习笔记--单例模式
- Spark源码阅读笔记:Standalone模式集群核心角色代码浅析
- Java 基础一些代码练习笔记(策略模式)
- 大话设计模式-第1章代码无错就是优?-简单的工厂模式
- QTP笔记——代码控制回放模式
- 【笔记】Eclipse and Java for Total Beginners—007