原创文章,欢迎转载,转载请注明出处,谢谢。


0. 前言

函数是 Go 的一级公民,本文从汇编角度出发看看我们常用的一些函数在干什么。

1. 函数

1.1 main 函数

在 main 函数中计算两数之和如下:

package main

func main() {
x, y := 1, 2
z := x + y
print(z)
}

使用 dlv 调试函数(不了解 dlv 的请看 Go plan9 汇编: 打通应用到底层的任督二脉):

# dlv debug
Type 'help' for list of commands.
(dlv) b main.main
Breakpoint 1 set at 0x45feca for main.main() ./ex4.go:3
(dlv) c
> main.main() ./ex4.go:3 (hits goroutine(1):1 total:1) (PC: 0x45feca)
1: package main
2:
=> 3: func main() {
4: x, y := 1, 2
5: z := x + y
6: print(z)
7: }

disass 查看对应的汇编指令:

(dlv)
TEXT main.main(SB) /root/go/src/foundation/ex4/ex4.go
ex4.go:3 0x45fec0 493b6610 cmp rsp, qword ptr [r14+0x10]
ex4.go:3 0x45fec4 763d jbe 0x45ff03
ex4.go:3 0x45fec6 55 push rbp
ex4.go:3 0x45fec7 4889e5 mov rbp, rsp
=> ex4.go:3 0x45feca* 4883ec20 sub rsp, 0x20
ex4.go:4 0x45fece 48c744241801000000 mov qword ptr [rsp+0x18], 0x1
ex4.go:4 0x45fed7 48c744241002000000 mov qword ptr [rsp+0x10], 0x2
ex4.go:5 0x45fee0 48c744240803000000 mov qword ptr [rsp+0x8], 0x3
ex4.go:6 0x45fee9 e8d249fdff call $runtime.printlock
ex4.go:6 0x45feee 488b442408 mov rax, qword ptr [rsp+0x8]
ex4.go:6 0x45fef3 e86850fdff call $runtime.printint
ex4.go:6 0x45fef8 e8234afdff call $runtime.printunlock
ex4.go:7 0x45fefd 4883c420 add rsp, 0x20
ex4.go:7 0x45ff01 5d pop rbp
ex4.go:7 0x45ff02 c3 ret
ex4.go:3 0x45ff03 e8d8cdffff call $runtime.morestack_noctxt
ex4.go:3 0x45ff08 ebb6 jmp $main.main
(dlv) regs
Rsp = 0x000000c00003e758

相信看过 Go plan9 汇编: 打通应用到底层的任督二脉 的同学对上述汇编指令已经有一定了解的。

这里进入 main 函数,执行到 sub rsp, 0x20 指令,该指令为 main 函数开辟 0x20 字节的内存空间。继续往下执行,分别将 0x10x20x3 放到 [rsp+0x18][rsp+0x10][rsp+0x8] 处(从汇编指令好像没看到 z := x + y 的加法,合理怀疑是编译器做了优化)。

继续,mov rax, qword ptr [rsp+0x8][rsp+0x8] 地址的值 0x3 放到 rax 寄存器中。然后,调用 call $runtime.printint 打印 rax 的值。实现输出两数之后。后续的指令我们就跳过了,不在赘述。

1.2 函数调用

在 main 函数中实现两数之和,我们没办法看到函数调用的过程。

接下来,定义 sum 函数实现两数之和,在 main 函数中调用 sum。重点看函数在调用时做了什么。

示例如下:

package main

func main() {
a, b := 1, 2
println(sum(a, b))
} func sum(x, y int) int {
z := x + y
return z
}

使用 dlv 调试函数:

