JIT是什么,它将怎样运行?

什么是 JIT?

名如其特点,JIT —— just in time,即时编译。
把它详细化点讲,就是
一个程序在它运行的时候创建并且运行了全新的代码,而并非那些最初作为这个程序的一部分保存在硬盘上的固有的代码。就叫 JIT。

这里有几点要看的:

  1. 程序需要运行
  2. 生成的代码是新的代码,并非作为原始程序的一部分被存在磁盘上的那些代码
  3. 不光生成代码,还要运行。
    需要提醒的是第三点,也就是 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在编译的作用以及最后的结果了

原文链接: https://www.cnblogs.com/wibus/p/13196001.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

    JIT是什么,它将怎样运行?

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/359215

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年3月2日 下午12:42
下一篇 2023年3月2日 下午12:43

相关推荐