[原]调试PInvoke导致的内存破坏
緣起
最近項(xiàng)目中遇到一個(gè)詭異的問(wèn)題,程序在升級(jí)到.net4.6.1后,執(zhí)行某個(gè)功能時(shí)會(huì)崩潰,提示訪問(wèn)只讀內(nèi)存區(qū)。大概規(guī)律如下:
debug版不崩潰,release版穩(wěn)定崩潰。
只有x64位的程序崩潰,32位及anycpu編譯出來(lái)的程序運(yùn)行不會(huì)崩潰。
出問(wèn)題的代碼范圍很小(按鈕點(diǎn)擊事件代碼不多)。
根據(jù)以上信息,各位小伙伴有什么思路嗎?
排查
由于release版可以穩(wěn)定重現(xiàn),而且范圍不大,故通過(guò)二分法(每次注釋掉一半代碼,看看是否崩潰,如果崩潰,接著注釋掉一半代碼,如果不崩潰說(shuō)明崩潰跟注釋掉的那段代碼有關(guān)...)很快定位到了導(dǎo)致問(wèn)題的代碼。
最后發(fā)現(xiàn)并不是由于升級(jí).net版本導(dǎo)致的,而是程序本身的問(wèn)題:
代碼中通過(guò)P/Invoke調(diào)用了原生 API GlobalMemoryStatus()。在定義MemoryStatus結(jié)構(gòu)體的時(shí)候強(qiáng)制按4字節(jié)定義了每一個(gè)字段。而在x64下MemoryStatus結(jié)構(gòu)體中的成員有些不是4字節(jié)大小,而是8字節(jié)大小!這樣,傳遞給GlobalMemoryStatus()的MemoryStatus參數(shù)(32字節(jié))比GlobalMemoryStatus()預(yù)期的(56字節(jié))小,導(dǎo)致GlobalMemoryStatus寫了不該寫的內(nèi)存!????????????
重現(xiàn)
我把有問(wèn)題的代碼獨(dú)立出來(lái)了,完整的測(cè)試代碼如下(請(qǐng)編譯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);
}
}
}
修復(fù)
只需要定義MemoryStatus的時(shí)候,注意字段的大小即可。正確的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版會(huì)崩潰?
我在測(cè)試機(jī)器上調(diào)查的原因是debug版本運(yùn)行的時(shí)候,關(guān)鍵內(nèi)存恰巧沒(méi)被破壞(太“幸運(yùn)”或者太不幸了),而在release版本中暴露了問(wèn)題。可能在其它機(jī)器上debug版本也會(huì)崩潰或者發(fā)生其它詭異的問(wèn)題。
說(shuō)明:測(cè)試代碼與項(xiàng)目中的實(shí)際代碼不一樣,有可能現(xiàn)象不一樣,但問(wèn)題的本質(zhì)是一樣的。
為什么運(yùn)行Any CPU編譯出來(lái)的程序不崩潰?
當(dāng)Platform target是Any CPU的時(shí)候,在工程屬性,Build下的Prefer 32-bit的選項(xiàng)默認(rèn)是勾選的,編譯的程序會(huì)作為 32 位進(jìn)程運(yùn)行,所以不會(huì)崩潰。如果取消勾選,則編譯出來(lái)的程序會(huì)作為 64 位應(yīng)用程序運(yùn)行,會(huì)崩潰。
build settings
關(guān)于Platform target的作用,具體參考《CLR via C#》,下圖是從《CLR via C#》中文版第 4 版上截取的。
/platform option 截自《CLR via C#》
總結(jié)
.net程序中,令人頭疼的內(nèi)存破壞問(wèn)題很難出現(xiàn)了,這極大的提高了程序的穩(wěn)定性。如果出現(xiàn)堆破壞,很有可能跟P/Invoke或者unsafe代碼相關(guān),可以重點(diǎn)排查相關(guān)代碼。
啟用托管調(diào)試助手(Managed Debugging Assistants, 下文簡(jiǎn)稱MDAs) 有時(shí)候會(huì)對(duì)調(diào)試問(wèn)題有極大的幫助,雖然我這次調(diào)試沒(méi)有借助MDAs,但我第一個(gè)想到的就是MDAs。
關(guān)于MDAs的介紹請(qǐng)參考參考資料第一條。
參考資料
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/
寫留言
總結(jié)
以上是生活随笔為你收集整理的[原]调试PInvoke导致的内存破坏的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: [ASP.NET Core 3框架揭秘]
- 下一篇: 被忽略的TraceId,可以用起来了