CGO
CGO
汇编
参考文章:https://mp.weixin.qq.com/s/YtTY23cWaE3M5ygAurj1Ig
GO语言的汇编并不是针对硬件框架的汇编,而是抽象出来的可移植汇编
使用的是GAS汇编,也就是(Gnu ASsembler) 可以通过
go build -gcflags "-N -l -S" main.go 2 > main.s
实现生成对应汇编代码
基础语法
大部分和平时接触的汇编是一样的,
操作码 源操作数 目标操作数
MOVQ $10, AX // 含义是:将 10 赋值给 AX 寄存器
Go 汇编会在指令后加上 B , W , L 或 Q , 分别表示操作数的大小为1个,2个,4个或8个字节。
MOVQ 数据传输
LEAQ 地址传输
PUSHQ
POPQ
....
源操作数表达
- Symbol +offset(register) offset是相对于symbol的偏移
变量定义和声明
其实主要是用data
和global
两个关键词 SB
寄存器是全局静态基指针,用于声明函数,全局变量中
Data 指定对应内存的值
DATA symbol+offset(SB)/width, value // symbol+offset 偏移量,width 宽度, value 初始值
Global 声明一个变量对应的符号,变量对应内存大小
GLOBL symbol(SB), flag, width // 名为 symbol, 内存宽度为 width, flag可省略
函数声明
TEXT pkgname·funcname(SB),flag,$16-24
- TEXT 代表函数的标识,告诉汇编器可以将数据放在TEXT区
- Pkgname 也就是包名
- Funcname 函数名字
- flag代表有特殊功能
- 16代表函数栈帧大小,24表示入参和返回大小
C
go里面调用c函数,并不是直接调用的,而是通过生成go函数来间接调用c函数
- 首先是将c语言函数转化为**_Cfunc_xxxx**的go函数
- 然后**_Cfunc_xxxx会调用_cgo_runtime_cgocall**
- 然后**_cgo_runtime_cgocall再调用runtime.cgocall**
- runtime.cgocall会调用entersyscall函数
- 最后调用runtime.asmcgocall
也就是调用链:
_Cfunc_xxx->_cgo_runtime_cgocall->runtime.cgocallruntime.entersyscall -> runtime.asmcgocall
在调用c函数的时候会创建一个 M 来运行 goroutine 并且标记M的incgo
属性为true代表正在执行cgo
mp := getg().m
mp.ncgocall++
mp.ncgo++
然后调用asmcgocall就是核心了
go的栈切换
参考文章:https://www.cnblogs.com/luozhiyun/p/14619585.html
https://segmentfault.com/a/1190000045073620
https://zhuanlan.zhihu.com/p/213745994
在分析asmcgocall之前,我们需要了解一下go语言是如何进行栈切换的,既然提到了栈就绕不开GMP模型.
M:线程 G:协程(goroutine) P:处理器
G一般是由M进行调度的,所以正常情况一个M+G构成的消息队列就可以实现调度.
计算机一般是多核cpu,所以有多个M,想想多个线程M从全局可运行协程队列获取协程的时候,是不是需要加锁呢?而加锁意味着低效。从而引入了P(一般与cpu的数目一致)来管理一个协程队列
P只能被一个M绑定,所以M通过P进行获取协程队列
每个M都有一个调度协程g0 ,g0不仅负责调度也负责创建,栈增长,垃圾收集,defer函数分配等工作,由于负责繁杂的工作,g0通常有着固定更大的栈,当然在这里我们更在意他的分配
- g0会获取一个可以运行的协程G
- 优先从全局可运行协程队列中获取协程
- 每个P都有一个可运行的协程队列,直接从P获取可用协程
- 当P和全局都没有协程的时候,忙碌的g0师傅就会选择去偷了,此时g0会去其他的P那里偷协程
- 然后切换到G的上下文上
asmcgocall
// func asmcgocall(fn, arg unsafe.Pointer) int32
// 在调度器栈上调用 fn(arg), 已为 gcc ABI 对齐,见 cgocall.go
TEXT ·asmcgocall(SB),NOSPLIT,$0-20
MOVQ fn+0(FP), AX
MOVQ arg+8(FP), BX
MOVQ SP, DX
// get_tls(CX) 获取TLS(线程局部存储)的goroutine指针
get_tls(CX)
// 将goroutine发送给R8寄存器
MOVQ g(CX), R8
// 检查goroutine指针是否存在
CMPQ R8, $0
JEQ nosave
// 有goroutine指针则继续执行
// 将g_m发送给r8 (M)
MOVQ g_m(R8), R8
// g0 发送给si (g0)
MOVQ m_g0(R8), SI
// g发送给di (G)
MOVQ g(CX), DI
// 比较g0是否等于当前的g
CMPQ SI, DI
// 如果等于则条转
JEQ nosave
// 获取g0发送给si
MOVQ m_gsignal(R8), SI
CMPQ SI, DI
JEQ nosave
// g0发送给si
MOVQ m_g0(R8), SI
// 保存goroutine的状态
CALL gosave<>(SB)
// 将g0换成当前的栈
MOVQ SI, g(CX)
// g0的sched+gobuf(用于保存goroutine信息)的sp给sp
MOVQ (g_sched+gobuf_sp)(SI), SP
// 于调度栈中(pthread 新创建的栈)
// 确保有足够的空间给四个 stack-based fast-call 寄存器
// 为使得 windows amd64 调用服务
// 开辟64空间
SUBQ $64, SP
ANDQ $~15, SP // 为 gcc ABI 对齐
// 将原本的g放在48位置
MOVQ DI, 48(SP) // 保存 g
// 将原本的栈顶发送给DI
MOVQ (g_stack+stack_hi)(DI), DI
SUBQ DX, DI
MOVQ DI, 40(SP) // 保存栈深 (不能仅保存 SP, 因为栈可能在回调时被复制)
MOVQ BX, DI // DI = AMD64 ABI 第一个参数
MOVQ BX, CX // CX = Win64 第一个参数
CALL AX // 调用 fn
// 恢复寄存器、 g、栈指针
get_tls(CX)
MOVQ 48(SP), DI
MOVQ (g_stack+stack_hi)(DI), SI
SUBQ 40(SP), SI
MOVQ DI, g(CX)
MOVQ SI, SP
MOVL AX, ret+16(FP)
RET
nosave:
// 在系统栈上运行,可能没有 g
// 没有 g 的情况发生在线程创建中或线程结束中(比如 Solaris 平台上的 needm/dropm)
// 这段代码和上面类似,但没有保存和恢复 g,且没有考虑栈的移动问题(因为我们在系统栈上,而非 goroutine 栈)
// 如果已经在系统栈上,则上面的代码可被直接使用,但而后进入这段代码的情况非常少见的 Solaris 上。
// 使用这段代码来为所有 "已经在系统栈" 的调用进行服务,从而保持正确性。
SUBQ $64, SP
ANDQ $~15, SP // ABI 对齐
MOVQ $0, 48(SP) // 上面的代码保存了 g, 确保 debug 时可用
MOVQ DX, 40(SP) // 保存原始的栈指针
MOVQ BX, DI // DI = AMD64 ABI 第一个参数
MOVQ BX, CX // CX = Win64 第一个参数
CALL AX
MOVQ 40(SP), SI // 恢复原来的栈指针
MOVQ SI, SP
MOVL AX, ret+16(FP)
RET