汇编程序设计与计算机体系结构软件工程师教程笔记:处理器、寄存器简介
《匯編程序設計與計算機體系結構: 軟件工程師教程》這本書是由Brain R.Hall和Kevin J.Slonka著,由愛飛翔譯。中文版是2019年出版的。個人感覺這本書真不錯,書中介紹了三種匯編器GAS、NASM、MASM異同,全部示例代碼都放在了GitHub上,包括x86和x86_64,并且給出了較多的網絡參考資料鏈接。這里只摘記了NASM和MASM,測試代碼僅支持Windows和Linux的x86_64。
1. 編程語言及數據的基礎知識
1.1 開篇語:
GNU Assembler(GAS)是一款基于Linux的匯編器,主要供GNU項目使用,它產生于1987年。
Netwide Assembler(NASM)是一款基于Linux的開源匯編器/反匯編器,適用于x86與x86_64,它產生于1996年。
Microsoft Macro Assembler(MASM)是微軟操作系統的專屬匯編器,隨Visual Studio一起發布,它產生于1981年。
1.3 計算機編程語言:
把匯編代碼翻譯成機器語言的過程通常稱為編碼(encoding),反向的過程,也就是把機器碼翻譯成匯編語言的過程,通常稱為解碼(decoding)。
指令集架構(Instruction Set Architecture, ISA)是計算機體系結構中與編程有關的方面。它指出了處理器所具備的指令、寄存器、內存架構、數據類型以及其它一些屬性,以供程序員使用。
指令集架構分為復雜與精簡兩種。復雜指令集(Complex Instruction Set Computing, CISC)架構中的指令,其長度(也就是表示該指令所需的字節數)不固定。之所以說它復雜,是因為一條指令有可能要完成多項任務。與之相對的ISA設計稱為精簡指令集(Reduced Instruction Set Computing, RISC),其中的所有指令都一樣長,而且只執行一項任務。
x86與x86_64都是CISC架構,而其它一些ISA設計方案則多為RISC架構。
反匯編是由反匯編器對包含機器語言的目標文件進行解碼之后所輸出的匯編代碼,這實際上相當于把機器語言的二進制序列解碼成匯編指令。大多數匯編器、編譯器、調試器以及開發環境都提供反匯編功能,例如NASM、GDB、LLVM、Xcode以及Visual Studio。還有一些獨立的反匯編器可供使用,像是Capstone、IDA、objdump與otool。
機器語言是針對特定的處理器而言的,不過,同一個系列的處理器能夠解讀同一種機器語言,因此,針對x86處理器所寫的代碼可以用在該系列的任何一款處理器上,x86_64也是如此。匯編代碼無法在不同系列的處理器之間移植。
x86/x86_64處理器家族,既包括Intel的Pentium、Core-Duo及Core i7,也包括AMD的Athlon、Phenom及Opteron。Intel與AMD的處理器設計方案都實現了x86指令集,但實現該指令集所用的具體技術(也就是微架構, microarchitecture)卻有所不同。
編程語言與相應文件及編程工具之間的關系,如下圖所示:
編譯器(compiler)根據處理器的指令集,把用高級語言寫成的源代碼轉化成匯編語言這種中介形式。然后,匯編器(assembler)把這些匯編代碼編碼成目標代碼(object code),這是一種可重定位的機器語言,其格式與具體的操作系統(Operating System, OS)平臺有關。鏈接器(linker)把多個目標文件及靜態庫(static library)合并起來,以形成一個可執行文件。最后,加載器(loader)把鏈接器在可執行文件中所生成的指令與動態庫(dynamic library)中的指令相結合,將這些機器碼載入內存,以供CPU(Central Processing Unit, 中央處理器)執行。
所謂可重定位,是指在將匯編代碼翻譯成目標代碼時,采用一種通用的形式來表示這些代碼,把第一條指令的開始位置記為0x0h這個地址,以后指令的位置都用它與前面指令相隔的字節數(或者說偏移量, offset)表示。這樣的話,在將程序實際載入內存時,只需要把第一條指令放在某個真實的地址上就可以了,而后續的各條指令則可以根據其偏移量安排在適當的位置上。
1.4 數據的表示:
數據在底層可以用二進制來表示,這種表示方式只使用1與0兩種數位。實際上,在執行機器語言所寫的代碼時,計算機只能采用二進制來運作。
在計數系統中,整數的符號可以用多種辦法表示,例如原碼、反碼及補碼。這三種表示方式都用最高有效位表示正負,0代表正,1代表負。當前的大多數架構(例如x86與x86_64)均采用補碼來表示帶符號的整數。原碼與反碼都有個缺點,就是0會以兩種形式出現,一種是正0,一種是負0。與之相比,補碼不會出現兩個0的問題。
ASCII(American Standard Code for Information Interchange, 美國信息交換標準代碼):是一種7位字符集,意味著它可以表示128種(也就是2^7種)不同的字符,其中大部分是英文字母。C++等高級語言使用ASCII作為默認的字符集。ASCII可以用很多方式來擴展,以表示其它拉丁系的字母及符號。
Unicode字符集能夠以任何一種辦法來編碼,其中最常用的一種叫做UTF-8(UTF是Unicode Transformation Format, Unicode轉換格式)。互聯網上的很多網頁用的都是這種字符編碼(character encoding)方式,它支持1112064個字符,其中每個字符可能要用1至4個octet來表示(octet指的是由8個二進制位所構成的組,相當于一個字節)。此外,還有其它一些Unicode字符編碼方式,例如UTF-16用一個或兩個16位的字(word)來表示字符,UTF-32用一個32位的值來表示字符。
1.5 布爾表達式:NOT、AND、OR、XOR
2. 處理器與計算機系統體系結構
2.2 體系結構概述:
主板是連接計算機主要組件的板卡。總線(bus)是指主板中的一組線路或導線,用來在組件之間傳輸數據。計算機中最主要的總線叫做系統總線(system bus),它實際上是數據總線、地址總線與控制總線這三者的合稱。系統總線使得CPU能夠與內存及計算機中的其它I/O設備相通信,為了確保通信正常,這些與CPU通信的設備必須與對應的總線連接起來。數據總線(data bus)用來在組件之間傳遞指令及數據,其中的指令指的是從內存中加載數據、把數據放入內存,或是從光驅中讀取數據等動作。為了正確傳遞信息,還必須傳輸指令所針對的內存地址和數據。地址總線(address bus)就負責地址方面的通信工作。此外,控制總線(control bus)會在組件之間傳輸信號,使得組件能夠在適當的時機通信,以確保同步運作。
除了系統總線,還需要一個部件才能使組件之間得以通信。這個部件就是主板上的系統時鐘(system clock)。系統時鐘的基本單位叫做時鐘周期(clock cycle),它的前半段稱為高電平期(up-tick),此時電壓由低變高(用二進制表示就是從0變成1),后半段稱為低電平期(down-tick),此時電壓由高變低(或者說從1變成0)。
要傳輸數據就必須有地方存儲這些數據及指令,為此,計算機必須擁有存儲器(memory)。下圖描述了各類存儲器之間的層次關系:
離CPU最近的存儲器是靜態隨機存取存儲器(Static Random Access Memory),簡稱SRAM。它實際上就在CPU芯片中。這種存儲器通常稱為緩存(cache)。SRAM是速度最快的存儲器類型。比它稍慢一些的是動態隨機存取存儲器(Dynamic RAM),簡稱DRAM。主板上有一些靠近CPU的槽位,其中插著的內存條就屬于這種類型的存儲器。大家通常所說的主存(main memory)一般指的就是DRAM,也可以簡稱RAM。還有一種存儲器比DRAM還慢,這就是機械硬盤或固態硬盤等磁盤(disk),主要相當于我們常說的硬盤。這種存儲器用來長期保存數據,而不像緩存或RAM只用來暫時保存。
把權重最大的字節(或者說最高有效字節, most significant byte),保存到地址最低的地方,由于權重最大的字節(也就是”大端”,big end)出現在最前面,因此這種方式叫做大端在前的字節順序(Big-Endian byte order),簡稱大端序或大尾序。還有一些計算機系統采用相反的順序,把權重最小的字節(“小端”,little end)保存在最前面,這種保存方式稱為小端在前的字節順序(Little-Endian byte order),簡稱小端序或小尾序。Intel的x86與x86_64采用小端序。
2.3 處理器:
CPU或者說處理器可以視為計算機的大腦。在計算機系統中,主要的算術運算與邏輯運算都靠這個部件來處理。宏觀地來看,CPU由4個主要的部分組成,它們是:算術邏輯單元(Arithmetic Logic Unit, ALU)、控制單元(Control Unit, CU)、CPU時鐘(CPU clock)及存儲器(包括緩存和寄存器)。
ALU是CPU中執行數學運算的子組件,它所執行的算術與邏輯運算針對的是整數型操作數(要注意操作數的類型是整數,浮點數由另一個組件處理)。CU負責指揮CPU中的數據流,以確保CPU里的其它子組件能夠在適當的時機接收到正確的數據,并做出相應的處理。CU還必須把指令執行周期(Instruction Execution Cycle)安排好,使得CPU指令能夠據此得以執行(此外,為了正確執行這些指令,計算機還需要完成其它一些子任務)。CPU時鐘與系統時鐘不同,它是CPU本身的時鐘,用來為CPU的操作計時。
有一個和CPU時鐘相關的術語叫做時鐘頻率,它的單位是赫茲(Hertz, Hz)。說的簡單一些:頻率為1Hz的處理器其時鐘每秒震蕩1次。當前的處理器都是以MHz(megahertz, 兆赫)或GHz(gigahertz, 千兆赫)來描述頻率的,1MHz意味著每秒震蕩一百萬次,1GHz意味著每秒震蕩十億次。如果要用時鐘周期而不是頻率來描述處理器的快慢,就給頻率取倒數。CPU的時鐘頻率是系統時鐘頻率的倍數,具體是幾倍由倍頻系數(multiplier)決定。
由于處理器要對數據執行操作,因此必須要有地方來保存這些用作操作數的數據,此外,操作結果以及操作所涉及的地址也得有地方保存。存放這些指令與數據的存儲器離ALU及CU越近越好。這種緊鄰CPU的存儲器就叫做cache(高速緩存,簡稱緩存),實際上,它跟邏輯電路一起位于CPU芯片中。
當前的處理器緩存通常分為三個級別,分別是L1緩存(一級緩存)、L2緩存(二級緩存)與L3緩存(三級緩存)。緩存本身的層次結構與存儲器的層次結構都遵循同一條原則:距離ALU越遠容量越大。L1與L2緩存都離ALU很近,不過L2要比L1稍遠一些,因此其容量也大一些。L3緩存一般出現在多核處理器中,它為所有CPU核心所共享,而L1與L2緩存則每個CPU核心都配有一套,如下圖所示:L3緩存是靜態存儲器中的最后一層,如果數據不保存在該層及其上方的各層中,那就只好放到它下方的RAM中了。
CPU提供了一些與緩存有關的指令,然而開發者一般都不用專門編寫代碼去訪問或操作緩存,因為緩存是由復雜的算法來控制的,以確保程序所需的數據能夠盡量出現在緩存中,所以開發者通常不需要干預這套機制。比方說,如果程序頻繁引用某個變量,那么處理器就有可能認為這份數據相當重要,從而將其預先獲取(prefetch, 簡稱預取)出來并放入緩存,使得程序以后訪問該數據時能夠快一些。CPU會在執行程序的過程中隨時根據情況來做出這種處理。
除了緩存之外,CPU中還有一種存儲器叫做寄存器(register),它的內容可以通過明確的地址來訪問,而且在此類存儲器中它是最快的一種。它位于整個存儲器層級的最頂端,其容量比緩存更小,速度也比緩存更快。寄存器是最貼近ALU的一小塊存儲區域,用來保存執行指令時所涉及的操作數、地址及結果。
處理器的每個核心都有自己的一套L1與L2緩存,與之類似,每個核心也都有自己的一套寄存器。然而,編寫匯編代碼的時候你不用指出當前操作的寄存器究竟處在哪個核心上,你只需要寫成寄存器的名字就可以了,至于這個寄存器到底指的是哪個核心上的寄存器則由CPU決定。下表列出了寄存器名稱:
寄存器可以分成4類:通用目的寄存器(General Purpose Register)、段寄存器(Segment Register)、標志寄存器(Flags Register)及指令指針寄存器(Instruction Pointer Register)。匯編程序所操作的基本上都是通用目的寄存器,其中,32位的通用寄存器有8個,64位的有16個。通用寄存器用來執行計算或移動數據。由于64位處理器是在32位設計方案的基礎上構建的,而32位處理器又是在16位設計方案的基礎上構建的,因此新式處理器不僅可以通過寄存器本身的名字來使用該寄存器,而且還能通過舊式處理器所用的名字將其當成舊式的寄存器來使用。此外,如果你要使用的數據或是你要執行的運算只需占據8個二進制位,那么可以把16位的寄存器想象成兩個8位的寄存器,這樣就可以用它來保存兩份數據了。凡是以小寄存器的名義來操作大寄存器的,其實操作的都是大寄存器中與這個小寄存器相對應的那一部分二進制位。
某些64位及32位寄存器的特殊用法:
(1). rax/eax通常是默認的累加寄存器。乘法等操作會將其中一部分結果自動存放到rax/eax中,調用函數的時候也需要把返回值保存在rax/eax中。因此,執行這些操作時不要用rax/eax保存一般的數據。
(2). rcx/ecx用來在執行循環的過程中記錄循環計數器的值。因此,在循環內部不要用rcx/ecx保存一般的數據。
(3). rbp/ebp用作棧幀中的幀指針。該寄存器用來指向棧中的數據。建議只把它當做專門的寄存器來用。
(4). rsp/esp是棧指針寄存器,這也是個與棧管理有關的寄存器,它一般指向活動棧幀的頂部。與前一個寄存器一樣,這個寄存器也只應該當成專門的寄存器來用。
(5). rsi/esi與rdi/edi是索引寄存器(index register, 也稱為變址寄存器),它和STOSB、MOVSB與SCASB這樣的字符串操作結合起來使用,以便保存、加載或掃描大量的數據。這些操作實際上會把CPU置于一種自動循環模式中,這要比開發者手工編寫循環更有效率。
(6). rip/eip是擴展版的指令指針寄存器。這個寄存器用來指向內存中的地址,以表示接下來應該獲取、解碼并執行的指令,它是在程序運行過程中自動調整的,不應該通過編程的手段修改。
(7). rflags/eflags是狀態與控制寄存器。LAHF與SAHF等特殊指令可以把CPU的一些狀態標志載入ah寄存器,或是將ah寄存器里的值保存到狀態寄存器中。除此以外,不應該用其他手段直接修改rflags/eflags。該寄存器里的二進制位是在執行完算術運算之后根據一套布爾規則自動設置的。盡管rflags是64位,但其中能夠用到的只有低32位,因此,x86與x86_64處理器用的是同一套狀態標志。
CPU標志(CPU flag)是一些二進制位的統稱,這些二進制位分別用來以某種方式控制CPU操作或反映CPU操作的狀態。下表列出了大多數開發環境中值得關注的8個標志位,其中某些標志可以由開發者通過LAHF及SAHF指令來操作。
64位處理器:x86_64指令集是對x86指令集(這是一種32位指令集)的擴充,因此能夠在32位環境下執行的操作,同樣可以放在64位的處理器中執行。由于x86_64處理器是64位的,因此其數據與地址都可以用64個二進制位來表示,然而,當前的x86_64處理器只用到了其中的低48位,所以說,盡管理論上能夠在2^64字節的地址空間中尋址,但實際上最多只支持2^48字節的地址空間。用48個二進制位來表示物理地址空間中的地址意味著RAM(內存)容量可達256TB,這比32位處理器所支持的4GB要高出很多。除了支持更大的RAM地址空間,x86_64處理器還多提供了8個通用的寄存器R8~R15.
指令的執行:要執行指令(例如ADD)必須遵循一系列步驟,這些步驟合起來構成指令執行周期(Instruction Execution Cycle)。指令執行周期一般表示成Fetch(獲取)、Decode(解碼)及Execute(執行)這三個大的階段。
2.4 輸入與輸出:
鍵盤、顯示器、網卡等外部設備需要通過I/O模塊(Input/Output Module)與電腦相連,而這些I/O模塊同時也與系統總線相連,使得外部設備能夠與計算機中的其它組件(例如CPU)通信。除了在處理器與外部設備之間提供緩沖通信(buffered communication)機制之外,I/O模塊還可以完成其它一些重要的操作,例如傳輸數據、對命令進行解碼,以及查詢設備狀態等。由于速度不同,I/O模塊必須提供數據緩沖區,使CPU的工作速度不會因為外部設備而受到影響,同時也令外部設備不會為CPU發來的大量數據所淹沒。
處理器執行I/O操作通常可以采用4種方式:程序I/O(Programmed I/O, PIO)、中斷驅動I/O(Interrupt-driven I/O)、直接內存訪問(Direct Memory Access, DMA)以及I/O通道(I/O Channel)。
程序加載:程序是由一個叫做程序加載器的工具來加載的。加載進來之后,CPU(主要是說它的eip/rip寄存器)指向程序的入口點,這個入口點可能叫做main或start等。下面描述啟動程序的一般步驟。當用戶開啟程序(例如用鼠標雙擊程序的圖標)時:
(1). 操作系統把文件的大小以及該文件在磁盤中的物理位置等信息獲取出來。
(2). 操作系統在內存中尋找合適的地點分配空間,并把必要的信息放在描述符表(descriptor table)中。
(3). 操作系統開始執行程序的第一條指令(也就是位于入口點的那條指令)。這時程序變為進程,并獲得由系統所賦予的ID。
(4). 該進程自行運作,而操作系統則會對進程所發出的資源請求予以響應。
(5). 進程結束并讓出它所占據的內存。
GitHub:https://github.com/fengbingchun/CUDA_Test
總結
以上是生活随笔為你收集整理的汇编程序设计与计算机体系结构软件工程师教程笔记:处理器、寄存器简介的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 王爽著的《汇编语言》第3版笔记
- 下一篇: 汇编程序设计与计算机体系结构软件工程师教