Search

[WWDC] LLDB: Beyond “po”

들어가며

LLDB가 어떻게 작동하는지에 대해서 이야기해봅니다.
+) 출력 format를 지정하는 강력한 메커니즘과 함께 소스 코드의 변수를 볼 수 있는 다른 방법도 제시합니다.
LLDB : Xcode에서 variable view를 작동시키는 debugger
여기서 정의한 변수와 변수의 유형을 볼 수 있습니다.(variable view)
Xcode로 디버깅하는 동안 명령을 직접 보내고 LLDB와 상호 작용할 수도 있습니다.(console)
당신의 앱이 버그를 조사하는 동안 소스 코드를 정의하는 변수 값을 print할 수 있는 기능도 포함됩니다.
LLDB는 이 작업을 수행할 수 있는 여러 가지 방법을 제공합니다.
각각은 서로 다른 상충 관계(trade-off)를 가지고 있습니다.

Trip Example

 지중해를 일주하는 유람선을 타러 갑시다.
struct Trip { var name: String // 이름 var destinations: [String] // 목적지 목록 }
Swift
복사

po

standing for print object description
이 명령을 사용하면 type의 instance를 텍스트로 표현한 object description를 얻을 수 있습니다.
System runtime에서 po를 기본적으로 제공하지만 커스텀해서 사용할 수도 있습니다.
CustomDebugStringConvertible 프로토콜 추가
*이 프로토콜은 debugDescription 이라는 단일 속성이 필요합니다.
이제 debugger에서 object description를 인쇄하면 기본 설명 대신 사용자가 제공한 설명을 볼 수 있습니다.
 : 변경 내용은 최상위 설명에만 적용됩니다. 하위 구조를 수정해야 하는 경우 CustomerReflectable Protocol 에 대한 설명서를 참조하세요!
*Objective-C 객체에 대해서도 가능합니다.
 : 그러나 po는 단순히 변수를 인쇄하는 것 이상의 일을 합니다.
 : cruise 이름의 대문자로 된 버전을 계산해낸다던가, 목적지를 알파벳 순으로 정렬할 수 있습니다.
po는 임의의 식을 평가할 수 있습니다.
 프로그램에서 주어진 prompt(console)에 컴파일되는 모든 것은 comment에 대한 argument로 전달될 수 있습니다.
 : po는 object description을 인쇄하기 위한 argument가 있는 expression이라는 명령어의 alias 입니다.
LECC와 LLDB는 keystrokes를 저장하는 편리한 방법
당신이 po를 직접 구현하고 싶다면 위의 예제처럼 구현하면 됩니다.
command alias <지정하고 싶은 별칭> <별칭을 지정할 명령>
Swift
복사

“po” Under the Hood

 po가 수행해야 하는 단계를 살펴봅시다.
LLDB를 사용하는 언어의 완전한 expressivity을 제공하기 위해 expression 자체를 구문 분석하고 평가하지 않습니다.
대신!”
 : 여기에 표시된 스니펫과 유사하게 지정한 식에서 컴파일할 수 있는 소스 코드를 생성하는 것으로 시작합니다.
 : 그런 다음 내장된 Swift 및 claim compiler를 사용하여 디버깅된 프로그램의 context에서 실행되는 코드를 컴파일합니다. 실행이 완료되면 LLDB는 결과 값에 access해야 합니다.
 : 여기서부터 object description를 가져와야 합니다. 이를 위해 LLDB는 이전 결과를 다른 소스 코드로 래핑합니다.
 : 디버그 프로세스의 컨텍스트에서 컴파일되고 실행됩니다. 이 실행의 결과는 LLDB가 po 명령의 결과로 표시할 문자열입니다.
 : po는 LLDB에서 변수를 인쇄하기 위해 우리가 제시하는 세 가지 방법 중 첫 번째에 불과해요!

p

print without the object description
po에서 제공하는 표현과 약간 다르지만 동일한 내용을 담고 있다는 점에서 비슷하다.
 : 결과 값에 $R0라는 이름이 지정된 것을 알아야 합니다.
LLDB의 특별한 컨벤션
각 식의 결과에 $R1, $R2와 같은 증분 이름이 지정됩니다.
프로젝트의 다른 변수와 동일한 방법으로 $R0를 참조 가능합니다.
p는 LLDB의 first-class command가 아닙니다.
expression 명령어에 대한 alias일 뿐이고 뒤에 -- object description이 없습니다.

“p” Under the Hood

