Go's Assembler 01: 阅读和理解 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
GopherDaily
V2EX    Go 编程语言

Go's Assembler 01: 阅读和理解

  •  
  •   GopherDaily 2023-09-24 22:46:16 +08:00 1472 次点击
    这是一个创建于 751 天前的主题,其中的信息可能已经有所发展或是发生改变。

    Go 在 Plan9 的基础上定义了自己的汇编语言. 代码经过编译后会生成对应的汇编语言, 随后根据目标平台生成精确的, 机器相关的指令. 具体可以参见 A Quick Guide to Go's Assembler.

    作为一个编译的门外汉来了解 Go 的编译逻辑的一个问题就是 Plan9 的资料稀缺, 导致理解 Go 的汇编结果时很容易卡在某个点. 所以我开了下脑洞, 既然 x64 的汇编资源非常全, 不如我们先看最终生成的 x64 汇编, 再来看 Go 汇编. X64 Cheat Sheet 是一份非常好的 X64 汇编入门文档, 可以按需阅读.

    全文使用 go1.21.1, 编译针对 linux/amd64.

    • 编译的命令为 GOOS=linux GOARCH=amd64 go21 build main.go,
    • 通过 objdump 获取 x64 汇编结果 objdump -j .text -S main > objdump,
    • 通过 go tool objdump 获取 Go 汇编结果 go21 tool objdump main > goobjdump.

    add

    //go:noinline func add(a, b int) (int, bool) { return a + b, true } 

    注解 //go:noinline 用于告诉编译器不要进行 inline 优化, 即避免编译器自动将调用这些函数的地方替换成函数代码.

    Go 在 1.17 从 stack-based calling convention 切换到了 register-based calling convention, 即之前通过 stack 在调用函数时传递参数和返回值, 这是 Plan9 的惯例, 之后通过寄存器传递参数和返回值, 带来了性能的提升.

    但在寄存器的使用上, Go 没有遵循 x64 的默认习俗. 调用者(caller) 将 add 的两个参数存放在寄存器 rax 和 rbx. 被调用者(callee) 将两个返回值存放在寄存器 rax 和 rbx.

    生成的 x64 汇编:

    cat -n objdump | grep "<main.add>:" -A 10 129310 0000000000457680 <main.add>: 129311 ; return a + b, true 129312 457680: 48 01 d8 addq %rbx, %rax ; 调用参数被保存在寄存器 rax 和 rbx 129313 457683: bb 01 00 00 00 movl $1, %ebx ; ebx 和 rbx 是同一个寄存器, ebx 对应前 4 字节, rbx 对应全部的 8 字节 129314 457688: c3 retq ; 返回值已经保存在寄存器 rax 和 rbx 内 

    对应的 Go 汇编:

    cat -n goobjdump | grep "TEXT main.add" -A 10 82843 TEXT main.add(SB) /Users/j2gg0s/go/src/github.com/j2gg0s/j2gg0s/examples/ssa/main.go 82844 main.go:5 0x457680 4801d8 ADDQ BX, AX 82845 main.go:5 0x457683 bb01000000 MOVL $0x1, BX 82846 main.go:5 0x457688 c3 RET 

    if

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

    生成的 x86 汇编:

    cat -n objdump | grep "<main.max>:" -A 30 129339 00000000004576a0 <main.max>: 129340 ; if a > b { 129341 4576a0: 48 39 c3 cmpq %rax, %rbx ; cmp 将 rbx-rax 的结果保存到条件寄存器 129342 4576a3: 7d 01 jge 0x4576a6 <main.max+0x6> ; 如果 cmp 的结果大于等于 0, 则跳转到对应地址的指令 129343 ; return a 129344 4576a5: c3 retq 129345 ; return b 129346 4576a6: 48 89 d8 movq %rbx, %rax ; 返回结果需要保存到 rax, 所以需要将 rbx 的值转移到 rax 129347 4576a9: c3 retq 

    对应的 Go 汇编:

    cat -n goobjdump | grep "TEXT main.max" -A 10 82848 TEXT main.max(SB) /Users/j2gg0s/go/src/github.com/j2gg0s/j2gg0s/examples/ssa/main.go 82849 main.go:10 0x4576a0 4839c3 CMPQ BX, AX 82850 main.go:10 0x4576a3 7d01 JGE 0x4576a6 82851 main.go:11 0x4576a5 c3 RET 82852 main.go:13 0x4576a6 4889d8 MOVQ BX, AX 82853 main.go:13 0x4576a9 c3 RET 

    for

    //go:noinline func sum(numbers []int) int { sum := 0 for i := 0; i < len(numbers); i++ { sum += numbers[i] } return sum } 

    生成的 x86 汇编:

    cat -n objdump | grep "<main.sum>:" -A 30 129371 00000000004576c0 <main.sum>: 129372 ; func sum(numbers []int) int { 129373 4576c0: 48 89 44 24 08 movq %rax, 8(%rsp) ; 将 rax 的值存放到 stack, TODO: 为什么需要这么做, 为什么是 8. 129374 4576c5: 31 c9 xorl %ecx, %ecx ; 清空寄存器 rcx 的前 4 字节 129375 4576c7: 31 d2 xorl %edx, %edx 129376 ; for i := 0; i < len(numbers); i++ { 129377 4576c9: eb 0a jmp 0x4576d5 <main.sum+0x15> 129378 ; sum += numbers[i] 129379 4576cb: 48 8b 34 c8 movq (%rax,%rcx,8), %rsi ; 将 numbers[i] 存在到 rsi, rax 是数组地址, rcx 是 i, 8 代表元素占 8 字节 129380 ; for i := 0; i < len(numbers); i++ { 129381 4576cf: 48 ff c1 incq %rcx 129382 ; sum += numbers[i] 129383 4576d2: 48 01 f2 addq %rsi, %rdx 129384 ; for i := 0; i < len(numbers); i++ { 129385 4576d5: 48 39 cb cmpq %rcx, %rbx ; 判断 i < len(numbers), rbx 保存了数组大小, 由调用者赋值 129386 4576d8: 7f f1 jg 0x4576cb <main.sum+0xb> 129387 ; return sum 129388 4576da: 48 89 d0 movq %rdx, %rax ; 返回结果需要存储在 rax 129389 4576dd: c3 retq 

    对应的 Go 汇编

    cat -n goobjdump | grep "TEXT main.sum" -A 10 82855 TEXT main.sum(SB) /Users/j2gg0s/go/src/github.com/j2gg0s/j2gg0s/examples/ssa/main.go 82856 main.go:17 0x4576c0 4889442408 MOVQ AX, 0x8(SP) 82857 main.go:17 0x4576c5 31c9 XORL CX, CX 82858 main.go:17 0x4576c7 31d2 XORL DX, DX 82859 main.go:19 0x4576c9 eb0a JMP 0x4576d5 82860 main.go:20 0x4576cb 488b34c8 MOVQ 0(AX)(CX*8), SI 82861 main.go:19 0x4576cf 48ffc1 INCQ CX 82862 main.go:20 0x4576d2 4801f2 ADDQ SI, DX 82863 main.go:19 0x4576d5 4839cb CMPQ BX, CX 82864 main.go:19 0x4576d8 7ff1 JG 0x4576cb 82865 main.go:22 0x4576da 4889d0 MOVQ DX, AX 

    main

    func main() { add(10, 20) max(10, 20) sum([]int{10, 20}) } 

    生成的 x86 汇编:

    cat -n objdump | grep "<main.main>:" -A 60 129393 00000000004576e0 <main.main>: 129394 ; func main() { 129395 4576e0: 49 3b 66 10 cmpq 16(%r14), %rsp ; Go 用于判断 stack 是否需要扩容的方法 129396 4576e4: 76 56 jbe 0x45773c <main.main+0x5c> 129397 4576e6: 55 pushq %rbp ; 在函数执行之前, 需要将 base pointer 暂存在 stack 129398 4576e7: 48 89 e5 movq %rsp, %rbp ; 并将 stack pointer 的值赋给 bp 129399 4576ea: 48 83 ec 28 subq $40, %rsp ; 在 stack 中预先分配 40 字节 129400 ; add(10, 20) 129401 4576ee: b8 0a 00 00 00 movl $10, %eax 129402 4576f3: bb 14 00 00 00 movl $20, %ebx 129403 4576f8: e8 83 ff ff ff callq 0x457680 <main.add> 129404 ; max(10, 20) 129405 4576fd: b8 0a 00 00 00 movl $10, %eax 129406 457702: bb 14 00 00 00 movl $20, %ebx 129407 457707: e8 94 ff ff ff callq 0x4576a0 <main.max> 129408 ; sum([]int{10, 20}) 129409 45770c: 44 0f 11 7c 24 18 movups %xmm15, 24(%rsp) ; xmm15 是 16 字节的寄存器, 配合 movups 用于清空栈中 24~40 字节 129410 457712: 48 c7 44 24 18 0a 00 00 00 movq $10, 24(%rsp) 129411 45771b: 48 c7 44 24 20 14 00 00 00 movq $20, 32(%rsp) 129412 457724: 48 8d 44 24 18 leaq 24(%rsp), %rax ; 将数组的地址存放到寄存器 rax 129413 457729: bb 02 00 00 00 movl $2, %ebx ; 将数据的元素个数存放到 rbx 129414 45772e: 48 89 d9 movq %rbx, %rcx 129415 457731: e8 8a ff ff ff callq 0x4576c0 <main.sum> 129416 ; } 129417 457736: 48 83 c4 28 addq $40, %rsp ; 释放在函数最初分配给栈的 40 字节 129418 45773a: 5d popq %rbp ; 恢复 base pointer 129419 45773b: c3 retq 129420 ; func main() { 129421 45773c: 0f 1f 40 00 nopl (%rax) 129422 457740: e8 9b ce ff ff callq 0x4545e0 <runtime.morestack_noctxt.abi0> 129423 457745: eb 99 jmp 0x4576e0 <main.main> 

    细心的同学可能会思考为什么调用 main.sum 之前初始化数组是用 24(%rsp). 这是因为虽然 go's calling convention 已经从 stack-base 切换到了 regisger-base. 但是可能出于兼容或者上面目的, 依然在 stack 为通过 register 传递的参数和返回值保留空间.

    Ref Function call argument and result passing:

    Beyond the arguments and results passed on the stack, the caller also reserves spill space on the stack for all register-based arguments (but does not populate this space).

    对应的 Go 汇编:

    cat -n goobjdump | grep "TEXT main.main" -A 1000 82868 TEXT main.main(SB) /Users/j2gg0s/go/src/github.com/j2gg0s/j2gg0s/examples/goasm/main.go 82869 main.go:25 0x4576e0 493b6610 CMPQ SP, 0x10(R14) 82870 main.go:25 0x4576e4 7656 JBE 0x45773c 82871 main.go:25 0x4576e6 55 PUSHQ BP 82872 main.go:25 0x4576e7 4889e5 MOVQ SP, BP 82873 main.go:25 0x4576ea 4883ec28 SUBQ $0x28, SP 82874 main.go:26 0x4576ee b80a000000 MOVL $0xa, AX 82875 main.go:26 0x4576f3 bb14000000 MOVL $0x14, BX 82876 main.go:26 0x4576f8 e883ffffff CALL main.add(SB) 82877 main.go:27 0x4576fd b80a000000 MOVL $0xa, AX 82878 main.go:27 0x457702 bb14000000 MOVL $0x14, BX 82879 main.go:27 0x457707 e894ffffff CALL main.max(SB) 82880 main.go:28 0x45770c 440f117c2418 MOVUPS X15, 0x18(SP) 82881 main.go:28 0x457712 48c74424180a000000 MOVQ $0xa, 0x18(SP) 82882 main.go:28 0x45771b 48c744242014000000 MOVQ $0x14, 0x20(SP) 82883 main.go:28 0x457724 488d442418 LEAQ 0x18(SP), AX 82884 main.go:28 0x457729 bb02000000 MOVL $0x2, BX 82885 main.go:28 0x45772e 4889d9 MOVQ BX, CX 82886 main.go:28 0x457731 e88affffff CALL main.sum(SB) 82887 main.go:29 0x457736 4883c428 ADDQ $0x28, SP 82888 main.go:29 0x45773a 5d POPQ BP 82889 main.go:29 0x45773b c3 RET 82890 main.go:25 0x45773c 0f1f4000 NOPL 0(AX) 82891 main.go:25 0x457740 e89bceffff CALL runtime.morestack_noctxt.abi0(SB) 82892 main.go:25 0x457745 eb99 JMP main.main(SB) 
    第 1 条附言    2023-09-25 17:14:17 +08:00
    水平不够,写不明白 :<
    5 条回复    2023-09-25 18:50:09 +08:00
    elechi
        1
    elechi  
       2023-09-25 13:43:42 +08:00
    水平不够,看不懂
    kingofzihua
        2
    kingofzihua  
       2023-09-25 14:53:12 +08:00
    水平不够,看不懂
    Atsushi
        3
    Atsushi  
       2023-09-25 16:19:41 +08:00
    水平不够,看不懂
    EvanPan
        4
    EvanPan  
       2023-09-25 17:15:49 +08:00
    水平不够,看不懂
    hancai
        5
    hancai  
       2023-09-25 18:50:09 +08:00
    水平不够,看不懂
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     1077 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 25ms UTC 23:05 PVG 07:05 LAX 16:05 JFK 19:05
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86