1.伪寄存器 #
伪寄存器常用的一般是下面的四个:
FP: Frame pointer: arguments and locals.
PC: Program counter: jumps and branches.
SB: Static base pointer: global symbols.
SP: Stack pointer: top of stack.
下面我们来翻译一下 官网1的对他们的解释,然后做一个总结,方便理解。
1.1.FP #
FP伪寄存器是一个用于引用函数参数的虚拟帧指针。编译器维护一个虚拟帧指针,并将堆栈上的参数引用为该伪寄存器的偏移量。因此0(FP)是函数的第一个参数,8(FP)是第二个参数(在64位机器上),以此类推。但是,当以这种方式引用一个函数参数时,有必要将名称放在开头,如first_arg+0(FP)和second_arg+8(FP)。(偏移量的含义(从帧指针出发的偏移量)与它在SB中的使用不同,在SB中,它是从符号出发的偏移量。) 汇编器强制执行这个约定,拒绝普通的0(FP)和8(FP)。实际的名称在语义上是不相关的,但应该用来记录参数的名称。值得强调的是,FP始终是一个伪寄存器,而不是硬件寄存器,即使在具有硬件帧指针的架构上也是如此。
The FP pseudo-register is a virtual frame pointer used to refer to function arguments. The compilers maintain a virtual frame pointer and refer to the arguments on the stack as offsets from that pseudo-register. Thus 0(FP) is the first argument to the function, 8(FP) is the second (on a 64-bit machine), and so on. However, when referring to a function argument this way, it is necessary to place a name at the beginning, as in first_arg+0(FP) and second_arg+8(FP). (The meaning of the offset—offset from the frame pointer—distinct from its use with SB, where it is an offset from the symbol.) The assembler enforces this convention, rejecting plain 0(FP) and 8(FP). The actual name is semantically irrelevant but should be used to document the argument’s name. It is worth stressing that FP is always a pseudo-register, not a hardware register, even on architectures with a hardware frame pointer.
对于带有Go原型的汇编函数,go vet会检查参数名和偏移量是否匹配。在32位系统上,64位值的低位和高位32位是通过在名称中添加一个_lo或_hi后缀来区分的,如arg_lo+0(FP)或arg_hi+4(FP)。如果一个Go原型没有给它的结果命名,那么预期的汇编名是ret。
For assembly functions with Go prototypes, go vet will check that the argument names and offsets match. On 32-bit systems, the low and high 32 bits of a 64-bit value are distinguished by adding a _lo or _hi suffix to the name, as in arg_lo+0(FP) or arg_hi+4(FP). If a Go prototype does not name its result, the expected assembly name is ret.
1.2.SP #
SP伪寄存器是一个虚拟栈指针,用于引用帧本地变量和为函数调用准备的参数。它指向本地栈帧的顶部,所以引用时应使用负偏移量,范围为[-framesize,0):x-8(SP),y-4(SP),以此类推。
The SP pseudo-register is a virtual stack pointer used to refer to frame-local variables and the arguments being prepared for function calls. It points to the top of the local stack frame, so references should use negative offsets in the range [−framesize, 0): x-8(SP), y-4(SP), and so on.
在具有名为SP的硬件寄存器的架构上,名称前缀可以区分对虚拟栈指针的引用和对架构SP寄存器的引用,即x-8(SP),y-4(SP),以此类推。也就是说,
x-8(SP)
和-8(SP)
是不同的内存位置:第一个是指虚拟栈指针伪寄存器,而第二个是指硬件的SP寄存器。
On architectures with a hardware register named SP, the name prefix distinguishes references to the virtual stack pointer from references to the architectural SP register. That is, x-8(SP) and -8(SP) are different memory locations: the first refers to the virtual stack pointer pseudo-register, while the second refers to the hardware’s SP register.
- 如何理解伪寄存器FP和SP呢?其实伪寄存器FP和SP相当于plan9伪汇编中的一个助记符,他们是根据当前函数栈空间计算出来的一个相对于物理寄存器SP的一个偏移量坐标。
- 伪SP和FP的相对位置是会变的,所以不应该尝试用伪SP寄存器去找那些本用FP+offset来引用的值,例如函数的入参和返回值。
- 官方文档中说的伪SP指向栈的top,是有问题的。其指向的局部变量位置实际上是整个栈的栈底(除caller BP 之外),所以说bottom更合适一些。
MOVQ 0(SP), AX
,这种前面没有flags的,它相当于实际的寄存器的值,不是伪寄存器了。
2.Go类汇编指令 #
2.1.寻址模式 #
- (DI)(BX2): The location at address DI plus BX2.
- 64(DI)(BX2): The location at address DI plus BX2 plus 64. These modes accept only 1, 2, 4, and 8 as scale factors.
2.2.结构体+寄存器 #
类似这种:
// (m_morebuf+gobuf_pc)(REGISTER)
MOVQ 8(SP), AX # f's caller's PC
MOVQ AX, (m_morebuf+gobuf_pc)(BX)
type m struct {
g0 *g // goroutine with scheduling stack
morebuf gobuf // gobuf arg to morestack //-----------morebuf-------------//
divmod uint32 // div/mod denominator for arm - known to liblink
//...
}
type gobuf struct {
// The offsets of sp, pc, and g are known to (hard-coded in) libmach.
//
// ctxt is unusual with respect to GC: it may be a
// heap-allocated funcval, so GC needs to track it, but it
// needs to be set and cleared from assembly, where it's
// difficult to have write barriers. However, ctxt is really a
// saved, live register, and we only ever exchange it between
// the real register and the gobuf. Hence, we treat it as a
// root during stack scanning, which means assembly that saves
// and restores it doesn't need write barriers. It's still
// typed as a pointer so that any other writes from Go get
// write barriers.
sp uintptr
pc uintptr // <<<---
g guintptr
ctxt unsafe.Pointer
ret sys.Uintreg
lr uintptr
bp uintptr // for GOEXPERIMENT=framepointer
}
我们从这个m_morebuf+gobuf_pc
就知道指的是这个m结构体中的morebuf结构体字段中的pc值。
3.函数调用栈分析 #
函数调用栈的知识(为方便起见,在函数A1中调用函数A2,我们称A1是caller,A2是callee); 栈指每个进程/线程/goroutine都有自己的调用栈,参数和返回值的传递,函数的局部变量存储通常是通过栈来完成的。和数据结构中的栈一样,内存栈也是后进先出的,地址从高地址开始增长到低地址。栈帧也称为帧,每一帧对应一个尚未返回的函数调用,帧本身以栈的形式存储数据。栈由许多帧组成,它描述函数之间的调用关系.
如下图所示:内存中的栈从高地址空间向低地址空间增长,栈顶小于栈底,分配栈空间对应sp值减小。
其中caller与callee的关系在go1.17版本以下是下图所示,go1.17+以上返回参数已使用 寄存器方式传递
3.1.示例 #
package main
func main() {
Sub(2,1)
}
//go:noinline
func Sub(a , b int) int {
d := a - b
return d
}
生成的汇编结果如下:
这里需要注意一点的是,上面都是在代码空间的,所以左边都是代码空间的地址,当我们分析栈空间的时候,需要查找栈空间地址的内容
下面来一步一步来看下调用的过程:
3.2.伪寄存器的位置 #
- 下面来做下实验。
- 确认伪FP, SP相对于真实存在的寄存器的位置点
- 我们伪FP应该在caller’s next pc + 8byte
- 伪SP应该在caller’s BP
- 确认伪FP, SP相对于真实存在的寄存器的位置点
main.go
package main
func test_FP_SP(a, b int64)(first uintptr, second uintptr)
func main(){
first, second := test_FP_SP(1, 2)
first -= second
_ = first
}
test_FP_SP.s
// func test_FP_SP(a, b int64)(first uintptr, second uintptr)
TEXT ·test_FP_SP(SB),$1040-16 // 这里的16是为了存caller调用call指令的时候,把它下一个pc地圵放入栈中与caller's BP,所以就减16
LEAQ x-0(SP), DI //
MOVQ DI, first+16(FP) // 将原伪寄存器SP偏移量存入返回值first
MOVQ SP, BP // 存储物理SP偏移量到BP寄存器
ADDQ $512, SP // 将物理SP偏移增加 0.5K
LEAQ x-0(SP), SI // 在上面中只改变了一个值就是SP这个寄存器,然后再次一模一样的把x-0(SP)给到了SI.
/* 第一个 MOVQ BP, SP */
MOVQ BP, SP // 恢复物理SP,因为修改物理SP后,伪寄存器FP/SP随之改变,
// 为了正确访问FP,先恢复物理SP
MOVQ SI, second+24(FP) // 将偏移后的伪寄存器SP偏移量存入返回值second
/* 第二个 MOVQ BP, SP */
//MOVQ BP, SP
RET // 从输出的second-first来看,正好相差0.5K
编译一下源代码:
# linux
go build -gcflags "-N -l" -o test .
# or
go build -gcflags "all=-N -l" -o test .
# xos:
go build -gcflags "all=-N -l" -ldflags=-compressdwarf=false -o test .
# result
[root@iZf8z14idfp0rwhiicngwqZ FP_SP]# tree .
.
├── main.go
├── test
└── test_FP_SP.s
我们用到的gdb命令:
gdb ./test
list
b 6
display /25i $pc-8
si
si
si
从上面的图中可以看出,go assemble中的x-0(SP)
与first+16(FP)
,其实都是与SP寄存器关联的,其中SP,伪FP,与伪SP的位置,在下图中已经标识出来了;
+------------------------+
| |
| |
| second |
| |
|-------------------------
| |
| |
| first |
| |
|------------------------|
| |
| |
| b |
| |
|------------------------|
| |
| |
| a |
| |
伪FP +------------------------+
| |
| |
| caller's pc |
| |
+------------------------+
| |
| |
| caller's BP |
| |
伪SP|callee's BP +------------------------+
| |
| |
| ... |
| |
| |
真实寄存器SP等于 +------------------------+
caller's SP - caller's next CP(8) - callee's stack size
上图已标识
当我们把/* 第一个 MOVQ BP, SP */
下面的注释掉,执行的话会panic,是因为PC寄存器读取错误,而不是注释掉的下一行导致的。
可以实验下:我们把/* 第二个 MOVQ BP, SP */
取消注释,它就正常执行,只是返回值不对而已。
3.3.添加汇编 #
go编译器在函数头添加额外汇编,判断当前Goroutine栈是否将越界,如将越界,需加空间
- go version go1.13.8 linux/amd64
0x0000000000459240 <main.test_FP_SP+0>: mov %fs:0xfffffffffffffff8,%rcx
0x0000000000459249 <main.test_FP_SP+9>: lea -0x398(%rsp),%rax
0x0000000000459251 <main.test_FP_SP+17>: cmp 0x10(%rcx),%rax
0x0000000000459255 <main.test_FP_SP+21>: jbe 0x4592ab <main.test_FP_SP+107>
// ...
0x00000000004592ab <main.test_FP_SP+107>: callq 0x450c70 <runtime.morestack_noctxt>
0x00000000004592b0 <main.test_FP_SP+112>: jmp 0x459240 <main.test_FP_SP>
- go version go1.15
=> 0x459240 <main.test_FP_SP>: mov %fs:0xfffffffffffffff8,%rcx
0x459249 <main.test_FP_SP+9>: mov 0x10(%rcx),%rsi
0x45924d <main.test_FP_SP+13>: cmp $0xfffffffffffffade,%rsi
0x459254 <main.test_FP_SP+20>: je 0x4592bd <main.test_FP_SP+125>
//...
0x4592bd <main.test_FP_SP+125>: callq 0x450c70 <runtime.morestack_noctxt>
0x4592c2 <main.test_FP_SP+130>: jmpq 0x459240 <main.test_FP_SP>
0x4592c7: add %al,(%rax)
0x4592c9: add %al,(%rax)
可以观察到是在头和尾部加上了跳转代码
0x10(%rcx)
$0xfffffffffffffade
type stack struct {
lo uintptr
hi uintptr
}
type g struct {
// Stack parameters.
// stack describes the actual stack memory: [stack.lo, stack.hi).
// stackguard0 is the stack pointer compared in the Go stack growth prologue.
// It is stack.lo+StackGuard normally, but can be StackPreempt to trigger a preemption.
// stackguard1 is the stack pointer compared in the C stack growth prologue.
// It is stack.lo+StackGuard on g0 and gsignal stacks.
// It is ~0 on other goroutine stacks, to trigger a call to morestackc (and crash).
stack stack // offset known to runtime/cgo
stackguard0 uintptr // offset known to liblink
stackguard1 uintptr // offset known to liblink
//...
其中rcx是g的地址,所以0x10(%rcx)
是就是g.stackguard0的值,当被设置需要抢占的时候,其值是:
StackPreempt = -1314 // 也就是 0xfff...fade
当设置需要抢占的时候,程序会会跳转到函数尾部的runtime.morestack_noctxt
函数
4.Go类汇编函数 #
dropg函数 #
dropg()函数:解除g和m之间连接关系,其实就是设置g->m = nil, m->currg = nil.
func dropg() {
_g_ := getg()
setMNoWB(&_g_.m.curg.m, nil)
setGNoWB(&_g_.m.curg, nil)
}
几个退出收尾函数 #
非main goroutine运行结束: goexit0
主动调度:gosched_m
- 剥夺调度(运行太久):gopreempt_m
- 其中 gopreempt_m与gosched_m内部都是调用的goschedImpl函数,所以功能都是一样。
- 剥夺调度(运行太久):gopreempt_m
被动调度: park_m
tls相关函数 #
普通的全局变量,一个线程对其进行了修改,所有线程都可以看到这个修改; 线程私有全局变量不同,每个线程都有自己的一份副本,某个线程对其所做的修改不会影响到其它线程的副本;
TLS代表的是伪寄存器,存储线程本地存储的基地址(基地址加上偏移地址能得到完整的一个地址), 语义上
MOVQ TLS, reg
off(reg)(TLS*1)
等于
off(TLS)
而(TLS*1)
说明是从线程本地存储的基地址上进行索引
runtime·rt0_go(SB) #
TEXT runtime·rt0_go(SB),NOSPLIT,$0
//...
LEAQ runtime·m0+m_tls(SB), DI // DI = &m0.tls,取m0的tls成员的地址到DI寄存器
CALL runtime·settls(SB) // 调用settls设置线程本地存储,其中settls的入参为DI寄存器
get_tls(BX) // 把TLS地址放入BX寄存器,测试刚刚那个绑定是否成功。
MOVQ $0x123, g(BX) // 0x123设置到了m0.tls[0]
MOVQ runtime·m0+m_tls(SB), AX
CMPQ AX, $0x123 // 比较确认前面设置是否成功
JEQ 2(PC)
CALL runtime·abort(SB)
settls #
设置段基地址
// set tls base to DI
TEXT runtime·settls(SB),NOSPLIT,$32
ADDQ $8, DI // ELF wants to use -8(FS) // https://akkadia.org/drepper/tls.pdf
MOVQ DI, SI // SI 是 arch_prctl的第二个参数 addr
MOVQ $0x1002, DI // ARCH_SET_FS, arch_prctl的第一个参数 code
MOVQ $SYS_arch_prctl, AX
SYSCALL
CMPQ AX, $0xfffffffffffff001
JLS 2(PC)
MOVL $0xf1, 0xf1
RET
主要使用系统API arch_prctl
// arch_prctl() sets architecture-specific process or thread state. code selects a subfunction and passes argument addr to it;
int arch_prctl(int code, unsigned long addr);
参数之一就是指定操作类型,其中:
ARCH_SET_FS 1: 代表将FS寄存器的64位基数设置为addr指定的参数操作,它定义的操作类型代号为0x1002
上面的步骤是把段基寄存器 设置为 m0.tls[0]
所在的地址.
get_tls(BX)与g(BX) #
这个get_tls(BX)
与g()
其实都是宏定义:
#ifdef GOARCH_amd64
#define get_tls(r) MOVQ TLS, r
#define g(r) 0(r)(TLS*1)
#endif
翻译过来就是:
MOVQ TLS, BX
0(BX)(TLS*1)
而0(BX)(TLS*1)
就等于0(TLS)
, 所以上面就是把0x123设置到了m0.tls[0]
, 然后通过比较CMPQ AX, $0x123
看刚刚测试那个绑定是否成功。
getg函数 #
getg()单独返回当前的g, 得到当前用户g,最好使用getg().m.curg
,因在系统或信号栈上执行时,这将分别返回当前M的 “g0 “或 “gsignal”。这通常不是你想要的。
想确定你是在用户栈还是系统栈上运行,可以使用getg() == getg().m.curg
// getg returns the pointer to the current g.
// The compiler rewrites calls to this function into instructions
// that fetch the g directly (from TLS or from the dedicated register).
func getg() *g