po는 어떻게 동작하는가? 다른 방법으로 볼 수는 없을까?
Xcode에서 브레이크 포인트를 걸면, 다음과 같이 변수를 볼 수 있다.
그 옆에서는 lldb에 직접 커맨드를 보낼 수 있다.
예시로 쓸 코드
struct Trip {
var name: String
var destination: [String]
}
let cruise = Trip(
name: "Mediterranean Cruise",
destination: ["Sorrento", "Capri", "Taormina"]
)
po
print object description을 뜻한다
특정 타입의 텍스트 형태의 description을 출력한다.
(lldb) po cruise
🔽 Trip
- name: "Mediterranean Cruise"
🔽 destination: 3 elements
- 0 : "Sprrento"
- 1 : "Capri"
- 2 : "Taormina"
기본 제공 되는 것이 있지만, CustomDebugStringConvertible을 이용하면 커스텀도 가능하다.
다만 이 경우는 Top level desription만 바뀐다.
그 아래의 구조까지 커스텀하고 싶으면, CustomReflectable을 써야 한다.
Objective-C도 debugDescription() 메소드를 구현하면 동일하게 동작한다.
extension Trip: CustomDebugStringConvertible {
var debugDescription: String { "Trip description" }
}
(lldb) po cruise
🔽 Trip description
- name: "Mediterranean Cruise"
🔽 destination: 3 elements
- 0 : "Sprrento"
- 1 : "Capri"
- 2 : "Taormina"
실제로는 해당 지점에서 컴파일 가능한 임의의 표현식을 평가하는 것이 가능하다.
(lldb) po cruise.name.uppercased()
"MEDITERRANEAN CRUISE"
(lldb) po cruise.destination.sorted()
🔽 destination: 3 elements
- 0 : "Capri"
- 1 : "Sprrento"
- 2 : "Taormina"
이는 사실 po가 alias이기 때문이다.
expression --object-description -- cruise
alias는 유저도 얼마든지 가능하다.
command alias my_po expression --object-description
(lldb) po cruise.name
"Mediterranean Cruise"
po의 동작 원리
(lldb) po view
사용하고 있는 언어의 표현력을 활용하기 위해, LLDB는 직접 표현식을 평가하지 않는다.
대신 컴파일이 가능한 소스코드를 주어진 표현식을 기반으로 만든다.
// 실제와는 다를 수 있지만, 대략 이런 식으로
func __lldb_expr() {
__lldb_res = view
}
만들어진 소스코드를 내장된 컴파일러로(여기서는 Swift) 컴파일하고, 디버깅 중인 프로그램 context에서 실행하고 그 결과를 돌려받는다.
결과 값을 받았으니 여기서 object desription을 가져와야 한다.
앞에서 가져온 결과 값을 다른 코드로 래핑한다.
func __lldb_expr2 -> String() {
return __lldb_res.description
}
이 코드 역시 아까와 마찬가지로 컴파일 된 뒤 디버깅 중인 프로그램 컨텍스트에서 실행 후, 결과를 가져온다.
이렇게 가져온 결과를 유저에게 보여준다.
p
object desription이 없는 print
po와는 결과값의 모양이 다르게 나올 것이다. 하지만 담긴 정보는 동일하다.
(lldb) p cruise
(Travel.Trip) $R0 = {
name = "Mediterranean Cruise"
destination = 3 values {
[0] = "Sorrento"
[1] = "Capri"
[2] = "Taormina"
}
}
$R0는 LLDB의 특별한 컨벤션이고, expression을 실행할 때 마다 뒤의 숫자가 올라간다.
이후 표현식 평가에서 해당 변수를 프로그램 안에서의 다른 변수처럼 그대로 쓸 수 있다.
(lldb) p $R0.description
([String]) $R1 = 3 values {
[0] = "Sorrento"
[1] = "Capri"
[2] = "Taormina"
}
po 역시 alias다
expression
동작 원리
처음 결과를 가져오는 부분까지는 po와 동일하다.
결과를 가져오면, dynamic type resolution을 수행한다.
dynamic type resolution은 무엇인가?
swift에서는 소스코드에서의 static representation과 runtime의 dynamic type이 일치할 필요는 없다.
protocol Activity { }
struct Trip: Activity {
var name: String
var destination: [String]
}
let cruise: Activity = Trip(...)
그래서 LLDB에서는 최대한 정확한 데이터를 보여주기 위해서, 결과값의 메타데이터를 확인해서 보여주게 된다.
결과 값에 대해서만 dynamic type resolution을 수행하기 때문에, 실제로 코드상으로 유효하지 않은 표현식(static)은 p 명령어가 실패하게 된다.
p cruise.name
error: <EXPR>:3:8: error: value of type 'Activity' has no member 'name'
성공하게 하려면, 강제로 캐스팅해야 할 것이다.
(lldb) p (cruise as! Trip).name
(String) $R0 = "Mediterranean Cruise"
이후, LLDB의 Formatter 서브 시스템으로 넘겨서 사람이 읽을 수 있는 형태로 포매팅하게 된다.
String이 Formatter를 거치지 않는 경우
(lldb) expression --raw -- cruise.name
(Swift.String) $R0 = {
_guts = {
_object = {
_countAndFlagBits = {
_value = 7305804402515733574
}
}
}
...
}
LLDB는 일반적으로 쓰이는 여러 타입에 대해서 기본적인 Formatter를 가지고 있다.
v
기본적으로는 p와 동일한 결과
Xcode 10.2에서 추가된 alias다
(lldb) frame variable cruise
위의 두 커맨드와 다르게 컴파일과 코드 실행 단계를 거치지 않기 때문에 더 빠르다.
대신 사용하는 언어와는 별개의 고유 문법을 가지고 있다.
동작 원리
(lldb) v variable.field1.field2
dynamic type resolution을 여러번 하면 뭐가 다르지?
코드상으로는 해당 프로퍼티가 보이지 않아도, 실제로 해당 프로퍼티가 있다면, 찾아서 읽을 수 있다.
protocol Activity { }
struct Trip: Activity {
var name: String
var destination: [String]
}
let cruise: Activity = Trip(...)
(lldb) v cruise.name
(String) cruise.name = "Mediterranean Cruise"
정리
LLDB Data Formatter 커스터마이징하기
Filters
이미 존재하는 Formatter의 결과를 filtering하기 위해 사용
(lldb) type filter add Travel.Trip --child name // Travel.Trip에 filter 추가. name만 출력한다.
(lldb) v cruise
(Travel.Trip) cruise = (name = "Mediterranean Cruise")
(lldb) type filter delete Travel.Trip // Travel.Trip에 걸려 있는 filter를 제거한다.
String Summaries
(lldb) type summary add Travel.Trip --summary-string
"$(var.name} from ${var.destinations[0]} to ${var.destinations[2]}"
Python 쪽에 제공되는 브릿지
Xcode 11부터, 이 스크립트도 Python 3로 바뀌었다.
스크립트 사용해보기
(lldb) script # Python Interpreter를 interactive 방식으로 실행
>>> cruise = lldb.frame.FindVariable("cruise") # 현재 프레임(SBFrame)에서, cruise라는 변수를 찾는다.(SBValue)
>>> print(cruise) # LLDB의 기본 data formatter를 사용
(Travel.Trip) cruise = {
name = "Mediterranean Cruise"
destination = 3 values
}
>>> destinations = cruise.GetChildMemberWithName("destination") # SBValue
>>> print(destinations)
([String]) destinations = 3 values {
[0] = "Sorrento"
[1] = "Capri"
[2] = "Taormina"
}
>>> count = destinations.GetNumChildren()
>>> begin = destinations.GetChildAtIndex(0) # SBValue
>>> print(begin)
(String) [0] = "Sorrento" # SBValue는 자기 부모와의 relation 정보를 유지한다.
>>> end = destinations.GetChildAtIndex(count - 1) # SBValue
>>> print(end)
(String) [2] = "Taormina"
>>> print("Trip from {} to {}".format(begin, end))
Trip from (String) name = "Sorrento" to (String) name = "Taormina"
>>> print("Trip from {} to {}".format(begin.GetSumary(), end.GetSummary()))
Trip from "Sorrento" to "Taormina"
파일로 만들어서 로딩하기
def SummaryProvider(value, _):
destinations = value.GetChildMemberWithName("destinations")
count = destinations.GetNumChildren()
if count == 0:
return "Empty trip"
begin = destinations.GetChildAtIndex(0).GetSummary()
end = destinations.GetChildAtIndex(count - 1).GetSummary()
return "Trip with {} stops from {} to {}".format(count, begin, end)
(lldb) command script import Trip.py
(lldb) type summary add Travel.Trip --python-function Trip.SummaryProvider
Synthetic children
원하는 자식 값만 보여주고 싶을 때
역시 Python으로 가능하다.
class ExampleSyntheticChildrenProvider:
def __init__(self, value, _):
...
def num_children(self):
...
def get_child_at_index(self, index):
...
def get_child_index(self, name):
...
적용
(lldb) command script import Trip.py // 이미 로딩된 파일을 다시 로딩하면 리로딩된다.
(lldb) type synthetic add Travel.Trip --python-class Trip.ExampleSyntheticChildrenProvider
다음 세션에서도 계속 사용하고 싶으면 홈 디렉토리의 ~/.lldbinit 파일에 커맨드를 명시해놓으면 된다.