本文快速阅读需要一定的汇编、Go、编译原理基础
因水平极其有限,错误难以避免,欢迎批评指正

1. Go与Plan 9
  • 一图胜千言:
  • 网传,开发Go的一些重要人物也是Plan 9项目的重要人物,所以Go汇编和一些工具链是Plan 9项目搬过来的。因为这个汇编独立与所有的CPU架构和操作系统(独立于操作系统,其实生成的汇编已经要使用寄存器了,每个架构寄存器情况不同)。所以Go项目需要为具体架构和操作系统生成目标机器代码。所以我们甚至可以把Go汇编理解成Go的一种IR。
  • Go汇编学习资料:
    • 官网
    • 《Go语言高级编程》第三章
  • 网上大部分书籍和资料的汇编停留在1.17以前的版本,但是1.17开始(最新的1.18支持更多架构)函数调用有了新ABI规范。所以如果我们的Go版本比较新,那么可能生成的汇编和网上各种教程里的不太一样。其实也没有关系,没有太大区别。本文的汇编是基于Go1.17生成的。
2. 一段相对简单的Go代码学习Go汇编
  • 前置知识:简单强调一下本文阅读预备知识中的一些知识点

    • 编译原理:一个程序编译的过程为词法分析,语法分析,语义分析,中间代码生成,代码分析和优化,目标代码生成。对于其它语言的编译器后端,生成的目标代码一般就是对应平台的汇编代码。再由对应汇编器处理。而对于Go,可以认为生成的目标代码在任何时候都是Plan 9汇编(屏蔽了操作系统带来的差异,如系统调用规范,而CPU带给Go汇编的主要差异就是寄存器数量和名字)。之后会再根据架构和操作系统翻译成对应的机器代码,所以也有人称Go在这个层面是平台无关性的。
    • 汇编基础:这里说一下调用约定,我们程序员一般研究的对象是Linux/x86-64,其调用约定为函数参数只有6个能放在寄存器中,多于6个需要放入栈中。返回地址也在寄存器中。而Go1.17之前,Go调用约定是返回值和调用参数都存放在栈中。现在最新版本的函数调用参数是使用寄存器的,带来了性能的提升。
      再说一下程序运行时候的内存布局,栈内存在内存中是由高地址向低地址延伸的,所以每个栈帧的栈低地址大于栈顶。
  • Go汇编与主流汇编较大区别介绍
    • 4个伪寄存器PC、FP、SP、SB。我们需要重点关注的是FP与SP。特别是SP也是部分架构中的实寄存器。以下内容如无特别表述,SP即表示伪SP。

      • FP:可以认为是当前栈帧的栈底(不包括参数返回值),当有寄存器放不下的调用参数或者有返回值时。这些对象的寻址会用到FP,且为正偏移(参数在FP高地址方向存储)。
      • SP:一定要注意区分真伪SP寄存器。伪SP也可以认为是栈底(不包括参数返回值),而真SP认为是栈顶。一般局部变量的寻址会使用伪SP。且为负偏移。伪寄存器一般需要一个标识符和偏移量为前缀,如果没有标识符前缀则是真寄存器。比如(SP)、+8(SP)没有标识符前缀为真SP寄存器,而a(SP)、b+8(SP)有标识符为前缀表示伪寄存器。
      • 一般一个函数的栈帧可以认为是真伪SP所指地址中间部分。上面的表述中,可能有人认为FP和SP一定是在一起的,但是由于返回地址等内存需求和内存对齐等原因,不是一起的。
    • Go汇编的调用约定中,所有信息都是由调用者保护的,所以可以看出,每个函数栈帧中包含了调用别的函数的参数和返回值空间。
3. Go汇编阅读
  • 阅读Go汇编常用的命令为go tool compile -N -l -S 。-N代表不优化,不然Go汇编和我们想象的可能大不一样,-l为不内联,-S为打印汇编信息。还有其它命令也可以使用。在线网站gossa可以实时查看某个函数的汇编代码
  • 源代码:
package mainfunc main() {var a int64 = 10var b int64 = 20a += sum(a, b)
}func sum(a int64, b int64) int64 {return a + b
}
  • Go汇编及解读:每行#开头的代码解释下一行汇编含义

    • 函数定义:TEXT 函数名(SB), [flags,] $栈大小[-参数及返回值大小]。再次注意,函数自己的参数及返回值不在自己的栈帧中。而自己栈帧大小包括调用别的函数的返回值及参数。flags一般很多,遇到时搜索一下啥意思

    • FUNCDATA和PCDATA:记录了函数中指针信息和调用信息等,panic时的调用情况及垃圾回收时的根对象都分别依赖它们。它们是编译器自行插入的,阅读时可以跳过

    • 使用go tool compile -S / go tool objdump命令输出的汇编来说,所有的 SP 都是真SP即SP寄存器中的地址。所以从下面汇编(使用go tool compile -S -N -l)可以看出没有负索引取值

    • a+24(SP)和40(SP):前者代表a的起始地址在SP上方24字节位置。后者代表的地址为SP上方40字节处。

