[32位汇编系列]003 汇编中__stdcall 调用约定以及参数传递

本文详细探讨了32位汇编中__stdcall调用约定的原理,通过一个窗口过程_WinProc的反汇编代码为例,解释了参数如何在栈中传递和局部变量的分配。文章介绍了Windows API大多数采用__stdcall约定,但也存在使用_cdecl的情况。通过分析栈空间布局,阐述了参数从右到左压栈、函数返回时清理栈的过程,并通过示例展示了如何访问和使用这些参数。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

看了上一篇文章[32位汇编系列]002 - 创建标准的windows窗口(3) 之后, 你是否对反汇编代码中有很多ebp+xxx 或者ebp-xxx之类的指令感觉很疑惑, 今天, 我还以 上一篇文章的例子 中的窗口过程_WinProc的反汇编代码作为例子, 来详细的说明一下在32位汇编中, 参数以及局部变量是如何被程序使用的。

 

首先你必须明白一个事实是, Windows的绝大部分api都是以__stdcall方式调用的, 之所以说绝大部分, 是因为有个别的api因为要用到不定参数, 所以, 采用了c/c++的__cdecl 调用约定。 如果你对什么是函数的调用约定不甚了解,那么我建议你先看看这篇文章:调用方式__cdecl和__stdcall的异同点

 

接着, 我们要详细的讨论一下这段代码【_WinProc的反汇编代码】:

 

00401000  /.  55                  push    ebp
00401001  |.  8BEC              mov     ebp, esp
00401003  |.  83C4 AC         add     esp, -54

 

上面这段代码, 是一个汇编的过程的经典代码,在子程序进行任何操作之前, 先保存2个重要的寄存器ebp和esp

通过push ebp 将ebp的值先压入栈中, 接着把当前esp的值赋值给ebp, 以后就可以通过操作ebp来访问各个

参数以及局部变量了, add esp, -54 , 这个是为局部变量保存空间, 这里申请了有3个局部变量:

    local    @stPS:PAINTSTRUCT
    local    @stRC:RECT
    local    @hDC

 

前面2个是结构PAINTSTRUCT和结构RECT, 后面一个是DWORD类型的变量

你不妨计算一下, 这三个局部变量所占用空间的大小

sizeof(PAINTSTRUCT) + sizeof(RECT) + sizeof(DWORD) = 64 + 16 + 4 = 84

这里是10进制, 换成16进制就是0x54 ,这就是为什么要用add esp, -54

需要注意的是, 这里所有的反汇编代码中出现的数字都是16进制的。

经过上面的分析, 我们来看看具体的栈空间示意图:

 

|..................|         低地址

----------------

|   ebp-54    |         hDC的地址 <--当前esp值

----------------

|   ebp-50    |        @stRC:RECT首地址

|-------------- |

|   ebp-4C    | 

----------------

|   ebp-48    |

----------------

|   ebp-44    |

----------------

|    ebp-40   |       @stPS:PAINTSTRUCT   首地址

----------------

|    ..........     |

-----------------

|    ebp         |     进入子程序, ebp被压入栈中

-----------------

|    ebp+4     |     子程序返回地址

-----------------

|    ebp+8     |     参数hWnd

-----------------

|    ebp+C     |     参数uMsg

-----------------

|     ebp+10  |     参数wParam

-----------------

|     ebp+14  |    参数lParam

------------------

|    ...........    |       高地址

 

从栈空间示意图可以清晰的看出, 进入子程序之前, 由调用程序把4个参数从右到做依次压入栈中

接着通过call指令把子程序_WinProc的返回地址也压入栈中

进入子程序后, 首先ebp被压入栈中, 接着为3个局部变量分配0x54个字节的局部空间

 

00401006  |.  8B45 0C         mov     eax, dword ptr [ebp+C]

从栈示意图, 可以很容易看出,ebp+C就是uMsg的地址, 这里就是把uMsg的值送入eax寄存器

 

00401009  |.  83F8 0F          cmp     eax, 0F

这里将uMsg的值和WM_PAINT=0F进行比较

 

0040100C  |.  75 55             jnz     short 00401063

如果uMsg不等于WM_PAINT, 那么就要跳转到下一个cmp比较了

 

