應用二進位制介面(英語:application binary interface,縮寫為ABI),這塊原始碼的文件是與函式傳參以及返回值傳遞到底是分配在棧還是暫存器上的呼叫規約
// 當前檢視原始碼的go版本
go version go1.17.3 windows/amd64
原始碼結構
abiStep
abiStep是一個ABI指令結構,它描述了值應該儲存在棧中還是暫存器中,以及他的棧位置和暫存器索引IDabiSeq
abiSeq看縮寫意思就是一系列的ABI指令,它包含了一個abiStep
列表,用valueStart
作為索引來定位函式第I個引數傳遞時所要用到的abiStep
列表,他還負責管理當前棧空間使用情況,暫存器使用情況abiDesc
abiDesc面向一個函式或者一個方法,它將入參和返回包裝在了兩個abiSeq
序列中,還包含了他們使用的棧空間,標記傳遞在棧中的是否為指標,暫存器中的是否為指標,方便暴露給GC
包級常量或變數的含義
var (
intArgRegs = abi.IntArgRegs * goexperiment.RegabiArgsInt
floatArgRegs = abi.FloatArgRegs * goexperiment.RegabiArgsInt
floatRegSize = uintptr(abi.EffectiveFloatRegSize * goexperiment.RegabiArgsInt)
)
intArgRegs
是整數暫存器的數量floatArgRegs
是浮點數暫存器的數量floatRegSize
浮點數暫存器的寬度,如果是0的話說明沒有浮點暫存器或者使用softfloat,它會將浮點型引數分配到棧上,目的是為了相容,因為使用場景很少,不太用考慮它的效能問題。因為go支援32位和64位的浮點數,它的值還可能是4或者8
// abiStepKind is the "op-code" for an abiStep instruction.
type abiStepKind int
const (
abiStepBad abiStepKind = iota
abiStepStack // copy to/from stack
abiStepIntReg // copy to/from integer register
abiStepPointer // copy pointer to/from integer register
abiStepFloatReg // copy to/from FP register
)
abiStepKind
定義了abiStep
描述的操作型別
abiStepBad
無效操作abiStepStack
棧分配abiStepIntReg
整數暫存器值分配abiStepPointer
整數暫存器指標分配abiStepFloatReg
浮點數暫存器分配
abiStep詳細
他的結構如下,他就是一條基本的ABI指令描述
type abiStep struct {
kind abiStepKind // 操作型別,上面有說了
// 值在記憶體中的偏移和大小
// offset 這塊的offset是相對於第一個引數來說的,好比一個字串資料需要佔用倆個暫存器,它的第一個元素dataptr的offset就是0,第二個元素len的offset就是8,是相對於資料結構起始的位置
offset uintptr
// size沒什麼好說的,它的大小
size uintptr // size in bytes of the part
stkOff uintptr // 棧傳遞的話,它的偏移,相對於第一個棧傳遞引數的開始位置
ireg int // 整數暫存器傳遞的話,它的暫存器索引
freg int // 浮點數暫存器傳遞的話,它的暫存器索引
}
abiSeq詳細
abiSeq就是abiStep的一系列組合,可以完成一系列步驟的一個封裝結構
type abiSeq struct {
// 這塊就是它的步驟列表
steps []abiStep
// 這個標記了第i個引數或返回值的abiStep位置,好比當前是引數傳遞,要傳第一個引數,那麼就是steps[valueStart[0]],以此類推
// valueStart和steps數量不對應的原因是,比如字串它佔一個vauleStart但是卻需要倆步step,懂了吧
valueStart []int
// 棧使用空間情況,當進行棧分配的時候,他就會增加
stackBytes uintptr // stack space used
// 暫存器的使用情況,會標記各種暫存器的當前用量,目的是和約定的總量進行對比,看能否繼續從暫存器分配
iregs, fregs int // registers used
}
func (a *abiSeq) stepsForValue(i int) []abiStep
獲取第i個引數的ABI指令描述序列。很簡單的一個函式,就是通過valueStart定位steps,就不看這個原始碼了。func (a *abiSeq) stepsForValue(i int) []abiStep { // s是開始位置 s := a.valueStart[i] var e int // a.steps和a.valueStart數量不對應,反正目的是為了獲取下一個引數傳遞描述資訊的一組ABI序列 if i == len(a.valueStart)-1 { e = len(a.steps) } else { e = a.valueStart[i+1] } return a.steps[s:e] }
func (a *abiSeq) stackAssign(size, alignment uintptr)
棧分配,它傳入一個大小和對齊方式,封裝成一個step新增到seq的steps列表後邊就好了func (a *abiSeq) stackAssign(size, alignment uintptr) { // 1. 將記憶體對齊到相應規格 a.stackBytes = align(a.stackBytes, alignment) // 封裝step描述 a.steps = append(a.steps, abiStep{ kind: abiStepStack, offset: 0, // 這塊0上面解釋過了 size: size, stkOff: a.stackBytes, }) a.stackBytes += size }
func (a *abiSeq) assignIntN(offset, size uintptr, n int, ptrMap uint8) bool
整數暫存器分配,它傳入一個偏移,大小,元素個數,是否包含指標或者是一個介面,返回是否分配成功func (a *abiSeq) assignIntN(offset, size uintptr, n int, ptrMap uint8) bool { // 元素個數高於8個,就報錯 if n > 8 || n < 0 { panic("invalid n") } // ptrMap != 0代表著有指標或者是介面,同時它的大小還不與平臺相對應 if ptrMap != 0 && size != ptrSize { panic("non-empty pointer map passed for non-pointer-size values") } // a.iregs+n 是我們當前a佔有的暫存器數量,intArgRegs是總數量 if a.iregs+n > intArgRegs { return false } // 開始分配 for i := 0; i < n; i++ { // 型別是一個整數暫存器 kind := abiStepIntReg // 判斷有沒有指標或者是不是一個介面 if ptrMap&(uint8(1)<<i) != 0 { kind = abiStepPointer } // 封裝 a.steps = append(a.steps, abiStep{ kind: kind, offset: offset + uintptr(i)*size, // 上層傳進來的offset都是0,這塊的偏移相對於他的0號元素來說的 size: size, ireg: a.iregs, // 暫存器索引 }) // 更新暫存器數量,當然這也標記著下一個暫存器的索引 a.iregs++ } return true }
func (a *abiSeq) assignFloatN(offset, size uintptr, n int) bool
浮點數暫存器分配,跟上面差不多,就是不用判斷是不是指標了func (a *abiSeq) assignFloatN(offset, size uintptr, n int) bool { if n < 0 { panic("invalid n") } if a.fregs+n > floatArgRegs || floatRegSize < size { return false } for i := 0; i < n; i++ { a.steps = append(a.steps, abiStep{ kind: abiStepFloatReg, offset: offset + uintptr(i)*size, size: size, freg: a.fregs, }) a.fregs++ } return true }
func (a *abiSeq) regAssign(t *rtype, offset uintptr) bool
暫存器分配的封裝,給定rtype型別後設資料和偏移量進行分析如何分配暫存器func (a *abiSeq) regAssign(t *rtype, offset uintptr) bool { // 分析rtype的kind,判斷他的底層型別是 switch t.Kind() { case UnsafePointer, Ptr, Chan, Map, Func: // 和指標相關的整數,ptrMap=0b1 return a.assignIntN(offset, t.size, 1, 0b1) case Bool, Int, Uint, Int8, Uint8, Int16, Uint16, Int32, Uint32, Uintptr: // 普通整數,就是0b0 return a.assignIntN(offset, t.size, 1, 0b0) case Int64, Uint64: switch ptrSize { case 4: // 如果平臺字長是4,那麼他需要2個4才能儲存 return a.assignIntN(offset, 4, 2, 0b0) case 8: // 如果平臺字長是8,那麼他需要1個8就能儲存 return a.assignIntN(offset, 8, 1, 0b0) } case Float32, Float64: return a.assignFloatN(offset, t.size, 1) case Complex64: return a.assignFloatN(offset, 4, 2) case Complex128: return a.assignFloatN(offset, 8, 2) case String: // 字串 [dataptr, len],dataptr就是指標 return a.assignIntN(offset, ptrSize, 2, 0b01) case Interface: // 介面的ptr是0b10說明他有倆個指標 return a.assignIntN(offset, ptrSize, 2, 0b10) case Slice: // slice同理,有一個指向底層陣列 return a.assignIntN(offset, ptrSize, 3, 0b01) case Array: // array會在rtype後續記憶體中存放和自己的型別相關的資訊,我們對該塊記憶體進行重新組織成arrayType,方便我們獲取特有的資訊 tt := (*arrayType)(unsafe.Pointer(t)) // 判斷tt的長度,如果長度為0,不用分配,長度為1,就分配一個資料,如果長度大於1,就返回失敗,通過棧分配吧 switch tt.len { case 0: // There's nothing to assign, so don't modify // a.steps but succeed so the caller doesn't // try to stack-assign this value. return true case 1: return a.regAssign(tt.elem, offset) default: return false } case Struct: // 結構體資料,也是先對映回去,再把所有的一個一個存進去,如果有一個失敗,就返回失敗,外面會回滾的 st := (*structType)(unsafe.Pointer(t)) for i := range st.fields { f := &st.fields[i] // 你看這塊的offset開始要新增fields的offset了,再次說明他這個offset是相對已0號元素來說的 if !a.regAssign(f.typ, offset+f.offset()) { return false } } return true default: print("t.Kind == ", t.Kind(), "\n") panic("unknown type kind") } panic("unhandled register assignment path") }
func (a *abiSeq) addRcvr(rcvr *rtype) (*abiStep, bool)
新增方法接受者到steps步驟列表,如果是棧分配,就返回abiStep。如果有指標就返回truefunc (a *abiSeq) addRcvr(rcvr *rtype) (*abiStep, bool) { a.valueStart = append(a.valueStart, len(a.steps)) var ok, ptr bool if ifaceIndir(rcvr) || rcvr.pointers() { // 返回rcvr是否在介面中或者其中是否包含指標,如果有指標或者是在介面中,進行暫存器傳值時ptrMap 0b1,二進位制的1代表這個是一個指標,涉及到GC回收 ok = a.assignIntN(0, ptrSize, 1, 0b1) ptr = true } else { // TODO(mknyszek): Is this case even possible? // The interface data work never contains a non-pointer // value. This case was copied over from older code // in the reflect package which only conditionally added // a pointer bit to the reflect.(Value).Call stack frame's // GC bitmap. // 如果接受者不在介面中並且不含指標 ok = a.assignIntN(0, ptrSize, 1, 0b0) ptr = false } if !ok { // 分配失敗,就進行棧分配 a.stackAssign(ptrSize, ptrSize) return &a.steps[len(a.steps)-1], ptr } return nil, ptr }
func (a *abiSeq) addArg(t *rtype) *abiStep
新增一個引數到steps步驟列表func (a *abiSeq) addArg(t *rtype) *abiStep { pStart := len(a.steps) a.valueStart = append(a.valueStart, pStart) if t.size == 0 { // 如果這個型別大小為0,為了降級到ABI0(穩定的ABI版本),我們需要棧分配,雖然他不佔據空間,但他會影響下一次記憶體對齊,因此我們還是需要執行此操作,但沒有必要生成一個step a.stackBytes = align(a.stackBytes, uintptr(t.align)) return nil } // 保留a的副本,方便我們暫存器分配失敗時回滾 aOld := *a // 暫存器分配開始,offset=0,分配失敗的話,就需要回滾,然後進行棧分配 if !a.regAssign(t, 0) { // 回滾並進行棧分配 *a = aOld // 棧分配成功會返回最後一個步驟的step a.stackAssign(t.size, uintptr(t.align)) return &a.steps[len(a.steps)-1] } return nil }
abiDesc詳細
abiDesc就開始面向函式或方法了,前面都是小打小鬧,這塊是整體封裝,它就是一個完整的流程,並記錄了所有引數或返回值的指標狀態
type abiDesc struct {
// 這就是兩個部分,呼叫和返回倆個步驟,都是單獨的abiSeq
call, ret abiSeq
// 這些欄位是為了給呼叫者分配棧空間,stackCallArgsSize是引數空間,retOffset是返回值開始的偏移量,spill是保留的額外空間的大小
stackCallArgsSize, retOffset, spill uintptr
// 一個點陣圖,指ABI引數和返回值是否是一個指標,用作棧空間反射呼叫堆指標空間點陣圖,棧中分配的指標點陣圖
// 與runtime.bitvector.一致
stackPtrs *bitVector
// 暫存器中分配的指標點陣圖
// inRegPtrs,入參 使用makeFuncStub methodValueCall使對GC可見
// outRegPtrs,返回 使用reflectcall使對GC可見
// 第i位是否包含指標
// 這倆個和GC有關,用來暴露給GC
// 這個IntArgRegBitmap是[(IntArgRegs + 7) / 8]uint8
inRegPtrs, outRegPtrs abi.IntArgRegBitmap
}
他沒有自己的方法,他有一個建構函式
func newAbiDesc(t *funcType, rcvr *rtype) abiDesc {
// We need to add space for this argument to
// the frame so that it can spill args into it.
//
// The size of this space is just the sum of the sizes
// of each register-allocated type.
//
// TODO(mknyszek): Remove this when we no longer have
// caller reserved spill space.
// 我們需要分配一個空間,可以將引數溢位到這裡
// 這個大小隻是每個暫存器分配型別的大小總和
// 當我們不再有呼叫者時刪除他
spill := uintptr(0)
// 棧分配指標點陣圖
stackPtrs := new(bitVector)
// 入參指標點陣圖
inRegPtrs := abi.IntArgRegBitmap{}
// 包裝inSeq
var in abiSeq
if rcvr != nil {
// 如果有接受者,就當做第一個引數傳入
stkStep, isPtr := in.addRcvr(rcvr)
if stkStep != nil {
// !=nil 證明棧分配,判斷是否有指標,並進行點陣圖修改
if isPtr {
stackPtrs.append(1)
} else {
stackPtrs.append(0)
}
} else {
// 如果是暫存器,則spill新增這個平臺字長,記錄暫存器使用總大小
spill += ptrSize
}
}
// 開始遍歷函式的入參列表
for i, arg := range t.in() {
stkStep := in.addArg(arg)
if stkStep != nil {
// 如果是棧分配,就更新點陣圖
addTypeBits(stackPtrs, stkStep.stkOff, arg)
} else {
// 如果是暫存器,就相應的修改spill空間大小
spill = align(spill, uintptr(arg.align))
spill += arg.size
for _, st := range in.stepsForValue(i) {
if st.kind == abiStepPointer {
// 如果有指標,就修改入參暫存器指標點陣圖
inRegPtrs.Set(st.ireg)
}
}
}
}
// 對齊到平臺字長
spill = align(spill, ptrSize)
// 字面意思
stackCallArgsSize := in.stackBytes
// 從這塊我們也能看出retOffset是相對於第一個引數來說的
retOffset := align(in.stackBytes, ptrSize)
// Compute the stack frame pointer bitmap and register
// pointer bitmap for return values.
outRegPtrs := abi.IntArgRegBitmap{}
// Compute abiSeq for output parameters.
var out abiSeq
// Stack-assigned return values do not share
// space with arguments like they do with registers,
// so we need to inject a stack offset here.
// Fake it by artificially extending stackBytes by
// the return offset.
out.stackBytes = retOffset
for i, res := range t.out() {
stkStep := out.addArg(res)
if stkStep != nil {
addTypeBits(stackPtrs, stkStep.stkOff, res)
} else {
for _, st := range out.stepsForValue(i) {
if st.kind == abiStepPointer {
outRegPtrs.Set(st.ireg)
}
}
}
}
// Undo the faking from earlier so that stackBytes
// is accurate.
out.stackBytes -= retOffset
return abiDesc{in, out, stackCallArgsSize, retOffset, spill, stackPtrs, inRegPtrs, outRegPtrs}
}
總結
不總接了,就是原始碼分析而已