Xcode에서 디버깅 하는 법에 대해 아라보자 - LLDB

Yang Si Yeon·2021년 6월 10일
1

해당 글은 야곰닷넷 코스 중 LLDB 정복 이라는 코스를 학습 후 정리한 글입니다.

0. LLDB(Low-Level Debugger)란?

LLVM은 Apple에서 진행한 Compiler에 필요한 Toolchain 개발 프로젝트이다.

LLDB는 LLVM의 Debugger Component를 개발하는 서브 프로젝트며, 얘는 LLVM 프로젝트를 통해 개발된 Clang Expression Parser, LLVM Diassembler 등 로우레벨 컨트롤이 가능한 모듈로 이루어져있다. C, C++, Objective-C, Swift를 지원하며, 현재 Xcode의 기본 디버거로 내장되어 있다.


1. LLDB 명령어 기초 문법

(lldb) command [subcommand] -option "this is argument"

  • Command와 Subcommand는 LLDB 내 Object의 이름. (etc. breakpoint, watchpoint, set, list ...) 이들은 모두 계층화 되어 있어, Command에 따라 사용 가능한 Subcommand 종류가 다르다.
  • Option은 Command 뒤 어느 곳에든 위치 가능, -(hyphen) 로 시작한다.
  • Argument에 공백이 포함되는 경우엔 ""으로 묶을 수 있다.

예시는 아래와 같다.

(lldb) breakpoint set --file test.c --line 12

breakpoint (Command)와 set (Subcommand)을 이용하며
--file option을 통해 test.c 파일 내
--line option을 통해 12번째 라인에
중단점을 set 해준다.


2. Help & Apropos

LLDB에는 수많은 Command와 Subcommand, Option이 존재하기 때문에, 필요한 기능이 있는지 확인할 때는 다음과 같은 Command를 사용할 수 있다.

  • Help: 해당 문법으로 사용가능한 Subcommand, Option 리스트나 사용법을 보여줌
# LDB에서 제공하는 Command가 궁금하다면,
(lldb) help

# 특정 CommandSubcommand, Option이 궁금하다면,
(lldb) help breakpoint
(lldb) help breakpoint set
  • Apropos: 원하는 기능의 명령어가 있는지 관련 키워드를 통해 알 수 있음
# reference count를 체크할 수 있는 명령어가 있는지 궁금하다면,
(lldb) apropos "reference count"
# 결과
# The following commands may relate to 'reference count':
#    refcount -- Inspect the reference count data for a Swift object

3. Breakpoint

프로그램에서 문제가 되는 지점을 찾아 멈추고, 여러가지 조건을 바꿔보며 테스트하기 위해 필요하다. Xcode에서는 코드 라인의 왼쪽 숫자를 누르면 쉽게 설정 가능함.

멈춰있는 breakpoint line의 우측에 녹색 햄버거 버튼을 위 아래로 드래그하면 다음 실행 지점을 변경할 수 있다.

condition
-condition (-c) option을 이용하면, breakpoint에 원하는 조건을 걸 수 있다. -c option 뒤의 expression이 true인 경우에만 breakpoint에서 멈춤.

# viewWillAppear 호출시, animated가 true인 경우에만 break
(lldb) breakpoint set --name "viewWillAppear" --condition animated
(lldb) br s -n "viewWillAppear" -c animated

Command 실행 & AutoContinue
-command (-C) option을 이용하면 break시 원하는 lldb command를 실행할 수 있다.
-auto-continue (-G) option은 command 실행 후 break에 걸린채로 있지 않고 프로그램을 자동 진행 시켜줌.

(lldb) breakpoint set -n "viewDidLoad" --command "po $arg1" -G1
(lldb) br s -n "viewDidLoad" -C "po $arg1" -G1

4. Stepping

Stepping은 프로세스를 단계별로 진행하면서 상태 변화를 관찰해 볼 수 있는 유용한 기능이다.

Stepping Over
(lldb) next Command를 이용하면, 현재 Break 걸려있는 지점에서 바로 다음 Statement로 Step Over 할 수 있다. Xcode에서는 왼쪽에서 4번째 건너뛰는 것 같이 생긴 버튼을 누르면 된다.

Stepping In
(lldb) step Command를 이용하면, 해당 Statement가 Function Call 인경우 Debugger를 해당 함수 내부에 위치한 시작지점으로 이동하게 한다. Xcode에서는 왼쪽에서 5번째 아래를 향한 화살표 버튼을 누르면 된다.

Stepping Out
현재 진행중인 function이 return 될 때 까지 프로그램을 진행한 후 프로그램 Break를 걸어주는 Stepping Action을 Stepping Out이라고 한다. (lldb) finish Command로 실행해볼 수 있다. Stack Memory 관점에서 Stepping Out은 Stack Frame을 Pop하는 것과 동일하다. Xcode에서는 왼쪽에서 6번째 위를 향한 화살표 버튼을 누르면 된다.

+) Continue
(lldb) continue Command는 다음 Breakpoint가 나타날때까지 프로그램을 진행한다. Xcode에서는 왼쪽에서 3번째 재생버튼 처럼 생긴 버튼을 누르면 된다.


Expression

(lldb) expression Command는 멈춰있는 동안 새로운 동작을 실행시킬 수 있다.

po

실무에서 LLDB Command 중 가장 많이 사용되는 것은 (lldb) po 일것이다. 이 Command는 객체에 대한 다양한 정보를 콘솔에 출력하여 확인할 수 있게 해준다. po는 (lldb) expression -O --의 Shorthand인데, 여기서 -O 옵션은 object의 description을 출력하겠다는 뜻이다.

po가 출력하는 description은 NSOjbect의 debugDescription이다. 따라서, 아래와 같이 debugDescription을 override 한다면,

override var debugDescription: String {
	return "이 객체의 debugDescription은 \(super.debugDescription) 입니다."
}

위와 같이 Customize된 po의 결과를 얻을 수 있다. 복잡한 내용의 description을 보기 쉽게 출력할 때 사용 가능하다.

Swift Context에서 Objective-C 표현을 사용하려면, -l (-language) option을 이용하자.

(lldb) expression -l objc -O -- "[Objective-C Expression]"

반대로 Objective-C Context에서 Swift 표현을 사용할 수 있다.


Variable

사용하기

(lldb) expression Command는 런타임에 여러 정보를 출력할 수 있을 뿐만 아니라 값을 변경할 수도 있다. LLDB는 내부적으로 값이 출력될 때 마다 local variable을 $R~의 형태로 만들어 저장한다. 이 값들은 해당 break context에서 벗어나도 사용 가능하며, 수정해서 사용할 수도 있다.

(lldb) expression self.view

# 결과
(UIView?) $R0 = 0x00007fcd81c02fb0 {
	UIKit.UIResponder = {
    	baseNSObject@0 = {
        	isa = UIView
        }
    }
}

위 명령어로 self.view 정보를 출력하는데, 출력된 정보를 보면 $R0 이라는 이름의 변수에 self.view가 저장되어 있다.

(lldb) expression $R0.backgroundColor = UIColor.blue

self.view가 저장된 $R0의 속성인 배경 색상 (backgroundColor) 을 변경한다.

(lldb) continue

코드를 마저 진행하고 시뮬레이터를 보면 배경 색상이 바뀌어 있는 것을 확인할 수 있다.

선언하기

(lldb) expression Command를 이용해서 변수를 직접 선언해서 사용할 수도 있다. 단, 사용하고자 하는 변수명 앞에 $ 문자를 붙여줘야 한다.

(lldb) expr let $somNumber = 10
(lldb) expr var $somString = "some string"

UIView 객체를 새로 생성한 후, 얘를 self.view에 붙여보자.

(lldb) expr var $view = UIView(frame: CGRect(x: 100, y: 100, width: 80, height: 80))
(lldb) expression
1 $view.backgroundColor = UIColor.blue
2 self.view.addSubview($view)
3
(lldb) continue

(lldb) expression 명령어를 입력한 후 엔터키를 입력하면, Multi-line Command를 입력할 수 있다. 이 Command Console은 다시 엔터키를 입력해주면 완료된다.

(lldb) continue 까지 입력해주면 멈춰있던 프로그램이 마저 실행되면서 $view가 화면에 나타난다.


--ignore-breakpoints option
해당 옵션으로 expression 실행 중 만나는 breakpoint를 ignore할지 여부를 선택할 수 있다. default 값은 --ignore-breakpoints true 이다.

# 실행 도중 breakpoint를 만나도 그냥 진행
(lldb) expression --ignore-breakpoints true --
(lldb) ex -i 1 --

# 실행 도중 breakpoin를 만나면 멈춤
(lldb) expression --ignore-breakpoints false --
(lldb) ex -i 0 --

Image

(lldb) image Command는 Module 내에서 나타나는 Symbol에 대한 자세한 정보를 알아낼 수 있다.

  • Module: process에 Load되어 실행되는 Code를 의미. Swift에서 Module은 메인 실행파일 뿐 아니라, Framework나 Library, Plugin 등도 포함하는 개념이다. 예를 들어 우리가 만든 Application 프로젝트를 포함해 UIKit, AppKit 등의 Library 까지도 모듈로 볼 수 있다.
  • Symbol: Method, Variable, Class 등 말그대로 기계가 아닌 사람의 눈으로 알아볼 수 있는 Soure Code를 이루는 작은 단위의 Symbol. Symbol Table은 Compile 된 Binary를 그에 맞는 Method, Variable, Class 등으로 상호 Mapping 해주는 역할을 한다. 또한 Binary가 Symbol로 번역되는 것을 Symbolicate 라고 한다.

Image Command를 이용하면, Framework에 대한 Private한 정보들 (private class, private method 등) 을 Header File에 공개되어 있는 것 이상으로 알아볼 수 있다. Private API들에 접근 가능하다면, 객체에 대한 숨겨진 Description을 확인하거나, 더 세부적인 속성을 확인할 수 있기 때문에 디버깅이 좀 더 간편해 진다.

Image List

(lldb) image list 는 현재 Process에 Load 되어 있는 모든 Module 들의 정보를 출력한다.

프로젝트 생성 이후 아무 내용도 구현하지 않고 (lldb) image list 를 실행한 모습이다.
위 목록에 나타나있는 첫 4개의 Module을 살펴보면,

  • HelloDebugger라는 이름의 Main Binary
  • dyld, dyld_sim 과 같이 Dynamic Library를 Linking하기 위해 필요한 Dynamic Link Editor
  • 우리가 익히 알고있는 Foundation Library
  • 이름을 봐도 알 수 없는 여러가지 Library 들,,

Image Dump

(lldb) image dump Command를 이용하면 Module의 세부적인 정보를 dumping(dump는 기억장치의 내용을 기록 하는 것) 해볼 수 있다.

(lldb) image dump symtab UIKitCore -s address Command를 실행하면 UIKitCore Library에 포함되어 있는 12만개가 넘는 Symbol Table이 출력된다. 또한, -s address option 을 이용했기 때문에 목록이 주소값으로 정렬되어 출력된다.

Image Lookup

image dump 명령을 통해 나온 Module 정보들은 유용하지만, 너무 많다. 이 내용들을 필터링 해서 볼 수 있는 Command가 (lldb) image lookup이다.

(lldb) help image lookup Command를 통해 알아보면,

# 함수 이름 (--function)
(lldb) image lookup -F "functionName"

# 주소값 (--address)
(lldb) image lookup -a "0x00address"

# 파일 이름 (--filename)
(lldb) image lookup -f "FileName.swift"

# 라인 번호 (--line)
(lldb) image lookup -f "FileName.swift" -l 15

# 정규식 이용 (--regex)
(lldb) image lookup -rn "regexExpression"

위와 같은 다양한 필터링 Option 들을 제공하고 있음을 확인할 수 있다.

(lldb) image lookup Command를 이용하면, 특정 상황에서 원하는 Symbol들을 보다 쉽게 찾을 수 있다. 예를 들어 Animation을 구현하다가 CALayer에서 제공하는 setter가 어떤게 있는지 궁금할 때

(lldb) image lookup -rn '\[CALayer\ set'

이렇게 정규식을 이용하면 간단하게 모든 setter API를 출력해볼 수 있다.

Crash Log Symbolicate 해보기 ✔️

(lldb) image lookup Command는 Symbolicate 되지 않은 Crash Log를 살펴볼 때 매우 유용하다.

다음 이미지는 위 부터 순서대로 전혀 Symbolicate 되지 않은 형태의 Log, 부분적으로 변환된 Log, 우리가 알아볼 수 있는 형태로 모두 Symbolicate 된 Log 이다.

가장 아래에 있는 Log와 같은 형태로 Symbolicate 되어 있다면, 어디서 Crash가 일어났는지 알아보기 쉬울 것이다. 하지만, 가장 위 같은 Crash Log만 있다면, 문제가 어디에서 일어난건지 어떻게 찾을 수 있을까?

일단 가장 위의 Backtrace를 자세히 살펴보자.

 0  The Elements  0x000000010003fc20 0x100034000 + 48160
 1  UIKit                    0x0000000187480070 0x187438000 + 295024
 2  UIKit                    0x000000018747feb0 0x187438000 + 294576
 ...

매 Line 마다 같은 패턴이기 때문에 0번 Line을 통해 알아보자면 위의 Crash Log 는

  • Binary Image Name : The Elements 는 Crash가 발생한 Main Application 실행 파일
  • Stack Address : 해당 Symbol의 Stack 메모리 내 주소값을 의미
  • Load Address : Application이 Load되어있는 주소값을 의미
  • Offset: 48160는 StackAddress와 LoadAddress 사이의 Offset
    ( Offset = StackAddress – LoadAddress )

위와 같은 Symbol 정보들을 제공한다. 위 정보들을 이용해 Crash를 일으킨 Symbol의 Address를 찾아서, dSYM 내의 문제아 Symbol의 Address를 찾아 어떤 놈인지 밝혀낼 수 있다.


Symbol Address 계산법은 아래와 같다.

symbol address = slide + stack address - load address
               = slide + offset
# slide value는 32bit architecture의 경우 0x4000, 
# 64bit architecture의 경우 0x100000000을 가진다.

이에 따라 Symbol Address를 계산해보면,

# 참고: 48160 (10진수) == BC20 (16진수)
symbol address = 0x100000000 + BC20 = ox10000BC20

0x10000BC20이 문제의 지점이다. 마지막으로 0x10000BC20 지점에는 어떤 Symbol이 있는지 확인해보면 된다. 터미널을 열고 $ lldb를 통해 LLDB 콘솔을 열자.

Xcode LLDB 콘솔이 아닌 터미널에서도 LLDB를 실행할 수 있다.

  • (lldb) target create "dSYM 경로"를 통해 LLDB를 dSYM File에 Attach 해준다.

  • (lldb) image lookup --address 0x10000bc20 Command를 통해 문제의 Symbol 위치를 검색해주면

  • HelloDebugger.ViewController.init에서 Crash가 났다는 것을 확인할 수 있다.

dSYM 경로
Xcode - Preference - Locations - Archives - 날짜 - 아카이브 파일 (패키지 내용보기) - dSYMs - 앱이름.app.dSYM - Contents - Resources - DWARF - 앱 이름


Alias

반복해서 사용하고 싶은 명령어가 너무 복잡하거나 긴경우에는 (lldb) command alias 별명 "줄이고 싶은 Command" Command를 이용하면 간단한 별명으로 줄여서 이용할 수 있다.

예를 들어 Swift Code 내에서 Objective-C expression을 출력하기 위해 (lldb) expression -l objc -O -- Command를 사용하는 경우가 종종 있는데,

(lldb) command alias pojc expression -l objc -O --

위 명령어를 실행해준 이후에는 (lldb) pojc "[Objective-C Expression]" 형태로 명령어를 사용할 수 있다.

Command Alias로 등록해두어도 별칭은 해당 빌드 시점이 지나면, LLDB Session이 끝남과 동시에 사라진다.
Xcode 실행 시 LLDB 초기화를 위해 사용되는 ~/.lldbinit 파일에 원하는 Command Alias를 추가해두면, 별칭을 매번 정해주지 않고 계속 사용할 수 있다.


참고

https://yagom.net/courses/start-lldb/

profile
가장 젊은 지금, 내가 성장하는 데에 쓰자

1개의 댓글

comment-user-thumbnail
2023년 2월 19일

잘 읽었습니다. 감사합니다:)

답글 달기