LLDB 디버거
디버깅을 토오한 코드 흐름, 메모리 상태 분석 등 프로그램 역분석을 어렵게 하기 위해 안티디버깅 기능 적용 여부를 점검
디버깅 탐지 기능이 누락되어 실행중인 앱의 동적 분석이 가능함을 확임
💡 LLDB가 xcode에 있는 디버그 툴 이름인줄,,LLDB는 XCode에만 있는것이 아닌,
일종의 디버깅 표준 LLDB docu를 참고
[WWDC22 LLDB]
https://developer.apple.com/kr/videos/play/wwdc2022/110370
LLDB로 할 수 있는거
LLDB를 사용하면 응용프로그램에 중단점을 설정하고, 실행을중단 할 수 있고
변수 및 개체의 상태를 검사하는 등의 작업을 수행 할 수 있음.
추가로 , Xcode 디버깅 할 때 자연스럽게 브레이크 포인트를 통해서 변수 값을 확인하고, 객체 상태를 확인하면서 호출 스택을 따라가던 작업들이
LLDB 를 통해 진행되었음을 알 수 있었다.
개발자에게는 편리한 디버거이고, LLDB로 역분석을 통해 앱 내부를 볼 수 있는 것이다. 이래저래 코드를 이해하고, 탐색할 수 있는 강력한 툴이긴하다.
디버그 정보 작동방식에 대한 이해
그럼 LLDB는 코드 정보를 어떻게 가져오는 걸까? 디버그 정보를 통해 가져오는데, 디버그 정보는 어떻게 생성되는지 보자.
디버그정보의 작동방식
컴파일러 > 기계어 생성 > 실행 파일의 주소를 소스파일이나 줄번호에 디버거 이동경로를 남김. > 이러한 이동경로를 디버그정보 > Object(ex. GameEngine.o)에 디버그 정보를 저장하고 > 보관, 배포를 위해 디버그정보를 .dSYM 번들에 연결할 수 있음.
정리해보면 대략적인 빌드 과정은 아래와 같다.
Swift 소스코드
↓
컴파일
↓
기계어 + 디버그 정보
↓
Object 파일
↓
dsymutil
↓
.dSYM 번들
디버그 정보 링커는 dsymutil이라고 하는데, LLDB는 Spotlight를 사용하여 .dSYM 번들을 찾기 때문에 디스크 상의 위치측면에서 매우 유현한 특성이 있음.
어찌됐던 디버그 정보가 없으면 기계어 주소를 통해 디스어셈블리를 보여주는 것이다.
GPT
CPU가 실제로 실행하는 것은 Swift/Objective-C 소스코드가 아니라 기계어 명령어 입니다. LLDB는 프로그램을 멈춘 위치의 기계어 주소 는 항상 알 수 있습니다.
그런데 그 주소를 원래 소스코드의 파일/라인으로 매핑하지 못하면, 보여줄 수 있는 가장 확실한 정보인 디스어셈블리 를 보여줍니다.
현재 실행 위치의 주소는 알고 있음
↓
그 주소에 해당하는 소스 파일/라인 정보를 찾으려고 함
↓
디버그 정보 또는 소스 파일 매핑이 부족함
↓
소스코드를 못 보여줌
↓
대신 해당 주소 주변의 기계어를 디스어셈블리해서 보여줌
위 문제가 WWDC에선 이렇게 설명이 된다. 빌드 서버나 CI/CD 환경에서 만든 앱을 로컬에서 디버깅할 때, 소스 경로가 달라서 LLDB가 파일을 못 찾을 수 있다. → 무슨소리일까? 보통 xcode로 빌드하지 서버에서 빌드할 일이 있나? 하고 GPT로 찾아보았다.
다른 사람의 Mac
CI/CD 서버
빌드 전용 머신
외부 업체 빌드 환경
배포용 아카이브 환경
위 환경과 내 로컬 경로가 다를 때 문제가 발생할 수 있는 상황으로, 즉 바이너리를 실제로 빌드한 환경의 경로와 내 로컬 경로가 다른 문제다.
CI/CD 빌드 환경
GitHub Actions에서 iOS 빌드
Jenkins에서 Xcodebuild 실행
Bitrise에서 iOS Archive 생성
Fastlane으로 TestFlight 배포
Xcode Cloud에서 배포 빌드 생성
사내 Mac mini 빌드 서버에서 IPA 생성
Git 에 올린 환경을 바꿀 수는 없기에 내 환경을 바꿔주면된다.
예를 들어 빌드 서버에서는 이런 경로에서 빌드됐다고 해보자.
/Volumes/BuildServer/workspace/MyApp
그런데 내 맥북에는 프로젝트가 여기에 있다.
/Users/taehoon/Projects/MyApp
dSYM 안의 디버그 정보에는 빌드 당시의 경로가 남아 있다.
그러면 LLDB는 /Volumes/BuildServer/workspace/MyApp 경로에서 소스를 찾으려고 한다. 하지만 내 컴퓨터에는 그런 경로가 없다. 그래서 이때 필요한 것이 source-map이다.
settings set target.source-map /Volumes/BuildServer/workspace /Users/taehoon/Projects
이렇게 설정하면 LLDB에게 알려주는 것이다.
빌드 당시의
/Volumes/BuildServer/workspace경로는내 로컬에서는
/Users/taehoon/Projects로 보면 돼.
이 설정은 .lldbinit 파일에 넣어서 자동 적용할 수도 있다.

frame variable은 Xcode의 변수 보기와 동일함
브레이크 시 보여지는 ‘변수 보기’에 해당하는 콘솔 명령어는 이거였음. 이걸 보고, LLDB를 GUI처럼 쓰고 있었구나 싶었다.

frame variable viewModel
LLDB는 디버거 이면서, 컴파일러이기도 하다.
LLDB 는 디버거임. 근데 컴파일러 기능도 가지고 있다고함. Swift 및 Clang 컴파일러 사본도 포함되어 있음. 즉, LLDB는 메모리 값을 보여주는 것이 아니라, 작은 Swift코드를 실행도함.
명령어에 따라 역할이 나눠지는데, 예를들어 po self.nextImage는 단순히 값을 꺼내 보여주는 것이 아니라,LLDB 안의 Swift 컴파일러가 self.nextImage 라는 코드를 해석하고 실행하는 과정이 들어간다.
1. 디버거로서의 LLDB
- 브레이크포인트
- 스택 확인
- 변수 확인
- 메모리 확인
2. 컴파일러로서의 LLDB
- po
- p
- expr
- Swift 표현식 평가
그래서 po가 동작하려면 더 많은 정보가 필요하다. Swift module, Clang module, SDK, import 정보, search path 등이 제대로 맞아야 한다.
Swift module, Clang module, SDK, import 정보, search path가 뭘까?
- Swift module: 내가 작성한 Swift 타입 정보
- Clang module: Objective-C/C 타입 설명서 (Apple 프레임워크가 내부적으로 Objective-C/C 기반 API를 Swift에 노출한 것이기 때문에 필요함. **(**UIKit,foundation, CoreGraphics, Webkit, Security 등)
- SDK: UIKit/Foundation 같은 Apple 프레임워크 정보
- import: 현재 파일에서 import된 모듈 정보
- search path: 그 모듈들이 실제로 어디에 있는지
LLDB는 내가 작성한 Swift 타입 정보, UIKit/Foundation 같은 Apple 프레임워크 정보, 현재 파일에서 import된 모듈 정보, 그리고 그 모듈들이 실제로 어디에 있는지에 대한 경로 정보를 필요로 한다.
LLDB는 메모리에 있는 값을 읽고 타입을 통해 정보를 보여준다.

LLDB는 메모리에있는 정보를 어떻게 형식이 지정된 아웃풋으로 변환할까? 타입을 읽어서 사람이 읽을 수 있는 형태로 보여준다.
메모리에는 바이트만 존재하고, 해당 바이트가 Int 인지 String 인지, 구조체인지, 클래스 인지 알아야 정보를 보여줄 수 가 있다.
그래서 LLDB는 타입 정보를 필요로 하고, Swift에서는 이 타입 정보를 여러 곳에서 가져올 수 있다.
Swift 리플렉션 메타데이터
dSYM의 디버그 정보
Swift module
Clang module
SDK
브리징 헤더
Reflection 메타데이터?**
- 앱 실행 중에 클래스, 메서드, 속성 등의 정보를 동적으로 가져오거나 수정 할 수 있는 기능 (코드가 자기 자신을 들여다보고 조작하는 능력)
- Swift에선 Mirror 라는 API를 지원하여 실행중에 이 값에 어떤 값이 들어있는지를 알 수 있습니다.
struct User {
let id: Int
let name: String
}
let user = User(id: 1, name: "태훈")
// 일반적인 접근
print(user.name)
// 컴파일러가 User 안에 name이 있다는 걸 알고 있음
// Mirror 접근
let mirror = Mirror(reflecting: user)
for child in mirror.children {
print(child.label, child.value)
}
// 실행 중에 필드 목록을 훑어봄

LLDB는 2가지 관점으로 왼쪽(디버깅 관점)은 Swift리플렉션 메타데이터과 dysm에서 타입을 가져옴, 오른쪽(컴파일러 관점)은 Module에서 타입 가져옴.
정적 라이브러리에서 Swift 디버깅(컴파일러 관점)이 꼬일 수 있는 이유
LLDB는 프로그램에 연결할 때 읽을 일치하는 SDK를 찾습니다.
Object 파일에서 직접 디버깅 할 때 LLDB는 빌드 시간 중에 있던 모든 비 SDK 모듈을 찾아냅니다. dsymutil은 모든 동적 라이브러리 프레임워크, dylib, 실행파일에 대해 dsym번들이란 디버그 정보를 아카이브를 패키징 할 수 있습니다.
각 .dSYM 번들은 브리징 헤더 textual Swift 인터페이스 파일 및 가장 중요한 디버그 정보를 포함할 수 있는 바이너리 Swift 모듈을 포함 할 수 있습니다.
정적 아카이브에 속하는 Swift모듈을 제외됩니다. → 커스텀 빌드 시스템을 사용하거나 정적 라이브러리 구성이 복잡한 경우에는 문제가 생길 수 있음.

dsymutil이 Swift모듈을 선택하려면 링커에 등록해야하는데 동적 라이브러리 및 실행파일의 경우 빌드 시스템이 자동으로 이작업을 수행함. 하지만 정적 아카이브는 링커에 의해 생성되지 않고 zip 파일과 같은 개체 파일의 모음일 뿐임.
이는 정적 아카이브를 연결하는 모든 실행 가능한 것과 동적 라이브러리가 링커에 Swift 모듈을 등록해야하는 책임이 있다는 것을 의미함.
대부분의 경우 Xcode 빌드 시스템이 수행하게됨. 근제 정적 아카이브 사용자 정의를 하게되면 -add-ast-path명령어를 써서 등록을 해줘야함.
정적 아카이브
→ object 파일들의 묶음
→ 링커가 직접 만든 최종 산출물이 아님
→ Swift module 정보가 자동 등록되지 않을 수 있음
→ 필요 시 -add-ast-path 확인
CI/CD 환경에서는 디버깅 옵션 직렬화도 신경 써야 한다
Swift 컴파일러는 디버깅을 위해 .swiftmodule 에 저장 할 수 있다.
예를들면 헤더 검색 경로, 빌드 옵션 관련 경로 정보 같은 것들을 저장 할 수 있는데, 같은 환경에서 빌드하고 같은환경에서 디버깅한다면 큰 문제가 없다. CI 서버에서 빌드한 앱을 개발자 로컬에서 디버깅한다면 문제가 생기게 된다.
이런 상황에서는 다음 설정을 고려할 수 있다.
SWIFT_SERIALIZE_DEBUGGING_OPTIONS = NO
Swift 컴파일러 옵션으로는 다음과 같다.
-no-serialize-debugging-options
→ 이거 CI 서버에서 빌드해서 로컬에서 디버깅 할 일이 있을까? 없지 않을까 싶다.
LLDB를 보게 된 이유는 보안 점검 항목 때문이었다.
디버깅 툴을 왜 막아야하는지 궁금하던차에 찾아보게되었는데, 실행중인 앱에 붙어서 내부 흐름을 볼 수 있어 문제가 됨을 확인 할 수 있었다.
특정 함수가 언제 호출되는지 확인
조건문 분기 확인
객체의 현재 값 확인
메모리 상태 확인
앱 실행 흐름 추적
민감 로직 위치 파악
개발자에게는 디버깅 기능이지만, 공격자나 분석자에게는 앱 내부를 들여다볼 수 있는 수단이 될 수도 있다.
근데 2가지 궁금증이 생긴다.
- LLDB → LLDB(Low-Level Debugger) 라는건데.. 정말쓸까?…쓴다면 어떻게 쓰고 있을까?
- LLDB 어떻게 막을수 있을까?
LLDB 이거 정말로 쓸까?
po 만 좀 쓰는 것 같다. Print Object로 브레이크
LLDB 막아볼까?
import Foundation
import Darwin
@_silgen_name("ptrace")
func ptrace(
_ request: Int32,
_ pid: pid_t,
_ addr: UnsafeMutableRawPointer?,
_ data: Int32
) -> Int32
func denyDebuggerSwift() {
let PT_DENY_ATTACH: Int32 = 31
ptrace(PT_DENY_ATTACH, 0, nil, 0)
}
ptrace는 프로세스 추적/디버깅과 관련된 low-level 시스템 함수입니다.
PT_DENY_ATTACH는 ptrace에 전달하는 요청 코드입니다.
let PT_DENY_ATTACH: Int32 = 31 : 이 프로세스에 디버거 attach를 거부한다 의미를 갖고있습니다.
원래 C언어에서 PT_DENY_ATTACH 이런 상수로 쓰입니다. 그런데 Swift에서 이 상수가 바로 안잡히는 경우가 있어서 숫자값인 31을 넣은 것입니다.
| 인자 | 값 | 의미 |
|---|---|---|
request | PT_DENY_ATTACH | 디버거 attach 거부 요청 |
pid | 0 | 현재 프로세스 대상 |
addr | nil | 이 요청에서는 사용하지 않음 |
data | 0 | 이 요청에서는 사용하지 않음 |
즉, 자연어로 풀면 현재 실행 중인 이 앱 프로세스에 대해
앞으로 디버거가 attach 되는 것을 거부해라. 입니다.
LLDB 디버깅 탐지하기(탈옥 단말기 연결해서 LLDB 설정)
마지막으로, 탈옥단말기에서 LLDB 디버깅 하는 방법으로 마무리를 하겠다.
💡 탈옥한 단말기에서만 가능
1단계: iPhone:/ root#
탈옥한 iPhone을 USB로 Mac에 연결한 뒤 Mac 터미널에서: ssh root@아이폰_IP주소 ex) ssh root@192.168.0.25
IP 확인 방법 (설정→ Wi-Fi → 연결된 Wi-Fi 오른쪽 ⓘ→ IP 주소 확인 )
2단계: 접속 후 앱 프로세스 찾기
ps -ef | grep 앱이름
ps -ef | grep -i 앱이름
ex)
ps -ef | grep -i IniTalkPay
ps -ef | grep -i Talk // 일부만 검색가능
3단계: LLDB로 attach
PID가 8611이면 iPhone 내부 셸에서:
lldb -p 8611
성공하면
Process 8611 stopped
Executable module set to ...
Architecture set to: arm64-apple-ios
이런 메시지가 나옵니다.
이러면 LLDB attach 성공입니다.