(十八) Dtrace vs objc_msgSend

1. Dtrace vs objc_msgSend

我們已經(jīng)看到了DTrace對Objective-C和Swift代碼的強大功能,無論是我們自己的源代碼,還是在類似UIKit的框架中的代碼。在對已編譯的源代碼執(zhí)行零修改的同時,使用DTrace跟蹤此代碼并進行有趣的調(diào)整。

不幸的是,當針對stripped的可執(zhí)行文件設(shè)置DTrace時,它無法創(chuàng)建任何探測來動態(tài)檢查這些函數(shù)。

然而,在探索蘋果代碼時,我們?nèi)匀挥幸粋€非常強大的盟友:objc_msgSend。下面我們將使用DTrace鉤住objc_msgSendentry,并取出該類的類名和Objective-C選擇器。

我們將用LLDB將生成一個DTrace腳本,用來跟蹤調(diào)用objc_msgSend的主可執(zhí)行文件中的代碼。

1.1 建立概念

我們將使用VCTransitions應用程序。它是一個非?;镜腛bjective-C/Swift應用程序,顯示了一個普通的UINavigationController的push過渡動畫和自定義過渡動畫。

打開這個項目,在模擬器上構(gòu)建并運行,然后快速瀏覽一下。需要注意的是,這個應用程序中有兩種方案:VCTransitionsStripped VCTransitions。確保在運行時選擇VCTransitions方案。我們稍后將詳細討論Stripped VCTransitions方案。

有兩個按鈕可以執(zhí)行兩次導航push,還有一個名為Execute Methods的按鈕,可以循環(huán)遍歷一個給定類的所有已知Objective-C方法。如果被遍歷的方法不接受任何參數(shù),則執(zhí)行該方法。

例如,顯示的第一個視圖控制器是ObjCViewController。如果點擊Execute Methods,它將調(diào)用anEmptyMethod以及所有重寫屬性的getter方法,因為所有這些方法都不需要參數(shù)。

跳到OjbCViewController.m并查看這個類實現(xiàn)的IBAction方法。在終端中生成一行DTrace,以確??梢钥吹竭@些方法被命中。確保模擬器處于活動狀態(tài)并運行VCTransitions項目。

sudo dtrace -n 'objc$target:ObjCViewController::entry' -p `pgrep VCTransitions`

DTrace會需要我們輸入密碼,然后回到模擬器中開始點擊Execute Methods按鈕。我們將會看到DTrace終端窗口充滿了ObjCViewController實現(xiàn)的IBAction方法。

CPU     ID                    FUNCTION:NAME
  2 364044 -executeLotsOfMethodsButtonTapped::entry
  2 364047             -anEmptyMethod:entry
  2 364048        -coolViewDTraceTest:entry
  2 364050     -coolBooleanDTraceTest:entry

現(xiàn)在,點擊其中一個push按鈕,就可以來到SwiftViewController。盡管這是UIViewController的一個子類,但是點擊IBActions不會為objcPID探測產(chǎn)生任何結(jié)果。盡管SwiftViewController實現(xiàn)或重寫了動態(tài)方法,并通過objc_msgSend執(zhí)行,但實際的代碼是Swift代碼(甚至是那些@objc橋接方法)。

如果SwiftViewController包含以下代碼:

