go语言调度器源代码情景分析之六:go汇编语言

2019-06-24 15:31:42 浏览数 (1)

go语言runtime(包括调度器)源代码中有部分代码是用汇编语言编写的,不过这些汇编代码并非针对特定体系结构的汇编代码,而是go语言引入的一种伪汇编,它同样也需要经过汇编器转换成机器指令才能被CPU执行。需要注意的是,用go汇编语言编写的代码一旦经过汇编器转换成机器指令之后,再用调试工具反汇编出来的代码已经不是go语言汇编代码了,而是跟平台相关的汇编代码。

go汇编格式跟前面讨论过的AT&T汇编基本上差不多,但也有些重要区别,本节就这些差异做一个简单说明。

寄存器

go汇编语言中使用的寄存器的名字与AMD64不太一样,下表显示了它们之间的对应关系:

除了这些跟AMD64 CPU硬件寄存器一一对应的寄存器外,go汇编还引入了几个没有任何硬件寄存器与之对应的虚拟寄存器,这些寄存器一般用来存放内存地址,引入它们的主要目的是为了方便程序员和编译器用来定位内存中的代码和数据。

下面重点介绍在go汇编中常见的2个虚拟寄存器的使用方法:

FP虚拟寄存器:主要用来引用函数参数。go语言规定函数调用时参数都必须放在栈上,比如被调用函数使用 first_arg 0(FP) 来引用调用者传递进来的第一个参数,用second_arg 8(FP)来引用第二个参数 ,以此类推,这里的first_arg和second_arg仅仅是一个帮助我们阅读源代码的符号,对编译器来说无实际意义, 0和 8表示相对于FP寄存器的偏移量。我们用一个runtime中的函数片段作为例子来看看FP的使用。

go runtime中有一个叫gogo的函数,它接受一个gobuf类型的指针

代码语言:javascript复制
// func gogo(buf *gobuf)
// restore state from Gobuf; longjmp
TEXT runtime·gogo(SB), NOSPLIT, $16-8
MOVQbuf 0(FP), BX// gobuf -->bx
......

MOVQ buf 0(FP), BX这一条指令把调用者传递进来的指针buf放入BX寄存器中,可以看到,在gogo函数是通过buf 0(FP)这种方式获取到参数的。从被调用函数(此处为gogo函数)的角度来看,FP与函数栈帧之间的关系如下图,可以看出FP寄存器指向调用者的栈帧,而不是被调用函数的栈帧。

SB虚拟寄存器:保存程序地址空间的起始地址。还记得在函数调用栈一节我们看过的进程在内存中的布局那张图吗,这个SB寄存器保存的值就是代码区的起始地址,它主要用来定位全局符号。go汇编中的函数定义、函数调用、全局变量定义以及对其引用会用到这个SB虚拟寄存器。对于这个虚拟寄存器,我们不用过多的关注,在代码中看到它时知道它是一个虚拟寄存器就行了。

操作码

AT&T格式的寄存器操作码一般使用小写且寄存器的名字前面有个%符号,而go汇编使用大写而且寄存器名字前没有%符号,比如:

代码语言:javascript复制
# AT&T格式
mov %rbp,%rsp

# go汇编格式
MOVQ BP,SP

操作数宽度(即操作数的位数)

AT&T格式的汇编指令中如果有寄存器操作数,则根据寄存器的名字(比如rax, eax, ax, al分别代表64,32,16和8位寄存器)就可以确定操作数到底是多少位(8,16,32还是64位),所以不需要操作码后缀,如果没有寄存器操作数又是访存指令的话,则操作码需要加上后缀b、w、l或q来指定到底存取内存中的多少个字节。

而go汇编中,寄存器的名字没有位数之分,比如AX寄存器没有什么RAX, EAX之类的名字,指令中一律只能使用AX。所以如果指令中有操作数寄存器或是指令需要访问内存,则操作码都需要带上后缀B(8位)、W(16位)、D(32位)或Q(64位)。

函数定义

还是以go runtime中的gogo函数为例:

代码语言:javascript复制
// func gogo(buf *gobuf)
// restore state from Gobuf; longjmp
TEXT runtime·gogo(SB), NOSPLIT, $16-8
......

下面对这个函数定义的第一行的各部分做个说明:

TEXT runtime·gogo(SB):指明在代码区定义了一个名字叫gogo的全局函数(符号),该函数属于runtime包。

NOSPLIT:指示编译器不要在这个函数中插入检查栈是否溢出的代码。

$16-8:数字16说明此函数的栈帧大小为16字节,8说明此函数的参数和返回值一共需要占用8字节内存。因为这里的gogo函数没有返回值,只有一个指针参数,对于AMD64平台来说指针就是8字节。go语言中函数调用的参数和函数返回值都是放在栈上的,而且这部分栈内存是由调用者而非被调用函数负责预留,所以在函数定义时需要说明到底需要在调用者的栈帧中预留多少空间。

go汇编还有一些用法比较特别的地方,现在不讨论,等我们分析源代码遇到它们时再结合上下文做详细说明。


最后,如果你觉得本文对你有帮助的话,麻烦帮忙点一下文末右下角的 在看 或转发到朋友圈,非常感谢!


0 人点赞