# Compile or Interpret ?

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

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

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

## 变量的动态性

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

```c

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

 struct demo_struct demo;

```

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

![ ](/files/-Lg_tPYtqxTIxtf41oKQ)

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

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

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

![](/files/-Lg_uRWKDfURsIqmv-f6)

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

![](/files/-Lg_urLDYvkr3w_tWOK6)

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

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

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

![](/files/-LgadxaB-QecdC1AltEo)

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

不仅如此，对于所有的赋值也要检查，因为一个变量可以同时是多种类型，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);
  
```

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

## 总结

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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://trance.gitbook.io/blog/compiler/compiler-interpreter.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
