dotnet C# 結構體出方法彈棧之後的行為

lindexi發表於2024-08-24

本文記錄我在 .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 的設計,不安全程式碼就是不安全,開發者使用不安全程式碼就需要自己處理好程式碼的安全和穩定

本文程式碼放在 githubgitee 上,可以使用如下命令列拉取程式碼。我整個程式碼倉庫比較龐大,使用以下命令列可以進行部分拉取,拉取速度比較快

先建立一個空資料夾,接著使用命令列 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 資料夾,即可獲取到原始碼

更多技術部落格,請參閱 部落格導航

相關文章