做爲一個程序員,當白天和黑夜沒有了界限,按照相對論的觀點,這時候他就變成了一個「程序猿」。
---------------佚名
今天下班早,但了無睡意,發篇以前欠下的博文。今天咱們簡單回顧一下當年學校裏「微機原理」和「彙編語言程序設計」這門課,年代有些久遠,可能好多人都記不得了,固然我也是,提早步入老齡化了呀。閒話不表。
講x86寄存器自己就是比較枯燥的一件事兒,因此我打算在講解的過程當中,有必要的經過一些例子來向你們展現一下寄存器的做用。這中間會用到NASM彙編語法,不過都比較好懂,對示例代碼中的指令不會作過多解釋,有須要的童鞋能夠去參閱NASM手冊。
在博文「
動態庫和靜態庫那點事兒
」裏,咱們瞭解到,彙編器(Assembler)是將彙編語言源代碼翻譯爲機器語言的程序。通常而言,彙編生成的目標代碼段,須要經連接器(Linker)處理纔可生成最終的可執行代碼。
彙編語言問世以前,程序員都是用二進制的「0」和「1」的序列進行編程,也就是咱們所說的機器語言,其痛苦程度可想而知。爲了減輕使用機器語言編程的痛苦,20世紀50年代初,出現了彙編語言。彙編語言用比較容易識別、記憶的助記符替代特定的二進制串。關於彙編語言的發展能夠參考「
彙編語言發展樹
」,算是瞭解一下課外知識。
那麼什麼又是彙編語言?彙編語言是爲特定CPU設計的一種面向機器的語言﹐和CPU的架構密切相關,由彙編執行指令和彙編僞指令組成。使用匯編語言編寫的程序,機器不能直接識別。彙編器會將彙編語言翻譯成機器語言,而彙編器把彙編語言翻譯成機器語言的過程稱爲彙編。
因此,這個彙編器的功能可大着呢,它充當了彙編語言源程序和機器語言之間的翻譯。而時下主流的彙編器,有如下幾款:
MASM:是微軟公司開發的彙編編譯器,採用Intel規定的彙編語法,在6.0版本之前單獨發佈,分masm.exe和link.exe。從6.0版開始MASM就更名ML了,由於它把編譯和連接組合在一塊兒了。9.0版的ML跟隨VC2008一塊兒發佈。之前咱們在學校學的就是這個傢伙。
TASM:是Borland公司開發的彙編編譯器,被普遍用於Turbo C,Quick Basic等編譯器,用做中間過渡編譯。它也能獨立的編譯純彙編或是Win32Asm的代碼。具備編譯快速,高效的特色。TASM徹底兼容MASM。因爲頭文件和庫不完整,在win32下使用TASM有些力不從心,Borland公司目前已放棄了對其的維護和開發。
FASM:Flat Assembler,是一個純粹用匯編語言寫成,並採用自展技術的編譯器。優勢是不須要連接直接能夠生成可執行文件。
NASM:(Netwide Assembler),是Linux 平臺上一個常常用到的彙編器,由Netwide公司開發。NASM是以可移植性和模塊化爲目標,以支持80x86爲基礎而設計的編譯器,它提供了很好的宏指令功能,支持多種目標文件格式,包括Linux和NetBSD、FreeBSD操做系統的a.out、ELF、COFF等文件格式,以及微軟公司16位OBJ和32位OBJ文件格式;它也能夠輸出無格式的二進制文件(如Dos.COM,.sys)。它的語法格式很簡單且易於理解,與Intel規定的很類似但卻沒有那麼複雜。NASM 採用的是人工編寫的語法分析器,於是執行速度要比 GAS 快不少,更重要的是它使用的是 Intel 彙編語法,能夠用來編譯用 Intel 語法格式編寫的彙編程序。
GAS:(GNU Assembler), 這是Linux 平臺的標準彙編器 ,它也是 GCC 所依賴的後臺彙編工具,一般包含在 binutils 軟件包中。GAS 使用標準的 AT&T 彙編語法(和Intel的標準語法有些區別),能夠用來彙編用 AT&T 格式編寫的程序。GCC會保證提供給它絕對正確的代碼,因此GAS的錯誤檢測功能至關弱。
重申:本文中全部的代碼都是Intel風格的NASM,當你弄明白了Intel風格的彙編語法後,再去看AT&T的彙編代碼,得其章法後絕對不成問題,我保證,除非.....那啥.....呵呵.......
80x86CPU,其內部的寄存器能夠分爲如下幾類:
通用寄存器
、
專用寄存器
、
段寄存器
x86的CPU其特性都是前向兼容,因此32位的寄存器能夠兼容16位和8位,16位的寄存器能夠兼容8位。也就是說,32位CPU能夠只使用其低16位,將它做爲16位CPU來對待;16位CPU能夠將其高8位和低8位分開,看成兩個8位CPU來使用。當要注意,有些寄存器是不能這樣分開使用的,後面咱們會提到。
1
、通用寄存器,一共有
8
個,其分類和關係以下:
全部以字母「
E
」開頭的寄存器都是
32
位,
AX,BX,CX,DX,SP,BP,SI,DI
都是
16
位寄存器。其中
AX,BX,CX
和
DX
又能夠分別拆開看成兩個
8
位寄存器來使用,而
SP,BP,SI
和
DI
卻不能這麼用。
這裏有些童鞋可能內心還在糾結,爲啥要叫「通用寄存器」呢?這八個寄存器,除了它們本身本職的工做之外,還能夠用來暫存和傳送數據。也就是說當咱們本身寫底層彙編代碼時能夠用着幾個寄存器來臨時保存數據,而後實現咱們特定的功能。以咱們最熟悉的系統調用爲例。在彙編層面,系統調用的實現機制和處理邏輯以下:
第一步,將系統調用號(不懂什麼系統調用號請猛擊這裏)num暫存到eax寄存器:mov eax, num css
第二步,將傳遞給系統調用的參數依次按順序放到ebx,ecx,edx,esi,edi這些寄存器裏; html
第三步,觸發0x80軟中斷,陷入內核執行系統調用:int 80h linux
第四步,函數的返回值保存在eax中。 程序員
亙古不變的例子「hello world」。咱們用write系統調用向標準輸出設備打印該字串:
- ;hello.asm
- section .data ;數據段開始
-
- msg db 「hello,world!」,0xA; 定義要顯示的字符串
- len equ $-msg; 定義字串長度,此操做後len的值就不能更改了
-
- section .text ;代碼段開始
- global _start ;指定函數入口
- _start: ; write的系統調用號是4,其格式爲write(fd,buf,buf_len)
- mov eax,4 ; 填充系統調用號到EAX寄存器
- mov ebx,1 ; 第一個參數,標準輸出,其文件句柄爲1。順便普及一下,標準輸入爲0,標準錯誤爲2。
- mov ecx,msg ; 第二個參數,緩衝區首地址。由咱們的消息變量msg來傳遞。
- mov edx,len ;第三個參數,消息長度,由len變量傳入。計算字符串msg長度時採用了一個小技巧」$-msg」。
- int 80h ;觸發系統調用軟件,接下來的任務都交給內核吧。
-
- ;程序退出,執行exit(0),它的代碼就很容易寫出了,其系統調用號是1。
- mov eax, 1 ; 填充系統調用號。
- mov ebx, 0 ; 返回碼是0。
- int 80h ; 陷入內核
編譯上述代碼,連接後運行,結果以下:
編譯時咱們用「
-f elf
」選項告訴
NASM
彙編器,咱們要生成的
elf
格式的目標文件。這個例子應該能夠確切解釋了這些寄存器通用性的原因了吧。
接下來咱們看一下EAX做爲邏輯累加其的用法。就是說在四則運算裏,EAX寄存器裏能夠保存一個操做數。加減就不討論了,看一下乘除法。除法運算中,被除數默認是存放在EAX寄存器中;乘法運算時,一個數默認也是放在EAX寄存器中,最後的乘積默認仍是保存在EAX中。以乘法運算爲例:
- ; mul.asm
- extern printf,exit ; 咱們在彙編中調用C庫的printf和exit函數,因此要用extern關鍵字對printf和exit進行聲明。
-
- SECTION .data ; 數據段
- var1: dd 40
- var2: dd 20
- fmt: db "result=%d", 10, 0 ; The printf format, "\n",'0'
-
- SECTION .text ; 代碼段.
-
- global _start
- _start:
- mov eax, [var1] ; 乘數1
- mov ebx, [var2] ; 乘數2
- mul ebx ; 執行乘法運算,結果保存在EAX寄存器裏。
-
- push eax ; result is here in EAX
- push dword fmt ; address of ctrl string
- call printf ; Call C function
-
- push dword 0
- call exit
編譯連接,並運行,結果以下:
連接命令中「--dynamic-linker /lib/ld-linux.so.2」說明咱們使用/lib/ld-linux.so.2來加載動態庫;「-lc」表示咱們要連接C庫/lib/libc.so,若是是「-lx」,默認連接/lib/libx.so庫。
EBX在寄存器間接尋址和查表時,通常是用來做爲偏移地址使用。既然EBX裏是偏移地址的值,那麼根據80x86實模式的尋址機制「段地址:[BX]」。若是DS=1234H,BX=2H,則指令:
move ax,[ds:bx]
會將物理內存中數據段DS裏偏移量是2字節的內存單元裏的數據裝載到ax寄存器裏,這個沒啥好說的。須要注意的是:若是指令中沒有明確給出段地址的話,缺省狀況下BX會使用數據段寄存器DS裏的值。也就是說:
move eax,[ds:bx] move ax,[bx]
備註:上述代碼在保護模式下運行會出現「段錯誤」,緣由之後解釋。
ECX寄存器
當在彙編指令中使用循環 LOOP 指令時,能夠經過 ECX 來指定須要循環的次數。也就是說,若是你要在彙編中用LOOP循環指令時,CPU默認狀況下會到ECX寄存器裏去找循環的終止條件。也就是說 CPU 在每一次執行 LOOP 指令的時候,都會作兩件事:一件就是令 ECX = ECX – 1,即令 ECX 計數器自動減去 1;還有一件就是判斷 ECX 中的值,若是 ECX 中的值爲 0 則會跳出循環,而繼續執行循環下面的指令,固然若是 ECX 中的值不爲 0 ,則會繼續執行循環中所指定的指令 。以下,計算1+2+3+…+10的結果,中咱們展現了ECX做爲循環計數器的用法:
- ; usage of ECX testecx.asm
- section .data
- output: db 「result is %d」, 0ah
- extern printf,exit
- section .text
- global _start
- _start:
- mov ecx,10 ; 循環10次,每循環一次ECX寄存器的值減1
- mov eax, 0 ; 循環求和的結果保存在EAX裏,因此初始化爲0
- jcxz done ; 這句代碼的做用是防止ecx初始被設置成0,執行循環時,EXC先減1,而後再判斷ECX是否爲0。若是一開始ECX就爲0,那麼減1後,就變成了-1,程序就會出問題。感興趣的童鞋,能夠將這句代碼註釋掉,並將ECX初始值設置爲0看一下程序的執行結果。
-
- sum_label:
- add eax,ecx ; 將ECX寄存器裏的值依次累加到EAX裏
- loop sum_label; 循環跳轉
-
- push eax ; 如下三行代碼完成printf(「%dn」,eax)的調用
- push output
- call printf
- done:
- push 0 ; 如下兩行代碼完成exit(0)的調用
- call exit
結果以下:
咱們說,ECX經常使用於循環計數,可是沒說「必定」,因此在你本身寫循環彙編時,徹底能夠本身控制循環條件,而且你又很是的不想用ECX,看下面的代碼。咱們用EAX和EBX通用寄存器完成上面ECX一樣的功能:
- ; another.asm
- section .data
- output db 「result is %d」, 0ah
- extern printf,exit
- section .text
- global _start
- _start:
- mov ebx,10 ; 循環10次
- mov eax, 0 ; 循環求和的結果保存在EAX裏,因此初始化爲0
-
- sum_label:
- add eax,ebx ; 將ECX寄存器裏的值依次累加到EAX裏
- dec ebx ; 咱們本身手動控制,將循環計數減1
- jz done ;若是ebx爲0,則循環結束,準備打印輸出結果
- jnz sum_label; 不然繼續循環
- done:
- push eax ; 如下三行代碼完成printf(「%dn」,eax)的調用
- push output
- call printf
- push 0 ; 如下兩行代碼完成exit(0)的調用
- call exit
此次循環時咱們沒用LOOP語句,而是本身用jnz和jz控制什麼時候結束循環。結果如出一轍,以下所示:
模塊化
ESP
寄存器實際上必須和
SS
段寄存器一塊兒使用,這裏就不討論它了。在段寄存器的部分再說。
在寄存器間接尋址指令裏,和
EBX
相似,
EBP
也能夠用於存儲目標數據的段內地址的偏移量。這裏有一點須要注意,那就是在寄存器間接尋址時,若是沒有明確給出段基地址時,
EBP
默認的段地址使用的是堆棧段寄存器SS
裏的值
。例如:
- mov ebx,0
- mov eax,[ebp] ;該指令意思是將SS:[EBX]表明的內存單元的值裝入EAX寄存器
- move ax,[cs:ebx] ;該指令明確的段地址是代碼段CS
這裏有一點比較關鍵,那就是80x86中,在實模式下使用的16位寄存器來存放地址的偏移量,也就是說能夠用於寄存器間接尋址的寄存器只有四個,分別是BX,BP,SI和DI。要儘可能避免用BP寄存器,由於BP通常用於堆棧尋址,而不是數據尋址。在保護模式下,EAX,EBX,ECX,EDX,ESP,EBP,ESI和EDI均可用於寄存器間接尋址。
注意,前面說的約定「BX、SI和DI的默認段基址寄存器都是DS,BP的默認段基址寄存器是SS」都是針對實模式而言,在保護模式下,Linux對X86的處理是,6個段寄存器CS,DS,SS,ES,FS和GS的值分別是:FS=GS=0,ES=SS=DS,CS=CS,也就是說實模式和保護模式的尋址方式不同。
二、專用寄存器
EIP(IP):
是存放下次將要執行的指令在代碼段的偏移量。在具備預取指令功能的系統中,下次要執行的指令一般已被預取到指令隊列中,除非發生轉移狀況。因此,在理解它們的功能時,不考慮存在指令隊列的狀況。
前面咱們知道
實模式下,因爲每一個段的最大範圍爲
64K
,因此,
EIP
中的高
16
位確定都爲
0
,此時,至關於只用其低
16
位的
IP
來反映程序中指令的執行次序。
EFLAGS(FLAGS):去google吧,我實在不想解釋它了。
三、段寄存器
(留到內存管理部分再講)
至於剩下的諸如在80386裏增長的四個系統表寄存器,
全局描述符表寄存器(GDTR)、中斷描述符表寄存器(IDTR)、局部描述符表寄存器(LDTR)、任務狀態寄存器(TR)
】、四個控制寄存器CR0~CR3,以及80486和奔騰(新增CR4)架構新增的功能定義這裏就不探究了,之後有用到時再說。
PS:最後仍是忍不住吐槽一句,CU的編輯器能不能優化一下呢?每次寫好博文,粘貼出來,調格式花了我1個多小時啊,1個多小時啊,唉~~~