class SwiftViewController: UIViewController, UIViewControllerTransitioningDelegate {
  @objc var coolViewDTraceTest: UIView? = nil
  @objc var coolBooleanDTraceTest: Bool = false
    // ...

Objective-CDTrace探測是否會觸發(fā)coolBooleanDTraceTestcoolViewDTraceTest?要回答這個問題,首先要看看這些Swift屬性是否作為objective-C探針公開。他們應該是,對吧?它們具有@objc屬性。

sudo dtrace -ln 'objc$target::*cool*Test*:entry' -p `pgrep VCTransitions`

只顯示Objective-CObjCViewController的屬性,而不顯示SwiftViewController!這是因為Swift提議160 https://github.com/apple/Swift-evolution/blob/master/proposals/0160-objc-inference.md,其中包括NSObject不再推斷@objc的提議。此外,Swift甚至不會為動態(tài)代碼創(chuàng)建Objective-C符號。

這意味著我們必須使用非Objective-Cprovider來查詢SwiftDTrace探測器。可以通過擴充DTrace腳本來探測*cool*Test*,如下所示:

sudo dtrace -n 'pid$target::*cool*Test*:entry' -p `pgrep VCTransitions`

這是使用objc_msgSend而不是objc$target探測的另一個原因。因為對objc_msgSend的調(diào)用將捕獲動態(tài)執(zhí)行的Swift代碼,而objc$target將錯過這些代碼。

在stripped scheme中重復剛才的操作步驟

運行的目標(可執(zhí)行文件)與VCTransitions應用程序完全相同,只是生成一個不包含任何調(diào)試信息的版本。選擇Stripped VCTransitions,運行。運行之后,暫停應用程序并啟動LLDB。

(lldb) lookup SwiftViewController
(lldb) lookup ObjCViewController

什么都沒有,為什么呢?因為這個可執(zhí)行文件的信息已被刪除。我們不能使用通??捎玫恼{(diào)試符號來引用內(nèi)存中的地址。
然而,LLDB足夠聰明,能夠意識到內(nèi)存中的這些位置實際上是函數(shù)。LLDB將為其沒有信息的方法生成唯一的函數(shù)名。自動生成的函數(shù)名將采用以下形式:

___lldb_unnamed_symbol[FUNCTION_ID]$$[MODULE_NAME]

這意味著我們可以使用以下查找命令列出LLDB在VCTransitions可執(zhí)行文件中生成的所有函數(shù):

(lldb) lookup VCTransitions
****************************************************
255 hits in: VCTransitions
****************************************************
___lldb_unnamed_symbol1$$VCTransitions
___lldb_unnamed_symbol2$$VCTransitions
___lldb_unnamed_symbol3$$VCTransitions
...

LLDB無法獲取這些函數(shù)的名稱。那么DTrace可以讀取精簡過的二進制文件中的內(nèi)容嗎?在終端中鍵入以下內(nèi)容:

~> sudo dtrace -n 'objc$target:ObjCViewController::entry' -p `pgrep VCTransitions`
Password:
   ID   PROVIDER            MODULE                          FUNCTION NAME
dtrace: failed to match objc96862:ObjCViewController::: No probe matches description

這將查詢VCTransitions進程,以獲取包含模塊ObjCViewController的探測計數(shù)。沒有任何命中。

1.2 如何繞過精簡過的沒有探針的二進制文件

那么,如何構(gòu)建一個DTrace探測來繞過這個無法檢查剝離的二進制文件的障礙呢?

由于我們知道Objective-C(和dynamic Swift)方法需要通過objc_msgSend,所以可以使用objc_msgSend知識來找出如何創(chuàng)建一個好的DTrace操作,該操作將打印出類的名稱和Objective-C選擇器。objc_msgSend函數(shù)簽名如下:

objc_msgSend(instance_or_class, SEL, ...);

因此,objc_msgSend將類或?qū)嵗鳛榈谝粋€參數(shù),將Objective-C選擇器作為第二個參數(shù),然后是可變數(shù)量的參數(shù)。

UIViewController *vc = [UIViewController new];
[vc setTitle:@"yay, DTrace"];

編譯器會將其轉(zhuǎn)換為以下偽代碼:

