Advanced+Apple+Debugging(17)

來源:互聯網
上載者:User

標籤:lldb

可怕的彙編, 第二部分

是時候重溫一下objc_class::demangledName(bool)c++函數中有趣的第二部分了.這一次彙編代碼將會聚焦於如果char不在char的初始位置裡--也就是說, 如果這個類還沒有被載入的時候這些邏輯做了哪些事情.
你需要在緊跟在位移55的後面的位移61的彙編指令處位置建立一個斷點.
你可以隨便調用一個類來看看哪些類沒有被載入都運行時裡, 我不知道你的進程裡的東西而你也不知道我的進程裡的東西!
取而代之的是, 在停在objc_class::demangledName(bool)處位移61的位置出建立一個symbolic斷點.
在Xcode中使用下面的步驟建立一個symbolic斷點:
? 為這個symbolic使用dlopen
? 第一步: 用br dis 1移除這個斷點
? 第二步: 用下面的命令在objc_class::demangledName(bool)位移61的地方設定一個斷點

br set -M objc_class::demangledName(bool) -R 61
? 選擇 "Automatically continue after evaluating actions".

圖片.png

重新構建並運行VCTransitions應用程式.
在這個斷點被處罰之前你不會非常深入到你的程式裡;你會看到dyld仍然忙於設定.
第二輪;我們從這裡出發:
圖片.png

? Offset 61:提供了在記憶體中的初始化位置是nil, 繼續運行到61, rax + 0x8解引用的地方然後再次儲存到rax裡.
? Offset 65: 值0x18被添加到rax裡然後存回rax. rax可能是一個持有一個可以解釋這個地址位移的資料的結構體.
? Offset 69: rax的值被解引用然後儲存到rbx中, 稍後將會傳給rdi 2指令.在那之後, 這個函數調用了copySwiftV1DemangledName函數並且設定了將這個類添加到運行時裡的邏輯.
但是對你來說, 直到你需要瀏覽這個函數為止.
隨時確認rdi將會在位移77處產生一個有效char*, 但是再說一次, 那將是在你自己的額時間裡做的事情. 你仍然需要寫一個DTrace指令碼.
重新轉換到代碼裡搜尋

你已經做了必要的重新搜尋來弄清楚如何在記憶體裡蛇形擷取代表一個類的字元數組.是時候實現這件事了.
在Starter檔案夾中有一個叫msgsendsnoop.d的DTrace指令碼. 你將用這個DTrace指令碼開始然後構建出相應的代碼. 如果經過測試是可行的, 你就需要將那些代碼轉換成能夠讓你動態擷取你想要的代碼的LLDB的python指令碼.
在終端中cd到starter檔案夾中.將檔案夾拖拽到終端中會自動產生路徑.
cat這個指令碼的內容:

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",
}
讓我們來拆解一下. 這個指令碼將會停在傳入的相應PID的objc_msgSend的入口探針處(這是pid$target的作用).一旦觸發了之後, selector的char*會被拷貝到核心中然後列印出來.
正如例子中將會發生的一樣, 也就是說-[UIView initWithFrame:] 將會被調用. 會列印出下面的內容:

0x00000000deadbeef, +|-[TODO initWithFrame:]
可以通過追蹤VCTransitions程式中所有調用的objc_msgSend來驗證一下這個是真的.

sudo ./msgsendsnoop.d -p pgrep VCTransitions
在一些類上點擊.這回讓你看到這些方法調用的頻率.

圖片.png
到了修複煩人的TODO時間了並且用類的實際名字替換他.
開啟msgsendsnoop.d檔案然後用下面的代碼替換pid$target::objc_msgSend:entry:

注意:我會推薦你輸入每一行代碼然後確保它是可以啟動並執行, 而不是一次輸入所有代碼. 一些DTrace的錯誤會被捕獲到.
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);
}
深呼吸一下. 下面是每一行代碼的意思:

this->selector 做了一個copyinstr, 因為你知道第二個參數(arg1)是一個Objective-C selector(它是一個c字串).因為C char*是以一個null字元做結尾的, DTrace可以自動檢測到讀多少資料.
轉眼間, 你就要copyin一些資料了. 然而, copyin需要一個size, 因為不像string, DTrace不知道資料在那裡結尾.你聲明了一個叫size的變數, 它等於一個指標的長度. 在x64中, 就是8 bytes.
這就是擷取一個類執行個體的引用的方法.記住, 解引用Objective-C 或者 Swift類執行個體的起始位置的指標會指向這個類.
現在是你在objc_class::demangledName(bool)彙編中學到的有趣的部分. 你將會複製在寄存器中找到的邏輯, 甚至會使用同樣的寄存器的名字!你正在使用rax來類比這個函數執行的邏輯.
這就是(rax + 0x38)設定this->rbx的代碼, 就像在真是的彙編中一樣.
如果this->rbx是0, 這就是最後一行代碼(這個類還沒有被載入).
你正在使用一個三元操作符來弄清楚哪一個局部變數被使用了.如果this->rbx是non-null, 就用this->rbx. 否則, 使用this->rax.
儲存一下你剛才做的工作.回到終端中 從心啟動這個DTrace指令碼:
sudo ./msgsendsnoop.d -p pgrep VCTransitions
喔喔喔喔喔喔喔喔喔喔喔喔喔!那個瘋狂的指令碼真的有用!
掃描一下你指令碼的內容, 看起來這個指令碼在objc_msgSend調用一個nil對象(也就是說 RDI也就是arg0 是0x0)的時候會拋出一些錯誤.
你可以用下面這個命令來查看這個錯誤:

