golang汇编语言基础

在深入阅读runtime和标准库的源码时候,发现底层有大片代码都会与汇编打交道,所以这篇文章主要是介绍golang使用到的汇编。

go汇编语言是一个不可忽视的技术。因为哪怕只懂一点点汇编,也便于更好地理解计算机原理,也更容易理解Go语言中动态栈/接口等高级特性的实现原理。

本文涉及到计算机架构体系相关的情况时,请假设我们是运行在 linux/amd64 平台上。

伪汇编

Go 编译器会输出一种抽象可移植的汇编代码,这种汇编并不对应某种真实的硬件架构。之后 Go 的汇编器使用这种伪汇编,为目标硬件生成具体的机器指令。

伪汇编这一个额外层可以带来很多好处,最主要的一点是方便将 Go 移植到新的架构上。相关的信息可以参考文后列出的 Rob Pike 的 The Design of the Go Assembler。

go 汇编语言的一个简单实例

思考下面这行代码:

//go:noinline
func add(a, b int32) (int32, bool) {
    
	return a + b, true 
}

func main() {
    add(10, 32) }

注意这里的 //go:noinline 编译器指令。不要省略掉这部分

将这段代码编译到汇编:

"".add STEXT nosplit size=20 args=0x10 locals=0x0
	0x0000 00000 (test1.go:5)	TEXT	"".add(SB), NOSPLIT|ABIInternal, $0-16
	0x0000 00000 (test1.go:5)	FUNCDATA	$0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x0000 00000 (test1.go:5)	FUNCDATA	$1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x0000 00000 (test1.go:5)	FUNCDATA	$2, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x0000 00000 (test1.go:6)	PCDATA	$0, $0
	0x0000 00000 (test1.go:6)	PCDATA	$1, $0
	0x0000 00000 (test1.go:6)	MOVL	"".b+12(SP), AX
	0x0004 00004 (test1.go:6)	MOVL	"".a+8(SP), CX
	0x0008 00008 (test1.go:6)	ADDL	CX, AX
	0x000a 00010 (test1.go:6)	MOVL	AX, "".~r2+16(SP)
	0x000e 00014 (test1.go:6)	MOVB	$1, "".~r3+20(SP)
	0x0013 00019 (test1.go:6)	RET
	0x0000 8b 44 24 0c 8b 4c 24 08 01 c8 89 44 24 10 c6 44  .D$..L$....D$..D
	0x0010 24 14 01 c3                                      $...
"".main STEXT size=65 args=0x0 locals=0x18
	0x0000 00000 (test1.go:9)	TEXT	"".main(SB), ABIInternal, $24-0
	0x0000 00000 (test1.go:9)	MOVQ	(TLS), CX
	0x0009 00009 (test1.go:9)	CMPQ	SP, 16(CX)
	0x000d 00013 (test1.go:9)	JLS	58
	0x000f 00015 (test1.go:9)	SUBQ	$24, SP
	0x0013 00019 (test1.go:9)	MOVQ	BP, 16(SP)
	0x0018 00024 (test1.go:9)	LEAQ	16(SP), BP
	0x001d 00029 (test1.go:9)	FUNCDATA	$0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x001d 00029 (test1.go:9)	FUNCDATA	$1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x001d 00029 (test1.go:9)	FUNCDATA	$2, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x001d 00029 (test1.go:10)	PCDATA	$0, $0
	0x001d 00029 (test1.go:10)	PCDATA	$1, $0
	0x001d 00029 (test1.go:10)	MOVQ	$137438953482, AX
	0x0027 00039 (test1.go:10)	MOVQ	AX, (SP)
	0x002b 00043 (test1.go:10)	CALL	"".add(SB)
	0x0030 00048 (test1.go:11)	MOVQ	16(SP), BP
	0x0035 00053 (test1.go:11)	ADDQ	$24, SP
	0x0039 00057 (test1.go:11)	RET
	0x003a 00058 (test1.go:11)	NOP
	0x003a 00058 (test1.go:9)	PCDATA	$1, $-1
	0x003a 00058 (test1.go:9)	PCDATA	$0, $-1
	0x003a 00058 (test1.go:9)	CALL	runtime.morestack_noctxt(SB)
	0x003f 00063 (test1.go:9)	JMP	0
	0x0000 64 48 8b 0c 25 00 00 00 00 48 3b 61 10 76 2b 48  dH..%....H;a.v+H
	0x0010 83 ec 18 48 89 6c 24 10 48 8d 6c 24 10 48 b8 0a  ...H.l$.H.l$.H..
	0x0020 00 00 00 20 00 00 00 48 89 04 24 e8 00 00 00 00  ... ...H..$.....
	0x0030 48 8b 6c 24 10 48 83 c4 18 c3 e8 00 00 00 00 eb  H.l$.H..........
	0x0040 bf                                               .
	rel 5+4 t=16 TLS+0
	rel 44+4 t=8 "".add+0
	rel 59+4 t=8 runtime.morestack_noctxt+0

