golang汇编指令笔记

Posted by Jason on Sunday, January 20, 2019

TOC

golang汇编指令学习

本文基于阅读golang assembly文章,然后根据阅读中的一些疑问,通过参考Go Assembly Programminggolang 汇编两篇博文进行补充,完成了golang底层生成汇编指令的相关知识总结,非常感激作者的分享。

命令

记录下其中用到的两条命令:

//golang源码转汇编,会输出生成的汇编指令
GOOS=linux GOARCH=amd64 go tool compile -S direct_topfunc_call.go

//查看可执行程序起始地址
objdump -j .text -t direct_topfunc_call | grep 'main.add'

汇编指令

寄存器

go 汇编中有4个核心的伪寄存器,这4个寄存器是编译器用来维护上下文、特殊标识等作用的:

  • FP(Frame pointer): arguments and locals(参数地址)。

指向由调用方提供的参数列表起始地址,通过偏移量指向不同参数或返回值。
通常在偏移量前包含参数名。例如MOVQ size+16(FP), AX


  • PC(Program counter): jumps and branches,PC(指令地址)

可用来按指令行数条转。
比如 JMP 2(PC) 表示以当前位置为 0 基准,往下跳到第 2 行。

  • SB(Static base pointer): global symbols(全局符号)

表示一个全局符号地址,通常应用于全局函数或数据。
例如 CALL add(SB) 表示对应符号名字为 add 的内存地址。

  • SP(Stack pointer): top of stack(栈局部变量内存地址)。用于本地局部变量操作的起始地址。
鉴于栈从底开始的操作方式,SP 实际是栈底位置(等同调整后的 BP 地址)。使用该方式访问局部变量,须添加变量名,如 x-8(SP)。如果省略变量名,则表示硬件寄存器。


  • BP:x86平台上寄存器,通常用来指示函数栈的起始位置,仅仅其一个指示作用

指令

汇编指令自左往右执行,汇编中数字常量以 $ 开头,十进制($10)和 十六进制($0x10)。 标签仅在函数內有效。

指令 作用
MOV(x) 移动内容,其中x代表字节长度,B:1bytes、W:2byte、L:4byte、Q:8byte
ADD 相加,ADD a,b,表示 b=a+b
SUB 相减,SUB a,b,表示 b=b-a
MUL 乘法,MUL $7,b, 表示 b=7*b
MOV 移动指针,MOV n(R1), R2, R2 = *(n+R1),移动n字节
JMP 跳转到某个标签或者行,JMP 2(pc),跳转到PC+2行
LEA 移动地址

golang底层一些知识

routine

  • golang中每个goroutine默认分配的栈空间初始大小为2k,如果运行过程中,栈的大小不够,routine会新生成一个2倍当前栈大小的空间,然后将旧栈内的数据copy到新栈中。这过程叫做栈分裂(stack-split);
  • golang栈底层分配在堆上面的;
  • 为了保证栈分裂的安全执行,而routine不会爆栈,在生成汇编代码时,栈分裂代码被分成 prologue 和 epilogue 两个部分:
    • prologue会检查当前goroutine是否已经用完了所有的空间,然后如果确实用完了的话,会直接跳转到后部;
    • epilogue 会触发栈增长 (stack-growth),然后再跳回到前部; 这样就形成了一个反馈循环,使我们的栈在没有达到饥饿的 goroutine 要求之前不断地进行空间扩张。

函数参数栈中布局

  • Go 的调用规约要求每一个参数都通过栈来传递,这部分空间由 caller 在其栈帧(stack frame)上提供。
    • 调用其它过程之前,caller 就需要按照参数和返回变量的大小来对应地增长(返回后收缩)栈。
    • 对应在main函数中指令就是:
//下面汇编代码,是根据(参考1)中golang代码生成的, 抽取main函数中一部分
//根据Go调用函数规约,在调用add函数前,main中分配了参数和返回值地址
//其中,sp开始字节数:
//0-4变量a; 4-8变量b;
//8-12保存被调用函数add的返回值;12-13存储返回bool类型;13-16用作字节对齐
0x000f 00015 (test.go:6)    SUBQ    $24, SP 
//保存BP寄存器地址
0x0013 00019 (test.go:6)    MOVQ    BP, 16(SP)
//移动栈地址
0x0018 00024 (test.go:6)    LEAQ    16(SP), BP

//其中$137438953482,其实就是两个入参a=10, b=32;
//引用参考1中分析结果
//$ echo 'obase=2;137438953482' | bc
//10000000000000000000000000000000001010
//\_____/\_____________________________/
//  32                             10
0x001d 00029 (test.go:6)    MOVQ    $137438953482, AX
//将32, 10赋值给b a
0x0027 00039 (test.go:6)    MOVQ    AX, (SP)
0x002b 00043 (test.go:6)    CALL    "".add(SB)
  • 函数调用时候,入参、返回值地址在栈中分布都是由右向左开始,对应到栈中就是自高地址到低地址,通过"".a+8(sp)、"".b+12(sp)可以看出,其中sp是当前函数栈底(即地址最低的位置)

  • MOVQ $137438953482, AX指令可以看出,该操作系统是小端,a位于低地址,b位于高地址,MOVQ AX, (SP)完成赋值,即$137438953482在内存上低位对应内存低地址。

大小端

对于数字0x12 34 56 78

大端:高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。一般网络传输中是大端.

低地址 -----> 高地址
0x12  |  0x34  |  0x56  |  0x78

小端:就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。

低地址 ------> 高地址
0x78  |  0x56  |  0x34  |  0x12

参考

  1. golang assembly
  2. Go Assembly Programming
  3. golang 汇编

「真诚赞赏,手留余香」

Jason Blog

真诚赞赏,手留余香

使用微信扫描二维码完成支付


comments powered by Disqus