Answers:
编译器产生汇编而不是适当的机器代码的其他原因是:
add eax,2
可转换为83 c0 02
或66 83 c0 02
,根据最新的一样发生指令use16
。
编译器通常确实将高级代码直接转换为机器语言,但是可以以模块化的方式进行构建,以便一个后端发出机器代码,而另一端发出机器代码(例如GCC)。代码生成阶段生成“代码”,它是机器代码的某种内部表示形式,然后必须将其转换为可用格式,例如机器语言或汇编代码。
从历史上看,许多著名的编译器直接输出机器代码。但是,这样做有些困难。通常,试图确认编译器工作正常的人会发现检查汇编代码输出比机器代码更容易。此外,有可能(并且在历史上很普遍)使用单遍C或Pascal编译器来生成汇编语言文件,然后可以使用两遍汇编器对其进行处理。直接生成代码将需要使用两次通过的C或Pascal编译器,或者一次通过的编译器,然后使用某种方式对正向跳转地址进行反向修补[如果运行时环境使启动程序的大小可用于固定点 编译器可以在代码末尾编写补丁列表,并让启动代码在运行时应用这些补丁;这种方法会使每个补丁点的可执行文件大小增加大约四个字节,但会提高程序生成速度。
如果目标是使编译器快速运行,则直接代码生成可以很好地工作。但是,对于大多数项目而言,如今生成汇编语言代码并对其进行汇编的成本确实不是主要问题。通常,让编译器以可以与其他编译器生成的代码很好地交互的形式生成代码,这是一个足够大的好处,足以证明增加编译时间是合理的。
即使使用相同指令集的平台也可能具有不同的可重定位目标文件格式。我可以想到“ a.out”(早期的UNIX),OMF,MZ(MS-DOS EXE),NE(16位Windows),COFF(UNIX系统V),Mach-O(OS X和iOS)以及ELF(Linux和其他),以及它们的变体,例如32位Windows上的XCOFF(AIX),ECOFF(SGI)和基于COFF的可移植可执行文件(PE)。产生汇编语言的编译器不需要太多有关目标文件格式的知识,从而使汇编器和链接器可以将这些知识封装在单独的进程中。
另请参见堆栈溢出上的OMF和COFF之间的差异。
通常,编译器在内部使用指令序列进行工作。每条指令将由一个数据结构表示,该数据结构表示其操作名称,操作数等。当操作数是地址时,这些地址通常是符号引用,而不是具体值。
输出汇编器相对简单。这几乎是采用编译器的内部数据结构并将其转储为特定格式的文本文件的问题。汇编器输出也相对易于阅读,这在您需要检查编译器正在执行的操作时很有用。
输出二进制目标文件的工作明显更多。编译器编写者需要了解所有指令的编码方式(在某些CPUS上可能微不足道),他们需要将一些符号引用转换为程序计数器的相对地址,而另一些符号引用则转换为二进制目标文件中的某种形式的元数据。 。他们需要以高度系统特定的格式写出所有内容。
是的,您绝对可以使编译器直接输出二进制对象,而无需将汇编程序写为中间步骤。像软件开发中的许多事情一样,问题是减少编译时间是否值得进行额外的开发和维护工作。
我最熟悉的编译器(freepascal)可以在所有平台上输出汇编程序,但只能直接在平台的子集上输出二进制对象。
除正常的可重定位代码外,编译器还应能够生成汇编器输出,这对程序员有利。
有一次我只是在LSI-11机器上的Unix System V上运行的C程序中找不到错误。似乎没有任何作用。最终,在无奈之下,我让Protable C编译器排除了其翻译的汇编器版本。我终于找到了错误!编译器分配的寄存器比机器中存在的更多!(编译器在仅具有寄存器R0至R7的机器上分配了寄存器R0至R8。)我设法解决了编译器中的错误,并使程序正常工作。
具有汇编器输出的另一个好处是尝试使用使用不同参数传递协议的“标准”库。后来的C编译器允许我使用参数设置协议(“ pascal”将使编译器按照给定的顺序添加参数,而不是C标准的相反顺序)。
另一个好处是允许程序员查看其编译器正在执行的艰巨工作。一个简单的C语句大约需要44条机器指令。从内存中加载值,然后迅速将其丢弃。等等等等...
我个人认为拥有编译器而不是可重定位的对象模块确实很愚蠢。在编译程序时,编译器会收集有关程序的大量信息。它通常将所有这些信息存储在一个符号表中。删除汇编代码后,它将引发所有此信息表。然后,汇编器检查已排除的代码,并重新收集编译器已经拥有的一些信息。但是,汇编器对For语句或While语句的If语句一无所知。因此,所有这些信息都丢失了。然后,汇编器生成可重定位的目标模块,而编译器则没有。
为什么???