[英]How does this asm for a stdcall function clean args from the stack?
[英]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 位。 function 復制了 memory(我可以在調試器中看到),但堆棧清理似乎不起作用
編輯:
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“秘密”將序言代碼插入您的function。 當我添加禁用它的選項時,function 現在對我有用。
當您在 C 代碼中切換到裝配模式時,您可以看到這一點,然后進入您的 function。 當已經在匯編源中時,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
從堆棧中清除 arguments 的 12 個字節。
從 STDCALL 調用約定的角度來看,您的代碼是正確的,因為它與堆棧使用有關。 存在一個嚴重的問題,即 Microsoft Windows STDCALL 調用約定要求調用者保留它使用的所有寄存器,但EAX 、 ECX和EDX除外。 您破壞了EDI和ESI ,並且在使用它們之前都需要保存它們。 在您的代碼中,您可以在它們的內容被破壞后保存它們。 您必須先將ESI和EDI都壓入堆棧。 這將需要您將 8 添加到相對於ESP的偏移量。 您的代碼應該如下所示:
__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 或堆棧問題的錯誤的問題。 我假設您收到類似於此的錯誤:
這可能是因為在混合 STDCALL 和 CDECL 調用約定時ESP不正確,也可能是由於保存的ESP的值被 function 破壞了。 在您的情況下,它似乎是后者。
我用這個代碼寫了一個小的 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 function 調用檢查ESP 。 不幸的是,它依賴於將ESP的原始值(在推送參數之前)保存到寄存器ESI中。 因此,在調用__MyMemcpy
后會進行運行時檢查,以查看ESP和ESI是否仍然是相同的值。 如果不是,您會收到有關ESP未正確保存的警告。
由於您的代碼錯誤地破壞了ESI (和EDI ),因此檢查失敗。 我已經對調試 output 進行了注釋,希望能提供更好的解釋。
您可以避免使用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 返回的情況下,通過從堆棧中刪除指定數量的字節(即ret 0ch
)在這種情況下,因為有 3 個 4 字節 arguments。
缺點是您必須生成 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 可以對聲明為 function 的PROC
進行特殊處理。 這可能會導致將來更難為不熟悉 MASM 細微差別的其他人維護代碼。 如果您不了解 MASM 可能生成什么,那么堅持自己編寫 function 的主體可能是更安全的選擇。 正如您所發現的,這還涉及關閉 PROLOGUE 和 EPILOGUE 代碼生成。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.