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