 vc = objc_msgSend(UIViewControllerClassRef, "new");
 objc_msgSend(vc, "setTitle:", @"yay, DTrace");

DTrace的角度來看,獲得Objective-C選擇器相當容易。只需要用到copyinstr(arg1)。從arg1復制Objective-C選擇器指針(也稱為char*)到內(nèi)核中,以便DTrace可以讀取它。

對于困難的部分:我們需要將第一個參數(shù)的類名作為char*傳遞給objc_msgSend。

DTrace不允許執(zhí)行任意方法,因此不能依賴Objective-C運行時,或它實現(xiàn)的任何方法,為我們挖掘信息。相反,我們可以在arg0實例的內(nèi)存中進行探索,并找到char*表示類名,然后將其自動轉(zhuǎn)換為DTrace腳本。

1.3 使用DTrace重新搜索方法調(diào)用

讓我們看看有沒有什么記錄在案的方法來追查這件事。在objc/runtime.h頭文件中,有以下聲明:

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
    Class super_class
OBJC2_UNAVAILABLE;
    const char *name
OBJC2_UNAVAILABLE;
    long version
OBJC2_UNAVAILABLE;
    long info
OBJC2_UNAVAILABLE;
    long instance_size
OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars
OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists
OBJC2_UNAVAILABLE;
    struct objc_cache *cache
OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols
OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */

回到在64位的機器上使用Objective-C 2.0的日子里,如果我們有一個指向一個有效類X的指針,我們就可以獲取到那個在#if !__OBJC2__中描述的const char *name

po *(char *)(X + 0x10)

不幸的是,這是相當久遠的事了。這個類結(jié)構(gòu)可以追溯到Objective-C 2.0之前。結(jié)構(gòu)和指針位置早就改變了。這意味著我們需要尋找一個接受Objective-C類(或該類的實例)并為該類返回char*的函數(shù),這樣我們就可以知道它在做什么。

幸運的是,跳回objc/runtime.h頭文件時,還有一個名為@getName的函數(shù)。class_getName具有以下簽名:

/**
* Returns the name of a class.
*
* @param cls A class object.
*
* @return The name of the class, or the empty string if cls is Nil. 
*/
OBJC_EXPORT const char *class_getName(Class cls) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);

此函數(shù)接受一個類并返回一個char*。我們將使用DTrace來跟蹤此方法,并查看該類在封面下調(diào)用的方法。

獲取對表示UIView的類的引用:

(lldb) p/x [UIView class]
(lldb) p/x [UIView class]
(Class) $0 = 0x00007fff898df878 UIView
(lldb) po class_getName(0x00007fff898df878)
0x00007fff526e8954
//記得使用(char *)
(lldb) po (char *)class_getName(0x00007fff898df878)
"UIView"

現(xiàn)在,我們將使用DTrace在幕后跟蹤所有非Objective-C方法類的getName調(diào)用。跳轉(zhuǎn)到新的終端會話并執(zhí)行以下代碼:

sudo dtrace -n 'pid$target:::entry' -p `pgrep VCTransitions`

跳轉(zhuǎn)回LLDB并使用對UIView類的引用重新執(zhí)行該類的getName函數(shù)。

~> sudo dtrace -n 'pid$target:::entry' -p `pgrep VCTransitions`
Password:
dtrace: description 'pid$target:::entry' matched 939026 probes
CPU     ID                    FUNCTION:NAME
  0 380442              class_getName:entry
  0 380435 objc_class::demangledName():entry
  2 364870        _NSPrintForDebugger:entry
  2 380732 objc_opt_respondsToSelector:entry

看起來,objc_class::demangledName()函數(shù)是一個有趣的探索地方。終止DTrace腳本。我們不想讓它破壞LLDB斷點。因為在LLDB斷點上設(shè)置DTrace探測可能會產(chǎn)生意外的結(jié)果。一旦DTrace腳本終止,就在
objc_class::demangledName()設(shè)置斷點,如下所示:

(lldb) b objc_class::demangledName(bool)
(lldb) exp -i0 -O -- class_getName([UIView class])
objc_class::demangledName()匯編
可怕的匯編,第一部分