# dlv debug
Type 'help' for list of commands.
(dlv) b main.main
Breakpoint 1 set at 0x45feca for main.main() ./ex6.go:3
(dlv) c
> main.main() ./ex6.go:3 (hits goroutine(1):1 total:1) (PC: 0x45feca)
1: package main
2:
=> 3: func main() {
4: a, b := 1, 2
5: println(sum(a, b))
6: }
7:
8: func sum(x, y int) int {
(dlv) disass
Sending output to pager...
TEXT main.main(SB) /root/go/src/foundation/ex6/ex6.go
ex6.go:3 0x45fec0 493b6610 cmp rsp, qword ptr [r14+0x10]
ex6.go:3 0x45fec4 764f jbe 0x45ff15
ex6.go:3 0x45fec6 55 push rbp
ex6.go:3 0x45fec7 4889e5 mov rbp, rsp
=> ex6.go:3 0x45feca* 4883ec28 sub rsp, 0x28
ex6.go:4 0x45fece 48c744241801000000 mov qword ptr [rsp+0x18], 0x1
ex6.go:4 0x45fed7 48c744241002000000 mov qword ptr [rsp+0x10], 0x2
ex6.go:5 0x45fee0 b801000000 mov eax, 0x1
ex6.go:5 0x45fee5 bb02000000 mov ebx, 0x2
ex6.go:5 0x45feea e831000000 call $main.sum
ex6.go:5 0x45feef 4889442420 mov qword ptr [rsp+0x20], rax
ex6.go:5 0x45fef4 e8c749fdff call $runtime.printlock
ex6.go:5 0x45fef9 488b442420 mov rax, qword ptr [rsp+0x20]

regs 查看寄存器状态:

(dlv) regs
Rip = 0x000000000045feca
Rsp = 0x000000c00003e758
Rbp = 0x000000c00003e758
...

继续往下分析指令的执行过程:

  1. sub rsp, 0x28: rsp 的内存地址减 0x28,意味着 main 函数开辟 0x28 字节的栈空间。
  2. mov qword ptr [rsp+0x18], 0x1mov qword ptr [rsp+0x10], 0x2:将 0x10x2 分别放到内存地址 [rsp+0x18][rsp+0x10] 中。
  3. mov eax, 0x1mov ebx, 0x2:将 0x10x2 分别放到寄存器 eaxebx 中。

跳转到 0x45feea 指令:

(dlv) b *0x45feea
Breakpoint 2 set at 0x45feea for main.main() ./ex6.go:5
(dlv) c
> main.main() ./ex6.go:5 (hits goroutine(1):1 total:1) (PC: 0x45feea)
1: package main
2:
3: func main() {
4: a, b := 1, 2
=> 5: println(sum(a, b))
6: }
7:
8: func sum(x, y int) int {
9: z := x + y
10: return z
(dlv) disass
Sending output to pager...
TEXT main.main(SB) /root/go/src/foundation/ex6/ex6.go
ex6.go:3 0x45fec0 493b6610 cmp rsp, qword ptr [r14+0x10]
ex6.go:3 0x45fec4 764f jbe 0x45ff15
ex6.go:3 0x45fec6 55 push rbp
ex6.go:3 0x45fec7 4889e5 mov rbp, rsp
ex6.go:3 0x45feca* 4883ec28 sub rsp, 0x28
ex6.go:4 0x45fece 48c744241801000000 mov qword ptr [rsp+0x18], 0x1
ex6.go:4 0x45fed7 48c744241002000000 mov qword ptr [rsp+0x10], 0x2
ex6.go:5 0x45fee0 b801000000 mov eax, 0x1
ex6.go:5 0x45fee5 bb02000000 mov ebx, 0x2
=> ex6.go:5 0x45feea* e831000000 call $main.sum
ex6.go:5 0x45feef 4889442420 mov qword ptr [rsp+0x20], rax
ex6.go:5 0x45fef4 e8c749fdff call $runtime.printlock
ex6.go:5 0x45fef9 488b442420 mov rax, qword ptr [rsp+0x20]
ex6.go:5 0x45fefe 6690 data16 nop

在执行 call $main.sum 前,让我们先看下内存分布:

(绿色部分表示 main 函数栈)

继续执行 call $main.sum:

(dlv) si
> main.sum() ./ex6.go:8 (PC: 0x45ff20)
TEXT main.sum(SB) /root/go/src/foundation/ex6/ex6.go
=> ex6.go:8 0x45ff20 55 push rbp
ex6.go:8 0x45ff21 4889e5 mov rbp, rsp
ex6.go:8 0x45ff24 4883ec10 sub rsp, 0x10
ex6.go:8 0x45ff28 4889442420 mov qword ptr [rsp+0x20], rax
ex6.go:8 0x45ff2d 48895c2428 mov qword ptr [rsp+0x28], rbx
ex6.go:8 0x45ff32 48c7042400000000 mov qword ptr [rsp], 0x0
ex6.go:9 0x45ff3a 4801d8 add rax, rbx
ex6.go:9 0x45ff3d 4889442408 mov qword ptr [rsp+0x8], rax
ex6.go:10 0x45ff42 48890424 mov qword ptr [rsp], rax
ex6.go:10 0x45ff46* 4883c410 add rsp, 0x10
ex6.go:10 0x45ff4a 5d pop rbp
ex6.go:10 0x45ff4b c3 ret
(dlv) regs
Rip = 0x000000000045ff20
Rsp = 0x000000c00003e728
Rbp = 0x000000c00003e758

可以看到,Rsp 寄存器往下减 8 个字节,压栈开辟 8 个字节空间。继续往下分析指令:

  1. push rbp:将 rbp 寄存器的值压栈,rbp 中存储的是地址 0x000000c00003e758。由于进行了压栈操作,这里的 Rsp 会往下减 8 个字节。
  2. mov rbp, rsp:将当前 rsp 的值给 rbprbpsum 函数栈的栈底。
  3. sub rsp, 0x10rsp 往下减 0X10 个字节,开辟16 个字节的空间,做为 sum 的函数栈,此时 rsp 的地址为 0x000000c00003e710,表示函数栈的栈顶。

执行到这里,我们画出内存分布图如下:

继续往下分析:

  1. mov qword ptr [rsp+0x20], raxmov qword ptr [rsp+0x28], rbx:分别将 rax 寄存器的值 1 放到 [rsp+0x20]:0x000000c00003e730rbx 寄存器的值 2 放到 [rsp+0x28]:0x000000c00003e738
  2. mov qword ptr [rsp], 0x0:将 0 放到 [rsp] 中。
  3. add rax, rbx:将 rax 和 rbx 的值相加,结果放到 rax 中,相加后 rax 中的值为 3。
  4. mov qword ptr [rsp+0x8], rax:将 3 放到 [rsp+0x8] 中。
  5. mov qword ptr [rsp], rax:将 3 放到 [rsp] 中。

根据上述分析,画出内存分布图如下:

可以看出,传给 sum 的形参 x 和 y 实际是在 main 函数栈分配的。

继续往下执行:

  1. add rsp, 0x10rsp 寄存器加 0x10 回收 sum 栈空间。
  2. pop rbp:将存储在 0x000000c00003e720 的值 0x000000c00003e758 移到 rbp 中。
  3. retsum 函数返回。

在执行 ret 指令前最后看下寄存器的状态:

(dlv) regs
Rip = 0x000000000045ff4b
Rsp = 0x000000c00003e728
Rbp = 0x000000c00003e758

我们知道 Rip 寄存器存储的是运行指令所在的内存地址,那么问题就来了,当函数返回时,要执行调用函数的下一条指令:

TEXT main.sum(SB) /root/go/src/foundation/ex6/ex6.go
ex6.go:5 0x45feea* e831000000 call $main.sum
ex6.go:5 0x45feef 4889442420 mov qword ptr [rsp+0x20], rax

这里我们需要 main.sum 返回后执行的下一条指令是 mov qword ptr [rsp+0x20], rax。可是 Rip 指令怎么获得指令所在的地址 0x45feef 呢?

答案在 call $main.sum 这里,这条指令会将下一条指令压栈,在 sum 函数调用 ret 返回时,将之前压栈的指令移到 Rip 寄存器中。这个压栈的内存地址是 0x000000c00003e728,查看其中的内容:

(dlv) print *(*int)(uintptr(0x000000c00003e728))
4587247

4587247 的十六进制就是 0x45feef

执行 ret

(dlv) si
> main.main() ./ex6.go:5 (PC: 0x45feef)
ex6.go:4 0x45fece 48c744241801000000 mov qword ptr [rsp+0x18], 0x1
ex6.go:4 0x45fed7 48c744241002000000 mov qword ptr [rsp+0x10], 0x2
ex6.go:5 0x45fee0 b801000000 mov eax, 0x1
ex6.go:5 0x45fee5 bb02000000 mov ebx, 0x2
ex6.go:5 0x45feea* e831000000 call $main.sum
=> ex6.go:5 0x45feef 4889442420 mov qword ptr [rsp+0x20], rax
ex6.go:5 0x45fef4 e8c749fdff call $runtime.printlock
ex6.go:5 0x45fef9 488b442420 mov rax, qword ptr [rsp+0x20]
ex6.go:5 0x45fefe 6690 data16 nop
ex6.go:5 0x45ff00 e85b50fdff call $runtime.printint
ex6.go:5 0x45ff05 e8f64bfdff call $runtime.printnl
(dlv) regs
Rip = 0x000000000045feef
Rsp = 0x000000c00003e730
Rbp = 0x000000c00003e758

可以看到 Rip 指向了下一条指令的位置。

继续往下执行:

  1. mov qword ptr [rsp+0x20], rax:将 3 放到 [rsp+0x20] 中,[rsp+0x20] 就是存放 sum 函数返回值的内存地址。
  2. call $runtime.printint:调用 runtime.printint 打印返回值 3。

分析完上述调用函数的过程我们可以画出函数栈调用的完整内存分布如下:

2. 小结

本文从汇编角度看函数调用的过程,力图做到对函数调用有个比较通透的了解。


Go plan9 汇编:说透函数栈的更多相关文章

  1. 谈谈arm下的函数栈

    引言 这篇文章简要说说函数是怎么传入参数的,我们都知道,当一个函数调用使用少量参数(ARM上是少于等于4个)时,参数是通过寄存器进行传值(ARM上是通过r0,r1,r2,r3),而当参数多于4个时,会 ...

  2. c函数调用过程原理及函数栈帧分析

    转载自地址:http://blog.csdn.net/zsy2020314/article/details/9429707       今天突然想分析一下函数在相互调用过程中栈帧的变化,还是想尽量以比 ...

  3. broadcom6838开发环境实现函数栈追踪

    在嵌入式设备开发中.内核为内核模块的函数栈追踪已经提供了非常好的支持,但用户层的函数栈追踪确没有非常好的提供支持. 在网上收集学习函数栈跟踪大部分都是描写叙述INTER体系架构支持栈帧的实现机制.或者 ...

  4. Cortex-M3双堆栈MSP和PSP+函数栈帧

    为了防止几百年以后找不到该文章,特此转载 ------------------------------------------------开始转载--------------------------- ...

  5. scala tail recursive优化,复用函数栈

    在scala中如果一个函数在最后一步调用自己(必须完全调用自己,不能加其他额外运算子),那么在scala中会复用函数栈,这样递归调用就转化成了线性的调用,效率大大的提高.If a function c ...

  6. online ddl 使用、测试及关键函数栈

    [MySQL 5.6] MySQL 5.6 online ddl 使用.测试及关键函数栈  http://mysqllover.com/?p=547 本文主要分为三个部分,第一部分是看文档时的笔记:第 ...

  7. golang defer 以及 函数栈和return

    defer 作为延迟函数存在,在函数执行结束时才会正式执行,一般用于资源释放等操作 参考一段代码https://mp.weixin.qq.com/s/yfH0CBnUBmH0oxfC2evKBA来分析 ...

  8. 详解递归(基础篇)———函数栈、阶乘、Fibonacci数列

    一.递归的基本概念 递归函数:在定义的时候,自己调用了自己的函数. 注意:递归函数定义的时候一定要明确结束这个函数的条件! 二.函数栈 栈:一种数据结构,它仅允许栈顶进,栈顶出,先进后出,后进先出.我 ...

  9. arm汇编进入C函数分析,C函数压栈,出栈,传参,返回值

    环境及代码介绍 环境和源码 由于有时候要透彻的理解C里面的一些细节问题,所有有必要看看汇编,首先这一切的开始就是从汇编代码进入C的main函数过程.这里不使用编译器自动生成的这部分汇编代码,因为编译器 ...

  10. x86汇编寄存器,函数参数入栈说明

    https://en.wikipedia.org/wiki/X86_calling_conventions

随机推荐

  1. SpringBoot读取配置文件的几种方式

    示例 user: name: zhaotian age: 18 sex: 男 @Value注解 @Value注解是Spring框架提供的用于注入配置属性值的注解,它可用于类的成员变量.方法参数和构造函 ...

  2. linux 查看端口状态

    查看端口 netstat -tlun 查看端口被那个服务占用 netstat -tunlp |grep 8080

  3. Exception in thread "main" java.lang.NoClassDefFoundError: io/netty/channel/EventLoopGroup

    最近在学习dubbo,跟着教程做,但是运行时报错,需要添加netty依赖 <dependency> <groupId>io.netty</groupId> < ...

  4. 使用gzexe加密shell脚本

    使用 gzexe 加密 shell 脚本是一个相对简单的过程.以下是具体的步骤: 编写你的 shell 脚本:首先,你需要有一个 shell 脚本文件,比如 myscript.sh. 确保脚本可执行: ...

  5. 可能是全网最适合入门的面向对象编程教程:Python实现-嵌入式爱好者必看!

    前言 对于嵌入式入门的同学来说,往往会遇到设备端处理能力不足.在面对大规模计算情况下需要借助上位机完成进一步的数据处理的情况.此时,Python 语言因其简单易用的特点和丰富多样的库成为了我们做上位机 ...

  6. MySql创建事件、计划、定时运行

    CREATE EVENT IF NOT EXISTS check_timeout_eventON SCHEDULE EVERY 30 MINUTEDOBEGIN UPDATE safetyApp_in ...

  7. oeasy教您玩转linux010210管理应用aptitude

    上一部分我们都讲了什么? 下载并运行了 hollywood hollywood 更新了源的信息 sudo apt update 查看所有已经安装的软件包 # dpkg deiban 本地包管理 dpk ...

  8. Excel VBA编程常用语句300句

    定制模块行为 1. Option Explicit '强制对模块内所有变量进行声明 Option Private Module '标记模块为私有,仅对同一工程中其它模块有用,在宏对话框中不显示 Opt ...

  9. ASP.NET MVC / WebAPI 路由机制详解

    从MVC到WebApi,路由机制一直都在其中扮演着重要的角色. 它可以很简单:如果你只需要会用一些简单的路由,如/Home/Index那么你只需要配置一个默认路由就能搞定. 它可以很神秘:你的url可 ...

  10. Nacos 高级详解:提升你的开发和部署效率

    Nacos 高级 一 .服务集群 需求 服务提供者搭建集群 服务调用者,依次显示集群中各服务的信息 搭建 修改服务提供方的controller,打印服务端端口号 package com.czxy.co ...