简体   繁体   English

堆栈清理不起作用(__stdcall MASM 函数)

[英]Stack cleanup not working (__stdcall MASM function)

there's something weird going on here.这里发生了一些奇怪的事情。 Visual Studio is letting me know the ESP value was not properly saved but I cannot see any mistakes in the code (32-bit, windows, __stdcall) Visual Studio 让我知道 ESP 值未正确保存,但我看不到代码中有任何错误(32 位、windows、__stdcall)

MASM code: MASM 代码:

.MODE FLAT, STDCALL
...
    memcpy PROC dest : DWORD, source : DWORD, size : DWORD
    MOV EDI, [ESP+04H]
    MOV ESI, [ESP+08H]
    MOV ECX, [ESP+0CH]
    AGAIN_:
    LODSB
    STOSB
    LOOP AGAIN_
    RETN 0CH
    memcpy ENDP

I am passing 12 bytes (0xC) to the stack then cleaning it up.我将 12 个字节(0xC)传递给堆栈然后清理它。 I have confirmed by looking at the symbols the functions symbol goes like "memcpy@12", so its indeed finding the proper symbol我通过查看符号确认了函数符号类似于“memcpy@12”,因此它确实找到了正确的符号

this is the C prototype:这是 C 原型:

extern void __stdcall * _memcpy(void*,void*,unsigned __int32);

Compiling in 32-bit.编译为 32 位。 The function copies the memory (I can see in the debugger), but the stack cleanup appears not to be working function 复制了 memory(我可以在调试器中看到),但堆栈清理似乎不起作用

EDIT:编辑:

MASM code: MASM 代码:

__MyMemcpy PROC _dest : DWORD, _source : DWORD, _size : DWORD
MOV EDI, DWORD PTR [ESP + 04H]
MOV ESI, DWORD PTR [ESP + 08H]
MOV ECX, DWORD PTR [ESP + 0CH]
PUSH ESI
PUSH EDI
__AGAIN:
LODSB
STOSB
LOOP __AGAIN
POP EDI
POP ESI
RETN 0CH
__MyMemcpy ENDP

C code: C 代码:

extern void __stdcall __MyMemcpy(void*, void*, int);

typedef struct {
 void(__stdcall*MemCpy)(void*,void*,int);
}MemFunc;

int initmemfunc(MemFunc*f){
f->MemCpy=__MyMemcpy
}

when I call it like this I get the error:当我这样称呼它时,我得到了错误:

MemFunc mf={0};
initmemfunc(&mf);
mf.MemCpy(dest,src,size);

when I call it like this I dont:当我这样称呼它时,我不会:

__MyMemcpy(dest,src,size)

The reason why the stack is corrupted is that MASM "secretly" inserts the prologue code to your function.堆栈损坏的原因是MASM“秘密”将序言代码插入您的function。 When I added the option to disable that, the function works for me now.当我添加禁用它的选项时,function 现在对我有用。

You can see this, when you switch to assembly mode while still in the C code and then step into your function.当您在 C 代码中切换到装配模式时,您可以看到这一点,然后进入您的 function。 It seems that VS doesn't swtich to assembly mode when already in the assembly source.当已经在汇编源中时,VS 似乎没有切换到汇编模式。

.586
.MODEL FLAT,STDCALL
OPTION PROLOGUE:NONE 

.CODE

mymemcpy PROC dest:DWORD, src:DWORD, sz:DWORD
    MOV EDI, [ESP+04H]
    MOV ESI, [ESP+08H]
    MOV ECX, [ESP+0CH]
AGAIN_:
    LODSB
    STOSB
    LOOP AGAIN_
    RETN 0CH
mymemcpy ENDP

END

Since you have provided an update to your question and comments suggesting you disable prologue and epilogue code generation for functions created with the MASM PROC directive I suspect your code looks something like this:由于您提供了对您的问题和评论的更新,建议您禁用使用 MASM PROC指令创建的函数的序言和结尾代码生成,我怀疑您的代码看起来像这样:

.MODEL FLAT, STDCALL
OPTION PROLOGUE:NONE
OPTION EPILOGUE:NONE

.CODE

__MyMemcpy PROC _dest : DWORD, _source : DWORD, _size : DWORD
    MOV EDI, DWORD PTR [ESP + 04H]
    MOV ESI, DWORD PTR [ESP + 08H]
    MOV ECX, DWORD PTR [ESP + 0CH]
    PUSH ESI
    PUSH EDI
__AGAIN:
    LODSB
    STOSB
    LOOP __AGAIN
    POP EDI
    POP ESI
    RETN 0CH
__MyMemcpy ENDP

END