sudo ./msgsendsnoop.d -p pgrep VCTransitions | grep invalid
讓我們用一個簡單的判斷句來修複一下那個bug.
在pid$target::objc_msgSend:entry後面添加一個判斷句:

pid$target::objc_msgSend:entry / arg0 > 0x100000000 /
這個判斷句的意思是說"如果第一個參數是nil或者這一段記憶體沒有被利用就不要運行這個DTrace動作".
通常情況下, 在macOS的使用者進程中, 這一段記憶體是不允許讀寫和執行的.如果那裡的數字小於0x100000000, DTrace就不會使用那段記憶體裡的資料.因此, 如果它小於那個數, Dtrace就會跳過它. 當然你也可以在LLDB中用下面這行代碼確認一下:

(lldb) image dump sections VCTransitions
在你閒置時候你可以確認一下.你仍然需要結束這個指令碼.

移除幹擾

老實說, 我不關心編譯器產生的記憶體管理的代碼. 也就是說我們要把retain或者release相關的代碼排除掉.
在你當前的探針上用新的從句建立一個新的DTrace探針:

pid$target::objc_msgSend:entry
{
this->selector = copyinstr(arg1);
}
/ old code below /
pid$target::objc_msgSend:entry / arg0 > 0x100000000 /
現在你在主從句跳過所有的記憶體邏輯之前在新的從句裡聲明了一個selector. 這會讓你在主從句的判斷句部分過濾Objective-C方法.
說到這兒, 現在主從句中判斷句的參數是:

pid$target::objc_msgSend:entry / arg0 > 0x100000000 / &&
this->selector != "retain" &&
this->selector != "release" /
現在會忽略所有與retain或release相等的代碼. 現在不需要在主從句中重新指定this->selector, 你再另外一個從句了已經做了. 儘管他不會造成壞的影響, 但它仍然是多餘的邏輯. 移除它, 或者如果你開心的話也可以不移除它.
你的兩個從句現在看起來應該是下面這個樣子:

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
圖片.png

哦 太棒了, 這次好多了!
但是這裡仍然有很多幹擾. 是時候將這個指令碼與LLDB聯合起來得到一些主執行檔案的相應輸出了.

用LLDB限定範圍

在starter檔案夾中有一個LLDB的python指令碼, 這個指令碼建立了一個DTrace指令碼然後用你剛才實現的邏輯運行這個指令碼.
你只可以在第一個地方使用這個指令碼.但是這個不太讓人開心.
這個檔案的名字叫做snoopie.py. 將這個檔案拷貝到你的~/lldb目錄下.如果你看過第二十二章“SB Examples, Improved Lookup”, 那麼在~/lldb目錄下應該有一個lldbinit.py檔案., 它會為你自動載入這個目錄下所有的指令碼.
如果你跳過了那一章, 那麼你則需要在你的~/.lldbinit檔案中假如下面這行代碼:

command script import ~/lldb/snoopie.py
你將會使用這個DTrace指令碼中僅僅值追只追蹤屬於VCTransitions可執行檔中的Objective-C/dynamic Swift 代碼的創造性解決方案過濾出相應代碼.通常情況下, 當窺探的代碼在framework中, 我經常會抓取載入到記憶體裡的TEXT 部分的模組然後對比TEXT前後的指令指標邊界(這一部分的記憶體負責執行代碼).如果這個指令指標在上下邊界之間然後你就可以假定你想要用Dtrace追蹤的代碼就是它.
不幸的是, 在objc_msgSend之後, 這個阻塞點會被應用到所有模組中的Objective-C代碼.這就意味著你可以依靠指令指標來告訴自己你在哪個模組中.
不然, 你就需要通過僅僅包含在主執行檔案中__DATA部分的一個類的獨立地址來得到自己當前在哪個模組中.
回到你之前的VCTransitions Xcodex項目裡.
構建並運行, 停止執行然後進到LLDB中. 然後輸入下面的內容:

(lldb) p/x (void *)NSClassFromString(@"ObjCViewController")
你將會得到ObjCViewController類的地址:

(void *) $0 = 0x000000010db34080
用這個地址檢查一下這些事在記憶體中的位置:

(lldb) image lookup -a 0x000000010db34080
你會得到一些類似下面的輸出:

Address: VCTransitions[0x0000000100012080]
(VCTransitions.DATA.objc_data + 40)
Summary: (void *)0x000000010db34058
因此, 你可以推斷出這個類在VCTransitions DATA部分裡的objc_data 子部分裡.
你將會使用LLDB Python模組來找出這個__DATA 分段的上下邊界.
現在你會用好用而古老的script命令來找出你怎樣通過LLDB建立這些代碼.
回到LLDB中, 輸入下面的內容:

(lldb) script path = lldb.target.executable.fullpath
這回給你一個代表可執行檔VCTransitions的SBFileSpec, 而且會將SBFileSpec複製給變數path.
列印出這個path確認它是有效:

(lldb) script path
你將會得到可執行檔的完整路徑.你可以用這個路徑從SBTarget中擷取到正確的SBModule.在LLDB中輸入下面的內容:

(lldb) script print lldb.target.module[path]
你將會獲得代表主可執行檔的SBModule.
在SBModule中有一個SBSections.你可以用sections屬性擷取到SBModule中所有的sections, 或者你可以用section[index]擷取摸個指定的section. 是的, 那個屬性遵循Python的getitem格式. 在LLDB中輸入下面的內容:

(lldb) script print lldb.target.module[path].section[0]
你將會得到一些類似下面的內容:

[0x0000000000000000-0x0000000100000000) VCTransitions.PAGEZERO
這種
getitem的實現也可以將SBSection作為一個目錄.因此你也可以訪問像下面這樣訪問PAGEZEROsection:

(lldb) script print lldb.target.module[path].section[‘PAGEZERO‘]
這就意味著你可以輕鬆的訪問
DATA SBSection像下面這樣:

(lldb) script print lldb.target.module[path].section[‘__DATA‘]
酷, 這是可行的. 將這個SBSection複製給一個叫section的變數, 像這樣:

(lldb) script section = lldb.target.module[path].section[‘__DATA‘]
現在你擁有了一個正確的section的引用. 這裡有一些你可以細分的subsections, 但是你可能想抓取完整的section, 因為他們在記憶體中是一個連續的地區.
從section中擷取load address, 想下面這樣:

(lldb) script section.GetLoadAddress(lldb.target)
這將會列印出起始位置.同時抓取你所在位置的size:

(lldb) script section.size
圖片.png

那麼這些內容給了你哪些資訊呢?你可以建立一個DTrace判斷句檢查一下這個類是否在記憶體中這些值的中間. 如果在, 執行這個Dtrace 動作.如果他們不在, 忽略.
讓我們動手把它實現出來!

修複snoopie指令碼

正如它表明的, 這個snoopie指令碼是可以啟動並執行, 因此你只需要添加一些邏輯判斷並過濾出執行個體.
開啟~/lldb/snoopie.py檔案, 然後找到generateDTraceScript函數.
移除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大的情況下解引用這個參數, 這表明記憶體中有一個有效執行個體.就是這樣!不需要更多的代碼!你已經做完了!
儲存一下你做的內容, 跳轉到LLDB控制台中, 在LLDB中要麼使用你自訂的reload_script命令要麼手動的輸入script import ~/.lldbinit重新載入這個內容.
載入成功之後, 在LLDB中, 試著運行一下:

(lldb) snoopie
將這個內容粘貼到終端視窗中並運行.
現在Dtrace只會解析你(精簡過的)主可執行檔中的代碼.

圖片.png

盡情的享受這個指令碼在你電腦上其他APP上的表現吧!

我們為什麼要學這些?

在最後會給你留一些作業.這個指令碼不能夠很好的適配Objective-C的分類. 例如, 這裡可能有一個類是在主檔案中用Objective-C的分類在不同的模組中實現的,. 你需要用一些創造性的方法檢查一下Objective-C selector 的 objc_msgSend是否在主可執行檔中實現了.
此外, 你當前代碼中的printf無法指明arg0是一個類方法或者不是一個類方法. 你需要進到記憶體中去弄清楚如何檢查arg0參數是一個類或者僅僅是一個執行個體.
你怎樣才能實現上面的內容呢?
? 如果arg0是一個類的執行個體, 那麼isa指標就會指向non-meta類.
? 如果arg0是一個類, 那麼isa指標就會指向meta類.
? 查看class_isMetaClass的彙編來弄明白一個類中的哪一個值表明了它是一個meta類或者不是一個meta類.
一旦你跳到記憶體中找到了決定一個類是不是meta類的方法, 複製你Dtrace指令碼中class_isMetaClass的邏輯.因為這可能是類的執行個體或者是類對象本身, 你可以在Dtrace指令碼中使用類似下面的三元運算子:

this->isMeta = ... // logic here
this->isMetaChar = this->isMeta ? ‘+‘ : ‘-‘
printf("0x%016p %c[%s %s]\n", arg0, this->isMetaChar,
this->classname,
this->selector);
額...isMetaChar. 在將來的某一天它會成為一個口袋妖怪(Pokémon)的名字.
祝你好運!

Advanced+Apple+Debugging(17)

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.