1. 程式人生 > >unity3d跨平臺原理

unity3d跨平臺原理

前言:

其實小匹夫在U3D的開發中一直對U3D的跨平臺能力很好奇。到底是什麼原理使得U3D可以跨平臺呢?後來發現了Mono的作用,並進一步瞭解到了CIL的存在。所以,作為一個對Unity3D跨平臺能力感興趣的U3D程式猿,小匹夫如何能不關注CIL這個話題呢?那麼下面各位看官就拾起語文老師教導我們的作文口訣(WhyWhatHow),和小匹夫一起走進CIL的世界吧~

Why?

回到本文的題目,U3D或者說Mono的跨平臺是如何做到的?

如果換做小匹夫或者看官你來做,應該怎麼實現一套程式碼對應多種平臺呢?

其實原理想想也簡單,生活中也有很多可以參考的例子,比如下圖(誰讓小匹夫是坐移動端開發的呢,只能物盡其用從自己身邊找例子了T.T):

像這樣一根線,管你是安卓還是ios都能充電。所以從這個意義上,這貨也實現了跨平臺。那麼我們能從它身上學到什麼呢?對的,那就是從一樣的能源(電)到不同的平臺(ios,安卓)之間需要一箇中間層過度轉換一下。

那麼來到U3D為何能跨平臺,簡而言之,其實現原理在於使用了叫CIL(Common Intermediate Language通用中間語言,也叫做MSIL微軟中間語言)的一種程式碼指令集,CIL可以在任何支援CLI(Common Language Infrastructure,通用語言基礎結構)的環境中執行,就像.NET是微軟對這一標準的實現,Mono則是對CLI的又一實現。由於CIL能執行在所有支援CLI的環境中,例如剛剛提到的.NET執行時以及Mono執行時,也就是說和具體的平臺或者CPU無關。這樣就無需根據平臺的不同而部署不同的內容了。所以到這裡,各位也應該恍然大了。程式碼的編譯只需要分為兩部分就好了嘛:

  1. 從程式碼本身到CIL的編譯(其實之後CIL還會被編譯成一種位元碼,生成一個CLI assembly)
  2. 執行時從CIL(其實是CLI assembly,不過為了直觀理解,不必糾結這種細節)到本地指令的即時編譯(這就引出了為何U3D官方沒有提供熱更新的原因:在iOS平臺中Mono無法使用JIT引擎,而是以Full AOT模式執行的,所以此處說的額即時編譯不包括IOS

What?

上文也說了CIL是指令集,但是不是還是太模糊了呢?所以語文老師教導我們,描述一個東西時肯定要先從外貌寫起。遵循老師的教導,我們不妨先通過工具來看看CIL到底長什麼樣。

工具就是ilasm了。下面小匹夫寫一個簡單的.cs看看生成的CIL程式碼長什麼樣。

C#程式碼:

class Class1
{
    public static void Main(string[] args)
    {
        System.Console.WriteLine("hi");
    }
}

CIL程式碼:

.class private auto ansi beforefieldinit Class1
       extends [mscorlib]System.Object
{
  .method public hidebysig static void  Main(string[] args) cil managed
  {
    .entrypoint
    // 程式碼大小       13 (0xd)
    .maxstack  8
    IL_0000:  nop
    IL_0001:  ldstr      "hi"
    IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_000b:  nop
    IL_000c:  ret
  } // end of method Class1::Main

  .method public hidebysig specialname rtspecialname 
          instance void  .ctor() cil managed
  {
    // 程式碼大小       7 (0x7)
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
    IL_0006:  ret
  } // end of method Class1::.ctor

} // end of class Class1

好啦。程式碼雖然簡單,但是也能說明足夠多的問題。那麼和CIL的第一次親密接觸,能給我們留下什麼直觀的印象呢?

  1. 以“.”一個點號開頭的,例如上面這份程式碼中的:.class、.method 。我們稱之為CIL指令(directive),用於描述.NET程式集總體結構的標記。為啥需要它呢?因為你總得告訴編譯器你處理的是啥吧。
  2. 貌似CIL程式碼中還看到了private、public這樣的身影。姑且稱之為CIL特性(attribute)。它的作用也很好理解,通過CIL指令並不能完全說明.NET成員和類,針對CIL指令進行補充說明成員或者類的特性的。市面上常見的還有:extends,implements等等。
  3. 每一行CIL程式碼基本都有的,對,那就是CIL操作碼咯。小匹夫從網上找了一份漢化的操作碼錶放在附錄部分,當然英文版的你的vs就有。

直觀的印象有了,但是離我們的短期目標,說清楚(或者說介紹個大概)CIL是What,甚至是終極目標,搞明白Uniyt3D為何能跨平臺還有2萬4千9百里的距離。

好啦,話不多說,繼續亂侃。

參照附錄中的操作碼錶,對照可以總結出一份更易讀的表格。那就是如下的表啦。

主要操作 運算元範圍/條件 運算元型別 運算元
縮寫 全稱 含義 縮寫 全稱 含義 縮寫 全稱 含義 縮寫 全稱 含義
ld load 將運算元壓到堆疊當中,相當於:
push ax
arg argument 引數 ? ? 運算元中的數值 .0 ? 第零個引數 
.1 ? 第一個引數
.2 ? 第二個引數
.3 ? 第三個引數
.s xx (short) 引數xx
a address 運算元的地址 只有 .s xx,參見ldarg.s
loc local 區域性變數 參見ldarg
fld field 欄位(類的全域性變數) 參見ldarg xx ? xx欄位,eg:
ldfld xx
c const 常量 .i4 int 4 bytes C#裡面的int,其他的型別例如short需要通過conv轉換 .m1 minus 1 -1
.0 ? 0
.1 ? 1
……
.8 8
.s (short) 後面跟一個位元組以內的整型數值(有符號的)
? ? 後面跟四個位元組的整型數值
.i8 int 8 bytes C#裡面的long ? ? 後面跟八個位元組的整型數值
.r4 real 4 bytes C#裡面的float ? ? 後面跟四個位元組的浮點數值
.r8 real 8 bytes C#裡面的double ? ? 後面跟八個位元組的浮點數值
null null 空值(也就是0) ? ? ? ? ? ?
st store 計算堆疊的頂部彈出當前值,相當於:
pop ax
參見ld 
conv convert 數值型別轉換,僅僅用純粹的數值型別間的轉換,例如int/float等 ? ? ? .i1 int 1 bytes C#裡面的sbyte ? ? ?
.i2 int 2 bytes C#裡面的short
.i4 int 4 bytes C#裡面的int
.i8 int 8 bytes C#裡面的long
.r4 real 4 bytes C#裡面的float
.r8 real 8 bytes C#裡面的double
.u4 uint 4 bytes C#裡面的uint
.u8 uint 8 bytes C#裡面的ulong
b/br branch 條件和無條件跳轉,相當於:
jmp/jxx label_jump
br ? ? 無條件跳轉 ? ? ? ? ? 後面跟四個位元組的偏移量(有符號)
.s (short) 後面跟一個位元組的偏移量(有符號)
false false 值為零的時候跳轉 ? ? ? 參見br
true true 值不為零的時候跳轉 ? ? ?
b eq equal to 相等 ? ? ?
ne not equal to 不相等 un unsigned or unordered 無氟好的(對於整數)或者無序的(對於浮點)
gt greater than 大於
lt less than 小於
ge greater than or equal to 大於等於
le less than or equal to 小於等於
call call 呼叫 ? ? ? ? ? (非虛擬函式) ?
? ? ? virt virtual 虛擬函式

在此,小匹夫想請各位認真讀表,然後心中默數3個數,最後看看都能發現些什麼。

基於堆疊

如果是小匹夫的話,第一感覺就是基本每一條描述中都包含一個”棧“。不錯,CIL是基於堆疊的,也就是說CIL的VM(mono執行時)是一個棧式機。這就意味著資料是推入堆疊,通過堆疊來操作的,而非通過CPU的暫存器來操作,這更加驗證了其和具體的CPU架構沒有關係。為了說明這一點,小匹夫舉個例子好啦。

大學時候學微控制器(大概是8086,記不清了)的時候記得做加法大概是這樣的:

add eax,-2

其中的eax是啥?暫存器。所以如果CIL處理資料要通過cpu的暫存器的話,那也就不可能和cpu的架構無關了。

當然,CIL之所以是基於堆疊而非CPU的另一個原因是相比較於cpu的暫存器,操作堆疊實在太簡單了。回到剛才小匹夫說的大學時候曾經學過的微控制器那門課程上,當時記得各種暫存器,各種標誌位,各種。。。,而堆疊只需要簡單的壓棧和彈出,因此對於虛擬機器的實現來說是再合適不過了。所以想要更具體的瞭解CIL基於堆疊這一點,各位可以去看一下堆疊方面的內容。這裡小匹夫就不拓展了。

面向物件

那麼第二感覺呢?貌似附錄的表中有new物件的語句呀。嗯,的確,CIL同樣是面向物件的。

這意味著什麼呢?那就是在CIL中你可以建立物件,呼叫物件的方法,訪問物件的成員。而這裡需要注意的就是對方法的呼叫。

回到上表中的右上角。對,就是對引數的操作部分。靜態方法和例項方法是不同的哦~

  1. 靜態方法:ldarg.0麼有被佔用,所以引數從ldarg.0開始。
  2. 例項方法:ldarg.0是被this佔用的,也就是說實際上的引數是從ldarg.1開始的。

舉個例子:假設你有一個類Murong中有一個靜態方法Add(int32 a, int32 b),實現的內容就如同它的名字一樣使兩個數相加,所以需要2個引數。和一個例項方法TellName(string name),這個方法會告訴你傳入的名字。

class  Murong
{
    public void TellName(string name)
    {
        System.Console.WriteLine(name);
    }

    public static int Add(int a, int b)
    {
       return a + b;
    }
}

靜態方法的處理:

那麼其中的靜態方法Add的CIL程式碼如下:

//小匹夫註釋一下。
.method public hidebysig static int32  Add(int32 a,
                                           int32 b) cil managed
{
  // 程式碼大小       9 (0x9)
  .maxstack  2
  .locals init ([0] int32 CS$1$0000)   //初始化區域性變數列表。因為我們只返回了一個int型。所以這裡聲明瞭一個int32型別。索引為0
  IL_0000:  nop
  IL_0001:  ldarg.0     //將索引為 0 的引數載入到計算堆疊上。
  IL_0002:  ldarg.1     //將索引為 1 的引數載入到計算堆疊上。
  IL_0003:  add          //計算
  IL_0004:  stloc.0      //從計算堆疊的頂部彈出當前值並將其儲存到索引 0 處的區域性變數列表中。
  IL_0005:  br.s       IL_0007
  IL_0007:  ldloc.0     //將索引 0 處的區域性變數載入到計算堆疊上。
  IL_0008:  ret           //返回該值
} // end of method Murong::Add

那麼我們呼叫這個靜態函式應該就是這樣咯。

Murong.Add(1, 2);

對應的CIL程式碼為:

  IL_0001:  ldc.i4.1 //將整數1壓入棧中
  IL_0002:  ldc.i4.2 //將整數2壓入棧中
  IL_0003:  call       int32 Murong::Add(int32,
                                         int32)  //呼叫靜態方法

可見CIL直接call了Murong的Add方法,而不需要一個Murong的例項。

例項方法的處理:

Murong類中的例項方法TellName()的CIL程式碼如下:

.method public hidebysig instance void  TellName(string name) cil managed
{
  // 程式碼大小       9 (0x9)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ldarg.1     //看到和靜態方法的區別了嗎?
  IL_0002:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_0007:  nop
  IL_0008:  ret
} // end of method Murong::TellName

看到和靜態方法的區別了嗎?對,第一個引數對應的是ldarg.1中的引數1,而不是靜態方法中的0。因為此時引數0相當於this,this是不用參與引數傳遞的。

那麼我們再看看呼叫例項方法的C#程式碼和對應的CIL程式碼是如何的。

//C#
Murong murong = new Murong(); murong.TellName("chenjiadong");

CIL:

.locals init ([0] class Murong murong)   //因為C#程式碼中定義了一個Murong型別的變數,所以區域性變數列表的索引0為該型別的引用。
//....
IL_0009:  newobj     instance void Murong::.ctor() //相比上面的靜態方法的呼叫,此處new一個新物件,出現了instance方法。
IL_000e:  stloc.0
IL_000f:  ldloc.0
IL_0010:  ldstr      "chenjiadong" //小匹夫的名字入棧
IL_0015:  callvirt   instance void Murong::TellName(string) //例項方法的呼叫也有instance

到此,受制於篇幅所限(小匹夫不想寫那麼多字啊啊啊!)CIL是What的問題大致介紹一下。當然沒有再拓展,以後小匹夫可能會再詳細寫一下這塊。

How?

記得語文老師說過,寫作文最重要的一點是要首尾呼應。既然咱們開篇就提出了U3D為何能跨平臺的問題,那麼接近文章的結尾咱們就再來

提問:

Q:上面的Why部分,咱們知道了U3D能跨平臺是因為存在著一個能通吃的中間語言CIL,這也是所謂跨平臺的前提,但是為啥CIL能通吃各大平臺呢?當然可以說CIL基於堆疊,跟你CPU怎麼架構的沒啥關係,但是感覺過於理論化、學術化,那還有沒有通俗化、工程化的說法呢?

A:原因就是前面小匹夫提到過的,.Net執行時和Mono執行時。也就是說CIL語言其實是執行在虛擬機器中的,具體到咱們的U3D也就是mono的執行時了,換言之mono執行的其實CIL語言,CIL也並非真正的在本地執行,而是在mono執行時中執行的,執行在本地的是被編譯後生成的原生程式碼。當然看官博的文章,他們似乎也在開發自己的“mono”,也就是被稱為指令碼的未來的IL2Cpp,這種類似執行時的功能是將IL再編譯成c++,再由c++編譯成原生程式碼,據說效率提升很可觀,小匹夫也是蠻期待的。

這裡為了“實現跨平臺式的演示”,小匹夫用mac給各位做個測試好啦:

從C#到CIL

新建一個cs檔案,然後使用mono來執行。這個cs檔案內容如下:

然後咱們直接在命令列中執行這個cs檔案試試~

說的很清楚,檔案沒有包含一個CIL映像。可見mono是不能直接執行cs檔案的。假如我們把它編譯成CIL呢?那麼我們用mono帶的mcs來編譯小匹夫的Test.cs檔案。

mcs Test.cs

生成了什麼呢?如圖:

好像沒見有叫.IL的檔案生成啊?反而好像多了一個.exe檔案?可是沒聽說Mac能執行exe檔案呀?可為啥又生成了.exe呢?各位看官可能要說,小匹夫你是不是拿windows截圖P的啊?嘿嘿,小匹夫可不敢。辣麼真相其實就是這個exe並不是讓Mac來執行的,而是留給mono執行時來執行的,換言之這個檔案的可執行程式碼形式是CIL的位元碼形態。到此,我們完成了從C#到CIL的過程。接下來就讓我們執行下剛剛的成果好啦。

1 mono Test.exe

 

結果是輸出了一個大大的“Hi”。這裡,就引出了下一個部分。

從CIL到Native Code

這個“HI”可是在小匹夫的MAC終端上出現的呀,那麼就證明這個C#寫的程式碼在MAC上執行的還挺“嗨”。

為啥呢?為啥C#寫的程式碼能跑在MAC上呢?這就不得不提從CIL如何到本機原生程式碼的過程了。Mono提供了兩種編譯方式,就是我們經常能看到的:JIT(Just-in-Time compilation,即時編譯)和AOT(Ahead-of-Time,提前編譯或靜態編譯)。這兩種方式都是將CIL進一步編譯成平臺的原生程式碼。這也是實現跨平臺的最後一步。下面就分頭介紹一下。

JIT即時編譯:

從名字就能看的出來,即時編譯,或者稱之為動態編譯,是在程式執行時才編譯程式碼,解釋一條語句執行一條語句,即將一條中間的託管的語句翻譯成一條機器語句,然後執行這條機器語句。但同時也會將編譯過的程式碼進行快取,而不是每一次都進行編譯。所以可以說它是靜態編譯和直譯器的結合體。不過你想想機器既要處理程式碼的邏輯,同時還要進行編譯的工作,所以其執行時的效率肯定是受到影響的。因此,Mono會有一部分程式碼通過AOT靜態編譯,以降低在程式執行時JIT動態編譯在效率上的問題。

不過一向嚴苛的IOS平臺是不允許這種動態的編譯方式的,這也是U3D官方無法給出熱更新方案的一個原因。而Android平臺恰恰相反,Dalvik虛擬機器使用的就是JIT方案。

AOT靜態編譯:

其實Mono的AOT靜態編譯和JIT並非對立的。AOT同樣使用了JIT來進行編譯,只不過是被AOT編譯的程式碼在程式執行之前就已經編譯好了。當然還有一部分程式碼會通過JIT來進行動態編譯。下面小匹夫就手動操作一下mono,讓它進行一次AOT編譯。

//在命令列輸入
mono --aot Test.exe

結果:

從圖中可以看到JIT time: 39 ms,也就是說Mono的AOT模式其實會使用到JIT,同時我們看到了生成了一個適應小匹夫的MAC的動態庫Test.exe.dylib,而在Linux生成就是.so(共享庫)。

AOT編譯出來的庫,除了包括我們的程式碼之外,還有被快取的元資料資訊。所以我們甚至可以只編譯元資料資訊而不變異程式碼。例如這樣:

//只包含元資料的資訊
mono --aot=metadata-only Test.exe

可見程式碼沒有被包括進來。

那麼簡單總結一下AOT的過程:

  1. 收集要被編譯的方法
  2. 使用JIT進行編譯
  3. 發射(Emitting)經JIT編譯過的程式碼和其他資訊
  4. 直接生成檔案或者呼叫本地彙編器或聯結器進行處理之後生成檔案。(例如上圖中使用了小匹夫本地的gcc)

Full AOT

當然上文也說了,IOS平臺是禁止使用JIT的,可看樣子Mono的AOT模式仍然會保留一部分程式碼會在程式執行時動態編譯。所以為了破解這個問題,Mono提供了一個被稱為Full AOT的模式。即預先對程式集中的所有CIL程式碼進行AOT編譯生成一個原生代碼映像,然後在執行時直接載入這個映像而不再使用JIT引擎。目前由於技術或實現上的原因在使用Full AOT時有一些限制,不過這裡不再多說了。以後也還會更細的分析下AOT。

總結:

好啦,寫到現在也已經到了凌晨3:04分了。感覺寫的內容也差不多了。那麼對本文的主題U3D為何能跨平臺以及CIL做個最終的總結陳詞:

  1. CIL是CLI標準定義的一種可讀性較低的語言。
  2. 以.NET或mono等實現CLI標準的執行環境為目標的語言要先編譯成CIL,之後CIL會被編譯,並且以位元碼的形式存在(原始碼--->中間語言的過程)。
  3. 這種位元碼執行在虛擬機器中(.net mono的執行時)。
  4. 這種位元碼可以被進一步編譯成不同平臺的原生程式碼(中間語言--->原生程式碼的過程)。
  5. 面向物件
  6. 基於堆疊

如果各位看官覺得文章寫得還好,那麼就容小匹夫跪求各位給點個“推薦”,謝啦~

附錄:

名稱 說明
Add 將兩個值相加並將結果推送到計算堆疊上。
Add.Ovf 將兩個整數相加,執行溢位檢查,並且將結果推送到計算堆疊上。
Add.Ovf.Un 將兩個無符號整數值相加,執行溢位檢查,並且將結果推送到計算堆疊上。
And 計算兩個值的按位“與”並將結果推送到計算堆疊上。
Arglist 返回指向當前方法的引數列表的非託管指標。
Beq 如果兩個值相等,則將控制轉移到目標指令。
Beq.S 如果兩個值相等,則將控制轉移到目標指令(短格式)。
Bge 如果第一個值大於或等於第二個值,則將控制轉移到目標指令。
Bge.S 如果第一個值大於或等於第二個值,則將控制轉移到目標指令(短格式)。
Bge.Un 當比較無符號整數值或不可排序的浮點型值時,如果第一個值大於第二個值,則將控制轉移到目標指令。
Bge.Un.S 當比較無符號整數值或不可排序的浮點型值時,如果第一個值大於第二個值,則將控制轉移到目標指令(短格式)。
Bgt 如果第一個值大於第二個值,則將控制轉移到目標指令。
Bgt.S 如果第一個值大於第二個值,則將控制轉移到目標指令(短格式)。
Bgt.Un 當比較無符號整數值或不可排序的浮點型值時,如果第一個值大於第二個值,則將控制轉移到目標指令。
Bgt.Un.S 當比較無符號整數值或不可排序的浮點型值時,如果第一個值大於第二個值,則將控制轉移到目標指令(短格式)。
Ble 如果第一個值小於或等於第二個值,則將控制轉移到目標指令。
Ble.S 如果第一個值小於或等於第二個值,則將控制轉移到目標指令(短格式)。
Ble.Un 當比較無符號整數值或不可排序的浮點型值時,如果第一個值小於或等於第二個值,則將控制轉移到目標指令。
Ble.Un.S 當比較無符號整數值或不可排序的浮點值時,如果第一個值小於或等於第二個值,則將控制權轉移到目標指令(短格式)。
Blt 如果第一個值小於第二個值,則將控制轉移到目標指令。
Blt.S 如果第一個值小於第二個值,則將控制轉移到目標指令(短格式)。
Blt.Un 當比較無符號整數值或不可排序的浮點型值時,如果第一個值小於第二個值,則將控制轉移到目標指令。
Blt.Un.S 當比較無符號整數值或不可排序的浮點型值時,如果第一個值小於第二個值,則將控制轉移到目標指令(短格式)。
Bne.Un 當兩個無符號整數值或不可排序的浮點型值不相等時,將控制轉移到目標指令。
Bne.Un.S 當兩個無符號整數值或不可排序的浮點型值不相等時,將控制轉移到目標指令(短格式)。
Box 將值類轉換為物件引用(O 型別)。
Br 無條件地將控制轉移到目標指令。
Br.S 無條件地將控制轉移到目標指令(短格式)。
Break 向公共語言結構 (CLI) 發出訊號以通知偵錯程式已撞上了一個斷點。
Brfalse 如果 value 為 false、空引用(Visual Basic 中的 Nothing)或零,則將控制轉移到目標指令。
Brfalse.S 如果 value 為 false、空引用或零,則將控制轉移到目標指令。
Brtrue 如果 value 為 true、非空或非零,則將控制轉移到目標指令。
Brtrue.S 如果 value 為 true、非空或非零,則將控制轉移到目標指令(短格式)。
Call 呼叫由傳遞的方法說明符指示的方法。
Calli 通過呼叫約定描述的引數呼叫在計算堆疊上指示的方法(作為指向入口點的指標)。
Callvirt 對物件呼叫後期繫結方法,並且將返回值推送到計算堆疊上。
Castclass 嘗試將引用傳遞的物件轉換為指定的類。
Ceq 比較兩個值。如果這兩個值相等,則將整數值 1 (int32) 推送到計算堆疊上;否則,將 0 (int32) 推送到計算堆疊上。
Cgt 比較兩個值。如果第一個值大於第二個值,則將整數值 1 (int32) 推送到計算堆疊上;反之,將 0 (int32) 推送到計算堆疊上。
Cgt.Un 比較兩個無符號的或不可排序的值。如果第一個值大於第二個值,則將整數值 1 (int32) 推送到計算堆疊上;反之,將 0 (int32) 推送到計算堆疊上。
Ckfinite 如果值不是有限數,則引發 ArithmeticException。
Clt 比較兩個值。如果第一個值小於第二個值,則將整數值 1 (int32) 推送到計算堆疊上;反之,將 0 (int32) 推送到計算堆疊上。
Clt.Un 比較無符號的或不可排序的值 value1 和 value2。如果 value1 小於 value2,則將整數值 1 (int32 ) 推送到計算堆疊上;反之,將 0 ( int32 ) 推送到計算堆疊上。
Constrained 約束要對其進行虛方法呼叫的型別。
Conv.I 將位於計算堆疊頂部的值轉換為 native int。
Conv.I1 將位於計算堆疊頂部的值轉換為 int8,然後將其擴充套件(填充)為 int32。
Conv.I2 將位於計算堆疊頂部的值轉換為 int16,然後將其擴充套件(填充)為 int32。
Conv.I4 將位於計算堆疊頂部的值轉換為 int32。
Conv.I8 將位於計算堆疊頂部的值轉換為 int64。
Conv.Ovf.I 將位於計算堆疊頂部的有符號值轉換為有符號 native int,並在溢位時引發 OverflowException。
Conv.Ovf.I.Un 將位於計算堆疊頂部的無符號值轉換為有符號 native int,並在溢位時引發 OverflowException。
Conv.Ovf.I1 將位於計算堆疊頂部的有符號值轉換為有符號 int8 並將其擴充套件為 int32,並在溢位時引發 OverflowException。
Conv.Ovf.I1.Un 將位於計算堆疊頂部的無符號值轉換為有符號 int8 並將其擴充套件為 int32,並在溢位時引發 OverflowException。
Conv.Ovf.I2 將位於計算堆疊頂部的有符號值轉換為有符號 int16 並將其擴充套件為 int32,並在溢位時引發 OverflowException。
Conv.Ovf.I2.Un 將位於計算堆疊頂部的無符號值轉換為有符號 int16 並將其擴充套件為 int32,並在溢位時引發 OverflowException。
Conv.Ovf.I4 將位於計算堆疊頂部的有符號值轉換為有符號 int32,並在溢位時引發 OverflowException。
Conv.Ovf.I4.Un 將位於計算堆疊頂部的無符號值轉換為有符號 int32,並在溢位時引發 OverflowException。
Conv.Ovf.I8 將位於計算堆疊頂部的有符號值轉換為有符號 int64,並在溢位時引發 OverflowException。
Conv.Ovf.I8.Un 將位於計算堆疊頂部的無符號值轉換為有符號 int64,並在溢位時引發 OverflowException。
Conv.Ovf.U 將位於計算堆疊頂部的有符號值轉換為 unsigned native int,並在溢位時引發 OverflowException。
Conv.Ovf.U.Un 將位於計算堆疊頂部的無符號值轉換為 unsigned native int,並在溢位時引發 OverflowException。
Conv.Ovf.U1 將位於計算堆疊頂部的有符號值轉換為 unsigned int8 並將其擴充套件為 int32,並在溢位時引發 OverflowException。
Conv.Ovf.U1.Un 將位於計算堆疊頂部的無符號值轉換為 unsigned int8 並將其擴充套件為 int32,並在溢位時引發 OverflowException。
Conv.Ovf.U2 將位於計算堆疊頂部的有符號值轉換為 unsigned int16 並將其擴充套件為 int32,並在溢位時引發 OverflowException。
Conv.Ovf.U2.Un 將位於計算堆疊頂部的無符號值轉換為 unsigned int16 並將其擴充套件為 int32,並在溢位時引發 OverflowException。
Conv.Ovf.U4 將位於計算堆疊頂部的有符號值轉換為 unsigned int32,並在溢位時引發 OverflowException。
Conv.Ovf.U4.Un 將位於計算堆疊頂部的無符號值轉換為 unsigned int32,並在溢位時引發 OverflowException。
Conv.Ovf.U8 將位於計算堆疊頂部的有符號值轉換為 unsigned int64,並在溢位時引發 OverflowException。
Conv.Ovf.U8.Un 將位於計算堆疊頂部的無符號值轉換為 unsigned int64,並在溢位時引發 OverflowException。
Conv.R.Un 將位於計算堆疊頂部的無符號整數值轉換為 float32。
Conv.R4 將位於計算堆疊頂部的值轉換為 float32。
Conv.R8 將位於計算堆疊頂部的值轉換為 float64。
Conv.U 將位於計算堆疊頂部的值轉換為 unsigned native int,然後將其擴充套件為 native int。
Conv.U1 將位於計算堆疊頂部的值轉換為 unsigned int8,然後將其擴充套件為 int32。
Conv.U2 將位於計算堆疊頂部的值轉換為 unsigned int16,然後將其擴充套件為 int32。
Conv.U4 將位於計算堆疊頂部的值轉換為 unsigned int32,然後將其擴充套件為 int32。
Conv.U8 將位於計算堆疊頂部的值轉換為 unsigned int64,然後將其擴充套件為 int64。
Cpblk 將指定數目的位元組從源地址複製到目標地址。
Cpobj 將位於物件(&、* 或 native int 型別)地址的值型別複製到目標物件(&、* 或 native int 型別)的地址。
Div 將兩個值相除並將結果作為浮點(F 型別)或商(int32 型別)推送到計算堆疊上。
Div.Un 兩個無符號整數值相除並將結果 ( int32 ) 推送到計算堆疊上。
Dup 複製計算堆疊上當前最頂端的值,然後將副本推送到計算堆疊上。
Endfilter 將控制從異常的 filter 子句轉移回公共語言結構 (CLI) 異常處理程式。
Endfinally 將控制從異常塊的 fault 或 finally 子句轉移回公共語言結構 (CLI) 異常處理程式。
Initblk 將位於特定地址的記憶體的指定塊初始化為給定大小和初始值。
Initobj 將位於指定地址的值型別的每個欄位初始化為空引用或適當的基元型別的 0。
Isinst 測試物件引用(O 型別)是否為特定類的例項。
Jmp 退出當前方法並跳至指定方法。
Ldarg 將引數(由指定索引值引用)載入到堆疊上。
Ldarg.0 將索引為 0 的引數載入到計算堆疊上。
Ldarg.1 將索引為 1 的引數載入到計算堆疊上。
Ldarg.2 將索引為 2 的引數載入到計算堆疊上。
Ldarg.3 將索引為 3 的引數載入到計算堆疊上。
Ldarg.S 將引數(由指定的短格式索引引用)載入到計算堆疊上。
Ldarga 將引數地址載入到計算堆疊上。
Ldarga.S 以短格式將引數地址載入到計算堆疊上。
Ldc.I4 將所提供的 int32 型別的值作為 int32 推送到計算堆疊上。
Ldc.I4.0 將整數值 0 作為 int32 推送到計算堆疊上。
Ldc.I4.1 將整數值 1 作為 int32 推送到計算堆疊上。
Ldc.I4.2 將整數值 2 作為 int32 推送到計算堆疊上。
Ldc.I4.3 將整數值 3 作為 int32 推送到計算堆疊上。
Ldc.I4.4 將整數值 4 作為 int32 推送到計算堆疊上。
Ldc.I4.5 將整數值 5 作為 int32 推送到計算堆疊上。
Ldc.I4.6 將整數值 6 作為 int32 推送到計算堆疊上。
Ldc.I4.7 將整數值 7 作為 int32 推送到計算堆疊上。
Ldc.I4.8 將整數值 8 作為 int32 推送到計算堆疊上。
Ldc.I4.M1 將整數值 -1 作為 int32 推送到計算堆疊上。
Ldc.I4.S 將提供的 int8 值作為 int32 推送到計算堆疊上(短格式)。
Ldc.I8 將所提供的 int64 型別的值作為 int64 推送到計算堆疊上。
Ldc.R4 將所提供的 float32 型別的值作為 F (float) 型別推送到計算堆疊上。
Ldc.R8 將所提供的 float64 型別的值作為 F (float) 型別推送到計算堆疊上。
Ldelem 按照指令中指定的型別,將指定陣列索引中的元素載入到計算堆疊的頂部。
Ldelem.I 將位於指定陣列索引處的 native int 型別的元素作為 native int 載入到計算堆疊的頂部。
Ldelem.I1 將位於指定陣列索引處的 int8 型別的元素作為 int32 載入到計算堆疊的頂部。
Ldelem.I2 將位於指定陣列索引處的 int16 型別的元素作為 int32 載入到計算堆疊的頂部。
Ldelem.I4 將位於指定陣列索引處的 int32 型別的元素作為 int32 載入到計算堆疊的頂部。
Ldelem.I8 將位於指定陣列索引處的 int64 型別的元素作為 int64 載入到計算堆疊的頂部。
Ldelem.R4 將位於指定陣列索引處的 float32 型別的元素作為 F 型別(浮點型)載入到計算堆疊的頂部。
Ldelem.R8 將位於指定陣列索引處的 float64 型別的元素作為 F 型別(浮點型)載入到計算堆疊的頂部。
Ldelem.Ref 將位於指定陣列索引處的包含物件引用的元素作為 O 型別(物件引用)載入到計算堆疊的頂部。
Ldelem.U1 將位於指定陣列索引處的 unsigned int8 型別的元素作為 int32 載入到計算堆疊的頂部。
Ldelem.U2 將位於指定陣列索引處的 unsigned int16 型別的元素作為 int32 載入到計算堆疊的頂部。
Ldelem.U4 將位於指定陣列索引處的 unsigned int32 型別的元素作為 int32 載入到計算堆疊的頂部。
Ldelema 將位於指定陣列索引的陣列元素的地址作為 & 型別(託管指標)載入到計算堆疊的頂部。
Ldfld 查詢物件中其引用當前位於計算堆疊的欄位的值。
Ldflda 查詢物件中其引用當前位於計算堆疊的欄位的地址。
Ldftn 將指向實現特定方法的本機程式碼的非託管指標(native int 型別)推送到計算堆疊上。
Ldind.I 將 native int 型別的值作為 native int 間接載入到計算堆疊上。
Ldind.I1 將 int8 型別的值作為 int32 間接載入到計算堆疊上。
Ldind.I2 將 int16 型別的值作為 int32 間接載入到計算堆疊上。
Ldind.I4 將 int32 型