Go语言汇编入门

2023-05-16

虽然在前面的文章中,分析代码已经接触了一些Go语言的汇编代码的注解,比如在slice和Go语言笔记以及以后的文章中都会使用到Go汇编。本章主要讲解Go汇编大致流程的框架,对于刚接触Go汇编理解Go函数栈是比较友好的,结合具体实例分析让人通俗易懂。当然本人也是最近才学Go汇编,有讲解不当的地方希望各位Gopher能给出指点,愿闻其详。

目录

前提知识点

Go语言函数调用栈理论

具体代码分析

plan9汇编

平台相关的汇编(x86汇编)

编译

gdb反编译

分析

总结


前提知识点

Linux进程在内存中的布局主要分为4个区域:代码区,数据区,堆和栈。具体请看Linux进程内存管理。

这里说一下栈,函数调用栈简称栈,在程序运行过程中,不管是函数的执行还是函数调用,栈都起着非常重要的作用,它主要被用来:

  • 保存函数的局部变量;
  • 向被调用函数传递参数;
  • 返回函数的返回值;
  • 保存函数的返回地址。返回地址是指从被调用函数返回后调用者应该继续执行的指令地址。

每个函数在执行过程中都需要使用一块栈内存用来保存上述这些值,我们称这块栈内存为某函数的栈帧(stack frame)。当发生函数调用时,因为调用者还没有执行完,其栈内存中保存的数据还有用,所以被调用函数不能覆盖调用者的栈帧,只能把被调用函数的栈帧“push”到栈上,等被调函数执行完成后再把其栈帧从栈上“pop”出去,这样,栈的大小就会随函数调用层级的增加而生长,随函数的返回而缩小,也就是说函数调用层级越深,消耗的栈空间就越大。栈的生长和收缩都是自动的,由编译器插入的代码自动完成,因此位于栈内存中的函数局部变量所使用的内存随函数的调用而分配,随函数的返回而自动释放,所以程序员不管是使用有垃圾回收还是没有垃圾回收的高级编程语言都不需要自己释放局部变量所使用的内存,这一点与堆上分配的内存截然不同。

AMD64 CPU提供了2个与栈相关的寄存器:

  • rsp寄存器,始终指向函数调用栈栈顶。
  • rbp寄存器,一般用来指向函数栈帧的起始位置。

有些编译器比如gcc会把参数和返回值放在特定寄存器中而不是栈中,go语言中函数的参数和返回值都是放在栈上的。

Go语言函数调用栈理论

刚开始接触go语言汇编是看饶大对汇编的简单介绍,刚开始看也不是很理解调用者与被调用者的栈帧示意图:

æ å¸§

在曹大的汇编解析中解释返回地址为:从原理上来讲,如果当前函数调用了其它函数,那么 return addr 也是在 caller 的栈上的,不过往栈上插 return addr 的过程是由 CALL 指令完成的。也就是说当前函数没有调用其他函数,则caller栈上没有返回地址内存的。此外需要注意的是,caller BP 是在编译期由编译器插入的,用户手写Go汇编代码时,计算 frame size 时是不包括这个 caller BP 部分的。

曹大也对plan9的寄存器总结如下:

  • 寄存器是CPU内部的存储单元,用于存放从内存读取而来的数据(包括指令)和CPU运算的中间结果,关于寄存器相关请看Linux 上下文切换 寄存器 内核线程 用户线程。
  • 在代码中看到的SB,SP,AX,CX等等这些都是Go汇编硬件寄存器。但是Go汇编语言中使用的寄存器的名字与AMD64不太一样,它们对应关系:
AMD64  rax rbx rcx rdx rdi rsi rbp rsp r8 r9 r10 r11 r12 r13 r14 rip
Plan9  AX  BX  CX  DX  DI  SI  BP  SP  R8 R9 R10 R11 R12 R13 R14 PC
  1. rip寄存器:存放的是CPU即将执行的下一条指令在内存中的地址。rip寄存器的值是CPU自动控制的,不需要我们用指令去修改。
  2. rsp 栈顶寄存器和rbp栈基址寄存器:这两个寄存器都跟函数调用栈有关,其中rsp寄存器一般用来存放函数调用栈的栈顶地址,而rbp寄存器通常用来存放函数的栈帧起始地址,编译器一般使用这两个寄存器加一定偏移的方式来访问函数局部变量或函数参数。
  3. 其他通用寄存器:rax, rbx, rcx, rdx, rsi, rdi, r8, r9, r10, r11, r12, r13, r14寄存器。CPU对这些寄存器的用途没有做特殊规定,程序员和编译器可以自定义其用途。
  • 除了这些跟AMD64 CPU硬件寄存器一一对应的寄存器外,Go汇编还引入了几个没有任何硬件寄存器与之对应的虚拟寄存器,这些寄存器一般用来存放内存地址,引入它们的主要目的是为了方便程序员和编译器用来定位内存中的代码和数据:

