本文記錄我在 .NET 9 裡測試的行為,在方法裡面建立的在棧上的結構體,在方法執行結束之後,棧上的結構體將會被彈棧進入不受管理區域,此時的結構體記憶體內容不會立刻被清空或被改寫
這是我在對 dotnet X11 棧空間被回收導致呼叫 XPutShmImage 閃退 部落格的內容進行更多的測試,確保和 X11 沒有關係,只是存 dotnet C# 的行為
如以下程式碼,在 Foo 方法裡面建立 F 結構體,此時 F 結構體將會在棧上分配。當 Foo 方法執行完成之後,將會彈棧。然而 Foo 的返回值就是指向 F 結構體的棧記憶體地址的指標,出了方法之後,嘗試獲取其欄位輸出
F* foo = Foo();
var a1 = foo->A1;
var a2 = foo->A2;
var a3 = foo->A3;
GC.KeepAlive(a1);
GC.KeepAlive(a2);
GC.KeepAlive(a3);
Console.WriteLine($"{a1} {a2} {a3}");
F* Foo()
{
F f = new F()
{
A1 = 100,
A2 = 200,
A3 = 300
};
return &f;
}
struct F
{
public int A1;
public int A2;
public int A3;
}
經過實驗測試,發現無論在 DEBUG 下,還是 RELEASE 都可以輸出符合預期的 100 200 300 的值。透過此實驗可以證明 dotnet C# 裡面沒有使用如 C++ - 面向基於堆疊的緩衝區保護的 Visual C++ 支援 - Microsoft Learn 文件所述的各種機制,如使用 0xCC 填充不被使用的地址空間
如果我在此基礎之上,繼續呼叫其他方法,讓其他方法壓入棧,這將會汙染或破壞 f 指標指向的結構體的內容。如下面所示
F* foo = Foo();
F2(100,100);
var a1 = foo->A1;
var a2 = foo->A2;
var a3 = foo->A3;
GC.KeepAlive(a1);
GC.KeepAlive(a2);
GC.KeepAlive(a3);
Console.WriteLine($"{a1} {a2} {a3}");
int F2(int n, int count)
{
if (count == 0)
{
return n;
}
if (n == count)
{
return n;
}
count--;
n = Random.Shared.Next();
return F2(n, count);
}
以上程式碼執行,所輸出的 a1 和 a2 和 a3 的值一般不會是 100 200 300 的值,而是會被後續的 F2 方法汙染了棧空間,導致值被改寫
透過以上測試可以瞭解到不安全程式碼確實不安全,在日常編寫過程中,如使用棧地址空間,則必須小心棧地址是否持續可用。這部分沒有其他兜底邏輯,需要開發者自行處理安全性問題
感覺這也很符合 C# dotnet 的設計,不安全程式碼就是不安全,開發者使用不安全程式碼就需要自己處理好程式碼的安全和穩定
本文程式碼放在 github 和 gitee 上,可以使用如下命令列拉取程式碼。我整個程式碼倉庫比較龐大,使用以下命令列可以進行部分拉取,拉取速度比較快
先建立一個空資料夾,接著使用命令列 cd 命令進入此空資料夾,在命令列裡面輸入以下程式碼,即可獲取到本文的程式碼
git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 0a4038a09597a2478aa6073a1382fc0ce60a766e
以上使用的是國內的 gitee 的源,如果 gitee 不能訪問,請替換為 github 的源。請在命令列繼續輸入以下程式碼,將 gitee 源換成 github 源進行拉取程式碼。如果依然拉取不到程式碼,可以發郵件向我要程式碼
git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin 0a4038a09597a2478aa6073a1382fc0ce60a766e
獲取程式碼之後,進入 Workbench/HilahawcaWarcerbakuwhi 資料夾,即可獲取到原始碼
更多技術部落格,請參閱 部落格導航