Linux下的ATT语法(即GNU as 汇编语法)入门
????????學(xué)習(xí)這么長(zhǎng)時(shí)間,一直在C語(yǔ)言這一層面上鉆研和打拼,日積月累,很多關(guān)于C的疑惑在書(shū)本和資料中都難以找到答案。程序員是追求完美的一個(gè)種群,其頭 腦中哪怕是存在一點(diǎn)點(diǎn)的思維黑洞都會(huì)讓其坐臥不寧。不久前在itput論壇上偶得《Computer Systems A Programmer's Perspective》(以下稱CS.APP)這本經(jīng)典好書(shū),中文有翻譯的《深入理解計(jì)算機(jī)系統(tǒng)》。是遂連夜拜讀以求解惑。雖說(shuō)書(shū)中沒(méi)有能正面的回答我的一些疑惑,但是它卻為我指明了一條通向 “無(wú)惑”之路 -- 這就是打開(kāi)匯編之門(mén)。
匯編語(yǔ)言是一門(mén)非常接近機(jī)器語(yǔ)言的語(yǔ)言,其語(yǔ)句與機(jī)器指令之間的對(duì)應(yīng)關(guān)系更加簡(jiǎn)單和清晰。打開(kāi)匯 編之門(mén)不僅僅能解除高級(jí)語(yǔ)言給你帶來(lái)的疑惑,它更能讓你更加的理解現(xiàn)代計(jì)算機(jī)的運(yùn)行體系,還有一點(diǎn)更加重要的是它給你帶來(lái)的是一種自信的感覺(jué),減少了你在 高處搖搖欲墜的恐懼,響應(yīng)了侯捷老師的“勿在浮沙筑高臺(tái)”的號(hào)召。現(xiàn)在學(xué)習(xí)匯編的目的已與以前大大不同了。正如CS.APP中所說(shuō)那樣“程序員學(xué)習(xí)匯編的 需求隨著時(shí)間的推移也發(fā)生了變化,開(kāi)始時(shí)是要求程序員能直接用匯編編寫(xiě)程序,現(xiàn)在則是要求能夠閱讀和理解優(yōu)化編譯器產(chǎn)生的代碼”。能閱讀和理解,這也恰恰 是我的需求和目標(biāo)。
以前接觸過(guò)匯編,主要是Microsoft MASM宏匯編,不過(guò)那時(shí)的認(rèn)識(shí)高度不夠加上態(tài)度不端正,錯(cuò)失了一個(gè)很好的學(xué)習(xí)機(jī)會(huì)。現(xiàn)在絕大部分時(shí)間是使用GCC在Unix系列平臺(tái)上工作,選擇匯編語(yǔ) 言當(dāng)然是GNU匯編了,恰好CS.APP中使用的也是GNU的匯編語(yǔ)法。由于學(xué)習(xí)匯編的主要目的還是“解惑”,所以形式上多是以C代碼和匯編代碼的比較。
1、匯編讓你看到更多
隨 著你使用的語(yǔ)言的層次的提高,你眼中的計(jì)算機(jī)將會(huì)越來(lái)越模糊,你的關(guān)注點(diǎn)也越來(lái)越遠(yuǎn)離語(yǔ)言本身而靠近另一端“問(wèn)題域”,比如通過(guò)JAVA,你更多看到的是 其虛擬機(jī),而看不到真實(shí)的計(jì)算機(jī);通過(guò)C,你看到的也僅僅是內(nèi)存一層;到了匯編語(yǔ)言,你就可以深入到寄存器一層自由發(fā)揮了。匯編程序員眼里的“獨(dú)特風(fēng)景” 包括:
a) “程序計(jì)數(shù)器(%eip)” -- 一個(gè)特殊寄存器,其中永遠(yuǎn)存儲(chǔ)下一條將要執(zhí)行的指令的地址;
b) 整數(shù)寄存器 -- 共8個(gè),分別是%eax、%ebx、%ecx、%edx、%esi、%ebi、%esp和%ebp,它們可以存整數(shù)數(shù)據(jù),可以存地址,也可以記錄程序狀態(tài) 等。早期每個(gè)寄存器都有其特殊的用途,現(xiàn)在由于像linux這樣的平臺(tái)多采用“平面尋址[1]”,寄存器的特殊性已經(jīng)不那么明顯了。
c) 條件標(biāo)志寄存器 -- 保存最近執(zhí)行的算術(shù)指令的狀態(tài)信息,用來(lái)實(shí)現(xiàn)控制流中的條件變化。
d) 浮點(diǎn)數(shù)寄存器 -- 顧名思義,用來(lái)存放浮點(diǎn)數(shù)。
雖說(shuō)寄存器的特殊性程度已經(jīng)弱化,但是實(shí)際上每個(gè)編譯器在使用這些寄存器時(shí)還是遵循一定的規(guī)則的,以后再說(shuō)。
2、初窺匯編
下面是一個(gè)簡(jiǎn)單的C函數(shù):
void dummy() {
int a = 1234;
int b = a;
}
我們使用gcc加-S選項(xiàng)將之轉(zhuǎn)換成匯編代碼如下(省略部分內(nèi)容):
movl $1234, -4(%ebp)
movl -4(%ebp), %eax
movl %eax, -8(%ebp)
看 了一眼又一眼,還是看不懂,只是發(fā)現(xiàn)些熟悉的內(nèi)容,因?yàn)樯厦嫣徇^(guò)如%ebp、%eax等。這只是個(gè)引子,讓我們感性的認(rèn)識(shí)一下匯編的“容貌”。我們一點(diǎn)點(diǎn) 地來(lái)看。咋看一眼匯編代碼長(zhǎng)得似乎很相似,沒(méi)錯(cuò),匯編代碼就是一條一條的“指令+操作數(shù)”的語(yǔ)句的集合。匯編指令是固定的,每條指令都有其固定的用途,而 操作數(shù)表示則有多種類(lèi)型。
1) 操作數(shù)表示
大部分匯編指令都有一個(gè)或多個(gè)操作數(shù),包括指令操作中的源和目的。一條標(biāo)準(zhǔn)的指令格式大 致是這樣的:“指令 + 源操作數(shù) + 目的操作數(shù)”,其中源操作數(shù)可以是立即數(shù)、從寄存器中讀出的數(shù)或從內(nèi)存中讀出的數(shù);而目的操作數(shù)則可以是寄存器或內(nèi)存。按這么一分類(lèi),操作數(shù)就大致有三 種:
a) 立即數(shù)表示法 -- 如“movl $1234, -4(%ebp)”中的“$1234”,就是一個(gè)立即數(shù)作為操作數(shù),按照GNU匯編語(yǔ)法,立即數(shù)表示為“$+整數(shù)”。立即數(shù)常用來(lái)表示代碼中的一些常數(shù), 如上例中的“$1234”。注意一點(diǎn)的是立即數(shù)不能作為目的操作數(shù)。
b) 寄存器表示法 -- 這種比較簡(jiǎn)單,它就是表示寄存器之內(nèi)容。如上面的“movl -4(%ebp), %eax”中的%eax就是使用寄存器表示法作源操作數(shù),而“movl %eax, -8(%ebp)”中的%eax則是使用寄存器表示法作目的操作數(shù)。
c) 內(nèi)存引用表示法 -- 計(jì)算出的該操作數(shù)的值表示的是相應(yīng)的內(nèi)存地址。匯編指令根據(jù)這個(gè)內(nèi)存地址訪問(wèn)相應(yīng)的內(nèi)存位置。如上例“movl -4(%ebp), %eax”中的“-4(%ebp)”,其表示的內(nèi)存地址為(%ebp寄存器中的內(nèi)容-4)得到的值。
2) 數(shù)據(jù)傳送指令
匯編語(yǔ)言中最最常用的指令 -- 數(shù)據(jù)傳送指令,也是我們接觸的第一種類(lèi)別的匯編指令。其指令的格式為:“mov 源操作數(shù), 目的操作數(shù)”。
mov 系列支持從最小一個(gè)字節(jié)到最大雙字的訪問(wèn)與傳送。其中movb用來(lái)傳送一字節(jié)信息,movw用來(lái)傳送二字節(jié),即一個(gè)字的信息,movl用來(lái)傳送雙字信息。 這些不詳說(shuō)了。除此以外mov系列還提供兩個(gè)帶位擴(kuò)展的指令movsbl和movzbl
==============================================================
匯編語(yǔ)言作為一種高效的,而且緊密結(jié)合硬件平臺(tái)的編程語(yǔ)言,在操作系統(tǒng),嵌入式開(kāi)發(fā)等領(lǐng)域都有著十分重要的作用。正因?yàn)閰R編依賴于硬件結(jié)構(gòu)(CPU指令碼),因此不同體系結(jié)構(gòu)上的匯編語(yǔ)言也大相徑庭。本文簡(jiǎn)單介紹了Linux下的AT&T語(yǔ)法(即GNU as 匯編語(yǔ)法),以及在Linux下匯編的基本方法。
AT&T語(yǔ)法起源于AT&T貝爾實(shí)驗(yàn)室,是在當(dāng)時(shí)用于實(shí)現(xiàn)Unix系統(tǒng)的處理器操作碼語(yǔ)法之上而形成的,AT&T語(yǔ)法和Intel語(yǔ)法主要區(qū)別如下:
AT&T使用$表示立即數(shù),Intel不用,因此表示十進(jìn)制2時(shí),AT&T為$2,而Intel就是2
AT&T在寄存器前加%,比如eax寄存器表示為%eax
AT&T 處理操作數(shù)的順序和Intel相反,比如,movl %eax, %ebx是將eax中的值傳遞給ebx,而Intel是這樣的mov ebx, eax
AT&T在助記符的后面加上一個(gè)單獨(dú)字符表示操作中數(shù)據(jù)的長(zhǎng)度,比如movl $foo, %eax等同于Intel的mov eax, word ptr foo
長(zhǎng)跳轉(zhuǎn)和調(diào)用的格式不同,AT&T為ljmp $section, $offset,而Intel為jmp section:offset
主要的區(qū)別就是這些,其他的細(xì)節(jié)還有很多,下面給出一個(gè)具體的例子來(lái)說(shuō)明
#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
這個(gè)程序的作用是查詢CPU的廠商ID,其中:
,ascii定義字符串(和Intel格式完全不同).section是聲明段的語(yǔ)句,.data和.text是段名,分別為數(shù)據(jù)段和代碼段, _start是gas(GNU匯編器)的默認(rèn)入口標(biāo)簽,表示程序從這里開(kāi)始執(zhí)行。.globl將_start聲明成了外部程序訪問(wèn)的標(biāo)簽。cpuid為指令請(qǐng)求CPU的指定信息,該指令用eax作為輸入,ebx,edx,ecx作為輸出,這里將0作為cpuid的輸入指令,請(qǐng)求返回CPU的廠商ID字符串。返回的結(jié)果,一個(gè)12字節(jié)的字符串,分別存儲(chǔ)在三個(gè)寄存器中,其中ebx存放低4位,edx中間4位,ecx高4位(注意順序!)。接下來(lái)定義一個(gè)指針edi,edi指向output的開(kāi)始地址,然后接著的3條語(yǔ)句將output里的x替換為廠商信息。28(%edi)中的28表示偏移量,即整個(gè)地址為%edi里的地址加上28個(gè)字節(jié),這個(gè)地址正好是output里第一個(gè)x的地址。再接下來(lái)就是打印結(jié)果了,這里用到了Linux的一個(gè)系統(tǒng)調(diào)用(int 0x80),該系統(tǒng)調(diào)用的參數(shù)分別為:eax 系統(tǒng)調(diào)用號(hào),ebx 要寫(xiě)入的文件描述符,ecx 字符串首地址,edx 字符串長(zhǎng)度,程序里這些個(gè)參數(shù)的值分別為4,1(標(biāo)準(zhǔn)輸出),output的地址和42。最后再次調(diào)用1號(hào)系統(tǒng)調(diào)用-退出函數(shù),返回shell,這次 ebx中的值是返回給shell的退出代碼,0表示無(wú)異常
然后匯編連接運(yùn)行程序:
[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所以返回的結(jié)果是GenuineIntel。
幾點(diǎn)說(shuō)明:
1)Linux的標(biāo)準(zhǔn)匯編環(huán)境為as,ld,gdb,gprof,objdump等GNU開(kāi)發(fā)調(diào)試工具,除了gdb外,其他全部隨binutils包發(fā)布。其中as使用的是AT&T語(yǔ)法。在Linux下也可以使用Nasm來(lái)進(jìn)行Intel格式的匯編程序編寫(xiě)
2)Linux下匯編的系統(tǒng)調(diào)用為int 0x80,和DOS下的int 21h大同小異,只不過(guò)傳遞參數(shù)不同
3)段聲明語(yǔ)句.section不需要像Intel格式那樣在段結(jié)尾的時(shí)候加上段結(jié)束標(biāo)志(SEGMENT/ENDS),下一個(gè)段的開(kāi)始自動(dòng)標(biāo)志著上個(gè)段的結(jié)束
4)簡(jiǎn)單程序的入口標(biāo)簽不是必須要定義的,ld會(huì)自己判斷入口,但是會(huì)給出警告
===========================================例子2
例 2. 求一組數(shù)的最大值的匯編程序
#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 .data data_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 registermovl data_items(,%edi,4), %eax # load the first byte of datamovl %eax, %ebx # since this is the first item, %eax is# the biggeststart_loop: # start loopcmpl $0, %eax # check to see if we've hit the endje loop_exitincl %edi # load next valuemovl data_items(,%edi,4), %eaxcmpl %ebx, %eax # compare valuesjle start_loop # jump to loop beginning if the new# one isn't biggermovl %eax, %ebx # move the value as the largestjmp start_loop # jump to loop beginningloop_exit:# %ebx is the status code for the exit system call# and it already has the maximum numbermovl $1, %eax #1 is the exit() syscallint $0x80
匯編、鏈接、執(zhí)行:
$ as max.s -o max.o $ ld max.o -o max $ ./max $ echo $?
這個(gè)程序在一組數(shù)中找到一個(gè)最大的數(shù),并把它作為程序的退出狀態(tài)。這組數(shù)在.data段給出:
data_items:.long 3,67,34,222,45,75,54,34,44,33,22,11,66,0
.long指示聲明一組數(shù),每個(gè)數(shù)占32位,相當(dāng)于C語(yǔ)言中的數(shù)組。這個(gè)數(shù)組開(kāi)頭有一個(gè)標(biāo)號(hào)data_items,匯編器會(huì)把數(shù)組的首地址作為data_items符號(hào)所代表的地址,data_items類(lèi)似于C語(yǔ)言中的數(shù)組名。data_items這個(gè)標(biāo)號(hào)沒(méi)有用.globl聲明,因?yàn)樗辉谶@個(gè)匯編程序內(nèi)部使用,鏈接器不需要知道這個(gè)名字的存在。除了.long之外,常用的數(shù)據(jù)聲明還有:
.byte,也是聲明一組數(shù),每個(gè)數(shù)占8位.ascii,例如.ascii "Hello world",聲明了11個(gè)數(shù),取值為相應(yīng)字符的ASCII碼。注意,和C語(yǔ)言不同,這樣聲明的字符串末尾是沒(méi)有'\0'字符的,如果需要以'\0'結(jié)尾可以聲明為.ascii "Hello world\0"。
data_items數(shù)組的最后一個(gè)數(shù)是0,我們?cè)谝粋€(gè)循環(huán)中依次比較每個(gè)數(shù),碰到0的時(shí)候讓循環(huán)終止。在這個(gè)循環(huán)中:
edi寄存器保存數(shù)組中的當(dāng)前位置,每次比較完一個(gè)數(shù)就把edi的值加1,指向數(shù)組中的下一個(gè)數(shù)。ebx寄存器保存到目前為止找到的最大值,如果發(fā)現(xiàn)有更大的數(shù)就更新ebx的值。eax寄存器保存當(dāng)前要比較的數(shù),每次更新edi之后,就把下一個(gè)數(shù)讀到eax中。
_start:movl $0, %edi
初始化edi,指向數(shù)組的第0個(gè)元素。
movl data_items(,%edi,4), %eax
這條指令把數(shù)組的第0個(gè)元素傳送到eax寄存器中。data_items是數(shù)組的首地址,edi的值是數(shù)組的下標(biāo),4表示數(shù)組的每個(gè)元素占4字節(jié),那么數(shù)組中第edi個(gè)元素的地址應(yīng)該是data_items + edi * 4,從這個(gè)地址讀數(shù)據(jù),寫(xiě)成指令就是上面那樣,這種地址的表示方式在下一節(jié)還會(huì)詳細(xì)解釋。
movl %eax, %ebx
ebx的初始值也是數(shù)組的第0個(gè)元素。下面我們進(jìn)入一個(gè)循環(huán),在循環(huán)的開(kāi)頭用標(biāo)號(hào)start_loop表示,循環(huán)的末尾之后用標(biāo)號(hào)loop_exit表示。
start_loop:cmpl $0, %eaxje loop_exit
比較eax的值是不是0,如果是0就說(shuō)明到達(dá)數(shù)組末尾了,就要跳出循環(huán)。cmpl指令將兩個(gè)操作數(shù)相減,但計(jì)算結(jié)果并不保存,只是根據(jù)計(jì)算結(jié)果改變eflags寄存器中的標(biāo)志位。如果兩個(gè)操作數(shù)相等,則計(jì)算結(jié)果為0,eflags中的ZF位置1。je是一個(gè)條件跳轉(zhuǎn)指令,它檢查eflags中的ZF位,ZF位為1則發(fā)生跳轉(zhuǎn),ZF位為0則不跳轉(zhuǎn),繼續(xù)執(zhí)行下一條指令。可見(jiàn)條件跳轉(zhuǎn)指令和比較指令是配合使用的,前者改變標(biāo)志位,后者根據(jù)標(biāo)志位做判斷,如果參與比較的兩數(shù)相等則跳轉(zhuǎn),je的e就表示equal。
incl %edimovl data_items(,%edi,4), %eax
將edi的值加1,把數(shù)組中的下一個(gè)數(shù)傳送到eax寄存器中。
cmpl %ebx, %eaxjle start_loop
把當(dāng)前數(shù)組元素eax和目前為止找到的最大值ebx做比較,如果前者小于等于后者,則最大值沒(méi)有變,跳轉(zhuǎn)到循環(huán)開(kāi)頭比較下一個(gè)數(shù),否則繼續(xù)執(zhí)行下一條指令。jle也是一個(gè)條件跳轉(zhuǎn)指令,le表示less than or equal。
movl %eax, %ebxjmp start_loop
更新了最大值ebx然后跳轉(zhuǎn)到循環(huán)開(kāi)頭比較下一個(gè)數(shù)。jmp是一個(gè)無(wú)條件跳轉(zhuǎn)指令,什么條件也不判斷,直接跳轉(zhuǎn)。loop_exit標(biāo)號(hào)后面的指令用exit系統(tǒng)調(diào)用退出程序。
總結(jié)
以上是生活随笔為你收集整理的Linux下的ATT语法(即GNU as 汇编语法)入门的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 电影岁月神偷的特色是啥?
- 下一篇: Facebook性能大提升的秘密:Hip