Interface method calls with the Go register ABI - Eli Bendersky's website

Interface method calls with the Go register ABI - Eli Bendersky's website

Eli Bendersky's website

This post takes a deeper look into how Go compiles method invocations;specifically, how it compiles interface method invocations. Many onlinedescriptions of this process are outdated, because the Go compiler has switchedto a new, register-based ABI inthe 1.17 release for AMD64. Go 1.18 enabledthe register-based ABI for additional architectures, but this post will focus on AMD64.

We'll look at a simple piece of Go code to demonstratethe machine code Go generates and explaining how it works. This code representsthe inner loop of Bubble sort:

func bubbleUp(x sort.Interface) {  n := x.Len()  for i := 1; i < n; i++ {    if x.Less(i, i-1) {      x.Swap(i, i-1)    }  }}

It goes through a data structure once, moving at least one element into itsfinal position. The data structure is abstracted away with the sort.Interfaceinterface, which lets us query itslength, check if one element is smaller than another and to swap elements.

type Interface interface {  Len() int  Less(i, j int) bool  Swap(i, j int)}

I've covered sort.Interface in more detail in an older post.Typically, the actual value passed into bubbleUp will be a slice of somesort, but it can really be anything, as long as it implementssort.Interface.

Interface layout in memory

First, let's discuss how values of interface types are laid out in memory by theGo compiler. This was also covered in another post,which I encourage you to read. A quick reminder: any interface value occupiestwo quadwords (on a 64-bit machine), holding two pointers: the first points tothe dispatch table for the methods of the value, and the second points to theruntime value itself.

This structure can be observed in the Go runtime:

type iface struct {  tab  *itab  data unsafe.Pointer}

The itab type is:

type itab struct {  inter *interfacetype  _type *_type  hash  uint32 // copy of _type.hash. Used for type switches.  _     [4]byte  fun   [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.}

I'll leave a fuller coverage of this type to another time, but for our purposesthe important field is fun, which is a variable-sized array of functionpointers - the dispatch table. This is where the generated code will expect tofind functions, as we'll see shortly.

Some notes on the internal Go ABI

The Go ABI discussed here is the internal ABIof Go. As the linked page is quick to warn the reader - this ABI is completelyunstable and can change between Go releases; in fact, as mentioned above itjust changed in very fundamental way very recently.

In the next section we're going to walk through a detailed disassembly of thebubbleUp function. In preparation for that, I'd like to cover some salientpoints from the current Go ABI that will be useful to grok the generated machinecode.

The current Go ABI is uses registers for function callarguments and return values. On the AMD64 architecture, the following registersare used in order:

RAX, RBX, RCX, RDI, RSI, R8, R9, R10, R11

Some values occupy multiple registers. One relevant example is interface values.As described in the previous section, interface values are represented by theiface structure, which occupies two quadwords. Therefore, when an interfacevalue is passed as the first argument to a function, it occupies RAX andRBX for the table pointer and the data pointer, respectively. If theinterface is passed as a second argument and the first argument already takesup RAX, it would be passed in RBX and RCX, and so on. By the way,the Go disassembly refers to these by the names AX, BX etc. (without theR prefix).

The same registers are used for the return values of functions, in the sameorder. For a function returning a single value (fitting into a quadword), thisvalue will be placed in RAX.

Moreover, in the current Go register ABI there are no callee-saved registers;values that are live across calls have to be stored on the stack by the callerbefore a call is performed.

Disassembling bubbleUp

Let's now work through the disassembly generated for bubbleUp by running:

$ go tool compile -S bubble.go

The disassembly is cleaned up a bit to remove some directives for the garbagecollector (PCDATA and FUNCDATA). Each block of disassembly is followedby an explanation of what it does.

This is the function prologue, which we won't cover:

"".bubbleUp STEXT size=182 args=0x10 locals=0x38 funcid=0x0 align=0x0  0x0000 00000 (bubble.go:11)  TEXT  "".bubbleUp(SB), ABIInternal, $56-16  0x0000 00000 (bubble.go:11)  CMPQ  SP, 16(R14)  0x0004 00004 (bubble.go:11)  JLS   152  0x000a 00010 (bubble.go:11)  SUBQ  $56, SP  0x000e 00014 (bubble.go:11)  MOVQ  BP, 48(SP)  0x0013 00019 (bubble.go:11)  LEAQ  48(SP), BP

The main code generated for the function's body is next:

0x0018 00024 (bubble.go:12)  MOVQ  AX, "".x+64(SP)0x001d 00029 (bubble.go:12)  MOVQ  BX, "".x+72(SP)0x0022 00034 (bubble.go:12)  MOVQ  24(AX), CX0x0026 00038 (bubble.go:12)  MOVQ  BX, AX0x0029 00041 (bubble.go:12)  CALL  CX

The parameter to bubbleUp is x sort.Interface - the interface value.As we've seen above, the interface value in Go is represented by two quadwords,and according to the ABI will be laid out in two consecutive registers: AXand BX. The code starts by saving these onto the stack because theregisters will be needed in a subsequent call.

Then the compiler needs to arrange for the call x.Len(). For this it needsto find which function to call, and then generate the code to call it.

The function to call is taken from 24(AX). He's how it works:

  • As mentioned earlier, AX is the first quadword of the interface valuex passed into bubbleUp.
  • Based on the layout of iface, AX is therefore a pointer to theitab structure.
  • The addressing mode 24(AX) means the address at AX+24 (24 is decimaland means literally "24 bytes"), so this is the value loaded into CX.
  • Since offset 24 in itab points to fun, this means that the addressof the first method in the interface is loaded into CX.
  • The methods in each interface are sorted by name [1], so the address ofx's Len method ends up in CX.

Now that the function is found, the code sets up its arguments for invocation.Since Len has no parameters, the only argument passed is the receiver (thevalue this method is invoked on). bubbleUp's own second argument - whichlives in BX is passed as the first argument to Len. Since BX isthe second quadword of the interface value - it holds the data pointer, whichis the value itself - exactly what we need.

0x002b 00043 (bubble.go:12)  MOVQ  AX, "".n+24(SP)

Len returns a single integer value, which is returned in AX. That'sn in the Go code. Here AX is saved onto the stack (because it's going tobe clobbered soon).

