2010年1月26日 星期二

MemTest86 + 4.00 pattern說明

注意;以下的陣列可能因版本不同會有修改;此處版本是memtest86 + 4.00
const struct tseq tseq[] = {
{1, 5, 4, 0, "[Address test, walking ones] "}, //pattern0
{1, 6, 4, 0, "[Address test, own address] "}, //pattern1
{1, 0, 4, 0, "[Moving inversions, ones & zeros] "}, //pattern2
{1, 1, 2, 0, "[Moving inversions, 8 bit pattern] "}, //pattern3
{1, 10, 50, 0, "[Moving inversions, random pattern] "}, //pattern4
{1, 7, 80, 0, "[Block move, 80 moves] "}, //pattern5
{1, 2, 2, 0, "[Moving inversions, 32 bit pattern] "}, //pattern6
{1, 9, 30, 0, "[Random number sequence] "}, //pattern7
{1, 11, 6, 0, "[Modulo 20, Random pattern] "}, //pattern8
{1, 8, 1, 0, "[Bit fade test, 90 min, 2 patterns] "}, //pattern9
{0, 0, 0, 0, NULL}
};
正常狀況下開機後只會測試pattern1~pattern8,因此若要測試pattern0及pattern9只好手動設定。
來看一下tsaq結構:以下的結構就是對應到上面的陣列元素
struct tseq {
short cache; //1:打開cpu快取;o:關閉快取
short pat; //pattern id注意這和patter執行順序無關
short iter; //pattern測試的重複次數
short errors; //此攔位一看就知道記錄著個別pattern error數量
char *msg;//這就是pattern的名稱:例如 "Address test, walking ones"
};
--> 閱讀更多...

2010年1月19日 星期二

AT&T 組合語言指令詳解2

這段時間在研究Linux源代碼,遇到了AT&T彙編,故轉貼個不錯的AT&T彙編貼如下

就像軟體的真諦——“給我一個標準,我用我的邏輯舞動世界”一樣,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__ ("" : output : input : modify);

其中,__asm__表示彙編代碼的開始,其後可以跟__volatile__(這是可選項),其含義是避免“asm”指令被刪除、移動或組合;然後就是小括弧,括弧中的內容是我們介紹的重點:

· ""為彙編指令部分,例如,"movl %%cr0,%0\n\t"。數位前加首碼“%“,如%1,%2等表示使用寄存器的樣板運算元。可以使用的運算元總數取決於具體CPU中通用寄存器的數量,如Intel可以有8個。指令中有幾個運算元,就說明有幾個變數需要與寄存器結合,由gcc在編譯時根據後面輸出部分和輸入部分的約束條件進行相應的處理。由於這些樣板運算元的首碼使用了”%“,因此,在用到具體的寄存器時就在前面加兩個“%”,如%%cr0。