 p가 수행해야 하는 단계를 살펴봅시다.
p는 description를 가져올 필요가 없기 때문에, 그만큼 많은 일을 할 필요가 없습니다.
 : 해당 diagram에서 po에 대한 설명을 상기시킬 수 있어요. 식을 컴파일하고 실행하는 첫 부분은 두 명령에서 정확히 동일합니다.
“그러나!”
결과를 얻으면 LLDB에서 dynamic type resolution이라는 단계를 수행합니다.
 : 좀 더 자세히 설명해보기 위해서 코드 예제를 조금 수정해볼게요.
프로토콜 Activity에 맞게 Trip 구조를 변경합니다.
In Swift, 런타임에서의 type의 정적 표현과 동적 표현은 반드시 같지 않습니다.
For example)
cruise는 Activity의 정적 type입니다. 그러나 런타임에서 변수는 동적 type인 Trip type의 instance를 가집니다.
cruise 값을 print하면 LLDB가 주어진 지점에서 주어진 변수에 대한 가장 정확한 타입을 표시하기 위해 결과 메타데이터를 다시 전달하기 때문에 Trip 타입을 얻게 됩니다.
 dynamic type resolution
p 명령어를 사용하면 dynamic type resolution은 expression의 결과만 수행합니다.
if cruise의 한 속성에 접근하고 싶다고 가정해 봅시다,
LLDB가 p를 통해 이 식을 평가하려고 할 때 cruise는 Activity 타입의 객체이며 name이라는 구성원을 가지고 있지 않다는 것을 알 수 있습니다. 실패!
why?
이 문제는 LLDB가 코드를 컴파일하기 때문에 발생합니다.
실행 중인 p와 표시되는 타입은 소스 코드에 있는 정적 타입입니다.
소스 코드에 cruise.name 이라는 식을 입력하는 것과 동일합니다. 정적 컴파일러가 오류로 인해 이를 거부합니다.
 : 오류를 없애려면 먼저 object를 동적 유형에 명시적으로 캐스팅한 다음 결과 필드에 access해야 합니다.
(*디버거, 소스 코드 모두 해당)
 : p의 일이 아직 다 끝나지 않았습니다. 결과에 dynamic type resolution를 수행한 후, LLDB는 결과 object를 formatter 하위 시스템으로 전달합니다.
type에 대한 formatter가 없는 경우 문자열은 $R0 처럼 보입니다.
직접 시도하려면 --raw 옵션을 사용하면 됩니다.
표준 라이브러리 type은 문자열과 정수와 같은 단순한 라이브러리 유형도 속도와 크기에 매우 최적화되어 있기 때문에 복잡한 표현을 가지고 있습니다.
formatter가 작동하면 $R1 처럼 문자 시퀀스로 표시합니다.
LLDB는 일반적으로 사용되는 여러 타입들을 알고 있으며 이들을 위한 formatter를 제공합니다.
사용자가 formatter를 커스터마이즈할 수 있습니다.

v

v의 출력은 우리가 방금 설명한 formatter에 의존하기 때문에 p와 정확히 동일합니다.
p와 동일한 expression를 출력하는 v
 : v는 Xcode 10.2에서 frame variable 명령에 대해 소개한 alias입니다.
v 명령어는 코드를 컴파일, 실행하지 않으므로 속도가 매우 빠릅니다.
v는 코드를 컴파일하지 않기 때문에 디버깅 중인 언어와 다를 수 있는 자체 구문을 수행합니다.
예를 들어, dot 연산자와 subscript 연산자를 사용하여 필드에 액세스합니다.
overload를 수행하지 않으면서 코드를 실행해야 하므로 계산된 속성을 평가할 수 없습니다.
 : 당신이 필요하다면 p나 po를 사용할 수 있습니다.
추측할 수 있듯이 v는 변수를 인쇄하는 다른 두 가지 메커니즘(p, po)과 상당히 다르게 작동합니다.

“v” Under the Hood

 우리는 변수를 인쇄하고 싶습니다.
1. v는 먼저 프로그램 상태를 참조합니다.
2. 메모리에서 변수를 찾습니다. 메모리에서 변수 값을 읽습니다.
3. dynamic type resolution를 수행합니다.
 : 사용자가 하위 필드에 액세스하도록 요청한 경우, 각 라운드에서 dynamic type resolution을 수행하는 각 하위 필드에 대해 단계를 반복합니다.
