Linux下的AT&T文法(即GNU as 彙編文法)入門

來源:互聯網
上載者:User

        學習這麼長時間,一直在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),下一個段的開始自動標誌著上個段的結束

4)簡單程式的入口標籤不是必須要定義的,ld會自己判斷入口,但是會給出警告

===========================================例子2

例 2. 求一組數的最大值的組譯工具

#PURPOSE: This program finds the maximum number of a#   set of data items.##VARIABLES: The registers have the following uses:## %edi - Holds the index of the data item being examined# %ebx - Largest data item found# %eax - Current data item## The following memory locations are used:## data_items - contains the item data. A 0 is used# to terminate the data# .section .datadata_items:   #These are the data items .long 3,67,34,222,45,75,54,34,44,33,22,11,66,0 .section .text .globl _start_start: movl $0, %edi   # move 0 into the index register movl data_items(,%edi,4), %eax # load the first byte of data movl %eax, %ebx  # since this is the first item, %eax is   # the biggeststart_loop:   # start loop cmpl $0, %eax   # check to see if we've hit the end je loop_exit incl %edi   # load next value movl data_items(,%edi,4), %eax cmpl %ebx, %eax  # compare values jle start_loop  # jump to loop beginning if the new    # one isn't bigger movl %eax, %ebx  # move the value as the largest jmp start_loop  # jump to loop beginningloop_exit: # %ebx is the status code for the exit system call # and it already has the maximum number movl $1, %eax   #1 is the exit() syscall int $0x80

彙編、連結、執行:

$ as max.s -o max.o$ ld max.o -o max$ ./max$ echo $?

這個程式在一組數中找到一個最大的數,並把它作為程式的退出狀態。這組數在.data段給出:

data_items: .long 3,67,34,222,45,75,54,34,44,33,22,11,66,0

.long指示聲明一組數,每個數佔32位,相當於C語言中的數組。這個數組開頭有一個標號data_items,彙編器會把數組的首地址作為data_items符號所代表的地址,data_items類似於C語言中的數組名。data_items這個標號沒有用.globl聲明,因為它只在這個組譯工具內部使用,連結器不需要知道這個名字的存在。除了.long之外,常用的資料聲明還有:

  • .byte,也是聲明一組數,每個數佔8位

  • .ascii,例如.ascii "Hello world",聲明了11個數,取值為相應字元的ASCII碼。注意,和C語言不同,這樣聲明的字串末尾是沒有'\0'字元的,如果需要以'\0'結尾可以聲明為.ascii "Hello world\0"

data_items數組的最後一個數是0,我們在一個迴圈中依次比較每個數,碰到0的時候讓迴圈終止。在這個迴圈中:

  • edi寄存器儲存數組中的當前位置,每次比較完一個數就把edi的值加1,指向數組中的下一個數。

  • ebx寄存器儲存到目前為止找到的最大值,如果發現有更大的數就更新ebx的值。

  • eax寄存器儲存當前要比較的數,每次更新edi之後,就把下一個數讀到eax中。

_start: movl $0, %edi

初始化edi,指向數組的第0個元素。

movl data_items(,%edi,4), %eax

這條指令把數組的第0個元素傳送到eax寄存器中。data_items是數組的首地址,edi的值是數組的下標,4表示數組的每個元素佔4位元組,那麼數組中第edi個元素的地址應該是data_items + edi * 4,從這個地址讀資料,寫成指令就是上面那樣,這種地址的表示方式在下一節還會詳細解釋。

movl %eax, %ebx

ebx的初始值也是數組的第0個元素。下面我們進入一個迴圈,在迴圈的開頭用標號start_loop表示,迴圈的末尾之後用標號loop_exit表示。

start_loop: cmpl $0, %eax je loop_exit

比較eax的值是不是0,如果是0就說明到達數組末尾了,就要跳出迴圈。cmpl指令將兩個運算元相減,但計算結果並不儲存,只是根據計算結果改變eflags寄存器中的標誌位。如果兩個運算元相等,則計算結果為0,eflags中的ZF位置1。je是一個條件跳轉指令,它檢查eflags中的ZF位,ZF位為1則發生跳轉,ZF位為0則不跳轉,繼續執行下一條指令。可見條件跳轉指令和比較指令是配合使用的,前者改變標誌位,後者根據標誌位做判斷,如果參與比較的兩數相等則跳轉,je的e就表示equal。

incl %edi movl data_items(,%edi,4), %eax

edi的值加1,把數組中的下一個數傳送到eax寄存器中。

cmpl %ebx, %eax jle start_loop

把當前數組元素eax和目前為止找到的最大值ebx做比較,如果前者小於等於後者,則最大值沒有變,跳轉到迴圈開頭比較下一個數,否則繼續執行下一條指令。jle也是一個條件跳轉指令,le表示less than or equal。

movl %eax, %ebx jmp start_loop

更新了最大值ebx然後跳轉到迴圈開頭比較下一個數。jmp是一個無條件跳轉指令,什麼條件也不判斷,直接跳轉。loop_exit標號後面的指令用exit系統調用退出程式。

  • 上一篇:IxEdit傻瓜式JavaScript開發工具(附下載、漢化版、視頻教程)
  • 下一篇:22個所見即所得 (WYSIWYG)線上 Web 編輯器
  • 相關文章

    聯繫我們

    該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

    如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

    A Free Trial That Lets You Build Big!

    Start building with 50+ products and up to 12 months usage for Elastic Compute Service

    • Sales Support

      1 on 1 presale consultation

    • After-Sales Support

      24/7 Technical Support 6 Free Tickets per Quarter Faster Response

    • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.