A note about this code: beware that if your source and destination buffers overlap this can cause problems.关于此代码的注释:请注意,如果您的源缓冲区和目标缓冲区重叠,这可能会导致问题。 If the buffers don't overlap then what you are doing should work.如果缓冲区不重叠,那么您正在做的事情应该有效。 You can avoid this by marking the pointers __restrict .您可以通过标记指针__restrict来避免这种情况。 __restrict is an MSVC/C++ extension that will act as a hint to the compiler that the argument doesn't overlap with another. __restrict是一个 MSVC/C++ 扩展,它将作为对编译器的提示,即参数不与另一个参数重叠。 This can allow the compiler to potentially warn of this situation since your assembly code is unsafe for that situation.这可能允许编译器潜在地警告这种情况,因为您的汇编代码对于这种情况是不安全的。 Your prototypes could have been written as:你的原型可以写成:

extern void __stdcall __MyMemcpy( void* __restrict, void* __restrict, int);

typedef struct {
    void(__stdcall* MemCpy)(void* __restrict, void* __restrict, int);
}MemFunc;

You are using PROC but not taking advantage of any of the underlying power it affords (or obscures).您正在使用PROC ,但没有利用它提供(或模糊)的任何潜在功能。 You have disabled PROLOGUE and EPILOGUE generation with the OPTION directive.您已使用OPTION指令禁用 PROLOGUE 和 EPILOGUE 生成。 You properly use RET 0Ch to have the 12 bytes of arguments cleaned from the stack.您正确使用RET 0Ch从堆栈中清除 arguments 的 12 个字节。

From a perspective of the STDCALL calling convention your code is correct as it pertains to stack usage.从 STDCALL 调用约定的角度来看,您的代码是正确的,因为它与堆栈使用有关。 There is a serious issue in that the Microsoft Windows STDCALL calling convention requires the caller to preserve all the registers it uses except EAX , ECX , and EDX .存在一个严重的问题,即 Microsoft Windows STDCALL 调用约定要求调用者保留它使用的所有寄存器,但EAXECXEDX除外。 You clobber EDI and ESI and both need to be saved before you use them.您破坏了EDIESI ,并且在使用它们之前都需要保存它们。 In your code you save them after their contents are destroyed.在您的代码中,您可以在它们的内容被破坏后保存它们。 You have to push both ESI and EDI on the stack first.您必须先将ESIEDI都压入堆栈。 This will require you adding 8 to the offsets relative to ESP .这将需要您将 8 添加到相对于ESP的偏移量。 Your code should have looked like this:您的代码应该如下所示:

__MyMemcpy PROC _dest : DWORD, _source : DWORD, _size : DWORD
    PUSH EDI                         ; Save registers first
    PUSH ESI
    MOV EDI, DWORD PTR [ESP + 0CH]   ; Arguments are offset by an additional 8 bytes
    MOV ESI, DWORD PTR [ESP + 10H]
    MOV ECX, DWORD PTR [ESP + 14H]
__AGAIN:
    LODSB
    STOSB
    LOOP __AGAIN
    POP ESI                          ; Restore the caller (non-volatile) registers
    POP EDI
    RETN 0CH
__MyMemcpy ENDP

You asked the question why it appears you are getting an error about ESP or a stack issue.您问了为什么您似乎收到有关 ESP 或堆栈问题的错误的问题。 I assume you are getting an error similar to this:我假设您收到类似于此的错误:

在此处输入图像描述

This could be a result of either ESP being incorrect when mixing STDCALL and CDECL calling conventions or it can arise out of the value of the saved ESP being clobbered by the function.这可能是因为在混合 STDCALL 和 CDECL 调用约定时ESP不正确,也可能是由于保存的ESP的值被 function 破坏了。 It appears in your case it is the latter.在您的情况下,它似乎是后者。

I wrote a small C++ project with this code that has similar behaviour to your C program:我用这个代码写了一个小的 C++ 项目,它与你的 C 程序有类似的行为:

#include <iostream>

extern "C" void __stdcall __MyMemcpy( void* __restrict, void* __restrict, int);

typedef struct {
    void(__stdcall* MemCpy)(void* __restrict, void* __restrict, int);
}MemFunc;

int initmemfunc(MemFunc* f) {
    f->MemCpy = __MyMemcpy;
    return 0;
}

char buf1[] = "Testing";
char buf2[200];

int main()
{
    MemFunc mf = { 0 };
    initmemfunc(&mf);
    mf.MemCpy(buf2, buf1, strlen(buf1));
    std::cout << "Hello World!\n" << buf2;
}

When I use code like yours that doesn't properly save ESI and EDI I discovered this in the generated assembly code displayed in the Visual Studio C/C++ debugger:当我使用像您这样无法正确保存ESIEDI的代码时,我在 Visual Studio C/C++ 调试器中显示的生成的汇编代码中发现了这一点:

在此处输入图像描述