FP: Frame pointer: arguments and locals.
PC: Program counter: jumps and branches.
SB: Static base pointer: global symbols.
SP: Stack pointer: top of stack.

所以上面硬件寄存器伪寄存器名字相同的有PC和SP。go tool compile -S生成的汇编代码中,常见的寄存器解释如下:
PC:程序计数器,指下一步要执行的程序,伪寄存器PC和硬件寄存器PC作用差不多。
SP:指向当前栈帧的栈顶。但是生成的汇编代码中都是硬件寄存器不是伪寄存器。
BP:指向当前栈帧的栈底,函数栈的起始位置(Go编译器会将函数栈空间自动加8,用于存储BP寄存器,跳过这8字节后才是函数栈上局部变量的内存)。
SB:保存的值就是进程在内存中代码区的起始地址,它主要用来定位全局符号。go汇编中的函数定义、函数调用、全局变量定义以及对其引用会用到这个SB虚拟寄存器,比如runtime.growslice(SB) 函数是全局的,os.Stdout(SB) 常量是全局的。
FP:主要用来引用函数参数。go语言规定函数调用时参数都必须放在栈上,比如被调用函数使用 first_arg+0(FP) 来引用调用者传递进来的第一个参数,用second_arg+8(FP)来引用第二个参数 ,以此类推,这里的first_arg和second_arg仅仅是一个帮助我们阅读源代码的符号,对编译器来说无实际意义,+0和+8表示相对于FP寄存器的偏移量。

具体代码分析

plan9汇编

Tips:以下代码在go1.12.6 windows/amd64版本下测试分析,版本不同在分析源码的时候略有不同。

Go语言生成的plan9汇编代码命令:

go tool compile -S test.go

还有其他命令方式,请转文末链接。

通过具体实例对调用者与被调用者的栈帧理解,代码:

func main() {
	n := 5
	f(n)
}
 
func f(n int) (r int) {
	m := 5
	m = n + m
	r = 2 * m
	fmt.Println(r)
	return r
}

通过命令生成汇编,其中main函数汇编:

