# 17|生成本地代码第2关:变量存储、函数调用和栈帧维护 你好,我是宫文学。 在上一节课里,我们已经初步生成了汇编代码和可执行文件。不过,很多技术细节我还没有来得及给你介绍,而且我们支持的语言特性也比较简单。 那么,这一节课,我就来给你补上这些技术细节。比如,我们要如何把逻辑寄存器映射到物理寄存器或内存地址、如何管理栈桢,以及如何让程序符合调用约定等等。 好了,我们开始吧。先让我们解决逻辑寄存器的映射问题,这其中涉及一个简单的寄存器分配算法。 ## 给变量分配物理寄存器或内存 在上一节课,我们在生成汇编代码的时候,给参数、本地变量和临时变量使用的都是逻辑寄存器,也就是只保存了变量的下标。那么我们要怎么把这些逻辑寄存器对应到物理的存储方式上来呢? 我们还是先来梳理一下实现思路吧。 其实,我们接下来要实现的寄存器分配算法,是一个比较初级的算法。你如果用clang或gcc把一个C语言的文件编译成汇编代码,并且不带-O1、-O2这样的优化选项,生成出来的汇编代码就是采用了类似的寄存器分配算法。现在我们就来看看这种汇编代码在实际存储变量上的特点。 首先,程序的参数都被保存到了内存里。具体是怎么来保存的呢?你可以先看看示例程序[param.c](https://gitee.com/richard-gong/craft-a-language/blob/master/16-18/param.c): ```plain void println(int a); int foo(int p1, int p2, int p3, int p4, int p5, int p6, int p7, int p8){ int x1 = p1*p2; int x2 = p3*p4; return x1 + x2 + p5*p6 + p7*p8; } int main(){ int a = 10; int b = 12; int c = a*b + foo(a,b,1,2,3,4,5,6) + foo(b,a,7,8,9,10,11,12); println(c); return 0; } ``` 这个示例程序所对应的汇编代码是[param.s](https://gitee.com/richard-gong/craft-a-language/blob/master/16-18/param.s),我摘取了其中的一部分,这段代码展示了参数是如何被保存到内存的: ```plain ## 把参数值保存到内存 movl %edi, -4(%rbp) movl %esi, -8(%rbp) movl %edx, -12(%rbp) movl %ecx, -16(%rbp) movl %r8d, -20(%rbp) movl %r9d, -24(%rbp) ``` 在这个示例程序中,foo函数有8个参数。根据[System V AMD64的ABI](https://gitee.com/richard-gong/craft-a-language/blob/master/13/System%20V%20Application%20Binary%20Interface%20AMD64%20Architecture%20Processor%20Supplement%20Draft%20Version%200.99.6.pdf)和C语言的调用约定,其中前6个参数是通过寄存器传递的,其他两个参数是通过栈桢传递的。 在汇编代码中,我们会发现这6个通过寄存器传递的参数,都先被保存到了栈桢中。这6个寄存器和参数的对应关系是这样的: ![图片](https://static001.geekbang.org/resource/image/22/33/22ff0091312f4df5fb6313bdf3045333.jpg?wh=1920x570) 我也把它们保存在栈桢里的位置画成了一张图,你可以看一下: ![图片](https://static001.geekbang.org/resource/image/21/e1/213c71d3e770fdc7972b622c77b8d6e1.jpg?wh=1920x1080) 你可以看到,这里面的前6个参数的位置,都是从rbp的位置依次向下4个字节,也就是一个整数的位置。参数1是-4(%rbp),参数2是-8(%rbp),依此类推。 而大于6个的参数,是保存在调用者的栈桢里的。其中参数7的地址是16(%rbp),也就是rbp指针往上16个字节。这里为什么要加上16个字节呢?这是因为,这16个字节中,有8个字节是返回地址,是由callq \_foo指令压到栈里的,还有8个字节是rbp之前的值,是由pushq rbp压到栈里的。 好,到目前为止,我们就知道如何在汇编代码里访问每个参数了。你可以查看代码库里的[lowerVars](https://gitee.com/richard-gong/craft-a-language/blob/master/16-18/asm_x86-64.ts#L1178)代码,它把每个参数转变成了一个内存地址类型的Oprand。 ```plain //处理参数 for (let varIndex:number = 0; varIndex