就像軟體的真諦——“給我一個標準,我用我的邏輯舞動世界”一樣,AT&T 彙編是組合語言裏的另一種標準,這是相對於鼎鼎大名的intel的x86彙編來說。即使對於電子專業的學生來說,一旦掌握了c51單片機的彙編,x86的彙編也已經入門了,但x86畢竟有著強大的寄存器,在串操作指令和作業系統類指令方面,單片機還是望塵莫及的。
下面的兩段話就輕易的涉及到了關於首碼、運算元的方向、操作碼的尾碼這些概念的不同。關於首碼,AT&T 彙編中,寄存器前被冠以“%”,立即數前被冠以“$”,十六進位數前被冠以“0x”。所以,如果gemfield在AT&T 語境中說到386的通用寄存器時,會這樣描述:8個32-bit寄存器 %eax,%ebx,%ecx,%edx,%edi,%esi,%ebp,%esp。比如:在stage1.s中,有這麼一個定義,#define ABS(x) (x-_start+0x7c00),那麼你就會知到0x7c00是個十六進位數(_start函數的入口位址就位於記憶體的0x7c00處)。
而在設置int 0x13的0x42功能號時,它是這麼說的:movb $0x42,%ah 。這句告訴了我們一些不同之處:首先,操作碼的尾碼l表示的是操作碼的大小,l是長整數32位元,那麼相應的,movw是16位,movb是8位;其次,立即數是用$首碼來表示的,就像$0x42;再次,寄存器的名字是有%首碼的,像例子中的%ah;最後,運算元的方向有點不一樣,比如把立即數$0x42放到寄存器%ah中,用的是movb $0x42,%ah,也即源運算元在前,目的運算元在後。
對於記憶體單元運算元來說,在AT&T 中是把寄存器用()括起來,而非【】。比如,movl %ebx,8(%si),將ebx寄存器裏的值放到記憶體位址是8(%si)的記憶體單元上。正好,這裏同時遇到了另一個問題,就是在AT&T 彙編中,間接定址方式是有別於x86的。上例中的8(%si)就相當於x86中的【si+8】。
還有一種叫做label(標號)的程式控制語句,比如,在stage1.s中,有這麼一段指令:
cmpb $GRUB_INVALID_DRIVE,%al
je 1f
movb %al,%dl
1:
pushw %dx
上面就用到了標號,je1f,前面的兩個數進行比較,如果相等就跳轉到1的位置。注意,1後面的f表示的是forward,即從je指令後繼續往前走來尋找1這個標號。所以,如果程式中有好幾個叫做1的標號,就要看是1f還是1b了,b代表backward,方向和f相反。青島之光論壇裏有這麼一個例子可以更好的幫助我們理解:
je 1f或者je 1b 是跳轉到對應的標號的地方。這裏的1表示標號(label),f和b表示向前還是向後,f(forward)向前,b(backward)向後,如下例:
1行 1: cmp $0, (%si)
2行 je 1f ///////跳轉到後面的1標示的地方,也就是第6行
3行 movsb
4行 stosb
5行 jmp 1b ////////跳轉到前面1表示的地方 ,也就是第1行
6行 1: jmp 1b ////////跳轉到前面1表示的地方,第6行,其實就是個閉環
然後,在AT&T 彙編中出現最多的大概就是稱作assembler directive的東西了,我們稱作“AT&T 組合語言程式代碼控制”。所有的彙編器命令名都由句號('.')開頭。命令名的其餘是字母,通常使用小寫。在青島之光論壇 http://www.civilnet.cn/bbs/read.php?tid=970 處有一篇關於程式碼控制的詳細介紹,下面gemfield只挑出幾個常用的來說明一下。
一、.byte 運算式(expression_rs)
.byte.byte可不帶參數或者帶多個運算式參數,運算式之間由逗點分隔。每個運算式參數都被彙編成下一個位元組。在stage1.s中,有這麼一段代碼:
after_BPB:
CLI
.byte 0x80,0xca
那麼編譯器在編譯時,就會在cli指令的下面接著放上0x80和0xca,因為每個運算式要佔用1個位元組,所以此處一共佔用2個位元組。
二、.word 運算式
這個運算式表示任意一節中的一個或多個運算式(同樣用逗號分開),運算式占一個字(兩個位元組)。類似的還有.long。例:.word 0x800
三、.file 字元(string)
.file 通告編譯器我們準備開啟一個新的邏輯檔。 string 是新檔案名。總的來說,檔案名是否使用引號‘"’都可以;但如果您希望規定一個空檔案名時,必須使用引號""。本語
句將來可能不再使用—允許使用它只是為了與舊版本的as編譯器程式相容。在as的一些配置中,已經刪除了.file以避免與其它的彙編器衝突。例如stage1.s中:.file ”stage1.s”。
四、.text 小節(subsection)
通知as編譯器把後續語句彙編到編號為subsection的正文子段的末尾,subsection是一個純粹的運算式。如果省略了參數subsection,則使用編號為0的子段。例:.text
五 、.code16
告訴編譯器生成16位元的指令
六、.globl symbol
.globl使得連接程式(ld)能夠看到symbol,如果gemfield在局部程式中定義了symbol,那麼與這個局部程式鏈結的局部程式也能存取symbol,例:
.globl SYMBOL_NAME(idt) 定義idt為全局符號。
七、.fill repeat , size , value
repeat, size 和value都必須是純粹的運算式。本命令生成size個位元組的repeat個副本。
Repeat可以是0或更大的值。Size 可以是0或更大的值, 但即使size大於8,也被視作8,以
相容其他的彙編器。各個副本中的內容取自一個8位元組長的數。最高4個位元組為零,最低的
4個位元組是value,它以as正在彙編的目標電腦的整數位元組順序排列。每個副本中的size
個位元組都取值於這個數最低的size個位元組。再次說明,這個古怪的動作只是為了相容其他
的彙編器。size參數和value參數是可選的。如果不存在第2個逗號和value參數,則假定value為零。如果不存在第1個逗號和其後的參數,則假定size為1。
例如,在linux初始化的過程中,對全局描述符表GDT進行設置的最後一句為:
.fill NR_CPUS*4,8,0,意思是.fill給每個cpu留有存放4個描述符的位置,並且每個描述符是8個位元組。
等等,不一而足。不過要注意的是,這種包含程式已初始化資料的節(.data)和包含程式程式還未初始化的資料的節(.bss),編譯器會把它們在4位元組上對齊,例如,.data是25位元組,那麼編譯器會將它放在28個位元組上。.
當這種以尾碼名.s編寫的A T&T格式的彙編代碼完成後,就是編譯和鏈結了。linux下有兩種方式,一種是使用組合語言程式GAS和連接程式ld:
as filename.s –o filename.o
ld filename.o –o filename 最終將源代碼轉換為目標檔.o再連接為可執行檔filename
另一種就是大名鼎鼎的被gemfield提到過的gcc:
gcc –o gemfield gemfield.S
根源程式gemfield.S的尾碼名可以使用大寫,是因為這樣可以使gcc自動識別組合語言程式中的c預處理命令,包括頭檔中的情況,像#include、#define 、#ifdef等。
本文少了嵌入式彙編的形式,才使得AT&T 彙編看起來井井有條,而非想像中的艱難。如果想要真正鍛煉一下這種彙編,源代碼stage1.s就是一個絕佳的習題。
本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/wangshenwq/archive/2009/10/19/4700481.aspx
AT&T的巨集指令 .p2align[wl]
.p2align[wl] abs-expr, abs-expr, abs-expr
Pad the location counter (in the current subsection) to a particular storage boundary. The first expression (which must be absolute) is the number of low-order zero bits the location counter must have after advancement. For example `.p2align 3' advances the location counter until it a multiple of 8. If the location counter is already a multiple of 8, no change is needed.
The second expression (also absolute) gives the fill value to be stored in the padding bytes. It (and the comma) may be omitted. If it is omitted, the padding bytes are normally zero. However, on some systems, if the section is marked as containing code and the fill value is omitted, the space is filled with no-op instructions.
The third expression is also absolute, and is also optional. If it is present, it is the maximum number of bytes that should be skipped by this alignment directive. If doing the alignment would require skipping more bytes than the specified maximum, then the alignment is not done at all. You can omit the fill value (the second argument) entirely by simply using two commas after the required alignment; this can be useful if you want the alignment to be filled with no-op instructions when appropriate.
The .p2alignw and .p2alignl directives are variants of the .p2align directive. The .p2alignw directive treats the fill pattern as a two byte word value. The .p2alignl directives treats the fill pattern as a four byte longword value. For example, .p2alignw 2,0x368d will align to a multiple of 4. If it skips two bytes, they will be filled in with the value 0x368d (the exact placement of the bytes depends upon the endianness of the processor). If it skips 1 or 3 bytes, the fill value is undefined.
Gcc嵌入式彙編
在Linux的源代碼中,有很多C語言的函數中嵌入一段組合語言程式段,這就是gcc提供的“asm”功能,例如在include/asm-i386/system.h中定義的,讀控制寄存器CR0的一個宏read_cr0(): #define read_cr0() ({ \
unsigned int __dummy; \
__asm__( \
"movl %%cr0,%0\n\t" \
"=r" (__dummy)); \
__dummy; \
}) 這種形式看起來比較陌生,這是因為這不是標準C所定義的形式,而是gcc 對C語言的擴充。其中__dummy為C函數所定義的變數;關鍵字__asm__表示彙編代碼的開始。括弧中第一個引號中為彙編指令movl,緊接著有一個冒號,這種形式閱讀起來比較複雜。
一般而言,嵌入式組合語言片段比單純的組合語言代碼要複雜得多,因為這裏存在怎樣分配和使用寄存器,以及把C代碼中的變數應該存放在哪個寄存器中。為了達到這個目的,就必須對一般的C語言進行擴充,增加對編譯器的指導作用,因此,嵌入式彙編看起來晦澀而難以讀懂。
1. 嵌入式彙編的一般形式:
__asm__ __volatile__ ("
其中,__asm__表示彙編代碼的開始,其後可以跟__volatile__(這是可選項),其含義是避免“asm”指令被刪除、移動或組合;然後就是小括弧,括弧中的內容是我們介紹的重點:
· "
· 輸出部分(output),用以規定對輸出變數(目標運算元)如何與寄存器結合的約束(constraint),輸出部分可以有多個約束,互相以逗號分開。每個約束以“=”開頭,接著用一個字母來表示運算元的類型,然後是關於變數結合的約束。例如,上例中:
:"=r" (__dummy)
“=r”表示相應的目標運算元(指令部分的%0)可以使用任何一個通用寄存器,並且變數__dummy 存放在這個寄存器中,但如果是:
:“=m”(__dummy)
“=m”就表示相應的目標運算元是存放在記憶體單元__dummy中。
表示約束條件的字母很多,表 2-5 給出幾個主要的約束字母及其含義:
表2.5 主要的約束字母及其含義
· 輸入部分(Input):輸入部分與輸出部分相似,但沒有“=”。如果輸入部分一個運算元所要求使用的寄存器,與前面輸出部分某個約束所要求的是同一個寄存器,那就把對應運算元的編號(如“1”,“2”等)放在約束條件中,在後面的例子中,我們會看到這種情況。
· 修改部分(modify):這部分常常以“memory”為約束條件,以表示操作完成後記憶體中的內容已有改變,如果原來某個寄存器的內容來自記憶體,那麼現在記憶體中這個單元的內容已經改變。
注意,指令部分為必選項,而輸入部分、輸出部分及修改部分為可選項,當輸入部分存在,而輸出部分不存在時,分號“:“要保留,當“memory”存在時,三個分號都要保留,例如system.h中的巨集定義__cli():
#define __cli() __asm__ __volatile__("cli": : :"memory")
2. Linux源代碼中嵌入式彙編舉例
Linux源代碼中,在arch目錄下的.h和.c檔中,很多檔都涉及嵌入式彙編,下面以system.h中的C函數為例,說明嵌入式彙編的應用。
(1)簡單應用
#define __save_flags(x) __asm__ __volatile__("pushfl ; popl %0":"=g" (x): /* no input */)
#define __restore_flags(x) __asm__ __volatile__("pushl %0 ; popfl": /* no output */
:"g" (x):"memory", "cc") 第一個巨集是保存標誌寄存器的值,第二個巨集是恢復標誌寄存器的值。第一個巨集中的pushfl指令是把標誌寄存器的值壓棧。而popl是把棧頂的值(剛壓入棧的flags)彈出到x變數中,這個變數可以存放在一個寄存器或記憶體中。這樣,你可以很容易地讀懂第二個宏。 (2) 較複雜應用
static inline unsigned long get_limit(unsigned long segment)
{
unsigned long __limit;
__asm__("lsll %1,%0"
:"=r" (__limit):"r" (segment));
return __limit+1;
} 這是一個設置段界限的函數,彙編代碼段中的輸出參數為__limit(即%0),輸入參數為segment(即%1)。Lsll是載入段界限的指令,即把segment段描述符中的段界限欄位裝入某個寄存器(這個寄存器與__limit結合),函數返回__limit加1,即段長。
(3)複雜應用
在Linux內核代碼中,有關字串操作的函數都是通過嵌入式彙編完成的,因為內核及用戶程式對字串函數的調用非常頻繁,因此,用彙編代碼實現主要是為了提高效率(當然是以犧牲可讀性和可維護性為代價的)。在此,我們僅列舉一個字串比較函數strcmp,其代碼在arch/i386/string.h中。
static inline int strcmp(const char * cs,const char * ct)
{
int d0, d1;
register int __res;
__asm__ __volatile__(
"1:\tlodsb\n\t"
"scasb\n\t"
"jne 2f\n\t"
"testb %%al,%%al\n\t"
"jne 1b\n\t"
"xorl %%eax,%%eax\n\t"
"jmp 3f\n"
"2:\tsbbl %%eax,%%eax\n\t"
"orb $1,%%al\n"
"3:"
:"=a" (__res), "=&S" (d0), "=&D" (d1)
:"1" (cs),"2" (ct));
return __res;
} 其中的“\n”是換行符,“\t”是tab符,在每條命令的結束加這兩個符號,是為了讓gcc把嵌入式彙編代碼翻譯成一般的彙編代碼時能夠保證換行和留有一定的空格。例如,上面的嵌入式彙編會被翻譯成:
1: lodsb //裝入串運算元,即從[esi]傳送到al寄存器,然後esi指向串中下一個元素
scasb //掃描串運算元,即從al中減去es:[edi],不保留結果,只改變標誌
jne2f //如果兩個字元不相等,則轉到標號2
testb %al %al
jne 1b
xorl %eax %eax
jmp 3f
2: sbbl %eax %eax
orb $1 %al
3:
這段代碼看起來非常熟悉,讀起來也不困難。其中1f 表示往前(forword)找到第一個標號為1的那一行,相應地,1b表示往後找。其中嵌入式彙編代碼中輸出和輸入部分的結合情況為:
· 返回值__res,放在al寄存器中,與%0相結合;
· 局部變數d0,與%1相結合,也與輸入部分的cs參數相對應,也存放在寄存器ESI中,即ESI中存放源字串的起始位址。
· 局部變數d1,與%2相結合,也與輸入部分的ct參數相對應,也存放在寄存器EDI中,即EDI中存放目的字串的起始位址。
通過對這段代碼的分析我們應當體會到,萬變不利其本,嵌入式彙編與一般彙編的區別僅僅是形式,本質依然不變。因此,全面掌握Intel 386 彙編指令乃突破閱讀低層代碼之根本。
Intel386彙編指令摘要
在閱讀Linux源代碼時,你可能遇到很多彙編指令,有些是你熟悉的,有些可能不熟悉,在此簡要列出一些常用的386彙編指令及其功能。
1. 位元操作指令
指令 功能
BT 位測試
BTC 位測試並求反
BTR 位測試並復位
BTS 位測試並置位
2.控制轉移類指令
指令 功能
CALL 調用過程
JMP 跳轉
LOOP 用ECX計數器的迴圈
LOOPNZ/LOOPNE 用ECX計數器且不為0的迴圈/用ECX計數器且不等的迴圈
RET 返回
3. 資料傳輸指令
指令 功能
IN 從埠輸入
LEA 裝入有效位址
MOV 傳送
OUT 從段口輸出
POP 從堆疊中彈出
POPA/POPAD 從棧彈出至所有寄存器
PUSH 壓棧
PUSH/PUSHAD 所有通用寄存器壓棧
XCHG 交換
4.標誌控制類指令
指令 功能
CLC 清0進位元標誌
CLD 清0方向標誌
CLI 清0中斷標誌
LAHF 將標誌寄存器裝入AH寄存器
POPF/POPFD 從棧中彈出至標誌位元
PUSHF/PUSHFD 將標誌壓棧
SAHF 將AH寄存器存入標誌寄存器
STC 置進位元標誌
STD 置方向標誌
STI 置中斷標誌
5.邏輯類指令
指令 功能
NOT 與
AND 非
OR 或
SAL/SHL 算術作移/邏輯左移
SAR 算術右移
SHLD 邏輯右移
TEST 邏輯比較
XOR 異或
6.串操作指令
指令 功能
CMPS/CMPSB/CMPSW/CMPSD 比較串運算元
INS/INSB/INSW/INSD 輸入串運算元
LODS/LODSB/LODSW/LODSD 裝入串運算元
MOVS/MOVSB/MOVSW/MOVSD 傳送串運算元
REP 重複
REPE/REPZ 相等時重複/為0時重複
SCAS/SCASB/SCASW/SCASD 掃描串運算元
STOS/STOSB/STOSW/STOSD 存儲串運算元
7.多段類操作指令
指令 功能
CALL 程序呼叫
INT 中斷過程的調用
INTO 溢出中斷過程的調用
IRET 中斷返回
JMP 跳轉
LDS 將指針轉入DS
LES 將指針轉入ES
LFS 將指針轉入FS
LGS 將指針轉入GS
LSS 將指針轉入SS
MOV 段寄存器的傳送
POP 從棧彈出至段寄存器
PUSH 壓棧
RET 返回
8.作業系統類指令
指令 功能
APPL 調整請求特權級
ALTS 清任務切換標誌
HLT 暫停
LAR 載入訪問權
LGDT 載入全局描述符表
LIDT 載入中斷描述符表
LLDT 載入局部描述符表
LMSW 載入機器狀態字
LSL 載入段界限
LTR 載入任務寄存器
MOV 特殊寄存器的資料傳送
SGDT 存儲全局描述符表
SIDT 存儲中斷描述符表
SMSW 存儲機器狀態字
STR 存儲任務寄存器
第六講 Linux中的組合語言
Linux中的組合語言
在閱讀Linux源代碼時,你可能碰到一些組合語言片段,有些組合語言出現在以.S為副檔名的彙編檔中,在這種檔中,整個程式全部由組合語言組成。有些彙編命令出現在以.c為副檔名的C檔中,在這種檔中,既有 C語言,也有組合語言,我們把出現在C代碼中的組合語言叫所“嵌入式”彙編。 不管這些彙編代碼出現在哪里,它在一定程度上都成為閱讀源代碼的攔路虎。
儘管C語言已經成為編寫作業系統的主要語言,但是,在作業系統與硬體打交道的過程中,在需要頻繁調用的函數中以及某些特殊的場合中,C語言顯得力不從心,這時,繁瑣但又高效的組合語言必須粉墨登場。 因此,在瞭解一些硬體的基礎上,必須對相關的組合語言知識也所有瞭解。
讀者可能有過在DOS作業系統下編寫組合語言程式的經歷,也具備一定的彙編知識。但是,在 Linux 的源代碼中,你可能看到了與 Intel的組合語言格式不一樣的形式,這就是AT&T的386組合語言。
一、AT&T與Intel組合語言的比較
我們知道,Linux是Unix家族的一員,儘管Linux的歷史不長,但與其相關的很多事情都發源於Unix。 就Linux所使用的386組合語言而言,它也是起源於Unix。 Unix最初是為PDP-11開發的,曾先後被移植到VAX及68000系列的處理器上,這些處理器上的彙編語言都採用的是AT&T的指令格式。當Unix被移植到i386時,自然也就採用了AT&T的匯編語言格式,而不是Intel的格式。儘管這兩種組合語言在語法上有一定的差異,但所基於的硬體知識是相同的,因此,如果你非常熟悉 Intel的語法格式,那麼你也可以很容易地把它“移植“到AT&T來。下面我們通過對照Intel與AT&T的語法格式,以便於你把過去的知識能很快地“移植”過來。
1.首碼
在Intel的語法中,寄存器和和立即數都沒有首碼。但是在AT&T中,寄存器前冠以“%”,而立即數前冠以“$”。在Intel的語法中,十六進位和二進位立即數尾碼分別冠以“h”和“b”,而在AT&T中,十六進位立即數前冠以“0x”,表2.2給出幾個相應的例子。
表2.2 Intel與AT&T首碼的區別
Intel語法: AT&T語法:
mov eax,8 Movl $8,%eax
mov ebx,0ffffh Movl $0xffff,%ebx
int 80h int $0x80
2. 運算元的方向
Intel與AT&T運算元的方向正好相反。在Intel語法中,第一個運算元是目的運算元,第二個運算元源運算元。而在AT&T中,第一個數是源運算元,第二個數是目的運算元。 由此可以看出,AT&T 的語法符合人們通常的閱讀習慣。
例如:
在Intel中, mov eax,[ecx] 在AT&T中,movl (%ecx),%eax
3.記憶體單元運算元
從上面的例子可以看出,記憶體運算元也有所不同。在 Intel的語法中,基寄存器用“[]”括起來,而在AT&T中,用“()”括起來。
例如:
在Intel中,mov eax,[ebx+5]
在AT&T,movl 5(%ebx),%eax
4.間接定址方式
與Intel的語法比較,AT&T間接定址方式可能更晦澀難懂一些。
Intel的指令格式是segreg:[base+index*scale+disp],而AT&T的格式是%segreg:disp(base,index,scale)。其中index/scale/disp/segreg全部是可選的,完全可以簡化掉。如果沒有指定scale而指定了index,則scale的缺省值為1。segreg段寄存器依賴于指令以及應用程式是運行在實模式還是保護模式下,在實模式下,它依賴於指令,而在保護模式下,segreg是多餘的。在AT&T中,當立即數用在scale/disp中時,不應當在其前冠以“$”首碼,表2.3給出其語法及幾個相應的例子。
表2.3 記憶體運算元的語法及舉例
Intel語法: AT&T語法:
mov eax,[ebx+20h] Movl0x20(%ebx),%eax
add eax,[ebx+ecx*2h Addl (%ebx,%ecx,0x2),%eax
lea eax,[ebx+ecx] Leal (%ebx,%ecx),%eax
sub eax,[ebx+ecx*4h-20h] Subl -0x20(%ebx,%ecx,0x4),%eax
指令 foo,segreg:[base+index*scale+disp] 指令 %segreg:disp(base,index,scale),foo
從表中可以看出,AT&T的語法比較晦澀難懂,因為[base+index*scale+disp]一眼就可以看出其含義,而disp(base,index,scale)則不可能做到這點。
這種定址方式常常用在訪問資料結構陣列中某個特定元素內的一個欄位,其中,base為陣列的起始位址,scale為每個陣列元素的大小,index為下標。如果陣列元素還是一個結構,則disp為具體欄位在結構中的位移。
5.操作碼的尾碼
在上面的例子中你可能已注意到,在AT&T的操作碼後面有一個尾碼,其含義就是指出操作碼的大小。“l”表示長整數(32位元),“w”表示字(16位元),“b”表示位元組(8位)。 而在Intel的語法中,則要在記憶體單元運算元的前面加上byte ptr、 word ptr,和dword ptr,“dword”對應“long”。表2.4給出幾個相應的例子。
表2.4 操作碼的尾碼舉例
Intel語法 AT&T語法
Mov al,bl movb %bl,%al
Mov ax,bx movw %bx,%ax
Mov eax,ebx movl %ebx,%eax
Mov eax, dword ptr [ebx] movl (%ebx),%eax
二、 AT&T組合語言的相關知識
在Linux源代碼中,以.S為副檔名的文件是“純”組合語言的文件。這裏,我們結合具體的例子再介紹一些AT&T組合語言的相關知識。
1.GNU組合語言程式GAS GNU Assembly和連接程式當你編寫了一個程式後,就需要對其進行彙編(assembly)和連接。在Linux下有兩種方式,一種是使用組合語言程式GAS和連接程式ld,一種是使用gcc。我們先來看一下GAS和ld:
GAS把組合語言原始檔案(.o)轉換為目標檔(.o),其基本語法如下:
as filename.s -o filename.o
一旦創建了一個目標檔,就需要把它連接並執行,連接一個目標檔的基本語法為:
ld filename.o -o filename
這裏 filename.o是目標檔案名,而filename 是輸出(可執行) 檔。GAS使用的是AT&T的語法而不是Intel的語法,這就再次說明了AT&T語法是Unix世界的標準,你必須熟悉它。如果要使用GNC的C編譯器gcc,就可以一步完成彙編和連接,例如:
gcc -o example example.S
這裏,example.S是你的組合語言程式,輸出檔(可執行檔)名為example。其中,擴展名必須為大寫的S,這是因為,大寫的S可以使gcc自動識別組合語言程式中的C預處理命令,像#include、 #define、 #ifdef、 #endif等,也就是說,使用gcc進行編譯,你可以在組合語言程式中使用C的預處理命令。
2. AT&T中的節(Section)
在AT&T的語法中,一個節由.section關鍵字來標識,當你編寫組合語言程式時,至少需要有以下三種節:
.section .data: 這種節包含程式已初始化的資料,也就是說,包含具有初值的那些變量,例如:
hello : .string "Hello world!\n"
hello_len : .long 13
.section .bss:這個節包含程式還未初始化的資料,也就是說,包含沒有初值的那些變量。當操作 系統裝入這個程式時將把這些變數都置為0,例如:
name : .fill 30 # 用來請求用戶輸入名字
name_len : .long 0 # 名字的長度 (尚未定義)
當這個程式被裝入時,name 和 name_len都被置為0。如果你在.bss節不小心給一個變數賦了初值,這個值也會丟失,並且變數的值仍為0。
使用.bss比使用.data的優勢在於,.bss節不佔用磁片的空間。在磁片上,一個長整數就足以存放.bss節。當程式被裝入到記憶體時,作業系統也只分配給這個節4個位元組的記憶體大小。 注意:編譯程序把.data和.bss在4位元組上對齊(align),例如,.data總共有34字節,那麼編譯程序把它對其在36位元組上,也就是說,實際給它36位元組的空間。
.section .text :這個節包含程式的代碼,它是唯讀節,而.data 和.bss是讀/寫節。
3.組合語言程式指令(Assembler Directive)
上面介紹的.section就是組合語言程式指令的一種,GNU組合語言程式提供了很多這樣的指令(directiv),這種指令都是以句點(.)為開頭,後跟指令名(小寫字母),在此,我們只介紹在內核源代碼中出現的幾個指令(以arch/i386/kernel/head.S中的代碼為例)。
(1)ascii "string"...
.ascii 表示零個或多個(用逗號隔開)字串,並把每個字串(結尾不自動加“0“位元組)中的字元放在連續的位址單元。
還有一個與.ascii類似的.asciz,z代表“0“,即每個字串結尾自動加一個”0“位元組,例如:
int_msg:
.asciz "Unknown interrupt\n"
(2).byte 運算式
.byte表示零或多個運算式(用逗號隔開),每個運算式被放在下一個位元組單元。
(3).fill 運算式
形式:.fill repeat , size , value
其中,repeat、 size 和value都是常量運算式。 Fill的含義是反復拷貝size個位元組。
Repeat可以大於等於0。 size也可以大於等於0,但不能超過8,如果超過8,也只取8。把repeat個位元組以8個為一組,每組的最高4個位元組內容為0,最低4位元組內容置為value。 Size和 value為可選項。如果第二個逗號和value值不存在,則假定value為0。如果第一個逗號和size不存在,則假定size為1。
例如,在Linux初始化的過程中,對全局描述符表GDT進行設置的最後一句為:
.fill NR_CPUS*4,8,0 /* space for TSS's and LDT's */
因為每個描述符正好占 8個位元組,因此,.fill給每個CPU留有存放4個描述符的位置。
(4).globl symbol
.globl使得連接程式(ld)能夠看到symbl。如果你的局部程式中定義了symbl,那麼,與這個局部程式連接的其他局部程式也能存取symbl,例如:
.globl SYMBOL_NAME(idt)
.globl SYMBOL_NAME(gdt)
定義idt和gdt為全局符號。
(5)quad bignums
.quad表示零個或多個bignums(用逗號分隔),對於每個bignum,其缺省值是8字節整數。如果bignum超過8位元組,則列印一個警告資訊;並只取bignum最低8位元組。例如,對全局描述符表的填充就用到這個指令:
.quad 0x00cf9a000000ffff /* 0x10 kernel 4GB code at 0x00000000 */
.quad 0x00cf92000000ffff /* 0x18 kernel 4GB data at 0x00000000 */
.quad 0x00cffa000000ffff /* 0x23 user 4GB code at 0x00000000 */
.quad 0x00cff2000000ffff /* 0x2b user 4GB data at 0x00000000 */
(6)rept count
把.rept指令與.endr指令之間的行重複count次,例如
.rept 3 .long 0
.endr
相當於
.long 0
.long 0
.long 0
(7)space size , fill
這個指令保留size個位元組的空間,每個位元組的值為fill。 size 和fill都是常量運算式。
如果逗號和fill被省略,則假定fill為0,例如在arch/i386/bootl/setup.S中有一句:
.space 1024
表示保留1024位元組的空間,並且每個位元組的值為0。
(8).word expressions
這個運算式表示任意一節中的一個或多個運算式(用逗號分開),運算式的值占兩個位元組,例如:
gdt_descr:
.word GDT_ENTRIES*8-1
表示變數gdt_descr的置為GDT_ENTRIES*8-1
(9).long expressions
這與.word類似
(10).org new-lc , fill
把當前節的位置計數器提前到new-lc(new location counter)。 new-lc或者是一個常量運算式,或者是一個與當前子節處於同一節的運算式。也就是說,你不能用.org橫跨節:如果new-lc是個錯誤的值,則.org被忽略。 .org只能增加位置計數器的值,或者讓其保持不變;但絕不能用.org來讓位置計數器倒退。 注意,位置計數器的起始值是相對於一個節的開始的,而不是子節的開始。當位置計數器被提升後,中間位置的位元組被填充值 fill(這也是一個常量運算式)。如果逗號和fill都省略,則fill的缺省值為0。 例如:.org 0x2000
ENTRY(pg0)
表示把位置計數器置為0x2000,這個位置存放的就是臨時頁表pg0。
三、 Gcc嵌入式彙編
在Linux的源代碼中,有很多C語言的函數中嵌入一段組合語言程式段,這就是 gcc提供的“asm”功能,例如在 include/asm-i386/system.h中定義的,讀控制寄存器 CR0的一個宏read_cr0():
#define read_cr0() ({ \
unsigned int __dummy; \
__asm__( \
"movl %%cr0,%0\n\t" \
:"=r" (__dummy)); \
__dummy; \
})
這種形式看起來比較陌生,這是因為這不是標準 C所定義的形式,而是gcc 對C語言的擴充。其中__dummy為C函數所定義的變數;關鍵字__asm__表示彙編代碼的開始。 括弧中第一個引號中為彙編指令movl,緊接著有一個冒號,這種形式閱讀起來比較複雜。一般而言,嵌入式組合語言片段比單純的組合語言代碼要複雜得多,因為這裏存在怎樣分配和使用寄存器,以及把C代碼中的變數應該存放在哪個寄存器中。 為了達到這個目的,就必須對一般的C語言進行擴充,增加對編譯器的指導作用,因此,嵌入式彙編看起來晦澀而難以讀懂。
1. 嵌入式彙編的一般形式:
__asm__ __volatile__ ("
其中,__asm__表示彙編代碼的開始,其後可以跟__volatile__(這是可選項),其含義是避免“asm”指令被刪除、移動或組合;然後就是小括弧,括弧中的內容是我們介紹的重點:
· "
· 輸出部分(output),用以規定對輸出變數(目標運算元)如何與寄存器結合的約束(constraint),輸出部分可以有多個約束,互相以逗號分開。 每個約束以“=”開頭,接著用一個字母來表示運算元的類型,然後是關於變數結合的約束。 例如,上例中:
:"=r" (__dummy)“=r”表示相應的目標運算元(指令部分的%0)可以使用任何一個通用寄存器,並且變數__dummy 存放在這個寄存器中,但如果是:
:“=m”(__dummy)
“=m”就表示相應的目標運算元是存放在記憶體單元__dummy中。
表示約束條件的字母很多,表 2-5 給出幾個主要的約束字母及其含義:
表2.5 主要的約束字母及其含義
字母: 含義:
m, v,o 表示記憶體單元
R 表示任何通用寄存器
Q 表示寄存器eax, ebx, ecx,edx之一
I, h 表示直接運算元
E, F 表示浮點數
G 表示“任意”
a, b.c d 表示要求使用寄存器eax/ax/al, ebx/bx/bl, ecx/cx/cl或edx/dx/dl
S, D 表示要求使用寄存器esi或edi
I 表示常數(0~31)
· 輸入部分(Input):輸入部分與輸出部分相似,但沒有“=”。如果輸入部分一個運算元所要求使用的寄存器,與前面輸出部分某個約束所要求的是同一個寄存器,那就把對應運算元的編號(如“1”,“2”等)放在約束條件中,在後面的例子中我們會看到這種情況。
· 修改部分(modify):這部分常常以“memory”為約束條件,以表示操作完成後記憶體中的內容已有改變,如果原來某個寄存器的內容來自記憶體,那麼現在記憶體中這個單元的內容已經改變。注意,指令部分為必選項,而輸入部分、輸出部分及修改部分為可選項,當輸入部分存在,而輸出部分不存在時,分號“:“要保留,當“memory”存在時,三個分號都要保留,例如system.h中的巨集定義__cli(): #define __cli() __asm__ __volatile__("cli": : :"memory")
2. Linux源代碼中嵌入式彙編舉例
Linux源代碼中,在arch目錄下的.h和.c檔中,很多檔都涉及嵌入式彙編,下面以system.h中的C函數為例,說明嵌入式彙編的應用。
(1)簡單應用
#define __save_flags(x) __asm__ __volatile__("pushfl ; popl %0":"=g" (x): /* no input */)
#define __restore_flags(x) __asm__ __volatile__("pushl %0 ; popfl": /* no output */ :"g" (x):"memory", "cc")
第一個巨集是保存標誌寄存器的值,第二個巨集是恢復標誌寄存器的值。第一個宏中的pushfl指令是把標誌寄存器的值壓棧。 而popl是把棧頂的值(剛壓入棧的flags)彈出到x變數中,這個變數可以存放在一個寄存器或記憶體中。這樣,你可以很容易地讀懂第二個宏。
(2) 較複雜應用
static inline unsigned long get_limit(unsigned long segment)
{
unsigned long __limit;
__asm__("lsll %1,%0"
:"=r" (__limit):"r" (segment));
return __limit+1;
}
這是一個設置段界限的函數,彙編代碼段中的輸出參數為__limit(即%0),輸入參數為segment(即%1)。Lsll是載入段界限的指令,即把segment段描述符中的段界限字段裝入某個寄存器(這個寄存器與__limit結合),函數返回__limit加1,即段長。(3)複雜應用在Linux內核代碼中,有關字串操作的函數都是通過嵌入式彙編完成的,因為內核及用戶程式對字串函數的調用非常頻繁,因此,用彙編代碼實現主要是為了提高效率(當然是以犧牲可讀性和可維護性為代價的)。在此,我們僅列舉一個字串比較函數strcmp,其代碼在arch/i386/string.h中。
static inline int strcmp(const char * cs,const char * ct)
{
int d0, d1;
register int __res;
__asm__ __volatile__(
"1:\tlodsb\n\t"
"scasb\n\t"
"jne 2f\n\t"
"testb %%al,%%al\n\t"
"jne 1b\n\t"
"xorl %%eax,%%eax\n\t"
"jmp 3f\n"
"2:\tsbbl %%eax,%%eax\n\t"
"orb $1,%%al\n"
"3:"
:"=a" (__res), "=&S" (d0), "=&D" (d1)
:"1" (cs),"2" (ct));
return __res;
}
其中的“\n”是換行符,“\t”是tab符,在每條命令的結束加這兩個符號,是為了讓gcc把嵌入式彙編代碼翻譯成一般的彙編代碼時能夠保證換行和留有一定的空格。例如,上面的嵌入式彙編會被翻譯成:
1: lodsb //裝入串運算元,即從[esi]傳送到al寄存器,然後esi指向串中下一個元素
scasb //掃描串運算元,即從al中減去es:[edi],不保留結果,只改變標誌
jne2f //如果兩個字元不相等,則轉到標號2
testb %al %al
jne 1b
xorl %eax %eax
jmp 3f
2: sbbl %eax %eax
orb $1 %al
3:
這段代碼看起來非常熟悉,讀起來也不困難。其中1f 表示往前(forword)找到第一個標號為1的那一行,相應地,1b表示往後找。其中嵌入式彙編代碼中輸出和輸入部分的結合情況為:
· 返回值__res,放在al寄存器中,與%0相結合;· 局部變數d0,與%1相結合,也與輸入部分的cs參數相對應,也存放在寄存器ESI中,即ESI中存放源字串的起始位址。· 局部變數d1, 與%2相結合,也與輸入部分的ct參數相對應,也存放在寄存器EDI中,即EDI中存放目的字串的起始位址。
通過對這段代碼的分析我們應當體會到,萬變不利其本,嵌入式彙編與一般彙編的區別僅僅是形式,本質依然不變。因此,全面掌握Intel 386 彙編指令乃突破閱讀低層代碼之根本。
http://book.csdn.net/bookfiles/824/10082424777.shtml
沒有留言:
張貼留言