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

golang这门语言,有个比较好的特性,就是支持函数的多返回值。想C,C++,Java等这些语言,是不支持函数多返回的。但是C,C++可以使用传递指针,实现函数多返回。但是,你有没有想过,golang是怎样实现函数多返回值的呢?

我们知道,C,C++是通过寄存器实现函数返回值的,也就是先把返回值写入到一个寄存器中,然后再从寄存器中,读到函数的返回值。golang也是这样实现的吗?

伟大的思想家孔子曾说过,在源码面前一切都如同裸奔。后来,鲁迅先生,总结了孔子的思想,说出了,在汇编面前,一切语法都是纸老虎。

下面我们通过golang的汇编指令,来看一下golang是怎样实现函数的多返回值的

在看汇编之前,我们先用go的 debug 函数看下函数的栈信息

代码很简单,不用解释了

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
}

我标红的这一行,就是 one 函数的栈信息,第一个参数 0x3 很好理解,就是我们传入的参数 3

, 但是后面这两个是啥?还有,我明明只传了一个参数,为啥会传入三个参数?

到这里,我也就不卖关子了,直接说了,后面这两个参数,就是one函数返回值的地址,也就是说,one函数返回值地址不在one函数中,而是在调用one函数的mian函数中。golang的函数返回值,和C,C++的不同,golang的返回值是通过栈内地址实现的(返回值的地址是由函数调用者提供)。

package main

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

func one(a int, b, c *int) {
}

也就是说,刚开始的那段代码,和这段在功能实现上,没有什么差别,只是golang编译器提供的一个语法糖。

下面通过汇编来看一下

这次我们不是对深入分析golang的汇编,只是从汇编层面,验证我们之前结论(golang函数多返回问题)

所以,不会死磕plan9汇编语法,说实话,plan9的很多知识我也不懂,大学没开过汇编的课程,这些东西都是因为兴趣自学的。

golang用的是plan9汇编,看plan9之前,先了解一下plan9的几个概念

go汇编中有4个伪寄存器

  • FP: Frame pointer,指向栈底位置,一般用来引用函数的输入参数,用来访问函数的参数
  • PC: Program counter: 程序计数器,用于分支和跳转
  • SB: Static base pointer: 一般用于声明函数或者全局变量
  • SP: Stack pointer:指向当前栈帧的局部变量的开始位置(栈顶位置),一般用来引用函数的局部变量

我们用这段代码进行汇编

package main

func main() {
 one(3)
}

func one(a int) (int, int) {
 return a, a + 5
}

使用 go tool compile -N -l -S main.go 得到汇编代码

"".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 (<unknown line number>) 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$..

我只截取了和main,one函数相关的部分

TEXT "".one(SB), NOSPLIT|ABIInternal, $0-24 这行最后, $0-24 的含义,0代表one函数的栈帧大小(局部变量+可能需要的额外调用函数的参数空间的总大小),因为one函数中没有额外开销,所有大小是0,24是传入参数和返回值的大小,单位是字节。传入的参数和返回值都是 int ,在64位机器上,大小是8个字节,64位。

简单画一下栈的示意图

看一下这句 0x0000 00000 (C:\Users\bruce\Desktop\go\main.go:8) MOVQ "".a+8(SP), AX

SP寄存器指向的是栈顶的位置, AX 是一个通用寄存器

MOVQ指令 把 参数a 也就是(SP+8)的值搬到AX中

0x0005 00005 (C:\Users\bruce\Desktop\go\main.go:8) MOVQ AX, "".~r1+16(SP)
同样,把AX中的值搬到r1(返回值b)

0x000a 00010 (C:\Users\bruce\Desktop\go\main.go:8) ADDQ $5, AX
ADDQ 指令把AX值+5

0x000e 00014 (C:\Users\bruce\Desktop\go\main.go:8) MOVQ AX, "".~r2+24(SP)
最后把AX的值搬到r2(返回值c)

0x0013 00019 (C:\Users\bruce\Desktop\go\main.go:8) RET
最后RET指令,one函数结束

总结

通过对golang进行汇编,真实了之前的结论

