我们如何从汇编到机器代码(代码生成)


16

有没有一种简单的方法可以可视化将代码组装到机器代码之间的步骤?

例如,如果您在记事本中打开有关二进制文件的文件,则会看到机器代码的文本格式表示形式。我假设您看到的每个字节(符号)都是对应的ascii字符,因为它是二进制值?

但是我们如何从汇编程序转换为二进制程序,幕后发生了什么?

Answers:


28

查看指令集文档,您会从图片微控制器中为每条指令找到类似这样的条目:

示例addlw指令

“ encoding”行告诉该指令二进制形式。在这种情况下,它总是以5开头,然后是一个无关位(可以是1或0),然后“ k”代表要添加的文字。

前几位称为“操作码”,对于每个指令来说都是唯一的。CPU基本上会查看操作码以查看它是什么指令,然后知道将“ k”解码为要添加的数字。

这很繁琐,但编码和解码并不困难。我有一个本科班,我们必须手工完成考试。

要真正制作一个完整的可执行文件,您还必须执行一些操作,例如分配内存,计算分支偏移并将其设置为ELF之类的格式,具体取决于您的操作系统。


10

汇编操作码在大多数情况下与基础机器指令一一对应。因此,您要做的就是识别汇编语言中的每个操作码,将其映射到相应的机器指令,然后将机器指令及其相应的参数(如果有)写到文件中。然后,对源文件中的每个其他操作码重复此过程。

当然,创建可执行文件以正确加载并在操作系统上运行不仅仅需要花费更多的精力,而且大多数体面的汇编程序确实具有一些附加功能,而不仅仅是将操作码简单映射到机器指令(例如宏)。


7

您需要的第一件事就是类似此文件的内容。这是NASM汇编程序使用的x86处理器的指令数据库(我帮助编写了该数据库,尽管实际上不是翻译指令的部分)。让我们从数据库中选择任意一行:

ADD   rm32,imm8    [mi:    hle o32 83 /0 ib,s]      386,LOCK

这意味着它描述了指令ADD。该指令有多种变体,此处要描述的特定变体是采用32位寄存器或存储器地址并添加立即数8位值(即,直接包含在指令中的常数)的变体。使用此版本的示例汇编指令如下:

add eax, 42

现在,您需要输入文本并将其解析为单独的指令和操作数。对于上面的指令,这可能会导致包含指令,结构ADD和操作数数组(对寄存器EAX和value 的引用42)的结构。一旦有了这种结构,就可以遍历指令数据库,找到与指令名称和操作数类型都匹配的行。如果找不到匹配项,则需要向用户显示该错误(通常是“操作码和操作数的非法组合”或类似内容)。

从数据库中获得该行后,我们将查看第三列,该指令的内容为:

[mi:    hle o32 83 /0 ib,s] 

这是一组指令,描述了如何生成所需的机器代码指令:

  • mi是操作数的descriptiuon:一个modr/m(寄存器或内存)操作(这意味着我们需要一个追加modr/m字节的指令,我们会到后来的结尾)和一个立即指令(这将在说明中使用)。
  • 接下来是 hle。这确定了我们如何处理“锁定”前缀。我们尚未使用“锁定”,因此我们将其忽略。
  • 接下来是o32。这告诉我们,如果我们要汇编16位输出格式的代码,则该指令需要一个操作数大小的覆盖前缀。如果我们要生成16位输出,则现在将生成前缀(0x66),但我认为我们不是继续进行。
  • 接下来是83。这是十六进制的文字字节。我们输出它。
  • 接下来是/0。这指定了我们在modr / m图元中需要的一些额外位,并导致我们生成它。该modr/m字节用于编码寄存器或间接存储器引用。我们有一个这样的操作数,一个寄存器。寄存器具有一个编号,该编号在另一个数据文件中指定:

    eax     REG_EAX         reg32           0
  • 我们检查是否reg32与原始数据库中要求的指令大小一致(的确如此)。该0是寄存器的数量。甲modr/m字节是由处理器指定的数据结构,即如下所示:

     (most significant bit)
     2 bits       mod    - 00 => indirect, e.g. [eax]
                           01 => indirect plus byte offset
                           10 => indirect plus word offset
                           11 => register
     3 bits       reg    - identifies register
     3 bits       rm     - identifies second register or additional data
     (least significant bit)
  • 由于我们正在使用寄存器,因此该mod字段为0b11

  • reg字段是我们正在使用的寄存器的编号,0b000
  • 由于该指令中只有一个寄存器,因此我们需要在rm字段中填写一些内容。这就是其中指定的额外数据的/0用途,因此我们将其放在rm字段中,0b000
  • modr/m因此,该字节为0b110000000xC0。我们输出这个。
  • 接下来是ib,s。这指定一个带符号的立即字节。我们查看操作数,并注意我们有一个立即数可用。我们将其转换为带符号的字节并输出(42=> 0x2A)。

