개발/Developing

[Go] Callstack 뜯어보기

lazykuna 2022. 5. 4. 00:48

이 포스트는 ravelin 블로그 포스트를 가져온 것임을 알립니다. https://syslog.ravelin.com/go-function-calls-redux-609fdd1c90fd

Go 콜스택을 확인할 수 있는 좋은 방법을 떠올렸는데, 여기 예제를 예시로 보여드리겠습니다.

package main

import (
    "fmt"
    "runtime"
    "unsafe"
)

func main() {
    f1(0xdeadbeef)
}

func f1(val int) {
    f2(0xabad1dea)
}

func f2(val int) {
    f3(0xbaddcafe)
}

func f3(val int) {
    local := val + 1

    display(uintptr(unsafe.Pointer(&local)))
}

func display(ptr uintptr) {
    mem := *(*[20]uintptr)(unsafe.Pointer(ptr))
    for i, x := range mem {
        fmt.Printf("%X: %X\n", ptr+uintptr(i*8), x)
    }

    showFunc(mem[2])
    showFunc(mem[5])
    showFunc(mem[8])
    showFunc(mem[11])
}

func showFunc(at uintptr) {
    if f := runtime.FuncForPC(at); f != nil {
        file, line := f.FileLine(at)
        fmt.Printf("%X is %s %s %d\n", at, f.Name(), file, line)
    }
}

main 부터 보면, 먼저 f1(0xdeadbeef) 가 호출되고, 이후 f2(0xabad1dea) , f3(baddcafe) 가 호출됩니다. 그리고 f3 로컬 변수인 local 의 값을 조작하고, 이를 unsafe.Pointer을 통해 포인터로 넘기게 됩니다. local 이 스택에 있을 것은 자명하니, 이렇게 하면 관련 정보를 볼 수 있겠지요?

결과는 아래와 같습니다.

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

주목해야 할 값들이 몇 개 있습니다.

  • f3 의 스택 변수 local 의 메모리 값이 C42003FF28 이고, 여기에 써진 값은 BADDCAFE + 1 == BADDCAFF 입니다. 잘 나타나고 있는 것을 볼 수 있습니다.
  • 위쪽 근처의 C42003FF40에는 f3 에 넘겨준 인자 값 BADDCAFE 을 볼 수 있네요.
  • 그 바로 아래의 C42003FF38 에서는 무언가 의미심장한 1088BEB 주소 값을 볼 수 있습니다. 추정컨데 .text 섹션에 있는 PC 주소로 보입니다. 실제로 runtime.FuncForPC 에 주소를 넣으니 콜스택 정보를 확인 할 수 있었습니다.

뭐, 대충 이런 패턴일 것입니다. 이러한 스택 포인터 리턴 어드레스나 파라미터 주소값들에 대해서 정리하면 대략 아래와 같습니다.

  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()

여기에서 여러가지 내용을 알 수 있는데,

  • 스택 포인터는 높은 주소에서부터 점점 아래 주소로 내려옵니다.
  • 함수 호출 시, 호출자는 함수 parameter들을 스택에 집어 넣고, 마지막으로 리턴 어드레스 및 스택 값(현재 PC, ESP) 를 집어넣습니다.
  • 앞서 push된 값들은 함수 수행이 끝나고 이전 함수로 되돌아갈 때 stack unwinding 하면서 사용됩니다.
  • 로컬 변수값들은 현재 스택 포인터(ESP) 뒷부분에 저장됩니다.

그럼 조금 더 복잡한 구조에 대해서 확인해 볼까요? 이를테면 리턴값이 있는 경우라던가...

package main

import (
    "fmt"
    "runtime"
    "unsafe"
)

func main() {
    f1(0xdeadbeef)
}

func f1(val int) {
    f2(0xabad1dea0001, 0xabad1dea0002)
}

func f2(val1, val2 int) (r1, r2 int) {
    f3(0xbaddcafe)
    return
}

func f3(val int) {
    local := val + 1

    display(uintptr(unsafe.Pointer(&local)))
}

보면 f2 함수가 인자 2개, 리턴값 2개로 바뀐 것을 확인할 수 있습니다.

바로 확인해보면,

  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()
  • 호출자는 리턴값 2개를 위해 여분의 스택 공간을 먼저 남겨 놓는 것을 확인할 수 있습니다. 아직 리턴하지 않은 상황이라서 초기화되지 않은 쓰레기값이 들어 있는 점을 명심해주세요!
  • 인자는 역순서로 스택에 담김.

정도를 확인할 수 있습니다.

같이 읽을만한 글