像往常一樣,這些東西一開始看起來很嚇人。實際上,我們將把程序匯編函數(shù)分成塊來研究。第一個塊將在偏移量0-68之間。檢查寄存器,這樣就知道我們在處理什么:

(lldb) po $rdi
UIView

我們得到UIView輸出,它是UIView類的description方法。但為什么這是第一個參數(shù)?函數(shù)簽名似乎表明它應該是bool值。因為,這是一個C++函數(shù),C++在對象上調(diào)用函數(shù)的方式就像OC。有一個隱式的第一個參數(shù),它是函數(shù)被調(diào)用的對象。但是在Swift中,作為第一個寄存器傳入的實例并不總是這樣。

轉(zhuǎn)到第二個參數(shù):

(lldb) po (bool)$rsi
true
  • Offset 13: 在這一行之后,函數(shù)序言就執(zhí)行完畢了。
  • Offset 17:這將esi賦值給r12d。這就是傳進來的Boolean值.我們之前查看的rsi并且看到了它是0, 因此r12d將會同樣是0。
  • Offset 20:rdi包含著UIView類的引用并且賦值給了r15。
  • Offset 33:這與r15的偏移量是0x20的值并且解引用,也就是說rax = (*([UIView class] + 0x20))。
  • Offset 37:這個值存儲在rax是用0x7ffffffffff8相與存儲到了rax。
  • Offset 48:這個值是rax偏移0x38后的值然后解引用并存儲在rbx。也就是說,rbx = *(rax + 0x38)。
  • Offset 52-55:檢查rab是否是0。如果它返回一個非零的數(shù)字,然后跳到<+310>結(jié)束這個函數(shù),在函數(shù)結(jié)束之前是正確的。

如果這個檢查偏移量55的值失敗了(也就是說,如果rbx0),執(zhí)行將會執(zhí)行下一句匯編指令<+61>。偏移量在0~55的邏輯是負責將一個Objective-C的類作為一個char*返回給你。如果(并且僅僅只在如果)那個類已經(jīng)被正確的加載之后。這通常發(fā)生在那個類里面至少有一個方法被執(zhí)行了。

例如,如果一個新類被調(diào)用了,然而在進程存活期間還沒有執(zhí)行任何初始化,偏移量在0~55之間的邏輯將會返回nil。稍后我們將會構(gòu)建一個command regex來確認這點。

看這些匯編,我們可以推斷出下面的內(nèi)容。如果有一個已經(jīng)初始化的X類的實例,將X偏移了0x20然后解引用它,輸出的內(nèi)容看起來應該是下面這個樣子:

*(uint64_t *)(X + 0x20)

然后用0x7ffffffffff8按位這個值:

*(uint64_t *)(X + 0x20) & 0x7ffffffffff8

接下來,使用這個值并偏移0x38這個值,然后解引用:

*(uint64_t *)((*(uint64_t *)(X + 0x20) & 0x7ffffffffff8) + 0x38)

這是最終的地址,因此我們只需要將它輸出到正確的類型里,char*

(char *)*(uint64_t *)((*(uint64_t *)(X + 0x20) & 0x7ffffffffff8) + 0x38)

如果有一個NSObject的引用,那么這個對象起始位置的內(nèi)存地址指向類自己(就是那個isa指針)。將所有這些內(nèi)容放在一起,將一個實例的類名作為一個char*獲取,看這個怪物:

(char *)*(uint64_t *)((*(uint64_t *)((*(uint64_t *)Instance_of_X) + 0x20) & 0x7ffffffffff8) + 0x38)

在LLDB中進行驗證:

(lldb) x/gx '0x000000010c09ce60 + 0x20'
0x10c09ce80: 0x0000608000064b80

(lldb) p/x 0x7ffffffffff8 & 0x0000608000064b80
0x0000608000064b80

(lldb) x/gx '0x0000608000064b80 + 0x38'
0x608000064bb8: 0x000000010bce319f

