Delphi汇编学习心得(不得闲) good
把以下4个前提搞清楚以后,学习和使用汇编才有意义:
--------------------------------------------------------------------------------------
前提1:寄存器说明:
EAX,EBX,EDX,ECX 通用寄存器,由程序员自己指定用途,也有一些不成文的用法:
EAX:常用于运算。
EBX:常用于地址索引。
ECX:常用于计数。
EDX:常用于数据传递。
EIP 指令寄存器,指出当前指令所在的地址。
ESP 栈指针,指向当前线程的栈顶(问题:是不是每个函数都有自己的栈,那每个线程都有一个栈是什么意思呢)。
EBP 栈基址指针,对调试起着很重要的作用(Base)。
EDI,ESI 没有规定作什么用,一般用在源指针和目标指针的操作
前提2:Register-Delphi默认模式
参数传递模式...前三个数据.eax,edx,ecx...超过三个参数部分.放在堆栈传递
头三个不大于4个字节(DWORD)的参数从左到右的传入EAX,EDX,ECX寄存器;接下去的参数按从左到右压栈。函数自己恢复堆栈。
浮点数总压栈,不管它所占的字节是多少。
对象方法总是有一个Self隐含参数,这个参数在所有的参数前面,即总是传给EAX(所以对于对象方法,程序员看到的第一个参数在EDX里)。
前提3:EBP基址指针的作用
EBP是基址指针寄存器:一般用来确认堆栈帧的起始位置,也就是指向栈底。也就是说,一般一个函数入口的地址也就存放在EBP中(所以一般在进入函数的时候将ebp寄存器内容压栈,即保存其函数的上级调用函数的栈基地址,以便于以后返回调用)。
前提4:查看汇编代码
可以使用Ctrl+Alt+C打开Cpu View的Cpu调试窗口看里面的汇编代码。
--------------------------------------------------------------------------------------
1. 对栈的研究
栈是向下增长的,即每压一次栈,栈顶的地址就减少一次,也可以说ESP的值就减小一次。
栈是线程相关的,每一个线程都拥有一个栈。
程序利用ESP可以很灵活地访问栈,不一定要执行PUSH和POP栈顶才会改变,直接操作ESP也可以改变栈顶,也就是说ESP决定了栈顶的值。栈是有最大值的,通过编程环境可以设置,超出最大值就会发生栈溢出。
看一个简单的例子,下面的指令是一条压栈指令,意思是将EAX的值压入栈中:
PUSH EAX
根据上面的性质,这条指令等价于下面的指令:
SUB ESP, 4
MOV ESP, EAX
Cliff:所谓的向下生长,就是下面的地址小(比如0x00000000),上面的地址大(比如FFFFFFFF)。分配栈的起始地址任意,但压栈的话,地址向下移动,地址值变小(就是在海平面压一块海绵下去,放一块海绵,地址就下降一些)。但是对于单个栈而言,栈就1M或者2M大小,在这个区间内怎么折腾都行,但不能任意扩大。但整个程序的运行,内存分配不见得是向下生长的,这里仅仅是栈。其实很简单,使用Delphi任意执行一条push语句,观察一下ESP值的变化就行了。
2. 函数如何被调用
其实很简单,就是一个跳转指令JMP,跳到函数的首地址去,并从那里开始执行指令。
比如下面的代码:
C := Add(10, 20);
按照上面的讨论,汇编代码应该如下:
MOV EAX, 10
MOV EDX, 20
JMP @Add
又遇到另一个问题:函数执行完后如何返回?为了解决这个问题,必须把 C := Add(10, 20)之后的指令地址保存起来。
MOV EAX, 10
MOV EDX, 20
PUSH [EIP + Len]
JMP @Add
大概有人觉得函数调用实在是很常用的事情,于是干脆把最后两条指令合成一条,变成了Call,所以最后的汇编代码如下:
MOV EAX, 10
MOV EDX, 20
CALL @Add
函数执行完后怎么在栈中找到返回地址?解决这个问题的关键点就是栈平衡。
假设它的代码是这样:
Function Add(a, b: Integer): Integer;
begin
Result := a + b;
end;
那么汇编代码就是这样:
ADD EAX, EDX
POP EDX // 问题:为什么是EDX?估计随便放的,执行完上一句Add指令以后EDX是闲置的,可以随便使用。执行函数前,把前一个函数的栈顶地址压栈了,现在只需简单出栈即可,出栈到任意一个寄存器以供跳转即可。跳转之后,这个寄存器存储的值也不再需要了。
JMP EDX
同样后两个指令太常用了,因此合成一条,成了Ret,最后的汇编代码是这样的:
ADD EAX, EDX
RET
从汇编角度函数调用大概就是如此。
结论:
① 未优化的Pascal代码与优化的汇编代码效率相差为45倍。
② 优化的Pascal代码与优化的汇编代码效率相差为20倍。
③ 优化的Pascal代码与未优化的汇编代码效率相差为2.9位。
④ 未优化的汇编代码与优化的汇编效率相差为7倍。
参考:
http://blog.csdn.net/suiyunonghen/article/details/2501737
http://blog.csdn.net/suiyunonghen/article/details/1909904
http://blog.csdn.net/suiyunonghen/article/details/1897142
http://blog.csdn.net/suiyunonghen/article/details/1897062
BASM for Beginners
http://dennishomepage.gugs-cats.dk/BASM-filer/BASMForBeginners1.htm
--------------------------------------------------------------------------------------
汇编中通用寄存器的目的
1、EAX和AX:累加器,所有的I/O指令用它来与外部设备传送信息
2、EBX和BX:在计算存储单元地址时常用作基地址寄存器
3、ECX和CX:保存计数值
4、EDX和DX:做四字或二字运算时,可以把EDX(DX)和EAX(AX)组合在一起存放一个四字或二字长的数据,在对某些I/O操作时,DX可以放I/O的端口地址
5、ESP和SP:堆栈栈顶指针。
6、EBP和BP:基址寄存器
7、ESI和SI:源变址
8、EDI和DI:目的变址
返回值(对于intel C++, Visual C++, GCC而言):
1. 如果是地址或者整数就放在eax中
2. 如果字节类型就放在al中
3. 如果是浮点数类型,就放在浮点数堆栈的栈顶
浮点数常用指令(float占用4个字节,double占用8个字节):
fld 压栈 fst 弹栈
fcom 比较 fnstsw 将协处理器的标志寄存器的内容拷贝到通用寄存器
fadd 加法 fdiv 除法 fmul 乘法 fsub 减法
例如:
float a=5.89+8.90
fld 40BC7AE1h ; 压栈 5.89
fadd 410E6666h; 加上 8.90
fst [esp+var_4]; 将浮点堆栈顶部的值弹到变量中
浮点堆栈一共占有8个单元,每个单元占8个字节,栈顶为st(0)或称st,栈底为st(7)
fld float对应的机器码是 0xD9
fld double对应的机器码是 0xDD
fld long double对于的机器码是 0xDB
除法运算比乘法慢10倍(不做优化的情况下),所以使用公式来优化:
a/b = (2^n/b)*(a/2^n)
VC++的Release版默认优化级别是O2