程式語言的底層描述(1)——彙編基礎概念的開始之入門
該主題預設在類unix作業系統中進行討論
gcc編譯器可通過不同的引數,生成不同階段的編譯檔案,比如,我們想生成可執行檔案,就用gcc -o;要生成目的碼.o,就用gcc -c;要生成彙編程式碼.s,就用gcc -S,
程式編譯的順序是,編譯器先根據.c原始檔產生.s彙編檔案,然後彙編器會把.s轉換成目標檔案.o,最後由連結器將.o檔案與unix庫函式合併,產生可執行檔案。
當我們得到.o時,還可以用objdump -d *.o,對目標檔案進行反彙編,螢幕會打印出十六進位制與彙編檔案的對應解釋。
以上說的都是操作方法,接下來的幾章我們將淺析組合語言的內幕。
首先來看組合語言的基礎知識
一:資料格式
由於計算機經歷從16位機擴充套件到32位機的過程,intel沿用的以前的屬於,即將16位(即2位元組)定義為“字”,32位定義為“雙字”。而8位仍成為“位元組”,用符號表示如下:
字:w 16位
雙字:l 32位
位元組:b 8位
單精度:s 8位
雙精度:l 32位
組合語言在操作上沿用了這樣的命名方式,比如資料傳送命令movl,我們就能看出這是對32位4位元組的操作
二:暫存器
組合語言簡單說就是遊走於暫存器、儲存器之間進行運算和操作的語言,因此暫存器的概念是至關重要的。
下面是整數暫存器示意圖:
31 15 7 0
%eax %ax |
|
||
%ecx %cx |
|
||
%edx %dx |
|
||
%ebx %bx |
|
||
%esi %si | |||
%edi %di | |||
%esp %sp | 棧指標 | ||
%ebp %bp | 幀指標 |
從示意圖來看,八個暫存器中,每個暫存器都是32位,每個暫存器都有直接引用前16位的暫存器標識,比如%esi的前16位暫存器可以用%si來直接訪問。同樣的道理,%bh、%bl還可以直接訪問%bx的高八位和第八位。
還有,%esp預設是棧指標,%ebp是幀指標,這些在後面講還有論述。
三:運算元指示符
常見運算元指示符有$、Imm、(),s
- $後面跟一個整數,表示“立即數”,類似C語言裡的常量數,如我們要把一個5賦給一個暫存器, 在語句中用$5表示;
- Imm稱為立即數偏移,顧名思義,是在定址時進行地址偏移的
- ()類似間接定址,類似C語言裡的“*”
- s伸縮因子,必須是1、2、4、8
通過下面這個例子方便理解:
假設下面五個地址代表的儲存器中分別儲存這五個值:
地址 | 值 |
0x100 | 0xFF |
0x104 | 0xAB |
0x108 | 0x13 |
0x10C | 0x11 |
一下是三個暫存器所儲存的值:
暫存器 | 值 |
%eax | 0x100 |
%ecx | 0x1 |
%edx | 0x3 |
當彙編語句中引用如下運算元所表示的值、對應的暫存器、以及分析如下:
運算元 | 值 | 註釋 | 備註 |
%eax | 0x100 | %eax | 呼叫暫存器,直接出暫存器內的值 |
0x104 | 0xAB | 絕對地址0x104 | 儲存器的絕對地址,直接出地址內的值 |
$0x108 | 0x108 | 立即數 | 相當於常量 |
(%eax) | 0xFF | 絕對地址0x100 | 間接定址,通過%eax引用地址0x100,得到值 |
4(%eax) | 0xAB | 絕對地址0x104 | 間接定址,%eax值偏置4引用地址0x104,得值 |
9(%eax,%edx) | 0x11 | 絕對地址0x10C | 間接定址,%eax、%edx值相加,再偏置9 |
260(%ecx,%edx) | 0x13 | 絕對地址0x108 | 同上 |
0xFC(,%ecx,4) | 0xFF | 絕對地址0xFF | 間接定址,%ecx值伸縮4倍(0x1*4),再偏置0xFC |
(%eax,%edx,4) | 0x11 | 絕對地址0x11 | 兩個例子的結合 |
倒數第二個例子比較有意思,很容易看成0xFC+4=0xFF,算數沒過關吧,嚯嚯,正確的應該是0xFC+4=0x100,而地址0x100裡面存的是0xFF,哇咔咔。
組合語言指令基礎
一:資料傳送指令mov
注意,組合語言的賦值語句順序和C語言是相反的。
mov實質上有兩種操作,一種是值傳送,比如movl $4, %eax,意思是把4這個數值傳送(覆蓋)給暫存器eax,也可以mov %edx, %eax實現暫存器之間的數值傳遞
另一種是將物件理解成地址,並根據這個地址找到相應的儲存器位置,讀出裡面的值並傳送,即間接定址 。還是以上面儲存器中的值為例,執行movl:
movl (%eax), %ecx // 間接定址,先獲取暫存器eax裡的值0x100,得到儲存器0x100裡的值0xFF。再賦給暫存器ecx
movl 4(%eax), %ecx //同上,先獲取4+0x100=0x104的值,再到儲存器0x104中取出0xAB,最後賦值給ecx
下面的例子是複雜的間接定址,可以參照上面的表格分析事例:
movl 257(%ecx, %edx, 2) , %ecx // 257 + 0x1 + 0x3*2 = 0x108,然後將0x108中的值0x13賦給暫存器ecx
通過上面兩條語句,暫存器ecx裡的值先賦成0xFF,再賦成0xAB,最後覆蓋成0x13。
二:棧指標%esp和幀指標%ebp
指令pushl %ebp,是壓棧,將%ebp裡的值壓入棧中,而棧指標是%esp,即棧頂,於是該語句等價於
subl $4, %esp //將棧指標的值減4——棧底是高地址,逐個往低地址擴充套件,所以要減4
movl %ebp, (%esp) //將%ebp裡的值,賦給新的棧指標中的地址所表示的儲存空間
指令pop %ebp,是出棧,將%esp裡的地址對應的空間中的值賦值給%ebp,等價於這樣兩條指令:
movl (%esp), %ebp
addl $4, %esp
……彙編裡(暫存器)的 操作只有兩種可能,將暫存器裡的值理解成儲存空間後,要麼往空間裡塞資料,要麼從空間裡把資料取出來……
%ebp和%eax兩個暫存器常被用於函式操作,比如將如下C程式碼對應到彙編程式碼:
int exchange(int *xp, int y) // movl 8(%ebp), %eax ——%ebp偏置8得到第一個形參xp,存放在%eax
{ // movl 12(%ebp), %edx ——%ebp偏置12得到第二個形參y,存放在%edx
int x = *xp; // movl (%eax), %ecx ——%eax儲存的地址儲存的值賦值給x,x區域性變數由%ecx臨時儲存
*xp = y; // movl %edx, (%eax) ——將y的值賦值給%eax間接引用的*xp
return x; // movl %ecx, %eax ——返回值也由儲存第一個形參的%eax儲存
}
至於為什麼如此分配暫存器,以後有機會討論過程連結時再細講。
三:特殊mov
假設%dh=AB, %eax=12345678
movb %dh,%al // %eax=123456AB
movsbl %dh,%eax // %eax=FFFFFFAB
movzbl %dh,%eax // %eax=000000AB
movb好理解,按照位元組來賦值,al是eax的低位,所以從78變成AB
賦值給%eax,剩下三個自己如何處理呢?
movsbl類似於算術擴充套件,要保留符號位,因此剩下位全置1
movzbl類似於邏輯擴充套件,因此剩下的全置0。
我查了半天也沒查到sbl和zbl的命名出處,自我理解是,b和l的解釋就像本文開頭表上說的那樣,代表單位元組和4位元組,兩個命令都是執行從單位元組傳送資料到四位元組。而s和z是區分剩下的三個位元組的處理策略。這裡可以舉個很簡單的例子:
char cC;
unsigned char uC;
int *cP;
unsigned int *uP;
……cC和uC賦值……;
*cp=(char )uC;//movzbl %al %edx ……單位元組無符數轉四位元組有符數
*uP=(unsigned char )cC;//movsbl %al %edx ……單位元組有符數轉四位元組無符數
上面這個例子很典型,當單位元組無符數強轉四位元組有符數時,無符數原本沒有帶符號位資訊,因此在轉成有符數時,其餘三個位元組位就用’0‘來填充,用movzbl。
當單位元組有符數強轉四位元組無符數時,有符數原本是帶有符號位資訊的,因此在轉成無符數時,其餘三個位元組也要保留符號,用movsbl。
movzbl的操作是百分之百填充’0‘的,而movsbl則不一定。如果被操作的有符數本身就是正數,則符號擴展出來也還是是’0‘,只有被操作有符數本身是負數時,才會出現“1”填充。