因此,完整的组装说明如下:0x83 0xC0 0x2A。将其发送到输出模块,并注意没有字节构成内存引用(输出模块可能需要知道它们是否是字节)。

重复每条指令。跟踪标签,以便您知道在引用标签时要插入的内容。添加用于传递给目标文件输出模块的宏和指令的工具。这基本上就是汇编程序的工作方式。


1
谢谢。很好的解释,但不应该是“ 0x83 0xC0 0x2A”而不是“ 0x83 0xB0 0x2A”,因为0b11000000 = 0xC0
Kamran

@Kamran $ cat > test.asm bits 32 add eax,42 $ nasm -f bin test.asm -o test.bin $ od -t x1 test.bin 0000000 83 c0 2a 0000003-...是的,你说的很对。:)
Jules

2

实际上,汇编器通常不直接生成一些二进制可执行文件,而是生成一些目标文件(稍后将其提供给链接器))。但是,也有例外(您可以使用一些汇编程序直接生成一些二进制可执行文件;它们并不常见)。

首先,请注意,今天许多汇编程序都是自由软件程序。因此,请下载并编译GNU源代码(binutils的一部分)和nasm的源代码。然后研究他们的源代码。顺便说一句,我建议为此目的使用Linux(这是对开发人员友好且对自由软件友好的OS)。

汇编器生成的目标文件主要包含代码段重定位指令。它以记录良好的文件格式组织,具体取决于操作系统。在Linux上,该格式(用于目标文件,共享库,核心转储和可执行文件)为ELF。该对象文件随后输入到链接器(最终生成可执行文件)。重定位由ABI指定(例如x86-64 ABI)。阅读Levine的书Linkers and Loaders了解更多。

这样的目标文件中的代码段包含带孔的机器代码(将在链接器的帮助下,借助重定位信息进行填充)。汇编器生成的(可重定位)机器代码显然特定于指令集体系结构。在86x86-64的(在大多数笔记本电脑或台式机处理器使用)的ISA是在细节上非常复杂。但是,出于教学目的,发明了一个名为y86或y86-64的简化子集。阅读幻灯片。这个问题的其他答案也可以解释这一点。您可能想读一本关于计算机体系结构的好书

大多数汇编程序分两次进行工作,第二次进行重定位或更正第一阶段的某些输出。他们现在使用通常的解析技术(因此,请阅读《龙书》)。

OS 内核如何启动可执行文件(例如,execve系统调用在Linux上的工作方式)是一个不同(且复杂)的问题。它通常设置一些虚拟地址空间(在过程这样做的execve(2) ...)然后重新初始化过程的内部状态(包括用户模式寄存器)。甲动态连接器 -如ld-linux.so(8)基于Linux可能在运行时被卷入。阅读一本好书,例如《操作系统:三篇简单的书》。该OSDEV维基也给出有用的信息。

PS。您的问题如此广泛,以至于您需要阅读几本书。我已经给出了一些(非常不完整的)参考。您应该找到更多。


1
关于目标文件格式,对于初学者,我建议查看NASM产生的RDOFF格式。故意将其设计为尽可能实际可行,并且仍然可以在多种情况下使用。NASM源包括该格式的链接器和加载器。(完整披露-我设计并撰写了所有这些内容)
Jules
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.