I have annotated the important parts.我已经注释了重要的部分。 The compiler has generated C runtime checks (these can be disabled, but they will just hide the problem and not fix it) including a check of ESP across a STDCALL function call.编译器已生成 C 运行时检查(这些可以禁用,但它们只会隐藏问题而不修复它),包括通过 STDCALL function 调用检查ESP Unfortunately it relies on saving the original value of ESP (before pushing parameters) into the register ESI .不幸的是,它依赖于将ESP的原始值(在推送参数之前)保存到寄存器ESI中。 As a result a runtime check is made after the call to __MyMemcpy to see if ESP and ESI are still the same value.因此,在调用__MyMemcpy后会进行运行时检查,以查看ESPESI是否仍然是相同的值。 If they aren't you get the warning about ESP not being saved correctly.如果不是,您会收到有关ESP未正确保存的警告。

Since your code incorrectly clobbers ESI (and EDI ) the check fails.由于您的代码错误地破坏了ESI (和EDI ),因此检查失败。 I have annotated the debug output to hopefully provide a better explanation.我已经对调试 output 进行了注释,希望能提供更好的解释。


You can avoid the use of a LODSB / STOSB loop to copy data.您可以避免使用LODSB / STOSB循环来复制数据。 There is an instruction that just this very operation ( REP MOVSB ) that copies ECX bytes pointed to by ESI and copies them to EDI .有一条指令就是这个操作 ( REP MOVSB ) 复制ESI指向的ECX字节并将它们复制到EDI A version of your code could have been written as:您的代码版本可以写成:

__MyMemcpy PROC _dest : DWORD, _source : DWORD, _size : DWORD
    PUSH EDI                         ; Save registers first
    PUSH ESI
    MOV EDI, DWORD PTR [ESP + 0CH]   ; Arguments are offset by an additional 8 bytes
    MOV ESI, DWORD PTR [ESP + 10H]
    MOV ECX, DWORD PTR [ESP + 14H]
    REP MOVSB
    POP ESI                          ; Restore the caller (non-volatile) registers
    POP EDI
    RETN 0CH
__MyMemcpy ENDP

If you were to use the power of PROC to save the registers ESI and EDI you could list them with the USES directive.如果您要使用PROC的功能来保存寄存器ESIEDI ,您可以USES指令列出它们。 You can also reference the argument locations on the stack by name.您还可以按名称引用堆栈上的参数位置。 You can also have MASM generate the proper EPILOGUE sequence for the calling convention by simply using ret .您还可以通过简单地使用ret让 MASM 为调用约定生成正确的 EPILOGUE 序列。 This will clean the up the stack appropriately and in the case of STDCALL return by removing the specified number of bytes from the stack (ie ret 0ch ) in this case since there are 3 4-byte arguments.这将适当地清理堆栈,并且在 STDCALL 返回的情况下,通过从堆栈中删除指定数量的字节(即ret 0ch )在这种情况下,因为有 3 个 4 字节 arguments。

The downside is that you do have to generate the PROLOGUE and EPILOGUE code that can make things more inefficient:缺点是您必须生成 PROLOGUE 和 EPILOGUE 代码,这会使事情变得更加低效:

.MODEL FLAT, STDCALL
.CODE

__MyMemcpy  PROC USES ESI EDI dest : DWORD, source : DWORD, size : DWORD
    MOV EDI, dest
    MOV ESI, source
    MOV ECX, size
    REP MOVSB                        ; Use instead of LODSB/STOSB+Loop  
    RET
__MyMemcpy  ENDP

END

The assembler would generate this code for you:汇编程序将为您生成此代码:

PUBLIC __MyMemcpy@12
__MyMemcpy@12:
    push        ebp  
    mov         ebp,esp            ; Function prologue generate by PROC
    push        esi                ; USES caused assembler to push EDI/ESI on stack
    push        edi  
    mov         edi,dword ptr [ebp+8]  
    mov         esi,dword ptr [ebp+0Ch]  
    mov         ecx,dword ptr [ebp+10h]  
    rep movs    byte ptr es:[edi],byte ptr [esi]
    ; MASM generated this from the simple RET instruction to restore registers,
    ; clean up stack and return back to caller per the STDCALL calling convention
    pop         edi                ; Assembler
    pop         esi  
    leave  
    ret         0Ch  

Some may rightly argue that having the assembler obscure all this work makes the code potentially harder to understand for someone who doesn't realize the special processing MASM can do with a PROC declared function.有些人可能正确地争辩说,让汇编程序掩盖所有这些工作会使代码可能更难理解,因为这些人没有意识到 MASM 可以对声明为 function 的PROC进行特殊处理。 This may result in harder to maintain code for someone else that is unfamiliar with MASM's nuances in the future.这可能会导致将来更难为不熟悉 MASM 细微差别的其他人维护代码。 If you don't understand what MASM may generate, then sticking to coding the body of the function yourself is probably a safer bet.如果您不了解 MASM 可能生成什么,那么坚持自己编写 function 的主体可能是更安全的选择。 As you have found that also involves turning PROLOGUE and EPILOGUE code generation off.正如您所发现的,这还涉及关闭 PROLOGUE 和 EPILOGUE 代码生成。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM