iOS/OSX 調試:跳舞吧!與LLDB共舞華爾茲

原文連接:http://www.objc.io/issue-19/lldb-debugging.htmlhtml

// 速翻,無校對版

前言

你是否嘔心瀝血的嘗試去理解代碼和打印出來的變量內容?git

NSLog(@"%@", whatIsInsideThisThing);

或是漏過函數調用來就簡化工程行爲?github

NSNumber *n = @7; // theFunctionThatShouldReallyBeCalled();

或者短路的檢查邏輯?shell

if (1 || theBooleanAtStake) { ... }

亦或者是函數的僞實現?express

int calculateTheTrickyValue {
  return 9;

  /*
   Figure this out later.
   ...
}

那是否是要不斷的重編譯,而後又開始新的輪迴?數組

構建軟件是複雜的並且BUG無處不藏。一個正常的修正過程是修改代碼,編譯,再次運行,而後祈禱上帝。數據結構

彷佛也不用墨守成規。你能夠用調試器啊!假設你已經知道怎麼檢視變量值,這裏有更多你須要掌握的東西。架構

這篇文章的目的是挑戰你的調試知識,把你可能知道得基礎知識點解析的更透徹,而後向你展現了一系列有趣的栗子。開始吧!ide

LLDB

LLDB是個開源調試器,REPL特性,自帶C++以及Python插件。它與Xcode綁定而且駐在控制檯界面化於窗口的下端。函數

調試器容許你在一個特定執行時刻暫停程序,檢視變量值,執行自定義命令,以及按你認爲合適得步驟進行程序步驟操控。(調試器主要功能戳這裏

你之前使用調試器的部分極可能僅限於Xcode的UI上打個斷點。可是這有些技巧,你能夠作一些更酷比的事情。經過GDB與LLDB之間對比是針對全部支持的命令行的一個很好鳥瞰式的學習法,你還可能想要去安裝Chisel,一套開源的LLDB插件讓你的調試更加有趣。

與此同時,讓咱們開始如何使用調試器打印變量值的旅程吧。

基礎

這裏有一個簡單短小的程序來打印字符串。注意到斷點被添加到了第八行:
圖片描述
程序到此會停下來而後打開控制檯,讓咱們能與調試器進行交互。此時咱們應該輸入什麼呢?

幫助

最簡單得命令是鍵入help,你能夠獲取一個命令行列表。若是你忘記一個命令或者想知道該命令更細緻的使用方法,那麼你能夠經過調用help <command>,好比help printhelp thread。若是你甚至忘記了命令自己,你能夠嘗試使用help help,可是若是你懂得足夠多,你可能已經完全不要這個命令了。

打印

打印值很容易,只要試着鍵入print命令:
圖片描述
LLDB實際上支持前綴命令判斷,因此你一樣可使用prin, pri或者p。可是你不能使用pr,由於LLDB不能分辨出你是不是想執行process命令。(吐槽幸虧p沒有歧義,暴露屬性)

你同時也注意到告終果帶一個$0。實際上你能夠用這個來引用變量!試着鍵入$0 + 7而後你會看到106。任何帶美圓符號是LLDB的命名空間,其存在是爲了爲你提供幫助。

表達式

若是你想修改一個值?修改,你說的算?好吧,修改!下面來一個簡單得表達式命令行:
圖片描述
這並不修改調試器中的值。實際上修改的是程序中的值!若是你繼續程序,它很神奇地會打印出42紅氣球(上下文)。

從如今開始注意一點,咱們爲了方便用pe代替printexpression

什麼是打印命令?

這裏有一個有意思的表達式來考慮下:p count = 18。若是咱們執行命令而後打印count的內容,咱們會看到它確實至關於執行了表達式count = 18

這二者的區別是print命令不帶參數,這點與expression不一樣。考慮e -h +17。在選擇是否要進行輸入源爲+17,帶-h標誌的操做,仍是選擇是否要進行計算區分17h操做,在這兩個選擇上面是不明確的。調試器認爲連字符致使了混淆,你可能得不到想要的結果。

幸運的是,這個解決方法十分簡單。使用--來表示表示符號的結束以及輸入源的開始。此時若是你想要用-h標誌,你可使用e -h -- +17,若是你想要進行區分,則你能夠執行e -- -h +17。不帶標誌則是十分普通,它(e --)有一個別名print

若是你鍵入help print而且往下拖拽,你會看到:

'print' is an abbreviation for 'expression --'.

打印對象

若是咱們嘗試鍵入

p objects

那輸出會有點冗繁:

(NSString *) $7 = 0x0000000104da4040 @"red balloons"

當嘗試打印一個更加複雜的數據結構時候會狀況會更糟:

(lldb) p @[ @"foo", @"bar" ]

(NSArray *) $8 = 0x00007fdb9b71b3e0 @"2 objects"

好吧,咱們想看下對象的description方法。咱們須要告訴expression命令做爲對象來打印這個結果,使用-O標誌(這不是0):

(lldb) e -O -- $8
<__NSArrayI 0x7fdb9b71b3e0>(
foo,
bar
)

很走運,e -O --也有別名,其別名爲po,咱們能夠只要這樣使用:

(lldb) po $8
<__NSArrayI 0x7fdb9b71b3e0>(
foo,
bar
)
(lldb) po @"lunar"
lunar
(lldb) p @"lunar"
(NSString *) $13 = 0x00007fdb9d0003b0 @"lunar"

打印變量

print命令有許多種不一樣的格式能夠由你來指定。它們以命令格式爲print/<fmt>或者更簡單p/<fmt>。接下來舉個栗子。

默認的格式:

(lldb) p 16
16

16進制格式:

(lldb) p/x 16
0x10

二進制格式(t表明tow):

(lldb) p/t 16
0b00000000000000000000000000010000
(lldb) p/t (char)16
0b00010000

你還可使用p/c打印字符,或者是p/s打印一個非終止類型的字符串char *。完整列表戳這裏

變量

至此你能夠打印對象跟簡單得類型,並能夠在調試器中使用expression命令更改它們的值,讓咱們使用一些變量來減小咱們輸入工做。你能夠聲明一個變量C來表示int a = 0,一樣你能夠在LLDB中作一樣的事情。而後,變量必須以美圓符號做爲開頭:

(lldb) e int $a = 2
(lldb) p $a * 19
38
(lldb) e NSArray *$array = @[ @"Saturday", @"Sunday", @"Monday" ]
(lldb) p [$array count]
2
(lldb) po [[$array objectAtIndex:0] uppercaseString]
SATURDAY
(lldb) p [[$array objectAtIndex:$a] characterAtIndex:0]
error: no known method '-characterAtIndex:'; cast the message send to the method's return type
error: 1 errors parsing expression

噢。LLDB不能識別出所牽扯的變量類型。不時會遇到,咱們能夠給一點提示:

(lldb) p (char)[[$array objectAtIndex:$a] characterAtIndex:0]
'M'
(lldb) p/d (char)[[$array objectAtIndex:$a] characterAtIndex:0]
77

變量特性讓調試器更容易被使用,你這麼認爲嗎?

流程控制

你的程序會在你打上斷點的位置停下來。

此時你看到在調試工具欄有四個按鈕,經過使用它們你能夠控制程序的執行流程:
圖片描述

這四個按鈕從左到右依次爲:繼續,單步,跳入,跳出。

首先,繼續按鈕將會讓你得程序繼續正常執行(可能一直運行或者遇到下一個斷點)。在LLDB中,你可使用process continue來繼續執行,別名爲c

其次,單步執行將會將單行代碼當作黑盒同樣執行。若是那行你調用了函數,那將不會進入這個函數,而是直接執行這個函數後繼續運行。LLDB中相對應的命令是thread step-overnext,或者 n

若是你想進入一個函數調用來檢查調試該函數的執行,你可使用第三個按鈕,跳入,LLDB一樣提供了thread step-instep, 和s。注意到nextstep在當前行代碼不涉及函數調用的時候效果是同樣的。

大部分知道c,n,s。可是還有第四個按鈕,跳出。若是你不當心跳入了一個函數而你本意是想跳過它,通常反應是不斷的按n知道函數返回。跳出幫你節省時間。它會執行到return語句(知道執行了出棧操做),而後會停下來。

舉個栗子

來看下以下的代碼片斷:
圖片描述

代碼停在斷點,而後咱們執行以下的命令行:

p i
n
s
p i
finish
p i
frame info

這裏,frame info將會告訴你當前行以及源文件是啥,能夠經過鍵入help framehelp thread,以及help process獲取更多信息。那麼輸出什麼呢?先思考以前的描述想下答案!

(lldb) p i
(int) $0 = 99
(lldb) n
2014-11-22 10:49:26.445 DebuggerDance[60182:4832768] 101 is odd!
(lldb) s
(lldb) p i
(int) $2 = 110
(lldb) finish
2014-11-22 10:49:35.978 DebuggerDance[60182:4832768] 110 is even!
(lldb) p i
(int) $4 = 99
(lldb) frame info
frame #0: 0x000000010a53bcd4 DebuggerDance`main + 68 at main.m:17

仍在17行的緣由是finish命令會讓程序運行直到isEven()函數返回,而後立刻中止。可是請注意,17行已經執行完了。

線程返回

還有一個特別幫的功能是你在調試的時候能夠用thread return來控制程序流程。它使用可選參數,將這個參數載入寄存器,單後立刻執行返回命令,而後函數出棧。這意味着剩下函數沒有被執行。這樣由於ARC的引用計數/記錄出現問題,或者遺漏一些清除操做。但在一個函數的開頭執行這個命令是一個很是棒得函數打樁而且反悔了一個僞結果。

讓咱們來對上述相同的代碼段跑以下的指令:

p i
s
thread return NO
n
p even0
frame info

在看答案以前鄉下結果,答案以下:

(lldb) p i
(int) $0 = 99
(lldb) s
(lldb) thread return NO
(lldb) n
(lldb) p even0
(BOOL) $2 = NO
(lldb) frame info
frame #0: 0x00000001009a5cc4 DebuggerDance`main + 52 at main.m:17

斷點

咱們一直都使用斷點來讓程序中止,檢視當前狀態從而捕獲BUG。可是若是咱們轉變對斷點的理解,咱們能夠得到更多可能。

A breakpoint allows you to instruct a program when to stop, and then allows the running of commands.

考慮在函數剛開始處打一個斷點,使用thread return來重寫函數行爲,而後繼續。如今想象下自動實現這種處理。是否是聽起來很牛X,不是麼?

斷點管理

Xcode提供了一套工具來建立和操做斷點。咱們將會逐一過一遍而且進行描述與之對應的LLDB命令行。

在Xcode的左面板上,有一堆按鈕集合。有一個長得很像斷點。點擊打開斷點導航欄,進去以後你一眼看到你所操做的全部斷點:
圖片描述

這裏你能夠看到全部的斷點 - 對應LLDB中的breakpoint list或者是br li。你能夠點擊單個斷點進行打開或者關閉 - 對應LLDB中的breakpoint enable <breakpointID>breakpoint disable <breakpointID>

(lldb) br li
Current breakpoints:
1: file = '/Users/arig/Desktop/DebuggerDance/DebuggerDance/main.m', line = 16, locations = 1, resolved = 1, hit count = 1

  1.1: where = DebuggerDance`main + 27 at main.m:16, address = 0x000000010a3f6cab, resolved, hit count = 1

(lldb) br dis 1
1 breakpoints disabled.
(lldb) br li
Current breakpoints:
1: file = '/Users/arig/Desktop/DebuggerDance/DebuggerDance/main.m', line = 16, locations = 1 Options: disabled

  1.1: where = DebuggerDance`main + 27 at main.m:16, address = 0x000000010a3f6cab, unresolved, hit count = 1

(lldb) br del 1
1 breakpoints deleted; 0 breakpoint locations disabled.
(lldb) br li
No breakpoints currently set.

建立斷點

(UI建立略了。。。是人都會吧。。)

在調試器中打斷點,使用breakpoint set命令:

(lldb) breakpoint set -f main.m -l 16
Breakpoint 1: where = DebuggerDance`main + 27 at main.m:16, address = 0x

縮寫能夠用brb是另一個徹底不一樣的命令,是_regexp-break的別名,可是它足夠健壯來進行建立上述命令同樣效果的斷點:

(lldb) b main.m:17
Breakpoint 2: where = DebuggerDance`main + 52 at main.m:17, address = 0x

你也能夠防止一個斷點在一個符號(C語言函數),而不用指定行數:

(lldb) b isEven
Breakpoint 3: where = DebuggerDance`isEven + 16 at main.m:4, address = 0x000000010a3f6d00
(lldb) br s -F isEven
Breakpoint 4: where = DebuggerDance`isEven + 16 at main.m:4, address

如今這些斷點會中止正在將要執行的函數,一樣適用與OC方法:

(lldb) breakpoint set -F "-[NSArray objectAtIndex:]"
Breakpoint 5: where = CoreFoundation`-[NSArray objectAtIndex:], address = 0x000000010ac7a950
(lldb) b -[NSArray objectAtIndex:]
Breakpoint 6: where = CoreFoundation`-[NSArray objectAtIndex:], address = 0x000000010ac7a950
(lldb) breakpoint set -F "+[NSSet setWithObject:]"
Breakpoint 7: where = CoreFoundation`+[NSSet setWithObject:], address = 0x000000010abd3820
(lldb) b +[NSSet setWithObject:]
Breakpoint 8: where = CoreFoundation`+[NSSet setWithObject:], address = 0x000000010abd3820

若是你想經過UI來建立象徵性斷點,你能夠點擊左下端斷點導航欄的+號:
圖片描述

而後選擇第三個選項:
圖片描述
此時出現彈出框讓你輸入好比-[NSArray objectAtIndex:]的符號,而後程序在這個函數調用的時候即可以中止下來,不論是你的代碼或者仍是大蘋果的代碼!

若是咱們看下其餘選項,咱們能夠發現一些有意思的選項,一樣提供了各類條件觸發的鍛鍊只要你點擊了Xcode的UI而且選擇了「Edit Breakpoint」選項:
圖片描述

如上圖,斷點只有在i爲99的時候纔會中止程序。你能夠一樣設置「 ignore」選項來告訴斷點在前n次調用的時候不用中止程序(條件爲真)。

這裏還有一個「Add Action」按鈕。。。

斷點動做

可能上面斷點的栗子中,你想知道每次斷點時候i值是多少。咱們可使用動做p i,而後當斷點觸發的時候咱們進入調試器,它會預先執行這個命令在將控制流程交給你以前:
圖片描述
你也能夠加多重動做,能夠是調試器指令,shell指令或者更健壯的打印信息:
圖片描述

如上你能夠看到打印出i值,還有強調語句,打印出自定義的表達式。

下面是上述功能用純LLDB命令代替Xcode的UI:

(lldb) breakpoint set -F isEven
Breakpoint 1: where = DebuggerDance`isEven + 16 at main.m:4, address = 0x00000001083b5d00
(lldb) breakpoint modify -c 'i == 99' 1
(lldb) breakpoint command add 1
Enter your debugger command(s).  Type 'DONE' to end.
> p i
> DONE
(lldb) br li 1
1: name = 'isEven', locations = 1, resolved = 1, hit count = 0
    Breakpoint commands:
      p i

Condition: i == 99

  1.1: where = DebuggerDance`isEven + 16 at main.m:4, address = 0x00000001083b5d00, resolved, hit count = 0

自動化,咱們來了!

計算值以後繼續

若是視線停留在斷點彈出框的底端,你會額外看到一個選項:「Automatically continue after evaluation actions(計算動做後自動執行)。」它只是一個勾選框,可是它卻有強大的能力。若是你勾選上了,調試器將會蘋果你全部的命令而後繼續執行程序。表面上看上跟斷點沒有打住同樣(除非你斷點太多了,拖慢了程序進度)。

這個勾選框功能與最後一個動做斷點繼續執行效果同樣,可是有勾選框更加容易點。對應調試器的指令以下:

(lldb) breakpoint set -F isEven
Breakpoint 1: where = DebuggerDance`isEven + 16 at main.m:4, address = 0x00000001083b5d00
(lldb) breakpoint command add 1
Enter your debugger command(s).  Type 'DONE' to end.
> continue
> DONE
(lldb) br li 1
1: name = 'isEven', locations = 1, resolved = 1, hit count = 0
    Breakpoint commands:
      continue

  1.1: where = DebuggerDance`isEven + 16 at main.m:4, address = 0x00000001083b5d00, resolved, hit count = 0

計算後自動繼續運行讓你能夠單獨經過使用斷點來修改你的程序!你能夠中止在單行,運行一個expression命令來改變變量,而後繼續。

舉個栗子

考慮下簡陋殘酷的「打印式調試」技術。不是用:

NSLog(@"%@", whatIsInsideThisThing);

而是用斷點處設置打印變量值替代吊打印日誌打印語句而後繼續。

不是用:

int calculateTheTrickyValue {
  return 9;

  /*
   Figure this out later.
   ...
  */
}