0040100E  |.  8D45 C0         lea     eax, dword ptr [ebp-40]

从栈示意图, 不难看出, 这里是把局部变量@stPS的地址送入eax

 

00401011  |.  50                   push    eax                              ; /pPaintstruct

将@stPS地址压栈

 

00401012  |.  FF75 08           push    dword ptr [ebp+8]                ; |hWnd

从栈示意图, 不难看出, ebp+8是窗口句柄hWnd的地址, 这里2个push指令

将BeginPaint函数用到的2个参数从右到左依次压入栈中

 

00401015  |.  E8 B0010000   call    <jmp.&user32.BeginPaint>         ; /BeginPaint

调用BeginPaint, BeginPaint函数返回之后, 会自动将上面2个push指令的压栈操作进行平衡

 

0040101A  |.  8945 AC          mov     dword ptr [ebp-54], eax

从栈示意图上,很容易看出, ebp-54正是局部变量@hDC的地址, 这里将BeginPaint的返回值即

设备句柄送入@hDC中保存

 

0040101D  |.  68 00FF0000   push    0FF00                            ; /Color = <LIGHTGREEN>

将颜色值压入栈中

 

00401022  |.  FF75 AC           push    dword ptr [ebp-54]               ; |hDC

将@hDC值压入栈中, 前面2个压栈, 为调用SetTextColor准备参数

 

00401025  |.  E8 24020000   call    <jmp.&gdi32.SetTextColor>        ; /SetTextColor

调用SetTextColor设置文字颜色, 同样, 前面2个压栈操作由SetTextColor再返回前进行平衡

 

0040102A  |.  6A 00               push    0                                ; /Color = <BLACK>

将背景颜色值压入栈中

 

0040102C  |.  FF75 AC           push    dword ptr [ebp-54]               ; |hDC

将@hDC值压入栈中, 和SetTextColor一样, 这里2个压栈操作为SetBkColor准备参数

 

0040102F  |.  E8 14020000   call    <jmp.&gdi32.SetBkColor>          ; /SetBkColor

调用SetBkColor设置背景色

 

00401034  |.  8D45 B0          lea     eax, dword ptr [ebp-50]

从栈示意图不难看出, ebp-50正是局部变量@stRC的地址

 

00401037  |.  50                   push    eax                              ; /pRect

将@stRC的地址压入栈中

 

00401038  |.  FF75 08          push    dword ptr [ebp+8]                ; |hWnd

将参数hWnd压入栈中, 前面2个压栈操作为GetClientRect准备参数

 

0040103B  |.  E8 B4010000   call    <jmp.&user32.GetClientRect>      ; /GetClientRect

调用GetClientRect得到客户区大小, 前面2个压栈由GetClientRect在内部进行平衡

 

00401040  |.  6A 25              push    25                               ; /Flags = DT_CENTER|DT_VCENTER|DT_SINGLELINE

将 DT_SINGLELINE or DT_VCENTER or DT_CENTER三者的组合值25压栈


00401042  |.  8D45 B0          lea     eax, dword ptr [ebp-50]          ; |

将局部变量@stRC的地址送入eax

 

00401045  |.  50                   push    eax                              ; |pRect

将@stRC地址压栈

 

00401046  |.  6A FF              push    -1                               ; |Count = FFFFFFFF (-1.)

将-1压栈, 就是字符个数, 这里表示以0结尾的字符串

 

00401048  |.  68 75204000   push    00402075                         ; |Text = "things have changed from now"

将要显示的字符串"things have changed from now" 的地址压栈

 

0040104D  |.  FF75 AC           push    dword ptr [ebp-54]               ; |hDC

将@hDC压栈, 上面5个压栈操作, 为DrawTextA准备参数

 

00401050  |.  E8 93010000   call    <jmp.&user32.DrawTextA>          ; /DrawTextA

调用DrawTextA显示字符串, 上面5个压栈操作由DrawTextA返回之前在内部进行平衡

 

00401055  |.  8D45 C0          lea     eax, dword ptr [ebp-40]

将@stPS地址送入eax

 

00401058  |.  50                    push    eax                              ; /pPaintstruct

