標籤:

.NET CLR怎麼保證執行正確的unsafe代碼不掛掉?

問題有兩個, 第一個reference是不是類似於Handle, 類似於一個地址的地址, 這樣.NET CLR在GC的時候, 可以挪動那個對象, 同時修改reference指向地址裡面保存的地址. 具體實現的時候是不是這樣?

第二個是, 一個實現上沒有錯誤的unsafe代碼, .NET CLR是不是需要保證用的對象不被挪走? 比如下面寫的:

public unsafe int Count(string str, char c)
{
int count = 0;
fixed(char* p = str)
{
for (int i = 0; i &< str.Length; ++i) count += p[i] == c ? 1 : 0; } return count; }


簡短回答:

第一個reference是不是類似於Handle, 類似於一個地址的地址, 這樣.NET CLR在GC的時候, 可以挪動那個對象, 同時修改reference指向地址裡面保存的地址. 具體實現的時候是不是這樣?

CLI VES規範里並沒有要求具體實現採用什麼方式實現managed pointer。具體到微軟的CLR / CoreCLR的實現的話,普通的managed pointer是個直接指針,而不是「指針的指針」——後者叫做handle,這種方式實現的堆叫做handle-based heap。

關於CLR的對象模型的實現,以及與其它一些JVM實現之類的對比,請跳傳送門:為什麼bs虛函數表的地址(int*)(bs)與虛函數地址(int*)*(int*)(bs) 不是同一個? - RednaxelaFX 的回答 這邊的例子可以顯示出direct-pointer-based heap與handle-based heap的差異。

顯然,對於要支持對象移動的GC heap,handle-based heap更容易實現,但這樣每次訪問對象內容都要做雙層解引用,性能(訪問效率)比使用直接指針的方案要差。

CLR的managed pointer使用直接指針實現,而CLR的GC又可以移動對象,這就要求CLR能夠準確跟蹤所有managed pointer所在的位置,無論它是「棧上」的局部變數、堆里的欄位還是從VM內部出發的引用,以保證CLR能夠:

  • 發現託管指針,通過其值來判斷對象的生死
  • 在活對象被移動之後,更新所有相關的託管指針的指向

於是CLR的GC默認的工作模式是「準確式GC」(precise GC,或者叫type-exact GC)。程序的元數據會準確指出棧上的變數和堆里對象的欄位哪些是託管指針。相關討論請跳傳送門:找出棧上的指針/引用

CLR也有一種備用的「保守式GC」(conservative GC)模式,不要求程序對棧上的變數是否為託管指針提供準確的描述。這主要是在開發過程的早期使用的,而在實際發布的產品中並沒有啟用這種模式。

近期使用了這種模式的項目之一是LLILC,微軟嘗試給CoreCLR做的基於LLVM的新編譯器。它為了早期開發方便而選擇先抄個捷徑,但這個決定似乎對後期開發帶來了負面影響:[八卦] LLILC項目貌似掛了… - 編程語言與高級語言虛擬機雜談(仮) - 知乎專欄

第二個是, 一個實現上沒有錯誤的unsafe代碼, .NET CLR是不是需要保證用的對象不被挪走?

CLR可以執行的託管代碼(managed code)分為兩類:

  • verifiable code:普通的C#代碼屬於這類。代碼的類型安全可以通過靜態校驗來保證。不可以使用裸指針。
  • unverifiable code:也就是unsafe code。CLR無法通過靜態校驗來保證類型安全,程序員要自己保證寫的代碼的語義是正確的。在unverifiable code中可以使用裸指針(非託管指針),指針既可以指向C-style的非託管內存,也可以指向被pin住的託管堆中的對象。還可以調用非託管的函數指針(calli)。

注意:unverifiable code仍然是managed code。MSIL有專門的unsafe子集來表達unsafe語義。

上面描述的重點內容之一,是指向託管堆對象的unsafe code中的指針,只能指向被pin住的對象。這個「object pinning」語義在C#里是通過fixed關鍵字來表達的。

為了高效地支持unsafe code(以及諸如System.Runtime.InteropServices.GCHandle的功能),CLR的GC必須要直接支持object pinning——即便託管堆里有對象正在被pin住,GC也要可以正常執行。

反例是例如HotSpot JVM,它的GC們都不直接支持object pinning,因而在執行JNI的critical系API(要求暫時不移動某些對象)時不得不暫時禁止GC執行。這就可以很悲劇…

如果不使用GCHandle,CLR只保證對象在unsafe code里是被pin住的,所以如果故意在unsafe code里把指向被pin住的對象的指針傳遞給unmanaged code保存起來,然後managed code一側離開unsafe code之後,unmanaged code還試圖去使用之前存下來的指針,那語義就是沒有保證的——對象可能已經被挪走了。

&<- @vczh 的回答里提到的例子就是這種情況。要保證安全的話,最好是在傳出指針給unmanaged code之前就在safe一側創建合適的GCHandle把目標對象一直pin住,直到unmanaged code不再需要那個指針才撤銷safe這邊的GCHandle。


CLR對unsafe的代碼不作任何保證。如果你想要譬如說一個handle不能被GC挪走的事情,你需要在safe的那邊明確指出來。通常來講你需要使用Marshal類。譬如說lambda表達式傳給C++代碼變成函數指針之後,跑著跑著被GC走得事情也是會發生的,你在C#這邊也要找個地方存放lambda表達式的reference。諸如此類。


一、

1、實現上一般不是用地址的地址,一般用直接地址。

2、但是CLI應該沒有要求用哪種形式,換言之用地址的地址實現也是可以的,但是考慮到效率問題一般都會用直接地址吧,畢竟只有GC的時候才需要修改。

3、GC回收的時候會合併內存,所以託管對象的地址會改變,與此同時引用會跟著改。

二、fixed不就是干這個的么?

R大說的是對的,一開始我沒看到地址的地址,想當然的以為你問的是GC會不會移對象和更新地址,答案是是。


CLR根本不保證unsafe的代碼safe。


推薦閱讀:

C#里的顯式介面實現是什麼原理?
如何開始學習CoreCLR源代碼?
Stack-based 的虛擬機有什麼常用的優化策略?
RyuJIT為什麼比JIT64編譯速度快?
程序集什麼玩意?我知道其表現形式為dll和exe,但是exe不是直接執行的文件嗎?而dll只是類庫,供exe調用代碼?

TAG:NET | CLR |