(lldb) po (char *)0x000000010bce319f
"UIView"

注意:這不適用于還沒有被初始化的Objective-C的類。為什么使用UIView的原因, 是因為如果我們可以在屏幕上看到UI, 那么UIView類已經(jīng)明確的被初始化了, 或者說至少有一個UIView被初始化了。

創(chuàng)建一個新的regex command來確認一下。

(lldb) command regex getcls 's/(.+)/expression -lobjc -O -- (char *)*(uint64_t*)((*(uint64_t *)((*(uint64_t *)%1) + 0x20) & 0x7ffffffffff8) + 0x38)/'

這條指令會從已經(jīng)加載到進程里的任何實例上抓取char*類名。將這條指令輸入到LLDB控制臺之后,用它驗證一下之前UIView

(lldb) getcls [UIView new]
"UIView"

現(xiàn)在看一下還沒有被初始化或者還沒有執(zhí)行任何方法的那些類,比如UIAlertController

(lldb) getcls [UIAlertController new]
nil

(lldb) po [UIAlertController class]
(lldb) getcls [UIAlertController new]
"UIAlertController"

記住如果這個類獨有的方法被執(zhí)行了,Objective-C運行時就會加載這個類。這個類方法(比如-[NSObject class])不是UIAlertController獨有的,為什么成功了呢?因為我們執(zhí)行了po這個對象,它的debugDescriptiondescription方法是這個類獨有(重寫)的!因此,在po一個UIAlertController類的時候,它會被加載到運行時里。

1.4 可怕的匯編, 第二部分

下面討論objc_class::demangledName(bool)C++函的第二部分。如果char*的初始位置不在感興趣的初始位置(也就是說,如果類尚未加載),則此匯編將關(guān)注邏輯的功能。我們需要在匯編指令偏移量61上創(chuàng)建斷點,該指令緊跟在偏移量55上的指令之后。

創(chuàng)建一個符號斷點,該斷點在objc_class::demangledName(bool)的偏移量61處停止。
使用以下詳細信息在Xcode中創(chuàng)建符號斷點:

  • 使用dlopen作為符號。

  • 在操作1中:使用br dis 1刪除此斷點。

  • 在操作2中:使用以下命令在objc_class::demangledName(bool)的偏移量61上設(shè)置斷點:

    br set -M objc_class::demangledName(bool) -R 61
    
  • 勾選Automatically continue after evaluating actions。

重新構(gòu)建并運行VCTransitions應用程序。

objc_class::demangledName(bool)匯編
  • 偏移量 61:如果內(nèi)存中的初始位置為nil,則控制繼續(xù)來到61,其中rax+0x8被解引用并再次存儲到rax中。

  • 偏移量 65:0x18被添加到rax并存儲回rax。rax可能是一個持有感興趣值的結(jié)構(gòu),這可以解釋偏移這個地址的原因。

  • 偏移量 69:rax處的值被解引用并存儲到rbx中,rbx稍后(2指令之后)將被傳遞到rdi。在這之后,會出現(xiàn)一條調(diào)用指令,根據(jù)反匯編注釋,該指令期望char const*作為第一個參數(shù)。

這對我們來說是這個函數(shù)的“有趣”部分。之后,該函數(shù)調(diào)用copySwiftV1DemangledName函數(shù),并設(shè)置將類加載到Objective-C運行時的邏輯。

1.5 重新轉(zhuǎn)換到代碼里搜索

我們已經(jīng)做了必要的研究,找出如何遍歷內(nèi)存以獲得類的字符數(shù)組表示。是時候?qū)嵤┻@件事了。

起始腳本中包含一個名為msgsendsnoop.d的骨架DTrace腳本。我們將從這個DTrace腳本開始并構(gòu)建它的代碼。完成工作和測試后,將把代碼傳輸?shù)絃LDB Python腳本中,該腳本將動態(tài)生成所需的代碼。

