



x86 Int 汇编语言的具体语法的定义如图2.5所示。我们使用GNU汇编器所使用的AT&T语法。程序以一个main标签开始,后面跟着一串指令。globl指令使主过程在外部是可见的,以便操作系统可以调用它。x86的程序存储在计算机的内存中。就我们的目的而言,计算机的内存可被看作64位地址到64位值上的映射。计算机在rip寄存器中有一个 程序计数器 (PC),它指向下一条要执行的指令的地址。对于大多数指令,程序计数器在指令执行后递增,以便指向内存中的下一条指令。大多数x86指令接受两个操作数,每个操作数可能是一个整数常量(称为 立即值 )、一个 寄存器 或一个内存位置。
寄存器是一种可保存64位值的特殊变量。计算机中有16个通用寄存器,它们的名称见图2.5。寄存器是用百分号“%”和跟在后面的名称来表示的,例如%rax。
图2.5 x86 Int 汇编语言语法(AT&T语法)
立即值使用符号$ n 写入,其中 n 是整数。使用语法 n (% r )来指定对内存的访问,它获取存储在寄存器 r 中的地址,指向该地址增加 n 个字节处的内存。结果地址会被用于加载或存储到内存,这取决于它是作为指令的源参数还是目的参数出现。
算术指令addq s , d ,从源 s 和目的 d 中取出值,进行算术运算,然后将结果写入目的地址 d 中。传送指令movq s , d ,从 s 中读取,并将结果传送到 d 中。调用指令callq label ,指令跳转到标签 label 指定的过程,retq从一个过程返回到它的调用者。我们将在本章和第8章进一步详细讨论过程调用。指令中最后一个字母q表示这些指令操作是四字节的,即64位值。
附录中包含本书使用的所有x86汇编指令的参考说明。
图2.6描述了一个计算10+32的x86汇编程序。指令movq $10, %rax将10传送到寄存器rax,然后addq $32,%rax将32与rax中的10相加,并将结果42放入rax中。最后一条指令retq通过将rax的整数返回给操作系统来完成main函数。操作系统将这个整数解释为程序的退出码。按照惯例,退出码为0表示程序成功完成,而所有其他退出码则表示各种错误。
图2.6 计算10+32的x86汇编程序
在下一个例子中,我们将展示如何使用内存来存储中间结果。图2.7给出了一个计算52+-10的x86汇编程序。这个程序使用一个称为 过程调用栈 (简称 栈 )的内存区域。栈包含了每个过程调用的单独帧,单个帧的内存布局如图2.8所示。寄存器rsp称为栈指针,它包含栈顶部项的地址。通常,我们用术语 指针 指代包含某个东西的地址。栈在内存中向下增长,因此我们通过将栈指针减去一个值来增加栈的大小。在过程调用的上下文中,返回地址是指紧随调用指令后的指令在调用程序中的位置。函数调用指令callq在跳转到过程之前将 返回地址 压入栈。寄存器rbp是 基指针 ,用于访问存储在当前过程调用帧中的变量。调用者的基指针存储在返回地址之后。图2.8显示了存储 n 个变量的帧的内存布局,变量从1到 n 编号。变量1存储在地址-8(%rbp),变量2存储在地址-16(%rbp),以此类推。
图2.7 计算52+-10的x86汇编程序
图2.8 帧的内存布局
在图2.7所示的程序中,我们考虑控制权如何从操作系统转移到main函数。操作系统发出一个callq main指令,将它的返回地址压入栈,然后跳转到main。在x86-64中,在执行任何callq指令之前,栈指针rsp必须先是16字节的倍数,这样当控制到达main时,rsp偏离了8个字节(因为callq推入了返回地址)。前三个指令是程序的典型 起始处理 。指令pushq %rbp首先从栈指针rsp中减去8,然后将地址为rsp的调用者的基指针保存在栈上。下一条指令movq %rsp, %rbp将基指针设置为当前栈指针,该栈指针指向旧基指针的位置。指令subq $16, %rsp将栈指针向下移动,以便为存储变量腾出足够的空间。这个程序需要一个变量(8字节),但是我们向上取整到16字节,以便使rsp是16字节对齐的,这样我们就可以进行对其他函数的调用了。
起始处理之后的第一条指令是movq $10,-8(%rbp),它将值10存储在变量1中。指令negq-8(%rbp)会将变量1的内容更改为-10。下一条指令将变量1中的-10移到rax寄存器中。最后,addq $52, %rax将52加到rax中的值上,将其内容更新为42。
main函数的 收尾 部分由最后三条指令组成。前两条指令将rsp和rbp寄存器恢复到过程开始时的状态。特别地,addq $16, %rsp将栈指针移动到指向旧基指针的位置。然后popq %rbp将旧的基指针恢复到rbp中,并在栈指针上加8。最后一条指令retq跳转回调用这条指令的过程,并将栈指针加8。
我们的编译器需要一种方便的表示来操作x86程序,因此我们为x86定义了一种抽象语法,如图2.9所示,我们把这种语言称为x86 Int 。它与x86 Int 的具体语法(图2.5)的主要区别在于标签、指令名和寄存器名是由字符串显式表示的。关于callq的抽象语法,Callq的AST节点包含一个整数,用于表示函数的元数,即其参数的数量,知道该值将有助于后面寄存器的分配(见第4章)。
图2.9 x86 Int 汇编语言的抽象语法