• Anatomy of a function Call in Go
  • By Phil Pearl
  • The Nuggets translation Project
  • Translator: xiaoyusilen
  • Proofreader: 1992chenlu, Zheaoli

Let’s look at some simple Go functions and see if we can understand how function calls work. We will do this by analyzing the assembly generated by the Go compiler from the function. That might be an unrealistic goal for a small blog, but don’t worry, assembly language is simple. Even a CPU can read it.

Image by Rob Baines github.com/telecoda/in…

That’s our first function. Yeah, we’re just adding two numbers.

func add(a, b int) int {
        return a + b
}Copy the code

We need to turn optimizations off when we compile so that we can understand the generated assembly code. We use the go build-gcflags ‘N -l’ command to do this. We can then print out the details of our function with go Tool objdump -s main.add func (where func is our package name, which is the executable we just compiled with Go Build).

If you haven’t studied assembly before, congratulations, you will be exposed to a whole new thing. In addition, I will complete the code for this blog post on a Mac, so it will generate Intel 64-bit assembly.

 main.go:20 0x22c0 48c744241800000000 MOVQ $0x0.0x18(SP)
 main.go:21 0x22c9 488b442408  MOVQ 0x8(SP), AX
 main.go:21 0x22ce 488b4c2410  MOVQ 0x10(SP), CX
 main.go:21 0x22d3 4801c8   ADDQ CX, AX
 main.go:21 0x22d6 4889442418  MOVQ AX, 0x18(SP)
 main.go:21 0x22db c3   RETCopy the code

Now what do we see? As shown below, each line is divided into four parts:

  • The name and line number of the source file (main.go:15). This line of source code is converted to a description with a line number. A single line of Go may be converted to a multi-line assembly.
  • The offset in the target file (for example, 0x22C0).
  • Machine code, for example, 48C744241800000000. This is the binary machine code that the CPU actually executes. We don’t need to watch this. Almost nobody watches this stuff.
  • Assembly representation of machine code, which is the part we want to understand.

Let’s focus on the last part, assembly language.

  • MOVQ, ADDQ and RET are instructions. They tell the CPU what needs to be done. The following parameters tell the CPU what to do.
  • SP, AX, and CX are CPU registers. Registers are where the CPU stores values, and the CPU has multiple registers to use.
  • SP is a special register used to store the current stack pointer. The stack is a register that records local variables, arguments, and function calls. Each Goroutine has a stack. When one function calls another function, and then another function calls another function, each function gets its own storage area on the stack. Create a storage area during a function call, subtracting the required storage size from the size of SP.
  • 0x8 (SP) refers to a storage unit that is 8 bytes longer than the storage unit pointed to by SP.

Therefore, we work on memory cells, CPU registers, instructions for moving values between memory and registers, and operations on registers. That’s pretty much all a CPU can do.

Now let’s look at each item starting with the first instruction. Remember that we need to load two arguments a and B from memory, add them, and return to the calling function.

  1. MOVQ $0x0, 0x18(SP)Place 0 in storage unit SP+0x18. This code looks a little abstract.
  2. MOVQ 0x8(SP), AXPlace the contents of storage unit SP+0x8 into the CPU register AX. Maybe this is one of the arguments that we use to load from memory?
  3. MOVQ 0x10(SP), CXPlace the contents of storage unit SP+0x10 in CPU register CX. That might be another parameter we need.
  4. ADDQ CX, AXAdd CX to AX, and store the result in AX. Ok, so now we’ve added the two parameters.
  5. MOVQ AX, 0x18(sp)Store the contents of register AX in storage unit SP+0x18. That’s what you’re storing.
  6. RETReturns the result to the calling function.

Remember that our function takes two arguments, a and b, and it evaluates a+b and returns the result. MOVQ 0x8(SP), AX moves the argument A to AX, where a is passed to the function in the stack of SP+0x8. MOVQ 0x10(SP), CX moves the argument B to CX, and b is passed to the function in the stack of SP+0x10. ADDQ, CX, AX add a and b. MOVQ AX, 0x18(SP) Stores the result in SP+0x18. The result of the addition is now stored on the stack of SP+0x18 and can be read from the stack when the function returns to call the function.

I’m assuming that a is the first parameter and b is the second parameter. I’m not sure that’s the case. It will take us a while to finish this, but this article is already quite long.

So what exactly does that first line of code, which is a bit of a mystery, do? MOVQ $0X0, 0X18(SP) stores 0 to SP+ 0X18, which is where we store the summation results. We can guess that this is because Go sets an uninitialized value to 0, and we have turned off optimization, which the compiler would have done even if it wasn’t necessary.

