# Compile or Interpret ?

经常听到的一句话是，编译和解释，然后又有人把它们用具体的语言来理解，比如 C 语言，是编译型语言，而 JavaScript 是解释型语言。它们的区别在于，最后变成机器指令的时机不同，我们最早说的 编译，意味在执行之前，它已经变成了机器指令，而 解释，直到执行它之前，它还是一些代码（字符串）。

但是，最后它们都会变成机器代码，所以我们不需要做这些区分，解释 和 编译 只是变成机器指令的时机不同。

不过，之所以会不一样，确实是因为语言的特点所决定的！

## 变量的动态性

在 C 中，我们所有的变量，都有类型声明，这意味这变量的空间，在我们声明的那一刻就已经确定了，它的结构是不会改变的。

```c

struct demo_struct {
    int arg1;
    int arg2;
    ...
}

 struct demo_struct demo;

```

所以我们可以使用 sizeof 这样的 宏～ 下面这张图更生动解释了这一点，来源会在最后给出。

![ ](https://1336358925-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LFRrBV_N_gPjP8kN5U4%2F-Lg_o7brTasrm_tZQ-Ed%2F-Lg_tPYtqxTIxtf41oKQ%2Fimage.png?alt=media\&token=6fec00f8-9953-4dcf-af24-acebd1d517a3)

而现在目前的大多数语言，都支持不声明具体类型，直接使用。

```javascript
var some = "this is a string"; // declaring a string
some = 36; // change the type on the fly
some = 3.14;
```

考虑上面的情况，一个变量可以同时是多种类型，那么它的空间就是不确定的，这个情况还比较容易处理，直接一次给 所有类型的空间都分配出来就好了。

![](https://1336358925-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LFRrBV_N_gPjP8kN5U4%2F-Lg_o7brTasrm_tZQ-Ed%2F-Lg_uRWKDfURsIqmv-f6%2Fimage.png?alt=media\&token=9d81ced8-502d-455a-8a28-8b318ae7002b)

这种上面函数的调用，则比较复杂了，在做语句检查的时候， obj obj2 obj3 可以说是一个一个类似的结构，但是实际的大小却绝对的不同。

![](https://1336358925-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LFRrBV_N_gPjP8kN5U4%2F-Lg_o7brTasrm_tZQ-Ed%2F-Lg_urLDYvkr3w_tWOK6%2Fimage.png?alt=media\&token=5da799d6-5425-4fad-a8fe-8bb26091aca0)

可能大家不知道，我们访问结构体成员的方式，其实就是 **地址的加减** 实现的，在 C语言下，所有的成员访问，都是确定的。而对于 JS 这一种，却难以处理。

所以，才有了 解释，这一操作，因为代码有着非常多的不确定因素，但是，**并不代表**直接静态的汇编不能够处理，回想我们在 C++ 所学的 template 这个关键字，其实仍然可以实现的，只是 需要在所有函数的调用语句之前，增加一个类型检查，如果出现了新的类型，那么就根据模版函数，**实现一个新的函数**。

{% hint style="info" %}
在C/C++ 编译是指翻译为汇编代码，这里把这俩个过程合并，其实后面的 汇编，在英文有组装意思，本质是因为 C/C++ 是一个个文件分开编译的，所有有了这一个说法。
{% endhint %}

![](https://1336358925-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LFRrBV_N_gPjP8kN5U4%2F-LgalZYXurwzYj--r8yB%2F-LgadxaB-QecdC1AltEo%2Fimage.png?alt=media\&token=59a41425-6451-40bf-b44a-4c85b23816d6)

实际上就是说，上面的代码会导致有三个构造函数，当然这是经过处理的，对于上层的编写代码的人员来说还是透明的，其实就是类似于 **函数名的重载**，只是这个重载是由编译器来实现罢了。

不仅如此，对于所有的赋值也要检查，因为一个变量可以同时是多种类型，Anyway，**事先编译也是可以实现**的。

## 事先编译，动态加载 - 传统的动态库

{% hint style="info" %}
这里的加载方式，指的就是利用 dlopen 加载， 至于我们链接过程指定的外部库，其实是一样的，本质就是 dlopen
{% endhint %}

考虑一下，如果我们把一块代码，事先**编译**成了机器指令，比如我们使用的一些函数的动态库，其实就是说，我们要动态加载进一个代码块，然后执行它。

{% hint style="info" %}
所以现在的脚本语言，其实都渐渐地开始支持静态类型的声明，就是为了加快脚本的执行速度
{% endhint %}

动态库，相信大家都不陌生，链接动态库的过程其实就是处理**代码对外部的函数，变量的引用**，把它们改成正确的值。

```c
#include <stdio.h>

int main() {
    printf("");
}

```

这是一个典型的例子，printf 就是一个外部函数，当我们执行这个程序的时候，printf 的实际地址其实并没有确认，程序有一个专门的部件（dynamic linker），负责处理这些外部函数的引用，本质就是把 printf 所在的动态库（其实就是二进制指令）加载进内存，然后改变调用的地址，让 CPU 顺利的转到 printf 所在的代码块执行。

其实这里就是一个二进制的处理过程，事先俩个模块都已经是二进制代码了，专门的部件需要**加载进内存，然后跳转执行**。

了解 dlopen 的人肯定不会陌生，加载一个外部的机器指令，需要处理的是函数，变量的引用，这些信息都要在头部用一些结构表示，**这些结构必须得存在，因为二进制代码非常不好阅读**。

{% hint style="info" %}
所有的动态加载，无论有什么快捷的方法，本质都必须通过这些表格指示来解决引用，看多了就会发现实现的方法大同小异。
{% endhint %}

比如上面这个代码，最后编译成二进制之后，必然头部有类似的结构

| 函数名    | 偏移   | 状态        |   |
| ------ | ---- | --------- | - |
| printf | 0x38 | UNDEFINED |   |

这样就表示了，程序在 0x38 偏移的地方的那条指令，有一个函数的引用没有解决，同样的，当你编译一个程序作为**动态库**的使用的时候，必然它的头部也有类似的结构。

| 函数名    | 偏移   | 状态      |
| ------ | ---- | ------- |
| printf | 0x64 | DYNAMIC |

这就意味着，在所说的偏移处就是一个函数开始的地方，它的名字为 printf。表指明了代码的地址。

现在，我们又要有一个概念，如果事先编译成了二进制指令，事后执行，那么必须要有相关的表格来记录这些未处理的引用，原因是因为 二进制指令 不能提供有效的信息。

动态库的链接其实就是繁琐的处理表头这些结构的过程，让指令可以正确的执行，无论是 dlopen 又或者是 链接 的时候指明了 动态库的库名，都有一个共同点，就是**操作系统**来掌管了代码加载的过程，我们只需要利用相关的接口就好了。

{% hint style="info" %}
把目标代码编译成库的形式加载，如果库里面有对外部函数的引用，可以使用 -fPIC 这个选项，那么就可以实现这个需求
{% endhint %}

## 执行时编译，手动跳转

解释执行的最大的一个好处就在于（本人看来），**更好的做代码优化，对于小模块代码非常友好**，原因之后我会解释。考虑 JS 在浏览器的使用，大部分都是一段不太长代码，而且很多时候存在异步的操作，还会更新相关的代码，如果每一次代码来了，都马上编译，然后调用上一小节的方法，dlopen，那么会是非常浪费时间的，大部分的时间都在 生成上面 提到的表头的数据，然后交给动态加载的模块来处理。

这里提到的**动态加载**，均是指利用操作系统的接口实现的接口，因为它是固定的操作，所以我们需要把代码的编译成**接口规定的结构**，如果仅仅只有10行的代码，却要做这以上的操作，数量一多的话，效率就不高了。

更重要的是，对于 X86 来说，段控制是以**页**为单位的，dlopen 会把库加载进新的一个页内，并把它设立为**只读，可执行**，那么每次动态加载一次代码（大小不足一页），都会耗费一个页的内存大小。

#### 什么是解释执行

大部分人认为的**解释执行**，就是**一条语句一条语句**的翻译，如果是这样，那是非常浪费时间的，就好像 DEBUG 一样，在我的理解，解释执行和编译执行是没有区别的，我不排除早期有这样处理的程序，当时如果你是编写者，你会这么处理么？

我认为的解释执行，非常简单，就是编写者自己处理 **函数和数据的引用**，凡是实现了这个功能，那么我们就认为它是解释执行的，即，没有使用操作系统提供的 dlopen 等接口，而是自己完成了 从代码 -> 机器指令这个过程，同时自己放入自己控制的代码段，解决外部函数和数据的引用。

大部分解释型语言，在翻译成机器指令，是一块一块（比如一个函数+函数所依赖的函数 ）的代码来编译成机器指令，同时，解决一些外部函数的引用，接着复制进代码段，然后跳转到目标代码执行，具体这个块的大小，都是有**考量**的，并不是一杠子打死，就是每次翻译一句，更别说现在很多脚本语言，其实就有整块代码全部翻译的，为了加快执行的速度。

所以，解释型语言，真正的做的事情，就是根据自己的需求，把若干长度的代码翻译成机器指令，加载执行。

## 代码段可写？

我每每思考这些解释型语言的实现，就觉得无非俩种： 1. 编译成动态库，然后动态加载 2. 用现有的代码模拟脚本语言，也就是一大堆的 switch case 来实现

会这么认为，因为我知道一点，内存不是可以随意执行的，所有的段（若干个连续页）都有权限，读写执行(r-w-x)，在最初接触程序的时候，我们就知道它大概有这么几个段，代码段，数据段，堆，桟，其中代码段就是只读的，然后在学习单片机的时候呢，又接触了51架构，里面的哈佛结构干脆直接把代码段独立到一个储存器，变成只读的，所以我的影响中，操作系统应该不可能提供让一个段即是可写又是可执行的，这个接口。虽然对于内核来说，只是更改几个标志位的事情，但是容易使程序被恶意控制。

原来，真的是我孤陋寡闻了，所有类型的操作系统都支持一点，就是**执行在堆上的指令**，并且可读可写可执行，在 \*nix 下，提供了 _**mprotect()**_ 这个系统调用，更改某个内存段的权限，当然不能是最开始的代码段，但是可以为堆。Windows 应该也提供了相关的 API&#x20;

当然，直接调用 _**mprotect**_ 可能会被 SELinux 这类的安全服务给拒绝并报告给用户，因为这属于恶意操作。更恰当的做法是利用 _**mmap()**_，获取一块可执行可写的内存，该函数会返回具体的起始地址。

```c
address_ = mmap(NULL, size, PROT_READ | PROT_WRITE | PROT_EXEC,
                  MAP_PRIVATE | MAP_ANONYMOUS ,
                  kMmapFd, kMmapFdOffset);
```

Windows 则是提供了，

```c

 address_ = VirtualAlloc(NULL, size,
                  MEM_COMMIT, PAGE_EXECUTE_READWRITE);
  
```

我相信， 所有的脚本编译器（解释器），都有这么关键的一步，只有如此，才能得到一个可写的代码段，这是所有的基础！我的疑惑也终于解开。

## 总结

这篇算是一个引子，关键得理解，解释执行 实现的关键，是拥有一块可写可执行的一块区域。