而是用斷點處調用thread return 9而後繼續執行。

帶動做的象徵斷點確實真的很強大。你也能夠添加這些斷點到你朋友的Xcode工程而且讓動做將全部信息細緻展現出來。接下來看看要耗時多久來進行計算以及會發生什麼吧。

調試器完整操做

在起舞以前還有一點須要咱們注意。你真的能夠在調試器中執行任何的C/OC/C++/Swift命令。比較弱的是咱們不能建立一個新的函數。。。這意味着沒有新的類,塊,函數,帶虛方法的C++類等等。除了這個,調試器什麼都能知足!

咱們能夠分配一些字節:

(lldb) e char *$str = (char *)malloc(8)
(lldb) e (void)strcpy($str, "munkeys")
(lldb) e $str[1] = 'o'
(char) $0 = 'o'
(lldb) p $str
(char *) $str = 0x00007fd04a900040 "monkeys"

或者咱們能夠檢查一些內存(使用x命令)來看咱們新數組的4個字節:

(lldb) x/4c $str
0x7fd04a900040: monk

咱們還能夠後三個字節:

(lldb) x/1w `$str + 3`
0x7fd04a900043: keys

當你所要的活結束的時候別忘記了釋放內存避免形成內存泄露:

(lldb) e (void)free($str)

