0x01 基本介紹
AsmJit是一個完整的JIT(just In Time, 運行時刻)的針對C++語言的匯編器,可以生成兼容x86和x64架構的原生代碼,不僅支持整個x86/x64的指令集(包括傳統的MMX和最新的AVX2指令集),而且提供了一套可以在編譯時刻進行語義檢查的API。AsmJit的使用也沒有任何的限制,適用於多媒體,虛擬機的后端,遠程代碼生成等等。
0x02 特性
- 完全支持x86/x64指令集(包括MMX,SSEx,AVX1/2,BMI,XOP,FMA3和FMA4);
- 底層次和高層次的代碼生成概念;
- 內置檢測處理器特性功能;
- 實現虛擬內存的管理,類似於malloc和free;
- 強大的日志記錄和錯誤處理能力;
- 體積小,可直接嵌入項目,編譯后的體積在150至200kb之間;
- 獨立性強,不需要依賴其他任何的庫(包括STL和RTTI )。
0x03 環境
1. 操作系統
- BSD系列
- Linux
- Mac
- Windows
2. C++編譯器
- Borland C++
- Clang
- GCC
- MinGW
- MSVC
- 其他的在”build.h”中文件中定義過的編譯器
3. 后端
- X86
- X64
0x04 代碼生成
AsmJit有着兩種完全不同的代碼生成概念,其不同點就在於生成代碼的方式。一種是稱為“Assembler”的低層次的代碼生成方法,通過直接操作物理寄存器的方式生成代碼,這種情況下AsmJit所做的工作只是簡單的對指令進行編碼,驗證和重定位。而另外的一種是稱為”Compiler”的高層次代碼生成方法,Compiler對使用虛擬寄存器的數量沒有限制,這就類似於高級程序設計語言中的變量,可以極大的簡化代碼的生成過程。Compiler在代碼生成成功以后再給這些虛擬寄存器(變量)分配相應的物理寄存器,這就需要一些額外的消耗,因為Compiler必須為代碼中的每一個結點(包括指令,函數聲明,函數調用)生成額外的信息,用來對變量的生命周期進行分析或者將使用變量的代碼轉換成使用物理寄存器的匯編語句。
此外,Compiler也需要了解函數原型和函數之間的調用約定。因此Compiler產生的代碼具有類似於高級程序設計語言一樣的函數原型,通過函數原型,Compiler可以通過在函數頭部和尾部插入額外的代碼來達到被可以其他函數調用的目的。但是我們不能說明上面兩種代碼的生成方式孰優孰劣,因為利用Assmebler的方式可以充分控制代碼的生成,而利用Compiler可以使得代碼的生成更方便,可移植性更強,然而,當涉及到物理寄存器分配時,Compiler有時效果並不太好,所以在已經進行分析的項目中,純粹的Assembler方式的生成是首選。
0x05 配置和編譯
AsmJit在設計之初的目的就是為了嵌入到任何項目之中。但是我們可以使用一些宏定義來添加或者刪除AsmJit庫的某些特性。生成AsmJit項目最直接的方法是使用cmake工具www.cmake.org ,但是如果只是在項目中嵌入AsmJit的源代碼,可以通過編輯” asmjit /config.h “文件來打開或者關閉某些特定的特性,最簡便的使用方法就是直接復制asmjit的源代碼到項目中,然后定義“ASMJIT_STATIC”宏。
1. 生成類型
- ASMJIT_EMBED —— 如果在cmake中指定這個參數,AsmJit則不會產生庫,而是將代碼直接嵌入到工程當中;
- ASMJIT_STATIC ——如果在cmake中指定這個參數,AsmJit則會生成靜態庫,默認將不會導出符號;
- 如果都不指定,AsmJit則會默認生成動態庫文件。
2. 生成模式
- ASMJIT_DEBUG —— 生成調試版本;
- ASMJIT_RELEASE —— 生成發行版本;
- ASMJIT_TRACE —— 生成的版本可以使用trace來調試bug,並且會使用“stdcout”將AsmJit運行的日志全部輸出;
- 如果這些都沒有定義的話,AsmJit就會檢測當前IDE編譯時用到的宏定義,例如:Debug/Release 等等。
3. 體系結構
- ASMJIT_BUILD_X86 —— 生成X86體系的后端;
- ASMJIT_BUILD_X64 —— 生成x64體系的后端;
- ASMJIT_BUILD_HOST —— 通過在編譯時檢測當前環境處理器的架構,生成和當前處理器架構一致的后端;
- 如果都不指定,則默認使用ASMJIT_BUILD_HOST。
4. 特性
- ASMJIT_DISABLE_COMPILER —— 禁用Compiler功能;
- ASMJIT_DISABLE_LOGGER —— 禁止產生日志;
- ASMJIT_DISABLE_NAMES —— 禁止使用字符串,如果使用則所有的指令和錯誤名稱將變成無效。
0x06 使用
1. 命名空間
AsmIit庫使用的是全局命名空間 “asmjit”`,但是其中只包含一些基本的內容,而針對特定處理器的代碼是用處理器、處理器的寄存器或者操作數作為前綴當成命名空間。例如針對x86和x64體系結構的類都會帶有有“X86“的前綴。通過` kx86 `枚舉的寄存器和操作數在“X86”的命名空間下都是可訪問的。雖然這種設計和AsmJit最初的版本不同,但是現在無疑是可移植性最好的。
2. 運行時刻和代碼生成器
要產生機器碼就要用到AsmJit的兩個類——“Runtime”和“CodeGen”。“RunTime”會指定代碼生成區域和存儲區域;”CodeGen”會指定代碼的生成方式和產生整個程序的控制流。接下來的所有的例子都將使用”Compiler”來生成代碼,並使用”JitRunTime”類來運行和存儲。
3. 指令操作數
操作數是處理器指令的一部分,指定了指令將要操作的數據,AsmJit中有5種操作數
- Reg 物理寄存器,只被Assembler使用
- Var 虛擬寄存器(變量),只被 Compiler使用
- Mem 用於引用內存地址
- Label 用於引用代碼地址
- Imm 直接用於編碼的立即數本身
所有操作的基類都是“Operand”, 它包含使用所有類型的操作數的接口,並且大多數是通過值傳遞,而不是通過指針傳遞。”Reg”,”Var”,”Mem”,”Label”和”Imm”類都是繼承自”Operand”並且提供不同的功能。依賴於處理器體系結構的操作數都會帶有處理器結構作為前綴,例如“X86Reg”,”X86Mem”。大多數的處理器都會提供幾種寄存器,例如X86/X64體系結構下的”X86GpReg”,”X86MmReg”,”X86FpReg”,”X86XmmReg”和”X86YmmReg”寄存器加上一些額外的段寄存器和”rip”寄存器。在使用代碼生成器時,必須使用AsmJit的接口來顯式地創建一些操作數。例如,labels是用代碼生成器類的newLabel()方法創建,而變量需要用針對不同體系結構的特定方法來創建,例如“newGpVar()”, “newMmVar()”和“newXmmVar()”。
4. 函數原型
AsmJit需要知道產生或調用的函數原型。AsmJit包含類型和寄存器之間的映射關系,並且用來表示函數原型。函數生成器是一個模板類,通過使用C/C++原生類型來生成可以描述函數參數和返回值的函數原型。它把C / C + +原生類型轉化為AsmJit特定的標識符並且使這些標識符訪問編譯器。
5. 實際使用
#include <asmjit/asmjit.h> using namespace asmjit; int main(int argc, char* argv[]) { // Create JitRuntime and X86 Compiler. JitRuntime runtime; X86Compiler c(&runtime); // Build function having two arguments and a return value of type 'int'. // First type in function builder describes the return value. kFuncConvHost // tells compiler to use a host calling convention. c.addFunc(kFuncConvHost, FuncBuilder2<int, int, int>()); // Create 32-bit variables (virtual registers) and assign some names to // them. Using names is purely optional and only greatly helps while // debugging. X86GpVar a(c, kVarTypeInt32, "a"); X86GpVar b(c, kVarTypeInt32, "b"); // Tell asmjit to use these variables as function arguments. c.setArg(0, a); c.setArg(1, b); // a = a + b; c.add(a, b); // Tell asmjit to return 'a'. c.ret(a); // Finalize the current function. c.endFunc(); // Now the Compiler contains the whole function, but the code is not yet // generated. To tell compiler to generate the function make() has to be // called. // Make uses the JitRuntime passed to Compiler constructor to allocate a // buffer for the function and make it executable. void* funcPtr = c.make(); // In order to run 'funcPtr' it has to be casted to the desired type. // Typedef is a recommended and safe way to create a function-type. typedef int (*FuncType)(int, int); // Using asmjit_cast is purely optional, it's basically a C-style cast // that tries to make it visible that a function-type is returned. FuncType func = asmjit_cast<FuncType>(funcPtr); // Finally, run it and do something with the result... int x = func(1, 2); printf("x=%d\n", x); // Outputs "x=3". // The function will remain in memory after Compiler is destroyed, but // will be destroyed together with Runtime. This is just simple example // where we can just destroy both at the end of the scope and that's it. // However, it's a good practice to clean-up resources after they are // not needed and using runtime.release() is the preferred way to free // a function added to JitRuntime. runtime.release((void*)func); // Runtime and Compiler will be destroyed at the end of the scope. return 0; }
上面代碼中的注釋已經非常清楚了,但還是有些細節需要說明。上面使用的產生和調用函數的調用約定” kFuncConvHost “。32位的架構包含一個廣泛的函數調用約定,所以了解C++編譯器所采用的調用約定是非常重要的,大多數編譯器默認采用cdecl的調用約定。但是在64位的架構上只有兩種調用約定,一種是Windows的Win64調用約定,另一種是類Unix系統采用的AMD64 調用約定。因此”KFuncConvHost”根據處理器的架構和操作系統可以被定義為Cdecl,Win64或者是AMD64。
整數的默認大小也取決於特定的平台,虛擬類型”kVarTypeIntPtr”和”kVarTypeUIntPtr”用來增強程序的可移植性,並且在使用指針時應該盡量用虛擬類型來定義。當沒有指定類型時,AsmJit總是使用默認類型”kVarTypeIntPtr”。 在上面的代碼中整數默認為32位。
函數以”c.addFunc()”開始,以”c.endFunc()“作為結束。不允許將函數代碼寫在函數之外,但是嵌入的數據時可以寫在函數的外部。
6. 標識符的使用
對於跳轉指令,函數調用和對代碼段的引用來說,標識符是必不可少的。標識符必須通過代碼生成器類的”newLabel()”方法顯示創建。下面就是一個使用標識符和條件跳轉指令的例子,如果參數是0,則返回”a+b”,否則返回”a-b“。
#include <asmjit/asmjit.h> using namespace asmjit; int main(int argc, char* argv[]) { JitRuntime runtime; X86Compiler c(&runtime); // This function uses 3 arguments. c.addFunc(kFuncConvHost, FuncBuilder3<int, int, int, int>()); // New variable 'op' added. X86GpVar op(c, kVarTypeInt32, "op"); X86GpVar a(c, kVarTypeInt32, "a"); X86GpVar b(c, kVarTypeInt32, "b"); c.setArg(0, op); c.setArg(1, a); c.setArg(2, b); // Create labels. Label L_Subtract(c); Label L_Skip(c); // If (op != 0) // goto L_Subtract; c.test(op, op); c.jne(L_Subtract); // a = a + b; // goto L_Skip; c.add(a, b); c.jmp(L_Skip); // L_Subtract: // a = a - b; c.bind(L_Subtract); c.sub(a, b); // L_Skip: c.bind(L_Skip); c.ret(a); c.endFunc(); // The prototype of the generated function changed also here. typedef int (*FuncType)(int, int, int); FuncType func = asmjit_cast<FuncType>(c.make()); int x = func(0, 1, 2); int y = func(1, 1, 2); printf("x=%d\n", x); // Outputs "x=3". printf("y=%d\n", y); // Outputs "y=-1". runtime.release((void*)func); return 0; }
在上面的例子中,有條件和無條件跳轉一起使用。標識符是通過傳遞”Compiler”的一個實例給”Label”的構造函數或者是使用”Label l = c.newLable()“由”Compiler“顯式的創建。每一個標識符都有唯一的標識,但它不是一個字符串,沒有任何方法來查詢已經存在的標識符的實例。標識符像其他的操作數一樣被通過賦值來移動,因此該標簽的副本將仍然引用原先的地址,而另一個復制的標識符將不會改變原來的標識符。
每個標識符通過”c.bind()”和代碼中的某一位置綁定,但是只能綁定一次!如果嘗試同一個標識符多次綁定將會觸發一個失敗的斷言。
7. 內存地址
x86/x64架構有幾種內存尋址方式,可以通過基址寄存器,變址寄存器和偏移尋址。AsmJit支持所有形式的內存尋址。內存操作數可以用”asmjit::x86Mem”創建,也可以使用相關的非成員函數例如:”asmjit::x86::ptr”`或者” asmjit::x86::ptr_abs “創建。使用”ptr”創建具有可選的索引寄存器和移位寄存器的內存操作數的使用和基礎;` ptr_abs `創建一個內存操作數指內存中的絕對地址(32位)和任選地具有一個索引寄存器。
下面的例子使用各種不同的內存尋址模型來演示怎么構建和使用它們。創建了一個接受一個數組和兩個分別用來指定計算和和返回的元素的索引的函數。
#include <asmjit/asmjit.h> using namespace asmjit; int main(int argc, char* argv[]) { JitRuntime runtime; X86Compiler c(&runtime); // Function returning 'int' accepting pointer and two indexes. c.addFunc(kFuncConvHost, FuncBuilder3<int, const int*, intptr_t, intptr_t>()); X86GpVar p(c, kVarTypeIntPtr, "p"); X86GpVar aIndex(c, kVarTypeIntPtr, "aIndex"); X86GpVar bIndex(c, kVarTypeIntPtr, "bIndex"); c.setArg(0, p); c.setArg(1, aIndex); c.setArg(2, bIndex); X86GpVar a(c, kVarTypeInt32, "a"); X86GpVar b(c, kVarTypeInt32, "b"); // Read 'a' by using a memory operand having base register, index register // and scale. Translates to 'mov a, dword ptr [p + aIndex << 2]'. c.mov(a, ptr(p, aIndex, 2)); // Read 'b' by using a memory operand having base register only. Variables // 'p' and 'bIndex' are both modified. // Shift bIndex by 2 (exactly the same as multiplying by 4). // And add scaled 'bIndex' to 'p' resulting in 'p = p + bIndex * 4'. c.shl(bIndex, 2); c.add(p, bIndex); // Read 'b'. c.mov(b, ptr(p)); // a = a + b; c.add(a, b); c.ret(a); c.endFunc(); // The prototype of the generated function changed also here. typedef int (*FuncType)(const int*, intptr_t, intptr_t); FuncType func = asmjit_cast<FuncType>(c.make()); // Array passed to 'func' const int array[] = { 1, 2, 3, 5, 8, 13 }; int x = func(array, 1, 2); int y = func(array, 3, 5); printf("x=%d\n", x); // Outputs "x=5". printf("y=%d\n", y); // Outputs "y=18". runtime.release((void*)func); return 0; }
8. 棧的使用
當沒有足夠的寄存來保存變量時,AsmJit將會使用棧來自動保存溢出的變量。”Compiler”對棧的框架進行管理的同時也提供一套接口來分配用戶指定的大小和對齊粒度的內存塊。
下面的例子中申請了256 bytes大小的棧,用0到255填充,然后迭代一次,計算所有值的和。
#include <asmjit/asmjit.h> using namespace asmjit; int main(int argc, char* argv[]) { JitRuntime runtime; X86Compiler c(&runtime); // Function returning 'int' without any arguments. c.addFunc(kFuncConvHost, FuncBuilder0<int>()); // Allocate a function stack of size 256 aligned to 4 bytes. X86Mem stack = c.newStack(256, 4); X86GpVar p(c, kVarTypeIntPtr, "p"); X86GpVar i(c, kVarTypeIntPtr, "i"); // Load a stack address to 'p'. This step is purely optional and shows // that 'lea' is useful to load a memory operands address (even absolute) // to a general purpose register. c.lea(p, stack); // Clear 'i'. Notice that xor_() is used instead of xor(), because xor is // unfortunately a keyword in C++. c.xor_(i, i); // First loop, fill the stack allocated by a sequence of bytes from 0 to 255. Label L1(c); c.bind(L1); // Mov [p + i], i. // // Any operand can be cloned and modified. By cloning 'stack' and calling // 'setIndex' we created a new memory operand based on stack having an // index register set. c.mov(stack.clone().setIndex(i), i.r8()); // if (++i < 256) // goto L1; c.inc(i); c.cmp(i, 256); c.jb(L1); // Second loop, sum all bytes stored in 'stack'. X86GpVar a(c, kVarTypeInt32, "a"); X86GpVar t(c, kVarTypeInt32, "t"); c.xor_(i, i); c.xor_(a, a); Label L2(c); c.bind(L2); // Movzx t, byte ptr [stack + i] c.movzx(t, stack.clone().setIndex(i).setSize(1)); // a += t; c.add(a, t); // if (++i < 256) // goto L2; c.inc(i); c.cmp(i, 256); c.jb(L2); c.ret(a); c.endFunc(); typedef int (*FuncType)(void); FuncType func = asmjit_cast<FuncType>(c.make()); printf("a=%d\n", func()); // Outputs "a=32640". runtime.release((void*)func); return 0; }
0x07 高級特性
AsmJit提供了很多的功能,但是不可能把所有的功能都在一篇文章中介紹,接下來一些用法沒有給出完整的例子,但是給出的提示對於使用AsmJit是非常有用的。
1. 日志和錯誤處理
在機器層面,故障是很常見的。AsmJit雖然已經利用了函數重載盡量來避免發生語義上的錯誤指令,但是AsmJit缺不能防止在語義上是正確的,但是包含了bug的代碼。因此日志系統是AsmJit基礎結構的重要組成部分,輸出的日志對於我們進行一些錯誤的分析非常有效。
AsmJit包含一個可擴展的日志接口,“Logger”類,並且由”FileLogger”,“StringLogger”實現。
”FileLogger”類可以通過一個標准的C語言的文件指針”FILE*”將日志信息記錄到文件中。
“StringLogger”類將日志信息記錄到內部的緩沖區。
Logger可以分配給任何一個代碼生成器實例,可以將單個的Logger實例分配給無限制的多個代碼生成器使用,雖然使用多線程來運行多個代碼生成器並不實用。因為”FileLogger”類使用的是標准的C語言的FILE* 文件流,是線程安全的,但是”StringLogger”類不是。
下面的一個小的代碼片段描述了怎么將日志信息寫入到文件指針”FILE*”中:
// Create logger logging to `stdout`. Logger life-time should always be // greater than lifetime of the code generator. FileLogger logger(stdout); // Create a code generator and assign our logger into it. X86Compiler c(...); c.setLogger(&logger); // ... Generate the code ...
下面的代碼片段說明了怎么講日志寫入到字符串中:
StringLogger logger; // Create a code generator and assign our logger into it. X86Compiler c(...); c.setLogger(&logger); // ... Generate the code ... printf("Logger Content:\n%s", logger.getString()); // You can also use `logger.clearString()` if the logger // instance will be reused.
可以通過”logger.setOption()”方法來配置logger顯示更多的信息,下面是一些可用的選項:
- kLoggerOptionBinaryForm ——為每一條指令生成二進制序列
- kLoggerOptionHexImmediate ——格式化立即數為16進制的形式
- kLoggerOptionHexDisplacement ——格式化內存偏移為16進制的形式
2. 代碼注入
代碼注入從一開始就是Compiler中的一個非常重要的概念。Compiler維護一個雙向鏈表中記錄着所有產生的指令,在這些指令”make()”方法調用前會影響雙向鏈表。所有的調用Compiler的指令將會在鏈表中插入節點來增加指令,函數,或者一些額外的信息。
X86Compiler c(...); X86GpVar a(c, kVarTypeInt32, "a"); X86GpVar b(c, kVarTypeInt32, "b"); Node* here = c.getCursor(); c.mov(b, 2); // Now, 'here' can be used to inject something before 'mov b, 2'. To inject // anything it's good to remember the current cursor so it can be set back // after the injecting is done. When setCursor() is called it returns the old // cursor. Node* oldCursor = c.setCursor(here); c.mov(a, 1); c.setCursor(oldCursor);
最后產生的結果如下:
c.mov(a, 1); c.mov(b, 2);
小結
項目上面用到了AsmJit,但是之前又沒接觸過,網上關於這方面的資料又比較少。加上學校又進入考試月,各種考試+課設無縫銜接,只能斷斷續續的寫點,內容比較松散,作為使用AsmJit的中文參考手冊還是好的。