Go语言汇编入门 - Go语言中文社区

Go语言汇编入门


虽然在前面的文章中,分析代码已经接触了一些Go语言的汇编代码的注解,比如在sliceGo语言笔记以及以后的文章中都会使用到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栈上没有返回地址内存的。

曹大也对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 ($GOROOTsrcfmtprint.go:275)     PCDATA  $2, $1
        0x0061 00097 ($GOROOTsrcfmtprint.go:275)     MOVQ    os.Stdout(SB), AX
        0x0068 00104 ($GOROOTsrcfmtprint.go:275)     PCDATA  $2, $2
        0x0068 00104 ($GOROOTsrcfmtprint.go:275)     LEAQ    go.itab.*os.File,io.Writer(SB), CX
        0x006f 00111 ($GOROOTsrcfmtprint.go:275)     PCDATA  $2, $1
        0x006f 00111 ($GOROOTsrcfmtprint.go:275)     MOVQ    CX, (SP)
        0x0073 00115 ($GOROOTsrcfmtprint.go:275)     PCDATA  $2, $0
        0x0073 00115 ($GOROOTsrcfmtprint.go:275)     MOVQ    AX, 8(SP)
        0x0078 00120 ($GOROOTsrcfmtprint.go:275)     PCDATA  $2, $1
        0x0078 00120 ($GOROOTsrcfmtprint.go:275)     PCDATA  $0, $0
        0x0078 00120 ($GOROOTsrcfmtprint.go:275)     LEAQ    ""..autotmp_13+72(SP), AX
        0x007d 00125 ($GOROOTsrcfmtprint.go:275)     PCDATA  $2, $0
        0x007d 00125 ($GOROOTsrcfmtprint.go:275)     MOVQ    AX, 16(SP)
        0x0082 00130 ($GOROOTsrcfmtprint.go:275)     MOVQ    $1, 24(SP)
        0x008b 00139 ($GOROOTsrcfmtprint.go:275)     MOVQ    $1, 32(SP)
        0x0094 00148 ($GOROOTsrcfmtprint.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

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/qq_31930499/article/details/100881461
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-06-27 18:06:00
  • 阅读 ( 1225 )
  • 分类:Go

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