Search

[WWDC] Debug Swift debugging with LLDB

들어가며

LLDB는?
Xcode와 함께 제공되는 기본 디버깅 기술입니다.
LLDB를 사용해서 응용 프로그램에서 이러한 작업들을 실행할 수 있습니다.
1.
breakpoint를 설정
2.
실행을 일시 중지
3.
변수와 object의 상태를 검사
4.
코드를 탐색
LLDB는 코드가 수행하는 작업을 이해하는 데 도움이 될 수 있으며 코드의 동작이 예상과 다른 지점을 찾을 수 있습니다.
>> 코드를 이해하고 탐색하는 강력한 도구
이번 세션에서는 Swift코드 디버깅에 고유한 영향을 미치는 몇 가지 고급 워크 플로우를 알아봅니다.

What’s going on here

Example) Text Adventure
 : 터미널에서 텍스트 인터페이스를 사용해서 게임을 실행해볼게요. inventory부터 확인해볼게요.
: 흠, 이 센서가 흥미로워 보이네요. 혹시 센서에 있는 아이폰을 사용할 수 있을까요?
⁇ : 내가 아이폰을 떨어뜨렸다고? 어, 그건 제가 보여드리고 싶었던 게 아니에요. 내 게임에 버그가 있는 것 같아요.
 : 명령이 올바르게 읽혔는지 먼저 확인해봅시다. “words” 변수에 토큰화된 명령이 포함되어 있어요.
 : 아; 예상대로 되지 않았네요. 이게 무슨 일이죠? 어제는 디버거를 문제없이 사용하다가 어젯밤에는 터미널에서 텍스트를 스타일링하기 위해 이 UI 프레임워크를 통합했어요. 이 프레임워크가 내 디버깅 문제와 관련이 있는지 궁금해요. 내가 명백하게 디버그 빌드를 다운로드했음에도 불구하고 프레임워크의 소스 코드에 발을 들여놓을 수 없어요.. 저것 좀 봐!
I only see disassembly.

Let’s start by figuring out why I couldn’t see any source code.

LLDB가 소스 코드를 표시하기 위해서 필요한 건 무엇일까요?
컴파일러는 함수를 컴파일할 때 기계어를 생성합니다.
그리고 debugger를 위한 breadcrumbs를 남겨서 실행 파일의 주소를 소스 파일과 line number로 매핑할 수 있고 그 반대도 가능합니다.
>> breadcrumbs : 디버그 정보
Apple 플랫폼에서는 디버그 정보가 개체 파일에 저장됩니다. 보관 및 배포를 위해 디버그 정보를 .dSYM bundle에 연결합니다.
>> dsymutil : 디버그 정보 linker
LLDB는 Spotlight를 사용해서 dSYM 번들의 위치를 찾습니다. dSYM는 disk 위치 측면에서 상당히 유연합니다.
back to the example!
먼저, LLDB가 실제로 프레임워크에 대한 dSYM을 찾았는지 확인합니다.
 : image list command로 할 수 있어요!
 : 예! LLDB가 프레임워크에 대한 dSYM을 찾았습니다. 즉, 디버그 정보에 액세스할 수 있습니다.
>> Why? 보관 및 배포를 위해 디버그 정보를 .dSYM bundle에 연결했기 때문에
 : image lookup를 사용하여 현재 주소에 대한 자세한 정보를 얻을 수 있습니다.
 : 아!! 소스 코드가 없는 이유를 알 것 같습니다. 여기서 이 원본 경로는 소스가 로컬 컴퓨터의 위치가 아니라 빌드 서버의 위치를 가리킵니다. 우리는 고칠 수 있어요! LLDB에는 이러한 경로를 리디렉션하는 데 사용할 수 있는 기본 제공 소스 맵이 있습니다.
해당 명령어를 사용하면 경로를 리디렉션 할 수 있어요.
 : 하지만 우리는 이 변경 사항을 더 영구히 변경했으면 합니다.
 : Product, Scheme, Edit Scheme으로 이동하거나 Play 버튼을 클릭하기만 하면 나타나는 Scheme 편집기에서 프로젝트별 LLDB init 파일을 정의할 수 있습니다.
 : 이제 LLDB를 설정했으니 프로젝트를 다시 실행해 봅시다. 그리고 이제 우리는 소스 코드를 가지고 있습니다.

LLDB Setting

LLDB는 settings set target.source-map을 사용하여 소스 경로를 다시 매핑할 수 있습니다.
 : 이 명령을 프로젝트의 .ldbinit 파일에 넣어서 자동으로 실행할 수 있어요.
각 .dSYM 번들에는 prefix remapping dictionary를 넣을 수 있는 XML.plist파일이 포함되어 있습니다.
서버에서 최신 빌드를 가져오는 다운로드 스크립트가 있는 경우 해당 스크립트를 수정하여 다운로드된 파일에 적절한 재매핑 사전을 자동으로 주입할 수 있습니다.
 : 프로세스에 대한 자세한 내용은 LLDB 웹 사이트를 참조해주세요!
