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.outresult = 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=$endaddressa.out: file format elf64-x86-64Disassembly 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()共享内存实现,则常被脚本小子用来暗藏过程或在内存中执行木马。