5.10 汇编语言:汇编过程与结构

立聪堂德州十三局店  金牌会员 | 2023-8-31 22:37:38 | 来自手机 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 886|帖子 886|积分 2658

过程的实现离不开堆栈的应用,堆栈是一种后进先出(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)减去相应的字节数。指令格式如下:
  1. PUSH operand
复制代码
其中,operand可以是8位,16位或32位的寄存器,立即数,以及内存中的某个值。例如,要将寄存器EAX的值压入堆栈中,可以使用以下指令:
  1. PUSH EAX
复制代码
从汇编代码的角度来看,PUSH指令将操作数存储到堆栈中,它实际上是一个入栈操作。
POP指令用于将堆栈中栈顶的值弹出到指定的目的操作数中,它执行的操作包括将堆栈顶部的值移动到指定的操作数,并将堆栈指针增加相应的字节数。指令格式如下:
  1. POP operand
复制代码
其中,operand可以是8位,16位或32位的寄存器,立即数,以及内存中的某个位置。例如,要将从堆栈中弹出的值存储到BX寄存器中,可以使用以下指令:
  1. POP EBX
复制代码
从汇编代码的角度来看,POP指令将从堆栈中取出一个值,并将其存储到目的操作数中,它是一个出栈操作。
在函数调用时,PUSH指令被用于向堆栈中推送函数的参数,这些参数可以是寄存器、立即数或者内存中的某个值。在函数返回之前,POP指令被用于将堆栈顶部的值弹出,并将其存储到寄存器或者内存中。
读者需要特别注意,在使用PUSH和POP指令时需要保证堆栈的平衡,也就是说,每个PUSH指令必须有对应的POP指令,否则堆栈会失去平衡,最终导致程序出现错误。
在读者了解了这两条指令时则可以执行一些特殊的操作,如下代码我们以数组入栈与出栈为例,执行PUSH指令时,首先减小ESP的值,然后把源操作数复制到堆栈上,执行POP指令则是先将数据弹出到目的操作数中,然后再执行ESP值增加4,并以此分别将数组中的元素压入栈,最终再通过POP将元素反弹出来。
  1.   .386p
  2.   .model flat,stdcall
  3.   option casemap:none
  4. include windows.inc
  5. include kernel32.inc
  6. includelib kernel32.lib
  7. include msvcrt.inc
  8. includelib msvcrt.lib
  9. .data
  10.   Array DWORD 1,2,3,4,5,6,7,8,9,10
  11.   szFmt BYTE '%d ',0dh,0ah,0
  12. .code
  13.   main PROC
  14.     ; 使用Push指令将数组正向入栈
  15.     mov eax,0
  16.     mov ecx,10
  17.   S1:
  18.     push dword ptr ds:[Array + eax * 4]
  19.     inc eax
  20.     loop S1
  21.    
  22.     ; 使用pop指令将数组反向弹出
  23.     mov ecx,10
  24.   S2:
  25.     push ecx                         ; 保护ecx
  26.     pop ebx                          ; 将Array数组元素弹出到ebx
  27.     invoke crt_printf,addr szFmt,ebx
  28.     pop ecx                          ; 弹出ecx
  29.     loop S2
  30.    
  31.     int 3
  32.   main ENDP
  33. END main