작업이 완료되면 결과를 데이터 formatter 하위 시스템으로 전달합니다.
p와 v의 작동 방식의 큰 차이
v는 dynamic type resolution를 여러 번 수행한다.
formatter는 실제로 dynamic type resolution를 한 번만 수행한다.
각 interpretation 단계에서 dynamic type resolution을 수행함으로써 v는 cruise가 Trip 유형의 object임을 이해하고 메모리에 있는 필드에 액세스할 수 있습니다.
 : v가 p보다 훨씬 강력하며 p가 명시적인 cast없이 자신의 유형을 확인할 수 있게 합니다.

Displaying Variables

 : how objects are presented.
po
object description
p, v
data formatter를 사용하여 object 표시
po, p
식을 컴파일하고 전체 언어에 액세스 가능
v
고유한 구문을 사용하여 식을 해석하고 해석의 각 단계에 대한 dynamic type resolution을 수행

Customizing Data Formatter

LLDB에서 Data Formatter는 디버거에서 변수를 표시하는 방법을 정의

building formatter

예를 들어 v를 사용할 때 cruise destination를 출력할 수 있으며 배열 요소가 읽기 쉬운 형식으로 표시됨
기본 formatter는 사용자 정의 형식과 표준 라이브러리에서 가져온 형식 모두에 적합
 : 그러나 때로는 기존 formatter를 수정하거나 직접 찾고 싶을 수도 있다.
 : LLDB의 data formatter는 확장 가능하기 때문에 가능합니다.
모든 type은 고유의 표현을 가질 수 있고 해당 표현을 사용자 정의할 수 있도록 LLDB는 아래 3가지 항목을 제공합니다.
filters
기존 formatter의 출력을 제한하는 데 사용
예를 들어 현재는 여행의 목적지가 몇 개밖에 없지만, 숫자가 늘어나면 destination이 너무 많이 출력되게 된다.
 : 필터를 추가하면 Trip 이름만 표시되도록 지정하면 이름만 나오는걸 볼 수 있다.
type filter delete Travel.Trip
이는 console의 formatter output뿐만 아니라 Xcode의 변수에도 영향을 미칠 수 있기 때문에 다른 단계로 넘어가기 전에 필터를 제거해줘야 한다.
String Summary
type의 문자열 표현을 제공
해당 type에 대한 정보를 한 눈에 제공하기 위해 po와 함께 사용할 수 있도록 구현한 description과 동등한 data formatter를 사용합니다.
(*filter와 마찬가지로 Xcode 변수 뷰에 영향을 미칩니다.)
 : Trip 의 모든 구성원들은 summary를 가지고 있지만, Trip 자체는 그렇지 않으니 그 부분을 수정해봅시다.
좋은 summary가 first destination이자 last destination
summary string에는 일반 텍스트 및 특수 변수가 포함될 수 있습니다.
이 변수들은 $ 기호로 시작하며 곱슬곱슬한 괄호로 둘러싸여 있습니다.
v 명령어와 동일한 구문을 사용합니다.
summary가 정의되는 type은 var. 로 액세스합니다.
 : 하지만 summary에 문제가 있습니다.
정확히 3개의 목적지가 포함된 여행에만 사용할 수 있어요! formatter는 배열의 개수처럼 계산된 변수에 액세스할 수 없기 때문에 마지막 요소(var.destination[2])를 하드 코딩해야 합니다.
 : 다행히도, 우리가 이용할 수 있는 또 다른 강력한 도구가 있습니다.

Python formatter

임의의 계산을 수행할 수 있으며 현재 디버그 세션 상태에 액세스하기 위한 여러 object를 제공하는 LLDB’s scripting bridge API에 대한 완전한 액세스 권한이 있습니다.
대상 : 현재 디버깅 중인 프로그램
프로세스, 스레드 및 프레임은 해당 런타임 정보에 대한 액세스를 제공합니다.
변수, 레지스터 또는 expression의 값은 SB value class로 표시됩니다.
data formatter는 value와 type을 탐색하는 데 사용되기 때문에 특히 유용합니다.
Xcode 11부터 시작하여 scripting은 Python3를 사용합니다.
LLDB API
Script 명령을 실행하면 상호 작용하는 Python 인터프리터에 의해 올라갑니다.
현재 프레임은 lldb.frame 변수에 액세스할 수 있습니다.
 SBFrame 인스턴스를 반환합니다.
 : 위의 예시에서 알 수 있듯 우리는 현재 프레임에 cruise라는 변수가 포함되어 있다는 것을 알고 있습니다. 따라서 변수 찾기를 사용해서 SB의 값을 얻을 수 있습니다.
 : object는 비밀리에 data formatter를 작동시키기 때문에 data formatter 출력과 동일하게 보이는 것은 당연한 일입니다.
