前言:
其實小匹夫在U3D的開發中一直對U3D的跨平台能力很好奇。到底是什么原理使得U3D可以跨平台呢?后來發現了Mono的作用,並進一步了解到了CIL的存在。所以,作為一個對Unity3D跨平台能力感興趣的U3D程序猿,小匹夫如何能不關注CIL這個話題呢?那么下面各位看官就拾起語文老師教導我們的作文口訣(Why,What,How),和小匹夫一起走進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無關。這樣就無需根據平台的不同而部署不同的內容了。所以到這里,各位也應該恍然大了。代碼的編譯只需要分為兩部分就好了嘛:
- 從代碼本身到CIL的編譯(其實之后CIL還會被編譯成一種位元碼,生成一個CLI assembly)
- 運行時從CIL(其實是CLI assembly,不過為了直觀理解,不必糾結這種細節)到本地指令的即時編譯(這就引出了為何U3D官方沒有提供熱更新的原因:在iOS平台中Mono無法使用JIT引擎,而是以Full AOT模式運行的,所以此處說的額即時編譯不包括IOS)
What?
上文也說了CIL是指令集,但是不是還是太模糊了呢?所以語文老師教導我們,描述一個東西時肯定要先從外貌寫起。遵循老師的教導,我們不妨先通過工具來看看CIL到底長什么樣。
工具就是ildasm了。下面小匹夫寫一個簡單的.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的第一次親密接觸,能給我們留下什么直觀的印象呢?
- 以“.”一個點號開頭的,例如上面這份代碼中的:.class、.method 。我們稱之為CIL指令(directive),用於描述.NET程序集總體結構的標記。為啥需要它呢?因為你總得告訴編譯器你處理的是啥吧。
- 貌似CIL代碼中還看到了private、public這樣的身影。姑且稱之為CIL特性(attribute)。它的作用也很好理解,通過CIL指令並不能完全說明.NET成員和類,針對CIL指令進行補充說明成員或者類的特性的。市面上常見的還有:extends,implements等等。
- 每一行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中你可以創建對象,調用對象的方法,訪問對象的成員。而這里需要注意的就是對方法的調用。
回到上表中的右上角。對,就是對參數的操作部分。靜態方法和實例方法是不同的哦~
- 靜態方法:ldarg.0么有被占用,所以參數從ldarg.0開始。
- 實例方法: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的過程。接下來就讓我們運行下剛剛的成果好啦。
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的過程:
- 收集要被編譯的方法
- 使用JIT進行編譯
- 發射(Emitting)經JIT編譯過的代碼和其他信息
- 直接生成文件或者調用本地匯編器或連接器進行處理之后生成文件。(例如上圖中使用了小匹夫本地的gcc)
Full AOT
當然上文也說了,IOS平台是禁止使用JIT的,可看樣子Mono的AOT模式仍然會保留一部分代碼會在程序運行時動態編譯。所以為了破解這個問題,Mono提供了一個被稱為Full AOT的模式。即預先對程序集中的所有CIL代碼進行AOT編譯生成一個本地代碼映像,然后在運行時直接加載這個映像而不再使用JIT引擎。目前由於技術或實現上的原因在使用Full AOT時有一些限制,不過這里不再多說了。以后也還會更細的分析下AOT。
總結:
好啦,寫到現在也已經到了凌晨3:04分了。感覺寫的內容也差不多了。那么對本文的主題U3D為何能跨平台以及CIL做個最終的總結陳詞:
- CIL是CLI標准定義的一種可讀性較低的語言。
- 以.NET或mono等實現CLI標准的運行環境為目標的語言要先編譯成CIL,之后CIL會被編譯,並且以位元碼的形式存在(源代碼--->中間語言的過程)。
- 這種位元碼運行在虛擬機中(.net mono的運行時)。
- 這種位元碼可以被進一步編譯成不同平台的原生代碼(中間語言--->原生代碼的過程)。
- 面向對象
- 基於堆棧
如果各位看官覺得文章寫得還好,那么就容小匹夫跪求各位給點個“推薦”,謝啦~
裝模作樣的聲明一下:本博文章若非特殊注明皆為原創,若需轉載請保留原文鏈接(http://www.cnblogs.com/murongxiaopifu/p/4211964.html)及作者信息慕容小匹夫
你也可以在游戲蠻牛讀到這篇文章:
匹夫細說Unity3D(三)——為何能跨平台?聊聊CIL(MSIL)
(出處: -u3d游戲開發者社區【游戲蠻牛】unity3d官網)
附錄:
名稱 | 說明 |
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 類型的值作為 int32 間接加載到計算堆棧上。 |
Ldind.I8 | 將 int64 類型的值作為 int64 間接加載到計算堆棧上。 |
Ldind.R4 | 將 float32 類型的值作為 F (float) 類型間接加載到計算堆棧上。 |
Ldind.R8 | 將 float64 類型的值作為 F (float) 類型間接加載到計算堆棧上。 |
Ldind.Ref | 將對象引用作為 O(對象引用)類型間接加載到計算堆棧上。 |
Ldind.U1 | 將 unsigned int8 類型的值作為 int32 間接加載到計算堆棧上。 |
Ldind.U2 | 將 unsigned int16 類型的值作為 int32 間接加載到計算堆棧上。 |
Ldind.U4 | 將 unsigned int32 類型的值作為 int32 間接加載到計算堆棧上。 |
Ldlen | 將從零開始的、一維數組的元素的數目推送到計算堆棧上。 |
Ldloc | 將指定索引處的局部變量加載到計算堆棧上。 |
Ldloc.0 | 將索引 0 處的局部變量加載到計算堆棧上。 |
Ldloc.1 | 將索引 1 處的局部變量加載到計算堆棧上。 |
Ldloc.2 | 將索引 2 處的局部變量加載到計算堆棧上。 |
Ldloc.3 | 將索引 3 處的局部變量加載到計算堆棧上。 |
Ldloc.S | 將特定索引處的局部變量加載到計算堆棧上(短格式)。 |
Ldloca | 將位於特定索引處的局部變量的地址加載到計算堆棧上。 |
Ldloca.S | 將位於特定索引處的局部變量的地址加載到計算堆棧上(短格式)。 |
Ldnull | 將空引用(O 類型)推送到計算堆棧上。 |
Ldobj | 將地址指向的值類型對象復制到計算堆棧的頂部。 |
Ldsfld | 將靜態字段的值推送到計算堆棧上。 |
Ldsflda | 將靜態字段的地址推送到計算堆棧上。 |
Ldstr | 推送對元數據中存儲的字符串的新對象引用。 |
Ldtoken | 將元數據標記轉換為其運行時表示形式,並將其推送到計算堆棧上。 |
Ldvirtftn | 將指向實現與指定對象關聯的特定虛方法的本機代碼的非托管指針(native int 類型)推送到計算堆棧上。 |
Leave | 退出受保護的代碼區域,無條件將控制轉移到特定目標指令。 |
Leave.S | 退出受保護的代碼區域,無條件將控制轉移到目標指令(縮寫形式)。 |
Localloc | 從本地動態內存池分配特定數目的字節並將第一個分配的字節的地址(瞬態指針,* 類型)推送到計算堆棧上。 |
Mkrefany | 將對特定類型實例的類型化引用推送到計算堆棧上。 |
Mul | 將兩個值相乘並將結果推送到計算堆棧上。 |
Mul.Ovf | 將兩個整數值相乘,執行溢出檢查,並將結果推送到計算堆棧上。 |
Mul.Ovf.Un | 將兩個無符號整數值相乘,執行溢出檢查,並將結果推送到計算堆棧上。 |
Neg | 對一個值執行求反並將結果推送到計算堆棧上。 |
Newarr | 將對新的從零開始的一維數組(其元素屬於特定類型)的對象引用推送到計算堆棧上。 |
Newobj | 創建一個值類型的新對象或新實例,並將對象引用(O 類型)推送到計算堆棧上。 |
Nop | 如果修補操作碼,則填充空間。盡管可能消耗處理周期,但未執行任何有意義的操作。 |
Not | 計算堆棧頂部整數值的按位求補並將結果作為相同的類型推送到計算堆棧上。 |
Or | 計算位於堆棧頂部的兩個整數值的按位求補並將結果推送到計算堆棧上。 |
Pop | 移除當前位於計算堆棧頂部的值。 |
Prefix1 | 基礎結構。此指令為保留指令。 |
Prefix2 | 基礎結構。此指令為保留指令。 |
Prefix3 | 基礎結構。此指令為保留指令。 |
Prefix4 | 基礎結構。此指令為保留指令。 |
Prefix5 | 基礎結構。此指令為保留指令。 |
Prefix6 | 基礎結構。此指令為保留指令。 |
Prefix7 | 基礎結構。此指令為保留指令。 |
Prefixref | 基礎結構。此指令為保留指令。 |
Readonly | 指定后面的數組地址操作在運行時不執行類型檢查,並且返回可變性受限的托管指針。 |
Refanytype | 檢索嵌入在類型化引用內的類型標記。 |
Refanyval | 檢索嵌入在類型化引用內的地址(& 類型)。 |
Rem | 將兩個值相除並將余數推送到計算堆棧上。 |
Rem.Un | 將兩個無符號值相除並將余數推送到計算堆棧上。 |
Ret | 從當前方法返回,並將返回值(如果存在)從調用方的計算堆棧推送到被調用方的計算堆棧上。 |
Rethrow | 再次引發當前異常。 |
Shl | 將整數值左移(用零填充)指定的位數,並將結果推送到計算堆棧上。 |
Shr | 將整數值右移(保留符號)指定的位數,並將結果推送到計算堆棧上。 |
Shr.Un | 將無符號整數值右移(用零填充)指定的位數,並將結果推送到計算堆棧上。 |
Sizeof | 將提供的值類型的大小(以字節為單位)推送到計算堆棧上。 |
Starg | 將位於計算堆棧頂部的值存儲到位於指定索引的參數槽中。 |
Starg.S | 將位於計算堆棧頂部的值存儲在參數槽中的指定索引處(短格式)。 |
Stelem | 用計算堆棧中的值替換給定索引處的數組元素,其類型在指令中指定。 |
Stelem.I | 用計算堆棧上的 native int 值替換給定索引處的數組元素。 |
Stelem.I1 | 用計算堆棧上的 int8 值替換給定索引處的數組元素。 |
Stelem.I2 | 用計算堆棧上的 int16 值替換給定索引處的數組元素。 |
Stelem.I4 | 用計算堆棧上的 int32 值替換給定索引處的數組元素。 |
Stelem.I8 | 用計算堆棧上的 int64 值替換給定索引處的數組元素。 |
Stelem.R4 | 用計算堆棧上的 float32 值替換給定索引處的數組元素。 |
Stelem.R8 | 用計算堆棧上的 float64 值替換給定索引處的數組元素。 |
Stelem.Ref | 用計算堆棧上的對象 ref 值(O 類型)替換給定索引處的數組元素。 |
Stfld | 用新值替換在對象引用或指針的字段中存儲的值。 |
Stind.I | 在所提供的地址存儲 native int 類型的值。 |
Stind.I1 | 在所提供的地址存儲 int8 類型的值。 |
Stind.I2 | 在所提供的地址存儲 int16 類型的值。 |
Stind.I4 | 在所提供的地址存儲 int32 類型的值。 |
Stind.I8 | 在所提供的地址存儲 int64 類型的值。 |
Stind.R4 | 在所提供的地址存儲 float32 類型的值。 |
Stind.R8 | 在所提供的地址存儲 float64 類型的值。 |
Stind.Ref | 存儲所提供地址處的對象引用值。 |
Stloc | 從計算堆棧的頂部彈出當前值並將其存儲到指定索引處的局部變量列表中。 |
Stloc.0 | 從計算堆棧的頂部彈出當前值並將其存儲到索引 0 處的局部變量列表中。 |
Stloc.1 | 從計算堆棧的頂部彈出當前值並將其存儲到索引 1 處的局部變量列表中。 |
Stloc.2 | 從計算堆棧的頂部彈出當前值並將其存儲到索引 2 處的局部變量列表中。 |
Stloc.3 | 從計算堆棧的頂部彈出當前值並將其存儲到索引 3 處的局部變量列表中。 |
Stloc.S | 從計算堆棧的頂部彈出當前值並將其存儲在局部變量列表中的 index 處(短格式)。 |
Stobj | 將指定類型的值從計算堆棧復制到所提供的內存地址中。 |
Stsfld | 用來自計算堆棧的值替換靜態字段的值。 |
Sub | 從其他值中減去一個值並將結果推送到計算堆棧上。 |
Sub.Ovf | 從另一值中減去一個整數值,執行溢出檢查,並且將結果推送到計算堆棧上。 |
Sub.Ovf.Un | 從另一值中減去一個無符號整數值,執行溢出檢查,並且將結果推送到計算堆棧上。 |
Switch | 實現跳轉表。 |
Tailcall | 執行后綴的方法調用指令,以便在執行實際調用指令前移除當前方法的堆棧幀。 |
Throw | 引發當前位於計算堆棧上的異常對象。 |
Unaligned | 指示當前位於計算堆棧上的地址可能沒有與緊接的 ldind、stind、ldfld、stfld、ldobj、stobj、initblk 或 cpblk 指令的自然大小對齊。 |
Unbox | 將值類型的已裝箱的表示形式轉換為其未裝箱的形式。 |
Unbox.Any | 將指令中指定類型的已裝箱的表示形式轉換成未裝箱形式。 |
Volatile | 指定當前位於計算堆棧頂部的地址可以是易失的,並且讀取該位置的結果不能被緩存,或者對該地址的多個存儲區不能被取消。 |
Xor | 計算位於計算堆棧頂部的兩個值的按位異或,並且將結果推送到計算堆棧上。 |