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