0x0030 00048 (bubble.go:12)  MOVL  $1, CX0x0035 00053 (bubble.go:13)  JMP   69               ----\0x0037 00055 (bubble.go:13)  MOVQ  "".i+32(SP), DX      |0x003c 00060 (bubble.go:13)  LEAQ  1(DX), CX            |0x0040 00064 (bubble.go:13)  MOVQ  "".n+24(SP), AX      |0x0045 00069 (bubble.go:13)  CMPQ  AX, CX          <<---/0x0048 00072 (bubble.go:13)  JLE   142

This implements a part of the loop's condition. It starts by assigningi = 1 in CX and jumps to the condition, which compares AX (remember,that's n) to CX and jumps out of the loop if n <= i.

The code between the JMP and its target handles the i++ part, actingon registers that were saved on the stack throughout the loop iteration.

Now it's time to call x.Less. It's the second function in the interface'sdispatch table, so its offset in itab is 32 (recall that the firstfunction's offset is 24).

0x004a 00074 (bubble.go:13)  MOVQ  CX, "".i+32(SP)0x004f 00079 (bubble.go:14)  MOVQ  "".x+64(SP), DX0x0054 00084 (bubble.go:14)  MOVQ  32(DX), SI0x0058 00088 (bubble.go:14)  LEAQ  -1(CX), DI0x005c 00092 (bubble.go:14)  MOVQ  DI, ""..autotmp_4+40(SP)0x0061 00097 (bubble.go:14)  MOVQ  "".x+72(SP), AX0x0066 00102 (bubble.go:14)  MOVQ  CX, BX0x0069 00105 (bubble.go:14)  MOVQ  DI, CX0x006c 00108 (bubble.go:14)  CALL  SI

This code grabs the iface back from the stack where it was saved, and placesit in DX. It then loads 32(DX) into SI - so SI will contain theaddress of Less method of x, as described before. We have to pass inthree arguments:

  • The value itself (the method receiver) in AX
  • i in BX
  • i-1 in CX (you see it happening with the LEAQ instruction that'sused to subtract 1 from i

There's a bit of a register dance here that's required to set up all thearguments properly, but otherwise it should be very clear.

0x006e 00110 (bubble.go:14)  TESTB  AL, AL0x0070 00112 (bubble.go:14)  JEQ  55

If Less returns a zero value (meaning false), we skip back to the nextloop iteration without calling Swap. Otherwise, the call to Swapfollows:

0x0072 00114 (bubble.go:15)  MOVQ  "".x+64(SP), DX0x0077 00119 (bubble.go:15)  MOVQ  40(DX), SI0x007b 00123 (bubble.go:15)  MOVQ  "".x+72(SP), AX0x0080 00128 (bubble.go:15)  MOVQ  "".i+32(SP), BX0x0085 00133 (bubble.go:15)  MOVQ  ""..autotmp_4+40(SP), CX0x008a 00138 (bubble.go:15)  CALL  SI

By now this should be familiar; this code finds the third function in theinterface's itab with 40(DX), sets up its arguments and calls it.

This is the back-edge to the loop condition handler (recall that it will jumppast this instruction when the loop is done):

0x008c 00140 (bubble.go:15)  JMP  55

And finally the function's epilogue and return:

0x008e 00142 (bubble.go:18)  MOVQ  48(SP), BP0x0093 00147 (bubble.go:18)  ADDQ  $56, SP0x0097 00151 (bubble.go:18)  RET0x0098 00152 (bubble.go:18)  NOP

[1]This is all, of course, implementation details, and can easily changein a future go release.

本文章由 flowerss 抓取自RSS,版权归源站点所有。

查看原文:Interface method calls with the Go register ABI - Eli Bendersky's website

Report Page