Some time ago in a previous post I promised to take a further look at how function calls and call stacks in Go work. I’ve think found a neat way to make good on that promise, so here goes.
So what is a call stack? Well, it’s an area of memory used to hold local variables and call parameters, and to track where functions should return to. Each goroutine has it’s own stack. You could almost say a goroutine is its stack.
Here’s the code I’m going to use to show the stack in action. It’s just a sequence of simple function calls. main() calls f1(0xdeadbeef), which then calls f2(0xabad1dea), which calls f3(0xbaddcafe). f3() then adds one to it’s parameter, and stores it in a local variable called local. It then takes the address of local and prints out memory starting at that address. Because local is on the stack, this prints the stack.
<a href="https://medium.com/media/2a80088cffc6cd7a93e09e95a51b8187/href">https://medium.com/media/2a80088cffc6cd7a93e09e95a51b8187/href</a>
Here’s the output of the program. It is a dump of memory starting at the address of local, shown as a list of 8-byte integers in hex. The address of each integer is on the left, and the int at the address is on the right.
We know local should equal 0xBADDCAFE + 1, or 0xBADDCAFF, and this is indeed what we see at the start of the dump.
C42003FF28: BADDCAFF
C42003FF30: C42003FF48
C42003FF38: 1088BEB
C42003FF40: BADDCAFE
C42003FF48: C42003FF60
C42003FF50: 1088BAB
C42003FF58: ABAD1DEA
C42003FF60: C42003FF78
C42003FF68: 1088B6B
C42003FF70: DEADBEEF
C42003FF78: C42003FFD0
C42003FF80: 102752A
C42003FF88: C420064000
C42003FF90: 0
C42003FF98: C420064000
C42003FFA0: 0
C42003FFA8: 0
C42003FFB0: 0
C42003FFB8: 0
C42003FFC0: C4200001A0
1088BEB is main.f2 /Users/phil/go/src/github.com/philpearl/stack/main.go 19
1088BAB is main.f1 /Users/phil/go/src/github.com/philpearl/stack/main.go 15
1088B6B is main.main /Users/phil/go/src/github.com/philpearl/stack/main.go 11
102752A is runtime.main /usr/local/Cellar/go/1.8/libexec/src/runtime/proc.go 194
If we carry on we continue to see this pattern. Below I’ve marked up the memory dump showing how the stack pointers track back through the dump and where the function parameters and return addresses sit.
C42003FF28: BADDCAFF Local variable in f3()
+-C42003FF30: C42003FF48
| C42003FF38: 1088BEB return to f2() main.go line 19
| C42003FF40: BADDCAFE f3() parameter
+-C42003FF48: C42003FF60
| C42003FF50: 1088BAB return to f1() main.go line 15
| C42003FF58: ABAD1DEA f2() parameter
+-C42003FF60: C42003FF78
| C42003FF68: 1088B6B return to main() main.go line 11
| C42003FF70: DEADBEEF f1() parameter
+-C42003FF78: C42003FFD0
C42003FF80: 102752A return to runtime.main()
From this we can see many things.
We can use the same technique to look at some slightly more complicated function calls. I’ve added more parameters, and some return values to f2() in this version.
<a href="https://medium.com/media/4859ad2040fc251a3e370dc57cc90e59/href">https://medium.com/media/4859ad2040fc251a3e370dc57cc90e59/href</a>
This time I’ve jumped straight to the marked-up output.
C42003FF10: BADDCAFF local variable in f3()
+-C42003FF18: C42003FF30
| C42003FF20: 1088BFB return to f2()
| C42003FF28: BADDCAFE f3() parameter
+-C42003FF30: C42003FF60
| C42003FF38: 1088BBF return to f1()
| C42003FF40: ABAD1DEA0001 f2() first parameter
| C42003FF48: ABAD1DEA0002 f2() second parameter
| C42003FF50: 110A100 space for f2() return value
| C42003FF58: C42000E240 space for f2() return value
+-C42003FF60: C42003FF78
| C42003FF68: 1088B6B return to main()
| C42003FF70: DEADBEEF f1() parameter
+-C42003FF78: C42003FFD0
C42003FF80: 102752A return to runtime.main()
From this we can see that
Hopefully all that made sense! If you got this far and enjoyed it or learned something, please hit that heart-button. I can’t earn internet points without you.
By day, Phil fights crime at ravelin.com. You can join him: https://angel.co/ravelin/jobs