复制代码
至此当读者理解了这两个指令之后,那么利用堆栈的先进后出特定,我们就可以实现将特殊的字符串反转后输出的效果,首先我们循环将字符串压入堆栈,然后再从堆栈中反向弹出来,这样就可以实现字符串的反转操作,这段代码的实现也相对较为容易;
  1.   .386p
  2.   .model flat,stdcall
  3.   option casemap:none
  4. include windows.inc
  5. include kernel32.inc
  6. includelib kernel32.lib
  7. include msvcrt.inc
  8. includelib msvcrt.lib
  9. .data
  10.   MyString BYTE "hello lyshark",0
  11.   NameSize DWORD ($ - MyString) - 1
  12.   szFmt BYTE '%s',0dh,0ah,0
  13. .code
  14.   main PROC
  15.     ; 正向压入字符串
  16.     mov ecx,dword ptr ds:[NameSize]
  17.     mov esi,0
  18.   S1: movzx eax,byte ptr ds:[MyString + esi]
  19.     push eax
  20.     inc esi
  21.     loop S1
  22.     ; 反向弹出字符串
  23.     mov ecx,dword ptr ds:[NameSize]
  24.     mov esi,0
  25.   S2: pop eax
  26.     mov byte ptr ds:[MyString + esi],al
  27.     inc esi
  28.     loop S2
  29.    
  30.     invoke crt_printf,addr szFmt,addr MyString
  31.     int 3
  32.   main ENDP
  33. END main
复制代码
10.2 PROC/ENDP

PROC/ENDP 伪指令是用于定义过程(函数)的伪指令,这两个伪指令可分别定义过程的开始和结束位置。此处读者需要注意,这两条伪指令并非是汇编语言中所兼容的,而是MASM编译器为我们提供的一个宏,是MASM的一部分,它允许程序员使用汇编语言定义过程(函数)可以像标准汇编指令一样使用。
对于不使用宏定义来创建函数时我们通常会自己管理函数栈参数,而有了宏定义这些功能都可交给编译器去管理,下面的一个案例中,我们通过使用过程创建ArraySum函数,实现对整数数组求和操作,函数默认将返回值存储在EAX中,并打印输出求和后的参数。
  1.   .386p
  2.   .model flat,stdcall
  3.   option casemap:none
  4. include windows.inc
  5. include kernel32.inc
  6. includelib kernel32.lib
  7. include msvcrt.inc
  8. includelib msvcrt.lib
  9. .data
  10.   MyArray  DWORD 1,2,3,4,5,6,7,8,9,10
  11.   Sum      DWORD ?
  12.   szFmt    BYTE '%d',0dh,0ah,0
  13. .code
  14.   ; 数组求和过程
  15.   ArraySum PROC
  16.     push esi                     ; 保存ESI,ECX
  17.     push ecx
  18.     xor eax,eax
  19.    
  20.   S1: add eax,dword ptr ds:[esi]   ; 取值并相加
  21.     add esi,4                    ; 递增数组指针
  22.     loop S1
  23.     pop ecx                      ; 恢复ESI,ECX
  24.     pop esi
  25.     ret
  26.   ArraySum endp
  27.   main PROC
  28.     lea esi,dword ptr ds:[MyArray]   ; 取出数组基址
  29.     mov ecx,lengthof MyArray         ; 取出元素数目
  30.     call ArraySum                    ; 调用方法
  31.     mov dword ptr ds:[Sum],eax       ; 得到结果
  32.     invoke crt_printf,addr szFmt,Sum
  33.     int 3
  34.   main ENDP
  35. END main
复制代码
接着我们来实现一个具有获取随机数功能的案例,在C语言中如果需要获得一个随机数一般会调用Seed函数,如果读者逆向分析过这个函数的实现原理,那么读者应该能理解,在调用取随机数之前会生成一个随机数种子,这个随机数种子的生成则依赖于0x343FDh这个特殊的常量地址,当我们每次访问该地址都会产出一个随机的数据,当得到该数据后,我们再通过除法运算取出溢出数据作为随机数使用实现了该功能。
  1.   .386p
  2.   .model flat,stdcall
  3.   option casemap:none
  4. include windows.inc
  5. include kernel32.inc
  6. includelib kernel32.lib
  7. include msvcrt.inc
  8. includelib msvcrt.lib
  9. .data
  10.   seed DWORD 1
  11.   szFmt    BYTE '随机数: %d',0dh,0ah,0
  12. .code
  13.   ; 生成 0 - FFFFFFFFh 的随机种子
  14.   Random32 PROC
  15.     push  edx
  16.     mov   eax, 343FDh
  17.     imul  seed
  18.     add   eax, 269EC3h
  19.     mov   seed, eax
  20.     ror   eax,8
  21.     pop   edx
  22.     ret
  23.   Random32 endp
  24.   
  25.   ; 生成随机数
  26.   RandomRange PROC
  27.     push  ebx
  28.     push  edx
  29.    
  30.     mov   ebx,eax
  31.     call  Random32
  32.     mov   edx,0
  33.     div   ebx
  34.     mov   eax,edx
  35.     pop   edx
  36.     pop   ebx
  37.     ret
  38.   RandomRange endp
  39.   main PROC
  40.   
  41.     ; 调用后取出随机数
  42.     call RandomRange
  43.     invoke crt_printf,addr szFmt,eax
  44.     int 3
  45.   main ENDP
  46. END main