cat一下腳本:

 starter>cat ./msgsendsnoop.d
#!/usr/sbin/dtrace -s
#pragma D option quiet

dtrace:::BEGIN
{
  printf("Starting... Hit Ctrl-C to end.\n");
}

pid$target::objc_msgSend:entry
{
  this->selector = copyinstr(arg1);
  printf("0x%016p, +|-[%s %s]\n", arg0, "__TODO__",
                                         this->selector);
}

讓我們把這個分解一下。傳入相應的PID,此腳本將在objc_msgSend entry probe上停止。一旦點擊,選擇器的char*被復制到內(nèi)核中并打印出來。例如,將要調(diào)用-[UIView initWithFrame:]。將打印出以下內(nèi)容:

0x00000000deadbeef, +|-[__TODO__ initWithFrame:]

通過跟蹤VCTransitions中的所有objc_msgSend調(diào)用來驗證這一點是否正確:

sudo ./msgsendsnoop.d -p `pgrep VCTransitions`

是時候修正這個煩人的TODO并用類的實際名稱替換它了。打開msgsendsnoop.d并替換現(xiàn)有的pid$target::objc_msgSend:entry

pid$target::objc_msgSend:entry
{
/* 1 */
  this->selector = copyinstr(arg1);
  /* 2 */
size = sizeof(uintptr_t);
  /* 3 */
  this->isa = *((uintptr_t *)copyin(arg0, size));
/* 4 */
  this->rax = *((uintptr_t *)copyin((this->isa + 0x20), size));
  this->rax =  (this->rax & 0x7ffffffffff8);
/* 5 */
  this->rbx = *((uintptr_t *)copyin((this->rax + 0x38), size));
  this->rax = *((uintptr_t *)copyin((this->rax + 0x8),  size));
/* 6 */
  this->rax = *((uintptr_t *)copyin((this->rax + 0x18), size));
/* 7 */
  this->classname = copyinstr(this->rbx != 0 ?
                               this->rbx  : this->rax);
  printf("0x%016p +|-[%s %s]\n", arg0, this->classname,
                                       this->selector);
}
  1. this->selector執(zhí)行copyinstr。因為我們知道第二個參數(shù)(arg1)是Objective-C選擇器(C string) 。由于C字符以空字符結(jié)尾,DTrace可以自動確定要讀取的數(shù)據(jù)量。
  2. 一會兒,我們就要復制一些數(shù)據(jù)。然而,copyin需要一個大小,因為與字符串不同,DTrace不知道任意數(shù)據(jù)何時結(jié)束。聲明一個名為size的變量,它等于指針的長度。在x64中,這將是8字節(jié)。
  3. 這是對實例類的引用。請記住,Objective-C或Swift實例的起始地址處的解引用指針將指向該類。
  4. 現(xiàn)在來看看從objc_class::demangledName(bool)中的匯編中學到的有趣的部分。我們將復制寄存器中的邏輯,甚至對寄存器使用相同的名稱!用rax來模擬這個函數(shù)執(zhí)行的邏輯。
  5. 這是(rax+0x38)設(shè)置為this->rbx的邏輯,就像在實際匯編中一樣。
  6. 如果this->rbx的值為0(也就是說類尚未加載),這是最后一行。
  7. 使用三元運算符來確定要使用哪個子句局部變量。如果this->rbx是非空的,使用它;否則,使用this->rax。

保存并跳轉(zhuǎn)到終端并重新啟動此DTrace腳本:

sudo ./msgsendsnoop.d -p `pgrep VCTransitions`

掃描腳本中的內(nèi)容時,偶爾當objc_msgSend調(diào)用nil對象時(即RDI,arg0是0x0),腳本似乎正在拋出錯誤。使用以下命令只能查看錯誤:

sudo ./msgsendsnoop.d -p `pgrep VCTransitions` | grep invalid

