Kotlin/Native 디버깅
Kotlin/Native 디버깅
Kotlin/Native 컴파일러는 디버그 정보가 있는 바이너리를 생성할 수 있고, 크래시 리포트를 심볼리케이트하기 위한 디버그 심볼 파일도 만들 수 있어요.
디버그 정보는 DWARF 2 사양과 호환되므로 LLDB, GDB 같은 최신 디버거 도구는:
- 중단점을 설정할 수 있어요.
- 스테핑을 사용할 수 있어요.
- 변수 및 타입 정보를 검사할 수 있어요.
DWARF 2 사양을 지원한다는 것은, DWARF 5 사양 이전에는 사양에 Kotlin 언어 타입에 대한 식별자가 없기 때문에 디버거가 Kotlin을 C89로 인식한다는 뜻이에요.
본문
디버그 정보가 있는 바이너리 생성하기
IntelliJ IDEA, Android Studio, Xcode에서 디버깅할 때는(빌드가 다르게 구성되지 않은 경우) 디버그 정보가 있는 바이너리가 자동으로 생성돼요.
디버깅을 수동으로 활성화하고 디버그 정보를 포함한 바이너리를 만들려면:
- Gradle 작업을 사용해요. 디버그 바이너리를 얻으려면
linkDebug*Gradle 작업을 사용하세요. 예:
./gradlew linkDebugFrameworkNative
작업은 바이너리 유형(예: linkDebugSharedNative)이나 타깃(예: linkDebugExecutableMacosArm64)에 따라 달라져요.
- 커맨드라인 컴파일러를 사용해요. 커맨드라인에서
-g옵션으로 Kotlin/Native 바이너리를 컴파일하세요:
kotlinc-native hello.kt -g -o terminator
그런 다음 디버거 도구를 실행해요. 예:
lldb terminator.kexe
디버거는 다음과 같이 출력해요:
$ cat - > hello.kt
fun main(args: Array<String>) {
println("Hello world")
println("I need your clothes, your boots and your motorcycle")
}
$ dist/bin/konanc -g hello.kt -o terminator
KtFile: hello.kt
$ lldb terminator.kexe
(lldb) target create "terminator.kexe"
Current executable set to 'terminator.kexe' (x86_64).
(lldb) b kfun:main(kotlin.Array<kotlin.String>)
Breakpoint 1: where = terminator.kexe`kfun:main(kotlin.Array<kotlin.String>) + 4 at hello.kt:2, address = 0x00000001000012e4
(lldb) r
Process 28473 launched: '/Users/minamoto/ws/.git-trees/debugger-fixes/terminator.kexe' (x86_64)
Process 28473 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
frame #0: 0x00000001000012e4 terminator.kexe`kfun:main(kotlin.Array<kotlin.String>) at hello.kt:2
1 fun main(args: Array<String>) {
-> 2 println("Hello world")
3 println("I need your clothes, your boots and your motorcycle")
4 }
(lldb) n
Hello world
Process 28473 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step over
frame #0: 0x00000001000012f0 terminator.kexe`kfun:main(kotlin.Array<kotlin.String>) at hello.kt:3
1 fun main(args: Array<String>) {
2 println("Hello world")
-> 3 println("I need your clothes, your boots and your motorcycle")
4 }
(lldb)
중단점 설정하기
최신 디버거는 중단점을 설정하는 여러 방법을 제공해요. 도구별로 자세히 살펴볼까요:
LLDB
- 이름으로:
(lldb) b -n kfun:main(kotlin.Array<kotlin.String>)
Breakpoint 4: where = terminator.kexe`kfun:main(kotlin.Array<kotlin.String>) + 4 at hello.kt:2, address = 0x00000001000012e4
-n은 선택 사항이며 기본적으로 적용돼요.
- 위치(파일 이름, 줄 번호)로:
(lldb) b -f hello.kt -l 1
Breakpoint 1: where = terminator.kexe`kfun:main(kotlin.Array<kotlin.String>) + 4 at hello.kt:2, address = 0x00000001000012e4
- 주소로:
(lldb) b -a 0x00000001000012e4
Breakpoint 2: address = 0x00000001000012e4
- 정규식으로. 람다(이름에
#기호가 있는)처럼 생성된 artifact를 디버깅할 때 유용할 수 있어요:
(lldb) b -r main\(
3: regex = 'main\(', locations = 1
3.1: where = terminator.kexe`kfun:main(kotlin.Array<kotlin.String>) + 4 at hello.kt:2, address = terminator.kexe[0x00000001000012e4], unresolved, hit count = 0
GDB
- 정규식으로:
(gdb) rbreak main(
Breakpoint 1 at 0x1000109b4
struct ktype:kotlin.Unit &kfun:main(kotlin.Array<kotlin.String>);
- 이름으로는 불가능한데, 위치 기준 중단점에서
:이 구분자이기 때문이에요:
(gdb) b kfun:main(kotlin.Array<kotlin.String>)
No source file named kfun.
Make breakpoint pending on future shared library load? (y or [n]) y
Breakpoint 1 (kfun:main(kotlin.Array<kotlin.String>)) pending
- 위치로:
(gdb) b hello.kt:1
Breakpoint 1 at 0x1000109b4: file hello.kt, line 2.
- 주소로:
(gdb) b *0x1000109b4
Breakpoint 2 at 0x1000109b4: file hello.kt, line 2.
중단점 예제 출력
다음은 실제 중단점 설정의 출력 예시예요:
$ lldb program.kexe
(lldb) target create "./program.kexe"
Current executable set to './program.kexe' (x86_64).
(lldb) b main.kt:2
Breakpoint 1: where = program.kexe`kfun:main(kotlin.Array<kotlin.String>) + 4 at main.kt:2, address = 0x0000000100000f64
(lldb) r
Process 28292 stopped
* thread #1, name = 'program.kexe', stop reason = breakpoint 1.1
frame #0: program.kexe`kfun:main(kotlin.Array<kotlin.String>) at main.kt:2
target 0: (program.kexe) stopped.
변수 검사하기
var 변수에 대한 변수 검사는 프리미티브 타입과 비프리미티브 타입 모두 기본적으로 동작해요:
$ cat -n main.kt
1 fun main(args: Array<String>) {
2 var x = 1
3 var y = 2
4 var p = Point(x, y)
5 println("p = $p")
6 }
7
8 data class Point(val x: Int, val y: Int)
$ lldb ./program.kexe -o 'b main.kt:5' -o
(lldb) target create "./program.kexe"
Current executable set to './program.kexe' (x86_64).
(lldb) b main.kt:5
Breakpoint 1: where = program.kexe`kfun:main(kotlin.Array<kotlin.String>) + 289 at main.kt:5
(lldb) r
Process 4985 stopped
* thread #1, name = 'program.kexe', stop reason = breakpoint 1.1
frame #0: program.kexe`kfun:main(kotlin.Array<kotlin.String>) at main.kt:5
2 var x = 1
3 var y = 2
4 var p = Point(x, y)
-> 5 println("p = $p")
6 }
7
8 data class Point(val x: Int, val y: Int)
Process 4985 launched: './program.kexe' (x86_64)
(lldb) fr var
(int) x = 1
(int) y = 2
(ObjHeader *) p = Point(x=1, y=2)
(lldb) v p->x
(int32_t) p->x = 1
iOS 애플리케이션 디버깅하기
iOS 애플리케이션을 디버깅할 때는 크래시 리포트를 자세히 분석해야 하는 경우가 있어요. 크래시 리포트는 일반적으로 메모리 주소를 읽을 수 있는 소스 코드 위치로 변환하는 과정인 심볼리케이션(symbolication)이 필요해요.
Kotlin 코드의 주소를 심볼리케이트하려면(예: Kotlin 코드에 해당하는 스택 트레이스 요소) 특수 디버그 심볼(.dSYM) 파일이 필요해요. 이 파일은 크래시 리포트의 메모리 주소를 함수나 줄 번호 같은 소스 코드의 실제 위치와 매핑해요.
Kotlin/Native 컴파일러는 Apple 플랫폼에서 릴리스(최적화된) 바이너리용 .dSYM 파일을 기본적으로 생성해요. Xcode에서 빌드하면 IDE가 표준 위치에서 .dSYM 파일을 찾아 심볼리케이션에 자동으로 사용해요. Xcode는 IntelliJ IDEA 템플릿에서 만든 프로젝트의 .dSYM 파일을 자동으로 감지해요.
다른 플랫폼에서는 -Xadd-light-debug 컴파일러 옵션을 사용해 생성된 바이너리에 디버그 정보를 추가할 수 있어요(크기가 커져요):
kotlin {
targets.withType<org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget> {
binaries.all {
freeCompilerArgs += "-Xadd-light-debug=enable"
}
}
}
크래시 리포트에 대한 자세한 내용은 Apple 문서를 참고하세요.
알려진 문제
- Python 바인딩의 성능.
- 디버거 도구에서의 표현식 평가는 지원되지 않으며, 현재 구현 계획도 없어요.
다음 단계?
iOS 코드에서 처리되지 않은 Kotlin 예외에 대한 크래시 리포트를 개선하는 방법을 알아보세요.
더 알아보기
- Kotlin/Native 바이너리 옵션
- Kotlin/Native 지원 타깃 및 호스트