"".main STEXT size=88 args=0x0 locals=0x30 funcid=0x0# main函数,ABIInternal代表使用了新的ABI,即不是所有参数都在栈中了,main函数栈帧占48字节0x0000 00000 (main.go:3)        TEXT    "".main(SB), ABIInternal, $48-0# 48可以计算出来,看完后再来理解一下:48 = 局部变量a,b  sum参数及返回地址  上一个栈帧BP 一共6个8B即48# 下面这几行是判断栈空间是否足够。不够进行栈扩容。同样的,GC时可以进行栈缩减0x0000 00000 (main.go:3)        CMPQ    SP, 16(R14)0x0004 00004 (main.go:3)        PCDATA  $0, $-20x0004 00004 (main.go:3)        JLS     810x0006 00006 (main.go:3)        PCDATA  $0, $-1# SP(栈顶)减少48,即为当前栈帧分配48字节。我们读代码时可以对称读,下面必定有个命令是加480x0006 00006 (main.go:3)        SUBQ    $48, SP# 先保存上一个栈帧的栈底(上一栈帧的起始)0x000a 00010 (main.go:3)        MOVQ    BP, 40(SP)# BP移动到新的栈帧栈底。我们可以发现,其实没有使用FP,如果有FP的话FP的值会为48(SP)。没有FP原因上面也说了。我们需要注意的是不是任何时候FP和伪SP/BP的位置间隔都是一样的。0x000f 00015 (main.go:3)        LEAQ    40(SP), BP0x0014 00020 (main.go:3)        FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)0x0014 00020 (main.go:3)        FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)# 可以看出就算没有优化,也是没有定义再赋值,而是直接给a赋值100x0014 00020 (main.go:4)        MOVQ    $10, "".a+24(SP)# 给b赋值200x001d 00029 (main.go:5)        MOVQ    $20, "".b+16(SP)# sum参数之一放到AX寄存器中0x0026 00038 (main.go:6)        MOVQ    "".a+24(SP), AX# 第二个参数放到BX寄存器中0x002b 00043 (main.go:6)        MOVL    $20, BX0x0030 00048 (main.go:6)        PCDATA  $1, $0# 调用sum函数。此时我们发现b下面还有16字节,其实是sum的调用参数0x0030 00048 (main.go:6)        CALL    "".sum(SB)# 返回结果存在寄存器AX中,这里存到栈中,可见在局部变量a上面0x0035 00053 (main.go:6)        MOVQ    AX, ""..autotmp_2+32(SP)# a值存在CX0x003a 00058 (main.go:6)        MOVQ    "".a+24(SP), CX# a与结果相加0x003f 00063 (main.go:6)        ADDQ    AX, CX# 相加结果赋值给a0x0042 00066 (main.go:6)        MOVQ    CX, "".a+24(SP)# BP变成上一个栈帧的栈底0x0047 00071 (main.go:7)        MOVQ    40(SP), BP# 函数调用完成之前,SP回归上一栈帧栈顶0x004c 00076 (main.go:7)        ADDQ    $48, SP# 返回,0x0050 00080 (main.go:7)        RET# 下面这几行对应上面栈扩容的跳转行。可以看见,栈扩容后又跳转回去重新判断栈是否有爆栈可能性0x0051 00081 (main.go:7)        NOP0x0051 00081 (main.go:3)        PCDATA  $1, $-10x0051 00081 (main.go:3)        PCDATA  $0, $-20x0051 00081 (main.go:3)        CALL    runtime.morestack_noctxt(SB)0x0056 00086 (main.go:3)        PCDATA  $0, $-10x0056 00086 (main.go:3)        JMP     00x0000 49 3b 66 10 76 4b 48 83 ec 30 48 89 6c 24 28 48  I;f.vKH..0H.l$(H0x0010 8d 6c 24 28 48 c7 44 24 18 0a 00 00 00 48 c7 44  .l$(H.D$.....H.D0x0020 24 10 14 00 00 00 48 8b 44 24 18 bb 14 00 00 00  $.....H.D$......0x0030 e8 00 00 00 00 48 89 44 24 20 48 8b 4c 24 18 48  .....H.D$ H.L$.H0x0040 01 c1 48 89 4c 24 18 48 8b 6c 24 28 48 83 c4 30  ..H.L$.H.l$(H..00x0050 c3 e8 00 00 00 00 eb a8                          ........rel 49+4 t=7 "".sum+0rel 82+4 t=7 runtime.morestack_noctxt+0
"".sum STEXT nosplit size=56 args=0x10 locals=0x10 funcid=0x0# 可见sum的栈帧大小为16B,参数大小为16B,存在上一个栈帧0x0000 00000 (main.go:9)        TEXT    "".sum(SB), NOSPLIT|ABIInternal, $16-16# sum函数有NOSPLIT修饰,所以没有栈扩容阶段0x0000 00000 (main.go:9)        SUBQ    $16, SP0x0004 00004 (main.go:9)        MOVQ    BP, 8(SP)0x0009 00009 (main.go:9)        LEAQ    8(SP), BP0x000e 00014 (main.go:9)        FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)0x000e 00014 (main.go:9)        FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)0x000e 00014 (main.go:9)        FUNCDATA        $5, "".sum.arginfo1(SB)# 这里注意一下,这里是把main的局部变量a存在AX寄存器中的值移动到了sum的参数a中。# 而sum的参数a存在main栈帧中,所以可以看出加24。# 一个偏移24一个偏移32。不是16和24的原因是,CALL和RET会进行隐式的PC/IP寄存器的值存储0x000e 00014 (main.go:9)        MOVQ    AX, "".a+24(SP)0x0013 00019 (main.go:9)        MOVQ    BX, "".b+32(SP)# 这个应该是return a + b变成了 r2 = a + b; return r2。先把r2区域置00x0018 00024 (main.go:9)        MOVQ    $0, "".~r2(SP)# 加法0x0020 00032 (main.go:10)       MOVQ    "".a+24(SP), AX0x0025 00037 (main.go:10)       ADDQ    "".b+32(SP), AX0x002a 00042 (main.go:10)       MOVQ    AX, "".~r2(SP)0x002e 00046 (main.go:10)       MOVQ    8(SP), BP0x0033 00051 (main.go:10)       ADDQ    $16, SP0x0037 00055 (main.go:10)       RET0x0000 48 83 ec 10 48 89 6c 24 08 48 8d 6c 24 08 48 89  H...H.l$.H.l$.H.0x0010 44 24 18 48 89 5c 24 20 48 c7 04 24 00 00 00 00  D$.H.\$ H..$....0x0020 48 8b 44 24 18 48 03 44 24 20 48 89 04 24 48 8b  H.D$.H.D$ H..$H.0x0030 6c 24 08 48 83 c4 10 c3                          l$.H....
go.cuinfo.packagename. SDWARFCUINFO dupok size=00x0000 6d 61 69 6e                                      main
""..inittask SNOPTRDATA size=240x0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................0x0010 00 00 00 00 00 00 00 00                          ........
gclocals·33cdeccccebe80329f1fdbee7f5874cb SRODATA dupok size=80x0000 01 00 00 00 00 00 00 00                          ........
"".sum.arginfo1 SRODATA static dupok size=50x0000 00 08 08 08 ff                                   .....
  • 可能你看了上面的汇编有疑问,不是说1.17开始一些架构ABI改变了吗。为什么还是有寄存器和栈空间中的来回复制。因为上面是加了不优化参数的汇编。当我们去掉-N。就可以看到。sum的栈帧占用内存为0。main栈帧空间也大大缩小(连局部变量a , b都不占用空间了)
  • 个人觉得如果看上面的Go汇编没什么阻碍,Go汇编就可以先学到这了,当我们真要到汇编层面找Bug或提升性能时。看不懂再边学边做就行。上来就学习完Go汇编所有细节,这个付出回报比相对于一般人来说是有点低的
