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


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. Spring Boot 整合

    什么是Spring Boot? 百度百科一下 创建Spring Boot项目 通过官网来创建(了解) 这里面的创建方式不做过多说明,只需要在 官网 里面创建好了,然后下载解压,就可以了,我这里直接使用 ...

  2. MySql 安装详细步骤

    一.官网下载 官网地址:https://dev.mysql.com/downloads/installer/ 二.开始安装 1.点击按装文件开始安装 2.只安装服务端就可以了,一直下一步 3. 4. ...

  3. 使用Stream流实现以List<Map<String, Object>>集合中Map的key值进行排序

    使用Stream流实现以List<Map<String, Object>>集合中Map的key值进行排序 创建一个list存入数据 List<Map<String, ...

  4. python 自动化神器 多平台纯代码RPA办公自动化python框架

    ​ Pyaibote是一款专注于纯代码RPA(机器人流程自动化)的强大工具,支持Android.Browser和Windows三大主流平台.无论您需要自动化安卓应用.浏览器操作还是Windows应用程 ...

  5. 解决方案 | Python中安装pix2tex latex ocr出现报错Cannot mix incompatible Qt library (6.6.2) with this library (6.7.2)

    一.问题 Python中安装pix2tex latex ocr出现报错Cannot mix incompatible Qt library (6.6.2) with this library (6.7 ...

  6. 探索Nuxt.js的useFetch:高效数据获取与处理指南

    title: 探索Nuxt.js的useFetch:高效数据获取与处理指南 date: 2024/7/15 updated: 2024/7/15 author: cmdragon excerpt: 摘 ...

  7. 网易数帆实时数据湖 Arctic 的探索和实践

    作者 | 蔡芳芳 采访嘉宾 | 马进 网易数帆平台开发专家 数据中台也要从离线为主走向实时化,湖仓一体是第一步. 数据从离线到实时是当前一个很大的趋势,但要建设实时数据.应用实时数据还面临两个难题.首 ...

  8. 怎么用git命令将其他分支的提交记录提取到当前分支上

    您可以使用 Git 命令 "cherry-pick" 将其他分支的提交记录提取到当前分支上.以下是使用 cherry-pick 命令的步骤:1. 切换到当前分支: `git che ...

  9. ModuleNotFoundError: No module named 'import_export'

    当你遇到 "ModuleNotFoundError: No module named 'import_export'" 错误时,这表示你的 Python 脚本或应用程序试图导入名为 ...

  10. ECMAScript 是什么?

    ECMAScript 是什么 简介 Ecma 标准定义了 ECMAScript 语言 ECMAScript 基于多种原始技术,最著名的是 JavaScript (Netscape) 和 JScript ...