堆栈清理不起作用(__stdcall MASM 函数)
Stack cleanup not working (__stdcall MASM function)
这里发生了一些奇怪的事情。 Visual Studio 让我知道 ESP 值没有正确保存,但我看不到代码中的任何错误(32 位,windows,__stdcall)
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
我将 12 个字节 (0xC) 传递到堆栈然后清理它。我通过查看符号确认函数符号类似于“memcpy@12”,所以它确实找到了正确的符号
这是 C 原型:
extern void __stdcall * _memcpy(void*,void*,unsigned __int32);
在 32 位中编译。该函数复制内存(我可以在调试器中看到),但堆栈清理似乎不起作用
编辑:
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代码:
extern void __stdcall __MyMemcpy(void*, void*, int);
typedef struct {
void(__stdcall*MemCpy)(void*,void*,int);
}MemFunc;
int initmemfunc(MemFunc*f){
f->MemCpy=__MyMemcpy
}
当我这样调用它时,出现错误:
MemFunc mf={0};
initmemfunc(&mf);
mf.MemCpy(dest,src,size);
当我这样称呼它时,我不会:
__MyMemcpy(dest,src,size)
堆栈损坏的原因是MASM“秘密地”将序言代码插入到您的函数中。当我添加禁用它的选项时,该功能现在对我有用。
当您在 C 代码中切换到汇编模式然后单步执行您的函数时,您可以看到这一点。似乎 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
由于您已经更新了问题和建议您禁用使用 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
关于此代码的注意事项:请注意,如果您的源缓冲区和目标缓冲区重叠,这可能会导致问题。如果缓冲区不重叠,那么你正在做的应该有效。您可以通过标记指针 __restrict
来避免这种情况。 __restrict
是一个 MSVC/C++ 扩展,它会提示编译器参数不与另一个重叠。这可能允许编译器潜在地警告这种情况,因为您的汇编代码对于这种情况是不安全的。您的原型可以写成:
extern void __stdcall __MyMemcpy( void* __restrict, void* __restrict, int);
typedef struct {
void(__stdcall* MemCpy)(void* __restrict, void* __restrict, int);
}MemFunc;
您正在使用 PROC
但没有利用它提供(或隐藏)的任何潜在功能。您已使用 OPTION
指令禁用了 PROLOGUE 和 EPILOGUE 生成。您正确使用 RET 0Ch
从堆栈中清除了 12 个字节的参数。
从 STDCALL 调用约定的角度来看,您的代码是正确的,因为它与堆栈使用有关。有一个严重的问题是 Microsoft Windows STDCALL calling convention 要求调用者保留它使用的所有寄存器,除了 EAX、ECX,以及 EDX。您破坏了 EDI 和 ESI,并且在使用它们之前 需要保存它们。在您的代码中,您可以在它们的内容被销毁后保存它们。您必须先将 ESI 和 EDI 压入堆栈。这将要求您将相对于 ESP 的偏移量加 8。您的代码应该如下所示:
__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
你问了为什么你会收到关于 ESP 的错误或堆栈问题。我假设您收到与此类似的错误:
这可能是由于 ESP 在混合使用 STDCALL 和 CDECL 调用约定时不正确,也可能是由于保存的 ESP 被函数破坏了。在你的情况下似乎是后者。
我用这段代码写了一个小的 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;
}
当我使用像你这样没有正确保存 ESI 和 EDI 的代码时,我在显示的生成的汇编代码中发现了这一点Visual Studio C/C++ 调试器:
我已经标注了重要的部分。编译器已生成 C 运行时检查(这些可以禁用,但它们只会隐藏问题而不是修复它)包括跨 STDCALL 函数调用检查 ESP。不幸的是,它依赖于将 ESP 的原始值(在推送参数之前)保存到寄存器 ESI 中。因此,在调用 __MyMemcpy
之后进行运行时检查,以查看 ESP 和 ESI 是否仍然是相同的值。如果不是,您会收到有关 ESP 未正确保存的警告。
由于您的代码错误地破坏了 ESI(和 EDI),因此检查失败。我对调试输出进行了注释,希望能提供更好的解释。
您可以避免使用 LODSB
/STOSB
循环来复制数据。有一条指令就是这个操作 (REP MOVSB
) 复制了 ESI 指向的 ECX 字节并将它们复制到 EDI。您的代码版本可以写成:
__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
如果您要使用 PROC
的功能来保存寄存器 ESI 和 EDI 您可以列出它们USES
指令。您还可以按名称引用堆栈上的参数位置。您还可以通过简单地使用 ret
让 MASM 为调用约定生成正确的 EPILOGUE 序列。这将适当地清理堆栈,在 STDCALL return 的情况下,通过从堆栈中删除指定数量的字节(即 ret 0ch
),在这种情况下,因为有 3 个 4 字节参数。
缺点是您必须生成 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
汇编程序将为您生成此代码:
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
有些人可能会正确地争辩说,让汇编器掩盖所有这些工作会使代码可能更难理解,因为那些没有意识到 MASM 可以对 PROC
声明的函数进行特殊处理的人。这可能会导致将来更难为不熟悉 MASM 细微差别的其他人维护代码。如果您不了解 MASM 可能生成什么,那么坚持自己编写函数主体可能是一个更安全的选择。正如您所发现的,这还涉及关闭 PROLOGUE 和 EPILOGUE 代码生成。
这里发生了一些奇怪的事情。 Visual Studio 让我知道 ESP 值没有正确保存,但我看不到代码中的任何错误(32 位,windows,__stdcall)
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
我将 12 个字节 (0xC) 传递到堆栈然后清理它。我通过查看符号确认函数符号类似于“memcpy@12”,所以它确实找到了正确的符号
这是 C 原型:
extern void __stdcall * _memcpy(void*,void*,unsigned __int32);
在 32 位中编译。该函数复制内存(我可以在调试器中看到),但堆栈清理似乎不起作用
编辑:
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代码:
extern void __stdcall __MyMemcpy(void*, void*, int);
typedef struct {
void(__stdcall*MemCpy)(void*,void*,int);
}MemFunc;
int initmemfunc(MemFunc*f){
f->MemCpy=__MyMemcpy
}
当我这样调用它时,出现错误:
MemFunc mf={0};
initmemfunc(&mf);
mf.MemCpy(dest,src,size);
当我这样称呼它时,我不会:
__MyMemcpy(dest,src,size)
堆栈损坏的原因是MASM“秘密地”将序言代码插入到您的函数中。当我添加禁用它的选项时,该功能现在对我有用。
当您在 C 代码中切换到汇编模式然后单步执行您的函数时,您可以看到这一点。似乎 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
由于您已经更新了问题和建议您禁用使用 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
关于此代码的注意事项:请注意,如果您的源缓冲区和目标缓冲区重叠,这可能会导致问题。如果缓冲区不重叠,那么你正在做的应该有效。您可以通过标记指针 __restrict
来避免这种情况。 __restrict
是一个 MSVC/C++ 扩展,它会提示编译器参数不与另一个重叠。这可能允许编译器潜在地警告这种情况,因为您的汇编代码对于这种情况是不安全的。您的原型可以写成:
extern void __stdcall __MyMemcpy( void* __restrict, void* __restrict, int);
typedef struct {
void(__stdcall* MemCpy)(void* __restrict, void* __restrict, int);
}MemFunc;
您正在使用 PROC
但没有利用它提供(或隐藏)的任何潜在功能。您已使用 OPTION
指令禁用了 PROLOGUE 和 EPILOGUE 生成。您正确使用 RET 0Ch
从堆栈中清除了 12 个字节的参数。
从 STDCALL 调用约定的角度来看,您的代码是正确的,因为它与堆栈使用有关。有一个严重的问题是 Microsoft Windows STDCALL calling convention 要求调用者保留它使用的所有寄存器,除了 EAX、ECX,以及 EDX。您破坏了 EDI 和 ESI,并且在使用它们之前 需要保存它们。在您的代码中,您可以在它们的内容被销毁后保存它们。您必须先将 ESI 和 EDI 压入堆栈。这将要求您将相对于 ESP 的偏移量加 8。您的代码应该如下所示:
__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
你问了为什么你会收到关于 ESP 的错误或堆栈问题。我假设您收到与此类似的错误:
这可能是由于 ESP 在混合使用 STDCALL 和 CDECL 调用约定时不正确,也可能是由于保存的 ESP 被函数破坏了。在你的情况下似乎是后者。
我用这段代码写了一个小的 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;
}
当我使用像你这样没有正确保存 ESI 和 EDI 的代码时,我在显示的生成的汇编代码中发现了这一点Visual Studio C/C++ 调试器:
我已经标注了重要的部分。编译器已生成 C 运行时检查(这些可以禁用,但它们只会隐藏问题而不是修复它)包括跨 STDCALL 函数调用检查 ESP。不幸的是,它依赖于将 ESP 的原始值(在推送参数之前)保存到寄存器 ESI 中。因此,在调用 __MyMemcpy
之后进行运行时检查,以查看 ESP 和 ESI 是否仍然是相同的值。如果不是,您会收到有关 ESP 未正确保存的警告。
由于您的代码错误地破坏了 ESI(和 EDI),因此检查失败。我对调试输出进行了注释,希望能提供更好的解释。
您可以避免使用 LODSB
/STOSB
循环来复制数据。有一条指令就是这个操作 (REP MOVSB
) 复制了 ESI 指向的 ECX 字节并将它们复制到 EDI。您的代码版本可以写成:
__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
如果您要使用 PROC
的功能来保存寄存器 ESI 和 EDI 您可以列出它们USES
指令。您还可以按名称引用堆栈上的参数位置。您还可以通过简单地使用 ret
让 MASM 为调用约定生成正确的 EPILOGUE 序列。这将适当地清理堆栈,在 STDCALL return 的情况下,通过从堆栈中删除指定数量的字节(即 ret 0ch
),在这种情况下,因为有 3 个 4 字节参数。
缺点是您必须生成 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
汇编程序将为您生成此代码:
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
有些人可能会正确地争辩说,让汇编器掩盖所有这些工作会使代码可能更难理解,因为那些没有意识到 MASM 可以对 PROC
声明的函数进行特殊处理的人。这可能会导致将来更难为不熟悉 MASM 细微差别的其他人维护代码。如果您不了解 MASM 可能生成什么,那么坚持自己编写函数主体可能是一个更安全的选择。正如您所发现的,这还涉及关闭 PROLOGUE 和 EPILOGUE 代码生成。