[原]调试PInvoke导致的内存破坏
緣起
最近項目中遇到一個詭異的問題,程序在升級到.net4.6.1后,執行某個功能時會崩潰,提示訪問只讀內存區。大概規律如下:
debug版不崩潰,release版穩定崩潰。
只有x64位的程序崩潰,32位及anycpu編譯出來的程序運行不會崩潰。
出問題的代碼范圍很小(按鈕點擊事件代碼不多)。
根據以上信息,各位小伙伴有什么思路嗎?
排查
由于release版可以穩定重現,而且范圍不大,故通過二分法(每次注釋掉一半代碼,看看是否崩潰,如果崩潰,接著注釋掉一半代碼,如果不崩潰說明崩潰跟注釋掉的那段代碼有關...)很快定位到了導致問題的代碼。
最后發現并不是由于升級.net版本導致的,而是程序本身的問題:
代碼中通過P/Invoke調用了原生 API GlobalMemoryStatus()。在定義MemoryStatus結構體的時候強制按4字節定義了每一個字段。而在x64下MemoryStatus結構體中的成員有些不是4字節大小,而是8字節大小!這樣,傳遞給GlobalMemoryStatus()的MemoryStatus參數(32字節)比GlobalMemoryStatus()預期的(56字節)小,導致GlobalMemoryStatus寫了不該寫的內存!????????????
重現
我把有問題的代碼獨立出來了,完整的測試代碼如下(請編譯x64版本):
using System;using System.Runtime.InteropServices;
namespace ConsoleApplication1
{
class Program
{
[StructLayout(LayoutKind.Sequential)]
public struct MemoryStatus
{
[MarshalAs(UnmanagedType.U4)]
public uint dwLength;
[MarshalAs(UnmanagedType.U4)]
public uint dwMemoryLoad;
[MarshalAs(UnmanagedType.U4)]
public uint dwTotalPhys;
[MarshalAs(UnmanagedType.U4)]
public uint dwAvailPhys;
[MarshalAs(UnmanagedType.U4)]
public uint dwTotalPageFile;
[MarshalAs(UnmanagedType.U4)]
public uint dwAvailPageFile;
[MarshalAs(UnmanagedType.U4)]
public uint dwTotalVirtual;
[MarshalAs(UnmanagedType.U4)]
public uint dwAvailVirtual;
}
[DllImport("kernel32.dll")]
public static extern void GlobalMemoryStatus(ref MemoryStatus memoryStatus);
class CMyClass
{
public int n1 = 0;
}
struct CMyStruct
{
public CMyClass data;
}
static void Main(string[] args)
{
CMyStruct myObj = new CMyStruct(); myObj.data = new CMyClass();
MemoryStatus memoryStatus = new MemoryStatus();
// this line will corrupt the stack if we run in x64.
// because memoryStatus is defined on the stack.
GlobalMemoryStatus(ref memoryStatus);
// myObj.data is corrupted
System.Console.WriteLine("{0}", myObj.data);
}
}
}
修復
只需要定義MemoryStatus的時候,注意字段的大小即可。正確的MemoryStatus定義如下:
public struct MemoryStatus{
[MarshalAs(UnmanagedType.U4)]
public uint dwLength;
[MarshalAs(UnmanagedType.U4)]
public uint dwMemoryLoad;
// 以下字段 4 bytes on 32-bit Windows, 8 bytes on 64-bit Windows.
[MarshalAs(UnmanagedType.SysUInt)]
public IntPtr dwTotalPhys;
[MarshalAs(UnmanagedType.SysUInt)]
public IntPtr dwAvailPhys;
[MarshalAs(UnmanagedType.SysUInt)]
public IntPtr dwTotalPageFile;
[MarshalAs(UnmanagedType.SysUInt)]
public IntPtr dwAvailPageFile;
[MarshalAs(UnmanagedType.SysUInt)]
public IntPtr dwTotalVirtual;
[MarshalAs(UnmanagedType.SysUInt)]
public IntPtr dwAvailVirtual;
}
思考
為什么debug版不崩潰?而release版會崩潰?
我在測試機器上調查的原因是debug版本運行的時候,關鍵內存恰巧沒被破壞(太“幸運”或者太不幸了),而在release版本中暴露了問題。可能在其它機器上debug版本也會崩潰或者發生其它詭異的問題。
說明:測試代碼與項目中的實際代碼不一樣,有可能現象不一樣,但問題的本質是一樣的。
為什么運行Any CPU編譯出來的程序不崩潰?
當Platform target是Any CPU的時候,在工程屬性,Build下的Prefer 32-bit的選項默認是勾選的,編譯的程序會作為 32 位進程運行,所以不會崩潰。如果取消勾選,則編譯出來的程序會作為 64 位應用程序運行,會崩潰。
build settings
關于Platform target的作用,具體參考《CLR via C#》,下圖是從《CLR via C#》中文版第 4 版上截取的。
/platform option 截自《CLR via C#》
總結
.net程序中,令人頭疼的內存破壞問題很難出現了,這極大的提高了程序的穩定性。如果出現堆破壞,很有可能跟P/Invoke或者unsafe代碼相關,可以重點排查相關代碼。
啟用托管調試助手(Managed Debugging Assistants, 下文簡稱MDAs) 有時候會對調試問題有極大的幫助,雖然我這次調試沒有借助MDAs,但我第一個想到的就是MDAs。
關于MDAs的介紹請參考參考資料第一條。
參考資料
Managed Debugging Assistants[1]
GlobalMemoryStatus[2]
《CLR via C#》[3]
References
[1]? Managed Debugging Assistants:
https://docs.microsoft.com/en-us/dotnet/framework/debug-trace-profile/diagnosing-errors-with-managed-debugging-assistants
[2]? GlobalMemoryStatus:
https://docs.microsoft.com/zh-cn/windows/win32/api/winbase/nf-winbase-globalmemorystatus?redirectedfrom=MSDN
[3] 《CLR via C#》:
https://book.douban.com/subject/4924165/
寫留言
總結
以上是生活随笔為你收集整理的[原]调试PInvoke导致的内存破坏的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [ASP.NET Core 3框架揭秘]
- 下一篇: 被忽略的TraceId,可以用起来了