· 輸出部分(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__ ("" : output : input : modify);
其中,__asm__表示彙編代碼的開始,其後可以跟__volatile__(這是可選項),其含義是避免“asm”指令被刪除、移動或組合;然後就是小括弧,括弧中的內容是我們介紹的重點:
· ""為彙編指令部分,例如,"movl %%cr0,%0\n\t"。數位前加首碼“%“,如%1,%2等表示使用寄存器的樣板運算元。可以使用的運算元總數取決於具體CPU中通用寄存器的數量,如Intel可以有8個。指令中有幾個運算元,就說明有幾個變量需要與寄存器結合,由gcc在編譯時根據後面輸出部分和輸入部分的約束條件進行相應的處理。 由於這些樣板運算元的首碼使用了”%“,因此,在用到具體的寄存器時就在前面加兩個“%”,如%%cr0。
· 輸出部分(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
--> 閱讀更多...

AT&T 組合語言指令詳解

AT&T 組合語言指令詳解
以下資料皆為網路搜尋的;
在閱讀linux/unix內核源代碼的時候,必須先掌握彙編,大家都知道,內核代碼用的編譯器是gcc,而gcc採用的是AT&T的彙編格式,與MS的intel有些區別。
一 AT&T的基本語法
語法上主要有以下幾個不同.
★ 寄存器命名原則
AT&T: %eax Intel: eax
★ 源/目的操作數順序
AT&T: movl %eax,%ebx Intel: mov ebx,eax
★ 常數/立即數的格式
AT&T: movl $_value,%ebx Intel: mov eax,_value
把_value的位址放入eax寄存器
AT&T: movl $0xd00d,%ebx Intel: mov ebx,0xd00d
★ 操作數長度標識
AT&T: movw %ax,%bx Intel: mov bx,ax
★尋址方式
AT&T: immed32(basepointer,indexpointer,indexscale)
Intel: [basepointer + indexpointer*indexscale + imm32)
Linux工作於保護模式下,用的是32位線性位址,所以在計算位址時
不用考慮segment-offset的問題.上式中的地址應為:
imm32 + basepointer + indexpointer*indexscale
下面是一些例子:
★直接尋址
AT&T: _booga ; _booga是一個全局的C變量
注意加上$是表示位址引用,不加是表示值引用.
註:對於局部變量,可以通過堆棧指針引用.
Intel: [_booga]
★寄存器間接尋址
AT&T: (%eax)
Intel: [eax]
★變址尋址
AT&T: _variable(%eax)
Intel: [eax + _variable]
AT&T: _array(,%eax,4)
Intel: [eax*4 + _array]
AT&T: _array(%ebx,%eax,8)
Intel: [ebx + eax*8 + _array]

二 基本的行內彙編

基本的行內彙編很簡單,一般是按照下面的格式
asm("statements");
例如:asm("nop"); asm("cli");
asm 和 __asm__是完全一樣的.
如果有多行彙編,則每一行都要加上 "\n\t"
例如:
asm( "pushl %eax\n\t"
"movl $0,%eax\n\t"
"popl %eax");
實際上gcc在處理彙編時,是要把asm(...)的內容"列印"到彙編
檔中,所以格式控制字元是必要的.
再例如:
asm("movl %eax,%ebx");
asm("xorl %ebx,%edx");
asm("movl $0,_booga);
在上面的例子中,由於我們在行內彙編中改變了edx和ebx的值,但是
由於gcc的特殊的處理方法,即先形成彙編檔,再交給GAS去彙編,
所以GAS並不知道我們已經改變了edx和ebx的值,如果程式的上下文
需要edx或ebx作暫存,這樣就會引起嚴重的後果.對於變量_booga也
存在一樣的問題.為瞭解決這個問題,就要用到擴展的行內彙編語法.

三 擴展的行內彙編

擴展的行內彙編類似於Watcom.
基本的格式是:
asm ( "statements" : output_regs : input_regs : clobbered_regs);
clobbered_regs指的是會被改變的寄存器.
下面是一個例子(為方便起見,我使用全局變量):
int count=1;
int value=1;
int buf[10];
void main()
{
asm(
"cld \n\t"
"rep \n\t"
"stosl"
:
: "c" (count), "a" (value) , "D" (buf[0])
: "%ecx","%edi" );
}
得到的主要彙編代碼為:
movl count,%ecx
movl value,%eax
movl buf,%edi
#APP
cld
rep
stosl
#NO_APP
cld,rep,stos就不用多解釋了.
這幾條語句的功能是向buf中寫上count個value值.
冒號後的語句指明輸入,輸出和被改變的寄存器.
通過冒號以後的語句,編譯器就知道你的指令需要和改變哪些寄存器,
從而可以優化寄存器的分配.
其中符號"c"(count)指示要把count的值放入ecx寄存器
類似的還有:
a eax
b ebx
c ecx
d edx
S esi
D edi
I 常數值,(0 - 31)
q,r 動態分配的寄存器
g eax,ebx,ecx,edx或內存變量
A 把eax和edx合成一個64位的寄存器(use long longs)
我們也可以讓gcc自己選擇合適的寄存器.
如下面的例子:
asm("leal (%1,%1,4),%0"
: "=r" (x)
: "0" (x) );
這段代碼實現5*x的快速乘法.
得到的主要彙編代碼為:
movl x,%eax
#APP
leal (%eax,%eax,4),%eax
#NO_APP
movl %eax,x
幾點說明:
1.使用q指示編譯器從eax,ebx,ecx,edx分配寄存器.
使用r指示編譯器從eax,ebx,ecx,edx,esi,edi分配寄存器.
2.我們不必把編譯器分配的寄存器放入改變的寄存器列表,因為寄存器
已經記住了它們.
3."="是標示輸出寄存器,必須這樣用.
4.數字%n的用法:
數字表示的寄存器是按照出現和從左到右的順序映射到用"r"或"q"請求
的寄存器.如果我們要重用"r"或"q"請求的寄存器的話,就可以使用它們.
5.如果強制使用固定的寄存器的話,如不用%1,而用ebx,則
asm("leal (%%ebx,%%ebx,4),%0"
: "=r" (x)
: "0" (x) );
注意要使用兩個%,因為一個%的語法已經被%n用掉了.
下面可以來解釋letter 4854-4855的問題:
1、變量加下劃線和雙下劃線有什麼特殊含義嗎?
加下劃線是指全局變量,但我的gcc中加不加都無所謂.
2、以上定義用如下調用時展開會是什麼意思?
#define _syscall1(type,name,type1,arg1) \
type name(type1 arg1) \
{ \
long __res; \
/* __res應該是一個全局變量 */
__asm__ volatile ("int $0x80" \
/* volatile 的意思是不允許優化,使編譯器嚴格按照你的彙編代碼彙編*/
: "=a" (__res) \
/* 產生代碼 movl %eax, __res */
: "0" (__NR_##name),"b" ((long)(arg1))); \
/* 如果我沒記錯的話,這裡##指的是兩次宏展開.
  即用實際的系統調用名字代替"name",然後再把__NR_...展開.
  接著把展開的常數放入eax,把arg1放入ebx */
if (__res >= 0) \
return (type) __res; \
errno = -__res; \
return -1; \
}

四.AT&T彙編與Intel彙編的比較

Intel和AT&T語法的區別

Intel和AT&T彙編語言的語法表面上各不相同,這將導致剛剛學會INTEL彙編的人第一次見到AT&T彙編時
會感到困惑,或者反之。因此讓我們從基礎的東西開始。
前綴
在Intel彙編中沒有寄存器前綴或者立即數前綴。而在AT&T彙編中寄存器有一個「%」前綴,立即數有
一個「$」前綴。Intel語句中十六進制和二進制數據分別帶有「h」和「b」後綴,並且如果十六進制
數字的第一位是字母的話,那麼數值的前面要加一個「0」前綴。
例如,
Intex Syntax
mov eax,1
mov ebx,0ffh
int 80h
AT&T Syntax
movl $1,%eax
movl $0xff,%ebx
int $0x80
就像你看到的,AT&T非常難懂。[base+index*scale+disp] 看起來比disp(base,index,scale)更好理解。
操作數的用法
intel語句中操作數的用法和AT&T中的用法相反。在Intel語句中,第一個操作數表示目的,第二個
操作數表示源。然而在AT&T語句中第一個操作數表示源而第二個操作數表示目的。在這種情形下AT&T語法
的好處是顯而易見的。我們從左向右讀,也從左向右寫,這樣比較自然。
例如,
Intex Syntax
instr dest,source
mov eax,[ecx]
AT&T Syntax
instr source,dest
movl (%ecx),%eax
存儲器操作數
如同上面所看到的,存儲器操作數的用法也不相同。在Intel語句中基址寄存器用「[」和「]」括起來
而在AT&T語句中是用「(」和「)」括起來的。
例如,
Intex Syntax
mov eax,[ebx]
mov eax,[ebx+3]
AT&T Syntax
movl (%ebx),%eax
movl 3(%ebx),%eax
AT&T語法中用來處理複雜的操作的指令的形式和Intel語法中的形式比較起來要難懂得多。在Intel語句
中這樣的形式是segreg:[base+index*scale+disp]。在AT&T語句中這樣的形式是
%segreg:disp(base,index,scale)。
Index/scale/disp/segreg 都是可選並且可以去掉的。Scale在本身沒有說明而index已指定的情況下
缺省值為1。segreg的確定依賴於指令本身以及程式運行在實模式還是pmode。在實模式下它依賴於
指令本身而pmode模式下它是不需要的。在AT&T語句中用作scale/disp的立即數不要加「$」前綴。
例如
Intel Syntax
instr foo,segreg:[base+index*scale+disp]
mov eax,[ebx+20h]
add eax,[ebx+ecx*2h]
lea eax,[ebx+ecx]
sub eax,[ebx+ecx*4h-20h]
AT&T Syntax
instr %segreg:disp(base,index,scale),foo
movl 0x20(%ebx),%eax
addl (%ebx,%ecx,0x2),%eax
leal (%ebx,%ecx),%eax
subl -0x20(%ebx,%ecx,0x4),%eax

後綴

就像你已經注意到的,AT&T語法中有一個後綴,它的意義是表示操作數的大小。「l」代表long,
「w」代表word,「b」代表byte。Intel語法中在處理存儲器操作數時也有類似的表示,
如byte ptr, word ptr, dword ptr。"dword" 顯然對應於「long」。這有點類似於C語言中定義的
類型,但是既然使用的寄存器的大小對應著假定的數據類型,這樣就顯得不必要了。
例子:
Intel Syntax
mov al,bl
mov ax,bx
mov eax,ebx
mov eax, dword ptr [ebx]
AT&T Syntax
movb %bl,%al
movw %bx,%ax
movl %ebx,%eax
movl (%ebx),%eax
注意:從此開始所有的例子都使用AT&T語法

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。
本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/kpgood/archive/2009/04/05/4049495.aspx
--> 閱讀更多...

2009年12月14日 星期一

●memtest86+教學 Part16

關於SMP在Memtest86+ V4.00與Memtest86 V3.5 之比較可以發現main.c中多了void initialise_cpus(void),因此先將Memtest86 V3.5 轉成exe檔來執行看看多加入SMP功能後測試時間是否減少.得知結果差異不大,因為其實作多核只是以輪流交替執行權來執行程式碼。我想未來應該會有改進吧,到時後就可以享受多核心CPU降低測試時間的好處了。
注意一點Memtest86 V3.5必須使用gcc 4-2-0以下的版本compile,但Memtest86+ V4.00則使用gcc 4-2-x以上版本。
--> 閱讀更多...

2009年6月10日 星期三

●memtest86+教學 Part15

仔細看一下controller.c之中ㄉstatic struct pci_memory_controller controllers[],你就可以知道目前memtest86+到底支援哪些chipsets,可別小看這ㄍ陣列,我們看一下這ㄍ陣列的元素是ㄍ資料結構如下:
struct pci_memory_controller {
unsigned vendor;
unsigned device;
char *name;
int tested;
void (*poll_fsb)(void);
void (*poll_timings)(void);
void (*setup_ecc)(void);
void (*poll_errors)(void);
};
上面綠色字ㄉ那幾ㄍ是函式指標。這種程式的寫法應該不會陌生吧!一般人很少能接觸這麼多種類的平台,即使接觸到ㄌ也不見得拿得到它們的data sheets,因此能寫出特定chipset的那些函式指標,真是佩服;然而有些還是memtest86作者能力所不及,為什麼ㄋ;例如poll_fsb_nothing,這樣ㄉ函式裡什麼東西也沒有,原因我想當然是沒有specifiction。我想Intel真是大方,它會將所有chipsets的data sheet提供給大家download,其他廠商可不會這樣作。這有關係ㄇ,這個關係可大ㄌ,就好比說show_spd這個routine,它目前只能support ich5以上的南橋晶片,若你的平台不是Intel的晶片組,那麼memtest show SPD code的function就只能看到"SMBus Controller not known",連chipset裡頭的相關Register你都碰不到,我想對該chipset就只能束手無策ㄌ。
--> 閱讀更多...

2009年5月9日 星期六

AMD64 & Intel EM64T


此圖是從intel datasheet:Intel64 and IA-32 Architectures Software Develops Manual Volume3A所截取下來,從這張圖就可知道CPU如何在各模式之間切換;及為什麼可以作到向下相容的架構。
圖二
圖一
AMD公司設計,可以在同一時間內處理64位的整數運算,並兼容于X86-32架構。其中支持64位邏輯定址,同時提供轉換為32位定址選項;但數據操作指令默認為32位和8位,提供轉換成64位和16位的選項;支持常規用途暫存器,如果是32位運算操作,就要將結果擴展成完整的64位。這樣,指令中有“直接執行”和“轉換執行”的區別,其指令字段是8位或32位,可以避免字段過長。
x86-64(也叫AMD64)的產生也並非空穴來風,x86處理器的32bit尋址空間限制在4GB內存,而IA-64的處理器又不能兼容x86。AMD充分考慮顧客的需求,加強x86指令集的功能,使這套指令集可同時支持64位的運算模式,因此AMD把它們的架構稱之為x86-64。在技術上AMD在x86-64架構中為了進行64位運算,AMD為其引入了新增了R8-R15通用暫存器作為原有X86處理器暫存器的擴充,但在而在32位環境下並不完全使用到這些暫存器。原來的暫存器諸如EAX、EBX也由32位擴張至64位。在SSE單元中新加入了8個新暫存器以提供對SSE2的支持。暫存器數量的增加將帶來性能的提升。與此同時,為了同時支持32和64位代碼及暫存器,x86-64架構允許處理器工作在以下兩種模式︰Long Mode(長模式)和Legacy Mode(傳統模式),Long模式又分為兩種子模式(64bit模式和Compatibility mode兼容模式)。該標準已經被引進在AMD伺服器處理器中的Opteron處理器。
而今年也推出了支持64位的EM64T技術,再還沒被正式命為EM64T之前是IA32E,這是英特爾64位擴展技術的名字,用來區別X86指令集。Intel的EM64T支持64位sub-mode,和AMD的X86-64技術類似,採用64位的線性平面尋址,加入8個新的通用暫存器(GPRs),還增加8個暫存器支持SSE指令。與AMD相類似,Intel的64位技術將兼容IA32和IA32E,只有在營運64位作業系統下的時候,才將會採用IA32E。IA32E將由2個sub-mode組成︰64位sub-mode和32位sub-mode,同AMD64一樣是向下兼容的。Intel的EM64T將完全兼容AMD的X86-64技術。現下Nocona處理器已經加入了一些64位技術,Intel的Pentium 4E處理器也支持64位技術。
應該說,這兩者都是兼容x86指令集的64位微處理器架構,但EM64T與AMD64還是有一些不一樣的地方
AMD的K8不是真正的64bit處理器,其和Intel的EM64T處理器一樣,不是真正的64bit處理器。Microsoft的WINXP64bit也不是真正意義上的64bit作業系統。真正64bit系統(硬體和軟體系統)出來的時候,現下這些所謂的64bit全部會淘汰,而這段時間會相當長,至少三年內難以實現;況且配件的更新速度很快,只要用的順手,何必在意以後會怎樣。順便說一下,現下的K8實際上是40bit處理器,而非64bit處理器;EM64T實際上是32bit處理器,只是在物理內存尋址上擴展到36bit。
AMD64 及 EM64T 倒底誰是一顆真正的 64 bit處理器,不用太在意, 這兩者都是由 32bit 處理器改進而來, 前陣子在 bbs 的 hardware 版, 為了AMD64 是不是創新的架構, 爭論了許久. 不過, 實際使用上, AMD64 不論是要當 32bit或 64 bit處理器來用, 算是不錯, 效能好, 溫度又低!

由 Intel EM64T 技術談 64 位元 CPU
由於軟體的功能越來越強大且多元化,所消耗的系統資源相對的也越多,因此在軟體的功能不斷提升的同時,硬體的處理能力也就捉襟見拙。而新技術不斷的發展,除了代表科技的進步外,也因應這必然的趨勢,造就了電腦平台的變革 —DDRⅡ、PCI Express、Hyper Threading 與 EM64T 等。
一、何謂 64 位元 CPU我們知道一個功能強大的軟體其實是由無數行的程式所組合而成,每一行的程式碼均會透過編譯器編譯成數筆資料,送到 CPU 內部的暫存器中等待執行。而所謂的 64 位元也就是在這資料的寬度、暫存器的大小以及執行指令的長度均為 64 位元,另外在記憶體的定址能力同樣也為 64 位元。
二、64 位元 CPU 的發展Intel CPU 的發展從 80386 之後便演進為 32 位元,到 1994 年 Intel 與 HP 共同開發 VLIW(Very Long Instruction Word)架構的 Itanium、Itanium2 的 IA-64 CPU,希望能將 HP 的 PA-RISC、COMPAQ 的 Alpha 以及 x86 架構的 PC 作一整合,以期降低研發高階伺服器處理器成本,並能跨平台的執行不同的作業系統。只是因為 Itanium、Itanium2 的 CPU 使用的是 VLIW 架構,雖然在執行 64 位元的作業系統與應用程式非常順暢,但於 32 位元的作業系統與應用程式中必須透過指令轉譯的方式來執行,導致在 32 位元的環境下效能不彰,因此發展上是叫好不叫座。在 64 位元 CPU 沈潛好一段時間之後,AMD 於 2003 年四月推出 64 位元 x86 架構的處理器 Opteron,以及 Apple 推出 PowerPC G5 的 RISC CPU,既可以執行 64 位元作業系統與應用程式,亦可向下相容於原本 32 位元的作業系統與應用程式,一時間 64 位元處理器的話題又熱絡起來。而 Intel 為了不讓競爭對手專美於前,於 2004 年春季 IDF 論壇中發表了 Intel 64bit Extension Technology 的新技術,並於 4 月份正式定名為 Intel Extended Memory 64 Technology(EM64T),並採用被稱之為 IA-32e 的新操作模式。
三、EM64T 技術Intel EM64T 是為了強化 IA-32 架構所發展的新技術。包含此技術的 IA-32 處理器除了可向下相容於現存的 32 位元軟體,並賦予軟體存取更多記憶體空間。並且允許那些開發於 32 位元定址模式下的軟體擁有執行於 64 位元定址空間的能力。相較於之前 IA32,擁有 EM64T 的 IA32e 最大的不同是將記憶體定址能力由原本 2 的 32 次方提升到 2 的 64 次方,也就是 18446744TB。
四、包含 EM64T 技術處理器的執行模式包含 IA32e 的處理器可執行在 Legacy IA-32 Mode 與 IA-32e Mode。IA-32e Mode 又包含了兩種子模式:1. Compatibility Mode:允許 64 位元的作業系統在不做任何的變動下執行大多數原生的 32 位元軟體。2. 64-bit Mode:允許 64 位元作業系統執行那些存取 64 位元位址空間的應用程式。
五、64-bit Mode 是用來在 64 位元作業系統下執行 64 位元應用程式,包含下列幾項特性:● 64 位元線性定址。● 8 個全新的一般暫存器(General-Purpose Register)。● 8 個全新 Streaming SIMD Extensions(SSE)暫存器支援 SSE、SSE2與SSE3。● 64 位元寬度的 GPR 與指令指標● 相同的 byte-register addressing● 更快速的中斷優化機制● 全新的指令指標相對定址模式64-bit Mode 可透過 64 位元作業系統藉由分割程式碼的準則來啟動。初始定址大小為 64 位元,初始運算元大小為 32 位元。
六、在 Compatibility Mode 中,絕大部分的 16 位元或 32 位元應用程式均可在不重新編譯的狀況下執行於 64 位元作業系統中。相同於 64-bit Mode,Compatibility Mode 可透過 64 位元作業系統藉由分割程式碼的準則來啟動。也就是說在同一時間中,64 位元應用程式也可被執行於 64-bit Mode,而不需重新編譯。
七、Legacy IA-32 Mode 包含保護模式、真實位址模式或是虛擬 8086 模式。針對這些模式撰寫的軟體,EM64T 的處理器均有完整的相容性。
上圖一列出 EM64T 處理器在不同模式下的差異:
上面圖二列出了在不同模式中所支援的暫存器類別:
八、64 位元處理器的迷思是不是購買了 64 位元處理器,相對的執行速度就是原本 32 位元的兩倍?其實以目前的作業系統 Windows XP 或是 Windows 2000 均為 32 位元的作業系統,而應用程式部分亦為 32 位元,也因此在程式碼分割與執行指令上仍為 32 位元寬,而 CPU 資料處理部分仍為 32 位元,也因此與目前的 IA-32 處理器並無差別。在未來 Windows XP 64-bit Edition for Extended System 上市後搭配 64 位元的應用程式,在效能方面才能有所提升。
--> 閱讀更多...

2009年5月6日 星期三

●memtest86+教學 Part14


圖一
上一篇我們已經知道頁目錄如何建立,並且要另外建立一個指向4個頁目錄基底位址的表格,名稱叫作pdp(page-directory-pointer-table)它非常重要,因為我們現在討論的這種2M分頁方式無論如何一定要建立4GB的頁目錄表,並區分成4等分,每一等分佔用4KByte(4096)的空間,也就是說每一等分負責1GB容量的映射(Mapping);這樣的說法不如來作一張圖(如圖一)會更容易理解。雖然圖中例子是共16G記憶體,但可支援到64G的Mapping。
現在我們來看看誰在使用pdp及頁目錄:
int map_page(unsigned long page)
{
unsigned long i;
struct pde
{
unsigned long addr_lo;
unsigned long addr_hi;
};
extern unsigned char pdp[];
extern struct pde pd2[];
unsigned long window = page >> 19;
if (FLAT (window == mapped_window)) { return 0; }
if (window == 0) { return 0; }
if (!v->pae (window >= 32)) {
/* Fail either we don't have pae support
* or we want an address that is out of bounds
* even for pae. */
return -1; }
/* Compute the page table entries... */
//由於左移符號會無法顯示在文章中,因此我把左移使用shl來表示,右移shr,小於lss
for(i = 0; i lss 1024; i++)
{
pd2[i].addr_lo = ((window & 1) shl 31) + ((i & 0x3ff) shl 21) + 0xE3;
pd2[i].addr_hi = (window shr 1);
}
/*以上這個for迴圈會重新建立pd2~pd3的頁目錄表*/
paging_off();
if (window gtr 1) { paging_on(pdp); }
mapped_window = window;
return 0;
}
這個函式可以說是memtest86分頁技術中最經典的routineㄌ。我們來看誰調用它:
有init.c中的cacheable():
/* Ensure the default set of pages are mapped */
map_page(0);
map_page(0x80000);
及main.c中的do_test():
map_page(v->map[0].pbase_addr)
在cacheable()調用它共2個;第一個map_page(0);代入0結果return 0,而且不會去啟用paging_on(pdp);第二個map_page(0x80000);代入0x80000結果也是return 0,而且不會去啟用paging_on(pdp);然而這只是程式的初始,當然不會去啟用分頁;所以真正會不斷調用它的便是map_page(v->map[0].pbase_addr);而且你會發現pd0~pd3好像只有pd2每次都會重新被計算,其實應該說是pd2及pd3每次都被重新計算,因為它的for迴圈是1024次,然而每個pd佔用512個頁目錄表項。所以我們可以開始推論:假設我們主機板插上6GB的記憶體;當測試0~4G時,並不會啟動分頁模式,當測試範圍為4~6G時;便會進入分頁,並且把pd2~pd3對應到4~6G;也就是說此時cpu access的位址雖然是2~4G,但實際上access 到的卻是實體物理位址4~6G。這樣ㄉ推論沒有說服力;所以一定要實際驗證才行。
順便來看一下paging_on:
static void paging_on(void *pdp)
{
if (!v->pae) return;
__asm__ __volatile__(
/* Load the page table address 此處的%0就是pdp的位址 */
"movl %0, %%cr3\n\t"
/* Enable pae cr4的bit5就是pae位元設為1*/
"movl %%cr4, %%eax\n\t"
"orl $0x00000020, %%eax\n\t"
"movl %%eax, %%cr4\n\t"
/* Enable paging cr0的PG位元設為1*/
"movl %%cr0, %%eax\n\t"
"orl $0x80000000, %%eax\n\t"
"movl %%eax, %%cr0\n\t"
:
: "r" (pdp)
: "ax"
);
}
P.S.
試想,為什麼要分頁,而且還分不同的分頁模式,為什麼要把事情搞的那麼複雜ㄋ,難不成就是要存心搞死我們這些人,難道就沒更好的辦法嗎?回顧歷史,32位元CPU已問世多年,我記得Win95那個時候我的系統記憶體也不過才32MB,但是CPU已經可以Access到4G的空間;所以那時RAM遠比CPU定址能力少得多,為了實現虛擬記憶體,讓作業系統可以多工,每個程式都擁有自己的4G位址空間,所以才搞分頁,因為記憶體容量有限,所以要把很多已執行但卻不會馬上用到的行程(Process)所佔用的空間置換(swap)到硬碟(或其他儲存媒體裝置),使正在執行的行程能有足夠的記體體使用權。曾幾何時,CPU定址仍然是32位元,但DRAM價跌容量升級,使很多使用者的安裝於系統的記憶體容量甚至大於4G,因此時代變了分頁技術也要跟著變,就像我們現在所探討的memtest86,就是因為要能符合時代須求所以要測試比4G容量更大的記憶體,且CPU也已經支援這種技術,所以我們才能借助對memtest86的了解,得知這樣的分頁模式。總之這樣的分頁模式和現行OS所實作的分頁稍有不同,就是因為須求不同的原因;或許未來OS設計會將資訊全數載入記憶體,便不需要swap到硬碟,透過PAE這樣的分頁方式來取得資訊,我想一定能更加速系統的運作,但先決條件是DRAM還要更便宜,容量再加大;純假設。
待續‧‧‧


--> 閱讀更多...

●memtest86+教學 Part13






分頁我想應該是memtest86的精華,若能真的把這部分搞懂,我想對IA32(x86)的認識又進入到更深一層的境界了。若你想對整個IA-32的分頁做作通盤認識你可以去參考Intel 64 and IA-32 Architectures Software Developer's Manual - Volume 3A System Programming Guide.pdf。由於一般書籍講到的分頁都是使用4kbyte的分頁模式,而且只講到CR3(或稱 PDBR,page directory base register),然而IA-32的分頁方式還分成好幾種模式;而且支援4K、2M、4M的分頁大小;並可定址到64G (36-BIT PHYSICAL ADDRESSING USING THE PAE PAGING MECHANISM);說的好像很複雜,其實就是在搞CR3、CR4這兩個暫存器(register)。然而其實也沒那麼簡單,無論如何,由於memtest86是採用2M分頁(page size extensions)及支援PAE paging mechanism使可定址到64G;因此我們至少要對這種分頁方式作一番說明:


首先我們先來看看爛豬腳(head.S)如何實作頁目錄,你可能會問不是還要有頁表嗎?這個問題非常好但是我不想在此解答,你直接參考我上面說的那份資料以及Paging Extensions for the Pentium Pro Processor 就可知道為何。

在head.S有這麼一段code:注意;在巨集中使用參數必須前面加上"\"符號
.macro ptes64 start, count=64
.quad \start + 0x0000000 + 0xE3 ;為什麼是E3,下面會解說
.quad \start + 0x0200000 + 0xE3
.quad \start + 0x0400000 + 0xE3
.quad \start + 0x0600000 + 0xE3
.quad \start + 0x0800000 + 0xE3
.quad \start + 0x0A00000 + 0xE3
.quad \start + 0x0C00000 + 0xE3
.quad \start + 0x0E00000 + 0xE3
.if \count-1ptes64 "(\start+0x01000000)",\count-1
.endif
.endm
.macro maxdepth depth=1
.if \depth-1maxdepth \depth-1
.endif
.endm
maxdepth
.balign 4096
.globl pd0
pd0: ptes64 0x0000000000000000
.balign 4096
.globl pd1
pd1: ptes64 0x0000000040000000
.balign 4096
.globl pd2
pd2: ptes64 0x0000000080000000
.balign 4096
.globl pd3
pd3: ptes64 0x00000000C0000000
.balign 4096
.globl pdp
pdp:
.long pd0 + 1
.long 0
.long pd1 + 1
.long 0
.long pd2 + 1
.long 0
.long pd3 + 1
.long 0
上面這段code最重要的就是ptes64那個巨集(macro);若你把pd0:、pd1:、pd2:、pd3:後面的ptes64巨集展開,便會得到0~4GB的頁目錄表,而且每個表項相差2MB。差別在於其每一個頁目錄表項佔用一個quad(8Byte),這和我之前介紹的那本"自己動手寫作業系統"所談到的分頁採用long為頁目錄,相差4個bytes;-而且書面說的也不搞pdp,原因如下:
Figure 3-21 shows the format for the page-directory-pointer-table and page-directory entries when 2-MByte pages and extended physicaladdresses are being used.
The major differences in these entries are as follows:
•A page-directory-pointer(pdp)-table entry is added.
•The size of the entries are increased from 32 bits to 64 bits
•The maximum number of entries in a page directory or page table is 512.
•The base physical address field in each entry is extended to 24 bits for 36-bit physical addressing (or extended to MAXPHYADDR-12 bits if MAXPHYADDR is different than 36).
另外針對"至少在各式各樣的Pentium簡介中有四個2M頁[1,2,3,4]"這句話的意思,答案如下:
Figure 3-19 shows how a page-directory-pointer table and page directories can be used to map linear addresses to 2-MByte pages when the PAE paging mechanism enabled. This paging method can be used to map up to 2048 pages (4 page-directory-pointer(pdp)-table entries times 512 page-directory entries) into a 4-GByte linear address space.也就是Figure 3-19中的bit30和bit31。
/*-----------------參考init.c map_page()--------------- *
0xE3 --
* Bit 0 = Present bit. 1 = PDE is present
* Bit 1 = Read/Write. 1 = memory is writable
* Bit 2 = Supervisor/User. 0 = Supervisor only (CPL 0-2)
* Bit 3 = Writethrough. 0 = writeback cache policy
* Bit 4 = Cache Disable. 0 = page level cache enabled
* Bit 5 = Accessed. 1 = memory has been accessed.
* Bit 6 = Dirty. 1 = memory has been written to.
* Bit 7 = Page Size. 1 = page size is 2 MBytes
* --------------------------------------------------*/
透過以上的認知我們要來看其他的code如何來實作分頁‧‧‧待續‧‧‧
--> 閱讀更多...

2009年5月4日 星期一

●memtest86+教學 Part12

今天我想把int 15h eax=E820h作個總結;首先我們看一下head.S:
/* Don't disable the a20 line */
/* Load 16bit data segments, to ensure the segment limits are set */
movl $REAL_DS, %eax
movl %eax, %ds
movl %eax, %es
movl %eax, %ss
movl %eax, %fs
movl %eax, %gs
/* Compute the stack base */
leal stack@GOTOFF(%ebx), %ecx
/* Compute the address of meminfo*/
leal mem_info@GOTOFF(%ebx), %edi #取得mem_info標籤的絕對位址並存到edi
/* switch to 16bit mode */
ljmp $REAL_CS, $1f - RSTART #這個ljmp就是跳到.code16,也就是下一行;因為ljmp的格式是 segment:offset ;segment就是$REAL_CS;offset就是$1f - RSTART(下一行1:標籤減掉RSTART;因為有這個定義#define RSTART startup_32就是head.S的頂端;所以$1f - RSTART就等於1:標籤相對於head.S的偏移位址)
1:
.code16
/* Disable Paging and protected mode */
/* clear the PG & PE bits of CR0 */
movl %cr0,%eax
andl $~((1 << 31)(1<<0)),%eax #關閉分頁及保護模式,回到真實模式
movl %eax,%cr0
/* make intersegment jmp to flush the processor pipeline * and reload %cs:%eip (to clear upper 16 bits of %eip). */
ljmp *(realptr - RSTART)
#這樣的語法通常是這種32位元和16位元的混合區段才會用到(切記)
#這樣的語法類似nasm的 jmp dword selector:offset;
#然而masm卻沒有這樣的機制語法;所以masm會如下這樣搞:
# db 0eah
# dw ofsfset
# dw segment
#以上三行是JMP到16bit時的用法,雖等同於 jmp dword ptr label(不支援),但masm好像只
#支援 jmp fword ptr label(從16bit跳到32bit),然而跳到32bit也可以這樣搞:
# db 0eah
# dw ofsfset , 0h
# dw segment

#也就是說從32bit跳到16bit會去提取2ㄍbyte當offset及2ㄍbyte當segment
#當從16bit跳到32bit會去提取4ㄍbyte當offset及2ㄍbyte當segment
real:
/* we are in real mode now
* set up the real mode segment registers : %ds, %ss, %es, %gs, %fs
*/
movw %cs, %ax
movw %ax, %ds
movw %ax, %es
movw %ax, %fs
movw %ax, %gs
movw %ax, %ss
/* Adjust the stack pointer */
movl %ecx, %eax
shrl $4, %eax #取得ss值
movw %ax, %ss
subl %ecx, %esp
/* Save my base pointer */
pushl %ebx
/* Setup %ds to point to my data area */
shrl $4, %edi
movl %edi, %ds
/* Enable interrupts or BIOS's go crazy */
sti
# Get memory size (extended mem, kB)
#define SMAP 0x534d4150
xorl %eax, %eax
movl %eax, (E88) #0x00
movl %eax, (E801) #0x04
movl %eax, (E820NR) #0x08
# Try three different memory detection schemes. First, try
# e820h, which lets us assemble a memory map, then try e801h,
# which returns a 32-bit memory size, and finally 88h, which
# returns 0-64m
# method E820H:
# the memory map from hell. e820h returns memory classified into
# a whole bunch of different types, and allows memory holes and
# everything. We scan through this memory map and build a list
# of the first 32 memory areas, which we return at [E820MAP].
# This is documented at http://www.teleport.com/~acpi/acpihtml/topic245.htm
#int 15h eax=0e820h使用說明:
#INPUT:eax=0e820h ebx=0(第一次呼叫必須為0)
#INPUT:es:di=指向一個可容納32*20byte的位址空間
#INPUT:ecx=20 edx=0534d4150h('SMAP')
#OUTPUT:CF(C旗標=0表示成功,否則失敗)
#OUTPUT:eax=0534d4150h('SMAP') return value
#OUTPUT:ecx=20
#OUTPUT:ebx如果等於0且cf=0,表示已經是最後一個位址範圍描述器
meme820:
xorl %ebx, %ebx # continuation counter
movw $E820MAP, %di # point into the whitelist
# so we can have the bios
# directly write into it.
jmpe820:
movl $0x0000e820, %eax # e820, upper word zeroed
movl $SMAP, %edx # ascii 'SMAP'
movl $20, %ecx # size of the e820rec
pushw %ds # data record.
popw %es #這ㄍpush pop很重要,因為得到ㄉ資料是存放在es:di
int $0x15 # make the call
jc bail820 # fall to e801 if it fails
cmpl $SMAP, %eax # check the return is `SMAP'
jne bail820 # fall to e801 if it fails
# cmpl $1, 16(%di) # is this usable memory?
# jne again820
# If this is usable memory, we save it by simply advancing %di by
# sizeof(e820rec).
good820:
movb (E820NR), %al # up to 32 entries
cmpb $E820MAX, %al
jnl bail820
incb (E820NR)
movw %di, %ax
addw $E820ENTRY_SIZE, %ax #define E820ENTRY_SIZE 20
movw %ax, %di
again820:
cmpl $0, %ebx # check to see if
jne jmpe820 # %ebx is set to EOF
bail820:
# method E801H:
# memory size is in 1k chunksizes, to avoid confusing loadlin.
# we store the 0xe801 memory size in a completely different place,
# because it will most likely be longer than 16 bits.
#以下是0xe801及E88我們不討論,因為現在用的主機板根本不會去呼叫它們
meme801:
‧‧‧
‧‧
‧
/* O.k. the BIOS query is done switch back to protected mode */
cli
/* Restore my saved variables */
popl %ebx
/* Get an convinient %ds */
movw %cs, %ax
movw %ax, %ds
/* Load the global descriptor table */
addr32 lgdt gdt_descr - RSTART
/* Turn on protected mode */
/* Set the PE bit in CR0 */
movl %cr0,%eax
orl $(1 SHL 0),%eax #回到保護模式
movl %eax,%cr0
/* flush the prefetch queue, and relaod %cs:%eip */
data32 ljmp *(protptr - RSTART)
prot:
.code32
‧‧‧
‧‧
‧
realptr:
.word real - RSTART #仔細觀察一下realptr: 這個標籤後面這兩個.word
.word 0x0000 #因為ljmp 後面是先offset值,然後才是segment值
protptr:
.long 0
.long KERNEL_CS #其實這ㄍlong只要定義為word就可以ㄌ
idt_real:

.word 0x400 - 1 # idt limit ( 256 entries)
.word 0, 0 # idt base = 0L
‧‧‧
.globl mem_info #匯出這個符號給c語言使用
mem_info: #以下這裡就是存放位址範圍描述器的地方
. = . + MEMINFO_SIZE #0x28c=(E88+E801+E820NR=0ch)+(20*32)
‧‧‧
stack:
. = . + 4096
stack_top:

由以上的代碼我們已經得到n個位址範圍描述器,接下來我們要回到init.c得知接下來是呼叫memsize.c中的mem_size();
由於test.h有這樣的定義:extern struct mem_info_t mem_info;
struct e820entry
{ unsigned long long addr; /* start of memory segment */
unsigned long long size; /* size of memory segment */
unsigned long type; /* type of memory segment */
};

struct mem_info_t
{ unsigned long e88_mem_k; /* 0x00 */
unsigned long e801_mem_k; /* 0x04 */
unsigned long e820_nr; /* 0x08 */
struct e820entry e820[E820MAX]; /* 0x0c */
/* 0x28c */};

所以就可將這些位址範圍描述器整理如下:
if (e820_nr == 0 && alt_mem_k == 0 && ext_mem_k == 0){
ext_mem_k = mem_info.e88_mem_k;
alt_mem_k = mem_info.e801_mem_k;
e820_nr = mem_info.e820_nr;
for (i=0; i< mem_info.e820_nr; i++) {
e820[i].addr = mem_info.e820[i].addr;
e820[i].size = mem_info.e820[i].size;
e820[i].type = mem_info.e820[i].type; }
}
--> 閱讀更多...

2009年4月21日 星期二

●memtest86+教學 Part11

圖二
圖一

我想在●memtest86+教學 Part9 中ㄉ介紹觀念還是很模糊,所以我貼出上面這張圖一及圖二,雖然我以visual PC 128MB作說明,但道理是一樣ㄉ。當然看這張圖必須參考memtest86+教學 Part6 中ㄉ圖PMAP-1。
--> 閱讀更多...