在计算机领域,堆栈是一个不容忽视的概念,堆栈是一种后进先出(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寄存器.
代码语言:javascript复制.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的顺序压栈保存.
代码语言:javascript复制.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处执行,从而程序会崩溃.
为了使代码更加的容易阅读,可以在上面的代码的基础上给每个变量的引用地址都定义一个符号并在代码中使用这些符号来完成编写.
代码语言:javascript复制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字节的堆栈空间:
代码语言:javascript复制MySub PROC
enter 8,0
MySub ENDP
经过编译器转换后,会首先转换为以下的样子:
代码语言:javascript复制MySub PROC
push ebp
mov ebp,esp
sub esp,8
MySub ENDP
上面的代码只有开头没有结尾,如果要使用ENTER指令分配空间的话,则必须在结尾加上LEAVE指令,这样程序才完整.
代码语言:javascript复制MySub PROC
enter 8,0
....
leave
ret
MySub ENDP
下面代码和上面代码作用是相同的,它首先为局部变量保留8字节的堆栈空间然后丢弃.
代码语言:javascript复制MySub PROC
push ebp
mov ebp,esp
sub esp,8
....
mov esp,ebp
pop ebp
ret
MySub ENDP
◆USES/LOCAL 伪指令◆
USES操作符: 该操作符用于指定需要压栈的寄存器,其会自动生成压栈出栈代码无需手动添加.
代码语言:javascript复制.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指令后面.
代码语言:javascript复制.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
局部变量:
代码语言:javascript复制.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(申请数组):
代码语言:javascript复制.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
代码语言:javascript复制.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.
普通参数传递:
代码语言:javascript复制.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
寄存器传递参数:
代码语言:javascript复制.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 提前声明,否则会报错
代码语言:javascript复制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寄存器中.
代码语言:javascript复制.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寄存器中.
代码语言:javascript复制.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
代码语言:javascript复制; 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
结构体的嵌套定义:
代码语言:javascript复制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
联合体的声明:
代码语言:javascript复制; 定义联合体
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前面.
一个简单的宏:
代码语言:javascript复制MyCode macro
xor eax,eax
xor ebx,ebx
xor ecx,ecx
xor edx,edx
endm
.code
main PROC
MyCode ; 将被替换为上面两行代码
ret
main ENDP
END main
一个代替求和函数的宏
代码语言:javascript复制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
宏参数的默认值: 通过定义默认值,可以不给默认的变量传递参数.
代码语言:javascript复制; 参数 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 终止宏代码的后面内容.
代码语言:javascript复制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取消指定宏的展开:
代码语言:javascript复制MySum macro
xor eax,eax
xor ebx,ebx
endm
.code
main PROC
MySum ; 这个会被展开
purge MySum ; 这个不会展开
MySum ; 这个宏也不会展开了
ret
main ENDP
END main
在宏内使用局部标号:
代码语言:javascript复制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
特殊操作符: &、<>、%、!
代码语言:javascript复制& ;替换操作符
<> ;字符串传递操作符
% ;表达式操作符, 也用于得到一个变量或常量的值
! ;转义操作符
;自定义的宏
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
过程小例子
整数求和: 通过使用汇编语言实现一个整数求和的小例子.
代码语言:javascript复制.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
汇编实现性能度量: 通过调用库函数,实现对指定代码执行的性能度量.
代码语言:javascript复制.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
生成伪随机数:
代码语言:javascript复制.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
生成自定义随机数:
代码语言:javascript复制.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