Skip to the content.

Advanced Apple Debugging & Reverse Engineering (raywenderlich.com)의 내용을 정리한 글이다.

Chapeter 1: Getting Started

(lldb) file /Applications/Xcode.app/Contents/MacOS/Xcode
Current executable set to '/Applications/Xcode.app/Contents/MacOS/Xcode' (arm64).
(lldb) process launch -e /dev/ttys001 --
Process 1809 launched: '/Applications/Xcode.app/Contents/MacOS/Xcode' (arm64)

이렇게 하면 lldb로 Xcode에 attach 및 실행을 할 수 있으며, 로그는 /dev/ttys001에 찍힌다. Ctrl + C를 누르면 pause를 할 수 있다.

(lldb) b -[NSView hitTest:]
Breakpoint 1: where = AppKit`-[NSView hitTest:], address = 0x00000001c74542d8
(lldb) c # 또는 continue
Process 1809 resuming

이렇게 되면 -[NSView hitTest:]에 breakpoint가 걸리며 실행될 때마다 아래처럼 pause가 걸린다.

Process 1809 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x00000001c74542d8 AppKit` -[NSView hitTest:] 
AppKit`-[NSView hitTest:]:
->  0x1c74542d8 <+0>:  pacibsp 
    0x1c74542dc <+4>:  sub    sp, sp, #0x60
    0x1c74542e0 <+8>:  stp    x26, x25, [sp, #0x10]
    0x1c74542e4 <+12>: stp    x24, x23, [sp, #0x20]
    0x1c74542e8 <+16>: stp    x22, x21, [sp, #0x30]
    0x1c74542ec <+20>: stp    x20, x19, [sp, #0x40]
    0x1c74542f0 <+24>: stp    x29, x30, [sp, #0x50]
    0x1c74542f4 <+28>: add    x29, sp, #0x50
Target 0: (Xcode) stopped.

현재 멈춘 곳의 정보를 알기 위해서는 po $rdi라고 입력하면 된다던데… 이건 x86_64 기준 register이다. (rdi, rsi, rdx, rcx, r8, r9, …, r15)

Apple Silicon 맥을 쓰는 유저라면 arm64이므로 register는 x0, x1, x2, x3, …, x30이 된다.

(lldb) po $rd1
error: expression failed to parse:
error: <user expression 1>:1:1: use of undeclared identifier '$rd1'
$rd1
^

(lldb) po $x0
<NSThemeFrame: 0x11c30f610>

이 상태에서 continue를 해도 계속 걸린다… NSView가 엄청나게 많은 구조여서 그런듯? 일단 pause가 계속 걸리는게 성가시니 지우자

(lldb) breakpoint delete
About to delete all breakpoints, do you want to do that?: [Y/n] y
All breakpoints removed. (1 breakpoint)

그리고 아래같은 새로운 breakpoint를 만들어준다. -n은 symbol 이름이며, -C는 breakpoint가 걸릴 때 실행할 명령어, -G1은 breapoint가 걸려도 pause하지 않겠다는 것이다. (-G0일 경우 pause한다.)

(lldb) breakpoint set -n "-[NSView hitTest:]" -C "po $x0" -G1
Breakpoint 2: where = AppKit`-[NSView hitTest:], address = 0x00000001c74542d8

이렇게 하고 continue를 하면 굉장히 많은 부분에서 breakpoint가 걸리며, -C가 실행되는 것을 볼 수 있다.

(lldb) c
Process 1809 resuming
(lldb)  po $x0
<NSTitlebarContainerView: 0x12662dab0>
(lldb)  po $x0
<DVTControllerContentView_ControlledBy_IDEWorkspaceTabController: 0x11c3bcc80>
(lldb)  po $x0
<NSView: 0x11c0148b0>
(lldb)  po $x0
<IDEWorkspaceDesignAreaSplitView:0x11c3bd9c0 delegate="(IDEWorkspaceDesignAreaSplitViewController)0x60000303d710" layout="constraints", dividers="views", arrangesAllSubviews="no">

이제 conditional breakpoint을 걸어본다. -c을 쓰면 할 수 있는데, 위에서 명령어를 실행하는 -C와 다른 것에 유의하자.

(lldb) breakpoint modify -c '(BOOL)[NSStringFromClass((id)[$x0 class]) containsString:@"IDESourceEditorView"]' -G0

이렇게 하면 Xcode에서 코드 입력창을 누를 때만 아래처럼 pause가 걸린다.

(lldb)  po $x0
IDESourceEditorView: Frame: (0.0, 0.0, 866.0, 900.0), Bounds: (0.0, 0.0, 866.0, 900.0) contentViewOffset: 0.0
Process 1809 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
    frame #0: 0x00000001c74542d8 AppKit` -[NSView hitTest:] 
AppKit`-[NSView hitTest:]:
->  0x1c74542d8 <+0>:  pacibsp 
    0x1c74542dc <+4>:  sub    sp, sp, #0x60
    0x1c74542e0 <+8>:  stp    x26, x25, [sp, #0x10]
    0x1c74542e4 <+12>: stp    x24, x23, [sp, #0x20]
    0x1c74542e8 <+16>: stp    x22, x21, [sp, #0x30]
    0x1c74542ec <+20>: stp    x20, x19, [sp, #0x40]
    0x1c74542f0 <+24>: stp    x29, x30, [sp, #0x50]
    0x1c74542f4 <+28>: add    x29, sp, #0x50
Target 0: (Xcode) stopped.

여기서 p/x 명령어로 breakpoint가 걸린 곳의 포인터 참조 주소를 알 수 있다.

(lldb) p/x $x0
(unsigned long) $192 = 0x0000000111ccf200

po $x0 했던 것 처럼, po에 참조 주소를 넣으면 해당 객체의 정보를 알 수 있다.

(lldb) po 0x0000000111ccf200
IDESourceEditorView: Frame: (0.0, 0.0, 866.0, 900.0), Bounds: (0.0, 0.0, 866.0, 900.0) contentViewOffset: 0.0

만약에 breakpoint가 걸린 NSViewisHidden 상태를 바꾸고 싶다면 아래와 같이 하면 된다.

(lldb) po [$x0 setHidden:!(BOOL)[$x0 isHidden]]; [CATransaction flush]
 nil

마찬가지로 이런 식의 Selector 전송도 된다.

(lldb) po [$x0 string]
//
//  ViewController.swift
//  Hello Debugger
//
//  Created by Jinwoo Kim on 5/5/22.
//

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }

}



만약에 Swift로 하고 싶은 경우 우선 필요한 module들을 import해준다.

(lldb) ex -l swift -- import Foundation
(lldb) ex -l swift -- import AppKit

이런 식으로 Swift 코드를 실행할 수 있다.

(lldb) ex -l swift -o -- unsafeBitCast(0x0000000111ccf200, to: NSView.self)
IDESourceEditorView: Frame: (0.0, 0.0, 866.0, 900.0), Bounds: (0.0, 0.0, 866.0, 900.0) contentViewOffset: 0.0

이런 식으로 insertText(_:)라는 함수 실행도 되는데… string은 안 된다. 아마 insertText(_:)NSView의 method가 아니더라도 어딘가에서 저렇게 생긴 Selector가 존재해서 그거 가져다가 쓰는 것 같은데, string이란 Selector는 없어서 그런듯? AnyObject가 Selector dispatch하는 방식을 생각하면 이해가 될지도 모른다. perform(_:)을 쓰면 될 것 같기도…

(lldb) ex -l swift -o -- unsafeBitCast(0x0000000111ccf200, to: NSView.self).insertText("Yay! Swift!")
0 elements

(lldb) po [$x0 string]
//
//  ViewController.swift
//  Hello Debugger
//
//  Created by Jinwoo Kim on 5/5/22.
//

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }

}

Yay! Swift!

(lldb) ex -l swift -o -- unsafeBitCast(0x0000000111ccf200, to: NSView.self).string
error: expression failed to parse:
error: <EXPR>:3:52: error: value of type 'NSView' has no member 'string'
unsafeBitCast(0x0000000111ccf200, to: NSView.self).string
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ^~~~~~

Chapter 2: Help & Apropos

help 명령어를 치면 무진장 많은 명령어들의 도움말들을 볼 수 있다.

(lldb) help
Debugger commands:
  apropos           -- List debugger commands related to a word or subject.
  breakpoint        -- Commands for operating on breakpoints (see 'help b' for shorthand.)
  command           -- Commands for managing custom LLDB commands.
  disassemble       -- Disassemble specified instructions in the current target.  Defaults to the
                       current function for the current thread and stack frame.
  expression        -- Evaluate an expression on the current thread.  Displays any returned value
                       with LLDB's default formatting.
  frame             -- Commands for selecting and examing the current thread's stack frames.
# 생략...

For more information on any command, type 'help <command-name>'.

이는 DerekSelander/LLDB~/.lldbinit에 설치했을 경우 그 명령어들의 도움말도 뜬다. 더 상세히 알고 싶으면 help breakpoint, help breakpoint name 이런 식으로 치면 된다.

(lldb) help breakpoint
Commands for operating on breakpoints (see 'help b' for shorthand.)

Syntax: breakpoint <subcommand> [<command-options>]

The following subcommands are supported:

      clear   -- Delete or disable breakpoints matching the specified source file and line.
      command -- Commands for adding, removing and listing LLDB commands executed when a breakpoint
                 is hit.
      delete  -- Delete the specified breakpoint(s).  If no breakpoints are specified, delete them
                 all.
      disable -- Disable the specified breakpoint(s) without deleting them.  If none are specified,
                 disable all breakpoints.
# 생략...

For more help on any particular subcommand, type 'help <command> <subcommand>'.

apropos를 쓸 경우 단어 검색이 가능하다.

(lldb) apropos swift
The following commands may relate to 'swift':
  swift             -- A set of commands for operating on the Swift Language Runtime.
  demangle          -- Demangle a Swift mangled name
  refcount          -- Inspect the reference count data for a Swift object
  swift-healthcheck -- Show the LLDB debugger health check diagnostics.
  dclass            -- Dumps info about objc/swift classes
# 생략...

만약에 reference count을 알아내는 명령어가 기억 안 난다! 싶으면 아래처럼 하면 refcount을 쓰면 된다는 것을 알 수 있다.

(lldb) apropos "reference count"
The following commands may relate to 'reference count':
  refcount -- Inspect the reference count data for a Swift object

Chapter 3: Attaching with LLDB

아래처럼 lldb에서 이미 실행된 프로세스에 attach 할 수 있다. (참고로 pgrep -x Xcode 명령어로 PID를 받아 올 수 있다.)

% lldb -n Xcode
(lldb) process attach --name "Xcode"

...

% lldb -p 3386
(lldb) process attach --pid 3386

만약 다음 번에 실행될 프로세스에 attach하고 싶은 경우, -w (--waitFor)를 쓰면 된다. 이럴 경우 파일 경로로 통해서도 설정할 수 있는데 이때는 -w가 필요 없다.

% lldb -n Finder -w
(lldb) process attach --name "Finder" --waitfor

...

% lldb -f /System/Library/CoreServices/Finder.app/Contents/MacOS/Finder
(lldb) target create "/System/Library/CoreServices/Finder.app/Contents/MacOS/Finder"
Current executable set to '/System/Library/CoreServices/Finder.app/Contents/MacOS/Finder' (arm64e).

-f을 통해 파일 경로로 지정했을 경우, process launch 명령어로 바로 실행시킬 수 있다.

(lldb) process launch
Process 3473 launched: '/System/Library/CoreServices/Finder.app/Contents/MacOS/Finder' (arm64e)

아래 명령어로 설정된 target을 지울 수 있다.

(lldb) target delete

이제 /bin/ls를 통해 실행 중에 옵션을 넣는 방법에 대해 알자면, 일단 /bin/ls로 설정한 후

% lldb -f /bin/ls
(lldb) target create "/bin/ls"
Current executable set to '/bin/ls' (arm64e).

위에서 설명했듯이 process launch를 실행하면 해당 프로그램을 실행할 수 있다.

(lldb) process launch
Process 3502 launched: '/bin/ls' (arm64e)
Applications	Documents	Movies		Postman		git
Brewfile	Downloads	Music		Public		lldb_commands
Desktop		Library		Pictures	Sites		theos
Process 3502 exited with status = 0 (0x00000000) 

-w 옵션을 쓰면 cd를 한 다음에 프로그램을 실행하게 된다. 예를 들어 아래와 같이 명령어를 입력하면

(lldb) process launch -w /Applications
Process 3512 launched: '/bin/ls' (arm64e)

# /Applications의 내용...

아래와 같은 내용이라 할 수 있다.

% cd /Applications
% ls

-- 옵션을 쓰면 프로그램 실행의 arguments를 지정할 수 있다.

(lldb) process launch -w /Applications
Process 3512 launched: '/bin/ls' (arm64e)

# /Applications의 내용...

이는 % ls /Applications와 같은거라 할 수 있다. 하지만 --는 아래이 ~ 문구가 같은 경우는 에러가 난다.

(lldb) process launch -- ~/Desktop
Process 3557 launched: '/bin/ls' (arm64e)
ls: ~/Desktop: No such file or directory
Process 3557 exited with status = 1 (0x00000001) 

이럴 경우 -X true를 써주면 된다.

(lldb) process launch -X true -- ~/Desktop
Process 3574 launched: '/bin/ls' (arm64e)

# ~/Desktop의 내용...

이는 run 명령어로도 할 수 있다.

(lldb) run ~/Desktop
Process 3583 launched: '/bin/ls' (arm64e)

# ~/Desktop의 내용...

만약에 현재 설정된 environment variable들을 보고 싶으면, env 명령어를 쓰면 된다.

(lldb) env
target.env-vars (dictionary of strings) =

책에서는 이런 식으로 environment variable을 설정할 수 있다는데… 내가 해보니 안 됨. shell이 달라서 그런듯…

(lldb) process launch -v LSCOLORS=Af -v CLICOLOR=1  -- /Applications/

이거는 터미널에서 아래 명령어와 같은거라 함.

$ LSCOLORS=Af CLICOLOR=1 ls /Applications/

만약에 출력 결과물을 파일로 저장하고 싶으면, -o를 쓰면 된다. 터미널에서 > 기호라고 생각하면 될듯하다.

(lldb) process launch -o /tmp/ls_output.txt -- /Applications

이러면 아래 터미널 명령어로 잘 저장되었는지 볼 수 있다.

% cat /tmp/ls_output.txt

-i을 쓰면 터미널의 < 기호같은 것을 쓸 수 있다.

(lldb) target create /usr/bin/wc

(lldb) process launch -i /tmp/ls_output.txt

(lldb) run

마지막 명령어의 경우 wc에 아무런 input이 없어서 멈출텐데, 해당 프로세스를 강제종로 시키고 싶으면 Ctrl + D를 누르면 된다.

Chapter 4: Stopping in Code

Symbol 검색하기

(lldb) image lookup -n "-[UIViewController viewDidLoad]"
1 match found in /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore:
        Address: UIKitCore[0x00000000004bd8d0] (UIKitCore.__TEXT.__text + 4957852)
        Summary: UIKitCore`-[UIViewController viewDidLoad]

정규식으로 Symbol 검색하기