"".main STEXT size=66 args=0x0 locals=0x18
        0x0000 00000 (test2.go:5)       TEXT    "".main(SB), ABIInternal, $24-0 //24是函数栈大小,0是参数和返回值的大小
        0x0000 00000 (test2.go:5)       MOVQ    TLS, CX
        0x0009 00009 (test2.go:5)       MOVQ    (CX)(TLS*2), CX
        0x0010 00016 (test2.go:5)       CMPQ    SP, 16(CX)
        0x0014 00020 (test2.go:5)       JLS     59
        0x0016 00022 (test2.go:5)       SUBQ    $24, SP //SP移向栈顶
        0x001a 00026 (test2.go:5)       MOVQ    BP, 16(SP)
        0x001f 00031 (test2.go:5)       LEAQ    16(SP), BP //BP在栈底占8个字节
        0x0024 00036 (test2.go:5)       FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x0024 00036 (test2.go:5)       FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x0024 00036 (test2.go:5)       FUNCDATA        $3, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x0024 00036 (test2.go:13)      PCDATA  $2, $0
        0x0024 00036 (test2.go:13)      PCDATA  $0, $0
        0x0024 00036 (test2.go:13)      MOVQ    $5, (SP) // 将参数5放到(SP)的位置
        0x002c 00044 (test2.go:13)      CALL    "".f(SB)
        0x0031 00049 (test2.go:14)      MOVQ    16(SP), BP //返回值16(SP)放到BP
        0x0036 00054 (test2.go:14)      ADDQ    $24, SP //SP移向栈底,与上面第7行对应
        0x003a 00058 (test2.go:14)      RET
        0x003b 00059 (test2.go:14)      NOP
        0x003b 00059 (test2.go:5)       PCDATA  $0, $-1
        0x003b 00059 (test2.go:5)       PCDATA  $2, $-1
        0x003b 00059 (test2.go:5)       CALL    runtime.morestack_noctxt(SB) //扩栈处理
        0x0040 00064 (test2.go:5)       JMP     0
        0x0000 65 48 8b 0c 25 28 00 00 00 48 8b 89 00 00 00 00  eH..%(...H......
        0x0010 48 3b 61 10 76 25 48 83 ec 18 48 89 6c 24 10 48  H;a.v%H...H.l$.H
        0x0020 8d 6c 24 10 48 c7 04 24 05 00 00 00 e8 00 00 00  .l$.H..$........
        0x0030 00 48 8b 6c 24 10 48 83 c4 18 c3 e8 00 00 00 00  .H.l$.H.........
        0x0040 eb be                                            ..
        rel 12+4 t=16 TLS+0
        rel 45+4 t=8 "".f+0
        rel 60+4 t=8 runtime.morestack_noctxt+0
  • 第2行$24-0如上所解释,0是main函数的参数和返回值的大小,24是函数main的栈大小,但是为什么是24,不应该是8吗?

因为BP占8个字节,n占8个字节,f函数的返回值占8个,所以是24个。但是有一种情况,函数内没有栈变量,那么栈的大小是0,这时并没有BP(写个测试函数试试)。但是还有一种情况,就是函数内变量很多,那么栈大小的分配并不是按几个变量大小+BP大小来算的,而是分配一块比较大的栈内存,会存一些临时变量(比如下面的f函数)。

  • 第3行MOVQ    TLS, CX中,指令MOVQ的Q表示操作数的宽度是64bit即8byte,还有B(8bit)、W(16bit)、D(32bit)。

f函数汇编:

"".f STEXT size=183 args=0x10 locals=0x60
        0x0000 00000 (test2.go:16)      TEXT    "".f(SB), ABIInternal, $96-16 //函数栈大小96,参数和返回值大小是16
        0x0000 00000 (test2.go:16)      MOVQ    TLS, CX
        0x0009 00009 (test2.go:16)      MOVQ    (CX)(TLS*2), CX
        0x0010 00016 (test2.go:16)      CMPQ    SP, 16(CX)
        0x0014 00020 (test2.go:16)      JLS     173
        0x001a 00026 (test2.go:16)      SUBQ    $96, SP //SP移向栈顶
        0x001e 00030 (test2.go:16)      MOVQ    BP, 88(SP)
        0x0023 00035 (test2.go:16)      LEAQ    88(SP), BP //栈底8个字节是BP
        0x0028 00040 (test2.go:16)      FUNCDATA        $0, gclocals·69c1753bd5f81501d95132d08af04464(SB)
        0x0028 00040 (test2.go:16)      FUNCDATA        $1, gclocals·568470801006e5c0dc3947ea998fe279(SB)
        0x0028 00040 (test2.go:16)      FUNCDATA        $3, gclocals·bfec7e55b3f043d1941c093912808913(SB)
        0x0028 00040 (test2.go:16)      FUNCDATA        $4, "".f.stkobj(SB)
        0x0028 00040 (test2.go:18)      PCDATA  $2, $0
        0x0028 00040 (test2.go:18)      PCDATA  $0, $0
        0x0028 00040 (test2.go:18)      MOVQ    "".n+104(SP), AX //AX = 参数n = 5
        0x002d 00045 (test2.go:18)      ADDQ    $5, AX // AX = AX + 5 = 10
        0x0031 00049 (test2.go:19)      SHLQ    $1, AX // AX = AX << 1 = 20
        0x0034 00052 (test2.go:19)      MOVQ    AX, ""..autotmp_19+64(SP) // 把AX(20)放到栈变量
        0x0039 00057 (test2.go:20)      MOVQ    AX, (SP) // 将AX(20)作为convT64参数
        0x003d 00061 (test2.go:20)      CALL    runtime.convT64(SB) //调用函数
        0x0042 00066 (test2.go:20)      PCDATA  $2, $1
        0x0042 00066 (test2.go:20)      MOVQ    8(SP), AX
        0x0047 00071 (test2.go:20)      PCDATA  $0, $1
        0x0047 00071 (test2.go:20)      XORPS   X0, X0
        0x004a 00074 (test2.go:20)      MOVUPS  X0, ""..autotmp_13+72(SP)
        0x004f 00079 (test2.go:20)      PCDATA  $2, $2
        0x004f 00079 (test2.go:20)      LEAQ    type.int(SB), CX
        0x0056 00086 (test2.go:20)      PCDATA  $2, $1
        0x0056 00086 (test2.go:20)      MOVQ    CX, ""..autotmp_13+72(SP)
        0x005b 00091 (test2.go:20)      PCDATA  $2, $0
        0x005b 00091 (test2.go:20)      MOVQ    AX, ""..autotmp_13+80(SP)
        0x0060 00096 (test2.go:20)      XCHGL   AX, AX
        0x0061 00097 ($GOROOT\src\fmt\print.go:275)     PCDATA  $2, $1
        0x0061 00097 ($GOROOT\src\fmt\print.go:275)     MOVQ    os.Stdout(SB), AX
        0x0068 00104 ($GOROOT\src\fmt\print.go:275)     PCDATA  $2, $2
        0x0068 00104 ($GOROOT\src\fmt\print.go:275)     LEAQ    go.itab.*os.File,io.Writer(SB), CX
        0x006f 00111 ($GOROOT\src\fmt\print.go:275)     PCDATA  $2, $1
        0x006f 00111 ($GOROOT\src\fmt\print.go:275)     MOVQ    CX, (SP)
        0x0073 00115 ($GOROOT\src\fmt\print.go:275)     PCDATA  $2, $0
        0x0073 00115 ($GOROOT\src\fmt\print.go:275)     MOVQ    AX, 8(SP)
        0x0078 00120 ($GOROOT\src\fmt\print.go:275)     PCDATA  $2, $1
        0x0078 00120 ($GOROOT\src\fmt\print.go:275)     PCDATA  $0, $0
        0x0078 00120 ($GOROOT\src\fmt\print.go:275)     LEAQ    ""..autotmp_13+72(SP), AX
        0x007d 00125 ($GOROOT\src\fmt\print.go:275)     PCDATA  $2, $0
        0x007d 00125 ($GOROOT\src\fmt\print.go:275)     MOVQ    AX, 16(SP)
        0x0082 00130 ($GOROOT\src\fmt\print.go:275)     MOVQ    $1, 24(SP)
        0x008b 00139 ($GOROOT\src\fmt\print.go:275)     MOVQ    $1, 32(SP)
        0x0094 00148 ($GOROOT\src\fmt\print.go:275)     CALL    fmt.Fprintln(SB)
        0x0099 00153 (test2.go:21)      MOVQ    ""..autotmp_19+64(SP), AX 
        0x009e 00158 (test2.go:21)      MOVQ    AX, "".r+112(SP) //将栈变量20放到返回值
        0x00a3 00163 (test2.go:21)      MOVQ    88(SP), BP
        0x00a8 00168 (test2.go:21)      ADDQ    $96, SP //SP移向栈底
        0x00ac 00172 (test2.go:21)      RET
        0x00ad 00173 (test2.go:21)      NOP
        0x00ad 00173 (test2.go:16)      PCDATA  $0, $-1
        0x00ad 00173 (test2.go:16)      PCDATA  $2, $-1
        0x00ad 00173 (test2.go:16)      CALL    runtime.morestack_noctxt(SB)
        0x00b2 00178 (test2.go:16)      JMP     0
        0x0000 65 48 8b 0c 25 28 00 00 00 48 8b 89 00 00 00 00  eH..%(...H......
        0x0010 48 3b 61 10 0f 86 93 00 00 00 48 83 ec 60 48 89  H;a.......H..`H.
        0x0020 6c 24 58 48 8d 6c 24 58 48 8b 44 24 68 48 83 c0  l$XH.l$XH.D$hH..
        0x0030 05 48 d1 e0 48 89 44 24 40 48 89 04 24 e8 00 00  .H..H.D$@H..$...
        0x0040 00 00 48 8b 44 24 08 0f 57 c0 0f 11 44 24 48 48  ..H.D$..W...D$HH
        0x0050 8d 0d 00 00 00 00 48 89 4c 24 48 48 89 44 24 50  ......H.L$HH.D$P
        0x0060 90 48 8b 05 00 00 00 00 48 8d 0d 00 00 00 00 48  .H......H......H
        0x0070 89 0c 24 48 89 44 24 08 48 8d 44 24 48 48 89 44  ..$H.D$.H.D$HH.D
        0x0080 24 10 48 c7 44 24 18 01 00 00 00 48 c7 44 24 20  $.H.D$.....H.D$
        0x0090 01 00 00 00 e8 00 00 00 00 48 8b 44 24 40 48 89  .........H.D$@H.
        0x00a0 44 24 70 48 8b 6c 24 58 48 83 c4 60 c3 e8 00 00  D$pH.l$XH..`....
        0x00b0 00 00 e9 49 ff ff ff                             ...I...
        rel 12+4 t=16 TLS+0
        rel 62+4 t=8 runtime.convT64+0
        rel 82+4 t=15 type.int+0
        rel 100+4 t=15 os.Stdout+0
        rel 107+4 t=15 go.itab.*os.File,io.Writer+0
        rel 149+4 t=8 fmt.Fprintln+0
        rel 174+4 t=8 runtime.morestack_noctxt+0

对于Go语言的内部函数,go tool compile -S是不能对其进行汇编输出。所以上面convT64和Fprintln函数就不深追了,根据上面的调用者与被调用者的栈帧示意图,整理了栈内存真实状况如下:

可以结合上图与main和f汇编代码来分析,就可以比较好理解调用者与被调用者的关系。

平台相关的汇编(x86汇编)

Tips:以下代码在go1.13 linux/amd64版本下测试分析,版本不同在分析源码的时候略有不同。

上面的方法是将代码生成plan9汇编代码。同时,也可以将代码编译成可执行程序(机器码),再使用gdb工具将机器码程序反编译成汇编代码,也是平台相关的汇编代码。

示例代码:

package main

func sum(a, b int) int {
        c := a + b

        return c
}

func main() {
    sum(1, 2)
}

编译

go build -gcflags "-N -l" test2.go

指定 -gcflags "-N -l" 关闭编译器优化,否则编译器可能把对sum函数优化成内联函数。

产生test2可执行文件。

gdb反编译

[root@localhost testcode]# gdb test2
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-51.el7
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /testcode/test2...done.
warning: File "/usr/local/go/src/runtime/runtime-gdb.py" auto-loading has been declined by your `auto-load safe-path' set to "$debugdir:$datadir/auto-load:/usr/bin/mono-gdb.py".
To enable execution of this file add
        add-auto-load-safe-path /usr/local/go/src/runtime/runtime-gdb.py
line to your configuration file "/root/.gdbinit".
To completely disable this security protection add
        set auto-load safe-path /
line to your configuration file "/root/.gdbinit".
For more information about this security protection see the
"Auto-loading safe path" section in the GDB manual.  E.g., run from the shell:
        info "(gdb)Auto-loading safe path"
  • 解决warning?

若第一次使用gdb调试go二进制程序,可能会出现上面的那个warning,那是跟调试runtime相关有关系,对下面调试没有关系。当然可以通过以上提示,新建/root/.gdbinit文件,内容设置如下:

add-auto-load-safe-path /usr/local/go/src/runtime/runtime-gdb.py
set auto-load safe-path /

重新打开gdb:

root@localhost testcode]# gdb test2         
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-51.el7
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /testcode/test2...done.
Loading Go Runtime support.

warning消失,Loading Go Runtime support.载入runtime相关支持。

(gdb) disas 'main.main'   # 反编译main.main函数
Dump of assembler code for function main.main:
   0x0000000000452370 <+0>:     mov    %fs:0xfffffffffffffff8,%rcx
   0x0000000000452379 <+9>:     cmp    0x10(%rcx),%rsp
   0x000000000045237d <+13>:    jbe    0x4523ad <main.main+61>
   0x000000000045237f <+15>:    sub    $0x20,%rsp
   0x0000000000452383 <+19>:    mov    %rbp,0x18(%rsp)
   0x0000000000452388 <+24>:    lea    0x18(%rsp),%rbp
   0x000000000045238d <+29>:    movq   $0x1,(%rsp)
   0x0000000000452395 <+37>:    movq   $0x2,0x8(%rsp)
   0x000000000045239e <+46>:    callq  0x452330 <main.sum>
   0x00000000004523a3 <+51>:    mov    0x18(%rsp),%rbp
   0x00000000004523a8 <+56>:    add    $0x20,%rsp
   0x00000000004523ac <+60>:    retq   
   0x00000000004523ad <+61>:    callq  0x449ea0 <runtime.morestack_noctxt>
   0x00000000004523b2 <+66>:    jmp    0x452370 <main.main>
End of assembler dump.
(gdb) disas 'main.sum'   # 反编译main.sum函数
Dump of assembler code for function main.sum:
   0x0000000000452330 <+0>:     sub    $0x10,%rsp
   0x0000000000452334 <+4>:     mov    %rbp,0x8(%rsp)
   0x0000000000452339 <+9>:     lea    0x8(%rsp),%rbp
   0x000000000045233e <+14>:    movq   $0x0,0x28(%rsp)
   0x0000000000452347 <+23>:    mov    0x18(%rsp),%rax
   0x000000000045234c <+28>:    add    0x20(%rsp),%rax
   0x0000000000452351 <+33>:    mov    %rax,(%rsp)
   0x0000000000452355 <+37>:    mov    %rax,0x28(%rsp)
   0x000000000045235a <+42>:    mov    0x8(%rsp),%rbp
   0x000000000045235f <+47>:    add    $0x10,%rsp
   0x0000000000452363 <+51>:    retq   
End of assembler dump.

得到main和sum函数的汇编代码。其中第一列是内存地址,在下面分析中会遇到。

分析

(gdb) b *0x0000000000452370          # 在main汇编代码第一条指令设置断点
Breakpoint 1 at 0x452370: file /testcode/test2.go, line 9.
(gdb) r            # 运行程序
Starting program: /testcode/test2 

Breakpoint 1, main.main () at /testcode/test2.go:9
9       func main() {
(gdb) disas    # =>表示当前block的地方
Dump of assembler code for function main.main:
=> 0x0000000000452370 <+0>:     mov    %fs:0xfffffffffffffff8,%rcx
   0x0000000000452379 <+9>:     cmp    0x10(%rcx),%rsp
   0x000000000045237d <+13>:    jbe    0x4523ad <main.main+61>
   0x000000000045237f <+15>:    sub    $0x20,%rsp
   0x0000000000452383 <+19>:    mov    %rbp,0x18(%rsp)
   0x0000000000452388 <+24>:    lea    0x18(%rsp),%rbp
   0x000000000045238d <+29>:    movq   $0x1,(%rsp)
   0x0000000000452395 <+37>:    movq   $0x2,0x8(%rsp)
   0x000000000045239e <+46>:    callq  0x452330 <main.sum>
   0x00000000004523a3 <+51>:    mov    0x18(%rsp),%rbp
   0x00000000004523a8 <+56>:    add    $0x20,%rsp
   0x00000000004523ac <+60>:    retq   
   0x00000000004523ad <+61>:    callq  0x449ea0 <runtime.morestack_noctxt>
   0x00000000004523b2 <+66>:    jmp    0x452370 <main.main>
End of assembler dump.
(gdb) i r rbp rsp rip      # block时,i r查看rbp,rsp和rip寄存器的值
rbp            0xc000032758     0xc000032758
rsp            0xc000032758     0xc000032758
rip            0x452370 0x452370 <main.main>

此时进程内存分布大致如下:

此时rip=0x452370还未执行,但是cpu下一条指令就会执行到。下面开始执行main.main函数的第一个指令0x452370,

main前3行汇编代码不关注(栈检查和抢占调度有关);main汇编代码第4行为main函数预留32字节栈空间,用来存放临时变量,调用sum函数的参数和返回值,rsp下移32字节;main汇编代码第5行将调用者的rbp保留在main函数栈中;main汇编代码第6行lea间接寻址指令,将rbp指向24(%rsp)位置的地址,此时进程内存分布如下:

main汇编代码第7行将数字1放到(%rsp)位置作为第一个参数;main汇编代码第8行将数字2放到8(%rsp)位置作为第二个参数,此时进程内存分布如下:

main汇编代码第9行开始调用main.sum函数,call指令有点特殊,刚开始执行它的时候rip指向的是call指令的下一条指令,也就是说rip寄存器的值是0x4523a3这个地址,但在call指令执行过程中,call指令会把当前rip的值(0x4523a3)入栈(入栈的位置就是返回地址),栈顶rsp也会下移到返回地址位置,然后把rip的值修改为call指令后面的操作数,这里是0x452330,也就是sum函数第一条指令的地址,这样cpu就会跳转到sum函数去执行。

与call有个相似的命令:JMP。但是JMP指令不会将rip当前值压栈,栈顶rsp调整,它是直接跳转到另一个地方运行,不会返回,一般是强制跳转。而call通过和ret指令返回配对使用。

此时的进程内存分布:

  • 怎么确认main函数rbp的地址?

最直接方法是从上面地址0xc000032578-0x8=0xc000032570。也可以使用gdb在sum汇编代码第一条指令地址打个断点,并观察此时的rbp,rsp和rip的值:

(gdb) b *0x452330            # 在sum汇编代码第一行设置断点
Breakpoint 2 at 0x452330: file /testcode/test2.go, line 3.
(gdb) c                  # 继续,跳过第1个断点
Continuing.

Breakpoint 2, main.sum (a=1, b=2, ~r2=824633835704) at /testcode/test2.go:3
3       func sum(a, b int) int {
(gdb) disas             # =>表示当前block的地方
Dump of assembler code for function main.sum:
=> 0x0000000000452330 <+0>:     sub    $0x10,%rsp
   0x0000000000452334 <+4>:     mov    %rbp,0x8(%rsp)
   0x0000000000452339 <+9>:     lea    0x8(%rsp),%rbp
   0x000000000045233e <+14>:    movq   $0x0,0x28(%rsp)
   0x0000000000452347 <+23>:    mov    0x18(%rsp),%rax
   0x000000000045234c <+28>:    add    0x20(%rsp),%rax
   0x0000000000452351 <+33>:    mov    %rax,(%rsp)
   0x0000000000452355 <+37>:    mov    %rax,0x28(%rsp)
   0x000000000045235a <+42>:    mov    0x8(%rsp),%rbp
   0x000000000045235f <+47>:    add    $0x10,%rsp
   0x0000000000452363 <+51>:    retq   
End of assembler dump.
(gdb) i r rbp rsp rip    # block时,i r查看rbp,rsp和rip寄存器的值
rbp            0xc000032750     0xc000032750
rsp            0xc000032730     0xc000032730
rip            0x452330 0x452330 <main.sum>

 

sum汇编代码第1行预留16字节空间,rsp下移16字节;sum汇编代码第2行将调用者main的rbp放到8(%rsp)位置;sum汇编代码第3行将rbp指向8(%rsp)位置的地址,此时进程内存分布如下:

sum汇编代码第4行把数字0放到40(%rsp)即main函数栈帧的返回值位置;sum汇编代码第5行24(%rsp)即在main函数栈帧中的1赋值为rax寄存器;sum汇编代码第6行将32(%rsp)即main函数栈帧中2和rax寄存器值相加结果赋值给rax寄存器;sum汇编代码第7行将rax结果放到(%rsp)位置即临时变量c;sum汇编代码第8行将rax结果放到main函数栈帧40(%rsp)即返回值的位置,此时进程内存分布如下:

sum汇编代码第9行将rbp指向8(%rsp)里面的值,即main函数的栈基址rbp;sum汇编代码第10行将rsp上移16个字节,此时进程内存分布如下:

sum汇编代码最后一行,retq指令,该指令把(%rsp)指向的栈单元中的0x4523a3取出给rip寄存器,返回地址出栈,rsp上移8字节,这样rip寄存器中的值就变成了main函数中调用sum的call指令的下一条指令,于是就返回到main函数中继续执行。此时进程内存分布如下:

继续执行main汇编代码第10行将rbp指向24(%rsp)栈单元的地址;main汇编代码第11行rsp上移32字节,此时rbp和rsp指向同一个位置,跟初始位置一样,此时进程内存分布如下:

main汇编代码第12行,retq指令,返回main函数的调用函数。main汇编代码最后2行和前3行作用是一样的,是go编译器插入检查栈溢出的代码和抢占调度相关,这里不关注。

总结

最后说一下学习Go汇编的方法:猜测含义、忽略不必要的语句和谷歌。


参考地址:

【Go程序生成汇编代码三种方式】https://colobu.com/2018/12/29/get-assembly-output-for-go-programs/

【饶大对汇编的简单介绍】https://qcrao.com/2019/03/20/dive-into-go-asm/

【曹大对汇编的完全解析】https://github.com/cch123/golang-notes/blob/master/assembly.md

【函数调用栈】https://cloud.tencent.com/developer/article/1450254

【CPU寄存器】https://cloud.tencent.com/developer/article/1450244

【go汇编语言】https://cloud.tencent.com/developer/article/1450272

【函数调用过程】https://cloud.tencent.com/developer/article/1450282

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Go语言汇编入门 的相关文章

随机推荐

  • FreeRTOS --(7)任务管理之任务切换

    现在创建任务 xff08 xTaskCreate xff09 启动调度器 xff08 vTaskStartScheduler xff09 xff0c 任务控制 xTaskDelay xff0c 以及Tick 中断 xff08 xPortSy
  • Spring Aop到底有什么用处?

    假如没有aop xff0c 在做日志处理的时候 xff0c 我们会在每个方法中添加日志处理 xff0c 比如 但大多数的日子处理代码是相同的 xff0c 为了实现代码复用 xff0c 我们可能把日志处理抽离成一个新的方法 但是这样我们仍然必
  • 获取对象自身的属性

    问题 找出对象 obj 不在原型链上的属性 注意这题测试例子的冒号后面也有一个空格 1 返回数组 xff0c 格式为 key value 2 结果数组不要求顺序 答案 Object keys 方法 xff08 156 ms xff09 返回
  • 重启linux后无法ssh登录

    博客搬运自我的个人博客 chantAria的博客 精力有限 新博客我会同步到CSDN 但博客内容的更新只会出现在个人博客 欢迎大家来玩耍哦 重启llinux后无法ssh登录的情况很多 其中一种情况是在之前update的时候产生了一个坏的内核
  • 使用FFmpeg将视频编码格式转化为H264编码

    背景介绍 xff1a web开发中涉及到视频播放的前端一般会使用h5的video标签对后端提供的视频文件url直接加载进行播放 xff0c 虽然视频文件后缀都是mp4 xff0c 但并非所有视频文件的编码格式都支持用video播放 xff0
  • Cause: org.xml.sax.SAXParseException; lineNumber: 5; columnNumber: 26; 1 字节的 UTF-8 序列的字节 1 无效。

    报错为 xff1a Cause org xml sax SAXParseException lineNumber 5 columnNumber 26 1 字节的 UTF 8 序列的字节 1 无效 是你的xml文件里面的 设置成了默认的UTF
  • Centos7 java服务开机自启动

    1 在 etc systemd system 目录下 新建脚本 cd span class token operator span usr span class token operator span lib span class toke
  • 我使用过的linux命令之strings

    strings命令用于输出文件中可打印的字符串 不论文件是普通文本 xff0c 还是可执行文件 xff0c 任何文件都可以 最常用的选项 xff1a a 扫描整个文件的任何段 xff0c 这是strings的默认行为 xff0c 但是这种默
  • HashMap的工作原理

    HashMap主要是用来处理键值对数据 xff0c 随着JDK版本的更新 xff0c JDK1 8对HashMap的底层也做了一些优化 xff0c HashMap是基于哈希表对Map接口的实现类 xff0c 它的特点是访问数据速度快 xff
  • 如何配置终端代理apt 代理

    1 临时用代理 xff0c 直接在终端里export代理 export http proxy 61 http 127 0 0 1 7890 export https proxy 61 http 127 0 0 1 7890 2 在 etc
  • ssh修改连接端口,以及修改端口之后连接不上的问题

    SSh服务配置文件路径一般都是在 etc ssh这个目录下面 sshd config 这个文件 使用VI vim编辑器 xff0c 打开sshd config这个文件 xff0c 搜索找到 port字段 去掉 xff0c 修改port 后面
  • FreeRTOS原理剖析:任务的创建

    1 任务创建API函数 任务的最基本功能是任务管理 xff0c 任务管理中最基本操作是任务的创建和删除 对于任务的创建和删除 xff0c 由于篇幅有点长 xff0c 分两篇分别讲解 在FreeRTOS中任务的创建函数如下 xff1a 函数描
  • @xmlAttribute等注解它的用处?

    用的是jdk自带的javax xml bind JAXBContext将对象和xml字符串进行相互转换 如果对要生成的 xml 格式有点些许的限制 xff0c 就会对生成xml的对象就需要进行些许控制 xff0c 控制对象的一个最可行的办法
  • C/C++ 分支预测(likely unlikely)

    看一些代码时 xff0c 会遇到likely unlikely 查了查网上的资料 xff0c 结合自己的理解记录一下 1 一些概念 指令周期 是指执行一条指令所需要的时间 xff0c 一般由若干个机器周期组成 xff0c 是从取指令 分析指
  • Vnc viewer与windows之间的复制粘贴

    用VNC连接到Linux之后 xff0c 最纠结的问题就是无法复制粘贴 其实很简单 xff0c 在Linux里面 xff0c 打开一个终端 xff0c 然后输入命令 xff1a vncconfig 之后 xff0c 会弹出一个窗口 不要关闭
  • Android studio 添加多语言支持

    环境 xff1a Android studio 3 2 执行步骤 xff1a 一 生成对应语言文件夹 选中你的工程 gt res gt 右键点击new gt 选中Android resource directory Available qu
  • VNC 远程环境搭建教程

    最近因项目需要使用到 VNC 远程工具 xff0c 因此记录使用过程 一 在 VNC 官网下载 VNC 服务端和客户端安装包 进入下载页面 二 注册 VNC 官网账号 三 在本地安装 VNC 客户端 xff0c 被远程电脑安装 VNC 服务
  • Ubuntu桌面出现Accept clipboard from viewers,Send clipboard to viewers,Send primary selection to vi等三行错误时

    如上图的错误时 1 输入以下神秘代码 sudo apt get install gnome core2 重启vnc服务3 若还不行 xff0c 则修改xstartup脚本 方法见下链接第五部分 修改xstartup
  • Python+ADB实现Android手机QQ自动点赞

    1 前言 前段时间看了些爬虫的知识 xff0c 然后又看到selenium xff0c Appium xff0c 在Appium环境设置过程中 xff0c 意外地看到这个帖子adb命令模拟按键事件 KeyCode xff0c 然后结合相关搜
  • Go语言汇编入门

    虽然在前面的文章中 xff0c 分析代码已经接触了一些Go语言的汇编代码的注解 xff0c 比如在slice和Go语言笔记以及以后的文章中都会使用到Go汇编 本章主要讲解Go汇编大致流程的框架 xff0c 对于刚接触Go汇编理解Go函数栈是