ios 디버깅

개요

버그가 많다.

겁나 많다.

xcode에서 안잡아주는것도 있다.

그럼 굉장히 골치아프다

디버깅 기법 공부해보고 써먹자



런타임 디버깅


Breakpoints

  • 일반적인 디버깅 방법 edit을 활용해서 condition, action등을 설정할 수 있음
  • 이외에도 디버깅 창으로 가면 더 있음
    • Swift Error Breakpoint : Swift에서 예외 발생 시 행, Type을 활용해서 특정 타입 에러 확인
    • Exception Breakpoint : objc, cpp 예외 발생 시 행, 항상 키는거 추천
    • Symbolic Breakpoint : 행 걸고 싶은 함수 이름 쓰면 됨, objc는 풀네임으로 swift는 맹글링 규칙에 따라
    • Runtime Issue Breakpoint : Diagnostics에 Runtimer으로 시작하는 부분에서 경고를 감지하면 행
    • Constraint Error Breakpoint : AutoLayout 제약조건 충돌하면 행
  • Xcode의 스텝 버튼을 Control + Shift 키를 누른 채로 클릭하면, 현재 스레드만 한 단계 실행하고 나머지 스레드는 행


Name Mangling

  • Objective-C Property
    • @property (nonatomic, strong) NSString *name; 이런 프로퍼티는 컴파일러가 자동으로 getter/setter를 생성
  • Swift Property
    • 모듈명.클래스명.프로퍼티명.(getter setter)로 자동생성


LLDB


자주 쓰는 명령어

  • po(print object) : 변수를 확인할 때 pretty print, debugDescription 출력
    • debugDescription과 description은 오버라이드 가능 (CALayer와 같이)
  • p : raw 구조로 print, expression --랑 같으며 코드를 실행함
  • v : 코드를 실행하지 않고 메모리에서 값을 읽어옴
  • e(expression) : 앱을 재시작하지 않고 런타임에 코드를 주입할 수 있음
  • bt : 크래시가 났을 때 현재 스레드의 함수 콜스택을 프린트 (디버그 네비게이터랑 같음)


디버깅 컨텍스트

  • 행이 걸릴 때, Swift or Non-Swift로 나뉘어 디버깅 컨텍스트를 사용함
  • Swift 컨텍스트에서 `po [UIApplication SharedApplication]과 같이 ObjC 문법을 입력하면 에러 발생
  • 일시정지로 멈추면 기본값인 Non-Swift 컨텍스트 사용 이 때 po UIApplication.shared와같이 swift 문법 입력시 에러발생
  • -l(language) 플래그로 컨텍스트를 강제로 지정 가능함
    • Objective-C 문법 강제: expression -l objc -O -- [UIApplication sharedApplication]
    • Swift 문법 강제: expression -l swift -O -- UIApplication.shared



Diagnostics

edit scheme 가면 있는 Diagnostics 옵션에 대해


Address Sanitizer

  • 런타임시에 발생하는 C/C++/Obj-C 기반 메모리 손상 버그를 찾아내는 기능
  • 섀도우메모리: 앱이 사용하는 메모리 공간의 8분의1에 해당하는 별도의 메모리 공간
    • 8바이트마다 해당 메모리의 상태를 기록하는 1바이트의 섀도우바이트있음
    • 0: 8바이트가 모두 유효함
    • 1~7 : 8바이트가 꽉차지 않은 할당
    • 음수(F*) : 스택, 힙, 데이터 등등 접근금지 코드
  • ASan은 컴파일시에 메모리 접근 가능 여부를 확인하는 코드를 삽입함
*addr = value;
------>>>
shadow_byte_addr=(addr>>3) + offset;
shadow_byte_value=*shadow_byte_addr;

if (shadow_byte_value!=0)
    check_memory_access(addr, shadow_byte_value);

*addr=value;
  • 3은 섀도우바이트 가져오는것

  • access함수에서 음수면 프로그램 강제종료
  • 레드존 : 메모리가 할당될 때 해당 메모리 앞뒤로 여분의 공간을 만들고 그 공간에 접근하면 섀도우바이트에 기록
  • 탐지되는 메모리 오류
    • 해제된 메모리에 다시 접근하려고 할 때
    • 해제된 메모리의 이중 해제
    • 할당되지 않은 메모리 해제
    • 함수 반환 후 스택 메모리 사용
    • 버퍼 오버플로우, 언더플로우


Detect use of stack after return

int *func() {
    int value = 42;
    return &value;
}

void doSomething() {
    int *pt = func();
    *pt = 43;
}
  • 함수 반환 후에 스택 메모리를 사용하는 지 탐지하는 기능
  • 성능상의 문제로 기본적으로는 비활성화


Thread Sanitizer

  • 멀티스레딩 환경에서 Data Race 버그 감지용 런타임 Sanitizer
  • ASan처럼 섀도우메모리를 사용해 어떤 스레드가 언제 접근했는지 모든 메모리 접근을 감시함
  • 모든 메모리 접근을 감시하므로 성능 저하가 매우매우 큼
  • 탐지되는 오류
    • Data Race
    • inout이나 캡처리스트에서 발생하는 swift access races
    • pthread_mutex_init으로 생성하지 않은 뮤텍스를 lock
    • Thread.detach() 등으로 생성한 스레드가 작업이 끝난 뒤에도 종료되지 않고 계속 남아있는 경우


Undefined Behavior Sanitizer

  • Swift에서는 사용불가
  • 개발자가 할리가 없는 실수를 잡아내는것(INT_MAX에 1을 더하거나, 0으로 나누기등등)


Main Thread Checker

  • UI 변경을 메인스레드에서 하지 않는 상황 감지


Thread Performance Checker

  • 진단 도구가 아니라 프로파일링 도우미 역할
  • Time Profiler에서 앱이 어디서 시간을 낭비하는지 찾아낼수있음
  • Thread State Trace에서 스레드의 상태(Runnint, Blocked, Idle)를 보고 왜 일안하는지 감시


Hardware Memory Tagging

  • ASan과 같으나 CPU가 모든 메모리 접근을 실시간으로 검사하는 하드웨어 메모리 보안기능
  • 성능 저하가 없어 release빌드에서도 항상 켜둘 수 있음
  • 메모리 포인터가 생성될 때 CPU는 해당 포인터의 사용되지 않는 상위 비트에 태그를 붙임
  • malloc으로 실제 할당이 되고 그곳을 가리키는 포인터와 동일한 태그를 붙임
  • CPU는 해당 메모리에 접근이 될 때마다 두 태그를 비교함
  • 좋은 기능이지만 너무 엄격하게 검사해서 디버깅할 때나 활성화, 태그가 다르면 앱 크래쉬 발생


Fall back to Guard Malloc

  • 매우매우매우 엄격한 것 malloc 대신 libgmalloc이라는 특수 할당자를 사용함
  • 이 할당자는 10바이트 할당에도 최소 1개의 가상메모리페이지(4kb or 16kb)를 통째로 할당
  • 잘안씀


Malloc Scribble

  • 할당은 되었으나 초기화 되지않은 메모리 사용과 해제된 메모리 사용 버그 감지


Malloc Guard Edges

  • 오버플로우, 언더플로우 감지


Zombie Objects

  • Use-after-free 감지
  • rc가 0이 되어도 메모리에서 해제시키지않음
  • 이 객체를 초기화하고 isa 포인터를 _NSZombie로 바꿔치기함
    • isa 포인터 : 객체가 어떤 클래스인지 가리키는 포인터
  • 이 객체에 다시 접근하면 NSZombie가 가로채서 앱 크래쉬


Malloc Stack Logging

  • 릭을 잡을 때 유용
  • Instruments에서 릭을 잡을 때 해당 누수가 어떤 코드에서 할당했는지 추적 가능


API Validation

  • 개발자가 Metal API를 부적절하게 호출하는 것을 감지


Shader Validation

  • 개발자가 Metal API를 부적절하게 호출하는 것을 더 엄격하게 감지


Show Graphics Overview

  • 앱의 그래픽 성능을 HUD로 보여줌


Log Graphics Overview

  • 앱의 그래픽 성능을 로깅함


Logging

  • print (swift)
    • stdout으로 콘솔에 출력
    • 출력할 문자열만 표시
    • 동기식으로 빠르게 출력만함
  • NSLog (objc)
    • stderr로 출력하여 시스템 로거랑 동기로 통신
    • 출력할 문자열 + 앱 + pid + threadID, 타임스탬프등등 메타데이터 포함
  • Logger (swift) / os_log (objc)
    • 비동기로 시스템 로깅하여 제일 빠름
    • debug, info, error, fault 등 로그의 중요도를 설정가능
    • 변수값 비공개 처리(콘솔에서는 확인 가능)



Instruments

많이 쓰는 도구 3가지

앱이 느릴때 사용하는 Time Profiler, 메모리 잡을 때 사용하는 Leaks, 네트워크 체크하는 Networks


Time Profiler

  • 앱에 행이 걸리거나 뭔가 느리다, 버벅인다 하는 작업을 찾을 때 사용
  • 일반적으로 메인스레드에서 무거운 작업을 하거나, 메인스레드가 우선순위가 낮은 다른 스레드의 작업을 기다리는 경우 행
  • 행은 100ms미만이 이상적, 250ms이상이면 딜레이가 눈에 보임
  • 메인 스레드가 Busy, Blocked된 것인지를 구분하는 것이 중요
  • busy는 짧지만 자주호출될때, block은 너무 오래걸릴때
  • 동기식 행 : 메인스레드가 무거운 작업을 동기적으로 처리할 때
  • 비동기식 행 : 완료된 비동기 작업들의 콜백이 메인스레드로 몰려들 때 발생
  • 대부분의 해결책은 동기적인 작업을 비동기적으로 변경하여 메인 스레드의 부담을 덜어주는 것


비동기

  • 데이터 로직은 백그라운드 스레드에서, UI 업데이트 로직은 메인 스레드에서
  • 비동기 전환에도 비용 발생, 컨텍스트 스위칭 발생, Data Race 발생, 디버깅 까다로움
  • 메인스레드를 1ms ~ 2ms 이상 잡고있으면 비동기로 전환 했을 때 유리
    • 네트워크, 디스크, DB I/O
    • 무거운 JSON 파싱
    • 이미지 처리(리사이징, 필터 적용 등)
    • 복잡한 연산(암호화, 대규모 배열 정렬 및 필터링)
  • 메인 스레드는 절대 대기하거나 일하지 않고, 항상 반응할 수 있도록 유지
  • async 키워드가 붙으면 이 함수는 일시 중지 될 수 있습니다 라는 뜻


Leaks, Allocations, memory graph

  • Leaks로 릭이 있는지를 파악하고 Alloctiontions, memory graph로 어디서 나는지 확인한다.
  • 앱을 쓸 수록 사용량이 올라갈때, 특정 화면에 들어갔다 나와도 메모리가 줄어들지 않을 때


네트워크

  • API 응답이 늦거나 데이터 사용량이 비정상적일 때 사용
  • 요청이 느릴때 (Latency) - 앱보다 외부적인(서버, 네트워크 상태) 요인일 가능성이 높음
  • 데이터 사용량이 많을 때 - 원본 이미지 수신, 불필요한 데이터 수신, 캐싱 동작 오류
  • 데이터 오류 - 404, 500등 에러가 발생할 때



Crash



dSYM(debugSymbol)

  • 크래시 리포트에 함수 이름, 줄 번호등 개발자가 읽을 수 있는 콜스택을 적어주는것
  • 빌드설정에서 Debug Information Format을 DWARF with dSYM File로 세팅
  • 앱 분석 공유에 동의한 App Store 사용자는 xcode에서 분석가능 (but firebase crashlytics를 주로 사용)

Zombie Objects

  • 런타임 함수에서의 크래시 (objc_msgSend, objc_release 등)
    • objc 런타임이 해제된 객체에 메시지를 보낼 수 없을 때 발생
    • objc 런타임이 해제된 객체를 다시 해제하려할 때 발생 (Autorelease 풀에서)
  • Unrecognized Selector
    • 해제된 객체가 사용하던 메모리를 재사용했는데 그 위치에 이전 객체의 Selector를 보낼때
    • doesNotRecognizeSelector 호출 스택 프레임

메모리 손상(Memory Corruption)

  • 메모리 접근 문제의 진짜 원인이 백트레이스에 안나타나는 경우가 있음
  • 잘못된 코드에 의해서 수정되고 한참뒤에 앱의 다른 부분이 그 메모리를 접근하면 크래쉬가 발생함
  • 이 경우 ASan 사용해서 사전에 잡아야됨
  • 메모리 접근 문제
    • Invalid Memory Fetch : 유효하지 않은 포인터를 역참조(사용)
    • Invalid Instruction Fetch : 예기치 않은 객체에 함수 호출 시도
    • 이 경우 Program Counter(pc) 레지스터를 봐야함
    • pc != crash address : Invalid Memory Fetch
    • pc == crash address : Invalid Instruction Fetch

크래시 종류

  • Exception Type
    • EXC_BAD_ACCESS (SIGSEGV): 가장 흔한 크래시. nil 포인터 접근, 해제된 메모리 접근 등 잘못된 메모리 주소에 접근했다는 뜻
    • EXC_CRASH (SIGABRT): “의도된” 크래시. Swift의 fatalError()나 처리되지 않은 Objective-C 예외(NSException)
    • EXC_BREAKPOINT (SIGTRAP): Swift 런타임이 감지한 오류, 잘못된 ! 사용시
    • EXC_RESOURCE: 메모리(OOM)나 CPU 등 리소스 한계를 초과하여 OS가 앱을 종료
  • Termination Reason
    • Namespace SPRINGBOARD, Code 0x8badf00d(Ate Bad Food) : 앱 실행(Launch) 시간이 너무 오래 걸림
    • Namespace SPRINGBOARD, Code 0xdead10cc(Dead Lock) : 앱이 백그라운드 작업을 제시간에 완료하지 않는 등 교착 상태에 빠짐
    • Namespace DYLD, Code 0x1(Missing Framework) : dyld가 라이브러리를 로드하지 못함