跳舞吧,騷年!

如今咱們已經清楚基礎步驟,是時候來整一些比較瘋狂的東西了。我過去曾寫過一篇博客(你們本身收藏。。。)發表在looking at the internals of NSArray。當時用了大量的NSLog語句,後來全用調試器搞定了。它是一個很好的調試器使用練習。

暢通無阻(無斷點模式)

當你的應用在跑的時候,Xcode中的調試工具欄展現一箇中止按鈕而非繼續狀態的按鈕:
圖片描述

選中這個按鈕的時候,應用遇到斷點將會中止(就像輸入了process interrupt)。這時候將會讓你進入調試器。

這裏有一個有趣的地方。若是你運行一個iOS應用,你能夠嘗試這個(全局變量可提供)

(lldb) po [[[UIApplication sharedApplication] keyWindow] recursiveDescription]
<UIWindow: 0x7f82b1fa8140; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0x7f82b1fa92d0>; layer = <UIWindowLayer: 0x7f82b1fa8400>>
   | <UIView: 0x7f82b1d01fd0; frame = (0 0; 320 568); autoresize = W+H; layer = <CALayer: 0x7f82b1e2e0a0>>

能夠看到整個層級!Chisel(上文說起)用pviews來實現。

更新UI

而後,經過上述的輸出,咱們能夠看到隱藏的視圖:

(lldb) e id $myView = (id)0x7f82b1d01fd0

而後在調試器中修改它的背景色:

(lldb) e (void)[$myView setBackgroundColor:[UIColor blueColor]]

在你下次繼續運行這個程序的時候你纔會看到變化。這由於這個變化須要傳遞給渲染服務而後視圖展現纔會被更新。

渲染服務其實是另外一個進程(稱做後臺),而且甚至咱們調試進程被中止了,這個後臺也不會被中止!

這意味着不經過繼續,你能夠執行:

(lldb) e (void)[CATransaction flush]

在模擬器中或者設備中的UI會進行刷新而你還在調試器中!Chisel提供了一個別名函數叫作caflush,而且它被用來實現其它捷徑像hide <view>show <view>還有其餘許多許多。全部的Chisel命令都有對應的文檔,因此就在安裝它以後鍵入help來爲所欲爲的獲取更多的信息吧。

壓入視圖控制器

想象一個簡單的應用有一個UINavigationController做爲根視圖控制器。你能夠在調試器中至關簡易的執行以下操做:

(lldb) e id $nvc = [[[UIApplication sharedApplication] keyWindow] rootViewController]

而後壓入子視圖控制器:

(lldb) e id $vc = [UIViewController new]
(lldb) e (void)[[$vc view] setBackgroundColor:[UIColor yellowColor]]
(lldb) e (void)[$vc setTitle:@"Yay!"]
(lldb) e (void)[$nvc pushViewContoller:$vc animated:YES]

