在计算机领域,堆栈是一个不容忽视的概念,堆栈是一种后进先出(LIFO,Last-In,First-Out)
的数据结构,这是因为最后压入堆栈的值总是最先被取出,而新数值在执行PUSH压栈时总是被加到堆栈的最顶端,数据也总是从堆栈的最顶端被取出,堆栈是个特殊的存储区
,主要功能是暂时存放数据和地址,通常用来保护断点和现场.
堆栈操作指令 在计算机领域,堆栈是一个不容忽视的概念,堆栈是一种后进先出(LIFO,Last-In,First-Out)
的数据结构,这是因为最后压入堆栈的值总是最先被取出,而新数值在执行PUSH压栈时总是被加到堆栈的最顶端,数据也总是从堆栈的最顶端被取出,堆栈是个特殊的存储区
,主要功能是暂时存放数据和地址,通常用来保护断点和现场.
当程序运行时,栈是由CPU直接管理
的线性
内存数组,它使用两个寄存器(SS和ESP)
来保存堆栈的状态.在保护模式下,SS寄存器存放段选择符(Segment Selector)
运行在保护模式下的程序不能对其进行修改,而ESP寄存器
的值通常是指向特定位置的一个32位偏移值
,我们很少需要直接操作ESP寄存器,相反的ESP寄存器总是由CALL,RET,PUSH,POP
等这类指令间接性的修改.
接着来简单介绍下关于堆栈操作的两个寄存器,CPU系统提供了两个特殊的寄存器用于标识位于系统栈顶端的栈帧. ESP 栈指针寄存器: 栈指针寄存器,其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶. EBP 基址指针寄存器: 基址指针寄存器,其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部.
◆堆栈参数传递◆ 在通常情况下ESP是可变的,随着栈的生产而逐渐变小,而EBP寄存器是固定的,只有当函数的调用后,发生入栈操作而改变.
1.在32位系统中,执行PUSH压栈
时,堆栈指针自动减4
,再将压栈的值复制到堆栈指针所指向的内存地址. 2.在32位系统中,执行POP出栈
时,从栈顶移走一个值并将其复制给内存或寄存器,然后再将堆栈指针自动加4
. 3.在32位系统中,执行CALL调用
时,CPU会用堆栈保存当前被调用过程的返回地址,直到遇到RET指令再将其弹出.
PUSH/POP指令: 在32位环境下,分别将数组中的元素100h-300h
压入堆栈,并且通过POP将元素反弹出来.
.data Array DWORD 100h,200h,300h,400h .code main PROC xor eax,eax push eax ; push 0 push DWORD PTR [Array] ; push 100 push DWORD PTR [Array+4] ; push 200 push DWORD PTR [Array+8] ; push 300 pop eax ; pop 300 pop eax ; pop 200 pop eax ; pop 100 pop eax ; pop 0 push 0 call ExitProcess main ENDP END main
PUSHFD/POPFD指令: PUSHFD在堆栈上压入EFLAGS寄存器的值,POPFD将堆栈的值弹出并送至EFLAGS寄存器.
.data SaveFlage DWORD ? .code main PROC pushfd ; 标识入栈 pop SaveFlage ; 弹出并保存到内存 push SaveFlage ; 从内存取出,并入栈 popfd ; 恢复到EFLAGS寄存器中 push 0 call ExitProcess main ENDP END main
PUSHAD/POPAD指令: 将通用寄存器按照EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI的顺序压栈保存.
.code main PROC pushad mov eax,1000 mov ebx,2000 mov ecx,3000 mov edx,4000 popad push 0 call ExitProcess main ENDP END main
◆声明局部变量◆ 高级语言程序中,在单个过程中创建使用和销毁的变量我们称它为局部变量(local variable)
,局部变量是在程序运行时,由系统动态的在栈上开辟的,在内存中通常在基址指针(EBP)
之下,尽管在汇编时不能给定默认值,但可以在运行时初始化,如下一段伪代码:
void MySub () { int var1 = 10 ; int var2 = 20 ; }
上面的一段代码经过C编译器转换后,会变成如下的样子,其中EBP-4
必须是4的倍数,因为默认就是4字节存储.
MySub PROC push ebp ; 将EBP存储在栈中 mov ebp,esp ; 堆栈框架的基址 sub esp,8 ; 创建局部变量空间 mov DWORD PTR [ebp-4],10 ; var1 = 10 mov DWORD PTR [ebp-8],20 ; var2 = 20 mov esp,ebp ; 从堆栈上删除局部变量 pop ebp ; 恢复EBP指针 ret 8 ; 返回,清理堆栈 MySub ENDP
如果去掉了上面的mov esp,ebp
,那么当执行pop ebp
时将会得到EBP等于10,执行RET指令会导致控制转移到内存地址10处执行,从而程序会崩溃.
为了使代码更加的容易阅读,可以在上面的代码的基础上给每个变量的引用地址都定义一个符号并在代码中使用这些符号来完成编写.
var1_local EQU DWORD PTR [ebp-4] var2_local EQU DWORD PTR [ebp-8] MySub PROC push ebp mov ebp,esp sub esp,8 mov var1_local,10 mov var2_local,20 mov esp,ebp pop ebp ret 8 MySub ENDP
◆ENTER/LEAVE 伪指令◆ ENTRE指令自动为被调用过程创建堆栈框架,它为局部变量保留堆栈空间并在堆栈上保存EBP,该指令执行后会执行以下动作.
1.在堆栈上压入EBP(push ebp) 2.把EBP设为堆栈框架的基指针(mov ebp,esp) 3.为局部变量保留适当的空间(sub esp,numbytes)
ENTER指令有两个参数,第一个操作数是一个常量,用于指定要为局部变量保留多少堆栈空间(numbytes),第二个参数指定过程的嵌套层数,这两个操作数都是立即数,numbytes总是向上取整为4的倍数,以使ESP按照双字边界地址对其.
比如以下代码,使用ENTER为局部变量保存8字节的堆栈空间:
MySub PROC enter 8,0 MySub ENDP
经过编译器转换后,会首先转换为以下的样子:
MySub PROC push ebp mov ebp,esp sub esp,8 MySub ENDP
上面的代码只有开头没有结尾,如果要使用ENTER指令分配空间的话,则必须在结尾加上LEAVE指令,这样程序才完整.
MySub PROC enter 8,0 .... leave ret MySub ENDP
下面代码和上面代码作用是相同的,它首先为局部变量保留8字节的堆栈空间然后丢弃.
MySub PROC push ebp mov ebp,esp sub esp,8 .... mov esp,ebp pop ebp ret MySub ENDP
◆USES/LOCAL 伪指令◆ USES操作符: 该操作符用于指定需要压栈的寄存器,其会自动生成压栈出栈代码无需手动添加.
.code main PROC mov eax,1 mov ebx,2 mov ecx,3 call mycall push 0 call ExitProcess main ENDP mycall PROC USES eax ebx ecx ; 生成压栈代码,自动压eax,ebx,ecx xor eax,eax ; 压栈的寄存器可以随意修改 xor ebx,ebx ; 过程结束后会自动恢复这些寄存器 ret mycall ENDP END main
LOCAL操作符: 在过程内声明一个或多个命名局部变量,并赋予相应的尺寸属性,该语句必须紧跟PROC指令后面.
.code main PROC LOCAL var1:WORD LOCAL var2:DWORD,var3:BYTE mov DWORD PTR [var1],1024 mov eax,DWORD PTR [var1] mov [var2],1024 ; DWORD mov eax,[var2] mov [var3],10 ; BYTE mov al,[var3] push 0 call ExitProcess main ENDP END main
局部变量:
.code lyshark PROC var1:WORD,var2:DWORD LOCAL @loca1:BYTE,@loca2:DWORD LOCAL @local_byte[100]:BYTE mov ax,var1 mov ebx,@loca2 lea ecx,@local_byte mov @local_byte[0],0 mov @local_byte[1],1 mov @local_byte[2],2 mov @local_byte[3],3 lyshark ENDP main PROC invoke lyshark,100,10000 ret main ENDP END main
LOCAL(申请数组):
.code main PROC LOCAL var[3]:DWORD mov var[0],100 mov var[1],200 mov eax,var[0] mov ebx,var[1] main ENDP END main
.code main PROC LOCAL ArrayDW[10]:DWORD LOCAL ArrayB[10]:BYTE lea eax,[ArrayDW] mov [ArrayDW],10 mov [ArrayDW + 4],20 mov [ArrayDW + 8],30 main ENDP END main
过程调用指令 CALL指令指示处理器在新的内存地址执行指令,当用户调用CALL指令时,该指令会首先将CALL指令的下一条指令的内存地址压入堆栈保存,然后将EIP寄存器修改为CALL指令的调用处,等调用结束后返回从堆栈弹出CALL的下一条指令地址.
1.当遇到CALL指令时,程序会经过计算得到CALL指令的下一条指令的地址,并将其压入堆栈. 2.接着会将EIP寄存器的地址指向被调用过程的地址,被调用过程被执行. 3.最后过程内部通过RET指令返回,将从堆栈中弹出EIP的地址,程序继续向下执行. 4.CALL相当于push+jmp,RET相当于pop+jmp.
普通参数传递:
.code sum PROC var1:DWORD,var2:DWORD,var3:DWORD mov eax,var1 mov ebx,var2 mov ecx,var3 ret sum ENDP main PROC invoke sum,10,20,30 ; 调用并传递参数 ret main ENDP END main
寄存器传递参数:
.code sum PROC add eax,ebx add eax,ecx ret sum ENDP main PROC mov eax,10 mov ebx,20 mov ecx,30 call sum ret main ENDP END main
使用PROTO声明: 如果调用的函数在之后实现, 须用 PROTO 提前声明,否则会报错
sum PROTO :DWORD,:DWORD,:DWORD ; 函数声明的主要是参数类型,省略参数名 .code main PROC invoke sum,10,20,30 ; 现在调用的是之后的函数 ret main ENDP sum PROC var1,var2,var3 mov eax,var1 add eax,var2 add eax,var3 ret sum ENDP END main
CALL/RET指令: 编写一个过程,实现对整数数组的求和,并将结果保存到EAX寄存器中.
.data array DWORD 1000h,2000h,3000h,4000h,5000h theSum DWORD ? .code main PROC mov esi,offset array ; ESI指向array mov ecx,lengthof array ; ECX=array元素个数 call ArraySum ; 调用求和指令 mov theSum,eax ; 将结果保存到内存 push 0 call ExitProcess main ENDP ArraySum PROC push esi ; 保存ESI,ECX push ecx mov eax,0 ; 初始化累加寄存器 L1: add eax,[esi] ; 每个整数都和EAX中的和相加 add esi,TYPE DWORD ; 递增指针,继续遍历 loop L1 pop ecx ; 恢复寄存器 pop esi ret ArraySum ENDP END main
通过该语句块配合可以生成自定义过程,下面我们创建一个名为Sum
的过程,实现EBX+ECX
并将结果保存在EAX
寄存器中.
.data TheSum DWORD ? .code main PROC mov ebx,100 ; 传递ebx mov ecx,100 ; 传递ecx call Sum ; 调用过程 mov TheSum,eax ; 保存结果到TheSum push 0 call ExitProcess main ENDP Sum PROC xor eax,eax add eax,ebx add eax,ecx ret Sum ENDP END main
INVOKE调用系统API: 默认情况下,会将返回结果保存在eax寄存器中.
.data szCaption db "MsgBox",0 szText db "这是一个提示框,请点击确定完成交互!",0 .code main PROC .WHILE (1) invoke MessageBox,NULL,offset szText,offset szCaption,MB_YESNO .break .if(eax == IDYES) .ENDW ret main ENDP END main
模块化调用: 首先创建一个sum.asm
然后在main.asm
中引用sum这个文件中的函数.
; sum.asm 首先编译这个文件,并将其放入指定目录下 .386 .model flat, stdcall .code sum PROC v1, v2, v3 mov eax, v1 add eax, v2 add eax, v3 ret sum ENDP end
; main.asm 直接引用编译后的lib文件即可 ;这里的引入路径可以是全路径, 这里是相对路径 includelib /masm32/lib/sum.lib ;子程序声明 sum proto :dword, :dword, :dword .code main PROC invoke sum,10,20,30 ;调用过程 ret main ENDP END main
结构与联合 结构(struct)时逻辑上互相关联的一组变量的模板或模式,结构中的单个变量称为域(field)
,程序的语句可以把结构作为一个实体进行访问,也可以对结构的单个域进行访问,结构通常包括不同类型的域,而联合(union)同样也是把多个标识符组合在一起,不过与结构不同的是,联合体共用用一块内存区域,内存的大小取决于联合体中最大的元素.
引用结构变量: 通过使用<>,{}
均可声明结构体,同时可以初始化,对结构体赋初值.
;定义结构 MyPoint struct pos_x DWORD ? pos_y DWORD ? MyPoint ends .data ;声明结构, 使用 <>、{} 均可 ptr1 MyPoint <10,20> ptr2 MyPoint {30,40} .code main PROC lea edx, ptr1 mov eax, (MyPoint ptr [edx]).pos_x ; 此时eax=10 mov ebx, (MyPoint ptr [edx]).pos_y ; 此时ebx=20 mov (MyPoint PTR [edx]).pos_x,100 ; 将100写入MyPoint.pos_x结构中存储 ret main ENDP END main
结构初始化: 以下定义了MyStruct
结构,并将user2
初始化,FName=lyshark,FAge=25.
MyStruct struct FName db 20 dup(0) FAge db 100 MyStruct ends .data user1 MyStruct <> user2 MyStruct <'lyshark',25> .code main PROC ;lea edx, user1 ;mov eax,DWORD PTR (MyStruct ptr[edx]).FName ;mov ebx,DWORD PTR (MyStruct ptr[edx]).FAge mov eax,DWORD PTR [user2.FName] ; eax=lyshark mov ebx,DWORD PTR [user2.FAge] ; ebx=25 ret main ENDP END main
使用系统结构: 通过调用GetLocalTime
获取系统时间,并存储到SYSTEMTIM
结构体中.
.data sysTime SYSTEMTIME <> ; 声明结构体 .code main PROC invoke GetLocalTime,addr sysTime ; 获取系统时间并放入sysTime mov eax,DWORD PTR sysTime.wYear ; 获取年份 mov ebx,DWORD PTR sysTime.wMonth ; 获取月份 mov ecx,DWORD PTR sysTime.wDay ; 获取天数 ret main ENDP END main
结构体的嵌套定义:
MyPT struct pt_x DWORD ? pt_y DWORD ? MyPT ends Rect struct Left MyPT <> Right MyPT <> Rect ends .data LyShark1 Rect <> LyShark2 Rect {<10,20>,<100,200>} .code main PROC mov [LyShark1.Left.pt_x],100 mov [LyShark1.Left.pt_y],200 mov [LyShark1.Right.pt_x],1000 mov [LyShark1.Right.pt_y],2000 mov eax,[LyShark1.Left.pt_x] ret main ENDP END main
联合体的声明:
; 定义联合体 MyUnion union My_Dword DWORD ? My_Word WORD ? My_Byte BYTE ? MyUnion ends .data test1 MyUnion {1122h}; ;只能存放初始值 .code main PROC mov eax, [test1.My_Dword] mov ax, [test1.My_Word] mov al, [test1.My_Byte] ret main ENDP END main
关于宏汇编 宏过程(Macro Procedure)
是一个命名的语汇编语句块,一旦定义后,宏过程就可以在程序中被调用任意多次,调用宏过程的时候,宏内的语句块将替换到调用的位置,宏的本质是替换,但像极了子过程,宏可定义在源程序的任意位置,但一般放在.data前面.
一个简单的宏:
MyCode macro xor eax,eax xor ebx,ebx xor ecx,ecx xor edx,edx endm .code main PROC MyCode ; 将被替换为上面两行代码 ret main ENDP END main
一个代替求和函数的宏
MySum macro var1, var2, var3 mov eax,var1 add eax,var2 add eax,var3 endm .code main PROC MySum 10,20,30 MySum 10,20,30,40 ; 多余的参数40会被忽略 ret main ENDP END main
宏参数的默认值: 通过定义默认值,可以不给默认的变量传递参数.
; 参数 var1、var2 通过 REQ 标识说明是必备参数 MySum macro var1:req, var2:req, var3:=<30> ; var3默认值是30 mov eax,var1 add eax,var2 add eax,var3 endm .code main PROC MySum 10,20 ret main ENDP END main
使用EXITM终止宏执行: 可使用关键字exitm 终止宏代码的后面内容.
MySum macro xor eax,eax xor ebx,ebx xor ecx,ecx exitm ; 只会清空前三个寄存器,后面的跳过了 xor edx,edx xor esi,esi endm .code main PROC MySum ret main ENDP END main
使用PURGE取消指定宏的展开:
MySum macro xor eax,eax xor ebx,ebx endm .code main PROC MySum ; 这个会被展开 purge MySum ; 这个不会展开 MySum ; 这个宏也不会展开了 ret main ENDP END main
在宏内使用局部标号:
MyMax macro var1,var2 LOCAL jump mov eax,var1 cmp eax,var2 jge jump xor eax,eax jump: ret endm .code main PROC MyMax 20,10 main ENDP END main
特殊操作符: &、<>、%、!
& ;替换操作符 <> ;字符串传递操作符 % ;表达式操作符, 也用于得到一个变量或常量的值 ! ;转义操作符 ;自定义的宏 mPrint macro Text PrintText '* &Text& *' endm .code main proc ;该宏会把参数直接替换过去 mPrint 1234 ;* 1234 * ;要保证参数的完整应该使用 <> mPrint 12,34 ;* 12 * mPrint <12,34> ;* 12,34 * ;需要计算结果应该使用 %() mPrint 34+12 ;* 34+12 * mPrint %(34+12) ;* 46 * ;用到 &、<、>、%、! 应该使用 ! 转义 mPrint 10 !% 2 = %(10/2)!! ;* 10 % 2 = 5! * ret main endp end main
过程小例子 整数求和: 通过使用汇编语言实现一个整数求和的小例子.
.data String WORD 100h,200h,300h,400h,500h .code main PROC ;lea edi,String ; 取String数组的基址 mov edi,offset String ; 同上,两种方式均可 mov ecx,lengthof String ; 取数组中的数据个数 mov ax,0 ; 累加器清零 L1: add ax,[edi] ; 加上一个整数 add edi,TYPE String ; 指向下一个数组元素,type(2byte) loop L1 push 0 call ExitProcess main ENDP END main
正向复制字符串: 使用汇编语言实现字符串的复制,将数据从source
复制到target
内存中.
.data source BYTE "hello lyshark welcome",0h target BYTE SIZEOF source DUP(0),0h ; 取源地址数据大小 .code main PROC mov esi,0 ; 使用变址寄存器 mov ecx,sizeof source ; 循环计数器 L1: mov al,source[esi] ; 从源地址中取一个字符 mov target[esi],al ; 将该字符存储在目标地址中 inc esi ; 递增,将指针移动到下一个字符 loop L1 push 0 call ExitProcess main ENDP END main
反向复制字符串: 使用汇编语言实现字符串的复制,将数据从source
复制到target
内存中且反向存储数据.
.data source BYTE "hello lyshark welcome",0h target BYTE SIZEOF source DUP(0),0h .code main PROC mov esi,sizeof source mov ecx,sizeof source mov ebx,0 L1: mov al,source[esi] mov target[ebx],al dec esi inc ebx loop L1 push 0 call ExitProcess main ENDP END main
查看内存与寄存器: 通过调用DumpMem/DumpRegs
显示内存与寄存器的快照.
.data array DWORD 1,2,3,4,5,6,7,8,9,0ah,0bh .code main PROC mov esi,offset array ; 设置内存起始地址 mov ecx,lengthof array ; 设置元素数据,偏移 mov ebx,type array ; 设置元素尺寸(1=byte,2=word,4=dword) call DumpMem ; 调用内存查询子过程 call DumpRegs ; 调用查询寄存器子过程 push 0 call ExitProcess main ENDP END main
汇编实现性能度量: 通过调用库函数,实现对指定代码执行的性能度量.
.data StartTime DWORD ? .code main PROC call GetMseconds ; 调用区本地时间过程 mov StartTime,eax ; 将返回值赋值给StartTime mov ecx,10 ; 通过调用延时过程,模拟程序的执行 L1: mov eax,1000 ; 指定延时1s=1000ms call Delay ; 调用延时过程 loop L1 call GetMseconds ; 再次调用本地时间过程 sub eax,StartTime ; 结束时间减去开始时间 call WriteDec ; 以十进制形式输出eax寄存器的值 push 0 call ExitProcess main ENDP END main
字符输出: WriteString(字符串)
,WriteInt(整数)
,WriteHex(16进制)
,WriteChar(字符)
,WriteDec(10进制)
.
.data Message BYTE "Input String:",0h String DWORD ? .code main PROC ; 设置控制台背景颜色 mov eax,yellow +(blue*16) ; 设置为蓝底黄字 call SetTextColor ; 调用设置过程 call Clrscr ; 清除屏幕,clear ; 提示用户一段话 mov edx,offset Message ; 指定输出的文字 call WriteString ; 调用回写过程 call Crlf ; 调用回车 push 0 call ExitProcess main ENDP END main
字符输入: ReadString(字符串)
,ReadInt(整数)
,ReadHex(16进制)
,ReadChar(字符)
,ReadDec(10进制)
.
.data Buffer BYTE 21 DUP(0) ; 输入缓冲区 ByteCount DWORD ? ; 存放计数器 .code main PROC mov edx,offset Buffer ; 指向缓冲区指针 mov ecx,sizeof Buffer ; 指定最多读取的字符数 call ReadString ; 读取输入字符串 mov ByteCount,eax ; 保存读取的字符数 push 0 call ExitProcess main ENDP END main
生成伪随机数:
.code main PROC mov ecx,5 ; 循环生成5个随机数 L1: call Random32 ; 生成随机数 call WriteDec ; 以十进制显示 mov al,TAB ; 水平制表符 call WriteChar ; 显示水平制表符 loop L1 call Crlf ; 回车 push 0 call ExitProcess main ENDP END main
生成自定义随机数:
.code main PROC mov ecx,5 ; 循环生成5个随机数 L1: mov eax,100 ; 0-99之间 call RandomRange ; 生成随机数 sub eax,50 ; 范围在-50-49 call WriteInt ; 十进制输出 mov al,TAB call WriteChar ; 输出制表符 loop L1 call Crlf ; 回车 push 0 call ExitProcess main ENDP END main