接下来一行一行地对这两个函数进行解析来帮助我们理解编译器在编译期间都做了什么事情。

函数 add

0x0000 00000 (test1.go:5) TEXT "".add(SB), NOSPLIT|ABIInternal, $0-16

  1. 0x0000: 当前指令相对于当前函数的偏移量。
  2. TEXT “”.add: TEXT 指令声明了 “”.add 是 .text 段(程序代码在运行期会放在内存的 .text 段中)的一部分,并表明跟在这个声明后的是函数的函数体。在链接期,"" 这个空字符会被替换为当前的包名: 也就是说,"".add 在链接到二进制文件后会变成 main.add。
  3. (SB): SB 是一个虚拟寄存器,保存了静态基地址(static-base) 指针,即我们程序地址空间的开始地址。"".add(SB) 表明我们的符号位于某个固定的相对地址空间起始处的偏移位置 (最终是由链接器计算得到的)。换句话来讲,它有一个直接的绝对地址: 是一个全局的函数符号。

objdump 这个工具能帮我们确认上面这些结论:

 ytlou@ytlou-mac ~/Desktop/golang/golang_study/study/basic/assembly $ objdump -j .text -t test1 | grep 'main.add'
00000000010512e0 l     F __TEXT,__text  main.add
  1. NOSPLIT: 向编译器表明不应该插入 stack-split 的用来检查栈需要扩张的前导指令。
    在我们 add 函数的这种情况下,编译器自己帮我们插入了这个标记: 它足够聪明地意识到,由于 add 没有任何局部变量且没有它自己的栈帧,所以一定不会超出当前的栈;因此每次调用函数时在这里执行栈检查就是完全浪费 CPU 循环了。
  2. $0-16: $0 代表即将分配的栈帧大小;而 $16 指定了调用方传入的参数大小。

Go 的调用规约要求每一个参数都通过栈来传递,这部分空间由 caller 在其栈帧(stack frame)上提供。

调用其它函数之前,caller 就需要按照参数和返回变量的大小来对应地增长(返回后收缩)栈。

Go 编译器没有 PUSH/POP 族的指令: 栈的增长和收缩是通过在栈指针寄存器 SP 上分别执行减法和加法指令来实现的

与大多数最近的编译器做法一样,Go 工具链总是在其生成的代码中,使用相对栈指针(stack-pointer)的偏移量来引用参数和局部变量。这样使得我们可以用帧指针(frame-pointer)来作为一个额外的通用寄存器,这一点即使是在那些寄存器数量较少的平台上也是一样的(例如 x86)。

“”.b+12(SP) 和 “”.a+8(SP) 分别指向栈的低 12 字节和低 8 字节位置(记住: 栈是向低位地址方向增长的!)。

.a 和 .b 是分配给引用地址的任意别名࿱

  • 10
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Golang(又称为Go)是Google公司开发出的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。Go语言具有以下特点:简洁、高效、安全、并发、跨平台等。在学习Golang的入门阶段,你可以了解以下几个方面: 1. Golang的特点:Go语言具有简洁、高效、安全、并发、跨平台等特点,这些特点使得Golang成为一种非常流行的编程语言。 2. Golang的变量作用域:在Golang中,变量的作用域可以分为全局作用域和局部作用域。 3. Golang的执行流程的两种方式:Golang的执行流程可以通过顺序执行和条件执行两种方式来实现。 4. 在Linux上安装Golang语言开发包:要开始学习Golang,你需要在Linux上安装Golang语言开发包。你可以在Golang官方网站上下载适合你的操作系统和架构的Golang安装包,并按照官方文档进行安装。 5. Golang变量的基本使用:在Golang中,你可以使用var关键字声明变量,并使用:=运算符进行变量的初始化和赋值操作。 6. Golang中整数的类型:在Golang中,整数类型可以分为有符号整数和无符号整数,不同的整数类型有不同的取值范围。 7. Golang基本数据类型的默认值:在Golang中,当你声明一个变量但没有进行初始化时,它的默认值将根据其数据类型而定。 8. 基本数据类型转换为String类型:在Golang中,你可以使用strconv包提供的函数将基本数据类型转换为字符串类型。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值