什么是 JIT?
名如其特點,JIT —— just in time,即時編譯。
把它詳細化點講,就是
一個程序在它運行的時候創建並且運行了全新的代碼,而並非那些最初作為這個程序的一部分保存在硬盤上的固有的代碼。就叫 JIT。
這里有幾點要看的:
- 程序需要運行
- 生成的代碼是新的代碼,並非作為原始程序的一部分被存在磁盤上的那些代碼
- 不光生成代碼,還要運行。
需要提醒的是第三點,也就是 JIT不光是生成新的代碼,它還會運行新生成的代碼,而這些代碼在存儲於磁盤上時不屬於該程序的一部分,它就是一個JIT。
JIT的兩個階段
我把JIT分為了兩個階段
階段1:在程序運行時創建機器代碼。
階段2:在程序運行時也執行該機器代碼。
第1階段是JITing 99%的挑戰所在,但它也是這個過程中不那么神秘的部分,因為這正是編譯器所做的。眾所周知的編譯器,如gcc和clang,將C/C++源代碼轉換為機器代碼。機器代碼被發送到輸出流中,但它很可能只保存在內存中(實際上,gcc和clang / llvm都有構建塊用於將代碼保存在內存中以便執行JIT)。第2階段,看下去 ::twemoji👅:
模擬一下JIT運行的過程
現代操作系統對於允許程序在運行時執行的操作可以說是非常挑剔。過去“海闊憑魚躍,天高任鳥飛”的日子隨着保護模式的出現而不復存在,保護模式允許操作系統以各種權限對虛擬內存塊的使用做出限制。因此,在“普通”代碼中,你可以在堆上動態創建新數據,但是你不能在沒有操作系統明確允許的情況下從堆中運行其內容。
在這一點上,我希望機器代碼只是數據 - 一個字節流,比如:
unsigned char[] code = {0x48, 0x89, 0xf8};
不同的人會有不同的視角,對某些人而言,0x48, 0x89, 0xf8只是一些可以代表任何事物的數據。 對於其他人來說,它是真實有效的機器代碼的二進制編碼,其對應的x86-64匯編代碼如下:
mov %rdi, %rax
其實可以看出機器碼就是比特流,所以將它加載進內存並不困難。而問題是應該如何執行。
好啦。下面我們就模擬一下執行新生成的機器碼的過程。假設JIT已經為我們編譯出了新的機器碼,是一個求和函數的機器碼:
//求和函數
long add4(long num) {
return num + 4;
}
//對應的機器碼
0x48, 0x83, 0xc0, 0x01, 0xc3
首先,動態的在內存上創建函數之前,我們需要在內存上分配空間。具體到模擬動態創建函數,其實就是將對應的機器碼映射到內存空間中。這里我們使用c語言做實驗,利用 mmap函數 來實現這一點。 ::twemoji🆗:
所以,我們就需要這些:
//頭文件
#include <unistd.h>
#include <sys/mman.h>
//定義函數
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offsize)
/*函數說明:
mmap()用來將某個文件內容映射到內存中,對該內存區域的存取即是直接對該文件內容的讀寫。*/
因為我們想要把已經是 比特流的“求和函數”在內存中創建出來,同時還要運行它。所以mmap有幾個參數需要注意一下。
而代表映射區域的保護方式,有下列組合:
PROT_EXEC //映射區域可被執行;
PROT_READ //映射區域可被讀取;
PROT_WRITE //映射區域可被寫入;
所以,我們的程序可以像是這個樣子:
#include<stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
//分配內存
void* create_space(size_t size) {
void* ptr = mmap(0, size,
PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_ANON,
-1, 0);
return ptr;
}
通過這一段代碼我們可以獲得一塊分配給我們存放代碼的空間。下一步就是實現一個方法將機器碼拷貝到分配給我們的那塊空間上去。使用 函數 memcpy 即可。
//在內存中創建函數
void copy_code_2_space(unsigned char* m) {
unsigned char macCode[] = {
0x48, 0x83, 0xc0, 0x01,
c3
};
memcpy(m, macCode, sizeof(macCode));
}
我們再整理一下,最后程序就變成這個樣子
#include<stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
//分配內存
void* create_space(size_t size) {
void* ptr = mmap(0, size,
PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_ANON,
-1, 0);
return ptr;
}
//在內存中創建函數
void copy_code_2_space(unsigned char* addr) {
unsigned char macCode[] = {
0x48, 0x83, 0xc0, 0x01,
0xc3
};
memcpy(addr, macCode, sizeof(macCode));
}
//main 聲明一個函數指針TestFun用來指向我們的求和函數在內存中的地址
int main(int argc, char** argv) {
const size_t SIZE = 1024;
typedef long (*TestFun)(long);
void* addr = create_space(SIZE);
copy_code_2_space(addr);
TestFun test = addr;
int result = test(1);
printf("result = %d\n", result);
return 0;
}
編譯
我們通過
gcc a.c
./a.out 1
我們可以得到
result=2
所以這就是JIT在編譯的作用以及最后的結果了