So what do we learn from this:

  • Well, it looks like the arguments are all on the stack, with the first argument stored in SP+0x8 and the other in a higher-numbered address.
  • And it appears that the result returned is stored in a higher-numbered address after the argument.

Now let’s look at another function. This function has a local variable, but we’ll keep it simple.

func add3(a int) int {
    b: =3
    return a + b
}Copy the code

We use the same procedure as before to get the assembly list.

TEXT main.add3(SB) 
/Users/phil/go/src/github.com/philpearl/func/main.go
 main.go:15 0x2280 4883ec10  SUBQ $0x10, SP
 main.go:15 0x2284 48896c2408  MOVQ BP, 0x8(SP)
 main.go:15 0x2289 488d6c2408  LEAQ 0x8(SP), BP
 main.go:15 0x228e 48c744242000000000 MOVQ $0x0.0x20(SP)

 main.go:16 0x2297 48c7042403000000 MOVQ $0x3.0(SP)

 main.go:17 0x229f 488b442418  MOVQ 0x18(SP), AX
 main.go:17 0x22a4 4883c003  ADDQ $0x3, AX
 main.go:17 0x22a8 4889442420  MOVQ AX, 0x20(SP)
 main.go:17 0x22ad 488b6c2408  MOVQ 0x8(SP), BP
 main.go:17 0x22b2 4883c410  ADDQ $0x10, SP
 main.go:17 0x22b6 c3   RETCopy the code

B: oh! It looks a little complicated. Let’s try it.

The first four instructions are based on line 15 in the source code. This line of code looks like this:

func add3(a int) int {Copy the code

This line of code doesn’t seem to do much. So this might be a way of declaring functions. Let’s analyze this.

  • SUBQ $0x10, SPSubtract 0x10 from SP =16. This operation frees up 16 bytes of stack space for us
  • MOVQ BP, 0x8(SP)Store the value in register BP into SP+8, and thenLEAQ 0x8(SP), BPLoad the contents of address SP+8 into BP. We now have space to store what was previously stored in the BP, and then store what was in the BP into the storage space just allocated, which helps to establish a stack area chain (or stack frame). This is a bit of a mystery, but I’m afraid we won’t solve it in this article.
  • At the end of this section isMOVQ $ 0x0, 0x20 (SP), which is similar to the last statement we just analyzed, initialize the return value to 0.

B := 3, MOVQ $03x, 0(SP) This solves one of our puzzles. When we subtract 0x10 = 16 from SP, we get space to store two 8-byte values: our local variable B is stored in SP+0, and the pre-bp values are stored in SP+0x08.

The next six lines of the assembly correspond to return A + b. This involves loading a and B from memory, adding them, and returning the result. Let’s look at each line in turn.

  • MOVQ 0x18(SP), AXParameters to be stored in SP+0x18aMove to register AX
  • ADDQ $0x3, AXAdd 3 to AX (which for some reason doesn’t use the local variable we stored in SP+0b, although compile-time optimization is turned off)
  • MOVQ AX, 0x20(SP)a+bThe result is stored in SP+0x20, which is where we return the result.
  • And then what we have is thetaMOVQ 0x8(SP), BPAs well asADDQ $0x10, SP, these will restore the old value of BP, and then add 0x10 to SP, setting it to the value at the start of the function.
  • And finally we haveRET, to be returned to the calling function.

So what have we learned?

  • The calling function allocates space on the stack for return values and parameters. The storage address of the return value is higher than the storage address of the parameter.
  • If the function being called has local variables, space is allocated for them by reducing the value of the stack pointer SP. It also does some mysterious things with register BP.
  • Any operation on SP and BP is reversed when the function returns.

Let’s look at how stack is used in the add3() method:

SP+0x20: the return value


SP+0x18: the parameter a


SP+0x10:?????? SP+0x08: the old value of BP

SP+0x0: the local variable bCopy the code

If you don’t think SP+0x10 is mentioned in the article, you don’t know what this is for. I can tell you, this is where the return address is stored. This is so that the RET directive knows where to go back.

This article is enough. Hopefully if you didn’t know how this stuff worked before, but now you feel like you have some idea, or if you’re intimidated by assembly, then maybe it’s not so arcane. If you’d like to know more about the compilation, let me know in the comments, and I’ll consider writing about it in a future article.

Now that you’re here, if you like my article or can learn something from it, please give me a thumbs up so that it can be seen by more people.

The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. Android, iOS, React, front end, back end, product, design, etc. Keep an eye on the Nuggets Translation project for more quality translations.