golang函数的多返回值不是通过寄存器传递,使用过使用调用值提供的地址,赋值实现的

先写这些吧,我也是刚接触golang的汇编,文中如有不正确的地方,还请指出

到此这篇关于通过汇编看golang函数的多返回值的文章就介绍到这了,更多相关汇编golang函数多返回值内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Golang 函数执行时间统计装饰器的一个实现详解

    背景 最近在搭一个新项目的架子,在生产环境中,为了能实时的监控程序的运行状态,少不了逻辑执行时间长度的统计.时间统计这个功能实现的期望有下面几点: 实现细节要剥离:时间统计实现的细节不期望在显式的写在主逻辑中.因为主逻辑中的其他逻辑和时间统计的抽象层次不在同一个层级 用于时间统计的代码可复用 统计出来的时间结果是可被处理的. 对并发编程友好 实现思路 统计细节的剥离 最朴素的时间统计的实现,可能是下面这个样子: func f() { startTime := time.Now() logicSt

  • Golang学习笔记之延迟函数(defer)的使用小结

    golang的defer优雅又简洁, 是golang的亮点之一.defer在声明时不会立即执行,而是在函数return后,再按照先进后出的原则依次执行每个defer,一般用于释放资源.清理数据.记录日志.异常处理等. 关键字defer于注册延迟调用.这些调用直到 ret 前才被执行,通常用于释放资源或错误处理. 一.当defer被声明时,其参数就会被实时解析 func a() { i := 0 defer fmt.Println(i) //输出0,因为i此时就是0 i++ defer fmt.P

  • Golang中的自定义函数详解

    不管是面向过程的编程,还是面向对象的编程,都离不开函数的概念,分别是,参数,函数名,返回值.接下来我们看看Go语言在这三个方面是做怎么操作的吧. 参数 谈到参数,我们在写函数或者是类中的方法的时候都需要考虑我们应该传递怎样的参数,或者是是否需要参数. 参数首先分为无参函数有参.无参也就是没有参数,也就不用写了. 有参 func functionTest() {  # 小括号内就是用来放参数的     # 函数体内 } Go语言是强数据类型的语言,参数是要指定类型的不然就报错.func 是函数的声

  • golang 实现每隔几分钟执行一个函数

    1.使用定时器 2.使用这种方式 go function() func function() { // TODO 具体逻辑 // 每5分钟执行一次 time.AfterFunc(5*time.Minute, function) } 补充:Golang:每天零点定时执行操作 我就废话不多说了,大家还是直接看代码吧~ import ( "time" "fmt" ) //定时结算Boottime表数据 func BoottimeTimingSettlement() { f

  • Golang记录、计算函数执行耗时、运行时间的一个简单方法

    先写一个公共函数, 比如在 common 包下有这么一个方法: // 写超时警告日志 通用方法 func TimeoutWarning(tag, detailed string, start time.Time, timeLimit float64) { dis := time.Now().Sub(start).Seconds() if dis > timeLimit { log.Warning(log.CENTER_COMMON_WARNING, tag, " detailed:&quo

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

    golang这门语言,有个比较好的特性,就是支持函数的多返回值.想C,C++,Java等这些语言,是不支持函数多返回的.但是C,C++可以使用传递指针,实现函数多返回.但是,你有没有想过,golang是怎样实现函数多返回值的呢? 我们知道,C,C++是通过寄存器实现函数返回值的,也就是先把返回值写入到一个寄存器中,然后再从寄存器中,读到函数的返回值.golang也是这样实现的吗? 伟大的思想家孔子曾说过,在源码面前一切都如同裸奔.后来,鲁迅先生,总结了孔子的思想,说出了,在汇编面前,一切语法都是

  • python在回调函数中获取返回值的方法

    python中有用到回调函数的时候,而回调函数又需要返回数值的时候,就需要先将所被传为回调函数的函数先赋值给一个变量,然后等回调结束之后,将这个变量取值回来就可以了. 如我用到到的调用xmlreader时,传入的一个函数需要取回返回值的代码: # 创建一个 XMLReader parser = xml.sax.make_parser() # turn off namepsaces parser.setFeature(xml.sax.handler.feature_namespaces, 0) #

  • shell函数内调用另一个函数(不带返回值和带返回值)

    目录 一.函数B调用不带返回值的函数A 二.函数B调用带返回值的函数A,并接收函数A的返回值进行输出 一.函数B调用不带返回值的函数A 新建文件,命名为 test.sh,添加如下代码: #!/bin/bash # 即将被调用的函数A function A(){ a="aaa" echo $a } # 函数B,直接调用A function B(){ A echo "bbb" } B 命令行中通过sh test.sh执行结果: 二.函数B调用带返回值的函数A,并接收函数

  • 从汇编看c++函数的默认参数的使用说明

    在c++中,可以为函数提供默认参数,这样,在调用函数的时候,如果不提供参数,编译器将为函数提供参数的默认值.下面从汇编看其原理. 下面是c++源码: 复制代码 代码如下: int add(int a = 1, int b = 2) {//参数a b有默认值    return a + b;}int main() {   int c= add();//不提供参数 } 下面是mian函数里面的汇编码: 复制代码 代码如下: ; 4    : int main() { push    ebp    m

  • linux shell自定义函数(定义、返回值、变量作用域)介绍

    linux shell 可以用户定义函数,然后在shell脚本中可以随便调用.下面说说它的定义方法,以及调用需要注意那些事项. 一.定义shell函数(define function) 语法: [ function ] funname [()] { action; [return int;] } 说明: 1.可以带function fun() 定义,也可以直接fun() 定义,不带任何参数. 2.参数返回,可以显示加:return 返回,如果不加,将以最后一条命令运行结果,作为返回值. retu

  • vue parseHTML 函数拿到返回值后的处理源码解析

    目录 引言 parseStartTag函数返回值 handleStartTag源码 tagName 及unarySlash 调用parser钩子函数 引言 继上篇文章: parseHTML 函数源码解析 var startTagMatch = parseStartTag(); if (startTagMatch) { handleStartTag(startTagMatch); if (shouldIgnoreFirstNewline(startTagMatch.tagName, html))

  • python的函数形参和返回值你了解吗

    目录 函数的返回值 函数的参数 不可变参数和可变参数 += 函数的参数 缺省参数 多值参数 元组和字典的拆包 总结 函数的返回值 一个函数执行后可以返回多个返回值 def measure(): print('测量开始....') temp=39 wetness=50 print("测量结束") #元组--可以包含多个数据,因此可以使用元组一次返回多个值 return (temp,wetness) result=measure() print(result) 运行结果: 测量开始....

  • golang函数的返回值实现

    函数可以有0或多个返回值,返回值需要指定数据类型,返回值通过return关键字来指定. return可以有参数,也可以没有参数,这些返回值可以有名称,也可以没有名称.go中的函数可以有多个返回值. return关键字中指定了参数时,返回值可以不用名称.如果return省略参数,则返回值部分必须带名称 当返回值有名称时,必须使用括号包围,逗号分隔,即使只有一个返回值 但即使返回值命名了,return中也可以强制指定其它返回值的名称,也就是说return的优先级更高 命名的返回值是预先声明好的,在函

  • C++中stack的pop()函数返回值解析

    目录 stack的pop()函数返回值 全部demo 分析 C++的返回值优化 从函数返回值 RVO stack的pop()函数返回值 int temp = s.pop(); cout<<temp<<endl; 运行代码会提示错误:error C2440: “初始化”: 无法从“void”转换为“int” 全部demo #include <iostream> #include <stack> using namespace std; int main() {

  • GoLang函数与面向接口编程全面分析讲解

    目录 一.函数 1. 函数的基本形式 2. 递归函数 3. 匿名函数 4. 闭包 5. 延迟调用defer 6. 异常处理 二.面向接口编程 1. 接口的基本概念 2. 接口的使用 3. 接口的赋值 4. 接口嵌入 5. 空接口 6. 类型断言 7. 面向接口编程 一.函数 1. 函数的基本形式 // 函数定义:a,b是形参 func add(a int, b int) { a = a + b } var x, y int = 3, 6 add(x, y) // 函数调用:x,y是实参 形参是函

随机推荐