(lldb) image lookup -rn test
1 match found in /Users/pookjw/Library/Developer/Xcode/DerivedData/Signals-byjsnxuqpxeyyddvxghkubpurqlj/Build/Products/Debug-iphonesimulator/Signals.app/Signals:
        Address: Signals[0x0000000100005e78] (Signals.__TEXT.__text + 15360)
        Summary: Signals`Signals.DetailViewController.test() throws -> () at DetailViewController.swift:52

Swift Symbol 검색하기

(lldb) image lookup -rn Signals.SwiftTestClass.name.setter
1 match found in /Users/pookjw/Library/Developer/Xcode/DerivedData/Signals-byjsnxuqpxeyyddvxghkubpurqlj/Build/Products/Debug-iphonesimulator/Signals.app/Signals:
        Address: Signals[0x000000010000b5fc] (Signals.__TEXT.__text + 37764)
        Summary: Signals`Signals.SwiftTestClass.name.setter : Swift.Optional<Swift.String> at SwiftTestClass.swift:32

(lldb) image lookup -rn Signals.SwiftTestClass.name.(getter|setter)
2 matches found in /Users/pookjw/Library/Developer/Xcode/DerivedData/Signals-byjsnxuqpxeyyddvxghkubpurqlj/Build/Products/Debug-iphonesimulator/Signals.app/Signals:
        Address: Signals[0x000000010000b58c] (Signals.__TEXT.__text + 37652)
        Summary: Signals`Signals.SwiftTestClass.name.getter : Swift.Optional<Swift.String> at SwiftTestClass.swift:32        Address: Signals[0x000000010000b5fc] (Signals.__TEXT.__text + 37764)
        Summary: Signals`Signals.SwiftTestClass.name.setter : Swift.Optional<Swift.String> at SwiftTestClass.swift:32

Swift Symbol로 breakpoint 걸기 (위에서 얻은 정확한 Symbol 이름을 넣어주면 된다)

(lldb) b Signals`Signals.SwiftTestClass.name.setter : Swift.Optional<Swift.String>
Breakpoint 3: where = Signals`Signals.SwiftTestClass.name.setter : Swift.Optional<Swift.String> at SwiftTestClass.swift:32, address = 0x00000001027d75fc

단축어로 breakpoint를 걸 수 있다.

(lldb) b -[UIViewController viewDidLoad]
Breakpoint 4: where = UIKitCore`-[UIViewController viewDidLoad], address = 0x000000018462b8d0

(lldb) b Signals.SwiftTestClass.name.setter : Swift.Optional<Swift.String>
Breakpoint 5: where = Signals`Signals.SwiftTestClass.name.setter : Swift.Optional<Swift.String> at SwiftTestClass.swift:32, address = 0x00000001027d75fc

정규식을 통해서도 걸 수 있다.

(lldb) rb SwiftTestClass.name.setter
Breakpoint 6: where = Signals`Signals.SwiftTestClass.name.setter : Swift.Optional<Swift.String> at SwiftTestClass.swift:32, address = 0x00000001027d75fc

(lldb) rb name\.setter
Breakpoint 7: 8 locations.

(lldb) rb '\-\[UIViewController\ '
Breakpoint 8: 796 locations.

# Category들만
(lldb) rb '\-\[UIViewController(\(\w+\))?\ '
Breakpoint 9: 796 locations.

# DetailViewController.swift 안에 있는 모드 코드에
(lldb) rb . -f DetailViewController.swift
Breakpoint 10: 65 locations.

# 모든 Symbol에 breakpoint 걸기...
(lldb) rb .

# 특정 Module에 있는 모든 Symbol에 breakpoint 걸기...
(lldb) rb . -s Commons
(lldb) rb . -s UIKitCore

만약에 breakpoint가 몇번 이상 걸리면 자동으로 delete되게 처리하려면 -o (one-shot)을 쓰면 된다.

(lldb) rb . -s UIKitCore -o 1

breakpoint 명령어들 예제를 적자면

# Commons moudle 안의 Swift 언어로 된 모든 Symbol들에 breakpoint를 건다.
(lldb) breakpoint set -L swift -r . -s Commons

# 모든 소스코드 파일 (-A)에서 "if let"이 포함되어 있으면 breakpoint를 건다.
(lldb) breakpoint set -A -p "if let"

# 특정 파일만 하고 싶으면
(lldb) breakpoint set -p "if let" -f MasterViewController.swift -f DetailViewController.swift

# Signals model 안에 있는 모든 소스코드에서 "if let"이 포함되어 있는 곳에 breakpoint를 건다.
(lldb) breakpoint set -p "if let" -s Signals -A

# `-[UIViewController viewDidLoad]`가 걸리면 명령어를 실행하고 pause하지 않는다.
(lldb) breakpoint set -n "-[UIViewController viewDidLoad]` -C "po $arg1" -G1

여담으로 디버깅 결과물들을 파일로 쓸 수 있다.

(lldb) breakpoint write -f /tmp/br.json
(lldb) platform shell cat /tmp/br.json

# re-import
(lldb) breakpoint read -f /tmp/br.json

현재 설정된 breakpoint 목록보기

# 전체 breakpoint
(lldb) breakpoint list

# 첫번째꺼만
(lldb) breakpoint list 1

# 간략하게 (briefly)
(lldb) breakpoint list 1 -b

# 첫번째꺼의 첫번째 symbol만
(lldb) breakpoint list 1.1

# 특정 breakpoint만 지우기
(lldb) breakpoint delete 1
(lldb) breakpoint delete 1.1

Chapter 5: Expression

NSObject.description을 override 해보면

override var description: String {
  return "Yay! debugging " + super.description
}

print("\(self)")로 찍어보면 위에서 정의한 description이 나오는 것을 확인할 수 있다.

Yay! debugging <Signals.MasterViewController: 0x7f8a0ac06b70>

마찬가지로 lldb에서 po self를 하면 똑같이 나온다.

(lldb) po self
Yay! debugging <Signals.MasterViewController: 0x7f8a0ac06b70>

이제 NSObject.debugDescription을 override 해보면

override var debugDescription: String {
  return "debugDescription: " + super.debugDescription
}

print(_:)에서는 여전히 description이 나오지만, po self에서는 debugDescription이 나오는 것을 확인할 수 있다.

(lldb) po self
debugDescription: Yay! debugging <Signals.MasterViewController: 0x7fb71fd04080>

이제 p self를 해보면 raw를 볼 수 있다.

