Golang Go语言中's assembler 03:function call

发布于 1周前 作者 h691938207 来自 Go语言

Go 1.1 Function Calls 中介绍了函数调用在编译&汇编层面的是实现, 其中比较特别的是 indirect call of func value. 新手在不知道这个点的情况下去看相关的汇编时很容易被卡住.

我们以如下代码为例子:

//go:noinline
func max(a, b int) int {
    if a > b {
        return a
    }
    return b
}

func main() { max(10, 20)

imax := max
imax(10, 20)

x := 1
y := 2
iadd := func(a, b int) int {
    return x + y + a + b
}
iadd(10, 20)

// 直接调用并不需要特殊实现, 即使是闭包
func(a, b int) int {
    return x + y + a + b
}(10, 20)

}

编译命令为 GOOS=linux GOARCH=amd64 GOSSAFUNC=main.main go21 build -gcflags=-l main.go, -l 用于告诉编译器不要进行 inline 优化. 反汇编命令为 x86_64-linux-gnu-objdump -D -S main > objdump.

main 函数对应的汇编:

cat -n objdump | grep main.main\>\: -A 65
138056	00000000004576a0 <main.main>:
138057	}
138058
138059	func main() {
138060	  4576a0:	49 3b 66 10          	cmp    0x10(%r14),%rsp
138061	  4576a4:	0f 86 97 00 00 00    	jbe    457741 <main.main+0xa1>
138062	  4576aa:	55                   	push   %rbp
138063	  4576ab:	48 89 e5             	mov    %rsp,%rbp
138064	  4576ae:	48 83 ec 38          	sub    $0x38,%rsp
138065		max(10, 20)
138066	  4576b2:	b8 0a 00 00 00       	mov    $0xa,%eax
138067	  4576b7:	bb 14 00 00 00       	mov    $0x14,%ebx
138068	  4576bc:	0f 1f 40 00          	nopl   0x0(%rax)
138069	  4576c0:	e8 bb ff ff ff       	call   457680 <main.max>
138070
138071		imax := max
138072		imax(10, 20)
138073	  4576c5:	48 8b 0d 74 49 01 00 	mov    0x14974(%rip),%rcx        # 46c040 <go:func.*+0x168>
138074	  4576cc:	b8 0a 00 00 00       	mov    $0xa,%eax
138075	  4576d1:	bb 14 00 00 00       	mov    $0x14,%ebx
138076	  4576d6:	48 8d 15 63 49 01 00 	lea    0x14963(%rip),%rdx        # 46c040 <go:func.*+0x168>
138077	  4576dd:	ff d1                	call   *%rcx
138078
138079		x := 1
138080		y := 2
138081		iadd := func(a, b int) int {
138082	  4576df:	44 0f 11 7c 24 20    	movups %xmm15,0x20(%rsp)
138083	  4576e5:	48 c7 44 24 30 00 00 	movq   $0x0,0x30(%rsp)
138084	  4576ec:	00 00
138085	  4576ee:	48 8d 0d 8b 00 00 00 	lea    0x8b(%rip),%rcx        # 457780 <main.main.func1>
138086	  4576f5:	48 89 4c 24 20       	mov    %rcx,0x20(%rsp)
138087	  4576fa:	48 c7 44 24 28 01 00 	movq   $0x1,0x28(%rsp)
138088	  457701:	00 00
138089	  457703:	48 c7 44 24 30 02 00 	movq   $0x2,0x30(%rsp)
138090	  45770a:	00 00
138091			return x + y + a + b
138092		}
138093		iadd(10, 20)
138094	  45770c:	48 8b 4c 24 20       	mov    0x20(%rsp),%rcx
138095	  457711:	b8 0a 00 00 00       	mov    $0xa,%eax
138096	  457716:	bb 14 00 00 00       	mov    $0x14,%ebx
138097	  45771b:	48 8d 54 24 20       	lea    0x20(%rsp),%rdx
138098	  457720:	ff d1                	call   *%rcx
138099
138100		func(a, b int) int {
138101			return x + y + a + b
138102		}(10, 20)
138103	  457722:	b8 01 00 00 00       	mov    $0x1,%eax
138104	  457727:	bb 02 00 00 00       	mov    $0x2,%ebx
138105	  45772c:	b9 0a 00 00 00       	mov    $0xa,%ecx
138106	  457731:	bf 14 00 00 00       	mov    $0x14,%edi
138107	  457736:	e8 25 00 00 00       	call   457760 <main.main.func2>
138108	}
138109	  45773b:	48 83 c4 38          	add    $0x38,%rsp
138110	  45773f:	5d                   	pop    %rbp
138111	  457740:	c3                   	ret
138112	func main() {
138113	  457741:	e8 9a ce ff ff       	call   4545e0 <runtime.morestack_noctxt.abi0>
138114	  457746:	e9 55 ff ff ff       	jmp    4576a0 <main.main>

直接调用

对 max 函数的直接调用是非常直观的, 对应 138066~138069 行. 首先将参数保存到两个寄存器, 再直接通过函数地址调用函数.

间接调用

但当我们将 max 赋值给一个变量再调用时, 即间接调用, 汇编代码就变得复杂起来了.

首先 rip 在 x64 中是一个非常特殊的寄存器, 永远等于下一个指令的地址. 所以 138073 行 mov 0x14974(%rip),%rcx # 46c040 <go:func.*+0x168> 是将 0x46c040(0x4576cc+0x14964) 的内容保存到寄存器 rcx.

定位到 0x46c040, 可以发现其属于 .rodata, 保存的内容是 457680, 也就是 main.max 在汇编的中地址.

cat -n objdump | grep 46c040\:
170516    46c040:       80 76 45 00             xorb   $0x0,0x45(%rsi)

cat -n objdump | grep main.max&gt;: -A 30 138019 0000000000457680 <main.max>: 138020 package main 138021 138022 //go:noinline 138023 func max(a, b int) int { 138024 if a > b { 138025 457680: 48 39 c3 cmp %rax,%rbx 138026 457683: 7d 01 jge 457686 <main.max+0x6> 138027 return a 138028 457685: c3 ret 138029 } 138030 return b 138031 457686: 48 89 d8 mov %rbx,%rax

那么 138077 行 call *%rcx 即是直接通过地址来调用 max 函数.

Go 1.1 Function Calls 中, 我们可以得知. 对于间接调用, 编译器会使用一块内存来保存函数地址和相关变量. 这么做主要是为了处理闭包, 即函数对外部变量的引用. 具体的前因后果可以参看原文.

这块内存的地址在调用函数前需要被保存到寄存器 rdx.

以 iadd(138079~138098) 为例, 上述逻辑会更为明显.

调用前在栈上分配 24 个字节, 0x20(%rsp) 用于保存函数地址, 0x28(%rsp) 和 0x30(%rsp) 用于保存引用的两个外部变量 x 和 y. 这块内存的地址随后又被保存到寄存器 rdx.

函数内基于寄存器 rdx, 偏移 8 个字节读取 x, 偏移 16 个字节读取到 y.

cat -n objdump | grep main.main.func1\>\: -A 30
138164  0000000000457780 <main.main.func1>:
138165      iadd := func(a, b int) int {
138166    457780:       48 8b 4a 08             mov    0x8(%rdx),%rcx
138167          return x + y + a + b
138168    457784:       48 03 4a 10             add    0x10(%rdx),%rcx
138169    457788:       48 01 c1                add    %rax,%rcx
138170    45778b:       48 8d 04 0b             lea    (%rbx,%rcx,1),%rax
138171    45778f:       c3                      ret

Link: https://github.com/j2gg0s/j2gg0s


Golang Go语言中's assembler 03:function call

更多关于Golang Go语言中's assembler 03:function call的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于Golang Go语言中's assembler 03:function call的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


在Golang(通常简称为Go)中,函数调用是编程中的基本操作之一,它允许一个函数执行另一个函数的代码。Go语言的汇编器(assembler)在处理函数调用时,会涉及一系列的低级操作,以确保正确的参数传递、栈管理以及控制流转移。

  1. 参数传递:在Go的汇编语言中,函数调用通常通过栈来传递参数。调用者负责将参数压入栈中,并按照被调用函数的参数列表顺序进行排列。被调用函数在栈上读取这些参数。

  2. 栈管理:函数调用会导致栈帧(stack frame)的创建和销毁。栈帧用于存储局部变量、函数参数以及返回地址等信息。Go的汇编器需要确保在函数调用前后栈的正确对齐和恢复。

  3. 控制流转移:函数调用会导致控制流从调用点转移到被调用函数的起始地址。这通常通过跳转指令(如CALL指令)实现。在函数返回时,控制流会跳转回调用点的下一条指令,这通常通过RET指令实现。

  4. 调用约定:Go语言有一套自己的调用约定(calling convention),它规定了参数传递、返回值处理以及栈帧布局等方面的规则。了解这些约定对于编写高效的汇编代码至关重要。

总的来说,Go语言的汇编器在处理函数调用时,需要精细地管理栈、参数和控制流,以确保程序的正确性和效率。对于高级Go程序员来说,虽然通常不需要直接编写汇编代码,但理解这些底层机制有助于更好地优化和理解Go程序的性能行为。

回到顶部