現(xiàn)在我們用一個簡單的謂詞來解決這個問題。緊跟在pid$target::objc_msgSend:entry之后,添加以下謂詞:

pid$target::objc_msgSend:entry / arg0 > 0x100000000 /

這意味著,“如果第一個參數(shù)為nil或內(nèi)存部分未被利用,則不要運行此DTrace操作。”通常,在macOS用戶區(qū)進程中,內(nèi)存的這一部分禁止讀取、寫入和執(zhí)行。如果有任何東西低于0x100000000DTrace就不會關(guān)心它,還有其他讀取內(nèi)存的東西。因此,如果低于這個數(shù)字,就讓DTrace跳過它。當然,可以使用LLDB通過以下命令確認這一點:

(lldb) image dump sections VCTransitions
移除干擾

老實說,我非常關(guān)心跟蹤編譯器生成的內(nèi)存管理代碼。這意味著任何有retainrelease的東西都需要離開這里。
使用當前探測上方的相同DTrace探測創(chuàng)建新子句:

pid$target::objc_msgSend:entry
{
  this->selector = copyinstr(arg1);
}
/* old code below */
pid$target::objc_msgSend:entry / arg0 > 0x100000000 /

現(xiàn)在,在主子句之前的一個新子句中聲明選擇器,其中包含所有的內(nèi)存跳躍邏輯。這將允許我們在主子句的謂詞部分中篩選Objective-C方法?,F(xiàn)在把主句中的謂語擴充一下:

pid$target::objc_msgSend:entry / arg0 > 0x100000000 / &&
                    this->selector != "retain" &&
                    this->selector != "release" /

這將忽略任何等于retainrelease的Objective-C選擇器。在這里,不需要在主子句中重新分配this->selector,現(xiàn)在在另一個子句中執(zhí)行它。最終的代碼是這樣:

pid$target::objc_msgSend:entry
{
  this->selector = copyinstr(arg1);
}
pid$target::objc_msgSend:entry / arg0 > 0x100000000 / &&
                    this->selector != "retain" &&
                  this->selector != "release" /
{
  size = sizeof(uintptr_t);
  this->isa = *((uintptr_t *)copyin(arg0, size));

  this->rax = *((uintptr_t *)copyin((this->isa + 0x20), size));
  this->rax =  (this->rax & 0x7ffffffffff8);
  this->rbx = *((uintptr_t *)copyin((this->rax + 0x38), size));
  
  this->rax = *((uintptr_t *)copyin((this->rax + 0x8),  size));
  this->rax = *((uintptr_t *)copyin((this->rax + 0x18), size));
  
  this->classname = copyinstr(this->rbx != 0 ?
                               this->rbx  : this->rax);
  printf("0x%016p +|-[%s %s]\n", arg0, this->classname,
                                       this->selector);
}

重新啟動這個腳本:

sudo ./msgsendsnoop.d -p `pgrep VCTransitions`

這次好多了,但是這里仍然有很多干擾。是時候?qū)⑦@個腳本與LLDB聯(lián)合起來得到一些主執(zhí)行文件的相應輸出了。

1.6 用LLDB限定范圍

starter文件夾中包含一個LLDB Python腳本,該腳本創(chuàng)建一個DTrace腳本,并使用剛剛實現(xiàn)的邏輯運行它。此文件名為snoopie.py。把這個文件復制到~/lldb目錄中。

我們將使用一個創(chuàng)造性的解決方案來過濾掉這個DTrace腳本中的代碼,從而只跟蹤屬于VCTransitions可執(zhí)行文件的Objective-C/dynamic Swift代碼。通常,在框架中窺探代碼時,我會經(jīng)常獲取模塊的__TEXT段,并將指令指針與加載到內(nèi)存中的__TEXT段(內(nèi)存中包含可執(zhí)行代碼的區(qū)域)的上下限進行比較。如果指令指針位于上下界之間,則可以假定要使用DTrace跟蹤代碼。

