學習這麼長時間,一直在C語言這一層面上鑽研和打拼,日積月累,很多關於C的疑惑在書本和資料中都難以找到答案。程序員是追求完美的一個種群,其頭 腦中哪怕是存在一點點的思維黑洞都會讓其坐臥不寧。不久前在itput論壇上偶得《Computer Systems A Programmer's Perspective》(以下稱CS.APP)這本經典好書,中文有翻譯的《深入理解計算機系統》。是遂連夜拜讀以求解惑。雖說書中沒有能正面的回答我的一些疑惑,但是它卻為我指明了一條通向 “無惑”之路 -- 這就是打開匯編之門。
匯編語言是一門非常接近機器語言的語言,其語句與機器指令之間的對應關系更加簡單和清晰。打開匯 編之門不僅僅能解除高級語言給你帶來的疑惑,它更能讓你更加的理解現代計算機的運行體系,還有一點更加重要的是它給你帶來的是一種自信的感覺,減少了你在 高處搖搖欲墜的恐懼,響應了侯捷老師的“勿在浮沙築高台”的號召。現在學習匯編的目的已與以前大大不同了。正如CS.APP中所說那樣“程序員學習匯編的 需求隨著時間的推移也發生了變化,開始時是要求程序員能直接用匯編編寫程序,現在則是要求能夠閱讀和理解優化編譯器產生的代碼”。能閱讀和理解,這也恰恰 是我的需求和目標。
以前接觸過匯編,主要是Microsoft MASM宏匯編,不過那時的認識高度不夠加上態度不端正,錯失了一個很好的學習機會。現在絕大部分時間是使用GCC在Unix系列平台上工作,選擇匯編語 言當然是GNU匯編了,恰好CS.APP中使用的也是GNU的匯編語法。由於學習匯編的主要目的還是“解惑”,所以形式上多是以C代碼和匯編代碼的比較。
1、匯編讓你看到更多
隨 著你使用的語言的層次的提高,你眼中的計算機將會越來越模糊,你的關注點也越來越遠離語言本身而靠近另一端“問題域”,比如通過JAVA,你更多看到的是 其虛擬機,而看不到真實的計算機;通過C,你看到的也僅僅是內存一層;到了匯編語言,你就可以深入到寄存器一層自由發揮了。匯編程序員眼裡的“獨特風景” 包括:
a) “程序計數器(%eip)” -- 一個特殊寄存器,其中永遠存儲下一條將要執行的指令的地址;
b) 整數寄存器 -- 共8個,分別是%eax、%ebx、%ecx、%edx、%esi、%ebi、%esp和%ebp,它們可以存整數數據,可以存地址,也可以記錄程序狀態 等。早期每個寄存器都有其特殊的用途,現在由於像linux這樣的平台多采用“平面尋址[1]”,寄存器的特殊性已經不那麼明顯了。
c) 條件標志寄存器 -- 保存最近執行的算術指令的狀態信息,用來實現控制流中的條件變化。
d) 浮點數寄存器 -- 顧名思義,用來存放浮點數。
雖說寄存器的特殊性程度已經弱化,但是實際上每個編譯器在使用這些寄存器時還是遵循一定的規則的,以後再說。
2、初窺匯編
下面是一個簡單的C函數:
void dummy() {
int a = 1234;
int b = a;
}
我們使用gcc加-S選項將之轉換成匯編代碼如下(省略部分內容):
movl $1234, -4(%ebp)
movl -4(%ebp), %eax
movl %eax, -8(%ebp)
看 了一眼又一眼,還是看不懂,只是發現些熟悉的內容,因為上面提過如%ebp、%eax等。這只是個引子,讓我們感性的認識一下匯編的“容貌”。我們一點點 地來看。咋看一眼匯編代碼長得似乎很相似,沒錯,匯編代碼就是一條一條的“指令+操作數”的語句的集合。匯編指令是固定的,每條指令都有其固定的用途,而 操作數表示則有多種類型。
1) 操作數表示
大部分匯編指令都有一個或多個操作數,包括指令操作中的源和目的。一條標准的指令格式大 致是這樣的:“指令 + 源操作數 + 目的操作數”,其中源操作數可以是立即數、從寄存器中讀出的數或從內存中讀出的數;而目的操作數則可以是寄存器或內存。按這麼一分類,操作數就大致有三 種:
a) 立即數表示法 -- 如“movl $1234, -4(%ebp)”中的“$1234”,就是一個立即數作為操作數,按照GNU匯編語法,立即數表示為“$+整數”。立即數常用來表示代碼中的一些常數, 如上例中的“$1234”。注意一點的是立即數不能作為目的操作數。
b) 寄存器表示法 -- 這種比較簡單,它就是表示寄存器之內容。如上面的“movl -4(%ebp), %eax”中的%eax就是使用寄存器表示法作源操作數,而“movl %eax, -8(%ebp)”中的%eax則是使用寄存器表示法作目的操作數。
c) 內存引用表示法 -- 計算出的該操作數的值表示的是相應的內存地址。匯編指令根據這個內存地址訪問相應的內存位置。如上例“movl -4(%ebp), %eax”中的“-4(%ebp)”,其表示的內存地址為(%ebp寄存器中的內容-4)得到的值。
2) 數據傳送指令
匯編語言中最最常用的指令 -- 數據傳送指令,也是我們接觸的第一種類別的匯編指令。其指令的格式為:“mov 源操作數, 目的操作數”。
mov 系列支持從最小一個字節到最大雙字的訪問與傳送。其中movb用來傳送一字節信息,movw用來傳送二字節,即一個字的信息,movl用來傳送雙字信息。 這些不詳說了。除此以外mov系列還提供兩個帶位擴展的指令movsbl和movzbl
==============================================================
匯編語言作為一種高效的,而且緊密結合硬件平台的編程語言,在操作系統,嵌入式開發等領域都有著十分重要的作用。正因為匯編依賴於硬件結構(CPU指令碼),因此不同體系結構上的匯編語言也大相徑庭。本文簡單介紹了Linux下的AT&T語法(即GNU as 匯編語法),以及在Linux下匯編的基本方法。
AT&T語法起源於AT&T貝爾實驗室,是在當時用於實現Unix系統的處理器操作碼語法之上而形成的,AT&T語法和Intel語法主要區別如下:
AT&T使用$表示立即數,Intel不用,因此表示十進制2時,AT&T為$2,而Intel就是2
AT&T在寄存器前加%,比如eax寄存器表示為%eax
AT&T 處理操作數的順序和Intel相反,比如,movl %eax, %ebx是將eax中的值傳遞給ebx,而Intel是這樣的mov ebx, eax
AT&T在助記符的後面加上一個單獨字符表示操作中數據的長度,比如movl $foo, %eax等同於Intel的mov eax, word ptr foo
長跳轉和調用的格式不同,AT&T為ljmp $section, $offset,而Intel為jmp section:offset
主要的區別就是這些,其他的細節還有很多,下面給出一個具體的例子來說明
#cpuid.s Sample program
.section .data
output:
.ascii "The processor Vendor ID is 'xxxxxxxxxxxx'n"
.section .text
.globl _start
_start:
movl $0, %eax
cpuid
movl $output, %edi
movl %ebx, 28(%edi)
movl %edx, 32(%edi)
movl %ecx, 36(%edi)
movl $4, %eax
movl $1, %ebx
movl $output, %ecx
movl $42, %edx
int $0x80
movl $1, %eax
movl $0, %ebx
int $0x80
這個程序的作用是查詢CPU的廠商ID,其中:
,ascii定義字符串(和Intel格式完全不同).section是聲明段的語句,.data和.text是段名,分別為數據段和代碼段, _start是gas(GNU匯編器)的默認入口標簽,表示程序從這裡開始執行。.globl將_start聲明成了外部程序訪問的標簽。cpuid為指令請求CPU的指定信息,該指令用eax作為輸入,ebx,edx,ecx作為輸出,這裡將0作為cpuid的輸入指令,請求返回CPU的廠商ID字符串。返回的結果,一個12字節的字符串,分別存儲在三個寄存器中,其中ebx存放低4位,edx中間4位,ecx高4位(注意順序!)。接下來定義一個指針edi,edi指向output的開始地址,然後接著的3條語句將output裡的x替換為廠商信息。28(%edi)中的28表示偏移量,即整個地址為%edi裡的地址加上28個字節,這個地址正好是output裡第一個x的地址。再接下來就是打印結果了,這裡用到了Linux的一個系統調用(int 0x80),該系統調用的參數分別為:eax 系統調用號,ebx 要寫入的文件描述符,ecx 字符串首地址,edx 字符串長度,程序裡這些個參數的值分別為4,1(標准輸出),output的地址和42。最後再次調用1號系統調用-退出函數,返回shell,這次 ebx中的值是返回給shell的退出代碼,0表示無異常
然後匯編連接運行程序:
[root@zieckey-laptop src]# as -o cpuid.o cpuid.s
[root@zieckey-laptop src]# ld cpuid.o -o cpuid
[root@zieckey-laptop src]# ./cpuid
The processor Vendor ID is 'GenuineIntel'
[root@zieckey-laptop src]#
本人的電腦是Pentium M的CPU所以返回的結果是GenuineIntel。
幾點說明:
1)Linux的標准匯編環境為as,ld,gdb,gprof,objdump等GNU開發調試工具,除了gdb外,其他全部隨binutils包發布。其中as使用的是AT&T語法。在Linux下也可以使用Nasm來進行Intel格式的匯編程序編寫
2)Linux下匯編的系統調用為int 0x80,和DOS下的int 21h大同小異,只不過傳遞參數不同
3)段聲明語句.section不需要像Intel格式那樣在段結尾的時候加上段結束標志(SEGMENT/ENDS),下一個段的開始自動標志著上個段的結