如何調(diào)試程序中存在錯(cuò)誤或CPU內(nèi)部發(fā)生的錯(cuò)誤?
如果把程序(program)中的每一條指令看作電影膠片的一幀,那么執(zhí)行程序的CPU就像一臺(tái)飛速運(yùn)轉(zhuǎn)的放映機(jī)。以英特爾P6系列CPU為例,其處理能力大約在300(一代產(chǎn)品Pentium Pro)~3000(奔騰III)MIPS。MIPS的含義是CPU每秒鐘能執(zhí)行的指令數(shù)(以百萬(wàn)指令為單位)。如果按3000MIPS計(jì)算,那么意味著每秒鐘大約有30億條指令“流”過(guò)這臺(tái)高速的“放映機(jī)”。這大約是電影膠片放映速度(24幀每秒)的1.25億倍。如此高的執(zhí)行速度,如果在程序中存在錯(cuò)誤或CPU內(nèi)部發(fā)生了錯(cuò)誤,該如何調(diào)試呢?
CPU的設(shè)計(jì)者們一開始就考慮到了這個(gè)問(wèn)題—— 如何在CPU中包含對(duì)調(diào)試的支持。就像在制作電影過(guò)程中人們可以慢速放映或停下來(lái)分析每一幀一樣,CPU也提供了一系列機(jī)制,允許一條一條地執(zhí)行指令,或者使其停在指定的位置。
以英特爾的IA結(jié)構(gòu)CPU為例,其提供的調(diào)試支持如下。
- INT 3指令:又叫斷點(diǎn)指令,當(dāng)CPU執(zhí)行到該指令時(shí)便會(huì)產(chǎn)生斷點(diǎn)異常,以便中斷到調(diào)試器程序。INT 3指令是軟件斷點(diǎn)的實(shí)現(xiàn)基礎(chǔ)。
- 標(biāo)志寄存器(EFLAGS)中的TF標(biāo)志:陷阱標(biāo)志位,當(dāng)該標(biāo)志為1時(shí),CPU每執(zhí)行完一條指令就產(chǎn)生調(diào)試異常。陷阱標(biāo)志位是單步執(zhí)行的實(shí)現(xiàn)基礎(chǔ)。
- 調(diào)試寄存器DR0~DR7:用于設(shè)置硬件斷點(diǎn)和報(bào)告調(diào)試異常的細(xì)節(jié)。
- 斷點(diǎn)異常(#BP):INT 3指令執(zhí)行時(shí)會(huì)導(dǎo)致此異常,CPU轉(zhuǎn)到該異常的處理例程。異常處理例程會(huì)進(jìn)一步將異常分發(fā)給調(diào)試器軟件。
- 調(diào)試異常(#DB):當(dāng)除INT 3指令以外的調(diào)試事件發(fā)生時(shí),會(huì)導(dǎo)致此異常。
- 任務(wù)狀態(tài)段(TSS)的T標(biāo)志:任務(wù)陷阱標(biāo)志,當(dāng)切換到設(shè)置了T標(biāo)志的任務(wù)時(shí),CPU會(huì)產(chǎn)生調(diào)試異常,中斷到調(diào)試器。
- 分支記錄機(jī)制:用來(lái)記錄上一個(gè)分支、中斷和異常的地址等信息。
- 性能監(jiān)視:用于監(jiān)視和優(yōu)化CPU及軟件的執(zhí)行效率。
- JTAG支持:可以與JTAG調(diào)試器一起工作來(lái)調(diào)試單獨(dú)靠軟件調(diào)試器無(wú)法調(diào)試的問(wèn)題。
除了對(duì)調(diào)試功能的直接支持,CPU的很多核心機(jī)制也為實(shí)現(xiàn)調(diào)試功能提供了硬件基礎(chǔ),比如異常機(jī)制、保護(hù)模式和性能監(jiān)視功能等。
CPU是Central Processing Unit的縮寫,即中央處理單元,或者叫中央處理器,有時(shí)也簡(jiǎn)稱為處理器(processor)。頭一款集成在單一芯片上的CPU是英特爾公司于1969年開始設(shè)計(jì)并于1971年推出的4004,與當(dāng)時(shí)的其他CPU相比,它的體積可算是微乎其微,因此,人們把這種實(shí)現(xiàn)在單一芯片上的CPU(Single-chip CPU)稱為微處理器(microprocessor)。目前,絕大多數(shù)(即使不是全部)CPU都是集成在單一芯片上的,甚至多核技術(shù)還把多個(gè)CPU內(nèi)核(core)集成在一塊芯片上,因此微處理器和處理器這兩個(gè)術(shù)語(yǔ)也幾乎被等同起來(lái)了。
盡管現(xiàn)代CPU的集成度不斷提高,其結(jié)構(gòu)也變得越來(lái)越復(fù)雜,但是它在計(jì)算機(jī)系統(tǒng)中的角色仍然非常簡(jiǎn)單,那就是從內(nèi)存中讀取指令(fetch instruction),然后解碼(decode)和執(zhí)行(execute)。指令是CPU可以理解并執(zhí)行的操作(operation),它是CPU能夠“看懂”的語(yǔ)言。本文將以這一核心任務(wù)為線索,介紹關(guān)于CPU的基本知識(shí)和概念。
指令和指令集
某一類CPU所支持的指令集合簡(jiǎn)稱為指令集(Instruction Set)。根據(jù)指令集的特征,CPU可以劃分為兩大陣營(yíng),即RISC和CISC。
精簡(jiǎn)指令集計(jì)算機(jī)(Reduced Instruction Set Computer,RISC)是IBM研究中心的John Cocke博士于1974年提出的。其基本思想是通過(guò)減少指令的數(shù)量和簡(jiǎn)化指令的格式來(lái)優(yōu)化和提高CPU執(zhí)行指令的效率。RISC出現(xiàn)后,人們很自然地把與RISC相對(duì)的另一類指令集稱為復(fù)雜指令集計(jì)算機(jī)(Complex Instruction Set Computer,CISC)。
RISC處理器的典型代表有SPARC處理器、PowerPC處理器、惠普公司的PA-RISC處理器、MIPS處理器、Alpha處理器和ARM處理器等。
CISC處理器的典型代表有x86處理器和DEC VAX-11處理器等。頭一款x86處理器是英特爾公司于1978年推出的8086,其后的8088、80286、80386、80486、奔騰處理器及AMD等公司的兼容處理器都是兼容8086的,因此人們把基于該架構(gòu)的處理器統(tǒng)稱為x86處理器。
基本特征
下面將以比較的方式來(lái)介紹RISC處理器和CISC處理器的基本特征和主要差別。除非特別說(shuō)明,我們用ARM處理器代表RISC處理器,用x86處理器代表CISC處理器。
一,大多數(shù)RISC處理器的指令都是等長(zhǎng)的(通常為4個(gè)字節(jié),即32比特),而CISC處理器的指令長(zhǎng)度是不確定的,最短的指令是1個(gè)字節(jié),有些長(zhǎng)的指令有十幾個(gè)字節(jié)(x86)甚至幾十個(gè)字節(jié)(VAX-11)。定長(zhǎng)的指令有利于解碼和優(yōu)化,其缺點(diǎn)是目標(biāo)代碼占用的空間比較大(因?yàn)橛行┲噶顩](méi)必要用4字節(jié))。對(duì)于軟件調(diào)試而言,定長(zhǎng)的指令有利于實(shí)現(xiàn)反匯編和軟件斷點(diǎn),我們將在4.1節(jié)詳細(xì)介紹軟件斷點(diǎn)。這里簡(jiǎn)要介紹一下反匯編。對(duì)于x86這樣不定長(zhǎng)的指令集,反匯編時(shí)一定要從一條有效指令的字節(jié)開始,依次進(jìn)行,比如下面3條指令是某個(gè)函數(shù)的序言。
- 0:000> u 47f000
- image00400000+0x7f000:
- 0047f000 55 push ebp
- 0047f001 8bec mov ebp,esp
- 0047f003 6aff push 0FFFFFFFFh
上面是從正確的起始位置開始反匯編,結(jié)果是正確的,但是如果把反匯編的起點(diǎn)向前調(diào)整兩個(gè)字節(jié),那么結(jié)果就會(huì)出現(xiàn)很大變化。
- 0:000> u 47effd
- image00400000+0x7effd:
- 0047effd 0000 add byte ptr [eax],al
- 0047efff 00558b add byte ptr [ebp-75h],dl
- 0047f002 ec in al,dx
- 0047f003 6aff push 0FFFFFFFFh
這就是所謂的指令錯(cuò)位。為了減少這樣的問(wèn)題,編譯器在編譯時(shí),會(huì)在函數(shù)的間隙填充nop或者int 3等單字節(jié)指令,這樣即使反匯編時(shí)誤從函數(shù)的間隙開始,也不會(huì)錯(cuò)位,可以幫助反匯編器順利“上手”。而上面的例子來(lái)自某個(gè)做過(guò)加殼保護(hù)的軟件,這樣的軟件不愿意被反匯編,所以故意在函數(shù)的間隙或者某些位置加上0來(lái)迷惑反匯編器。
二,RISC處理器的尋址方式(addressing mode)比CISC要少很多,我們稍后將單獨(dú)介紹。
三,與RISC相比,CISC處理器的通用寄存器(general register)數(shù)量較少。例如16位和32位的x86處理器都只有8個(gè)通用寄存器:AX/EAX、BX/EBX、CX/ECX、DX/EDX、SI/ESI、DI/EDI、BP/EBP、SP/ESP(E開頭為32位,為Extended之縮寫),而且其中的BP/EBP和SP/ESP常常被固定用來(lái)維護(hù)棧,失去通用性。64位的x86處理器增加了8個(gè)通用寄存器(R8~R15),但是總量仍然遠(yuǎn)遠(yuǎn)小于RISC處理器(通常多達(dá)32個(gè))。寄存器位于CPU內(nèi)部,可供CPU直接使用,與訪問(wèn)內(nèi)存相比,其效率更高。
四,RISC的指令數(shù)量也相對(duì)較少。就以跳轉(zhuǎn)指令為例,8086有32條跳轉(zhuǎn)指令(JA、JAE、JB、JPO、JS、JZ等),而ARM處理器只有兩條跳轉(zhuǎn)指令(BLNV和BLEQ)。跳轉(zhuǎn)指令對(duì)流水線執(zhí)行很不利,因?yàn)橐坏┯龅教D(zhuǎn)指令,CPU就需要做分支預(yù)測(cè)(branch prediction),而一旦預(yù)測(cè)失敗,就要把已經(jīng)執(zhí)行的錯(cuò)誤分支結(jié)果清理掉,這會(huì)降低CPU的執(zhí)行效率。但是豐富的跳轉(zhuǎn)指令為編程提供了很多方便,這是CISC處理器的優(yōu)勢(shì)。
五,從函數(shù)(或子程序)調(diào)用(function/procedure call)來(lái)看,二者也有所不同。RISC處理器因具有較多的寄存器,通常就有足夠多的寄存器來(lái)傳遞函數(shù)的參數(shù)。而在CISC中,即使用所謂的快速調(diào)用(fast call)協(xié)定,也只能將兩個(gè)參數(shù)用寄存器來(lái)傳遞,其他參數(shù)仍然需要用棧來(lái)傳遞。從執(zhí)行速度看,使用寄存器的速度更快。我們將在后面關(guān)于調(diào)用協(xié)定的內(nèi)容中進(jìn)一步討論函數(shù)調(diào)用的細(xì)節(jié)。
鑒于以上特征,RISC處理器的實(shí)現(xiàn)相對(duì)來(lái)說(shuō)簡(jiǎn)單一些,這也是很多低成本的供嵌入式系統(tǒng)使用的處理器大多采用RISC架構(gòu)的一個(gè)原因。關(guān)于RISC和CISC的優(yōu)劣,一直存在著很多爭(zhēng)論,采用兩種技術(shù)的處理器也在相互借鑒對(duì)方的優(yōu)點(diǎn)。比如從P6系列處理器的一代產(chǎn)品Pentium Pro開始,英特爾的x86處理器就開始將CISC指令先翻譯成等長(zhǎng)的微操作(micro-ops或µops),然后再執(zhí)行。微操作與RISC指令很類似,因此很多時(shí)候又被稱為微指令。因此可以說(shuō)今天的主流x86處理器(不包括那些用于嵌入式系統(tǒng)的x86處理器)的內(nèi)部已經(jīng)具有了RISC的特征。此外,ARM架構(gòu)的v4版本引入了Thumb指令集,允許混合使用16位指令和32指令,指令的長(zhǎng)度由單一一種變?yōu)閮煞N,程序員可以根據(jù)需要選擇短指令和長(zhǎng)指令,不必再拘泥于一種長(zhǎng)度,這樣可使編譯好的目標(biāo)程序更加緊湊。