无法理解增量汇编程序/编译器/基于映像的系统是如何工作的



我正在学习一个小汇编,对于我的下一个项目,我想学习如何制作增量汇编程序。通过";增量汇编程序";我指的是在运行时接受新代码的汇编程序。

通常,汇编的工作流程是编写文件并将其提供给汇编程序+链接器,然后在另一端获得可执行文件。与之形成对比的是基于图像的系统,如Smalltalk或SBCL(lisp),在这些系统中,您有一个正在运行的图像,其中函数/表达式是增量添加的。

有人知道在这样的系统中是如何实现的吗?假设我们在linux操作系统上工作,他们是否只是在每次执行新函数/表达式时编辑ELF文件并重新加载整个映像?或者有没有一种方法可以加载ELF文件的内容,然后在它上面动态执行汇编(即不向磁盘写入任何其他文件)?

有人能给我举一个最简单的例子吗?或者关于这种基于图像的系统以及它们是如何制作的书籍/博客?

对于像大多数Lisp这样基于图像的系统来说,这个问题的答案很简单(但有时很麻烦)。

编译器/汇编程序采用源代码,其最终结果,作为任何编译器或汇编程序的最终结果,是一个或多个八位字节数组,表示生成的目标代码,可能还有与之相关的一些数据,以及代码所指的名称信息、代码定义的名称信息和重新定位信息等

在传统的系统中,这些数组被费力地写入一个文件(很久以前,当机器几乎没有内存时,可能需要在创建它们时将它们写入文件),然后调用一些程序,将其中的几个文件粘合到一个文件中,修补引用等等。然后,生成的文件被加载到机器的内存中,还有更多的补丁完成了,最后机器被告知运行它。程序立即崩溃,整个过程现在需要重新完成。(我在这里回避了细节)。

然后需要某种协议——以一种或多种文件标准格式的形式——允许所有这些不同的工具根据需要多次将数据拖放到内存中。ELF就是这样一种标准:还有几十种其他标准。

在基于图像的系统中,不需要任何官僚作风:编译器/汇编程序会像以前一样生成某种八位字节数组,以及一些数据表示。所有这些数据都只存在于内存中,该阵列的大部分修补工作可能是在创建时完成的这个数组现在是可执行代码,所以原则上只需要告诉机器"开始运行这个"。在实践中,在现代机器上,还需要做更多的工作:它所在的内存需要标记为可执行的,而且可能需要发生一点舞蹈,因为标记为可运行的内存无法写入,等等

你可以在工作中看到:

> (defun foo (x y)
(+ x y))
foo
> (compile 'foo)
foo
nil
nil
> (describe (function foo))
#<Function foo 80200109F4> is a function
code           #<code foo (76) 80200109C0>
constants      (0 #<Function foo 80200109F4> foo #(#(1 0) 0) (x y))

因此,foo函数(编译器产生的东西)有两个组成部分:它的代码,是一个正在编写机器将执行的八位字节数组的对象。事实上,在我使用的实现(LispWorks)中,有几个函数可以询问有关函数代码的问题:

> (system:function-code-length #'foo)
76

它有76个八位字节长,如果您(disassemble 'foo),您将看到这确实是代码的长度:

> (disassemble 'foo)
[...]
75:      90               nop

你可以在内存中找到它的地址:

> (system:function-code-address #'foo)
550292752884

你可以看到,当GC重新定位它时,这个地址可能会改变:

> (clean-down)
51183616
> (system:function-code-address #'foo)
559151419204

(LW中的clean-down进行了相当大的GC:它"如果可能的话,可以释放内存并减小图像的大小"。)

总之:增量的、基于图像的编译器/汇编程序所做的与基于文件的编译器/汇编器所做的相同。。。除了不将数据写入文件,复制到另一个文件,然后将最终文件读回内存,也不需要使用文件格式。它只是依赖于这样一个事实,即编译后的代码已经在内存中并在那里运行。

实时编译的基本方法是解释器(或虚拟机)一次使用一个方法/函数,并为那些频繁执行的方法/函数创建机器代码。此机器代码是在读/写/执行内存段中创建的,不是ELF的一部分。然后,当要执行该方法/函数时,解释器或VM跳到机器代码,并等待它从动态创建的代码返回。因此,整个应用程序不受编译器或汇编程序的约束,只受选定的方法/函数和框架或应用程序(解释器或VM)的其余部分的约束,与原始ELF中的情况相同。

相关内容

最新更新