汇编与高级语言
1. 汇编基础知识
1.1. 寄存器
寄存器
|
用途
|
EAX,EBX,EDX,ECX
|
通用寄存器,由程序员自己指定用途,也有一些不成文的用法:
EAX:常用于运算。
EBX:常用于地址索引。
ECX:常用于计数。
EDX:常用于数据传递。
|
EIP
|
指令寄存器,指出当前指令所在的地址。
|
ESP
|
栈指针,指向当前线程的栈顶。
|
EBP
|
栈基址指针,对调试起着很重要的作用。
|
EDI,ESI
|
没有规定作什么用,一般用在源指针和目标指针的操作。
|
FR
|
标志寄存器,由多个标志位组成,存放运算结果的标志,比如借位,进位,是否为0等等。
|
FS
|
在Windows中,FS:[0]用来指向异常处理机制的链接头。
|
说明:
l ESP和EBP对高级语言的函数实现起着非常重要的作用。
l FS是SEH(Structured Exception Handling)中起重要作用的一个段寄存器,它的0偏移指向异常结构连表的表头,Windows在进行结构化异常处理时,就是从FS:[0]开始遍历异常结构并调用其中的异常处理函数的。
1.2. 堆栈
堆是一块内存区域,一般用于内存的动态分配和释放,比如用New方法分配一个指针,此时即在程序地址空间的堆中分配了一块内存。又比如Delphi的对象也是在堆中创建的。
栈是一种先进后出的列表数据结构,在高级语言的编程中使用广泛,在低级语言中更是不可或缺的基础概念。栈也是一个内存区域,不过它具有快速灵活的特点,CPU直接提供指令去访问栈。
从汇编的角度来看,栈具有如下的性质:
l 栈有两个基础动作,压栈(PUSH)和出栈(POP)。
l 栈是向下增长的,即每压一次栈,栈顶的地址就减少一次,也可以说ESP的值就减小一次。
l 栈是线程相关的,每一个线程都拥有一个栈。
l 程序利用ESP可以很灵活地访问栈,不一定要执行PUSH和POP栈顶才会改变,直接操作ESP也可以改变栈顶,也就是说ESP决定了栈顶的值。
l 栈是有最大值的,通过编程环境可以设置,超出最大值就会发生栈溢出。
看一个简单的例子,下面的指令是一条压栈指令,意思是将EAX的值压入栈中:
PUSH EAX
根据上面的性质,这条指令等价于下面的指令:
SUB ESP, 4
MOV ESP, EAX
用下面的图表示指令的操作过程:
2. 调用规则
2.1. 从汇编的角度看函数调用
汇编语言没有变量的概念,因此对函数的调用,第一个要解决的问题是参数要如何传递,有的将参数放在栈中,有的将参数放在寄存器中,对于参数压栈的还要确定是从最左边的参数开始压栈,还是从最右边开始,所有这些,就构成了调用规则的内容。
第二个问题是函数如何被调用,其实很简单,就是一个跳转指令JMP,跳到函数的首地址去,并从那里开始执行指令。
比如下面的代码:
C := Add(10, 20);
按照上面的讨论,汇编代码应该如下:
MOV EAX, 10
MOV EDX, 20
JMP @Add
现在我们又遇到另一个问题:函数执行完后如何返回?在调用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函数执行完毕后,只要在栈中找到这个地址,执行点就可以回来了。大概有人觉得函数调用实在是很常用的事情,于是干脆把最后两条指令合成一条,变成了Call,所以最后的汇编代码如下:
MOV EAX, 10
MOV EDX, 20
CALL @Add
接下来看看Add函数,函数执行完后怎么在栈中找到返回地址?解决这个问题的关键点就是栈平衡,不管函数对栈如何操作,但一定要保证在函数退出时栈现场和刚进来时的一样,这里包括栈顶和栈的内容一样。只要做到这一点,就可以确定在函数将返回时的栈顶值就是正确的返回值,我们只需要从栈顶弹出这个值,再执行一个跳转就行了。
假设它的代码是这样:
Function Add(a, b: Integer): Integer;
begin
Result := a + b;
end;
那么汇编代码就是这样:
ADD EAX, EDX
POP EDX
JMP EDX
同样后两个指令太常用了,因此合成一条,成了Ret,最后的汇编代码是这样的:
ADD EAX, EDX
RET
从汇编角度函数调用大概就是如此。
2.2. 调用规则
调用规则讨论的是函数的参数怎么传递,函数结果又是怎么返回,另外栈平衡由谁负责(函数本身或调用者)。下面就介绍几个比较常用的调用规则:
Register
Delphi默认的调用规则,效率非常高,但规则很复杂,下面是它的简要规则:
1. 头三个不大于4个字节(DWORD)的参数从左到右的传入EAX,EDX,ECX寄存器;接下去的参数按从左到右压栈。
比如函数: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
push 50
call Add1
mov I, eax
end;
end;
2. 浮点数总压栈,不管它所占的字节是多少。
3. 对象方法总是有一个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. 栈现场必须由函数自己清理。
Stdcall
Windows API的标准调用规则,效率不高,但规则很简单:
1. 参数总是从右向左地压栈。
比如,对于函数:function Add3(a, b: Integer): 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类似,参数总是从右向左地压栈。
2. 栈现场必须由调用者清理。
这就为可变参数提供了可能,举一个例子,C语言里面有一个运行时函数叫Sprintf,类似于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', '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,$10
Safecall
Safecall常用于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; safecall;如何用汇编代码调用之?
1) 首先是将其转换成StdCall的声明:
function Add4(a, b: Integer; out Re: Integer): HRESULT; stdcall;
2) 按照Stdcall的调用规则调用:
var
I: Integer;
begin
//I := Add4(10, 20);
asm
lea eax, I
push eax
push 20
push 10
call Add4
end;
end;
2. 函数返回时,Delphi自动检查其返回值,如果小于0,就引发reSafeCallError异常。
比如I := 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, Pointer(-1)); // loses error address
Error(reSafeCallError);
end;
Result := ResultCode;
end;
3. 栈框架(Stack Frame)
Stack Frame是一项非常有用的技术,特别是对于高级语言,可以说,如果没有Stack Frame,就没有Call Stack。
函数一般都有如下的汇编代码框架
begin
push ebp
mov ebp,esp
... ...
mov esp,ebp
pop ebp
end;
在调用push ebp,mov ebp, esp之后,栈的现场是这样的:
由此可知,对于每一个函数,EBP总是指向函数进入时的栈顶,那么上图中的“EBP的前一个值”应该就是调用该函数的上一级函数进入时的栈顶了,依此类推,最终将形成下面的图示:
上图实际上就形成了一个链表的结构,用下面的记录来表示:
PStackFrame = ^TStackFrame
TStackFrame = Record
Prev: PStackFrame;
CallerAddr: Pointer;
end;
也就是说,函数调用的同时也在增加这个栈框架链表。举一个例子,假设有A,B,C三个函数,A调用B,B调用C,则最终的栈框架链表如下图所示:
利用栈框架就可以实现调试里面的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 := 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;
Line: Integer;
begin
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(StackFrame^.CallerAddr, UnitName, ProcName, Line)
else
Break;
StackFrame := PStackFrame(StackFrame^.Prev);
end;
end;
4. Move函数比较
我例出了四个Move函数的运行比较,旨在说明即使是汇编也有很大的速度差异。
System.Move请到System单元下查看;FastCode.Move到http://fastcode.sourceforge.net/下载,下面是MyMove_Assembly和MyMove_pascal的实现代码:
procedure MyMove_Pascal(const Source; var Dest; Count: Integer);
var
S, D: PChar;
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
D[I] := S[I]
else
for I := 0 to count-1 do
D[I] := S[I];
end;
procedure MyMove_Assembly(const Source; var Dest; Count: Integer);
asm
{ EAX Pointer to source }
{ EDX Pointer to destination }
{ ECX Count }
CMP EAX, EDX
JZ @endProc
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. 钩选优化指令的情况:
根据上面的结果,我得出了下面的结论,并有下面的建议。
结论:
① 未优化的Pascal代码与优化的汇编代码效率相差为45倍。
② 优化的Pascal代码与优化的汇编代码效率相差为20倍。
③ 优化的Pascal代码与未优化的汇编代码效率相差为2.9位。
④ 未优化的汇编代码与优化的汇编效率相差为7倍。
建议:
① 对于应用程序员来说,除非遇到效率要求非常高的地方,否则尽量不要写汇编代码,因为经过优化的高级语言效率已经非常高。
② 理解汇编与高级语言的关系,能够通过查看汇编代码解决困难的问题。
相关推荐
6. **汇编与高级语言的交互**:学习如何使用汇编语言编写函数供高级语言调用,或者反过来,调用高级语言编写的函数。 7. **调试与反汇编**:学习使用调试工具来分析和调试汇编程序,以及如何将已有的机器码转换成...
总之,汇编语言涉及底层计算机硬件操作,理解寄存器、堆栈和调用规则对于编写高效的系统级代码和理解高级语言的底层实现至关重要。尽管现代编程更多依赖高级语言,但了解汇编语言的基本原理可以帮助开发者更好地优化...
计算机语言是人与计算机进行交流的工具,主要分为高级语言、汇编语言和机器语言三种类型,它们各自具有独特的特点和用途。 高级语言是相对于汇编语言而言的,旨在使编程更加接近人类自然语言和数学表达。这类语言...
12. **高级主题**:可能包括浮点运算、多处理器环境下的并行处理、汇编与高级语言的混合编程等。 《汇编语言(第2版)》这本书可能会以实例驱动的方式讲解这些概念,帮助读者从基础到进阶逐步掌握汇编语言。通过...
本资源包含了汇编语言程序设计的课件,这些课件通常包括PPT或PDF格式的讲义,详细讲解了汇编语言的基础概念、语法结构、指令集、寻址方式、程序流程控制以及汇编与高级语言的交互等内容。课件中的实例和图解有助于...
7. **汇编与高级语言的交互**:讨论了如何在C、C++等高级语言中嵌入汇编代码,或者使用汇编语言编写特定功能的库。 学习汇编语言不仅需要理解指令和语法,还需要对计算机体系结构有深入的了解。通过《汇编语言...
4. **实践汇编与高级语言交互**:学习如何用汇编语言编写函数并被高级语言调用,理解调用约定。 5. **调试与分析**:使用调试工具理解程序执行过程,分析代码效率。 ### 四、汇编语言的应用场景 1. **操作系统...
让我们深入探讨这三大类计算机语言:机器语言、汇编语言以及高级语言。 **机器语言**是计算机能够直接理解和执行的语言,由二进制代码(0和1)组成。这种语言与硬件紧密相连,每个指令都对应着计算机内部特定的电路...
7. **汇编与高级语言的接口**:讲解如何在C或C++等高级语言中嵌入汇编代码,以及使用汇编语言编写系统级程序,如中断处理程序。 8. **优化技巧**:讨论如何通过调整汇编代码提高程序运行效率,如减少指令条数、利用...
5.1 编译器与解释器:高级语言需要经过编译器转换成汇编语言,再由汇编器翻译成机器码。 5.2 静态链接与动态链接:静态链接将所有依赖库直接合并到可执行文件,而动态链接在运行时加载所需库。 六、实践应用 6.1 ...
8. **汇编与高级语言的交互**:汇编语言通常与C/C++等高级语言结合使用,理解汇编代码如何被链接到高级语言程序中,以及如何通过接口进行调用。 9. **调试与分析**:学会使用汇编语言调试工具,如GDB,能够帮助你...
5. **汇编与高级语言交互**:学习如何用汇编语言编写函数,供C/C++等高级语言调用,或者反过来,调用高级语言编写的函数。 《微机原理与汇编语言习题解答》中的习题涵盖了上述所有主题,通过解答这些习题,学生可以...
7. **汇编与高级语言的交互**:了解如何在汇编程序中调用C语言函数,或者反过来在C程序中嵌入汇编代码。 8. **调试技巧**:学习如何使用DEBUG工具或其它调试器分析和调试汇编代码,找出程序中的错误。 9. **优化...
讲解如何将高级语言翻译成汇编代码,以及汇编语言与机器语言的关系。 2. **语法和指令**:详细阐述汇编语言的语法结构,包括指令格式、注释、常量和变量定义。常见的指令如数据传输指令(例如MOV)、算术运算指令...
7. **汇编与高级语言交互**:讲解如何在汇编程序中嵌入C/C++代码,或者在高级语言中调用汇编函数,实现混合编程。 8. **实操案例**:通过实际编程练习,比如编写简单的计算器程序、内存操作示例或图形界面应用,...
此外,资料可能还会包含汇编与高级语言的交互,如使用汇编编写函数供C/C++调用,或者在调试过程中使用汇编查看底层执行情况。这部分内容对于深入理解程序执行机制和优化程序性能非常有帮助。 最后,针对“期末复习...
8. **汇编与高级语言的交互**:在实际应用中,汇编语言往往与C、C++等高级语言混合使用,了解如何在高级语言中嵌入汇编代码,或者用汇编编写特定功能的库函数,是提高程序性能的有效手段。 通过练习"汇编语言程序...
6. **汇编与高级语言的接口**:探讨如何在汇编语言程序中嵌入C或C++代码,或者反过来,在高级语言程序中调用汇编代码。 7. **程序优化**:分析如何通过精心设计的汇编代码来提高程序的运行效率。 8. **实模式与...
2. **依赖硬件**: 汇编语言与特定CPU架构绑定,移植性差。 3. **控制精细**: 通过直接操作硬件资源,汇编可以实现非常精细的控制,如精确的中断处理、定时等。 4. **调试方便**: 在系统级调试和性能优化时,汇编语言...