太阳高照 發表於 2020-6-22 09:42:42

通过汇编看golang函数的多返回值问题

<p>golang这门语言,有个比较好的特性,就是支持函数的多返回值。想C,C++,Java等这些语言,是不支持函数多返回的。但是C,C++可以使用传递指针,实现函数多返回。但是,你有没有想过,golang是怎样实现函数多返回值的呢?</p>
<p>我们知道,C,C++是通过寄存器实现函数返回值的,也就是先把返回值写入到一个寄存器中,然后再从寄存器中,读到函数的返回值。golang也是这样实现的吗?</p>
<p>伟大的思想家孔子曾说过,在源码面前一切都如同裸奔。后来,鲁迅先生,总结了孔子的思想,说出了,在汇编面前,一切语法都是纸老虎。</p>
<p>下面我们通过golang的汇编指令,来看一下golang是怎样实现函数的多返回值的</p>
<p>在看汇编之前,我们先用go的 <strong>debug</strong> 函数看下函数的栈信息</p>
<p>代码很简单,不用解释了</p>
<div class="jb51code">
<pre class="brush:csharp;">
package main
import (
"fmt"
"runtime/debug"
)

func main() {
one(3)
}

func one(a int) (int, int) {
fmt.Println(string(debug.Stack()))
return a, a + 5
}</pre>
</div>
<p style="text-align: center"><img alt="" src="https://img.jbzj.com/file_images/article/202006/2020062209241417.png" />&nbsp;</p>
<p>我标红的这一行,就是 <strong>one</strong> 函数的栈信息,第一个参数 <strong>0x3</strong> 很好理解,就是我们传入的参数 <strong>3</strong></p>
<p>, 但是后面这两个是啥?还有,我明明只传了一个参数,为啥会传入三个参数?</p>
<p>到这里,我也就不卖关子了,直接说了,后面这两个参数,就是one函数返回值的地址,也就是说,one函数返回值地址不在one函数中,而是在调用one函数的mian函数中。golang的函数返回值,和C,C++的不同,golang的返回值是通过栈内地址实现的(返回值的地址是由函数调用者提供)。</p>
<div class="jb51code">
<pre class="brush:csharp;">
package main

func main() {
var b, c *int
one(3, b, c)
}

func one(a int, b, c *int) {
}</pre>
</div>
<p>也就是说,刚开始的那段代码,和这段在功能实现上,没有什么差别,只是golang编译器提供的一个语法糖。</p>
<p><span style="color: #ff0000"><strong>下面通过汇编来看一下</strong></span></p>
<p>这次我们不是对深入分析golang的汇编,只是从汇编层面,验证我们之前结论(golang函数多返回问题)</p>
<p>所以,不会死磕plan9汇编语法,说实话,plan9的很多知识我也不懂,大学没开过汇编的课程,这些东西都是因为兴趣自学的。</p>
<p>golang用的是plan9汇编,看plan9之前,先了解一下plan9的几个概念</p>
<p>go汇编中有4个伪寄存器</p>
<ul>
<li>FP: Frame pointer,指向栈底位置,一般用来引用函数的输入参数,用来访问函数的参数</li>
<li>PC: Program counter: 程序计数器,用于分支和跳转</li>
<li>SB: Static base pointer: 一般用于声明函数或者全局变量</li>
<li>SP: Stack pointer:指向当前栈帧的局部变量的开始位置(栈顶位置),一般用来引用函数的局部变量</li>
</ul>
<p>我们用这段代码进行汇编</p>
<div class="jb51code">
<pre class="brush:csharp;">
package main

func main() {
one(3)
}