4. 最后我来绘制一下上面汇编代码中栈内存的情况
------
celler BP (8 bytes)
------             main函数栈帧 BP
sum.ret (8 bytes)
------
main.a (8 bytes)
------
main.b (8 bytes)
------
sum.b (8 bytes)
------
sum.a (8 bytes)
------            main函数栈帧 SP
ret addr (8 bytes)
------
caller(main) BP (8 bytes)
------            sum函数栈帧 BP
临时变量 (8 bytes)
------            sum函数栈帧 SP

了解Go第一步:Go与Plan 9汇编语言相关推荐

  1. 领域建模——架构设计的第一步(下)

    领域建模--架构设计的第一步(下) 正如上一篇所述,在领域驱动设计中策略设计侧重于子域的拆分和集成,其结果是合理划分的子域以及它们之间的交互关系.当系统已经被拆分成子域之后,领域驱动设计中的技术维度则 ...

  2. 使用myeclipse的第一步

    使用myeclipse的第一步 将以下代码copy放在一个包中运行,然后在控制台输入任意字符,回车,然后控制台打印一串密匙,这里你输入的就是账号,控制台返回的就是注册码,点击MyEclipse-> ...

  3. python安装包_迈出Python学习第一步:Python开发环境的下载与安装

    所谓"磨刀不误砍柴工"."工欲善其事,必先利其器",都在告诉我们一个道理:要做好一个事情,事先做好充分的准备工作是非常重要的.所以在我们正式学习用Python编 ...

  4. ecshop入门第一步,替换ecshop模板的显示图片

    所有的都是模范默认模板来制作,所以应该参考默认模板的一些数据设置 1.找到themes\default\images文件夹下 screenshot.png 图片 默认如下: 在你的模板文件夹下新建一个 ...

  5. python采集第一步

    2019独角兽企业重金招聘Python工程师标准>>> 上次说要做一个http://www.m4493.com/的美女站点,需要使用python进行数据采集 接下来我们就开始采集数据 ...

  6. 初学架构设计的第一步:需求、愿景与架构

    初学架构设计的第一步:需求.愿景与架构 了解<需求>.<愿景>与<架构>三者的关系.也就是<需求分析>.<观想愿景>与<架构设计> ...

  7. 使用html测试数据库连接与操作(含界面) 第一步界面设计

    前面写的那篇文章,只是实现了页面刷新就读取数据库的过程,还不能有人工干预,还不像一个正式的东东,这可能和我以前写c#养成的习惯似的,总想弄的完美点,第一步先吧界面弄出来,说实话,具体怎么通过按钮出发p ...

  8. php内容采集系统,第一步、采集规则

    采集第一步工作是设置目标网站的采集规则 先确定采集哪一个目标网站,我们用 腾讯科技频道为例:http://tech.qq.com/all/newtech.htm 一.列表采集规则 1.设置采集规则和编 ...

  9. 程序员失业第一步?斯坦福研究员用AI从编译器反馈中学习改Bug

    来源:AI科技评论 本文约2700字,建议阅读10分钟 本文介绍了来自斯坦福大学的两位研究员研究了如何使用AI来自动修复程序,以期未来程序修复自动化可以大大提高编程和学习编程的效率. 众所周知,程序员 ...

  10. SAP WM LT15不能取消二步法确认场景中只做过第一步确认的TO单

    SAP WM LT15不能取消二步法确认场景中只做过第一步确认的TO单 1,如下TO 需要2步法确认,TO#3000006418 看其确认状态时黄色三角形状态,说明第一步确认(LT1D)已经被业务人员 ...