最後執行:

(lldb) caflush // e (void)[CATransaction flush]

你會看到立刻壓入了一個視圖控制器。

找到按鈕的目標

想象下你調試器中有一個變量,$myButton,你想要去建立它,並從UI中抓取它,或者簡單地只是你想在斷點停下來的時候將它做爲個局部變量。你可能想知道當你點擊它的時候是誰接收了這個動做。這裏展現達到這點有多麼的簡單:

(lldb) po [$myButton allTargets]
{(
    <MagicEventListener: 0x7fb58bd2e240>
)}
(lldb) po [$myButton actionsForTarget:(id)0x7fb58bd2e240 forControlEvent:0]
<__NSArrayM 0x7fb58bd2aa40>(
_handleTap:
)

如今你可能想在事件發生的時候添加一個斷點。只要在LLDB或者Xcode設置象徵性斷點在-[MyEventListener _handleTap:]。and you are all set to go!

觀察實例變量值變化

想象一個假設的場景你有一個UIView且它的_layer實例變量被重寫了。由於這裏可能不涉及方法,咱們不能使用象徵性斷點。取而代之的是咱們想觀察一個內存地址何時被寫入了。

首先咱們須要找到_layer對象在那裏:

(lldb) p (ptrdiff_t)ivar_getOffset((struct Ivar *)class_getInstanceVariable([MyView class], "_layer"))
(ptrdiff_t) $0 = 8

