• MASM32汇编中关于栈的总结


    关于函数调用中栈的总结

    1.栈,是由高地址分配到低地址的。

    32位程序中,寄存器ebp指向栈的起始位置,也就是栈的基地址(高地址)

    寄存器esp指向栈的目前位置,也就是栈顶。

    现在假设程序才运行,ebp和esp寄存器都是0x00190000h

    接下来的指令是push 0x1800004H,那么ebp还是0x00190000h

    因为push一个整数占用4字节,所以esp-4=0x0018fffcH

    所以esp=0x0018FFFCH,

    再来看看内存分配,这里以小端为例。

    0x0018FFFCH 0X0018FFFDH 0X0018FFFEH 0X18FFFFH
    04 00 00 18

    所以,esp减少,代表压入数据,esp增大,代表弹出数据

    2.栈的应用

    1.临时保存值。

    这里以寄存器为例。

    假定一开始eax的值为5

    指令 eax寄存器的值
    push eax 5
    inc eax 6
    dec eax 5

    2.保存函数调用的返回地址。

    假设程序是顺序执行。

    那么,在执行call指令的时候,会先往栈中push call指令往下一条语句的地址

    3.传递函数参数。

    调用一个有参数的函数,都是先push一些参数,然后再执行call指令。

    4.存放函数中的局部变量。

    OD跟踪一个函数执行时,往往能看见过程一开始,就是

    sub esp,4这样的语句,这代表分配了4字节的空间给某个局部变量。

    使用那个局部变量的时候,往往用的ebp-4来代表那个地址。

    3.CALL指令的调用过程

    1.将调用函数使用的参数入栈

    2.将call指令往后需要执行的指令入栈,函数执行完毕后用到

    3.保存原始ebp指针的值

    4.为函数准备栈空间,ebp指向函数的栈的基地址

    5.为函数中的局部变量开辟栈空间

    6.执行函数语句

    7.清理局部变量,恢复ebp指针,(leave指令)

    8.返回执行call指令往后执行的指令,清理函数调用push的栈(retn)

    	.386
    	.model flat,stdcall
    	option casemap:none
    
    include windows.inc
    include user32.inc
    includelib user32.lib
    include kernel32.inc
    includelib kernel32.lib
    
    	.code
    _add proc a:word,b:word
    	local @bl1:dword
    	ret
    _add endp
    start:
    	push 1
    	push 2
    	call _add
    	add eax,1
    end start
    

    这样的代码,我们编译链接,在OD中运行,查看字节码。

    00401000                        | 55                         | push ebp                                        |
    00401001                        | 8BEC                       | mov ebp,esp                                     |
    00401003                        | 83C4 FC                    | add esp,FFFFFFFC                                |
    00401006                        | C745 FC 05000000           | mov dword ptr ss:[ebp-4],5                      |
    0040100D                        | C9                         | leave                                           |
    0040100E                        | C2 0800                    | ret 8                                           |
    00401011 <my.EntryPoint>        | 6A 01                      | push 1                                          |
    00401013                        | 6A 02                      | push 2                                          |
    00401015                        | E8 E6FFFFFF                | call my.401000                                  |
    0040101A                        | 83C0 01                    | add eax,1                                       |                                   |
    

    接下来我们逐条解释 。

    编写这8条即可保证堆栈平衡。比如调用函数前,栈里面有7个4字节的指针,运行到最后(add eax,1)的时候,栈里面就应该还是7个4字节的指针,并且数据不变。

    (1)因为这个过程有两个参数,所以这里push两个参数,如果改成只push一个,那么执行完函数的时候,start函数里面,原先的栈就被破坏了,栈失衡可能会造成运行错误。

    (2)大家使用OD跟着调试的时候,可以注意观察堆栈的数据,进入调用函数的时候,栈顶保存的就是call指令后面的那条指令的地址

    (3)(4)(5)经常,调用过程的时候,

    总是看见过程开头是这几句,就拿这里为例

    00401000                        | 55                         | push ebp                                        |
    00401001                        | 8BEC                       | mov ebp,esp                                     |
    00401003                        | 83C4 FC                    | add esp,FFFFFFFC                                |
    00401006                        | C745 FC 05000000           | mov dword ptr ss:[ebp-4],5                      |
    
    

    每个函数都有自己的栈空间,ebp指向的就是函数栈空间的起点,所以这里会先push ebp,

    保存start里面的栈的基址。

    mov ebp,esp。调整函数的栈的基址。为什么要这样,我们接着往下看。

    add esp,FFFFFFC,也就是栈顶往下移动了4字节,也就是分配给@bl1的空间。

    而机器码里面可是没有这个局部变量名称的,那么如何做到使用这个空间呢?

    也就是这一句 mov dword ptr ss:[ebp-4],5 。对局部变量的操作,主要通过栈基址的偏移来操作

    所以,调用函数,才需要保存原始栈基址,设置新的栈基址。

    (6)这里没有涉及,很多时候,通过OD,我们能看到

    pushad
    xxxxxx
    popad
    

    这样的语句,也是利用栈来保存寄存器的值

    (7)函数一开头就保存了start的栈基址,那么,为了维持堆栈平衡,函数结束的时候,就应该恢复栈基址。

    这是通过leave指令实现的。

    等价于

    mov esp,ebp
    pop ebp
    

    回到start函数,栈的基地址恢复了,栈顶地址也恢复了,也就是回到了调用call指令以后,执行函数过程前的状态。

    (8)最后一个ret 8,因为这个函数调用,占用8字节,所以会再弹出下一条指令地址后,再弹出8字节的数据。

    至此,函数调用中的栈就完毕了。

    4.栈溢出

    且看代码:

    	.386
    	.model flat,stdcall
    	option casemap:none
    
    include windows.inc
    include user32.inc
    includelib user32.lib
    include kernel32.inc
    includelib kernel32.lib
    	
    	.data
    szText db 'HelloWorldPE',0
    szText2 db 'Touch Me!',0
    szShellCode dd 0fffffffh,0dddddddh,0040103ah,0
    
    	.code
    _memCopy proc _lpSrc
    	local @buf[4]:byte
    	pushad
    	mov al,1
    	mov esi,_lpSrc
    	lea edi,@buf
    	.while al!=0
    		mov al,byte ptr [esi]
    		mov byte ptr [edi],al
    
    		inc esi
    		inc edi
    	.endw
    	popad
    	ret
    _memCopy endp
    
    start:
    	invoke _memCopy,addr szShellCode
    	invoke MessageBox,NULL,offset szText,NULL,MB_OK
    	invoke MessageBox,NULL,offset szText2,NULL,MB_OK
    	invoke ExitProcess,NULL
    end start
    
    

    看代码可能会以为弹出两个信息框,实际上只会弹出一个。这里的shellCode是经过专门设计的。

    在_memCopy过程中,edi指向了栈中分配的局部变量的地址,随后又不断越界修改,改变了栈空间内存里面的数据,最重要的,就是改变了call指令返回以后需要执行的指令的地址,这个可以利用OD调试跟踪运行。

  • 相关阅读:
    图片处理连环画特效
    卡片翻页算法
    android 自定义属性
    android 中捕获全局异常
    c++ 学习笔记
    图片怀旧特效处理
    Linux 网络配置
    指针参数传递
    python 读写文件
    PopupWindow 点击外面取消
  • 原文地址:https://www.cnblogs.com/dayq/p/15994441.html
Copyright © 2020-2023  润新知