最新文章

  1. Latex中数学公式中常用符号(持续更新)
  2. python 中类属性共享问题
  3. java tomcat日志中文乱码问题解决
  4. 幼儿园课程体系结构图_全人教育、均衡发展课程体系介绍
  5. SQL 表之间的更新
  6. mysql utf8mb4 java_mysql中utf8和utf8mb4区别
  7. Consul入门01 - 安装Consul
  8. 我理解Docker的过程2
  9. 拥抱时序数据库,构筑IoT时代下智慧康养数据存储底座
  10. WPF: 共享Grid宽度或高度的方法
  11. 正则匹配获取中括号中的内容
  12. USB-AUDIO初步分析
  13. window使用fliqlo 教程
  14. 如何帮助空降经理人成功?
  15. python分数类_Python——处理分数类Fraction
  16. 数据分析_python数据可视化(基于matplotlib+pandas)
  17. 在新学期,立新规划!
  18. ComM(通信管理)和CanNm(network)
  19. 防火墙登录web页面(ensp)
  20. day72 JavaWeb框架阶段——RabbitMQ消息队列【了解常见的MQ产品,了解RabbitMQ的5种消息模型,会使用Spring AMQP】

热门文章

  1. 【教学类-29-03】20230409《门牌号-黏贴版(5层*5间)灰底下划线》-(中班《我爱我家》偏数学)
  2. 秒云X焱融科技成功落地电力设计行业云原生超融合虚拟化场景
  3. Ansible Tower 全方位整理
  4. hub无法登录 unity_Unity助力可口可乐圣诞节品牌营销
  5. Windows——关于Word2016/2019提示需要修复问题处理
  6. scrollTo与scrollBy用法以及TouchSlop与VelocityTracker解析
  7. Python打包与解压zip
  8. MySQL-SQL语句-DML
  9. 视觉SLAM 第7讲 本质矩阵 基础矩阵 单应矩阵 知识点/证明/理解/秩/自由度
  10. 数学分析教程史济怀练习9.7