cruise에 있는 destination 멤버 이름들은 GetChildMember를 호출하여 액세스할 수 있습니다.
그 결과 원하는 배열을 나타내는 다른 SB값이 생성되었습니다.
 : 이번에는 마지막 요소의 index를 하드 코딩하지 않고 Python을 사용하여 이전 formatter를 모방해 볼게요.
원하는 SB 값에 GetNumChildren을 사용하여 element의 수를 얻을 수 있습니다.
GetChildAtIndex를 사용해서 첫 번째 element와 마지막 element에 액세스할 수 있습니다.
인쇄된 값은 상황에 맞게 구분됩니다. 배열에는 인덱스가 포함되어 있습니다.
SB 값 instance는 상위 관계의 context를 유지합니다.
 : 우리는 모든 것을 하나의 끈으로 묶을 수 있어요.
print begin, end를 통해 SB value object에 대한 설명을 얻을 수 있습니다.
우리가 여기서 정말로 원하는 것은 summary이기 때문에, GetSummary를 사용하여 포맷된 값을 검색하고 사용할 수 있습니다.
 : 결과가 문자열 자체만 배치한 걸 볼 수 있어요!
 : 모든 걸 합쳐보자! Trip.py라는 파일을 만듭니다!
*formatter는 디버거 console에서 직접 정의하거나 파일을 사용하여 LLDB에 로드할 수 있습니다.
1. SB value을 input argument로 함수에 전달합니다.
2. 나머지 구현은 이전에 실행했던 것과 거의 동일
Python를 사용하여 formatter를 정의하면 제어 흐름이 존재합니다.
첫 번재 및 마지막 대상에 대한 summary를 가져오고 summary string으로 돌아갑니다.
 : 새로운 summary 공급자를 LLDB에 로드합시다.
command script import 명령을 사용하여 수행합니다.
1. Trip type에 사용할 새 formatter를 지정해야 합니다.
2. type summary를 사용하여 type를 추가하고 포맷할 type과 사용할 공급자 함수를 제공합니다.(fully qualified type사용)
3. 모든 것이 연결된 상태에서 v는 이제 Python summary provider를 사용하여 cruise object를 print합니다.
summary는 console에 표시될 뿐만 아니라 Xcode의 variable view에도 표시됩니다.
Xcode Variable View에 type를 늘린 것과 같이 노출되는 children의 type를 사용자는 정의할 수 있습니다.
Python에서는 각 하위 항목이 SB value를 가지며, 각각 고유한 summary를 가질 수 있습니다.
 : 사용자의 가상의 하위 공급자를 정의하는 것은 summary provider를 정의하는 것과 유사합니다.
함수 대신 특정 메서드를 구현하는 클래스를 정의합니다.
init 외에도 num_children get_child_at_index get_child_index 을 제공합니다.
이전과 마찬가지로 명령 script 가져오기를 사용해서 Python 소스 코드를 LLDB에 로드합니다. 이전에 이 파일을 이미 로드했으면 명령을 다시 실행하면 파일이 다시 로드됩니다.
 : 사용자 지정 공급자를 정의하는 작업을 거친 후 디버그 세션이 끝날 때 해당 공급자를 잃고 싶지 않습니다.
console에서 입력하는 모든 명령은 홈 디텍토리의 .ldbinit 파일에 보존될 수 있습니다.
*.ldbinit 이 파일은 디버그 세션을 시작할 때 자동으로 로드됩니다.

Summary

LLDB에는 디버깅하는 동안 프로그램 상태를 볼 수 있는 다양한 기능이 존재합니다.
p, po, v
값을 표시할지, 코드를 실행할지, object desctiption을 가져올지 여부에 따라서 사용합니다.
filter, string summaries, synthetic children
고유한 data formatter를 사용자 정의합니다.
Python 3
Python 2로 작성된 스크립트가 있으며 Python 3와 호환되도록 업데이트합니다.