By 宋寶華 / 本系列文章交流與討論:@宋寶華Barry
設(shè)備驅(qū)動程序有時需要用匯編實現(xiàn)一些代碼片斷,因此讓我們看看Linux上匯編編程的不同特性。
圖A.1顯示了Linux在PC兼容系統(tǒng)上的引導(dǎo)順序,是第2章“內(nèi)核一瞥”中圖2.1的縮減版。圖中的固件組件是用不同的匯編語法實現(xiàn)的:
· BIOS通常全部用匯編編寫。一些流行的PC BIOS使用像Microsoft Macro Assembler (MASM)這樣的匯編來編碼。
· Linux 引導(dǎo)程序,像LILO和GRUB用C與匯編混合編寫。SYSLINUX引導(dǎo)程序整個用Netwide Assembler(NASM)匯編編寫。
· 實模式的Linux啟動代碼使用GNU匯編器(GAS)編碼。
· 保護模式的BIOS調(diào)用用內(nèi)聯(lián)匯編編寫。內(nèi)聯(lián)匯編是GCC支持的結(jié)構(gòu),在C語句之間插入?yún)R編。
圖 A.1. 固件組件與匯編語法
在圖A.1中,上面的兩個組件通常遵守基于Intel的匯編語法,而下面的兩個用AR&T(或GAS)語法來編碼。也有一些例外,GRUB的匯編部分就使用GAS。
為了演示這兩種語法之間的差異,考慮如下輸出一個字節(jié)到并口的代碼。在BIOS或引導(dǎo)程序所使用的Intel格式中,你將會編寫代碼:
- mov dx, 03BCh ;0x3BC is the I/O address of the parallel port
-
- mov al, 0ABh ;0xAB is the data to be output
-
- out dx, al ;Send data to the parallel port
然而,如果你想從Linux實模式啟動代碼中完成同樣的工作,你將需要編寫如下代碼:
- movw $0x3BC, %dx
-
- movb $0xAB, %al
-
- outb %al, %dx
你會發(fā)現(xiàn),不像Intel格式,在AT&T語法中,首先出現(xiàn)的是源操作數(shù),目的操作數(shù)在其后。AT&T格式中的寄存器名字由%開始,立即數(shù)用$開始。AT&T的操作碼為了指定內(nèi)存操作數(shù)的寬度,都帶有后綴如b(針對字節(jié))和w(針對字);而Intel語法中通過查看操作數(shù)而不是操作碼來實現(xiàn)此目的。在Intel語法中,為了移動指針引用,你需要為操作數(shù)指定前綴,如byte ptr。
學(xué)習(xí)AT&T語法的益處是它被GAS和內(nèi)聯(lián)GCC所支持,而GAS和GCC不僅運行于基于Intel的系統(tǒng)上,也運行于各種處理器架構(gòu)。
下面,讓我們使用GCC內(nèi)聯(lián)匯編重寫前面的代碼片斷,它是你在保護模式的內(nèi)核將要用到的:
- unsigned short port = 0x3BC;
-
- unsigned char data = 0xAB;
-
- asm("outb %%al, %%dx\n\t"
-
- :
-
- : "a" (data), "d" (port)
-
- );
GCC支持的匯編格式通常如下:
- asm(assembly : output operand constraints : input operand constraints : clobbered operand specifier );
在操作數(shù)項,a,b,c,d,S和D分別代表EAX,EBX,ECX,EDX,ESI和EDI寄存器。輸入操作數(shù)constraint用于在執(zhí)行匯編指令之前,將數(shù)據(jù)從提供的變量里拷貝至寄存器。關(guān)于GCC內(nèi)聯(lián)匯編語法的細節(jié)請查看GCC 內(nèi)聯(lián)匯編指南(
www./gferg/ldp/GCC-Inline-Assembly-HOWTO.html)。
在我們的例子中,唯一用到的constraint是針對輸入操作數(shù)的。此約束有效地拷貝data的值至AL寄存器,以及port的值至DX寄存器。在內(nèi)聯(lián)匯編中,寄存器名由%%開始,因為%被用于指定提供的操作數(shù)。%i代表第i個操作數(shù),因此,在前面的例子內(nèi)聯(lián)匯編代碼片斷中,如果你想指定data和port,可以分別使用%0和%1。
為了對內(nèi)聯(lián)匯編轉(zhuǎn)換有更清晰的了解,讓我們看看對應(yīng)于前面的內(nèi)聯(lián)匯編片斷、通過提供-s命令行參數(shù)給GCC,由編譯器產(chǎn)生的匯編代碼。為了理解,請閱讀針對產(chǎn)生的每行代碼的注釋:
- movw $956, -2(%ebp) # Value of data in stack set to 0x3BC
-
- movb $-85, -3(%ebp) # Value of port in stack set to 0xAB
-
- movb -3(%ebp), %al # movb 0xAB, %al
-
- movw -2(%ebp), %dx # movw 0x3BC, %dx
-
- #APP # Marker to note start of inline assembly
-
- outb %al, %dx # Write to parallel port
-
- #NO_APP # Marker to note end of inline assembly
你也可以在用戶模式的程序中使用內(nèi)聯(lián)匯編。下面是用內(nèi)聯(lián)匯編編寫的一個應(yīng)用程序,調(diào)用syslog()系統(tǒng)調(diào)用以從內(nèi)核的printk()的環(huán)形緩沖區(qū)中讀取最后的128字節(jié):
- #define READ_COMMAND 3 /* First argument to
-
- syslog() system call */
-
- #define MSG_LENGTH 128 /* Third argument to syslog() */
-
- int
-
- main(int argc, char *argv[])
-
- {
-
- int syslog_command = READ_COMMAND;
-
- int bytes_to_read = MSG_LENGTH;
-
- int retval;
-
- char buffer[MSG_LENGTH]; /* Second argument to syslog() */
-
- asm volatile(
-
- "movl %1, %%ebx\n" /* READ_COMMAND */
-
- "movl %2, %%ecx\n" /* buffer */
-
- "movl %3, %%edx\n" /* bytes_to_read */
-
- "movl $103, %%eax\n" /* __NR_syslog */
-
- "int $128\n" /* Generate System Call */
-
- "movl %%eax, %0" /* retval */
-
- :"=r" (retval)
-
- :"m"(syslog_command),"r"(buffer),"m"(bytes_to_read)
-
- :"%eax","%ebx","%ecx","%edx");
-
- if (retval > 0) printf("%s\n", buffer);
-
- }
正如在第4章“打下基礎(chǔ)”中所學(xué)到的,int $128(或者int 0x80)指令產(chǎn)生一個軟中斷,陷入系統(tǒng)調(diào)用。由于系統(tǒng)調(diào)用導(dǎo)致從用戶模式至內(nèi)核模式的轉(zhuǎn)換,故函數(shù)參數(shù)未傳入用戶或內(nèi)核堆棧中,而是在CPU寄存器中。此系統(tǒng)調(diào)用號(在include/asm-your-arch/unistd.h中有完整列表)存儲在EAX寄存器中。對于syslog()系統(tǒng)調(diào)用,調(diào)用號是103。如果查看syslog()的參考頁,將會發(fā)現(xiàn)它需要三個參數(shù):命令,存放返回數(shù)據(jù)的緩沖區(qū)的地址,以及緩沖區(qū)的長度。這些分別通過EBX、ECX和EAX來傳遞。返回值被從EAX傳遞至retval。此內(nèi)聯(lián)匯編調(diào)用被轉(zhuǎn)換為如下語句:
retval = syslog(syslog_command, buffer, bytes_to_read);
如果你編譯并運行此代碼,將會看到如下從內(nèi)核的環(huán)形緩沖區(qū)中獲取的輸出:
- 0:0:0:0: Attached scsi removable disk sda
-
- <5>sd 0:0:0:0: Attached scsi generic sg0 type 0
-
- <7>usb-storage: device scan complete
-
- ...
arch/x86/kernel/entry_32.S中的所有內(nèi)核系統(tǒng)調(diào)用trap會保存所有的寄存器內(nèi)容至堆棧,因此 ,即使用戶空間的代碼使用CPU寄存器來傳遞參數(shù),實際上系統(tǒng)調(diào)用處理函數(shù)還是從堆棧中取其參數(shù),。為了確保系統(tǒng)調(diào)用例程預(yù)期的參數(shù)在堆棧中,都用GCC屬性asmlinkage進行標記。需要注意的是asmlinkage與asm(或__asm__)沒有任何關(guān)系,后者用于聲明內(nèi)聯(lián)匯編。
讓我們通過演示一個內(nèi)聯(lián)匯編的例子來結(jié)束本節(jié),此例子修改自基于PowerPC的電路板的Linux引導(dǎo)程序。假設(shè)此電路板上的flash存儲器不支持背景操作(BackGround Operation,BGO)。這意味此引導(dǎo)程序代碼從flash執(zhí)行時,不能寫入flash;但有時這是必須的,例如如果引導(dǎo)程序需要更新內(nèi)核映象,而此映象存放于flash的另一部分。一個解決方案是修改引導(dǎo)程序,以便用于寫入和擦除flash的引導(dǎo)代碼完全從指令cache(I-cache)中執(zhí)行,而數(shù)據(jù)段放入數(shù)據(jù)cache(D-cache)中。示例用的GCC內(nèi)聯(lián)匯編編寫的宏用于完成將必要的引導(dǎo)程序指令搬入I-cache的工作。為了理解此代碼片斷,你需要有一定的PowerPC匯編知識:
- /* instr_length is the number of instructions to touch
-
- into I-cache. _load_i$_copy and _end_i$_copy are
-
- program labels */
-
- #define load_into_icache_copy(instr_length) \
-
- asm volatile("lis %%r3, 0x1@h\n \
-
- ori %%r3, %%r3, 0x1@l\n \
-
- mticcr %%r3\n \
-
- isync\n \
-
- \n \
-
- lis %%r6, _end_i$_copy@h\n \
-
- ori %%r6, %%r6, _end_i$_copy@l\n \
-
- icbt %%r0, %%r6\n \
-
- lis %%r4, %0@h\n \
-
- ori %%r4, %%r4, %0@l\n \
-
- mtctr %%r4\n \
-
- _load_i$_copy: \
-
- addis %%r6, %%r6, 32@ha\n \
-
- addi %%r6, %%r6, 32@l\n \
-
- icbt %%r0, %%r6\n \
-
- bdnz _load_i$_copy\n \
-
- _end_i$_copy: \
-
- nop\n" \
-
- : \
-
- : "i"(instr_length) \
-
- :"%r6","%r4","%r0","r8","r9");
調(diào)試
為了調(diào)試實模式內(nèi)核,不能使用我們在第21章“調(diào)試設(shè)備驅(qū)動”中所討論、使用過的調(diào)試器,如kdb或kgdb。調(diào)試內(nèi)核匯編片斷的便捷方式是將代碼轉(zhuǎn)換為Intel類型的語法后,使用DOS調(diào)試工具。但調(diào)試器是在16位時代編寫的,因此,不能調(diào)試32位的代碼,例如不能調(diào)試初始化EAX寄存器的代碼。從Internet上可以下載一些32位的免費調(diào)試器。第21章所討論的JTAG調(diào)試器是萬金油,因為這一工具可用于調(diào)試BIOS,引導(dǎo)程序,Linux實模式代碼,以及內(nèi)核與BIOS之間的交互。