Source paths는 언어에 특화된 것이 아니므로 이 방법은 Swift, C++, Objective-C 프로젝트에서 동일하게 작동해요!
build server farm에서 소스 코드가 컴파일되면 소스 파일의 원격 경로가 컴퓨터마다 다를 수 있어요.
머신당 하나의 remap prefix를 정의할 필요가 없도록, 우리는 컴파일로가 소스 경로를 디버그 정보에 넣기 전에 정규화하도록 지시할 수 있습니다.
-debug-prefix-map 옵션을 사용하여 수행됩니다.
이 방식으로 시스템별 path prefix는 LLDB의 로컬 경로에 remapped될 수 있는 Canonical placeholder name(고유한 표준 자리 표시자)로 대체될 수 있습니다.
back to example!
저는 word에 대한 객체 설명을 print하려고 했습니다.
⁇ : 그것은 효과가 없었다. 사실 'words'이라는 표현을 평가하는 것만으로도 효과가 없었다.
적어도 변수 보기에서 변수를 볼 수 있습니다.
Xcode 변수 뷰와 동등한 콘솔은 frame variable 또는 v 명령입니다.

We need to learn more about LLDB

그렇다면 포는 무엇이고 왜 여전히 작동하지 않는 것일까요?
참고로 LLDB는 디버거입니다. 그러나 LLDB는 단순한 디버거가 아닙니다. 그것은 컴파일러이기도 합니다!
LLDB는 디버거 기능 외에도 Swift, Clang compiler의 기능을 포함하고 있어요.
이러한 컴파일러가 p, po alias를 통해 알 수 있는 LLDB expression evaluator를 구동합니다.
expression evaluator는 변수를 보는 것을 넘어 계산을 수행
함수를 호출
프로그램 상태를 변경
디버거는 로컬 변수를 어떻게 포맷할까요?
컴파일러가 제공하는 디버그 정보는 디버거에 변수가 저장된 위치를 알려줍니다.
하지만 이 정보로는 LLDB는 임의의 원시 바이트 모음만 보여줄 수 있을거예요.
 : 그러면 어떻게 nicely formatted output를 만들어낼 수 있을까요?
The answer is types.
Type information를 통해 LLDB는 소스 변수의 구조와 메모리 레이아웃을 이해할 수 있습니다.
Type information를 통해 LLDB는 type이 어떤 필드를 가지며 type를 통해 LLDB가 적절한 data formatter를 사용하여 pretty-print를 할 수 있는지 알 수 있어요.
 : 이제 Type Information의 출처를 알아보겠습니다.
frame variablev 명령이 있는 Debugger side에서는 LLDB가 디버그 정보에서 type information를 가져와요.
LLDB는 Swift Reflection metadata에서 type를 가져와요.
expression evaluator, po가 있는 Compiler side에서는 LLDB는 type information를 모듈로부터 가져와요.
이런 깔끔한 분리는 Xcode 14에 새로나온 것으로 expression evaluator가 아니더라도 variable view가 완전히 기능할 수 있는 이유를 설명해줍니다.!
모듈은 컴파일러가 형식 선언을 구성하는 방법이에요.
 : 여러분에게 편리한 새로운 기능을 보여드리고 싶어요

Swift-healthcheck

컴파일러 측에서 발생하고 있는 문제를 어떻게 진단하나요?
올해 LLDB는 swift-healthcheck 명령을 추가했어요. 모듈 가져오기에 실패했는지 확인하는 첫 번째 stop입니다.
 : 어떻게 작동하는지 보여드리죠
문제가 발생한 후 swift-healthcheck를 실행하면 Swift expression evaluator configuration 로그에 접근할 수 있습니다.
로그 끝에 LLDB가 TerminalUI를 가져오는데 문제가 있음을 알 수 있어요.
이름에 근거해서 저는 TerminalInterface framework의 구현사항이라는걸 유추할 수 있어요.
이 누락된 모듈은 문제가 됩니다.
self 타입은 UI 구현에 비해 Generic하고 해당 type를 포함하는 모듈이 없으면 expression evaluator가 동적 type인 self를 실현할 수 없기 때문이에요.
 : 그동안 LLDB 컴파일러가 Swift 모듈을 어떻게 찾는지 살펴봐요!
1.
내 앱에는 자체 스위프트 모듈이 있습니다. 이 모듈은 Foundation과 같은 시스템 프레임워크를 가져올 수 있습니다.
>> 시스템 프레임워크 : SDK에 상주하는 안정적인 textual stable Swift Interface file
2.
모든 스위프트 모듈은 module map file의 도움을 받아서 그룹화된 하나 이상의 Clang 모듈을 가져올 수 있어요. Clang 모듈은 다른 Clang 모듈에 의존할 수 있습니다.
>> Clang 모듈 : 헤더 파일에 대한 고급 이름
3.
앱은 로컬로 빌드된 프레임워크에 속하는 Swift 모듈을 가져올 수도 있어요.
4.
SDK의 일부가 아닌 textual Swift interface file도 가져올 수 있습니다.
5.
앱은 Swift 코드가 포함된 정적 라이브러리와 링크될 수 있습니다. 이 라이브러리는 Swift 모듈과 함께 제공됩니다.
6.
Clang 모듈을 가져올 수 있는 briding-header도 있습니다.
7.
LLDB만의 특수 기능으로, 일부 모듈 내용은 디버그 정보에서만 재구성할 수 있어요.
 : 정말 많은 source들이죠! LLDB는 이걸 어떻게 다 찾나요?
