• po는 어떻게 동작하는가? 다른 방법으로 볼 수는 없을까?

  • Xcode에서 브레이크 포인트를 걸면, 다음과 같이 변수를 볼 수 있다.

    스크린샷 2022-08-13 오후 6.39.42.png

  • 그 옆에서는 lldb에 직접 커맨드를 보낼 수 있다.

    • 여기서 버그를 찾기 위해 변수 값을 출력하는 명령이 포함된다.
    • 변수 출력에는 여러 방법이 있고, 각각 장단점을 가진다.

    스크린샷 2022-08-13 오후 6.41.48.png

  • 예시로 쓸 코드

    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이 일치할 필요는 없다.

            • 아래 코드에서는 static representation은 Activity, dynamic type은 Trip이다.
            protocol Activity { }
            struct Trip: Activity {
            	var name: String
            	var destination: [String]
            }
            
            let cruise: Activity = Trip(...)
            
          • 그래서 LLDB에서는 최대한 정확한 데이터를 보여주기 위해서, 결과값의 메타데이터를 확인해서 보여주게 된다.

        • 결과 값에 대해서만 dynamic type resolution을 수행하기 때문에, 실제로 코드상으로 유효하지 않은 표현식(static)은 p 명령어가 실패하게 된다.

          • 코드로 전환할 때는 타입 정보가 static representation 밖에 없기 때문.
          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를 거치지 않는 경우

          • String과 Int같은 단순해 보이는 것도 사실 복잡한 구조를 가지고 있다.
          • 이는 속도와 크기 면에서 최적화되어 있기 때문이다.
          (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
      
    • 위의 두 커맨드와 다르게 컴파일과 코드 실행 단계를 거치지 않기 때문에 더 빠르다.

    • 대신 사용하는 언어와는 별개의 고유 문법을 가지고 있다.

      • ex. 필드 접근은 dot syntax로 동일하지만, 계산 프로퍼티에서는 사용할 수 없다.
        • It won’t perform overload the resolution 도 있는데, 뭔지 모르겠다.
    • 동작 원리

      (lldb) v variable.field1.field2
      
      • 프로그램 상태를 확인해서 메모리에서 해당 변수의 주소를 찾아낸다.
      • 해당 메모리 주소에서 값을 읽어낸다.
      • 읽어낸 값에 대해 dynamic type resolution을 수행한다.
      • 해당 값의 하위 필드를 읽으려고 하는 경우는 위 과정을 반복한다.
        • p와 po는 dynamic type resolution을 딱 1번만 한다.
      • 값 찾기가 종료된 경우, 결과값을 Formatter로 넘긴다.
    • 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"
        
  • 정리

    스크린샷 2022-08-13 오후 11.35.59.png

  • LLDB Data Formatter 커스터마이징하기

    • Filters
    • String Summaries
    • Synthetic children
    • 다음 세션에서도 계속 사용하고 싶으면 홈 디렉토리의 ~/.lldbinit 파일에 커맨드를 명시해놓으면 된다.
    • formatter reference