不幸的是,我們正在尋找objc_msgSend,它是所有模塊中用于Objective-C代碼的咽喉點。這意味著我們不能依賴指令指針來告訴我們所處的模塊。相反,我們需要隔離一個類的地址,使其僅包含在主可執(zhí)行文件的__DATA段中。

運行、停止執(zhí)行VCTransitions并啟動LLDB。然后鍵入以下內(nèi)容:

(lldb) p/x (void *)NSClassFromString(@"ObjCViewController")
(void *) $0 = 0x00000001045e1bc0
(lldb) image lookup -a 0x00000001045e1bc0
      Address: VCTransitions[0x0000000100013bc0] (VCTransitions.__DATA.__objc_data + 40)
      Summary: (void *)0x00000001045e1b98: ObjCViewController

因此,可以推斷該類位于VCTransitions __DATA段中的__objc_data段中。我們將使用LLDB Python模塊來查找這個數(shù)據(jù)段的上下限。使用script命令來查找如何通過LLDB模塊創(chuàng)建此代碼。在LLDB中,鍵入以下內(nèi)容:

(lldb) script path = lldb.target.executable.fullpath

(lldb) script print(lldb.target.module[path])
(x86_64) /Users/xxx/Library/Developer/Xcode/DerivedData/VCTransitions-gxzcdoxigcmztnduvannyvlhzqff/Build/Products/Debug-iphonesimulator/VCTransitions.app/VCTransitions

(lldb) script print(lldb.target.module[path].section[0])
[0x0000000000000000-0x0000000100000000) VCTransitions.__PAGEZERO

(lldb) script print(lldb.target.module[path].section['__PAGEZERO'])
[0x0000000000000000-0x0000000100000000) VCTransitions.__PAGEZERO

(lldb) script print(lldb.target.module[path].section['__DATA'])
[0x0000000100010000-0x0000000100015000) VCTransitions.__DATA

SBModule中,有一些SBSection。可以使用sections屬性獲取SBModule中的所有節(jié),也可以使用section[index]獲取特定節(jié)。

(lldb) script section = lldb.target.module[path].section['__DATA']

獲取section的加載地址和大小,如下所示:

(lldb) script section.GetLoadAddress(lldb.target)
4368228352
(lldb) script section.size
20480

現(xiàn)在,我們可以生成DTrace謂詞,檢查類是否在內(nèi)存中的這些值之間。如果是,則執(zhí)行DTrace操作。如果不是,就別理他們。讓我們來實現(xiàn)這個!

1.7 修復snoopie腳本

這個snoopie.py腳本按之前的說的方式工作,因此我們只需向謂詞添加一些小邏輯,以便僅過濾實例。打開~/lldb/snoopie.py并找到到generateDTraceScript函數(shù)。刪除dataSectionFilter=...行。然后添加以下代碼:

target = debugger.GetSelectedTarget()
path = target.executable.fullpath
section = target.module[path].section['__DATA']
start_address = section.GetLoadAddress(target)
end_address = start_address + section.size
dataSectionFilter = '''{} <= *((uintptr_t *)copyin(arg0,
    sizeof(uintptr_t))) &&
   *((uintptr_t *)copyin(arg0, sizeof(uintptr_t))) <= {}
'''.format(start_address, end_address)

這里有趣的一點是,獲取arg0,當arg0大于0x100000000時才解引用它,用來表示內(nèi)存中存在有效實例。來到LLDB控制臺,通過自定義的reload_script命令或通過命令script import~/.lldbinit手動重新加載LLDB中的內(nèi)容。

(lldb) reload_script
(lldb) snoopie
Copied script to clipboard... paste in Terminal

將內(nèi)容粘貼到終端窗口,現(xiàn)在DTrace只分析主(精簡過的)可執(zhí)行文件中的代碼。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容