JIT = just in time , 简略来说就是在运行时动静编译。一个程序在它运行的时候创立并且运行了全新的代码,而并非那些最后作为这个程序的一部分保留在硬盘上的固有的代码。其实蕴含两个概念,一个是动静生成代码,再一个是动静运行代码。
咱们都晓得,计算机运行的都是机器码,而汇编语言的全称应该是“机器码注记语言”,每一条汇编都对应一串机器码。而 JIT 的原理就是在内存中生成和运行一段代码。
生成的过程,是编译器干的,当然手动也是能够的,而在内存中运行一段代码,则是依赖操作系统提供的 mmap syscall 来实现的。
比方,上面是一个求和的机器码
// 求和函数
long add(long num) {return num + 2;}
// 对应机器码
0x55,0x48,0x89,0xe5,0x48,0x89,0x7d,0xf8,0x48,0x8b,0x45,0xf8,
, 0x83, 0xc0, 0x02,0x5d,0xc3
动静地在内存上创立函数之前,咱们须要在内存上调配空间。具体到模仿动态创建函数,其实就是将对应的机器码映射到内存空间中。这里咱们应用 c 语言,利用 mmap 函数来实现这一点。而 mmap 函数的底层就是对操作系统 mmap syscall 的一个封装。
头文件:#include <unistd.h> #include <sys/mman.h>
定义函数:void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offsize);
参数阐明:参数 阐明
start 指向欲对应的内存起始地址,通常设为 NULL,代表让零碎主动选定地址,对应胜利后该地址会返回。length 代表将文件中多大的局部对应到内存。其中,prot 代表映射区域的保护方式,有下列组合
PROT_EXEC 映射区域可被执行;PROT_READ 映射区域可被读取;PROT_WRITE 映射区域可被写入;参数 flags:影响映射区域的各种个性。在调用 mmap() 时必须要指定 MAP_SHARED 或 MAP_PRIVATE
咱们须要这块代码可读可执行,所以咱们能够这样来创立一块空间
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
// 分配内存
void* createSpace(size_t size) {
void* ptr = mmap(0, size,
PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_ANON,
-1, 0);
return ptr;
}
咱们能够试试把“可执行”PROT_EXEC 权限去掉,看看后果如何。
这样咱们就取得了一块调配给咱们寄存代码的空间。下一步就是实现一个办法将机器码拷贝到调配给咱们的那块空间下来。应用 memcpy 即可。
// 内存中创立函数
void copyCodeToMem(unsigned char* addr) {unsigned char macCode[] = {
0x55,
0x48,0x89,0xe5,
0x48,0x89,0x7d,0xf8,
0x48,0x8b,0x45,0xf8,
0x48, 0x83, 0xc0, 0x02,
0x5d,
0xc3
};
memcpy(addr, macCode, sizeof(macCode));
}
残缺的代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
// 分配内存
void* createSpace(size_t size) {
void* ptr = mmap(0, size,
PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_ANON,
-1, 0);
return ptr;
}
long add(long num) {return num + 2;}
// 内存中创立函数
void copyCodeToMem(unsigned char* addr) {unsigned char macCode[] = {
0x55,
0x48,0x89,0xe5,
0x48,0x89,0x7d,0xf8,
0x48,0x8b,0x45,0xf8,
0x48, 0x83, 0xc0, 0x02,
0x5d,
0xc3
};
memcpy(addr, macCode, sizeof(macCode));
}
int main(int argc, char** argv) {
const size_t SIZE = 1024;
typedef long (*demo)(long);
void* addr = createSpace(SIZE);
copyCodeToMem(addr);
demo d1 = addr;
long result = d1(1);
printf("result = %ld\n", result);
return 0;
}
编译运行后果如下:
[root@VM-0-7-centos develop]# gcc demo.c
[root@VM-0-7-centos develop]# ./a.out
result = 3
copyCodeToMem 中的这段机器码是怎么来的呢,其实就是先编译 add 函数,而后 dump 进去的机器码。
[root@VM-0-7-centos develop]#startaddress=$(nm -n a.out |grep add| awk '{print"0x"$1;exit}')
[root@VM-0-7-centos develop]#endaddress=$(nm -n a.out |grep -A1 add| awk '{getline;print"0x"$1;exit}')
[root@VM-0-7-centos develop]#objdump -S a.out --start-address=$startaddress --stop-address=$endaddress
a.out: file format elf64-x86-64
Disassembly of section .text:
0000000000400613 <add>:
400613: 55 push %rbp
400614: 48 89 e5 mov %rsp,%rbp
400617: 48 89 7d f8 mov %rdi,-0x8(%rbp)
40061b: 48 8b 45 f8 mov -0x8(%rbp),%rax
40061f: 48 83 c0 02 add $0x2,%rax
400623: 5d pop %rbp
400624: c3 retq
不必 objdump 来查看机器码也是能够的,咱们能够间接用 gcc - S 来生成汇编代码,通过查表得形式失去机器码。当然,不同的架构体系有不同的表。
当然,实在的 JIT 不会只有这么简略,然而基本原理大同小异,其重点也在代码的动静生成和优化上。事实上,动静获取一段程序的机器码也不是通过 objdump 来做的,古代的编译器,从前端到后端,每一个步骤都是解耦的,比方 llvm 就提供了这样的能力
mmap 能被用来动静执行代码或映射一段内存,更最常见的用处还是文件映射实现共享内存。和 mmap 相似的 memfd_create() 共享内存实现,则常被脚本小子用来暗藏过程或在内存中执行木马。