func one(a int) (int, int) {
return a, a + 5
}</pre>
</div>
<p>使用 <code>go tool compile -N -l -S main.go</code> 得到汇编代码</p>
<div class="jb51code">
<pre class="brush:plain;">
"".main STEXT nosplit size=2 args=0x0 locals=0x0
0x0000 00000 (C:\Users\bruce\Desktop\go\main.go:3)TEXT "".main(SB), NOSPLIT|ABIInternal, $0-0
0x0000 00000 (C:\Users\bruce\Desktop\go\main.go:3)FUNCDATA$0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (C:\Users\bruce\Desktop\go\main.go:3)FUNCDATA$1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (C:\Users\bruce\Desktop\go\main.go:3)FUNCDATA$3, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (C:\Users\bruce\Desktop\go\main.go:4)PCDATA $2, $0
0x0000 00000 (C:\Users\bruce\Desktop\go\main.go:4)PCDATA $0, $0
0x0000 00000 (C:\Users\bruce\Desktop\go\main.go:4)XCHGL AX, AX
0x0001 00001 (&lt;unknown line number&gt;) RET
0x0000 90 c3         ..
"".one STEXT nosplit size=20 args=0x18 locals=0x0
0x0000 00000 (C:\Users\bruce\Desktop\go\main.go:7)TEXT "".one(SB), NOSPLIT|ABIInternal, $0-24
0x0000 00000 (C:\Users\bruce\Desktop\go\main.go:7)FUNCDATA$0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (C:\Users\bruce\Desktop\go\main.go:7)FUNCDATA$1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (C:\Users\bruce\Desktop\go\main.go:7)FUNCDATA$3, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (C:\Users\bruce\Desktop\go\main.go:8)PCDATA $2, $0
0x0000 00000 (C:\Users\bruce\Desktop\go\main.go:8)PCDATA $0, $0
0x0000 00000 (C:\Users\bruce\Desktop\go\main.go:8)MOVQ "".a+8(SP), AX
0x0005 00005 (C:\Users\bruce\Desktop\go\main.go:8)MOVQ AX, "".~r1+16(SP)
0x000a 00010 (C:\Users\bruce\Desktop\go\main.go:8)ADDQ $5, AX
0x000e 00014 (C:\Users\bruce\Desktop\go\main.go:8)MOVQ AX, "".~r2+24(SP)
0x0013 00019 (C:\Users\bruce\Desktop\go\main.go:8)RET
0x0000 48 8b 44 24 08 48 89 44 24 10 48 83 c0 05 48 89 H.D$.H.D$.H...H.
0x0010 44 24 18 c3          D$..</pre>
</div>
<p>我只截取了和main,one函数相关的部分</p>
<p><code>TEXT "".one(SB), NOSPLIT|ABIInternal, $0-24</code> 这行最后, <strong>$0-24</strong> 的含义,0代表one函数的栈帧大小(局部变量+可能需要的额外调用函数的参数空间的总大小),因为one函数中没有额外开销,所有大小是0,24是传入参数和返回值的大小,单位是字节。传入的参数和返回值都是 <strong>int</strong> ,在64位机器上,大小是8个字节,64位。</p>
<p><img alt="" src="https://img.jbzj.com/file_images/article/202006/2020062209241418.png" /></p>
<p>简单画一下栈的示意图</p>
<p>看一下这句 <code>0x0000 00000 (C:\Users\bruce\Desktop\go\main.go:8) MOVQ "".a+8(SP), AX</code></p>
<p>SP寄存器指向的是栈顶的位置, <strong>AX</strong> 是一个通用寄存器</p>
<p>MOVQ指令 把 参数a 也就是(SP+8)的值搬到AX中</p>
<p><code>0x0005 00005 (C:\Users\bruce\Desktop\go\main.go:8) MOVQ AX, "".~r1+16(SP)</code> <br />
同样,把AX中的值搬到r1(返回值b)</p>
<p><code>0x000a 00010 (C:\Users\bruce\Desktop\go\main.go:8) ADDQ $5, AX</code> <br />
<strong>ADDQ</strong> 指令把AX值+5</p>
<p><code>0x000e 00014 (C:\Users\bruce\Desktop\go\main.go:8) MOVQ AX, "".~r2+24(SP)</code> <br />
最后把AX的值搬到r2(返回值c)</p>
<p><code>0x0013 00019 (C:\Users\bruce\Desktop\go\main.go:8) RET</code> <br />
最后RET指令,one函数结束</p>
<p><span style="color: #ff0000"><strong>总结</strong></span></p>
<p>通过对golang进行汇编,真实了之前的结论</p>
<p>golang函数的多返回值不是通过寄存器传递,使用过使用调用值提供的地址,赋值实现的</p>
<p>先写这些吧,我也是刚接触golang的汇编,文中如有不正确的地方,还请指出</p>
<p>到此这篇关于通过汇编看golang函数的多返回值的文章就介绍到这了,更多相关汇编golang函数多返回值内容请搜索琼殿技术社区以前的文章或继续浏览下面的相关文章希望大家以后多多支持琼殿技术社区!</p>
                           
                            <div class="art_xg">
                              <b>您可能感兴趣的文章:</b><ul><li>详解golang函数多返回值错误处理与error类型</li><li>golang函数的返回值实现</li><li>GO语言中函数命名返回值的使用</li></ul>
                            </div>

                        </div>
                        <!--endmain-->
頁: [1]
查看完整版本: 通过汇编看golang函数的多返回值问题