zoukankan      html  css  js  c++  java
  • Golang源码学习:使用gdb调试探究Golang函数调用栈结构

    本文所使用的golang为1.14,gdb为8.1。

    一直以来对于函数调用都仅限于函数调用栈这个概念上,但对于其中的详细结构却了解不多。所以用gdb调试一个简单的例子,一探究竟。

    函数调用栈的结构(以下简称栈)

    栈包含以下作用:

    • 存储函数返回地址。
    • 保存调用者的rbp。
    • 保存局部变量。
    • 为被调用函数预留返回值内存空间。
    • 向被调用函数传递参数。

    每个函数在执行时都需要一段内存来保存上述的内容,这段内存被称为函数的“栈帧

    一般CPU中包含两个与栈相关的寄存器:

    • rsp:始终指向整个函数调用栈的栈顶
    • rbp:指向栈帧的开始位置

    但存储函数返回地址的内存单元的地址并不在rbp~rsp之间。而是在0x8(%rbp)的位置

    栈的工作原理

    栈是一种后进先出(LIFO)的结构,在Linux AMD64环境中,golang栈由高地址向低地址生长。

    当发生函数调用时,由于调用者未执行完成,栈帧还要继续使用,不可以被调用者覆盖,所以要在当前栈顶外继续为被调用者划分栈帧。这个操作叫做压栈(push),并向外移动rbp、rsp,栈空间随之增长。

    与之对应的,当被调用者执行完成时,其栈帧就会被收回。这个操作叫出栈(pop),并向内移动rbp、rsp,栈空间随之缩小。调用者继续执行

    栈空间的生长和收缩是由编译器生成的代码自动管理的的,与堆不同(手动或者gc)。

    流程图

    先给出流程图,好心里有个数:

    代码及编译

    指定 -gcflags="-N -l" 是为了关闭编译器优化。

    go build -gcflags="-N -l" -o test test.go
    

    为了方便查看内存内容,将变量都声明为了int64。

    package main
    
    func main() {
    	caller()
    }
    
    func caller() {
    	var a int64 = 1
    	var b int64 = 2
    	callee(a, b)
    }
    
    func callee(a, b int64) (int64, int64) {
    	c := a + 5
    	d := b * 4
    	return c, d
    }
    

    反汇编代码

    反汇编的内容为:

    • 指令地址
    • 指令相对于当前函数起始位置以字节为单位的偏移
    • 指令内容
    gdb test
    

    断点打在caller方法上,因为主要的研究对象是caller与callee。

    (gdb) b main.caller
    Breakpoint 1 at 0x458360: file /root/study/test.go, line 7.
    

    输入run 运行程序。

    caller函数反汇编,/s 表示将源代码与汇编代码一起显示,如不指定则只显示汇编代码。

    可使用step(s)按源码级别调试,或者stepi(si)按汇编指令级别调试。

    下面是caller、callee的反汇编代码和源码注释,还有与之相关的内存结构对照表。

    (gdb) disassemble /s
    Dump of assembler code for function main.caller:
    7   func caller() {
    => 0x0000000000458360 <+0>:     mov    %fs:0xfffffffffffffff8,%rcx  # 将当前g的指针存入rcx
       0x0000000000458369 <+9>:     cmp    0x10(%rcx),%rsp              # 比较g.stackguard0和rsp
       0x000000000045836d <+13>:    jbe    0x4583b0 <main.caller+80>    # 如果rsp较小,表示栈有溢出风险,调用runtime.morestack_noctxt
       0x000000000045836f <+15>:    sub    $0x38,%rsp       # 划分0x38字节的栈空间
       0x0000000000458373 <+19>:    mov    %rbp,0x30(%rsp)  # 保存调用者main的rbp
       0x0000000000458378 <+24>:    lea    0x30(%rsp),%rbp  # 设置此函数栈的rbp
    
    8       var a int64 = 1
       0x000000000045837d <+29>:    movq   $0x1,0x28(%rsp)  # 局部变量a入栈
    
    9       var b int64 = 2
       0x0000000000458386 <+38>:    movq   $0x2,0x20(%rsp)  # 局部变量b入栈
    
    10      callee(a, b)
       0x000000000045838f <+47>:    mov    0x28(%rsp),%rax  # 读取第一个参数到rax
       0x0000000000458394 <+52>:    mov    %rax,(%rsp)      # callee第一个参数入栈
       0x0000000000458398 <+56>:    movq   $0x2,0x8(%rsp)   # callee第二个参数入栈
       0x00000000004583a1 <+65>:    callq  0x4583c0 <main.callee> # 调用callee
    
    11  }
       0x00000000004583a6 <+70>:    mov    0x30(%rsp),%rbp  # rbp还原为main的rbp
       0x00000000004583ab <+75>:    add    $0x38,%rsp       # rsp还原为main的rsp
       0x00000000004583af <+79>:    retq                    # 返回
    <autogenerated>:
       0x00000000004583b0 <+80>:	callq  0x451b30 <runtime.morestack_noctxt>
       0x00000000004583b5 <+85>:	jmp    0x458360 <main.caller>
    End of assembler dump.
    

    callee函数反汇编

    (gdb) s  # 单步调试进入的callee函数
    main.callee (a=1, b=2, ~r2=824634073176, ~r3=0) at /root/study/test.go:13
    13	func callee(a, b int64) (int64, int64) {
    
    (gdb) disassemble /s
    Dump of assembler code for function main.callee:
    13  func callee(a, b int64) (int64, int64) {
    => 0x00000000004583c0 <+0>:     sub    $0x18,%rsp        # 划分0x18大小的栈
       0x00000000004583c4 <+4>:     mov    %rbp,0x10(%rsp)   # 保存调用者caller的rbp
       0x00000000004583c9 <+9>:     lea    0x10(%rsp),%rbp   # 设置此函数栈的rbp
       0x00000000004583ce <+14>:    movq   $0x0,0x30(%rsp)   # 初始化第一个返回值为0
       0x00000000004583d7 <+23>:    movq   $0x0,0x38(%rsp)   # 初始化第二个返回值为0
    
    14      c := a + 5
       0x00000000004583e0 <+32>:    mov    0x20(%rsp),%rax   # 从内存中获取第一个参数值到rax
       0x00000000004583e5 <+37>:    add    $0x5,%rax         # rax+=5
       0x00000000004583e9 <+41>:    mov    %rax,0x8(%rsp)    # 局部变量c入栈
    
    15      d := b * 4
       0x00000000004583ee <+46>:    mov    0x28(%rsp),%rax   # 从内存中获取第二个参数值到rax
       0x00000000004583f3 <+51>:    shl    $0x2,%rax         # rax*=2
       0x00000000004583f7 <+55>:    mov    %rax,(%rsp)       # 局部变量d入栈
    
    16      return c, d
       0x00000000004583fb <+59>:    mov    0x8(%rsp),%rax    # 局部变量c的值存储到rax
       0x0000000000458400 <+64>:    mov    %rax,0x30(%rsp)   # 将c赋值给第一个返回值
       0x0000000000458405 <+69>:    mov    (%rsp),%rax       # 局部变量d的值存储到rax
       0x0000000000458409 <+73>:    mov    %rax,0x38(%rsp)   # 将d赋值给第二个返回值
    
    17  }
       0x000000000045840e <+78>:    mov    0x10(%rsp),%rbp   # rbp还原为caller的rbp
       0x0000000000458413 <+83>:    add    $0x18,%rsp        # rsp还原为caller的rsp
       0x0000000000458417 <+87>:    retq                     # 返回
    
    End of assembler dump.
    
    

    内存结构对照表

    一些结论

    • golang通过rsp加偏移量访问栈帧。
    • 被调用者的入参是位于调用者的栈中。
    • caller会为有返回值的callee,在栈中预留返回值内存空间。而callee在执行return时,会将返回值写入caller在栈中预留的空间。
    • 意外收获是了解了多值返回的实现。
  • 相关阅读:
    NTP服务安装
    Teambition 的使用
    搭建svn服务器和测试
    通过NTP协议进行时间同步
    转利用OpenSSL库对Socket传输进行安全加密(RSA+AES)
    转源码编译安装MySQL5.6.12详细过程
    CentOS 6.4安装ffmpeg2.4.2 支持h.265
    转:CentOS 6.4 64-bit编译安装ffmpeg
    Work 2013 博客园挂博客
    FLASH和EEPROM的最大区别
  • 原文地址:https://www.cnblogs.com/flhs/p/12510178.html
Copyright © 2011-2022 走看看