将@stPS地址压栈

 

00401059  |.  FF75 08           push    dword ptr [ebp+8]                ; |hWnd

将参数hWnd压栈, 上面2个压栈操作为EndPaint准备参数

 

0040105C  |.  E8 8D010000   call    <jmp.&user32.EndPaint>           ; /EndPaint

调用EndPaint, 释放BeginPaint申请的资源, 上面2个压栈操作由EndPaint内部平衡

 

00401061  |.  EB 2E               jmp     short 00401091

WM_PAINT消息处理完毕, 跳转到子程序出口处

 

00401063  |>  83F8 10         cmp     eax, 10

比较uMsg是不是等于WM_CLOSE

 

00401066  |.  75 14              jnz     short 0040107C

不是, 则跳转到DefWindowProc处理处

 

00401068  |.  FF35 04304000 push    dword ptr [403004]               ; /hWnd = NULL

将全局变量hWinMain的值压入栈中, 为DestroyWindow准备参数

 

0040106E  |.  E8 69010000   call    <jmp.&user32.DestroyWindow>      ; /DestroyWindow

调用DestroyWindow销毁窗口

 

00401073  |.  6A 00               push    0                                ; /ExitCode = 0

将程序退出代码0压栈, 为PostQuitMessage准备参数

 

00401075  |.  E8 98010000   call    <jmp.&user32.PostQuitMessage>    ; /PostQuitMessage

调用PostQuitMessage向窗口过程发送一个WM_QUIT消息, 让消息循环可以退出

 

0040107A  |.  EB 15              jmp     short 00401091

WM_CLOSE消息处理完毕,跳转到子程序出口处

 

0040107C  |>  FF75 14         push    dword ptr [ebp+14]               ; /lParam
0040107F  |.  FF75 10           push    dword ptr [ebp+10]               ; |wParam
00401082  |.  FF75 0C           push    dword ptr [ebp+C]                ; |Message
00401085  |.  FF75 08           push    dword ptr [ebp+8]                ; |hWnd

上面4个压栈操作, 从由到左, 依次将DefWindowProcA用到的4个参数压栈

 

00401088  |.  E8 49010000   call    <jmp.&user32.DefWindowProcA>     ; /DefWindowProcA

调用默认窗口处理过程,上面的4个压栈操作由DefWindowProcA内部进行平衡

 

0040108D  |.  C9                   leave

此句代码相当于下面2句代码:

mov esp, ebp    ;   恢复子程序开始处ebp被压入栈的时候的esp值

pop  ebp           ;   恢复ebp进入子程序前的值,同时由于pop操作, esp此时也恢复成了进入子程序之前的值

 

0040108E  |.  C2 1000          retn    10

此句代码相当于

pop hWnd

pop uMsg

pop wParam

pop lParam

pop eip         ; 此句代码并不正确, 只是形象的表示ret的作用, 将子程序的返回地址送入eip, 让程序可以接着执行

retn 后面的10 表示弹出的字节数0x10, 总共有16个字节(10进制表示)

 

由此可见, 通过

push ebp

mov ebp, esp

add   esp, XXXX

以及

leave

retn  YYYY

就可以让栈保持平衡, 子程序之前是什么样, 子程序返回之后还是什么样, 无论你子程序之间进行什么样的操作

有多少次压栈, 有多少次出栈, 栈是否平衡, 但是子程序退出后, 通过这种机制, 能够确保子程序能够正确的

返回, 需要特别注意的是, 在子程序内部前面不要随意更改ebp的值, 它的值一旦改动, 程序估计就要崩掉了


00401091  |>  33C0              xor     eax, eax

将eax清0, 表示子程序返回的值为0, 即正常返回, 任何子程序的返回值都要放在eax中

 

00401093  |.  C9                   leave
00401094  /.  C2 1000          retn    10

这2句跟上面一样, 在另一个分支退出之前, 保持栈的平衡

 

 

好了, 到目前为止, 通过对每句代码的仔细讲解, 以及配上栈示意图, 你是否明白了程序中参数的传递方式

以及参数的使用方式?

<script src="http://pagead2.googlesyndication.com/pagead/show_ads.js" type="text/javascript"></script>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值