复制代码
10.3 局部参数传递

在汇编语言中,可以使用堆栈来传递函数参数和创建局部变量。当程序执行到函数调用语句时,需要将函数参数传递给被调用函数。为了实现参数传递,程序会将参数压入栈中,然后调用被调用函数。被调用函数从栈中弹出参数并执行,然后将返回值存储在寄存器中,最后通过跳转返回到调用函数。
局部变量也可以通过在栈中分配内存来创建。在函数开始时,可以使用push指令将局部变量压入栈中。在函数结束时,可以使用pop指令将变量从栈中弹出。由于栈是后进先出的数据结构,局部变量的创建可以很方便地通过在栈上压入一些数据来实现。
局部变量是在程序运行时由系统动态的在栈上开辟的,在内存中通常在基址指针(EBP)之下,尽管在汇编时不能给定默认值,但可以在运行时初始化,如下一段C语言伪代码:
  1. void MySub()
  2. {
  3.   int var1 = 10;
  4.   int var2 = 20;
  5. }
复制代码
上述的代码经过C编译后,会变成如下汇编指令,其中EBP-4必须是4的倍数,因为默认就是4字节存储,如果去掉了mov esp,ebp,那么当执行pop ebp时将会得到EBP等于10,执行RET指令会导致控制转移到内存地址10处执行,从而程序会崩溃。
  1. MySub PROC
  2.   push ebp                  ; 将EBP存储在栈中
  3.   mov ebp,esp               ; 堆栈框架的基址
  4.   sub esp,8                 ; 创建局部变量空间(分配2个局部变量)
  5.   mov DWORD PTR [ebp-8],10  ; var1 = 10
  6.   mov DWORD PTR [ebp-4],20  ; var2 = 20
  7.   mov esp,ebp               ; 从堆栈上删除局部变量
  8.   pop ebp                   ; 恢复EBP指针
  9.   ret 8                     ; 返回,清理堆栈
  10. 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返回到调用函数。
在使用堆栈传参和创建局部变量时,需要谨慎考虑栈指针的位置,并确保遵守调用约定以确保正确地传递参数和返回值。
  1. var1_local EQU DWORD PTR [ebp-8]   ; 添加符号1
  2. var2_local EQU DWORD PTR [ebp-4]   ; 添加符号2
  3. MySub PROC
  4.   push ebp
  5.   mov ebp,esp
  6.   sub esp,8
  7.   mov var1_local,10
  8.   mov var2_local,20
  9.   mov esp,ebp
  10.   pop ebp
  11.   ret 8
  12. MySub ENDP
复制代码
接着我们来实现一个具有功能的案例,首先为了能更好的让读者理解我们先使用C语言方式实现MakeArray()函数,该函数的内部是动态生成的一个MyString数组,并通过循环填充为星号字符串,最后使用POP弹出,并输出结果,观察后尝试用汇编实现。
[code]void makeArray(){  char MyString[30];  for(int i=0;i
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

立聪堂德州十三局店

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表