如今咱們知道($myView + 8)這個內存地址被寫入了:

(lldb) watchpoint set expression -- (int *)$myView + 8
Watchpoint created: Watchpoint 3: addr = 0x7fa554231340 size = 8 state = enabled type = w
    new value: 0x0000000000000000

對應Chisel裏面的wivar $myView _layer

在非重寫方法上的象徵性斷點

想象你想知道何時-[MyViewController viewDidAppear:]被調用了。若是MyViewController實際上沒有實現這個方法,可是父類實現了呢?咱們能夠設置一個斷點來看看具體狀況:

(lldb) b -[MyViewController viewDidAppear:]
Breakpoint 1: no locations (pending).
WARNING:  Unable to resolve breakpoint to any actual locations.

由於LLDB根據符號搜索,它找不到該方法,因此你的斷點將不會被觸發。你所須要作的是設置一個條件,[self isKindofClass:[MyViewController class]],而後見這個斷點設在UIViewController上。通常來講,設置一個這樣的條件是有效的,可是,這裏無效是由於咱們沒有父類該方法的實現。

viewDidAppear:是大蘋果寫的,因此沒有對應的符號;在方法內部也沒有self。若是你想要使用在象徵性斷點內使用self,你須要知道它在那裏(可能在寄存器也可能在棧上;在x86你可能在$esp+4找到它)。這是個經過的歷程,由於你知道已經知道有四種體系架構了。吐槽略。。幸運的是,Chisel已經完成了這些封裝,你能夠調用bmessage

(lldb) bmessage -[MyViewController viewDidAppear:]
Setting a breakpoint at -[UIViewController viewDidAppear:] with condition (void*)object_getClass((id)$rdi) == 0x000000010e2f4d28
Breakpoint 1: where = UIKit`-[UIViewController viewDidAppear:], address = 0x000000010e11533c

LLDB與Python

LLDB有完整的內置Python支持。若是你在LLDB上輸入腳本,它會打開一個Python REPL。若是你在LLDB中鍵入script,它會打開一個Python REPL。你能夠傳入一行Python語句到script命令來不進入REPL的狀況下進行執行腳本:

(lldb) script import os
(lldb) script os.system("open http://www.objc.io/")

這容許你建立各類各樣的酷比命令。將這個丟入文件,~/myCommands.py:

def caflushCommand(debugger, command, result, internal_dict):
  debugger.HandleCommand("e (void)[CATransaction flush]")

而後在LLDB中運行以下:

command script import ~/myCommands.py

或者,將這行代碼放置於/.lldbinit讓LLDB每次運行的時候都執行一次。Chisel不過就是一堆Python腳本用來組合字符串,而後告訴LLDB來執行這些字符串。聽起來很簡單吧!呃?

馳騁調試器

略。。。 「樂觀」調試!

相關文章
相關標籤/搜索