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


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. Nuxt框架中内置组件详解及使用指南(二)

    title: Nuxt框架中内置组件详解及使用指南(二) date: 2024/7/7 updated: 2024/7/7 author: cmdragon excerpt: 摘要:"本文详 ...

  2. VulnHub-DC-7渗透流程

    DC-7 kali:192.168.157.131 靶机:192.168.157.151 信息收集 nmap -sV -A -p- 192.168.157.151 虽然有robots.txt等敏感文件 ...

  3. Solo 开发者周刊 (第4期):什么样的新科技,能提高生活效率?

    这里会整合 Solo 社区每周推广内容.产品模块或活动投稿,每周五发布.在这期周刊中,我们将深入探讨开源软件产品的开发旅程,分享来自一线独立开发者的经验和见解.本杂志开源,欢迎投稿. 好文推荐 AI生 ...

  4. redis基本数据结构-列表

    redis基本数据结构-列表list 特性 每个列表键最多存储 2^32 - 1个字符串元素 元素在列表中有序 元素在列表中不唯一 向列表左侧添加元素 lpush key value lpush nu ...

  5. 聊天chat封装

    说明:连接状态,客户端ID,在线状态,连接中,当前聊天会话ID,当前聊天对象ID,总未读数, 聊天功能实现首先要保证当前用户已经登录状态 监听登录时更新会话列表 监听退出时更新会话列表 发起聊天的时候 ...

  6. 最简GIF解析代码gif_jumper,用于stb_image的小改进

    gif jumper gif支持多帧动画,但是没有存储总帧数,解析gif直到结束才能知道总帧数. 所以gif解析代码,要么采用链表,要么不停realloc()分配内存,stb_image的代码就是如此 ...

  7. 【进阶篇】一文搞清楚网页发起 HTTP 请求调用的完整过程

    目录 前言 一.HTTP协议 1.1基本概念 1.2工作原理 二.请求过程 2.1域名解析 2.2TCP 连接 2.3发送 HTTP 请求 2.4服务器应答 2.5响应内容 2.6关闭连接 三.客户端 ...

  8. 使用vxe-table组件,控制台报错:缺少必要的“{0}”参数,这可能会导致出现错误

    这是由于使用vxe表格,给列属性设置 type="html" 只需要开启存,需要启用column-config.useKey与row-config.useKey就可以了

  9. pytest数据驱动 pandas

    pytest数据驱动 pandas 主要过程:用pandas读取excel里面的数据,然后进行百度查询,并断言 pf = pd.read_excel('data_py.xlsx', usecols=[ ...

  10. Fiddler使用界面介绍-左侧会话面板

    左侧会话面板,是Fiddler抓取的请求数据展示