过程的实现离不开堆栈的应用,堆栈是一种后进先出(LIFO)的数据结构,最后压入栈的值总是最先被弹出,而新数值在执行压栈时总是被压入到栈的最顶端,栈主要功能是暂时存放数据和地址,通常用来保护断点和现场。
栈是由CPU管理的线性内存数组,它使用两个寄存器(SS和ESP)来保存栈的状态,SS寄存器存放段选择符,而ESP寄存器的值通常是指向特定位置的一个32位偏移值,我们很少需要直接操作ESP寄存器,相反的ESP寄存器总是由CALL,RET,PUSH,POP等这类指令间接性的修改。
CPU提供了两个特殊的寄存器用于标识位于系统栈顶端的栈帧。
- ESP 栈指针寄存器:栈指针寄存器,其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。
- EBP 基址指针寄存器:基址指针寄存器,其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。
在通常情况下ESP是可变的,随着栈的生成而逐渐变小,而EBP寄存器是固定的,只有当函数的调用后,发生入栈操作而改变。
- 执行PUSH压栈时,堆栈指针自动减4,再将压栈的值复制到堆栈指针所指向的内存地址。
- 执行POP出栈时,从栈顶移走一个值并将其复制给内存或寄存器,然后再将堆栈指针自动加4。
- 执行CALL调用时,CPU会用堆栈保存当前被调用过程的返回地址,直到遇到RET指令再将其弹出。
10.1 PUSH/POP
PUSH和POP是汇编语言中用于堆栈操作的指令,它们通常用于保存和恢复寄存器的值,参数传递和函数调用等。
PUSH指令用于将操作数压入堆栈中,它执行的操作包括将操作数复制到堆栈的栈顶,并将堆栈指针(ESP)减去相应的字节数。指令格式如下:其中,operand可以是8位,16位或32位的寄存器,立即数,以及内存中的某个值。例如,要将寄存器EAX的值压入堆栈中,可以使用以下指令:从汇编代码的角度来看,PUSH指令将操作数存储到堆栈中,它实际上是一个入栈操作。
POP指令用于将堆栈中栈顶的值弹出到指定的目的操作数中,它执行的操作包括将堆栈顶部的值移动到指定的操作数,并将堆栈指针增加相应的字节数。指令格式如下:其中,operand可以是8位,16位或32位的寄存器,立即数,以及内存中的某个位置。例如,要将从堆栈中弹出的值存储到BX寄存器中,可以使用以下指令:从汇编代码的角度来看,POP指令将从堆栈中取出一个值,并将其存储到目的操作数中,它是一个出栈操作。
在函数调用时,PUSH指令被用于向堆栈中推送函数的参数,这些参数可以是寄存器、立即数或者内存中的某个值。在函数返回之前,POP指令被用于将堆栈顶部的值弹出,并将其存储到寄存器或者内存中。
读者需要特别注意,在使用PUSH和POP指令时需要保证堆栈的平衡,也就是说,每个PUSH指令必须有对应的POP指令,否则堆栈会失去平衡,最终导致程序出现错误。
在读者了解了这两条指令时则可以执行一些特殊的操作,如下代码我们以数组入栈与出栈为例,执行PUSH指令时,首先减小ESP的值,然后把源操作数复制到堆栈上,执行POP指令则是先将数据弹出到目的操作数中,然后再执行ESP值增加4,并以此分别将数组中的元素压入栈,最终再通过POP将元素反弹出来。- .386p
- .model flat,stdcall
- option casemap:none
- include windows.inc
- include kernel32.inc
- includelib kernel32.lib
- include msvcrt.inc
- includelib msvcrt.lib
- .data
- Array DWORD 1,2,3,4,5,6,7,8,9,10
- szFmt BYTE '%d ',0dh,0ah,0
- .code
- main PROC
- ; 使用Push指令将数组正向入栈
- mov eax,0
- mov ecx,10
- S1:
- push dword ptr ds:[Array + eax * 4]
- inc eax
- loop S1
-
- ; 使用pop指令将数组反向弹出
- mov ecx,10
- S2:
- push ecx ; 保护ecx
- pop ebx ; 将Array数组元素弹出到ebx
- invoke crt_printf,addr szFmt,ebx
- pop ecx ; 弹出ecx
- loop S2
-
- int 3
- main ENDP
- END main
复制代码 至此当读者理解了这两个指令之后,那么利用堆栈的先进后出特定,我们就可以实现将特殊的字符串反转后输出的效果,首先我们循环将字符串压入堆栈,然后再从堆栈中反向弹出来,这样就可以实现字符串的反转操作,这段代码的实现也相对较为容易;- .386p
- .model flat,stdcall
- option casemap:none
- include windows.inc
- include kernel32.inc
- includelib kernel32.lib
- include msvcrt.inc
- includelib msvcrt.lib
- .data
- MyString BYTE "hello lyshark",0
- NameSize DWORD ($ - MyString) - 1
- szFmt BYTE '%s',0dh,0ah,0
- .code
- main PROC
- ; 正向压入字符串
- mov ecx,dword ptr ds:[NameSize]
- mov esi,0
- S1: movzx eax,byte ptr ds:[MyString + esi]
- push eax
- inc esi
- loop S1
- ; 反向弹出字符串
- mov ecx,dword ptr ds:[NameSize]
- mov esi,0
- S2: pop eax
- mov byte ptr ds:[MyString + esi],al
- inc esi
- loop S2
-
- invoke crt_printf,addr szFmt,addr MyString
- int 3
- main ENDP
- END main
复制代码 10.2 PROC/ENDP
PROC/ENDP 伪指令是用于定义过程(函数)的伪指令,这两个伪指令可分别定义过程的开始和结束位置。此处读者需要注意,这两条伪指令并非是汇编语言中所兼容的,而是MASM编译器为我们提供的一个宏,是MASM的一部分,它允许程序员使用汇编语言定义过程(函数)可以像标准汇编指令一样使用。
对于不使用宏定义来创建函数时我们通常会自己管理函数栈参数,而有了宏定义这些功能都可交给编译器去管理,下面的一个案例中,我们通过使用过程创建ArraySum函数,实现对整数数组求和操作,函数默认将返回值存储在EAX中,并打印输出求和后的参数。- .386p
- .model flat,stdcall
- option casemap:none
- include windows.inc
- include kernel32.inc
- includelib kernel32.lib
- include msvcrt.inc
- includelib msvcrt.lib
- .data
- MyArray DWORD 1,2,3,4,5,6,7,8,9,10
- Sum DWORD ?
- szFmt BYTE '%d',0dh,0ah,0
- .code
- ; 数组求和过程
- ArraySum PROC
- push esi ; 保存ESI,ECX
- push ecx
- xor eax,eax
-
- S1: add eax,dword ptr ds:[esi] ; 取值并相加
- add esi,4 ; 递增数组指针
- loop S1
- pop ecx ; 恢复ESI,ECX
- pop esi
- ret
- ArraySum endp
- main PROC
- lea esi,dword ptr ds:[MyArray] ; 取出数组基址
- mov ecx,lengthof MyArray ; 取出元素数目
- call ArraySum ; 调用方法
- mov dword ptr ds:[Sum],eax ; 得到结果
- invoke crt_printf,addr szFmt,Sum
- int 3
- main ENDP
- END main
复制代码 接着我们来实现一个具有获取随机数功能的案例,在C语言中如果需要获得一个随机数一般会调用Seed函数,如果读者逆向分析过这个函数的实现原理,那么读者应该能理解,在调用取随机数之前会生成一个随机数种子,这个随机数种子的生成则依赖于0x343FDh这个特殊的常量地址,当我们每次访问该地址都会产出一个随机的数据,当得到该数据后,我们再通过除法运算取出溢出数据作为随机数使用实现了该功能。- .386p
- .model flat,stdcall
- option casemap:none
- include windows.inc
- include kernel32.inc
- includelib kernel32.lib
- include msvcrt.inc
- includelib msvcrt.lib
- .data
- seed DWORD 1
- szFmt BYTE '随机数: %d',0dh,0ah,0
- .code
- ; 生成 0 - FFFFFFFFh 的随机种子
- Random32 PROC
- push edx
- mov eax, 343FDh
- imul seed
- add eax, 269EC3h
- mov seed, eax
- ror eax,8
- pop edx
- ret
- Random32 endp
-
- ; 生成随机数
- RandomRange PROC
- push ebx
- push edx
-
- mov ebx,eax
- call Random32
- mov edx,0
- div ebx
- mov eax,edx
- pop edx
- pop ebx
- ret
- RandomRange endp
- main PROC
-
- ; 调用后取出随机数
- call RandomRange
- invoke crt_printf,addr szFmt,eax
- int 3
- main ENDP
- END main
复制代码 10.3 局部参数传递
在汇编语言中,可以使用堆栈来传递函数参数和创建局部变量。当程序执行到函数调用语句时,需要将函数参数传递给被调用函数。为了实现参数传递,程序会将参数压入栈中,然后调用被调用函数。被调用函数从栈中弹出参数并执行,然后将返回值存储在寄存器中,最后通过跳转返回到调用函数。
局部变量也可以通过在栈中分配内存来创建。在函数开始时,可以使用push指令将局部变量压入栈中。在函数结束时,可以使用pop指令将变量从栈中弹出。由于栈是后进先出的数据结构,局部变量的创建可以很方便地通过在栈上压入一些数据来实现。
局部变量是在程序运行时由系统动态的在栈上开辟的,在内存中通常在基址指针(EBP)之下,尽管在汇编时不能给定默认值,但可以在运行时初始化,如下一段C语言伪代码:- void MySub()
- {
- int var1 = 10;
- int var2 = 20;
- }
复制代码 上述的代码经过C编译后,会变成如下汇编指令,其中EBP-4必须是4的倍数,因为默认就是4字节存储,如果去掉了mov esp,ebp,那么当执行pop ebp时将会得到EBP等于10,执行RET指令会导致控制转移到内存地址10处执行,从而程序会崩溃。- MySub PROC
- push ebp ; 将EBP存储在栈中
- mov ebp,esp ; 堆栈框架的基址
- sub esp,8 ; 创建局部变量空间(分配2个局部变量)
- mov DWORD PTR [ebp-8],10 ; var1 = 10
- mov DWORD PTR [ebp-4],20 ; var2 = 20
- mov esp,ebp ; 从堆栈上删除局部变量
- pop ebp ; 恢复EBP指针
- ret 8 ; 返回,清理堆栈
- MySub ENDP
复制代码 为了使上述代码片段更易于理解,可以在上述的代码的基础上给每个变量的引用地址都定义一个符号,并在代码中使用这些符号,如下代码所示,代码中定义了一个名为MySub的过程,该过程将两个局部变量分别设置为10和20。
在该过程中,首先使用push ebp指令将旧的基址指针压入栈中,并将ESP寄存器的值存储到ebp中。这个旧的基址指针将在函数执行完毕后被恢复。然后,我们使用sub esp,8指令将8字节的空间分配给两个局部变量。在堆栈上分配的空间可以通过var1_local和var2_local符号来访问。在这里,我们定义了两个符号,将它们与ebp寄存器进行偏移以访问这些局部变量。var1_local的地址为[ebp-8],var2_local的地址为[ebp-4]。然后,我们使用mov指令将10和 20分别存储到这些局部变量中。最后,我们将ESP寄存器的值存储回ebp中,并使用pop ebp指令将旧的基址指针弹出堆栈。现在,栈顶指针(ESP)下移恢复上面分配的8个字节的空间,最后通过ret 8返回到调用函数。
在使用堆栈传参和创建局部变量时,需要谨慎考虑栈指针的位置,并确保遵守调用约定以确保正确地传递参数和返回值。- var1_local EQU DWORD PTR [ebp-8] ; 添加符号1
- var2_local EQU DWORD PTR [ebp-4] ; 添加符号2
- 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
复制代码 接着我们来实现一个具有功能的案例,首先为了能更好的让读者理解我们先使用C语言方式实现MakeArray()函数,该函数的内部是动态生成的一个MyString数组,并通过循环填充为星号字符串,最后使用POP弹出,并输出结果,观察后尝试用汇编实现。
[code]void makeArray(){ char MyString[30]; for(int i=0;i |