(lldb) p self
(Signals.MasterViewController) $R0 = 0x0000000149615e70 {
  UIKit.UITableViewController = {
    baseUIViewController@0 = {
      baseUIResponder@0 = {
        baseNSObject@0 = {
          isa = Signals.MasterViewController
        }
      }
      _overrideTransitioningDelegate = 0x0000000000000000
      _view = some {
        some = 0x000000014a02ee00 {
          baseUIScrollView@0 = {
            baseUIView@0 = {

# 생략...

또한 pop가 불릴 경우, 출력의 메모리 주소는 $R0, $R1, $R2… 이런 식으로 register에 저장되며, continue가 되면 다 날라간다.

(lldb) p self
(Signals.MasterViewController) $R0 = 0x00000001286053c0 {
# 생략

(lldb) p self
(Signals.MasterViewController) $R1 = 0x00000001286053c0 {
# 생략

(lldb) p self
(Signals.MasterViewController) $R2 = 0x00000001286053c0 {
# 생략

(lldb) po self
debugDescription: Yay! debugging <Signals.MasterViewController: 0x1286053c0>

(lldb) p self
(Signals.MasterViewController) $R4 = 0x00000001286053c0 {
# 생략

(lldb) po $R5
debugDescription: Yay! debugging <Signals.MasterViewController: 0x1286053c0>

(lldb) po $R6
debugDescription: Yay! debugging <Signals.MasterViewController: 0x1286053c0>

또한 type 명령어로 통해 p에서 나오는 결과를 커스텀 할 수 있다.

(lldb) type summary add Signals.MasterViewController --summary-string "Wahho!"
(lldb) p self
(Signals.MasterViewController) $R8 = 0x00000001286053c0 Wahho!

# 초기화
(lldb) type summary clear

po에 대해 좀 더 알아보자면, po는 코드 실행도 가능하다. 다만 현재 context 언어 (Swift, Objective-C)에 맞게 써야 한다.

(lldb) po [UIApplication sharedApplication]
error: expression failed to parse:
error: <EXPR>:8:16: error: expected ',' separator
[UIApplication sharedApplication]
               ^
              ,

(lldb) po UIApplication.shared
<UIApplication: 0x1485042c0>
(lldb) po [UIApplication sharedApplication]
<UIApplication: 0x1485042c0>

(lldb) po UIApplication.shared
error: expression failed to parse:
error: No module map file in /Users/pookjw/Library/Developer/Xcode/DerivedData/Signals-cwkqbgfljqhkztfcsvolnunfygne/Build/Products/Debug-iphonesimulator/Commons.framework

error: <user expression 2>:1:15: property 'shared' not found on object of type 'UIApplication'
UIApplication.shared

Context에 상관 없이 특정 언어를 지정해서 실행할 수도 있다.

(lldb) expression -l objc -O -- [UIApplication sharedApplication]
<UIApplication: 0x1485042c0>

(lldb) expression -l swift -O -- UIApplication.shared
<UIApplication: 0x1485042c0>

expression-O는 아래와 같다.

-O ( --object-description )
            Display using a language-specific description API, if possible.

변수를 선언할 수도 있다. 물론 ARC는 안 된다.

# 이렇게 하면 안 된다.
(lldb) po id test = [NSObject new]
(lldb) po test
error: use of undeclared identifier 'test

# 이렇게 해야 한다.
(lldb) po id $test = [NSObject new]
(lldb) po $test
<NSObject: 0x600000718100>

(lldb) po [$test release]
0x0000000104af8000

# Swift에서...
(lldb) po id $test = [NSObject new]
(lldb) expression -l swift -O -- $test
<NSObject: 0x600000f580c0>

# 이런건 아직 안 되는듯? Bridging이 생각하는 것 처럼 되진 않는다 함
(lldb) expression -l swift -O -- $test.description
error: expression failed to parse:
error: <EXPR>:3:1: error: cannot find '$test' in scope
$test.description
^~~~~

expression-i 플래그에 대해서도 알아보자. expression을 통해 명령어가 실행됐을 때, 그 명령어를 현재 breakpoint에도 걸리게 할지를 정할 수 있다.

-i <boolean> ( --ignore-breakpoints <boolean> )
            Ignore breakpoint hits while running expressions
# `-[UIViewController viewDidLoad]`에 breakpoint를 설정한다.
(lldb) b -[UIViewController viewDidLoad]
Breakpoint 3: where = UIKitCore`-[UIViewController viewDidLoad], address = 0x000000018462b8d0

# `-i` 플래그를 안 쓰면 위에서 정의한 breakpoint가 기본적으로 무시된다.
(lldb) expression -l swift -O -- self.viewDidLoad()
Yay! debugging <Signals.MasterViewController: 0x135510060>
0 elements

# 하지만 `-i 0`으로 설정하면 breakpoint 때문에 에러가 난다.
(lldb) expression -l swift -O -i 0 -- self.viewDidLoad()
error: Execution was interrupted, reason: breakpoint 3.1.
The process has been left at the point where it was interrupted, use "thread return -x" to return to the state before expression evaluation.

마지막으로 expression은 format을 지정해서 출력할 수 있다. 예를 들어, -G의 경우 GDB format이다. 이건 지엽적이라 그냥 책을 보고 공부하자.

(lldb) expression -G x -- 10
(int) $0 = 0x0000000a

Chapter 6: Thread, Frame & Stepping Around

Stack과 Heap

lldb로 backtrace를 볼 수 있다.

(lldb) b Signals.MasterViewController.viewWillAppear(Swift.Bool) -> ()
Breakpoint 2: where = Signals`Signals.MasterViewController.viewWillAppear(Swift.Bool) -> () at MasterViewController.swift:53, address = 0x0000000104c0ad44
(lldb) c
Process 19367 resuming

# breakpoint가 걸리면

# backtrace 보기
(lldb) thread backtrace
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
  * frame #0: 0x0000000104c0ad44 Signals`MasterViewController.viewWillAppear(animated=false, self=0x0000000000000000) at MasterViewController.swift:53
    frame #1: 0x0000000104c0b27c Signals`@objc MasterViewController.viewWillAppear(_:) at <compiler-generated>:0
    frame #2: 0x0000000184631720 UIKitCore`-[UIViewController _setViewAppearState:isAnimating:] + 604

# 현재 frame 정보
(lldb) frame info
frame #0: 0x0000000104c0ad44 Signals`MasterViewController.viewWillAppear(animated=false, self=0x0000000000000000) at MasterViewController.swift:53

# Thread 목록 보기
(lldb) thread list
Process 19367 stopped
* thread #1: tid = 0x72601, 0x0000000104c0ad44 Signals`MasterViewController.viewWillAppear(animated=false, self=0x0000000000000000) at MasterViewController.swift:53, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
  thread #2: tid = 0x726ad, 0x00000001cba33ce4 libsystem_kernel.dylib`__workq_kernreturn + 8
  thread #3: tid = 0x726ae, 0x00000001cba888fc libsystem_pthread.dylib`start_wqthread

# Thread 변경
(lldb) thread select 7
(lldb) thread backtrace
* thread #7, queue = 'com.apple.UIKit.KeyboardManagement'
  * frame #0: 0x00000001cba33dd4 libsystem_kernel.dylib`__ulock_wait + 8
    frame #1: 0x0000000104ff25fc libdispatch.dylib`_dlock_wait + 52
    frame #2: 0x0000000104ff23ac libdispatch.dylib`_dispatch_thread_event_wait_slow + 52

# frame 변경
(lldb) frame select 10
frame #10: 0x00000001809258d8 Foundation`__88-[NSXPCConnection _sendInvocation:orArguments:count:methodSignature:selector:withProxy:]_block_invoke_3 + 208
Foundation`__88-[NSXPCConnection _sendInvocation:orArguments:count:methodSignature:selector:withProxy:]_block_invoke_3:
->  0x1809258d8 <+208>: b      0x180925c38               ; <+1072>
    0x1809258dc <+212>: adrp   x8, 385088
    0x1809258e0 <+216>: ldr    x8, [x8, #0xf50]
    0x1809258e4 <+220>: cmp    x21, x8
    0x1809258e8 <+224>: b.eq   0x1809259dc               ; <+468>
    0x1809258ec <+228>: adrp   x8, 385088
    0x1809258f0 <+232>: ldr    x8, [x8, #0xf58]
    0x1809258f4 <+236>: cmp    x21, x8
(lldb) frame info
frame #10: 0x00000001809258d8 Foundation`__88-[NSXPCConnection _sendInvocation:orArguments:count:methodSignature:selector:withProxy:]_block_invoke_3 + 208

step은 아래처럼 할 수 있다.

# 둘이 같은듯? 코드를 step 할 수 있음
(lldb) next
(lldb) step -a 1

# 얘는 Assembly를 step
(lldb) step
(lldb) step -a 0

# Assembly step을 끝냄 (코드 step으로 돌아가는거임)
(lldb) finish

현재 frame에 존재하는 변수들 알아볼 수 있다. Xcode에서 뜨는거랑 같은 원리.

(lldb) frame variable
(Bool) animated = false
(Signals.MasterViewController) self = 0x0000000137d27740 {
  UIKit.UITableViewController = {
    baseUIViewController@0 = {
      baseUIResponder@0 = {
        baseNSObject@0 = {
          isa = Signals.MasterViewController
        }
      }
# 생략...

# Flat하게 보기
(lldb) frame variable -F
animated._value = 0
self = 0x0000000137d27740
self =
self.isa = Signals.MasterViewController
self._overrideTransitioningDelegate = 0x0000000000000000
self._view = some
# 생략...

# 특정 이름만 보기
(lldb) frame variable -F animated
animated._value = 0

(lldb) frame variable -F self
self = 0x0000000137d27740
self =
self.isa = Signals.MasterViewController
self._overrideTransitioningDelegate = 0x0000000000000000
self._view = some
self._view.some = 0x0000000140851400
self._view.some.isa = UITableView
# 생략...

Chapter 7: Image

현재 load된 Image 목록을 아래처럼 알아낼 수 있다.

(lldb) image list
[  0] 73AB11C0-1DF9-3381-87D6-C9CF673C054C 0x0000000104c08000 /Users/pookjw/Library/Developer/Xcode/DerivedData/Signals-akjtdbeivvcitldylztlxnerzrbj/Build/Products/Debug-iphonesimulator/Signals.app/Signals 
[  1] F82A5CF7-0669-37E6-949A-963C97AA692C 0x0000000104f38000 /usr/lib/dyld 
[  2] 6E6188A1-7B04-3AE8-BE81-CB35B05B8358 0x0000000104e50000 /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/dyld_sim 
# 생략...

(lldb) image list Foundation
[  0] C5E56BF5-55BE-3ABB-947C-CDC542A7B3B5 0x00000001806fd000 /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/Foundation.framework/Foundation 

모든 symbol을 dump 뜰 수도 있다.

(lldb) image dump symtab UIKitCore -s address
Symtab, file = /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore, num_symbols = 167998 (sorted by address):
               Debug symbol
               |Synthetic symbol
               ||Externally Visible
               |||
Index   UserID DSX Type            File Address/Value Load Address       Size               Flags      Name
------- ------ --- --------------- ------------------ ------------------ ------------------ ---------- ----------------------------------
[    0]      0     Code            0x0000000000003234 0x0000000184171234 0x0000000000000168 0x000e0000 -[UIColorWell _commonInit]
[    1]      1     Code            0x000000000000339c 0x000000018417139c 0x0000000000000058 0x000e0000 -[UIColorWell initWithFrame:]

# 생략...

symbol을 검색할 수도 있다.

(lldb) image lookup -n "-[UIViewController viewDidLoad]"
1 match found in /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore:
        Address: UIKitCore[0x00000000004bd8d0] (UIKitCore.__TEXT.__text + 4957852)
        Summary: UIKitCore`-[UIViewController viewDidLoad]

(lldb) image lookup -rn '\[UIViewController\ '
843 matches found in /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore:
        Address: UIKitCore[0x00000000004b16c8] (UIKitCore.__TEXT.__text + 4908180)
        Summary: UIKitCore`-[UIViewController _presentationControllerClassName]        Address: UIKitCore[0x00000000004b16e8] (UIKitCore.__TEXT.__text + 4908212)
        Summary: UIKitCore`-[UIViewController navigationInsetAdjustment]        Address: UIKitCore[0x00000000004b1734] (UIKitCore.__TEXT.__text + 4908288)
# 생략...

Type의 정보를 알아낼 수도 있다.

# C 구조체
(lldb) image lookup -t sigaction
Best match found in /Users/pookjw/Library/Developer/Xcode/DerivedData/Signals-akjtdbeivvcitldylztlxnerzrbj/Build/Products/Debug-iphonesimulator/Signals.app/Signals:
id = {0x000057ee}, name = "sigaction", byte-size = 16, decl = signal.h:286, compiler_type = "struct sigaction {
    __sigaction_u __sigaction_u;
    sigset_t sa_mask;
    int sa_flags;
}"

(lldb) image lookup -t NSObject
Best match found in /Users/pookjw/Library/Developer/Xcode/DerivedData/Signals-akjtdbeivvcitldylztlxnerzrbj/Build/Products/Debug-iphonesimulator/Signals.app/Signals:
id = {0x000002b8}, name = "NSObject", byte-size = 8, decl = NSObject.h:53, compiler_type = "@interface NSObject{
    Class isa;
}
@end"

또한 NSObject(IvarDescription) Category에는 재밌는 것들이 있는데,

(lldb) image lookup -rn NSObject\(IvarDescription\)
7 matches found in /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore:
        Address: UIKitCore[0x0000000001002368] (UIKitCore.__TEXT.__text + 16773428)
        Summary: UIKitCore`-[NSObject(IvarDescription) __ivarDescriptionForClass:]        Address: UIKitCore[0x000000000100251c] (UIKitCore.__TEXT.__text + 16773864)
        Summary: UIKitCore`-[NSObject(IvarDescription) _ivarDescription]        Address: UIKitCore[0x000000000100262c] (UIKitCore.__TEXT.__text + 16774136)
        Summary: UIKitCore`-[NSObject(IvarDescription) __propertyDescriptionForClass:]        Address: UIKitCore[0x0000000001002ab0] (UIKitCore.__TEXT.__text + 16775292)
        Summary: UIKitCore`-[NSObject(IvarDescription) _propertyDescription]        Address: UIKitCore[0x0000000001002ba8] (UIKitCore.__TEXT.__text + 16775540)
        Summary: UIKitCore`-[NSObject(IvarDescription) __methodDescriptionForClass:]        Address: UIKitCore[0x0000000001003088] (UIKitCore.__TEXT.__text + 16776788)
        Summary: UIKitCore`-[NSObject(IvarDescription) _methodDescription]        Address: UIKitCore[0x0000000001003180] (UIKitCore.__TEXT.__text + 16777036)
        Summary: UIKitCore`-[NSObject(IvarDescription) _shortMethodDescription]

요약하면 _ivarDescription으로 ivar들을 볼 수 있다. 예를 들어 UIApplication 같은 경우

(lldb) po [[UIApplication sharedApplication] _ivarDescription]
<UIApplication: 0x15e504610>:
in UIApplication:
	_delegate (<UIApplicationDelegate>*): <Signals.AppDelegate: 0x600002d610a0>
	_remoteControlEventObservers (long): 0
	_topLevelNibObjects (NSArray*): nil
	_networkResourcesCurrentlyLoadingCount (long): 0
	_hideNetworkActivityIndicatorTimer (NSTimer*): nil
	_editAlertController (UIAlertController*): nil
	_statusBar (UIStatusBar*): nil
	_statusBarRequestedStyle (long): 0
	_statusBarWindow (UIStatusBarWindow*): nil
# 생략

이런 재밌는 정보들이 나온다. UIStatusBar는 iOS 13 이후로 UIScene의 등장에 따라 사라진 것 같다.

(lldb) po [UIApplication sharedApplication]->_statusBar
 nil

Chapter 8: Watchpoints

breakpoint의 경우 Stack 영역만 관찰이 가능하고 Heap 영역의 변화는 감지할 수 없다. 예를 들어 특정 포인터의 값을 변경하는 것을 관찰하고 싶다면, setter/getter가 존재하지 않을 경우 (ivar에 직접 접근하는 경우) 관찰이 불가능하다. 이런 경우 watchpoint를 써야 한다.

아래처럼 shouldEnableSignalHandling라는 property가 있다고 하자. ivar는 _shouldEnableSignalHandling로 된다.

@interface UnixSignalHandler : NSObject
@property (nonatomic) BOOL shouldEnableSignalHandling;
@end

UnixSignalHandler의 메모리가 할당될 경우, 시작 포인터 주소로부터 offset을 더하면 _shouldEnableSignalHandling의 포인터 주소가 나온다. 이 offset을 구하기 위해 dump를 해준다. (가끔 명령어 실행해도 안 뜰 때가 있는듯? 로드가 덜 되어서 그런가)

(lldb) language objc class-table dump UnixSignalHandler -v
isa = 0x1006145d8 name = UnixSignalHandler instance size = 56 num ivars = 4 superclass = NSObject
  ivar name = source type = id size = 8 offset = 24
  ivar name = _shouldEnableSignalHandling type = bool size = 1 offset = 32

_shouldEnableSignalHandling의 경우 사이즈가 1 byte, offset은 32인 것을 알 수 있다. 만약 UnixSignalHandler의 포인터 값이 0x6000017b9ac0일 경우,

(lldb) po 0x6000017b9ac0
<UnixSignalHandler: 0x6000017b9ac0>

(lldb) p/x 0x6000017b9ac0 + 32
(long) $1 = 0x00006000017b9ae0

(lldb) watchpoint set expression -s 1 -w write -- 0x00006000017b9ae0
Watchpoint created: Watchpoint 1: addr = 0x6000017b9ae0 size = 1 state = enabled type = w
    new value: 0

이런 식으로 _shouldEnableSignalHandling에 watchpoint를 정의할 수 있다. -s는 size를 정의할 수 있으며 위에서 사이즈는 1인 것을 알 수 있었다. 실제로 아래 코드에서 자동으로 pause가 걸리는 것을 확인할 수 있다.

- (void)setShouldEnableSignalHandling:(BOOL)shouldEnableSignalHandling {
  self->_shouldEnableSignalHandling = shouldEnableSignalHandling;
  // 생략
}

정확히 어떤 부분이 watchpoint에 걸렸는지 보고 싶을 경우, disassemble을 해보면

(lldb) disassemble -m

# 생략

** 146 	  self->_shouldEnableSignalHandling = shouldEnableSignalHandling;
   147 	  sigset_t signals;

    0x10060a9b0 <+32>:  ldrb   w8, [sp, #0xf]
    0x10060a9b4 <+36>:  ldur   x9, [x29, #-0x8]
    0x10060a9b8 <+40>:  and    w8, w8, w10
    0x10060a9bc <+44>:  strb   w8, [x9, #0x20]
->  0x10060a9c0 <+48>:  mov    w8, #-0x1

# 생략

이런 식으로 끈다. strb을 보면 x90x20을 더한 값을 w8에 할당하고, w8-0x1을 할당하는 것을 확인할 수 있다. 이를 검증하기 위해 x90x20의 값을 보면

(lldb) po $x9
<UnixSignalHandler: 0x6000017b9ac0>

(lldb) p/d 0x20
(int) $6 = 32

이렇게 예상이 맞는걸 볼 수 있다.

아래 명령어로 현재 설정된 watchpoint들을 볼 수 있다.

(lldb) watchpoint list
Number of supported hardware watchpoints: 4
Current watchpoints:
Watchpoint 2: addr = 0x6000017b9ae0 size = 1 state = enabled type = w
    new value: 0

# 짧게 (brief)
(lldb) watchpoint list -b
Number of supported hardware watchpoints: 4
Current watchpoints:
Watchpoint 2: addr = 0x6000017b9ae0 size = 1 state = enabled type = w

수정할 수도 있다.

# condition을 설정
(lldb) watchpoint modify 2 -c '*(BOOL*)0x6000017b9ae0 == 0'
1 watchpoints modified.

# 명령어 추가 - backtrace (bt) 보기
(lldb) watchpoint command add 2
Enter your debugger command(s).  Type 'DONE' to end.
> bt 5
> continue
> DONE

# condition을 비롯한 모든 arguments 제거
(lldb) watchpoint modify 2
1 watchpoints modified.

# 명령어만 제거
(lldb) watchpoint command delete 2

또한 아래 명령어로 watchpoint를 모두 지울 수 있다.

# 모두 삭제
(lldb) watchpoint delete
About to delete all watchpoints, do you want to do that?: [Y/n] y
All watchpoints removed. (1 watchpoints)

참고로 Xcode에서 GUI로 watchpoint 설정을 편하게 할 수 있긴 하다.

Chapter 9: Persisting and Customizing Commands

~/.lldbinit 파일을 통해 lldb가 실행될 떄 명령어를 실행시키도록 할 수 있는데, 이를 통해 커스텀 명령어 설정이 가능하다. 예를 들어 아래 명령어를 등록하면

command alias Yay_Autolayout expression -l objc -O -- [[[[[UIApplication sharedApplication] keyWindow] rootViewController] view] recursiveDescription]

Yay_Autolayout 명령어를 실행하면 recursiveDescription를 볼 수 있다.

(lldb) Yay_Autolayout
<_UISplitViewControllerPanelImplView: 0x13fe065d0; frame = (0 0; 428 926); autoresize = W+H; gestureRecognizers = <NSArray: 0x600003178090>; layer = <CALayer: 0x600003fa6f40>>
   | <_UIPanelControllerContentView: 0x13fe0bf10; frame = (0 0; 428 926); autoresize = W+H; layer = <CALayer: 0x600003fa6d00>>
# 생략

help를 보면

(lldb) help Yay_Autolayout
Evaluate an expression on the current thread.  Displays any returned value with
LLDB's default formatting.  Expects 'raw' input (see 'help raw-input'.)

Syntax: Yay_Autolayout <cmd-options> -- <expr>

Command Options Usage:
  Yay_Autolayout [-AFLORTgp] [-f <format>] [-G <gdb-format>] [-a <boolean>] [-j <boolean>] [-X <source-language>] [-v[<description-verbosity>]] [-i <boolean>] [-l <source-language>] [-t <unsigned-integer>] [-u <boolean>] [-d <none>] [-S <boolean>] [-D <count>] [-P <count>] [-Y[<count>]] [-V <boolean>] [-Z <count>] -- <expr>

# 생략

이렇게 기본적으로 설정된 도움말 문구들이 보이는데… -H를 통해 문구를 지정할 수 있다.

command alias -H "Yay_Autolayout will get the root view and recursively dump all the subviews and their frames" -h "Recursively dump views" -- Yay_Autolayout expression -l objc -O -- [[[[[UIApplication sharedApplication] keyWindow] rootViewController] view] recursiveDescription]

이러면 기본 문구 + 커스텀 문구를 볼 수 있다.

(lldb) help Yay_Autolayout
Evaluate an expression on the current thread.  Displays any returned value with
LLDB's default formatting.  Expects 'raw' input (see 'help raw-input'.)

Syntax: Yay_Autolayout <cmd-options> -- <expr>

Command Options Usage:
  Yay_Autolayout [-AFLORTgp] [-f <format>] [-G <gdb-format>] [-a <boolean>] [-j <boolean>] [-X <source-language>] [-v[<description-verbosity>]] [-i <boolean>] [-l <source-language>] [-t <unsigned-integer>] [-u <boolean>] [-d <none>] [-S <boolean>] [-D <count>] [-P <count>] [-Y[<count>]] [-V <boolean>] [-Z <count>] -- <expr>

# 생략

Yay_Autolayout will get the root view and recursively dump all the subviews and
their frames

# 생략

argument를 받을 수 있는 커스텀 명령어를 만들 경우, 아래처럼 할 수 있다.

command alias _cpo expression -l objc -O --

이를 실험해보기 위해 viewDidLoad에서 breakpoint를 설정하고 아래처럼 해보면 된다.

(lldb) po self
<Signals.MasterViewController: 0x125e0f320>

(lldb) _cpo 0x125e0f320
<Signals.MasterViewController: 0x125e0f320>

Chapter 10: Regex Commands

Chapter 9에서 alias를 통해 커스텀 명령어를 하는 법을 익혔지만 한계점이 있어서 정규식을 써야 할 때가 있다. 아래와 같은 구조다.

s/<regex>/<subst>/

만약에 모든 조건을 다 받아 들일 수 있는 명령어를 만들고 싶을 경우, (.+)로 쓰면 된다.

(lldb) command regex rlook 's/(.+)/image lookup -rn %1/'

그러면 위에서 생성한 rlook 명령어를 아래처럼 쓸 수 있다.

(lldb) rlook viewDidLoad
6 matches found in /Users/pookjw/Library/Developer/Xcode/DerivedData/Signals-gjiobvqhhrhmpabmuvxjbklzgovn/Build/Products/Debug-iphonesimulator/Signals.app/Signals:
        Address: Signals[0x0000000100002558] (Signals.__TEXT.__text + 736)
        Summary: Signals`Signals.MasterViewController.viewDidLoad() -> () at MasterViewController.swift:37        Address: Signals[0x0000000100002d08] (Signals.__TEXT.__text + 2704)

# 생략

좀 더 응용하면 아래처럼도 할 수 있다.

(lldb) command regex -- tv 's/(.+)/expression -l objc -O -- @import QuartzCore; [%1 setHidden:!(BOOL)[%1 isHidden]]; (void)[CATransaction flush];/'
(lldb) tv [[[UIApp keyWindow] rootViewController] view]

특정 조건일 경우에만 발동시키고 싶으면 <regex> 부분을 고치면 된다. 아래는 1) 0~9까지의 숫자가 들어 오거나, 2) $로 시작하거나 3) @로 시작하거나 4) [로 시작할 때의 조건이다.

(lldb) command regex getcls 's/(([0-9]|\$|\@|\[).*)/cpo [%1 class]/'
(lldb) getcls @""
__NSCFConstantString

(lldb) getcls @[]
__NSArray0

(lldb) getcls @[@""]
__NSSingleObjectArrayI

(lldb) getcls @[@"", @3]
__NSArrayI

(lldb) getcls [UIDevice currentDevice]
UIDevice

하지만 getcls self를 치면 위 조건에 부합하지 않아서 에러가 뜬다.

(lldb) getcls self
error: getcls

이럴 경우 조건에 따라 명령어를 다르게 실행시키는 것이 가능하다.

(lldb) command regex getcls 's/(([0-9]|\$|\@|\[).*)/cpo [%1 class]/' 's/(.+)/expression -l swift -O -- type(of: %1)/'

(lldb) getcls self
Signals.MasterViewController

(lldb) getcls self .title
Swift.Optional<Swift.String>

여러가지 argument를 받을 수 있게 할 수도 있다.

(lldb) command regex swiftpersel 's/(.+)\s+(\w+)/expression -l swift -O -- %1.perform(NSSelectorFromString("%2"))/'
(lldb) swiftpersel UIApplication.shared statusBar

Chapter 11: Assembly Register Calling Convention

pushq   %rbx
subq    $0x228, %rsp
movq    %rdi, %rbx

이런 식으로 x86_64 Assembly 코드가 있을 경우 pushq, mov 같은 것들은 opcode라고 부르며, rbx, rsp 같은 것은 registers라고 부른다. %$ 기호가 있을 경우 AT&T 포맷이며 없을 경우 Intel 포맷이라 한다. 이는 Rosetta 환경에서 확 인이 가능하다.

(lldb) disassemble -m -F att

** 55  	  override func viewDidLoad() {

Registers`ViewController.viewDidLoad():
    0x104a964c0 <+0>:   pushq  %rbp
    0x104a964c1 <+1>:   movq   %rsp, %rbp
    0x104a964c4 <+4>:   pushq  %r13
    0x104a964c6 <+6>:   subq   $0x88, %rsp


(lldb) disassemble -m -F intel

** 55  	  override func viewDidLoad() {

Registers`ViewController.viewDidLoad():
    0x104a964c0 <+0>:   push   rbp
    0x104a964c1 <+1>:   mov    rbp, rsp
    0x104a964c4 <+4>:   push   r13
    0x104a964c6 <+6>:   sub    rsp, 0x88

이제 registers에 대해 알아보자면, 아래 Objective-C 코드의 경우

@implementation ViewController

- (void)logWithName:(NSString *)name old:(int)old location:(NSString *)location {
    NSLog(@"Hello world, I am %@. I'm %d, and I live in %@.", name, old, location);
}

@end
NSString *name = @"Zoltan";
[self logWithName:name old:30 location:@"my father's basement"];

arm64 registers는 아래처럼 된다.

(lldb) register read
General Purpose Registers:
        x0 = 0x0000600003bb8c00
        x1 = 0x0000000102153360  "logWithName:old:location:"
        x2 = 0x0000000102154040  @"Zoltan"
        x3 = 0x000000000000001e
        x4 = 0x0000000102154060  @"my father's basement"
# 생략

(lldb) po $x0
<ViewController: 0x600003bb8c00>

(lldb) p/d $x3
(unsigned long) $1 = 30

Objective-C에서 registers에 대해 알아 보자면, 아래와 같은 호출이 있다고 하면

[NSApplication sharedApplication];

NSString *helloWorldString = [@"Can't Sleep; " stringByAppendingString:@"Clowns will eat me"];

컴파일러는 아래처럼 처리한다.

id NSApplicationClass = [NSApplication class];
((void (*)(id, SEL))objc_msgSend)(NSApplicationClass, @selector(sharedApplication));

NSString *helloWorldString2;
helloWorldString2 = ((NSString * (*)(NSString *, SEL, NSString *))objc_msgSend)(@"Can't sleep; ", @selector(stringByAppendingString:), @"Clowns will eat me");

objc_msgSend()를 써보고 싶으면 #import <objc/message.h>를 하면 된다. 또한 type casting을 해줘야 하는 이유는 여기에 설명되어 있다.

이제 parameters들이 registers에 어떻게 작동하는지 보면

func executeLotsOfArguments(one: Int, two: Int, three: Int, four: Int, five: Int, six: Int, seven: Int, eight: Int, nine: Int, ten: Int, eleven: Int) -> Int {
  print("arguments are: \(one), \(two), \(three), \(four), \(five), \(six), \(seven), \(eight), \(nine), \(ten), \(eleven)")
  return 100
}

_ = executeLotsOfArguments(one: 1, two: 2, three: 3, four: 4, five: 5, six: 6, seven: 7, eight: 8, nine: 9, ten: 10, eleven: 11)

x86_64의 경우 RDI, RSI, RDX, RCX, R8, R9 순서인 것을 볼 수 있다. 6개가 넘어가는 나머지 parameter는 다음 Stack으로 넘어 간다고 한다.

(lldb) register read -f d
General Purpose Registers:
       rax = 4301950400  MyApp2`MyApp2.ViewController.executeLotsOfArguments(one: Swift.Int, two: Swift.Int, three: Swift.Int, four: Swift.Int, five: Swift.Int, six: Swift.Int, seven: Swift.Int, eight: Swift.Int, nine: Swift.Int, ten: Swift.Int, eleven: Swift.Int) -> Swift.Int at ViewController.swift:27
       rbx = 105553143088512
       rcx = 4
       rdx = 3
       rdi = 1
       rsi = 2
       rbp = 12968306944
       rsp = 12968306728
        r8 = 5
        r9 = 6
       r10 = 2043
       r11 = 101
       r12 = 140703316248320  libobjc.A.dylib`objc_msgSend
       r13 = 105553143088512
       r14 = 104
       r15 = 140703316248320  libobjc.A.dylib`objc_msgSend
       rip = 4301950400  MyApp2`MyApp2.ViewController.executeLotsOfArguments(one: Swift.Int, two: Swift.Int, three: Swift.Int, four: Swift.Int, five: Swift.Int, six: Swift.Int, seven: Swift.Int, eight: Swift.Int, nine: Swift.Int, ten: Swift.Int, eleven: Swift.Int) -> Swift.Int at ViewController.swift:27
    rflags = 514
        cs = 43
        fs = 0
        gs = 0

arm64의 경우 x0, x1, x2, x3, x4, x5, x6, x7, x9이며, 총 9개 까지만 하나의 Stack에 받을 수 있다. 특이한 점은 마지막 x9은 마지막 parameter라는 것이다.

2022-05-21 20:46:00.258362+0900 MyApp2[3130:93721] Hello world, I am Zoltan. I'm 30, and I lve in my fathers' basement.
(lldb) register read -f d
General Purpose Registers:
        x0 = 1
        x1 = 2
        x2 = 3
        x3 = 4
        x4 = 5
        x5 = 6
        x6 = 7
        x7 = 8
        x8 = 4334657492  MyApp2`MyApp2.ViewController.executeLotsOfArguments(one: Swift.Int, two: Swift.Int, three: Swift.Int, four: Swift.Int, five: Swift.Int, six: Swift.Int, seven: Swift.Int, eight: Swift.Int, nine: Swift.Int, ten: Swift.Int, eleven: Swift.Int) -> Swift.Int at ViewController.swift:27
        x9 = 11
       x10 = 6132230160
       x11 = 2045
       x12 = 36
       x13 = 3171026983
       x14 = 3173126144
       x15 = 66
       x16 = 6729930300  libswiftCore.dylib`swift_bridgeObjectRelease
       x17 = 1025507328
       x18 = 0
       x19 = 105553177138944
       x20 = 105553177138944
       x21 = 7744348115  
       x22 = 105553156183536
       x23 = 105553166615104
       x24 = 7986454528  (void *)0x00000001dc0780b8: NSNumber
       x25 = 7999535768  @"contentViewController"
       x26 = 7744345436  
       x27 = 7744347429  
       x28 = 7744351469  
        fp = 6132230368
        lr = 4334655412  MyApp2`MyApp2.ViewController.viewDidLoad() -> () + 584 at ViewController.swift:18:13
        sp = 6132230160
        pc = 4334657492  MyApp2`MyApp2.ViewController.executeLotsOfArguments(one: Swift.Int, two: Swift.Int, three: Swift.Int, four: Swift.Int, five: Swift.Int, six: Swift.Int, seven: Swift.Int, eight: Swift.Int, nine: Swift.Int, ten: Swift.Int, eleven: Swift.Int) -> Swift.Int at ViewController.swift:27
      cpsr = 1610616832

만약에 func executeLotsOfArguments(one:two:three:four:five:six:seven:eight:nine:ten:eleven:)의 return 값을 알고 싶을 경우, func에서 breakpoint를 걸고

(lldb) finish
arguments are: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11

# arm64
(lldb) register read
General Purpose Registers:
        x0 = 0x0000000000000064
# 생략
(lldb) register read x0 -fd
      x0 = 100

# x86_64
(lldb) register read
General Purpose Registers:
       rax = 0x0000000000000064
# 생략
(lldb) register read rax -fd
     rax = 100

이렇게 x0 또는 rax에 return 값이 있는 것을 알 수 있다.

이렇게 배운 register로 SpringBoard에 장난을 치자면, 우선 아래 명령어로 Simulator의 UUID를 가져온다.

% xcrun simctl list
-- iOS 15.5 --
    iPhone 13 Pro Max (98093B34-CB15-492E-8553-0BBCCEB8BF6C) (Shutdown)

% open /Applications/Xcode.app/Contents/Developer/Applications/Simulator.app --args -CurrentDeviceUDID 98093B34-CB15-492E-8553-0BBCCEB8BF6C

% lldb -n SpringBoard
(lldb) process attach --name "SpringBoard"

이러면 원하는 Simulator를 실행할 수 있고, SpringBoard에 attach 할 수 있다. 그리고 아래 string을 할당해주고 주소를 가져온다. autoreleasepool은 존재하지 않을 것이기에, 메모리는 계속 살아 있을 것이다.

(lldb) p/x @"Yay! Debugging!"
(__NSCFString *) $0 = 0x00006000010a57a0 @"Yay! Debugging!"

그리고 모든 UILabel의 글자를 방금 만든 string 값으로 대체해준다. arm64는 x2, x86_64는 rdx가 되어야 한다.

(lldb) breakpoint set -n "-[UILabel setText:]" -C "po $x2 = 0x00006000010a57a0" -G1
Breakpoint 1 where = UIKitCore`-[UILabel setText:], address = 0x000000018523a010

그러면 아래처럼 잘 작동하는 것을 확인할 수 있다.

Chapter 12: Assembly & Memory

AT&T Assembly의 경우 대략적으로 아래 형태인데

movq  $ox78,  %rax

이는 각각 opcode, source, destination이라 할 수 있다. 0x78이라는 hex 값을 RAX register에 옮긴다는 것이다.

또한 ~/.lldbinit 파일에 아래와 같은 setting들을 추가할 수 있다. prologue/epilogue에 대한 설명은 여기에서 잘 되어 있는듯…

settings set target.x86-disassembly-flavor intel
settings set target.skip-prologue false

이렇게 Intel로 설정한다면, 아래처럼 더 깔끔하게 보인다.

mov rax,  0x78

아래를 등록하면 cpx 명령어도 만들 수 있다. Objective-C context에서 명령을 실행하고 hex값을 받아 올 수 있다. (참고로 마지막에 띄어쓰기 있다.)

command alias -H "Print value in ObjC context in hexadecimal" -h "Print in hex" -- cpx expression -f x -l objc -- 

이제 rip, pc register에 대해 알아보자면, NSApplicationDelegate에 아래처럼 짜고 aBadMethod()에서 breakpoint를 걸어준다.

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
  func applicationWillBecomeActive(_ notification: Notification) {
    print("\(#function)")
    self.aBadMethod()
  }
  
  func aBadMethod() {
    print("\(#function)")
  }
  
  func aGoodMethod() {
    print("\(#function)")
  }
}

breapoint가 걸렸을 때 register들을 보면

# x86_64 (Rosetta)
(lldb) register read
General Purpose Registers:
       rax = 0x00000001040c5610  Registers`Registers.AppDelegate.aBadMethod() -> () at AppDelegate.swift:38
       rbx = 0x000000030c38e4f8
       rcx = 0x00007ffffffffff8
       rdx = 0x00000000c6a1809d
       rdi = 0x00000000000000d4
       rsi = 0x0000600001762700
       rbp = 0x000000030c38e420
       rsp = 0x000000030c38e338
        r8 = 0x0000000000002700
        r9 = 0x0000000000000040
       r10 = 0x00000000000007fb
       r11 = 0x00000000000000ff
       r12 = 0x0000000000001400
       r13 = 0x00006000000109e0
       r14 = 0x0000000000000000
       r15 = 0x0000000000000000
       rip = 0x00000001040c5610  Registers`Registers.AppDelegate.aBadMethod() -> () at AppDelegate.swift:38
    rflags = 0x0000000000000206
        cs = 0x000000000000002b
        fs = 0x0000000000000000
        gs = 0x0000000000000000

# arm64
(lldb) register read
General Purpose Registers:
        x0 = 0x000000012c5070e0
        x1 = 0x00000001cc52b43
        x2 = 0x000000012c5070e0
        x3 = 0x0000600000dd0d80
        x4 = 0x0000000000000018
        x5 = 0x00000001cc52b43e
        x6 = 0x0000000000006690
        x7 = 0x0000000000000002
        x8 = 0x0300000104cc9209 (0x0000000104cc9209) (void *)0x380000000104cc95
        x9 = 0x0100000000000000
       x10 = 0x0000000000000000
       x11 = 0x000000000000000f
       x12 = 0x000000000000000e
       x13 = 0x00006000031cc0d0
       x14 = 0x0000000000000000
       x15 = 0x0000000104cc9208  (void *)0x0000000104cc9538: _TtC6MyApp413SceneDelegate
       x16 = 0x000000010503bf30  libobjc.A.dylib`objc_retain
       x17 = 0x0000000104cc1a8c  MyApp4`@objc MyApp4.SceneDelegate.sceneWillEnterForeground(__C.UIScene) -> () at <compiler-generated>
       x18 = 0x0000000000000000
       x19 = 0x0000600000dd1148
       x20 = 0x0000600000dd0d80
       x21 = 0x00000001cc52b43e
       x22 = 0x000000012c5070e0
       x23 = 0x0000600000dd0d80
       x24 = 0x000000012c5070e0
       x25 = 0x00000001cc52c044
       x26 = 0x00006000031e8100
       x27 = 0x0000000000000001
       x28 = 0x000000012c5070e0
        fp = 0x000000016b13bc30
        lr = 0x0000000104cc1ac0  MyApp4`@objc MyApp4.SceneDelegate.sceneWillEnterForeground(__C.UIScene) -> () + 52 at <compiler-generated>
        sp = 0x000000016b13bc10
        pc = 0x0000000104cc1a5c  MyApp4`MyApp4.SceneDelegate.sceneWillEnterForeground(__C.UIScene) -> () at SceneDelegate.swift:14
      cpsr = 0x00001000

현재 method의 register는 rip 또는 pc에 할당된 것을 볼 수 있다.

# x86_64 (Rosetta)
(lldb) cpx $rip
(unsigned long) $0 = 0x00000001040c5610

# arm64
(lldb) cpx $pc
(unsigned long) $0 = 0x0000000104cc1a5c

여기서 나는 lldb 상에서 aBadMethod() 대신에 aGoodMethod()가 불리도록 설정하고 싶어서, lldb로 aGoodMethod의 주소를 알아내기 위해 아래처럼 하면

(lldb) image lookup -vrn ^Registers.*aGoodMethod
1 match found in /Users/pookjw/Library/Developer/Xcode/DerivedData/Registers-adpzdnequcnjmhaupcjnjugkrbto/Build/Products/Debug/Registers.app/Contents/MacOS/Registers:
        Address: Registers[0x0000000100008810] (Registers.__TEXT.__text + 20512)
        Summary: Registers`Registers.AppDelegate.aGoodMethod() -> () at AppDelegate.swift:42
         Module: file = "/Users/pookjw/Library/Developer/Xcode/DerivedData/Registers-adpzdnequcnjmhaupcjnjugkrbto/Build/Products/Debug/Registers.app/Contents/MacOS/Registers", arch = "x86_64"
    CompileUnit: id = {0x00000000}, file = "/Users/pookjw/Downloads/dbg-materials-editions-3.0/12. Assembly & Memory/projects/starter/Registers/Registers/AppDelegate.swift", language = "swift"
       Function: id = {0x4000000fd}, name = "Registers.AppDelegate.aGoodMethod() -> ()", mangled = "$s9Registers11AppDelegateC11aGoodMethodyyF", range = [0x00000001040c5810-0x00000001040c5a09)
       FuncType: id = {0x4000000fd}, byte-size = 8, decl = AppDelegate.swift:42, compiler_type = "() -> ()
"
         Blocks: id = {0x4000000fd}, range = [0x1040c5810-0x1040c5a09)
      LineEntry: [0x00000001040c5810-0x00000001040c582c): /Users/pookjw/Downloads/dbg-materials-editions-3.0/12. Assembly & Memory/projects/starter/Registers/Registers/AppDelegate.swift:42
         Symbol: id = {0x00000362}, range = [0x00000001040c5810-0x00000001040c5a10), name="Registers.AppDelegate.aGoodMethod() -> ()", mangled="$s9Registers11AppDelegateC11aGoodMethodyyF"
       Variable: id = {0x40000011a}, name = "self", type = "Registers.AppDelegate", location = DW_OP_fbreg -16, decl = AppDelegate.swift:42

0x00000001040c5810인 것을 알 수 있다. 이걸 현재 rip 또는 pc에 써주면 된다.

# x86_64 (Rosetta)
(lldb) register write rip 0x00000001040c5810

# arm64
(lldb) register write pc 0x00000001040c5810

여기서 중요한 점은 lldb에서 continue 명령어를 치면 안 되고, Xcode에서 직접 버튼으로 continue를 해야 한다. 버그인듯? 암튼 그러면 aBadMethod()가 안 불리고 aGoodMethod()가 불리는 것을 확인할 수 있다.

x86에서의 8비트~64비트 registers는 아래와 같다.

그러면 아래처럼 64비트로 정의된 hex 값을 낮은 비트에서 아래처럼 볼 수 있다. 여기서 dl, dhdx를 쪼갰을 때 low와 high를 뜻한다고 한다.

(lldb) register write rdx 0x123456789ABCDEF
(lldb) p/x $rdx
(unsigned long) $0 = 0x0123456789abcdef
(lldb) p/x $edx
(unsigned int) $1 = 0x89abcdef
(lldb) p/x $dx
(unsigned short) $2 = 0xcdef
(lldb) p/x $dl
(unsigned char) $3 = 0xef
(lldb) p/x $dh
(unsigned char) $4 = 0xcd

이제 메모리에 대해 알아보자면, method에서 breakpoint를 찍을 경우

(lldb) cpx $rip
(unsigned long) $0 = 0x000000010454f610

이렇게 현재 rip의 주소를 가져오는게 있다. 그리고 아래 명령어를 통해 memory에 로드된 assembly 코드를 볼 수 있다. -fi는 instruction 포맷으로 보겠다는 것이고, -c1은 1개만 보겠다는건데 이건 나중에 자세히 설명하겠다.

(lldb) memory read -fi -c1 0x000000010454f610
->  0x10454f610: 55  push   rbp

이는 disassemble 명령어로도 볼 수 있다. -b가 붙으면 opcode (예시는 opcode가 55이다)도 같이 볼 수 있다.

(lldb) disassemble -b
Registers`AppDelegate.aBadMethod():
->  0x10454f610 <+0>:   55                       push   rbp

만약에 opcode만 알고 있을 경우 assembly를 알고 싶으면 아래처럼 하면 된다.

(lldb) expression -f i -l objc -- 0x55
(int) $5 = 55  push   rbp

# 이미 Objective-C context일 경우
(lldb) p/i 0x55
(int) $6 = 55  push   rbp

이제 다른 장난을 쳐보자면, -c4로 보면 더 많은 assembly 코드를 볼 수 있는데

(lldb) memory read -fi -c4 $rip
->  0x7ff819137f6e: 4c 89 ef           mov    rdi, r13
    0x7ff819137f71: 48 8b 75 d0        mov    rsi, qword ptr [rbp - 0x30]
    0x7ff819137f75: ff 15 95 52 4d 3d  call   qword ptr [rip + 0x3d4d5295] ; (void *)0x00007ff8165a4700: objc_msgSend
    0x7ff819137f7b: e9 6e ff ff ff     jmp    0x7ff819137eee            ; <+495>

0x7ff819137f71를 보면 48 8b 75 d0라는 opcode를 가지고 있다. 이 opcode로 assembly를 보려고 하면

# 예시처럼 아예 안 뜨거나 엉뚱한 코드가 뜬다.
(lldb) p/i 0x488b75d0
(int) $8 =

(lldb) p/i 0xd0758b48
(unsigned int) $9 = 48 8b 75 d0  mov    rsi, qword ptr [rbp - 0x30]

위처럼 배열 순서를 바꿔줘야 원하는 값이 나오는 것을 알 수 있다. 이것을 endianness라고 부른다. 이걸 memory 명령어에서 간단하게 해결하고 싶을 경우

(lldb) memory read -s4 -c1 -fx 0x7ff819137f71
0x7ff819137f71: 0xd0758b48

(lldb) p/i 0xd0758b48
(unsigned int) $10 = 48 8b 75 d0  mov    rsi, qword ptr [rbp - 0x30]

이렇게 해결된 것을 볼 수 있다.

Chapter 13: Assembly & the Stack

Stack은 사진처럼 높은 주소에서 낮은 주소로 내려가는 방식이며, 이 사이즈는 커널에서 유한한 값으로 크기를 정해준다. 만약 이 크기를 넘길 경우 stack overflow가 일어나게 된다.

x86_64

RSPRBP

RSPRBP 값의 설정은 function prologue에서 일어난다. Xcode나 lldb 상에서 현재 frame을 바꿀 경우, RSPRBP의 값이 바뀌는 것을 볼 수 있다.

push opcode

pushRSP의 pointer 값을 낮춘다. 64비트 architecture의 경우 보통 8 bytes 만큼의 pointer 값을 낮춘다고 한다. 다시 말하자면 RSP는 pointer이며, pushRSP pointer에 새로운 값을 할당해 주기도 한다. 예를 들어 push 0x5일 경우 pseudocode는 아래처럼 된다.

RSP = RSP - 0x8
*RSP = 0x5

pop opcode

poppush의 반대 역할을 한다. RSP의 값을 가져오고 pointer 값을 올려준다. 예를 들어 pop rdx일 경우 pseudocode는 아래처럼 된다.

RDX = *RSP // 0x5
RSP = RSP + 0x8

call opcode

call은 함수 호출을 해준다. push를 통해 함수 호출이 끝날 경우 어디로 가야 할지 알려주는 구조다. 만약에 0x7fffb34df410이라는 함수가 있을 경우,

0x7fffb34de913 <+227>: call   0x7fffb34df410            
0x7fffb34de918 <+232>: mov    edx, eax

여기서 call이 불리면 아래처럼 된다.

mov   rip, 0x7fffb34de918 // RIP = 0x7fffb34de918 -> 함수 호출이 끝날 경우 돌아 올 경로 지정

// 함수 시작

// Prologue
push  rip                 // RSP = RSP - 0x8; *RSP = RIP  -> RIP의 값을 백업
mov   rip, rsp            // RIP = RSP

// 함수 내용
/* */

// Epilogue
pop   rsp                 // RDI = *RSP; RSP = RSP + 0x8 -> 백업한 RIP 값을 되돌림
ret                       // RDI (0x7fffb34de918)으로 이동함

잘 이해가 안 간다면 이 사이트에서 아래처럼 코드 작성하고 x86_64 gcc 12.1로 설정하고 아래 코드를 입력하면

int hello() { return 3; }

int walk(int i) { return i + hello(); };

이렇게 Assembly 코드가 나온다. 여기서는 rbprip과 같은 역할을 하는듯? 이걸 보면 이해가 될지도.

hello:
        push    rbp
        mov     rbp, rsp
        mov     eax, 3
        pop     rbp
        ret
walk:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 8
        mov     DWORD PTR [rbp-4], edi
        mov     eax, 0
        call    hello
        mov     edx, DWORD PTR [rbp-4]
        add     eax, edx
        leave
        ret

hello를 보면 알다시피, poppush는 서로 상응해야 한다. 그렇지 않을 경우 ret이 불릴 때 엉뚱한 곳으로 가버릴 것이다.

RBPRSP 실습

이제 위에서 배운 이론이 정말 뜻대로 작동하는지 보자. RSP, RDP, RDI, RDX의 값을 쉽게 보기 위해 아래와 같은 alias를 만들어준다.

(lldb) command alias dumpreg register read rsp rbp rdi rdx

아래처럼 생긴 코드가 있다고 가정하자.

#ifndef StackWalkthrough_h
#define StackWalkthrough_h

void StackWalkthrough(int x);

#endif /* StackWalkthrough_h */
.globl _StackWalkthrough


_StackWalkthrough:
      push  %rbp
      movq  %rsp, %rbp
      movq  $0x0, %rdx
      movq  %rdi, %rdx
      push  %rdx
      movq  $0x0, %rdx
      pop   %rdx
      pop   %rbp
      ret

첫번째 줄부터 breakpoint를 걸어보고 StackWalkthrough(5)로 실행해보면

push   rbp        <- paused
mov    rbp, rsp
mov    rdx, 0x0
mov    rdx, rdi
push   rdx
mov    rdx, 0x0
pop    rdx
pop    rbp
ret 

dumpreg, x/gx $rsp, p/x $rsp 치면 아래처럼 된다. 참고로 p/x는 그 자체의 값만을 출력하는데, 만약에 pointer일 경우 x/gx로 통해 pointer의 값을 알려준다. C에서 void *p가 있을 경우 p를 그대로 출력할지, 아니면 *p를 출력할지의 차이라고 생각하면 된다. x/gxmemory read의 alias라고 한다.

(lldb) dumpreg
     rsp = 0x0000000304aa9368
     rbp = 0x0000000304aa9390
     rdi = 0x0000000000000005
     rdx = 0x000000000000001c

(lldb) x/gx $rsp
0x304aa9368: 0x000000010046fefc

(lldb) p/x $rsp
(unsigned long) $1 = 0x0000000304aa9368

si (step-inst)를 실행하면 다음 instruction으로 이동하게 되는데

push   rbp
mov    rbp, rsp <- paused
mov    rdx, 0x0
mov    rdx, rdi
push   rdx
mov    rdx, 0x0
pop    rdx
pop    rbp
ret 
(lldb) dumpreg
     rsp = 0x0000000304aa9360
     rbp = 0x0000000304aa9390
     rdi = 0x0000000000000005
     rdx = 0x000000000000001c

(lldb) x/gx $rsp
0x304aa9360: 0x0000000304aa9390

이렇게 push rbp가 잘 된 것을 볼 수 있다. 또 si를 해보면

push   rbp
mov    rbp, rsp
mov    rdx, 0x0 <- paused
mov    rdx, rdi
push   rdx
mov    rdx, 0x0
pop    rdx
pop    rbp
ret 
(lldb) p/x $rbp
(unsigned long) $10 = 0x0000000304aa9360

(lldb) p (BOOL)($rbp == $rsp)
(BOOL) $11 = YES
push   rbp
mov    rbp, rsp
mov    rdx, 0x0
mov    rdx, rdi <- paused
push   rdx
mov    rdx, 0x0
pop    rdx
pop    rbp
ret 
(lldb) p/x $rdx
(unsigned long) $12 = 0x0000000000000000
push   rbp
mov    rbp, rsp
mov    rdx, 0x0
mov    rdx, rdi
push   rdx      <- paused
mov    rdx, 0x0
pop    rdx
pop    rbp
ret 
(lldb) p/x $rdx
(unsigned long) $13 = 0x0000000000000005 // parameter에 5가 들어 왔으므로 rdi는 5이며, 이를 rdx에 할당해준 것.
push   rbp
mov    rbp, rsp
mov    rdx, 0x0
mov    rdx, rdi
push   rdx
mov    rdx, 0x0 <- paused
pop    rdx
pop    rbp
ret 
(lldb) p/x $rsp
(unsigned long) $15 = 0x0000000304aa9358

(lldb) x/gx $rsp
0x304aa9358: 0x0000000000000005

여기서는 rsp가 0x2 만큼 감소한 것을 알 수 있고, rsp의 값이 rdx의 주소값이 된 것을 알 수 있다.

push   rbp
mov    rbp, rsp
mov    rdx, 0x0
mov    rdx, rdi
push   rdx
mov    rdx, 0x0
pop    rdx      <- paused
pop    rbp
ret 
(lldb) dumpreg
     rsp = 0x0000000304aa9358
     rbp = 0x0000000304aa9360
     rdi = 0x0000000000000005
     rdx = 0x0000000000000000
push   rbp
mov    rbp, rsp
mov    rdx, 0x0
mov    rdx, rdi
push   rdx
mov    rdx, 0x0
pop    rdx
pop    rbp      <- paused
ret
(lldb) dumpreg
     rsp = 0x0000000304aa9360
     rbp = 0x0000000304aa9360
     rdi = 0x0000000000000005
     rdx = 0x0000000000000005
push   rbp
mov    rbp, rsp
mov    rdx, 0x0
mov    rdx, rdi
push   rdx
mov    rdx, 0x0
pop    rdx
pop    rbp
ret             <- paused
(lldb) dumpreg
     rsp = 0x0000000304aa9368
     rbp = 0x0000000304aa9390
     rdi = 0x0000000000000005
     rdx = 0x0000000000000005

이런 식으로 poppush가 상응했기 때문에 rsp, rbp가 처음 주소와 똑같은 값을 지닌 것을 알 수 있다. 또한 rdx에 return할 값이 있으므로, 계속 진행되는 코드에서 rdx를 받아서 처리할 것이다.

여러 개의 Parameter가 들어 올 경우

아래처럼 여러 개의 Parameter가 들어 오는 함수가 있다고 가정하자.

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    [self executeLotsOfArgumentsWithOne:1 two:2 three:3 four:4 five:5 six:6 seven:7 eight:8 nine:9 ten:10];
}

- (NSString *)executeLotsOfArgumentsWithOne:(NSUInteger)one two:(NSUInteger)two three:(NSUInteger)three four:(NSUInteger)four five:(NSUInteger)five six:(NSUInteger)six seven:(NSUInteger)seven eight:(NSUInteger)eight nine:(NSUInteger)nine ten:(NSUInteger)ten {
    return @"Mom, what happened to the cat?";
}

@end

그리고 - (NSString *)에 breakpoint를 걸면 아래처럼 보인다. 내가 주석넣은 부분들이 각 parameter의 위치를 말한다. 보면 6개까지는 rbp의 아래쪽에 할당되는데, 7개로 넘어가면 rbp의 위쪽(바깥쪽)에 할당된다. 아마 컴파일러가 frame의 크기를 더 크게 잡아주고, 윗부분에 공간을 만들어서 그 부분에 초과되는 parameter를 담는 것이라 추측된다.

MyApp3`-[ViewController executeLotsOfArgumentsWithOne:two:three:four:five:six:seven:eight:nine:ten:]:
->  0x104f38170 <+0>:  push   rbp
    0x104f38171 <+1>:  mov    rbp, rsp
    0x104f38174 <+4>:  mov    rax, qword ptr [rbp + 0x38]   // ten
    0x104f38178 <+8>:  mov    rax, qword ptr [rbp + 0x30]   // nine
    0x104f3817c <+12>: mov    rax, qword ptr [rbp + 0x28]   // eight
    0x104f38180 <+16>: mov    rax, qword ptr [rbp + 0x20]   // seven
    0x104f38184 <+20>: mov    rax, qword ptr [rbp + 0x18]   // six
    0x104f38188 <+24>: mov    rax, qword ptr [rbp + 0x10]   // five
    0x104f3818c <+28>: mov    qword ptr [rbp - 0x8], rdi    // self
    0x104f38190 <+32>: mov    qword ptr [rbp - 0x10], rsi   // Selector
    0x104f38194 <+36>: mov    qword ptr [rbp - 0x18], rdx   // one
    0x104f38198 <+40>: mov    qword ptr [rbp - 0x20], rcx   // two
    0x104f3819c <+44>: mov    qword ptr [rbp - 0x28], r8    // three
    0x104f381a0 <+48>: mov    qword ptr [rbp - 0x30], r9    // four
    0x104f381a4 <+52>: lea    rdi, [rip + 0xe65]        ; @"Mom, what happened to the cat?"
    0x104f381ab <+59>: pop    rbp
    0x104f381ac <+60>: jmp    0x104f382dc               ; symbol stub for: objc_retainAutoreleaseReturnValue

Debugging Info

그러면 난 각 parameter의 위치를 어떻게 알았을까?를 설명하자면, 일단 Xcode를 보면 아직 push rbp에 코드가 멈춰 있어서 값 할당이 되지 않아 Xcode에서는 아래처럼 쓰레기값들이 보인다.

모든 symbol을 dump 해보자. 참고로 symfile은 ‘Dump the debug symbol file for one or more target modules.’이라고 한다. 그리고 "one" 이렇게 검색하면 아래처럼 우리가 원하는 정보가 나오는데,

(lldb) image dump symfile MyApp3

0x2cdd125e8:     Variable{0x1000000c3}, name = "self", type = {000000010000018a} 0x000060001C02DEB8 (ViewController *const), scope = parameter, location = DW_OP_fbreg -8, artificial
0x2cdd10dd8:     Variable{0x1000000cf}, name = "_cmd", type = {0000000100000194} 0x000060000078B538 (SEL), scope = parameter, location = DW_OP_fbreg -16, artificial
0x2cdd0fee8:     Variable{0x1000000db}, name = "one", type = {00000001000001bd} 0x000060002001F178 (NSUInteger), scope = parameter, decl = ViewController.m:18, location = DW_OP_fbreg -24
0x2cdd50d58:     Variable{0x1000000e9}, name = "two", type = {00000001000001bd} 0x000060002001F178 (NSUInteger), scope = parameter, decl = ViewController.m:18, location = DW_OP_fbreg -32
0x2bdb31078:     Variable{0x1000000f7}, name = "three", type = {00000001000001bd} 0x000060002001F178 (NSUInteger), scope = parameter, decl = ViewController.m:18, location = DW_OP_fbreg -40
0x2cdd13c78:     Variable{0x100000105}, name = "four", type = {00000001000001bd} 0x000060002001F178 (NSUInteger), scope = parameter, decl = ViewController.m:18, location = DW_OP_fbreg -48
0x2cdd13d98:     Variable{0x100000113}, name = "five", type = {00000001000001bd} 0x000060002001F178 (NSUInteger), scope = parameter, decl = ViewController.m:18, location = DW_OP_fbreg +16
0x2cdd13eb8:     Variable{0x100000121}, name = "six", type = {00000001000001bd} 0x000060002001F178 (NSUInteger), scope = parameter, decl = ViewController.m:18, location = DW_OP_fbreg +24
0x2cdd13fd8:     Variable{0x10000012f}, name = "seven", type = {00000001000001bd} 0x000060002001F178 (NSUInteger), scope = parameter, decl = ViewController.m:18, location = DW_OP_fbreg +32
0x2cdd140f8:     Variable{0x10000013d}, name = "eight", type = {00000001000001bd} 0x000060002001F178 (NSUInteger), scope = parameter, decl = ViewController.m:18, location = DW_OP_fbreg +40
0x2cdd14218:     Variable{0x10000014b}, name = "nine", type = {00000001000001bd} 0x000060002001F178 (NSUInteger), scope = parameter, decl = ViewController.m:18, location = DW_OP_fbreg +48
0x2cdd14338:     Variable{0x100000159}, name = "ten", type = {00000001000001bd} 0x000060002001F178 (NSUInteger), scope = parameter, decl = ViewController.m:18, location = DW_OP_fbreg +56

DW_OP_fbreg -48 이것을 보면 rbp에서의 offset을 말하는 것이다. 이런 식으로 내가 알아냈던 것이다. 이제 lea까지 이동해보면

MyApp3`-[ViewController executeLotsOfArgumentsWithOne:two:three:four:five:six:seven:eight:nine:ten:]:
    0x1046c6170 <+0>:  push   rbp
    0x1046c6171 <+1>:  mov    rbp, rsp
    0x1046c6174 <+4>:  mov    rax, qword ptr [rbp + 0x38]
    0x1046c6178 <+8>:  mov    rax, qword ptr [rbp + 0x30]
    0x1046c617c <+12>: mov    rax, qword ptr [rbp + 0x28]
    0x1046c6180 <+16>: mov    rax, qword ptr [rbp + 0x20]
    0x1046c6184 <+20>: mov    rax, qword ptr [rbp + 0x18]
    0x1046c6188 <+24>: mov    rax, qword ptr [rbp + 0x10]
    0x1046c618c <+28>: mov    qword ptr [rbp - 0x8], rdi
    0x1046c6190 <+32>: mov    qword ptr [rbp - 0x10], rsi
    0x1046c6194 <+36>: mov    qword ptr [rbp - 0x18], rdx
    0x1046c6198 <+40>: mov    qword ptr [rbp - 0x20], rcx
    0x1046c619c <+44>: mov    qword ptr [rbp - 0x28], r8
    0x1046c61a0 <+48>: mov    qword ptr [rbp - 0x30], r9
->  0x1046c61a4 <+52>: lea    rdi, [rip + 0xe65]        ; @"Mom, what happened to the cat?"
    0x1046c61ab <+59>: pop    rbp
    0x1046c61ac <+60>: jmp    0x1046c62dc               ; symbol stub for: objc_retainAutoreleaseReturnValue

Xcode에서 값이 정상적으로 나오는 것을 확인할 수 있고, 아래처럼 lldb 상에서도 값을 확인할 수 있다.

(lldb) x/gx '$rbp - 0x18'
0x30cf4c8e8: 0x0000000000000001

(lldb) p/x $rbp - 0x18
(unsigned long) $3 = 0x000000030cf4c8e8

(lldb) p/d $rbp - 0x18 # 사실 이건 필요 없다. 메모리 주소값을 10진수로 변환시킨 것 뿐이니까.
(unsigned long) $4 = 13102270696

참고로 qword ptr은 Assembly에서 pointer의 값이라고 한다. mov qword ptr [rbp - 0x8], rdi의 경우 rbp - 0x8의 값을 rdi에 할당해준 셈이다.

Chapter 14: Hello, Ptrace

System call은 커널에서 제공하는 low-level 서비스다. 아래 명령어로 그 목록을 볼 수 있다. -l은 list이며, -n argument를 지정할 수 있다고 한다.

# 총 개수
% sudo dtrace -ln 'syscall:::entry' | wc -l
    533

% sudo dtrace -ln 'syscall:::entry'
   ID   PROVIDER            MODULE                          FUNCTION NAME
  164    syscall                                             syscall entry
  166    syscall                                                exit entry
  168    syscall                                                fork entry
  170    syscall                                                read entry
# 생략...

-l이 없으면 조건문이 해당될 때마다 명령을 실행한다. syscall, ptrace, entry 조건일 경우 printf가 실행된다.

% sudo dtrace -n 'syscall::ptrace:entry { printf("%s(%d, %d, %d, %d) from %s\n", probefunc, arg0, arg1, arg2, arg3, execname); }'
dtrace: description 'syscall::ptrace:entry ' matched 1 probe
CPU     ID                    FUNCTION:NAME
  0    216                     ptrace:entry ptrace(8, 1489, 0, 0) from debugserver

  0    216                     ptrace:entry ptrace(13, 1489, 6147, 0) from debugserver

깔끔하게 보고 싶을 경우 (명령에 대한 출력만 보고 싶을 경우) -q를 쓰면 된다.

% sudo dtrace -qn 'syscall::ptrace:entry { printf("%s(%d, %d, %d, %d) from %s\n", probefunc, arg0, arg1, arg2, arg3, execname); }'
ptrace(14, 1460, 0, 0) from debugserver
ptrace(13, 1460, 6403, 0) from debugserver

만약에 Finder에 attach를 시도할 경우 위처럼 로그가 찍히는데

% lldb -n Finder
(lldb) process attach --name "Finder"
Process 664 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
    frame #0: 0x00000001a6ea2890 libsystem_kernel.dylib` mach_msg_trap  + 8
libsystem_kernel.dylib`mach_msg_trap:
->  0x1a6ea2890 <+8>: ret    
libsystem_kernel.dylib'mach_msg_overwrite_trap:    0x1a6ea2894 <+0>: mov    x16, #-0x20
    0x1a6ea2898 <+4>: svc    #0x80
    0x1a6ea289c <+8>: ret    
libsystem_kernel.dylib'semaphore_signal_trap:    0x1a6ea28a0 <+0>: mov    x16, #-0x21
    0x1a6ea28a4 <+4>: svc    #0x80
    0x1a6ea28a8 <+8>: ret    
libsystem_kernel.dylib'semaphore_signal_all_trap:    0x1a6ea28ac <+0>: mov    x16, #-0x22
Target 0: (Finder) stopped.
Executable module set to "/System/Library/CoreServices/Finder.app/Contents/MacOS/Finder".
Architecture set to: arm64e-apple-macosx-.
(lldb)

이때 debugserver라는 프로세스가 생성되고 ptrace를 호출해서 Finder에 attach한다. 그러면 이 debugserver는 어떻게 생성된 프로세스일까?를 알아보면

% pgrep debugserver
1624

# PPID는 parent process id인듯?
% ps -fp `pgrep -x debugserver`
  UID   PID  PPID   C STIME   TTY           TIME CMD
  501  1624  1620   0 10:19PM ttys007    0:00.12 /Applications/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Resources/debugserver --fd=5 --native-regs --setsid
  
# 이런 식으로 PPID 알 수도 있고
% ps -o ppid=$(pgrep -x debugserver)
 1620
  
# PPID가 뭔지 보려면
% ps -a 1620
  PID TTY           TIME CMD
 1620 ttys007    0:04.90 /Applications/Xcode.app/Contents/Developer/usr/bin/lldb -n Finder

Finder에서 ptrace를 호출해서 debugserver가 실행되고 lldb와 통신하는 구조다.

이제 Xcode에서 아래와 같은 스크립트를 생성하고 실행하면

while true {
    sleep(2)
    print("helloptrace")
}

아까 dtrace 명령어에서 아래와 같이 나온다.

ptrace(14, 2028, 0, 0) from debugserver
ptrace(13, 2028, 6147, 0) from debugserver

ptrace가 뭔지 알기 위해 Xcode에서 ⌘ + Shift + O를 누르고 /usr/include/sys/ptrace.h로 이동하면 아래와 같이 나온다.

enum {
    ePtAttachDeprecated __deprecated_enum_msg("PT_ATTACH is deprecated. See PT_ATTACHEXC") = 10
};

#define PT_TRACE_ME     0       /* child declares it's being traced */
#define PT_READ_I       1       /* read word in child's I space */
#define PT_READ_D       2       /* read word in child's D space */
#define PT_READ_U       3       /* read word in child's user structure */
#define PT_WRITE_I      4       /* write word in child's I space */
#define PT_WRITE_D      5       /* write word in child's D space */
#define PT_WRITE_U      6       /* write word in child's user structure */
#define PT_CONTINUE     7       /* continue the child */
#define PT_KILL         8       /* kill the child process */
#define PT_STEP         9       /* single step the child */
#define PT_ATTACH       ePtAttachDeprecated     /* trace some running process */
#define PT_DETACH       11      /* stop tracing a process */
#define PT_SIGEXC       12      /* signals as exceptions for current_proc */
#define PT_THUPDATE     13      /* signal for thread# */
#define PT_ATTACHEXC    14      /* attach to running process with signal exception */

#define PT_FORCEQUOTA   30      /* Enforce quota for root */
#define PT_DENY_ATTACH  31

#define PT_FIRSTMACH    32      /* for machine-specific requests */

__BEGIN_DECLS


int     ptrace(int _request, pid_t _pid, caddr_t _addr, int _data);


__END_DECLS

man ptrace로 보면 더 자세한 설명을 볼 수 있다. 다만 애플은 이 부분을 자세히 공개하지 않는 편이라 몇개만 나와 있다.

     PT_TRACE_ME   This request is one of two used by the traced process; it declares that
                   the process expects to be traced by its parent.  All the other
                   arguments are ignored.  (If the parent process does not expect to trace
                   the child, it will probably be rather confused by the results; once the
                   traced process stops, it cannot be made to continue except via
                   ptrace().) When a process has used this request and calls execve(2) or
                   any of the routines built on it (such as execv(3)), it will stop before
                   executing the first instruction of the new image.  Also, any setuid or
                   setgid bits on the executable being executed will be ignored.

     PT_DENY_ATTACH
                   This request is the other operation used by the traced process; it
                   allows a process that is not currently being traced to deny future
                   traces by its parent.  All other arguments are ignored.  If the process
                   is currently being traced, it will exit with the exit status of
                   ENOTSUP; otherwise, it sets a flag that denies future traces.  An
                   attempt by the parent to trace a process which has set this flag will
                   result in a segmentation violation in the parent.

     PT_CONTINUE   The traced process continues execution.  addr is an address specifying
                   the place where execution is to be resumed (a new value for the program
                   counter), or (caddr_t)1 to indicate that execution is to pick up where
                   it left off.  data provides a signal number to be delivered to the
                   traced process as it resumes execution, or 0 if no signal is to be
                   sent.

     PT_STEP       The traced process continues execution for a single step.  The
                   parameters are identical to those passed to PT_CONTINUE.

     PT_KILL       The traced process terminates, as if PT_CONTINUE had been used with
                   SIGKILL given as the signal to be delivered.

     PT_ATTACH     This call has been replaced with PT_ATTACHEXC.

     PT_ATTACHEXC  This request allows a process to gain control of an otherwise unrelated
                   process and begin tracing it.  It does not need any cooperation from
                   the to-be-traced process.  In this case, pid specifies the process ID
                   of the to-be-traced process, and the other two arguments are ignored.
                   This request requires that the target process must have the same real
                   UID as the tracing process, and that it must not be executing a setuid
                   or setgid executable.  (If the tracing process is running as root,
                   these restrictions do not apply.)  The tracing process will see the
                   newly-traced process stop and may then control it as if it had been
                   traced all along. Note that this call differs from the prior call (
                   PT_ATTACH) in that signals from the child are delivered to the parent
                   as Mach exceptions (see EXC_SOFT_SIGNAL).

     PT_DETACH     This request is like PT_CONTINUE, except that it does not allow
                   specifying an alternate place to continue execution, and after it
                   succeeds, the traced process is no longer traced and continues
                   execution normally.

첫번째로 ptrace(14, 2028, 0, 0)를 보면, _request는 14번, 즉 PT_ATTACHEXC이 불린 것을 확인할 수 있다. 또한 설명에 의하면 PT_ATTACHEXC는 나머지 두 개의 argument는 무시된다고 한다. (the other two arguments are ignored)

두번째로 ptrace(13, 2028, 6147, 0)는 13번, PT_THUPDATE인데 signal for thread#라고만 나와 있고 자세한 정보는 안 나와 있다.

attach 거부하기

이 방법은 Revserse Enginering을 막기 위해 쓰인다. lldb로 attach를 시도해서 프로그램의 내부 구조를 알아내는 것을 방지하기 위함이다.

ptrace

ptrace에는 PT_DENY_ATTACH라는 request가 존재한다. 다른 argument는 필요하지 않으며 만약 tracing되고 있을 경우 exit(ENOTSUP)이 불린다고 한다.

#import <sys/ptrace.h>
ptrace(PT_DENY_ATTACH, 0, nil, 0)

이렇게 코드를 삽입하고 Xcode에서 Run을 하면 lldb로 자동으로 attach가 되면서 실행되는데, 위 코드때문에 ENOTSUP (45) 값이 반환되면서 강제종료된다.

Program ended with exit code: 45

여기서 생각을 해보면 어딘가에서 ptrace가 불려야지만 tracing을 막을 수 있다. 즉, ptrace가 불리기 전에 ptrace에 breakpoint를 찍고 함수 실행을 무력화시키면 tracing 방어 코드를 무력화시킬 수 있다.

이를 하기 위해 -w 옵션을 쓰자.

% sudo lldb -n "helloptrace" -w
(lldb) process attach --name "helloptrace" --waitfor

그리고 프로그램을 실행하면 lldb에서 아래처럼 뜬다.

Process 2216 stopped
* thread #1, stop reason = signal SIGSTOP
    frame #0: 0x0000000100018560 dyld` _dyld_start 
dyld`_dyld_start:
->  0x100018560 <+0>:  mov    x0, sp
    0x100018564 <+4>:  and    sp, x0, #0xfffffffffffffff0
    0x100018568 <+8>:  mov    x29, #0x0
    0x10001856c <+12>: mov    x30, #0x0
    0x100018570 <+16>: b      0x100018e84               ; start
dyld'mach_init:    0x100018574 <+0>: pacibsp 
    0x100018578 <+4>:  stp    x20, x19, [sp, #-0x20]!
    0x10001857c <+8>:  stp    x29, x30, [sp, #0x10]
Target 0: (helloptrace) stopped.
Executable module set to "/Users/pookjw/Library/Developer/Xcode/DerivedData/helloptrace-feiyuwxjcaporybbozwhdvquecxd/Build/Products/Debug/helloptrace".
Architecture set to: arm64e-apple-macosx-.
(lldb)

여기서 ptrace에 대해 조사해보면

(lldb) image lookup -n ptrace
1 match found in /usr/lib/system/libsystem_kernel.dylib:
        Address: libsystem_kernel.dylib[0x00000001802f70b0] (libsystem_kernel.dylib.__TEXT.__text + 35792)
        Summary: libsystem_kernel.dylib`__ptrace

__ptrace에 breakpoint를 걸면 된다는 것을 알 수 있다.

(lldb) breakpoint set -n '__ptrace'
Breakpoint 2: where = libsystem_kernel.dylib`__ptrace, address = 0x00000001a6eab0b0

아니면 regex로 해도 되고… 경고가 뜨는데 왜인지 잘 됨…

(lldb) rb ptrace -s libsystem_kernel.dylib
Breakpoint 1: no locations (pending).
WARNING:  Unable to resolve breakpoint to any actual locations.

그리고 continue를 하면 ptrace 호출이 일어날 때 pause가 걸리는데, 이때 아래처럼 처리하면 ptrace 함수가 호출될 때 아무것도 안하게 처리하면서 tracing 방어 코드를 무력화 할 수 있다.

(lldb) thread return 0

sysctl

과거 macOS의 iTunes 앱에서 sysctl를 통해 tracing을 막았다고 한다. 아래가 예시 코드다.

import Darwin

let mib: UnsafeMutablePointer<Int32> = .allocate(capacity: 4)
mib[0] = CTL_KERN
mib[1] = KERN_PROC
mib[2] = KERN_PROC_PID
mib[3] = getpid()

var size: Int = MemoryLayout<kinfo_proc>.size
var info: kinfo_proc? = nil

sysctl(mib, 4, &info, &size, nil, 0)

while true {
    if (info.unsafelyUnwrapped.kp_proc.p_flag & P_TRACED) > 0 {
        exit(0)
    }
    sleep(2)
}
#import <Foundation/Foundation.h>
@import Darwin;

int main(int argc, const char * argv[]) {
    int mib[4];
    mib[0] = CTL_KERN;
    mib[1] = KERN_PROC;
    mib[2] = KERN_PROC_PID;
    mib[3] = getpid();
    
    size_t size = sizeof(struct kinfo_proc);
    struct kinfo_proc info;
    
    while (YES) {
        sysctl(mib, 4, &info, &size, NULL, 0);
        
        if (info.kp_proc.p_flag & P_TRACED) {
            exit(0);
        }
        [NSThread sleepForTimeInterval:2.0f];
    }
    
    return 0;
}

마찬가지로 이 코드를 무력화시키려면 아래 symbol에서 비슷하게 하면 된다.

(lldb) image lookup -n sysctl
1 match found in /usr/lib/system/libsystem_c.dylib:
        Address: libsystem_c.dylib[0x00000001801f20e0] (libsystem_c.dylib.__TEXT.__text + 18484)
        Summary: libsystem_c.dylib`sysctl

Chapter 15: Dynamic Frameworks

iOS 8 이후로 서드파티 dynamic library가 허용되었다. 이러한 dynamic library들은 런타임 상에서 dyld를 통해 required framework와 optional framework를 메모리에 불러온다. required framework를 불러 올 때 문제가 발생하면 프로그램이 종료된다. optional framework는 불러 올 떄 문제가 발생해도 실행에 문제는 없지만 해당 framework에 있는 기능을 쓸 수 없다.

연습을 위해 사진처럼 Objective-C 기반 iOS 앱을 만들고 CoreBluetooth는 Optional, CallKit은 Required로 설정한다.

이제 otool로 컴파일된 Mach-O 파일을 본다. -l-L 옵션을 쓸건데 man otool로 설명을 보면 아래와 같다.

       -l     Display the load commands.

       -L     Display the names and version numbers of the shared libraries that the
              object file uses, as well as the shared library ID if the file is a shared
              library.

-L는 shared library들만 보여준다.

% otool -L /Users/pookjw/Library/Developer/Xcode/DerivedData/DeleteMe-cscvhbosmipliudcpaymflapbzrg/Build/Products/Debug-iphonesimulator/DeleteMe.app/DeleteMe
/Users/pookjw/Library/Developer/Xcode/DerivedData/DeleteMe-cscvhbosmipliudcpaymflapbzrg/Build/Products/Debug-iphonesimulator/DeleteMe.app/DeleteMe:
    /System/Library/Frameworks/CoreBluetooth.framework/CoreBluetooth (compatibility version 1.0.0, current version 1.0.0, weak)
    /System/Library/Frameworks/CallKit.framework/CallKit (compatibility version 1.0.0, current version 1.0.0)
    /System/Library/Frameworks/Foundation.framework/Foundation (compatibility version 300.0.0, current version 1860.0.0)
    /usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
    /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1311.120.1)
    /System/Library/Frameworks/CoreFoundation.framework/CoreFoundation (compatibility version 150.0.0, current version 1860.0.0)
    /System/Library/Frameworks/UIKit.framework/UIKit (compatibility version 1.0.0, current version 5610.0.0)

Optional로 설정된 framework는 weak로 표기된 것을 볼 수 있다.

-l는 모든 load command를 보여준다. 너무 많으므로 shared library 부분만 보여주고 나머지는 생략하겠다.

 % otool -l /Users/pookjw/Library/Developer/Xcode/DerivedData/DeleteMe-cscvhbosmipliudcpaymflapbzrg/Build/Products/Debug-iphonesimulator/DeleteMe.app/DeleteMe
/Users/pookjw/Library/Developer/Xcode/DerivedData/DeleteMe-cscvhbosmipliudcpaymflapbzrg/Build/Products/Debug-iphonesimulator/DeleteMe.app/DeleteMe:
Load command 14
          cmd LC_LOAD_WEAK_DYLIB
      cmdsize 96
         name /System/Library/Frameworks/CoreBluetooth.framework/CoreBluetooth (offset 24)
   time stamp 2 Thu Jan  1 09:00:02 1970
      current version 1.0.0
compatibility version 1.0.0
Load command 15
          cmd LC_LOAD_DYLIB
      cmdsize 80
         name /System/Library/Frameworks/CallKit.framework/CallKit (offset 24)
   time stamp 2 Thu Jan  1 09:00:02 1970
      current version 1.0.0
compatibility version 1.0.0
Load command 16
          cmd LC_LOAD_DYLIB
      cmdsize 88
         name /System/Library/Frameworks/Foundation.framework/Foundation (offset 24)
   time stamp 2 Thu Jan  1 09:00:02 1970
      current version 1860.0.0
compatibility version 300.0.0
Load command 17
          cmd LC_LOAD_DYLIB
      cmdsize 56
         name /usr/lib/libobjc.A.dylib (offset 24)
   time stamp 2 Thu Jan  1 09:00:02 1970
      current version 228.0.0
compatibility version 1.0.0
Load command 18
          cmd LC_LOAD_DYLIB
      cmdsize 56
         name /usr/lib/libSystem.B.dylib (offset 24)
   time stamp 2 Thu Jan  1 09:00:02 1970
      current version 1311.120.1
compatibility version 1.0.0
Load command 19
          cmd LC_LOAD_DYLIB
      cmdsize 96
         name /System/Library/Frameworks/CoreFoundation.framework/CoreFoundation (offset 24)
   time stamp 2 Thu Jan  1 09:00:02 1970
      current version 1860.0.0
compatibility version 150.0.0
Load command 20
          cmd LC_LOAD_DYLIB
      cmdsize 80
         name /System/Library/Frameworks/UIKit.framework/UIKit (offset 24)
   time stamp 2 Thu Jan  1 09:00:02 1970
      current version 5610.0.0
compatibility version 1.0.0

Optional로 설정된 framework는 LC_LOAD_WEAK_DYLIB, Required로 설정된 framework는 LC_LOAD_DYLIB로 설정된 것을 볼 수 있다.

load command 수정하기

lldb에서 현재 CallKit이 load 됐을 경우, 아래와 같이 확인할 수 있고 경로를 얻을 수 있다.

(lldb) image list CallKit
[  0] E3559B5C-5EFD-32BA-AFD4-5029B235A5D9 0x000000019006d000 /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/CallKit.framework/CallKit 

또한 pgrep -fl 명령어로 프로세스의 경로를 얻을 수 있다.

% pgrep -fl DeleteMe
1873 /Users/pookjw/Library/Developer/CoreSimulator/Devices/B2EBBD70-5B97-4BB3-A82F-B56D8C1B3BAF/data/Containers/Bundle/Application/AA081877-0D2C-4A69-A2D4-C8E42988DF85/DeleteMe.app/DeleteMe

우리는 CallKit 대신에 NotificationCenter를 load 하도록 변경할 것이다. 이를 위해 아래와 같이 변수를 할당한다.

app=/Users/pookjw/Library/Developer/CoreSimulator/Devices/B2EBBD70-5B97-4BB3-A82F-B56D8C1B3BAF/data/Containers/Bundle/Application/AA081877-0D2C-4A69-A2D4-C8E42988DF85/DeleteMe.app/DeleteMe
CK=/System/Library/Frameworks/CallKit.framework/CallKit
NC=/System/Library/Frameworks/NotificationCenter.framework/NotificationCenter

install_name_tool을 통해 CallKit 대신에 NotificationCenter를 load 하도록 변경할 수 있다. code signature가 망가진다는 경고문이 뜨는 것을 볼 수 있다.

% install_name_tool -change "$CK" "$NC" "$app"
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/install_name_tool: warning: changes being made to the file will invalidate the code signature in: /Users/pookjw/Library/Developer/CoreSimulator/Devices/B2EBBD70-5B97-4BB3-A82F-B56D8C1B3BAF/data/Containers/Bundle/Application/AA081877-0D2C-4A69-A2D4-C8E42988DF85/DeleteMe.app/DeleteMe

변경이 잘 되었는지 확인하기 위해 otool -L로 보면 잘 된 것을 볼 수 있다.

% otool -L "$app"
/Users/pookjw/Library/Developer/CoreSimulator/Devices/B2EBBD70-5B97-4BB3-A82F-B56D8C1B3BAF/data/Containers/Bundle/Application/AA081877-0D2C-4A69-A2D4-C8E42988DF85/DeleteMe.app/DeleteMe:
    /System/Library/Frameworks/CoreBluetooth.framework/CoreBluetooth (compatibility version 1.0.0, current version 1.0.0, weak)
    /System/Library/Frameworks/NotificationCenter.framework/NotificationCenter (compatibility version 1.0.0, current version 1.0.0)
    /System/Library/Frameworks/Foundation.framework/Foundation (compatibility version 300.0.0, current version 1860.0.0)
    /usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
    /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1311.120.1)
    /System/Library/Frameworks/CoreFoundation.framework/CoreFoundation (compatibility version 150.0.0, current version 1860.0.0)
    /System/Library/Frameworks/UIKit.framework/UIKit (compatibility version 1.0.0, current version 5610.0.0)

lldb 상에서도 확인이 가능하다. 참고로 Xcode로 다시 빌드하면 안 되며 앱 재실행이 필요하다.

(lldb) image list CallKit
error: no modules found that match 'CallKit'
(lldb) image list NotificationCenter
[  0] C5ECD0DA-BAC5-31F8-B5A9-00E1C286F306 0x0000000196ba1000 /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/NotificationCenter.framework/NotificationCenter 

런타임에서 framework load하기

우선 ~/.lldbinitls라는 명령어를 만들어준다.

command regex ls 's/(.+)/expression -l objc -O -- @import Foundation; [[NSFileManager defaultManager] contentsOfDirectoryAtPath:@"%1" error:nil]/'

이미 lldb가 실행된 상태면 아래 명령어로 ~/.lldbinit를 새로고침 할 수 있다.

(lldb) command source ~/.lldbinit

image list -d (-d는 directory) 명령어로 load된 framework의 경로를 알 수 있는데

(lldb) image list -d UIKit
[  0] /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/UIKit.framework

/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks에 Public Framework들이 모여 있는 것을 알 수 있으므로, 방금 만든 ls로 그 목록들을 볼 수 있다.

(lldb) ls /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks
<__NSArrayM 0x600002dc8030>(
MetricKit.framework,
Network.framework,
GameKit.framework,
AddressBookUI.framework,
Speech.framework,
# 생략...

만약에 lldb로 framework를 load하고 싶으면 아래 명령어로 할 수 있다.

(lldb) process load /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/Speech.framework/Speech
Loading "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/Speech.framework/Speech"...ok
Image 0 loaded.

(lldb) process load MessageUI.framework/MessageUI
Loading "MessageUI.framework/MessageUI"...ok
Image 1 loaded.

frmaework 탐색하기

책에는 아래 명령어로 모든 Objective-C class(+) method들을 볼 수 있게 한다는데 내가 했을 때는 안 됐다.

command regex dump_stuff "s/(.+)/image lookup -rn '\+\[\w+(\(\w+\))?\ \w+\]$' %1 /"

대신 이렇게 고치니 되더라…

command regex dump_stuff "s/(.+)/image lookup -rn '\+\[\w*(\(\w*\))?(\ \w*\])?' %1 /"

아래처럼 활용하면 된다.

(lldb) dump_stuff CoreBluetooth
42 matches found in /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/CoreBluetooth.framework/CoreBluetooth:
        Address: CoreBluetooth[0x000000000000e81c] (CoreBluetooth.__TEXT.__text + 53080)
        Summary: CoreBluetooth`+[CBCentralManager supportsFeatures:]        Address: CoreBluetooth[0x000000000001889c] (CoreBluetooth.__TEXT.__text + 94168)
        Summary: CoreBluetooth`+[CBUtil preSSPStringToPairingCode:]        Address: CoreBluetooth[0x0000000000018930] (CoreBluetooth.__TEXT.__text + 94316)
# 생략...

마찬가지로 ivars, methods, lmethods 명령어들도 만들 수 있다. methods는 class 내에서만 정의된 method들을 볼 수 있고, lmethods는 superclass까지 포함해서 볼 수 있다.

command regex ivars 's/(.+)/expression -l objc -O -- [%1 _ivarDescription]/'

command regex methods 's/(.+)/expression -l objc -O -- [%1 _shortMethodDescription]/'

command regex lmethods 's/(.+)/expression -l objc -O -- [%1 _methodDescription]/'

Chapter 16: Hooking & Executing Code with dlopen & dlsym

dlopendlsym을 통해 C, C++, Swift의 코드를 hook 해본다.

C

사진처럼 getenv에 breakpoint를 걸고 po (char *)$x0 (x86_64는 $rdi)로 하고 자동으로 continue 되도록 하고 앱을 실행하면 로그창에 아래와 같이 뜬다.

"MallocDebugReport"

"MallocErrorStop"

"MallocErrorSleep"

"MallocNanoMaxMagazines"

"_MallocNanoMaxMagazines"

"LIBDISPATCH_STRICT"

"LIBDISPATCH_COOPERATIVE_POOL_STRICT"

"DYLD_INSERT_LIBRARIES"

"NSZombiesEnabled"

# 생략...
만약에 모든 Environment Variables를 앱 실행시 보고 싶다면, **Edit Scheme** -> **Environment Variables** -> `DYLD_PRINT_ENV`을 value 없이 추가하면 된다.

여담으로 `dyld`가 load command를 통해 함수를 로드하기 전에는 함수 hook이 쉽다고 한다. 함수가 최초로 실행될 경우 `dyld`가 module 내에서 location을 찾는 활동이 최초 한 번 발생하고 그 다음 호출부터는 발생하지 않는다. 이러한 과정이 일어나기 전에 hook을 하면 쉽지만, 과정이 일어난 이후에 hook을 하려면 어렵다고 한다.

우리가 C 함수를 hook하는 가장 간단한 방법은 똑같은 함수를 만드는 것이다.

#import <dlfcn.h>
#import <assert.h>
#import <stdio.h>
#import <dispatch/dispatch.h>
#import <string.h>

char * getenv(const char *name) {
    return "YAY!";
}

getenv를 호출하면

if let cString: UnsafeMutablePointer<CChar> = getenv("HOME") {
    let homeEnv: String = String(cString: cString)
    print("HOME env: \(homeEnv)")
}

원래는 아래같이 나오는데

HOME env: /Users/pookjw/Library/Developer/CoreSimulator/Devices/B2EBBD70-5B97-4BB3-A82F-B56D8C1B3BAF/data/Containers/Data/Application/4286ECFA-E4FA-4104-93E9-6A9A19CCEC2A

아래처럼 hook이 제대로 되는 것을 볼 수 있다.

HOME env: YAY!

그렇다면 원래 함수도 같이 실행하고 싶다면? 만약 아래와 같이 하면 recursive가 되어서 stack overflow가 일어날 것이다.

char * getenv(const char *name) {
    return getenv(name);
    return "YAY!";
}

이를 해결하기 위해 dlopendlsym이 쓰인다. 우선 getenv의 symbol을 조사하자.

(lldb) image lookup -s getenv
1 symbols match 'getenv' in /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/system/libsystem_c.dylib:
        Address: libsystem_c.dylib[0x0000000000056814] (libsystem_c.dylib.__TEXT.__text + 350220)
        Summary: libsystem_c.dylib`getenv

handle의 주소는 /usr/lib/system/libsystem_c.dylib``, symbol은 getenv이므로

char * getenv(const char *name) {
    void *handle = dlopen("/usr/lib/system/libsystem_c.dylib", RTLD_NOW);
    assert(handle);
    void *real_getenv = dlsym(handle, "getenv");
    printf("Real getenv: %p\nFake getenv: %p\n", real_getenv, getenv);
    return "YAY!";
}
Real getenv: 0x103f2e814
Fake getenv: 0x1024fbdf8
HOME env: YAY!

이렇게 원래 함수의 주소를 얻어 올 수 있다. 좀 더 수정을 거치면

char * getenv(const char *name) {
    static void *handle;
    static char * (*real_getenv)(const char *);

    static dispatch_once_t onceToken;

    dispatch_once(&onceToken, ^{
        handle = dlopen("/usr/lib/system/libsystem_c.dylib", RTLD_NOW);
        assert(handle);
        real_getenv = dlsym(handle, "getenv");
    });
    
    if (strcmp(name, "HOME") == 0) {
        return "/WOOT";
    }
  
    return real_getenv(name);
}

이렇게 parameter에 HOME이 들어 올 때만 /WOOT라는 값을 반환하도록 설정할 수 있다. 참고로 C에서 함수는 포인터이므로, char * (*real_getenv)이라고 표기되었다.

이제 아래 코드를 호출해보면 아래와 같은 결과가 나온다.

if let cString: UnsafeMutablePointer<CChar> = getenv("HOME") {
    let homeEnv: String = String(cString: cString)
    print("HOME env: \(homeEnv)")
}

if let cString: UnsafeMutablePointer<CChar> = getenv("PATH") {
    let homeEnv: String = String(cString: cString)
    print("PATH env: \(homeEnv)")
}
HOME env: /WOOT
PATH env: /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/bin:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/bin:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/sbin:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/sbin:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/local/bin

Swift

Swift로 아래와 같은 코드가 있다고 하자.

public class CopyrightImageGenerator {

  // MARK: - Properties
  private var imageData: Data? {
    guard let data = ds_private_data else { return nil }
    return Data(bytes: data, count: Int(ds_private_data_len))
  }

  private var originalImage: UIImage? {
    guard let imageData = imageData else { return nil }

    return UIImage(data: imageData)
  }

  public var watermarkedImage: UIImage? {
    guard let originalImage = originalImage,
      let topImage = UIImage(named: "copyright",
                             in: Bundle(identifier: "com.razeware.HookingSwift"),
                             compatibleWith: nil) else {
        return nil
    }

    let size = originalImage.size
    UIGraphicsBeginImageContext(size)

    let area = CGRect(x: 0, y: 0, width: size.width, height: size.height)
    originalImage.draw(in: area)

    topImage.draw(in: area, blendMode: .normal, alpha: 0.50)

    let mergedImage = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    return mergedImage
  }

  // MARK: - Initializers
  public init() {}
}

설명을 붙이자면, private로 originalImage라는 이미지가 있고, public으로 watermarkedImage가 있다. watermarkedImageoriginalImage에 watermark를 붙이는 역할을 한다. 우리는 private로 정의된 originalImage을 호출해서 watermark를 붙이는 과정을 없애려고 한다.

우선 originalImage의 binary 파일 주소를 알아내면

(lldb) image lookup -rn HookingSwift.*originalImage
1 match found in /Users/pookjw/Library/Developer/Xcode/DerivedData/Watermark-awdrgxtzjdbtczaizjthfqodngfq/Build/Products/Debug-iphonesimulator/Watermark.app/Frameworks/HookingSwift.framework/HookingSwift:
        Address: HookingSwift[0x0000000000003208] (HookingSwift.__TEXT.__text + 328)
        Summary: HookingSwift`HookingSwift.CopyrightImageGenerator.originalImage.getter : Swift.Optional<__C.UIImage> at CopyrightImageGenerator.swift:41

./Frameworks/HookingSwift.framework/HookingSwift이다. 또한 symtab (symbol table)도 보면 우리가 봐야 할 주소는 0x0000000000003208이므로

(lldb) image dump symtab -m HookingSwift
Symtab, file = /Users/pookjw/Library/Developer/Xcode/DerivedData/Watermark-awdrgxtzjdbtczaizjthfqodngfq/Build/Products/Debug-iphonesimulator/Watermark.app/Frameworks/HookingSwift.framework/HookingSwift, num_symbols = 112:
               Debug symbol
               |Synthetic symbol
               ||Externally Visible
               |||
Index   UserID DSX Type            File Address/Value Load Address       Size               Flags      Name
------- ------ --- --------------- ------------------ ------------------ ------------------ ---------- ----------------------------------
# 생략...
[    8]     55 D X Code            0x0000000000003208 0x000000010070b208 0x00000000000000d4 0x000f0000 $s12HookingSwift23CopyrightImageGeneratorC08originalD033_71AD57F3ABD678B113CF3AD05D01FF41LLSo7UIImageCSgvg
# 생략...

symbol이 $s12HookingSwift23CopyrightImageGeneratorC08originalD033_71AD57F3ABD678B113CF3AD05D01FF41LLSo7UIImageCSgvg인 것을 알 수 있다.

따라서 우리는 아래와 같은 코드를 짤 수 있다.

if let handle: UnsafeMutableRawPointer = dlopen("./Frameworks/HookingSwift.framework/HookingSwift", RTLD_NOW),
    let sym: UnsafeMutableRawPointer = dlsym(handle, "$s12HookingSwift23CopyrightImageGeneratorC08originalD033_71AD57F3ABD678B113CF3AD05D01FF41LLSo7UIImageCSgvg") {
    print(sym)
}

그러면 콘솔에 0x000000010436f208 같은 주소가 찍힌다. 여담으로 이걸로 breakpoint를 설정할 수 있다.

(lldb) breakpoint set -a 0x000000010436f208
Breakpoint 1: where = HookingSwift`HookingSwift.CopyrightImageGenerator.originalImage.getter : Swift.Optional<__C.UIImage> at CopyrightImageGenerator.swift:41, address = 0x000000010436f208

# b (_regexp-break)로 해도 된다.
(lldb) b HookingSwift`HookingSwift.CopyrightImageGenerator.originalImage.getter : Swift.Optional<__C.UIImage>
Breakpoint 2: where = HookingSwift`HookingSwift.CopyrightImageGenerator.originalImage.getter : Swift.Optional<__C.UIImage> at CopyrightImageGenerator.swift:41, address = 0x000000010436f208

아래와 같이 private로 호출된 originalImage를 호출할 수 있게 된다.

if let handle: UnsafeMutableRawPointer = dlopen("./Frameworks/HookingSwift.framework/HookingSwift", RTLD_NOW),
    let sym: UnsafeMutableRawPointer = dlsym(handle, "$s12HookingSwift23CopyrightImageGeneratorC08originalD033_71AD57F3ABD678B113CF3AD05D01FF41LLSo7UIImageCSgvg") {
    let imageGenerator = CopyrightImageGenerator()
      
    typealias privateMethodAlias = @convention(c) (Any) -> UIImage?
    let originalImageFunction: privateMethodAlias = unsafeBitCast(sym, to: privateMethodAlias.self)
    let originalImage: UIImage? = originalImageFunction(imageGenerator)
    self.imageView.image = originalImage
}

Chapter 17: Exploring & Method Swizzling Objective-C Frameworks

UIDebuggingInformationOverlay를 iOS 15 arm64 환경에서 써보자