《2022年2022年汇编与高级语言 .pdf》由会员分享,可在线阅读,更多相关《2022年2022年汇编与高级语言 .pdf(14页珍藏版)》请在taowenge.com淘文阁网|工程机械CAD图纸|机械工程制图|CAD装配图下载|SolidWorks_CaTia_CAD_UG_PROE_设计图分享下载上搜索。
1、汇编与高级语言1.汇编基础知识1.1.寄存器寄存器用途EAX,EBX,EDX,ECX 通用寄存器,由程序员自己指定用途,也有一些不成文的用法:EAX:常用于运算。EBX:常用于地址索引。ECX:常用于计数。EDX:常用于数据传递。EIP指令寄存器,指出当前指令所在的地址。ESP栈指针,指向当前线程的栈顶。EBP栈基址指针,对调试起着很重要的作用。EDI,ESI 没有规定作什么用,一般用在源指针和目标指针的操作。FR 标志寄存器,由多个标志位组成,存放运算结果的标志,比如借位,进位,是否为0 等等。FS 在 Windows中,FS:0 用来指向异常处理机制的链接头。说明:ESP和 EBP对高级语
2、言的函数实现起着非常重要的作用。FS是 SEH(Structured Exception Handling)中起重要作用的一个段寄存器,它的0偏移指向异常结构连表的表头,Windows在进行结构化异常处理时,就是从 FS:0 开始遍历异常结构并调用其中的异常处理函数的。1.2.堆栈堆是一块内存区域,一般用于内存的动态分配和释放,比如用New方法分配一个指针,此时即在程序地址空间的堆中分配了一块内存。又比如Delphi 的对象也是在堆中创建的。栈是一种先进后出的列表数据结构,在高级语言的编程中使用广泛,在低级语言中更是不可或缺的基础概念。栈也是一个内存区域,不过它具有快速灵活的特点,CPU 直接
3、提供指令去访问栈。从汇编的角度来看,栈具有如下的性质:栈有两个基础动作,压栈(PUSH)和出栈(POP)。栈是向下增长的,即每压一次栈,栈顶的地址就减少一次,也可以说ESP的值就减小一次。栈是线程相关的,每一个线程都拥有一个栈。程序利用 ESP可以很灵活地访问栈,不一定要执行PUSH 和 POP 栈顶才会改变,直接操作ESP也可以改变栈顶,也就是说 ESP决定了栈顶的值。栈是有最大值的,通过编程环境可以设置,超出最大值就会发生栈溢出。看一个简单的例子,下面的指令是一条压栈指令,意思是将EAX的值压入栈中:PUSH EAX 根据上面的性质,这条指令等价于下面的指令:SUB ESP,4 MOV E
4、SP,EAX 名师资料总结-精品资料欢迎下载-名师精心整理-第 1 页,共 14 页 -用下面的图表示指令的操作过程:2.调用规则2.1.从汇编的角度看函数调用汇编语言没有变量的概念,因此对函数的调用,第一个要解决的问题是参数要如何传递,有的将参数放在栈中,有的将参数放在寄存器中,对于参数压栈的还要确定是从最左边的参数开始压栈,还是从最右边开始,所有这些,就构成了调用规则的内容。第二个问题是函数如何被调用,其实很简单,就是一个跳转指令JMP,跳到函数的首地址去,并从那里开始执行指令。比如下面的代码:C:=Add(10,20);按照上面的讨论,汇编代码应该如下:MOV EAX,10 MOV ED
5、X,20 JMP Add 名师资料总结-精品资料欢迎下载-名师精心整理-第 2 页,共 14 页 -现在我们又遇到另一个问题:函数执行完后如何返回?在调用Add函数时,执行点跳到函数里面去,但当函数执行完之后,执行点必须返回到C:=Add(10,20)下面的语句,可是此时已经没有办法得到那个指令地址。为了解决这个问题,必须把 C:=Add(10,20)之后的指令地址保存起来,一般压到栈中是比较好的做法,汇编代码成了下面的样子:MOV EAX,10 MOV EDX,20 PUSH EIP+Len JMP Add EIP+LEN就是 JMP Add 的下一条指令的地址,现在当Add函数执行完毕后,
6、只要在栈中找到这个地址,执行点就可以回来了。大概有人觉得函数调用实在是很常用的事情,于是干脆把最后两条指令合成一条,变成了Call,所以最后的汇编代码如下:MOV EAX,10 MOV EDX,20 CALL Add 接下来看看 Add函数,函数执行完后怎么在栈中找到返回地址?解决这个问题的关键点就是栈平衡,不管函数对栈如何操作,但一定要保证在函数退出时栈现场和刚进来时的一样,这里包括栈顶和栈的内容一样。只要做到这一点,就可以确定在函数将返回时的栈顶值就是正确的返回值,我们只需要从栈顶弹出这个值,再执行一个跳转就行了。假设它的代码是这样:Function Add(a,b:Integer):In
7、teger;begin Result:=a+b;end;那么汇编代码就是这样:ADD EAX,EDX POP EDX JMP EDX 同样后两个指令太常用了,因此合成一条,成了Ret,最后的汇编代码是这样的:ADD EAX,EDX RET 从汇编角度函数调用大概就是如此。2.2.调用规则调用规则讨论的是函数的参数怎么传递,函数结果又是怎么返回,另外栈平衡由谁负责(函数本身或调用者)。下面就介绍几个比较常用的调用规则:RegisterDelphi 默认的调用规则,效率非常高,但规则很复杂,下面是它的简要规则:1.头三个不大于 4 个字节(DWORD)的参数从左到右的传入EAX,EDX,ECX 寄
8、存器;接下去的参数按从左到右压栈。比如函数:function Add1(I1:Byte;I2:Int64;I3:Integer;I4:Integer;I5:Integer):Integer;用汇编来调用就是这样的:var I:Integer;begin /I:=Add1(10,20,30,40,50);asm mov al,10 push 0 push 20 mov edx,30 mov ecx,40 名师资料总结-精品资料欢迎下载-名师精心整理-第 3 页,共 14 页 -push 50 call Add1 mov I,eax end;end;2.浮点数总压栈,不管它所占的字节是多少。3.对
9、象方法总是有一个Self 隐含参数,这个参数在所有的参数前面,即总是传给EAX。比如一个类中有一个方法:function Add2(a,b:Integer):Integer;用汇编调用如下所示:var I:Integer;begin /I:=Add2(10,20);asm mov eax,Self mov edx,10 mov ecx,20 call Add2 mov I,eax end;end;4.栈现场必须由函数自己清理。StdcallWindows API的标准调用规则,效率不高,但规则很简单:1.参数总是从右向左地压栈。比如,对于函数:function Add3(a,b:Integer
10、):Integer;stdcall;下在是调用的代码:var I:Integer;begin /I:=Add3(10,20);asm push 20 push 10 call Add3 mov I,eax end;end;2.栈现场必须由函数自己清理。Cdecl这是 C语言的标准调用规则,在Delphi 中很少需要用到这种规则,但Delphi 仍然提供了支持。1.与 stdcall类似,参数总是从右向左地压栈。名师资料总结-精品资料欢迎下载-名师精心整理-第 4 页,共 14 页 -2.栈现场必须由调用者清理。这就为可变参数提供了可能,举一个例子,C语言里面有一个运行时函数叫Sprintf,类
11、似于 Delphi 的 Format,C的声明如下:int sprintf(char*buffer,const char*format,argument.);用 Delphi 的声明则是这样的:function sprintf(buffer:PChar;const format:PChar):Integer;cdecl;varargs;external msvcrt.dll name sprintf;用 Delphi 可以这样调用:var S:string;begin SetLength(S,30);sprintf(PChar(S),%s and%s are good friends,tom,
12、jacky);ShowMessage(S);end;是不是很神奇,函数声明明明只有两个参数,但调用的时候却可以传入任意多的参数,对函数本身来说,它并不知道参数有多少,因此是无法清理栈现场的,只有调用者知道有多少个参数,所以栈现场由调用者清理,下面是调用这个函数的汇编代码:/sprintf(PChar(S),%s and%s are good friends,tom,jacky);push$00453d00 push$00453d10 push$00453d14 mov eax,ebp-$04 call LStrToPChar push eax call sprintf add esp,$10S
13、afecallSafecall常用于 COM,Delphi 作了很多处理,使得函数返回值小于0 时,自动抛出异常。1.任何 Safecall函数,都可以转换成等价的Stdcall函数。例 1:procedure Proc();safecall;Function Proc():HResult;stdcall;例 2:Function func():Integer;safecall;Function func(out Re:Integer):HResult;stdcall;问题:假设有一个 Safecall的函数:function Add4(a,b:Integer):Integer;safecal
14、l;如何用汇编代码调用之?1)首先是将其转换成 StdCall 的声明:function Add4(a,b:Integer;out Re:Integer):HRESULT;stdcall;2)按照 Stdcall的调用规则调用:var I:Integer;begin 名师资料总结-精品资料欢迎下载-名师精心整理-第 5 页,共 14 页 -/I:=Add4(10,20);asm lea eax,I push eax push 20 push 10 call Add4 end;end;2.函数返回时,Delphi 自动检查其返回值,如果小于0,就引发 reSafeCallError异常。比如 I
15、:=Add4(10,20)这一句,实际的汇编代码是这样的:lea eax,ebp-$04 Push eax push$14 push$0a call Add4 call CheckAutoResult CheckAutoResult 是 System单元的一个 RTL函数,负责检查函数的返回结果:function _CheckAutoResult(ResultCode:HResult):HResult;begin if ResultCode 0 then begin if Assigned(SafeCallErrorProc)then SafeCallErrorProc(ResultCode,
16、Pointer(-1);/loses error address Error(reSafeCallError);end;Result:=ResultCode;end;3.栈框架(Stack Frame)Stack Frame 是一项非常有用的技术,特别是对于高级语言,可以说,如果没有Stack Frame,就没有 Call Stack。函数一般都有如下的汇编代码框架beginpush ebp mov ebp,esp.mov esp,ebp pop ebp end;在调用 push ebp,mov ebp,esp 之后,栈的现场是这样的:名师资料总结-精品资料欢迎下载-名师精心整理-第 6 页,
17、共 14 页 -由此可知,对于每一个函数,EBP总是指向函数进入时的栈顶,那么上图中的“EBP的前一个值”应该就是调用该函数的上一级函数进入时的栈顶了,依此类推,最终将形成下面的图示:上图实际上就形成了一个链表的结构,用下面的记录来表示:PStackFrame=TStackFrame TStackFrame=Record Prev:PStackFrame;CallerAddr:Pointer;end;也就是说,函数调用的同时也在增加这个栈框架链表。举一个例子,假设有A,B,C 三个函数,A调用 B,B调用 C,则最终的栈框架链表如下图所示:名师资料总结-精品资料欢迎下载-名师精心整理-第 7
18、页,共 14 页 -利用栈框架就可以实现调试里面的Call Stack。原理很简单,只要遍历 StackFrame 链表,根据 CallerAddr获得每一个函数名。下面是第一个例子,遍历StackFrame 链表,并取出每一个CallerAddr:procedure OutputCallStack(CallStackProc:TCallStackProc1);overload;var StackFrame:PStackFrame;i:Integer;begin asm MOV StackFrame,EBP end;if Assigned(CallStackProc)then begin i:
19、=0;while i 10 do begin Inc(i);CallStackProc(StackFrame.CallerAddr);StackFrame:=StackFrame.Prev;end;end;end;如果想获得每一个调用函数的详细信息,需要调试符号的帮助,下面一个例子利用生成的Map文件,也可以获得一些函数信息(需要 JCLDebug的支持):procedure OutputCallStack(CallStackProc:TCallStackProc2);overload;var StackFrame:PStackFrame;ProcName,UnitName:string;Li
20、ne:Integer;begin 名师资料总结-精品资料欢迎下载-名师精心整理-第 8 页,共 14 页 -asm MOV StackFrame,EBP end;if Assigned(CallStackProc)then while True do begin ProcName:=ProcOfAddr(StackFrame.CallerAddr);UnitName:=ModuleOfAddr(StackFrame.CallerAddr);Line:=LineOfAddr(StackFrame.CallerAddr);if UnitName then CallStackProc(StackFr
21、ame.CallerAddr,UnitName,ProcName,Line)else Break;StackFrame:=PStackFrame(StackFrame.Prev);end;end;4.Move函数比较我例出了四个 Move函数的运行比较,旨在说明即使是汇编也有很大的速度差异。System.Move请到 System单元下查看;FastCode.Move到 http:/ MyMove_Assembly和 MyMove_pascal的实现代码:procedure MyMove_Pascal(const Source;var Dest;Count:Integer);var S,D:P
22、Char;I:Integer;begin S:=PChar(Source);D:=PChar(Dest);if S=D then Exit;if Cardinal(D)Cardinal(S)then for I:=count-1 downto 0 do DI:=SI else for I:=0 to count-1 do DI:=SI;end;procedure MyMove_Assembly(const Source;var Dest;Count:Integer);asm EAX Pointer to source EDX Pointer to destination ECX Count C
23、MP EAX,EDX JZ endProc 名师资料总结-精品资料欢迎下载-名师精心整理-第 9 页,共 14 页 -PUSH EDI PUSH ESI PUSH ECX CMP EAX,EDX JL DownLoop UpLoop:MOV ESI,EAX MOV EDI,EDX REP MOVSB JMP exit DownLoop:LEA ESI,EAX+ECX-1 LEA EDI,EDX+ECX-1 STD REP MOVSB CLD exit:POP ECX POP ESI POP EDI endProc:end;下面是运行结果的比较:i.未钩选优化指令的情况:ii.钩选优化指令的情况
24、:根据上面的结果,我得出了下面的结论,并有下面的建议。结论:名师资料总结-精品资料欢迎下载-名师精心整理-第 10 页,共 14 页 -未优化的 Pascal 代码与优化的汇编代码效率相差为45倍。优化的 Pascal 代码与优化的汇编代码效率相差为20 倍。优化的 Pascal 代码与未优化的汇编代码效率相差为2.9 位。未优化的汇编代码与优化的汇编效率相差为7 倍。建议:对于应用程序员来说,除非遇到效率要求非常高的地方,否则尽量不要写汇编代码,因为经过优化的高级语言效率已经非常高。理解汇编与高级语言的关系,能够通过查看汇编代码解决困难的问题。高级语言反汇编程序的函数调用过程Jim Chan
25、 10/25/2001 查看:字体:宋体字形:常规大小:小五字符集:CHINESE_GB2312 摘要:本文说明高级语言编译成汇编语言后,高级语言中函数调用的汇编程序过程。正文:高级语言编译成汇编程序以后,在高级语言中的函数调用的汇编程序过程如下:1.将函数参数入栈,第一个参数在栈顶,最后一个参数在栈底。2.执行 CALL指令,调用该函数,进入该函数代码空间。a.执行 CALL指令,将 CALL指令下一行代码的地址入栈。b.进入函数代码空间后,将基址指针EBP入栈,然后让基址指针EBP指向当前堆栈栈顶,并使用它访问存在堆栈中的函数输入参数及堆栈中的其他数据。c.堆栈指针 ESP减少一个值,如4
26、4H,向上移动一个距离,留出一个空间给该函数作为临时存储区。/以上准备工作做好后,函数正式被执行,如下所示。d.将其他指针或寄存器中的值入栈,以便在函数中使用这些寄存器。e.执行代码。f.执行 return()返回执行结果,将要返回的值存入EAX中。g.步骤 2.d 中的指针出栈。h.将 EBP的值传给堆栈指针ESP,使 ESP复原为 2.c 之前的值。此时进入函数时EBP的值在栈顶。i.基址指针 EBP出栈,复原为 2.b 之前的 EBP的值。j.执行 RET指令,“调用函数”的地址出栈,本函数返回到CALL指令的下一行。3.函数返回到 CALL指令下一行,将堆栈指针加一个数值,以使堆栈指针
27、恢复到以上步骤1 执行之前的值。该数值是上面第一步入栈参数的总长度。注意:1.堆栈指针 ESP指向栈顶的新入栈数据的最低位。2.MOV指令中偏移指针指向被“MOV”的数据的最低位。如下面指令是将ebp+8到 ebp+11四个字节的内容传到eax 寄存器中。00402048 mov eax,dword ptr ebp+8一个例子如下:高级语言代码中的函数调用如下:117:bR=t1(p);汇编代码如下:00401FB8 mov ecx,dword ptr ebp-8 ;将参数放入 ecx 寄存器00401FBB push ecx ;参数入栈00401FBC call ILT+10(t1)(004
28、0100f);函数调用,下一行地址 00401FC1入栈00401FC1 add esp,4 ;函数返回,堆栈指针加 4,复原为 00401FB8时的值00401FC4 mov dword ptr ebp-10h,eax;从 eax 中取出高级语言中的函数返回值,放入 bR变量中其中 t1 函数如下:125:BOOL t1(void*p)126:00402030 push ebp ;ebp入栈名师资料总结-精品资料欢迎下载-名师精心整理-第 11 页,共 14 页 -00402031 mov ebp,esp ;ebp指向此时堆栈的栈顶00402033 sub esp,44h ;esp减少一个值
29、,空出一段存储区00402036 push ebx ;将三个寄存器的值入栈,以便在函数中使用它00402037 push esi ;00402038 push edi ;00402039 lea edi,ebp-44h ;0040203C mov ecx,11h ;00402041 mov eax,0CCCCCCCCh ;00402046 rep stos dword ptr edi ;127:int*q=(int*)p;00402048 mov eax,dword ptr ebp+8 ;ebp+8指向函数输入参数的最低位地址;如果是 ebp+4则指向函数返回地址00401FC1的最低位,值为
30、 C1 0040204B mov dword ptr ebp-4,eax ;128:return 0;0040204E xor eax,eax ;返回值放入 eax 寄存器中129:00402050 pop edi ;三个寄存器出栈00402051 pop esi ;00402052 pop ebx ;00402053 mov esp,ebp ;esp复原00402055 pop ebp ;ebp出栈,它的值也复原了00402056 ret ;返回到此时栈顶存储的代码地址:00401FC1;故而如果不幸被修改了返回地址,程序就会出现意外以上汇编代码由VC+6.0编译得到。堆栈在 EBP入栈后的
31、情况:低位高位内存地址堆栈0012F600 edi=0012F6000012F604 44h 的空间0012F6400012F644 ebp 被赋值后指向该单元,此时 ebp=0012F644 AC F6 12 00 ebp 赋值为 esp 之前的值0012F648 C1 1F 40 00 返回地址0012F64C ebp+8A0 F6 12 00 函数实参 p 的值0012F650 注:存储器存储空间堆栈按从高到低的排列,左边标注的地址是其右下方存储单元的最低位地址。如0012F644指向 0012F6AC的 AC字节,AC在栈顶。图中存储器中的内容按从低到高位书写,“AC F6 12 00
32、”=0 x0012F6AC高级语言的一段汇编代码分析int fun(int a,int b)a=0 x4455;b=0 x6677;return a+b;名师资料总结-精品资料欢迎下载-名师精心整理-第 12 页,共 14 页 -int main()fun(0 x8899,0 x1100);return 0;主函数调用fun()函数时的汇编代码:1:00401078 push 1100h/函数 fun()的参数入栈2:0040107D push 8899h/函数 fun()的参数入栈3:00401082 call ILT+5(fun)(0040100a)/fun()函数调用-|5:004010
33、87 add esp,8/释放堆栈|-|/函数 fun()运行时的汇编代码:ebp是函数的入口指针,因此要入栈保存,等待函数结束时返回。4:00401000 push ebp/将 ebp 值入栈,将ebp 作为访问参数的基值00401001 mov ebp,esp/将栈顶指针放入ebp,作为基址.a=0 x4455/c语言代码,汇编代码在下一行00401018 mov dword ptr ebp+8,4455h/本地变量初始化,对栈操作b=0 x6677/c语言代码,汇编代码在下一行0040101F mov dword ptr ebp+0Ch,6677h/本地变量初始化,对栈操作return
34、a+b/c语言代码,汇编代码在下两行00401026 mov eax,dword ptr ebp+8/将 a 的值移入eax 寄存器中00401029 add eax,dword ptr ebp+0Ch/a加 b.0040102F mov esp,ebp/将栈顶指针放回esp 00401031 pop ebp/弹出 ebp 00401032 ret/返回注:在开始处将esp 放入 ebp 是因为 esp 是个经常变化的值。一旦,函数里出现pop 或 push他就会变化。这样很不容易定位参数的于内存中的位置。因此,我们需要一个不会变化的东西作为访问参数的基准。因此此处ebp 值不变,始终指向刚开
35、始的堆栈顶,而 esp 则随着压栈和出栈而指向新的栈顶。/-内存高地址|参数 2:0 x1100h|+-+ebp+0ch|参数 1:0 x8899h|名师资料总结-精品资料欢迎下载-名师精心整理-第 13 页,共 14 页 -+-+ebp+08h|函数返回地址|0 x00401087|第三行汇编码的call 语句会自动将函数返回地址压栈+-+ebp+04h|ebp 副本|-/内存低地址=栈顶ebp ebp和函数返回值也是32 位,所以占4 个字节。call 把它下一条语句的地址(eip)push进了堆栈。因为函数调用完了,要用 ret 返回。而 ret 怎么知道返回哪里呢?因为ret 指令 pop 了 call 指令 push给他的地址,然后返回到了这个地址(将 该地址放到eip 中)。因此 call 有个隐式的push操作,ret 有个隐式的pop 操作。名师资料总结-精品资料欢迎下载-名师精心整理-第 14 页,共 14 页 -