LLDB가 모듈을 찾을 수 있도록 모듈을 패키징하는 것이 빌드 시스템의 일이에요.
시스템 프레임워크의 모듈은 SDK에 남아요.
LLDB는 프로그램에 첨부할 때 SDK와 일치하는 SDK를 찾아요.
객체 파일에서 직접 디버깅할 땐 LLDB는 빌드 시 SDK가 아닌 모든 모듈을 찾아요.
Dsymutil은 모든 동적 라이브러리, 프레임워크 또는 dylib 및 실행 파일에 대해 DSYM 번들로 불리는 디버그 정보 아카이브를 패키징할 수 있다.
각 .dSYM 번들은 브리징 헤더, 텍스트 스위프트 인터페이스 파일, 그리고 가장 중요한 디버그 정보를 포함할 수 있는 바이너리 스위프트 모듈을 포함할 수 있어요.
 : 그게 모든 걸 커버해요. 전부?
 : static archives에 속하는 Swift 모듈을 제외한 모든 항목입니다.

Register Swift modules with the Linker

Swift module을 dsymutil로 픽업하려면 linker에 등록해야 합니다.
동적 라이브러리 및 실행 파일의 경우 빌드 시스템이 자동으로 이 작업을 수행해요.
하지만 정적 아카이브는 링커에 의해 생성되지 않으며 zip 파일과 같은 객체 파일의 모음일 뿐이에요.
Swift module을 linker에 등록하는 책임은 static archive에 연결하는 모든 실행 파일 또는 동적 라이브러리에 있습니다.
>> 많은 경우 Xcode 빌드 시스템이 이를 대신해줘요.
>> 고유한 사용자 지정 빌드 시스템을 유지 관리하거나 사용자 지정 빌드 규칙을 정의한 경우 주의해야 합니다!

Apple Linker

Apple 링커를 사용할 경우 Swift 모듈을 -add-ast-path 옵션으로 등록해야 합니다.
빌드 로그를 확인하여 해당되는지 확인합니다.
dsymutil을 사용하여 실행 파일의 기호 테이블을 dump하고 swift module에 대한 grep을 사용하여 실행 가능한지 확인할 수 있습니다.

Linux

Swift driver는 이진 Swift 모듈 파일을 나머지 디버그 정보와 함께 binary로 링크할 수 있는 개체로 변환하는 -module wrap 동작을 지원합니다.
LLDB가 알아서 거기서 찾을 겁니다.
프레임워크 빌드 시스템의 일부로 정적 아카이브가 사용돼요.
>> dSYM 번들에서 빠진 것은 Static Archive에 속한 Swift 모듈이었습니다.
 : fixed version of the framework를 받았고, 누락된 정적 모듈을 링커에 등록했고 dsymutil이 이를 수집할 수 있어요.
 : 이제 self도 해결됐습니다.
 : 그리고 "word"에 대한 객체 설명을 인쇄할 수 있습니다.
 : 어차피 콘솔을 사용하고 있기 때문에, 저는 s alias를 사용하여 parseFrom 함수에 들어갑니다.
 : 이제 우리는 또한 쉽게 버그를 찾을 수 있는데, 이것은 단지 복사, 붙여넣기 오류네요.
 : 마무리하기 전에 한 가지 더 주의할 사항이 있어요.
Swift Compiler는 Clang header 검색 경로 및 기타 관련 옵션을 바이너리 .swift 모듈 파일로 직렬화해요.
이는 Clang 모듈 종속성을 가져오는 것이 빌드하는 동안만 작동하기 때문에 좋습니다.
다른 기계에 구축하면 이러한 로컬 경로가 손상될 수 있어요.
바이너리 .swift 모듈을 다른 머신으로 전송하기 전에 -no-serialize-debugging-options 컴파일러 플래그를 사용하여 빌드하는 것을 고려하세요.
>> Xcode에서는 SWIFT_SERIALIZE_DEBUGGGING_OPTIONS 설정을 통해 제어됩니다.
다음 설정 중 하나를 사용하여 LLDB에 이러한 검색 경로를 다시 도입할 수 있습니다.

정리하며

1.
다른 개발자에게 이진 프레임워크를 보냈을 때 디버거의 코드로 들어갈 것으로 예상되지 않는다면 스위프트 모듈을 textual .swiftinterface 파일로 보내는 것이 가장 좋습니다.
2.
디버깅할 것으로 예상되는 빌드 서버 또는 CI를 설정하는 경우 바이너리 Swift 모듈을 빌드하고 search path serialization를 고려해야 합니다.
-debug-prefix-map 옵션을 사용하여 디버그 정보의 서버에서 원본 경로로 정규화할 수 있어요.