導(dǎo)讀:程序運(yùn)行過(guò)程中,有些數(shù)據(jù)被莫名修改了,在哪里修改的?又是怎么修改的?這個(gè)代碼我只想知道是否運(yùn)行過(guò),或者運(yùn)行了多少次,但是不想讓程序停下來(lái),或者僅打印調(diào)試信息,怎么辦?當(dāng)這個(gè)變量設(shè)置成某個(gè)數(shù)據(jù)后,我想讓程序自動(dòng)暫停下來(lái)進(jìn)行分析,怎么辦?
以上問(wèn)題的所有答案就在本節(jié)內(nèi)容:斷點(diǎn)窗口(KEIL)。
本節(jié)內(nèi)容將顛覆你之前對(duì)斷點(diǎn)調(diào)試的認(rèn)知。這個(gè)調(diào)試技巧魚鷹也用了半年多了,當(dāng)時(shí)知道這個(gè)調(diào)試方法的時(shí)候特別興奮,感覺(jué)發(fā)現(xiàn)了新大陸。而這個(gè)調(diào)試技巧也在魚鷹接手公司項(xiàng)目代碼的時(shí)候快速解決了不少疑難雜癥,而前些天又?jǐn)U展學(xué)習(xí)了這個(gè)技巧的功能,更是讓魚鷹在學(xué)會(huì)之后輕松解決了好幾個(gè)一般調(diào)試方法很難解決的 BUG,相信這個(gè)技巧也將為魚鷹之后的開(kāi)發(fā)調(diào)試之旅發(fā)揮更大的作用。
我們知道常規(guī)的斷點(diǎn)調(diào)試是在想觀察哪里的問(wèn)題時(shí)就在對(duì)應(yīng)的代碼地址設(shè)置斷點(diǎn),并且一旦運(yùn)行到斷點(diǎn)位置會(huì)讓程序自動(dòng)暫停運(yùn)行,這種斷點(diǎn)調(diào)試功能確實(shí)為開(kāi)發(fā)者解決 bug 立下了汗馬功勞,但是這種方式有很大的局限性,因?yàn)楹芏鄷r(shí)候我們并不需要讓程序停下來(lái),而只想知道是否在這段代碼運(yùn)行過(guò),或者說(shuō)發(fā)生問(wèn)題的位置根本不能停下來(lái),否則就會(huì)讓整個(gè)系統(tǒng)功能出現(xiàn)問(wèn)題,比如中斷處理函數(shù)的調(diào)試,程序一旦停下了也就失去了所有中斷的后續(xù)響應(yīng);比如兩個(gè)設(shè)備通信,一方采用常規(guī)斷點(diǎn)的方式調(diào)試,肯定會(huì)打斷正常的通信過(guò)程,而這可不是我們想要的,我們只想知道在收到或發(fā)送數(shù)據(jù)后得到環(huán)境快照,而并不想讓程序停下來(lái)。以上這些問(wèn)題可以采用打印方式解決,但是打印調(diào)試也有很多弊端:
以串口為例:
1、你必須添加必要的打印和串口驅(qū)動(dòng)代碼,如果你使用 printf 函數(shù),你還得重定向(如果對(duì)空間要求高的話,你得知道使用 printf 差不多要占用 1K 大小代碼空間)。
2、如果打印效率比較低,常規(guī)波特率 9600 和 115200 打印一個(gè)字符串耗時(shí)可能比較久,那么對(duì)于中斷頻率較高的函數(shù)就可能就不適用了。如果你使用 printf 函數(shù),你還得考慮函數(shù)是否可重入問(wèn)題。
3、在代碼中引入調(diào)試代碼有風(fēng)險(xiǎn),本來(lái)程序運(yùn)行沒(méi)有問(wèn)題的,一旦引入調(diào)試代碼之后可能就出現(xiàn)了問(wèn)題,這種情況對(duì)于擁有豐富開(kāi)發(fā)經(jīng)驗(yàn)的人來(lái)說(shuō)應(yīng)該見(jiàn)怪不怪了。原因就在于打印輸出時(shí)間太久,打亂了程序運(yùn)行的節(jié)奏(而這也是我推薦使用 ITM 調(diào)試的一個(gè)原因,因?yàn)樗妮敵鲂时却谝叩枚啵蛘叽蛴『瘮?shù)本身有問(wèn)題,也會(huì)導(dǎo)致程序運(yùn)行出現(xiàn)問(wèn)題。
4、調(diào)試完畢之后,你必須把對(duì)應(yīng)的調(diào)試代碼刪除(不管是刪除代碼還是使用宏,都要進(jìn)行這一步),不然會(huì)影響運(yùn)行效率。而人是健忘的(也不能說(shuō)健忘,可能只是因?yàn)閷W⒂?BUG 本身,容易忘記其它細(xì)枝末節(jié),而解決 bug 之后的欣喜更可能忘記后續(xù)處理工作了)這個(gè)時(shí)候你可以嘗試用 #warnning。但是這一步還是必不可少。
而以上問(wèn)題的解決方案就是 KEIL 的斷點(diǎn)調(diào)試窗口!
首先打開(kāi)數(shù)據(jù)觀察點(diǎn)的窗口:
快捷鍵是 Ctrl + B。
可以看到如下窗口:
當(dāng)然你也可以通過(guò)下面這種方式打開(kāi)并設(shè)置:
從這里你會(huì)發(fā)現(xiàn),其實(shí)這個(gè)窗口就是用來(lái)管理你設(shè)置的斷點(diǎn)的。平常使用的設(shè)置斷點(diǎn)方法只是其中的一種特例罷了。
首先要知道的就是,調(diào)試器支持的斷點(diǎn)數(shù)量是有限的,具體有多少視情況而定,一旦 KEIL 警告你設(shè)置斷點(diǎn)太多,那么就要?jiǎng)h除一些斷點(diǎn)了:
常規(guī)用法
1、代碼位置運(yùn)行次數(shù)
有些時(shí)候我們想知道某些代碼的運(yùn)行次數(shù),比如進(jìn)入中斷處理函數(shù)的次數(shù),尋常的斷點(diǎn)設(shè)置方式必然會(huì)讓程序停止在中斷程序中,但有些時(shí)候我們并不希望它停下來(lái)。這個(gè)時(shí)候,你只需要打開(kāi)該窗口,找到已有的對(duì)應(yīng)斷點(diǎn)位置,雙擊之后就可以看到類似下面的窗口:
此時(shí),你將 Count 的值設(shè)置的盡可能大一些,那么就可以讓程序運(yùn)行多次之后才停止。
比如我們?cè)O(shè)置 Count 的值為 100 次,那么必須在該代碼位置運(yùn)行 100 次才會(huì)讓程序暫停。當(dāng)你設(shè)置完后點(diǎn)擊【Define】后,就會(huì)詢問(wèn)你是否需要重新定義,你選擇“是”即可。
這樣你的斷點(diǎn)變成了這樣:
后面的 count=100 表示剩余運(yùn)行次數(shù)為 100,運(yùn)行 100 次后將停止程序。前面的 00 代表斷點(diǎn)號(hào),E 代表這是一個(gè)執(zhí)行斷點(diǎn),0x080016B0 代表代碼地址,后面的是源碼位置。
當(dāng)這個(gè)斷點(diǎn)位置運(yùn)行了 2 次,重新打開(kāi)該窗口(刷新數(shù)據(jù)),發(fā)現(xiàn)這個(gè)數(shù)變成了 98,從而可以推算出,已經(jīng)運(yùn)行了多少了。如果說(shuō)你想讓這段代碼運(yùn)行 2 次后停止,那么你只需要一開(kāi)始設(shè)置 Count 的值為 2 即可。
2、數(shù)據(jù)訪問(wèn)
有些時(shí)候我們需要知道一些變量會(huì)在哪里被訪問(wèn),那么你可以設(shè)置該變量的訪問(wèn)條件。比如魚鷹想知道 emOspery 變量會(huì)在哪里被讀取?那么你只需設(shè)置如下:
定義之后就是這樣:
因?yàn)?Count 值設(shè)置為 1,所以每一次讀取 emOspery 的操作都將使程序停止。比如這段代碼:
還有后面的打印函數(shù)也使用 emOsprey 變量,所以也會(huì)導(dǎo)致程序運(yùn)行停止。可能你會(huì)感到奇怪,為什么 emOsprey++這樣的操作也會(huì)涉及到讀取?事實(shí)上你理解了 CPU 寄存器存在的意義也就明白了。
而當(dāng)你設(shè)置為寫(Write)訪問(wèn)時(shí),你會(huì)發(fā)現(xiàn)從復(fù)位程序開(kāi)始運(yùn)行后,程序會(huì)停止在某個(gè)地方,這是為什么?當(dāng)你知道全局變量會(huì)在進(jìn)入 main 函數(shù)之前被初始化時(shí),你也就明白為什么了。
在這里我們選擇使用 Objects 訪問(wèn),即按整個(gè)變量對(duì)象進(jìn)行訪問(wèn),上面的 emOsprey 變量實(shí)際上是 uint16_t,所以 len 為 2,即字節(jié)大小。也就說(shuō),如果你設(shè)置為 Objects 訪問(wèn),那么它會(huì)根據(jù)實(shí)際的情況設(shè)置訪問(wèn)范圍。
為了更好的說(shuō)明這一點(diǎn),我構(gòu)造一個(gè)結(jié)構(gòu)體。
這個(gè)結(jié)構(gòu)體大小可以看出是 6 個(gè)字節(jié)。
然后設(shè)置訪問(wèn)該結(jié)構(gòu)體的條件:
如果我們按 Objects 訪問(wèn)的話,那么下面的每一條語(yǔ)句都會(huì)導(dǎo)致程序運(yùn)行的停止。
這是因?yàn)檫@些數(shù)據(jù)都在 Osprey 結(jié)構(gòu)體的范圍內(nèi)(從這里也可以了解到,只要在 len 的范圍內(nèi)的訪問(wèn)都會(huì)導(dǎo)致程序停止運(yùn)行,所以你可以試試將 Size 設(shè)置得更大)。
而如果設(shè)置為 Byte 訪問(wèn)的話,那么就只有第一條語(yǔ)句才會(huì)導(dǎo)致程序停止運(yùn)行:
實(shí)際上如果你希望只在某個(gè)結(jié)構(gòu)體成員變量被訪問(wèn)時(shí)才停止,那么直接這么設(shè)置就可以:
你會(huì)發(fā)現(xiàn)設(shè)置是如此之簡(jiǎn)單。
實(shí)際上還有一種更為通用的訪問(wèn)方式,即按地址訪問(wèn)。
上面可以看出 Ospery.Ospery1 成員變量的地址為 0x20000016(由此我們知道也可以通過(guò)這個(gè)來(lái)看出一個(gè)結(jié)構(gòu)體變量的地址是多少)。所以我們可以這樣設(shè)置:
而代碼位置的斷點(diǎn)設(shè)置亦是如此。
斷點(diǎn)太多,怎么知道程序因何停止?看你的命令窗口就知道了:
3、數(shù)據(jù)匹配
有些時(shí)候,我們并不關(guān)注地址訪問(wèn)情況,而對(duì)變量的數(shù)據(jù)內(nèi)容感興趣。比如說(shuō)魚鷹想讓變量emOspery 等于 1 時(shí)停下來(lái),怎么設(shè)置?
只要簡(jiǎn)單的設(shè)置 emOspery == 1 即可(注意必須設(shè)置訪問(wèn)條件,并且 Size 設(shè)置正確)。
事實(shí)上你也可以設(shè)置兩個(gè)變量相等作為條件:
設(shè)置為不等也是可以的:
當(dāng)然還有其它支持的運(yùn)算就靠你們自己去發(fā)現(xiàn)了(可支持運(yùn)算:&,&&,<,<=,>,>= ,==,!=)。
注意:以上內(nèi)容可以組合使用,比如讀、寫條件,計(jì)數(shù)器計(jì)數(shù)等可以同時(shí)設(shè)置。滿足條件時(shí)就會(huì)讓程序運(yùn)行停止。
高級(jí)用法
以上為比較常規(guī)的調(diào)試功能,現(xiàn)在說(shuō)說(shuō)魚鷹剛學(xué)習(xí)的技能,這個(gè)技能的使用靈活性更大,而且對(duì)于解決疑難雜癥更是不二之選。
首先設(shè)置一個(gè)你需要的斷點(diǎn):
打開(kāi)斷點(diǎn)窗口,并雙擊你之前設(shè)置的斷點(diǎn):
設(shè)置 Command 為【printf(“USRAT_Init()\n”)】(注意\n,否則可能不能輸出,這個(gè)應(yīng)該是 KEIL 的一個(gè) bug)。最后【Define】
清空你之前的命令(如果你不嫌亂的話,也可以不清空):
那么你的程序每次運(yùn)行到這個(gè)代碼位置都會(huì)在Command 窗口輸出一條信息:
但是你的程序并不會(huì)停止。
如果說(shuō)你想讓斷點(diǎn)代碼位置運(yùn)行多次之后才輸出一條信息也是可以的,只要設(shè)置Count 即可。
這里可能你會(huì)問(wèn),這 printf 不就是我們寫的打印函數(shù)嗎?事實(shí)上,是,也不是。
這個(gè)函數(shù)是打印函數(shù)沒(méi)錯(cuò),但是這是 KEIL 調(diào)用的打印函數(shù),輸出位置是 Command 窗口,和你自己寫的代碼沒(méi)一點(diǎn)關(guān)系,每次觸發(fā)條件時(shí)KEIL 都會(huì)調(diào)用該函數(shù)進(jìn)行打印,而不會(huì)讓你的程序暫停運(yùn)行。事實(shí)上這個(gè) Command 絕不僅僅只是設(shè)置 printf這么簡(jiǎn)單,如果真是這樣我也不會(huì)如此推崇它了,感興趣的可以去官網(wǎng)查找關(guān)于調(diào)試命令的使用方法。
因?yàn)槭抢?KEIL 去執(zhí)行打印任務(wù),所以對(duì)你的程序幾乎沒(méi)有任何影響,并且在你設(shè)置斷點(diǎn)后也不用擔(dān)心刪除代碼問(wèn)題,可以放心飲用。還有一個(gè)額外的好處就是,對(duì)于所有能設(shè)置調(diào)試斷點(diǎn)的單片機(jī)都適用,因此對(duì)于調(diào)試器也就沒(méi)有過(guò)多的要求了,比如說(shuō),不管你是用JLINK、ST-LINK 還是CMSIS-DAP(CMSIS-DAP 不能使用ITM,所以魚鷹才會(huì)想著用別的方式替代。總算是找到了,而且它在某些方面更出色),都可以這么用。
現(xiàn)在摘錄官網(wǎng)一些關(guān)于斷點(diǎn)窗口的知識(shí):
表達(dá)式定義斷點(diǎn)類型:
§當(dāng)設(shè)置標(biāo)志 **Read **或 **Write **或兩者時(shí),訪問(wèn)中斷(A)被定義 。發(fā)生指定的內(nèi)存訪問(wèn)時(shí)會(huì)觸發(fā)斷點(diǎn)。以字節(jié)為單位指定內(nèi)存訪問(wèn)窗口的大小,或者以表達(dá)式的對(duì)象大小指定。對(duì)于此斷點(diǎn)類型,**Expression **必須解析為內(nèi)存地址和內(nèi)存類型。允許的運(yùn)算符(&,&&,<。<=。>,> =,= =和!=)在程序執(zhí)行暫停或執(zhí)行**命令**之前比較變量值 。
§當(dāng)Expression解析為代碼地址時(shí),將執(zhí)行執(zhí)行中斷(E)。到達(dá)指定的代碼地址時(shí)觸發(fā)斷點(diǎn)。代碼地址必須引用 CPU 指令的第一個(gè)字節(jié)。
§當(dāng)Expression不能簡(jiǎn)化為地址時(shí),定義條件中斷(C)。當(dāng)條件表達(dá)式變?yōu)?TRUE 時(shí),斷點(diǎn)將觸發(fā)。在每條 CPU 指令之后重新計(jì)算條件表達(dá)式,并且會(huì)大大減慢程序執(zhí)行速度。
該計(jì)數(shù)值指定的次數(shù)的斷點(diǎn)表達(dá)式必須計(jì)算為 TRUE 斷點(diǎn)觸發(fā)之前的數(shù)目。
當(dāng)命令被指定的μVision 執(zhí)行語(yǔ)句,然后恢復(fù)執(zhí)行程序。此處指定的命令可以是μVision 調(diào)試或信號(hào)功能。要從這些函數(shù)中暫停程序執(zhí)行,請(qǐng)?jiān)O(shè)置系統(tǒng)變量break。
注意
當(dāng)在模擬器中將訪問(wèn)斷點(diǎn)(讀或?qū)懀┰O(shè)置為外設(shè)寄存器(SFR)時(shí),即使應(yīng)用程序未訪問(wèn)外設(shè)寄存器,斷點(diǎn)也可能觸發(fā)。發(fā)生這種情況是因?yàn)棣蘓ision 模擬器在應(yīng)用程序驅(qū)動(dòng)和模擬器內(nèi)部訪問(wèn)之間沒(méi)有區(qū)別。
里面有一個(gè)比較關(guān)鍵的就是關(guān)于條件中斷(C),如果你設(shè)置的表達(dá)式不是一個(gè)代碼地址,也沒(méi)有設(shè)置讀寫訪問(wèn)條件,那么就會(huì)被設(shè)置為條件中斷,一旦設(shè)置為條件中斷,那么會(huì)在每條匯編指令后計(jì)算表達(dá)式,這會(huì)影響程序正常運(yùn)行速度,所以沒(méi)有必要的話,不要設(shè)置為條件中斷。
設(shè)置斷點(diǎn)的一般錯(cuò)誤總結(jié):
當(dāng)彈出以下窗口時(shí),說(shuō)明斷點(diǎn)設(shè)置錯(cuò)誤,需要查看命令窗口才能知道具體錯(cuò)誤信息。
**a) **斷點(diǎn)太多
刪除一些斷點(diǎn)即可
**b) **重復(fù)定義斷點(diǎn)
這是因?yàn)橹澳阋呀?jīng)定義了這個(gè)斷點(diǎn),而現(xiàn)在你又定義了這個(gè)斷點(diǎn),這個(gè)時(shí)候你可以選擇覆蓋之前的斷點(diǎn)或者保留之前的斷點(diǎn)
**c) **不允許對(duì)同一個(gè)資源設(shè)置不同類型斷點(diǎn)
這個(gè)是由于對(duì)同一個(gè)資源準(zhǔn)備設(shè)置不同斷點(diǎn)導(dǎo)致的,需要?jiǎng)h除之前的設(shè)置的斷點(diǎn)才行。
**d) **表達(dá)式錯(cuò)誤
檢查你的表達(dá)式是否正確,注意如果你使用了運(yùn)算符,那么對(duì)于浮點(diǎn)變量的支持好像并不正常,不管你怎么設(shè)置,都說(shuō)表達(dá)式錯(cuò)誤。
到此,斷點(diǎn)窗口(前期我叫它數(shù)據(jù)觀察點(diǎn),我也不知道從哪看到的這個(gè)詞,后來(lái)覺(jué)得還是斷點(diǎn)窗口比較準(zhǔn)確)的內(nèi)容就結(jié)束了。這個(gè)小節(jié)內(nèi)容對(duì)于調(diào)試而言絕對(duì)是一大利器,也是魚鷹決定寫這個(gè)KEIL 調(diào)試系列文章的主要原因。但是以上所有的調(diào)試內(nèi)容都有一個(gè)很大的局限性,就是它只能定格在某一刻(如果你使用Command 命令就不一樣了),而這一刻前面的所有信息都無(wú)法知曉。這個(gè)時(shí)候就要了解另一個(gè)調(diào)試技能,ITM,它能將程序從出生(復(fù)位程序開(kāi)始)到死亡(死循環(huán)或者斷電)的大部分信息記錄下來(lái)。這個(gè)章節(jié)內(nèi)容早已發(fā)布,感興趣的就去前面看一看咯。
審核編輯:劉清
-
寄存器
+關(guān)注
關(guān)注
31文章
5425瀏覽量
123652 -
計(jì)數(shù)器
+關(guān)注
關(guān)注
32文章
2288瀏覽量
96122 -
CMSIS
+關(guān)注
關(guān)注
0文章
41瀏覽量
12259 -
調(diào)試器
+關(guān)注
關(guān)注
1文章
312瀏覽量
24210 -
串口驅(qū)動(dòng)
+關(guān)注
關(guān)注
2文章
86瀏覽量
19058
原文標(biāo)題:數(shù)據(jù)被篡改了,無(wú)法在線調(diào)試該怎么定位?
文章出處:【微信號(hào):eOsprey,微信公眾號(hào):魚鷹談Linux】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
CAN發(fā)送接收過(guò)程中收到到的數(shù)據(jù)過(guò)大怎么辦??
認(rèn)證名字寫錯(cuò)了 無(wú)法修改了 怎么辦
MSP430F149 芯片運(yùn)行過(guò)程中Flash被更改
STM32在調(diào)試程序的過(guò)程中發(fā)現(xiàn)按鍵切換會(huì)導(dǎo)致程序卡死怎么辦?
嵌入式中的變量被意外修改該怎么辦呢
程序運(yùn)行過(guò)程中可以直接調(diào)用rt_device_control修改串口屬性嗎
RT-Thread studio在仿真過(guò)程中想實(shí)現(xiàn)程序的復(fù)位該怎么辦
命令提示符被禁用怎么辦
Python如何防止數(shù)據(jù)被修改Python中的深拷貝與淺拷貝的問(wèn)題說(shuō)明

內(nèi)存被修改了怎么辦
內(nèi)存卡被寫保護(hù)怎么辦
原來(lái)單片機(jī)中還隱藏了這些調(diào)試技巧 | 顛覆認(rèn)知

水泵控制使用過(guò)程中出現(xiàn)信號(hào)報(bào)警怎么辦

在焊接過(guò)程中發(fā)現(xiàn)錫膏太稀怎么辦?

電力補(bǔ)償電容器運(yùn)行過(guò)程中缺相怎么辦?

評(píng)論