一.前言

    作为一名初级的嵌入式软件开发从业者,工作中大部分项目以C语言实现。使用C语言来编写代码,通常我们可以预测到编译生成的汇编/机器编码的大致情况,在不同的芯片架构上,有其相应的ABI标准。而近年来逐渐流行起来的Go语言编程,虽然同样语法上和C语言语法都有较为简单的特点,也都是编译型的静态语言,但我们对它在基本类型——函数参数的传递方式就了解很少了。另外,Go语言的函数可以有多个返回值,其底层机制是如何实现的,也需要分析探究一下。

    本文将记录Golang官方编译器生成的可执行文件在ARM/Linux平台上函数传递的分析过程。测试使用的设备是一部安卓手机,在其中安装了Bash、Git、Vim、GDB等开源软件(安装过程请参考https://pan.baidu.com/s/1i5o6Lwh中的《安装记录.docx》),并在C4DROID中找到了可在安卓中执行的GCC编译器,这样就可以在手机上编译Golang编译器了,其版本为1.4.3。之所以选择该版本的Golang编译器,是因为后续版本的Golang编译器的编译可能会依赖现有的Golang编译器,这就会产生一个鸡生蛋、蛋生鸡的问题了。使用TELNET登入手机后,可以查看到,现在已经可以正常工作了:


二.获取当前的栈指针

    我们知道Go语言的函数定义的关键字为func,其返回值可以为空,或者有多个返回值,于是我们猜测它是以栈来传递参数和返回值的,当然事实确实也是如此,那么在分析函数在调用瞬间、函数返回瞬间,其参数和返回值在栈空间上如何分配这个问题之前,我们需要获取当前函数执行的栈指针。但与C语言不同,Go语言并不支持内联汇编,那我们就需要使用其它的方法。

    这个方法也比较直接,使用汇编来实现,参照Golang代码库的一些汇编格式,我们创建了一个nonsafe包,提供了一个Fetchsp()函数:


    当这个函数被引用,生成的可执行文件中该汇编生成的指令为:


    可以看到,我们将栈指针存放在了栈指针的4字节偏移处,这样的就隐含了我们已知函数返回值的方法,后面我们会对其进一步验证。另外,RET伪指令被编译为add pc, lr, #0,可见Golang 1.4.3版本的ARM汇编器仍不完善。

三.简单函数的参数传递和返回值

    有了当前函数的栈指针后,我们就可以直接把函数在调用前后的栈上面的数据使用fmt包中的类printf函数输出查看了。相应的代码为:


    直接编译并执行,可以得到结果:


    虽然如此,我们不能确定Fetchsp()返回的栈针是正确的,这就需要对生成的可执行文件进一步查看,得到在调用Fetchsp()之前的指令地址:


    上图中高亮的部分是我们感兴趣的地方。使用gdb调试,让它在调用Fetchsp()之前停下来:


    这样,我们就可以确定,Fetchsp函数在Golang 1.4.3版本的正确性了。通过对执行结果的仔细分析,可以得出其下结论:

    函数在传递参数时,在当前函数栈指针4字节偏移处开始传参,[sp + 0x4]为函数第一个参数(0x7e0 = 2016),[sp + 0x8]为函数第二个参数(0x7d9 = 2009);当函数返回时,返回参数存放在最后一个参数之后的位置,即[sp+ 0xc](0x3ee = 2016 * 2009 / (2016 + 2009) = 1006)。

四.interface{}作为函数参数的传递

    Go语言中的函数也支持多个、不确定的参数传递,在《Programming In Go》一书的5.2.2.2一节中提到了类型检测,修改过后的函数如下:


    可以看到,函数classifier()除了第一个参数类型是确定的之外,其他参数都是类型和个数不确定的。在C语言中,也有类似的函数,如printf(const char *, …)等。但是我们知道,诸如printf()之类的函数,可变参数部分基本上都是压入栈的,而且没有类型信息,其类型是根据格式化字符串来推测的。而Go语言则支持类型检测,其实现的机制是怎样的呢?

    通过对生成的可执行文件分析,可以确定在调用classifier()函数之前,入栈了四个参数或者说,入栈了四个值:


    除了第一个指针函数外,那么可以说只有三个值用于interface{}的多个参数传递了。于是我们猜测这三个值类似于一个结构体,包含了所有的可变参数的类型信息和其相应的值。经过测试,我们使用下面的代码将其输出:


    这一段代码比较难看,并不是因为写的很复杂,而是因为使用了很多的“强制类型转换”。在Go语言中,指针不参与数学运算,只好将其转化为uintptr类型,再转化为指针并对其解引用。先来看一下执行的结果:


    从上图可以看到,上面解构造的解析可变参数的功能可以得到可变参数的信息,与类型检测得到的参数值很相近,这就说明了上面的解析是正确的。那么,可变参数列表的信息结构是怎样的呢?

    可以确认,一个可变参数列表在栈上传递会有三个值,其中后两个值很可能是相同的,表示为可变参数的个数(在例子中我们传入了6个可变参数),而第一个值指向了一段栈空间,其中会有两倍于可变参数的数据,每两个为一组,其中第一个值为一个指针或标识,用于确定该参数的类型;第二个值为该参数的指针,同样的,该指针指向栈空间,此处存放了该参数的值。其中有一个例外是nil,它的类型和指针都是0。为了进一步确认这一点,可以查看最后一个参数,float32(2016),使用MATLAB可以验证其十六进制的表示为0x44fc0000:


五.总结

    了解Golang语言编译出的可执行文件的函数传参机制,可能对学习Go语言的帮助并不大。另外,这些函数传参的机制在GCCGO编译生成的可执行文件中并不存在。尽管如此,了解一些Golang底层的实现细节,仍然是十分有趣的,也可以略微满足一下我们对Golang的"ABI"探究的心理。


Logo

更多推荐