與普通函數(shù)一樣,系統(tǒng)調(diào)用通常需要一些輸入/輸出參數(shù),這些參數(shù)可能包括實際值(即數(shù)字)、用戶模式進程地址空間中的變量地址,甚至包括指向用戶模式函數(shù)指針的數(shù)據(jù)結(jié)構(gòu)的地址(參見第11章“信號相關(guān)的系統(tǒng)調(diào)用”部分)。
Linux系統(tǒng)下,所有系統(tǒng)調(diào)用的入口點都是system_call()或sysenter_entry(),這兩個函數(shù)至少需要一個參數(shù):系統(tǒng)調(diào)用號,其保存在寄存器eax中。要不然,內(nèi)核怎么知道你想干什么呢?比如,我們在寫多進程應(yīng)用的時候,必須要用的fork()系統(tǒng)調(diào)用,在調(diào)用int $80或者sysenter匯編指令之前,必須設(shè)置eax寄存器的值為2(__NR_fork)。通常,這個操作都是在libc庫中的函數(shù)中完成的,對于系統(tǒng)調(diào)用號我們不太在意而已。
fork()系統(tǒng)調(diào)用不需要其他參數(shù)。然而,許多系統(tǒng)調(diào)用確實需要額外的參數(shù),這些參數(shù)必須由應(yīng)用程序顯式傳遞。例如,mmap()系統(tǒng)調(diào)用可能需要最多六個附加參數(shù)(除了系統(tǒng)調(diào)用號之外)。
普通C函數(shù)的參數(shù)通常將其值寫入程序棧(用戶態(tài)棧和內(nèi)核態(tài)棧)來傳遞。因為系統(tǒng)調(diào)用是一種跨內(nèi)核態(tài)和用戶態(tài)的特殊函數(shù),所以,用戶態(tài)或內(nèi)核態(tài)棧都不能使用。相反,在發(fā)起系統(tǒng)調(diào)用之前,將參數(shù)寫入CPU寄存器中。然后內(nèi)核在調(diào)用系統(tǒng)調(diào)用服務(wù)例程之前將存儲在CPU寄存器中的參數(shù)復(fù)制到內(nèi)核態(tài)棧上,因為后者是普通的C函數(shù)。
為什么內(nèi)核不直接從用戶棧復(fù)制參數(shù)到內(nèi)核棧?首先,同時處理兩個堆棧是很復(fù)雜的;其次,寄存器的使用使得系統(tǒng)調(diào)用處理程序的結(jié)構(gòu)類似于其他異常處理程序的結(jié)構(gòu)。
然而,要在寄存器中傳遞參數(shù),必須滿足2個條件:
每個參數(shù)的長度不能超過寄存器的長度(32位)。
除了在eax中傳遞的系統(tǒng)調(diào)用號之外,參數(shù)的數(shù)量不能超過6個,因為80×86處理器的寄存器數(shù)量非常有限。
第一個條件總是為真,因為根據(jù)POSIX標(biāo)準,不能存儲在32位寄存器中的大參數(shù)必須通過引用傳遞。一個典型的例子是settimeofday()系統(tǒng)調(diào)用,它必須讀取64位結(jié)構(gòu)。
對于需要6個以上參數(shù)的系統(tǒng)調(diào)用,使用單個寄存器指向進程地址空間中包含參數(shù)值的內(nèi)存區(qū)域。當(dāng)然,程序員不必關(guān)心其中的細節(jié)。與每個C函數(shù)調(diào)用一樣,當(dāng)調(diào)用封裝服務(wù)例程時,參數(shù)會自動保存在棧上。這個服務(wù)例程會采用合適方法將參數(shù)傳遞給內(nèi)核。
傳遞系統(tǒng)調(diào)用號及其參數(shù)的寄存器依次為:eax(系統(tǒng)調(diào)用號)、ebx、ecx、edx、esi、edi和ebp。如前所述,system_call()和sysenter_entry()通過使用SAVE_ALL宏將這些寄存器的值保存在內(nèi)核棧上。因此,當(dāng)系統(tǒng)調(diào)用服務(wù)例程進入棧時,它會找到system_call()或sysenter_entry()的返回地址,然后是存儲在ebx中的參數(shù)(系統(tǒng)調(diào)用的第一個參數(shù)),存儲在ecx中的參數(shù),等等(參見第4章“為中斷處理程序保存寄存器”一節(jié))。這個堆棧配置與普通函數(shù)調(diào)用完全相同。因此,服務(wù)例程可以通過使用常用的c語言結(jié)構(gòu)輕松地引用其參數(shù)。
讓我們看一個示例。sys_write()服務(wù)例程,用來處理write()系統(tǒng)調(diào)用,聲明如下:
intsys_write(unsignedintfd,constchar*buf,unsignedintcount)
C編譯器會將其編譯為匯編語言函數(shù),并期望在棧頂,返回地址的正下方,也就是保存ebx,ecx和edx寄存器的地方找到fd、buf和count參數(shù)。
在許多情況下,系統(tǒng)調(diào)用可能不使用參數(shù),但服務(wù)例程需要知道在系統(tǒng)調(diào)用發(fā)起之前CPU寄存器的內(nèi)容。例如,do_fork()函數(shù)需要知道這些寄存器的值,以便將它們拷貝到子進程的thread字段域內(nèi)(可以查看第3章的thread字段)。這種情況下,使用類型為pt_regs的單參數(shù),允許服務(wù)例程訪問內(nèi)核棧保存的值(使用SAVE_ALL宏保存,查看第4章的do_IRQ函數(shù))。
intsys_fork(structpt_regsregs)
服務(wù)例程的返回值寫入到eax寄存器中,這是C編譯器自動完成的,當(dāng)遇到return n;時就會自動將n保存到eax寄存器中。
1 驗證參數(shù)
內(nèi)核在響應(yīng)用戶系統(tǒng)調(diào)用之前必須檢查所有系統(tǒng)調(diào)用參數(shù)。檢查的類型取決于系統(tǒng)調(diào)用和具體參數(shù)。還是以write()系統(tǒng)調(diào)用為例:fd應(yīng)該是一個標(biāo)識文件的文件描述符,所以sys_write()必須檢查fd是之前打開的文件描述符嗎,進程是否允許對其進行寫操作。如果有條件不滿足,則返回負值,錯誤碼-EBADF。
但是,有一類型的檢查對于所有系統(tǒng)調(diào)用是通用的。每當(dāng)參數(shù)指定地址時,內(nèi)核必須檢查是否在進程的地址空間中。檢查有兩種方法:
驗證線性地址是否屬于進程地址空間,如果是,包含該地址的內(nèi)存是否有正確的訪問權(quán)限。
僅驗證該線性地址小于PAGE_OFFSET(也就是沒有落在為內(nèi)核保留的間隔地址范圍內(nèi))。
早期的Linux執(zhí)行第一種檢查,但是這相當(dāng)耗時,因為必須為系統(tǒng)調(diào)用中的每個地址參數(shù)執(zhí)行檢查;更重要的是,這通常是無用的,因為錯誤程序并不總是常見。
因此,從2.2版本開始,Linux采用了第2種方案。該方法非常高效,因為不用對進程所使用的內(nèi)存區(qū)域描述符進行掃描。很明顯,這是粗放式的檢查:驗證線性地址小于PAGE_OFFSET,是驗證其正確性的必要條件但不是充分條件。但是,使用這方法并沒有風(fēng)險,因為后面會造成其它錯誤。
該方法將真正的檢查推遲到了最后時刻,也就是說,物理分頁單元將線性地址轉(zhuǎn)換成物理地址的時候。我們將在后面的“動態(tài)地址檢查:修復(fù)代碼”中,闡述頁錯誤異常處理程序如何檢測到那些用戶態(tài)傳遞給內(nèi)核的錯誤地址(參數(shù))。
此刻,可能有人會想為什么執(zhí)行這種粗檢查?因為這對于保護進程地址空間和內(nèi)核地址空間的非法訪問是至關(guān)重要的。第2章中我們得知,物理內(nèi)存從線性地址PAGE_OFFSET開始映射。這意味著內(nèi)核服務(wù)例程能夠訪問內(nèi)存中的所有內(nèi)存頁。因此,如果不做粗檢查,用戶進程可能會傳遞屬于內(nèi)核地址空間的地址作為參數(shù),然后,就能夠訪問這些內(nèi)存而不會造成頁錯誤異常。
對系統(tǒng)調(diào)用的地址參數(shù)進行檢查可以使用access_ok()宏實現(xiàn),主要作用于兩個參數(shù):addr和size。它會檢查addr和addr + size - 1之前確定的間隔是否合理。本質(zhì)上,等價于下面的C函數(shù):
intaccess_ok(constvoid*addr,unsignedlongsize) { unsignedlonga=(unsignedlong)addr; if(a+sizecurrent_thread_info()->addr_limit.seg) return0; return1; }
該函數(shù)首先檢查addr + size是否大于2^32-1,因為GNU C編譯器(gcc)將無符號長整形和指針表示為32位數(shù)字,所以,相當(dāng)于檢查溢出。該函數(shù)還會檢查addr + size是否超過了存儲在current的``thread_info數(shù)據(jù)結(jié)構(gòu)中的addr_limit.seg字段中的值。對于普通進程,該字段為PAGE_OFFSET;對于內(nèi)核線程,該值為0xffffffff。該字段可以通過get_fs和set_fs`宏動態(tài)修改;這允許內(nèi)核繞過安全檢查,直接調(diào)用系統(tǒng)調(diào)用服務(wù)例程,直接將內(nèi)核數(shù)據(jù)段中的地址傳遞給它們。
verify_area()可以執(zhí)行access_ok()相同的功能。該函數(shù)已經(jīng)廢棄。
2 訪問進程地址空間
系統(tǒng)服務(wù)例程經(jīng)常需要讀寫進程地址空間中的數(shù)據(jù)。Linux提供了一組宏方便這種讀寫請求。下面我們描述其中的兩個:get_user( )和put_user( )。前者用來從用戶態(tài)地址空間讀取1、2或4個連續(xù)字節(jié),后者則是寫入相同字節(jié)數(shù)。
函數(shù)具有兩個參數(shù)x和變量ptr。ptr決定傳輸多少個字節(jié)。因此,在get_user(x,ptr)函數(shù)中,會根據(jù)ptr指向變量的大小將函數(shù)展開為__get_user_1()、__get_user_2()或__get_user_4()匯編函數(shù)中的一個。下面以__get_user_2()為例:
__get_user_2: /*檢查用戶空間內(nèi)存地址是否有效*/ addl$1,%eax/*eax+1,即用戶空間內(nèi)存地址加1*/ jcbad_get_user/*如果進位標(biāo)志位(carry)被設(shè)置,則跳轉(zhuǎn)到bad_get_user*/ /*用于檢查是否溢出*/ movl$0xffffe000,%edx/*棧大小設(shè)置為0xffffe000(4KB)*/ andl%esp,%edx/*將棧指針寄存器按照邊界對齊*/ cmpl24(%edx),%eax/*將?;?24,其所指向的內(nèi)存地址與eax值進行比較*/ /*用于檢查線性地址是否小于addr_limit.seg*/ jaebad_get_user/*如果線性地址≥addr_limit.seg,則跳轉(zhuǎn)到bad_get_user*/ 2:movzwl-1(%eax),%edx/*eax中的地址減1,去除一個16位無符號數(shù)進行零擴展到32位*/ /*將結(jié)果寫入到edx中*/ xorl%eax,%eax/*異或操作,清零eax*/ ret/*返回到函數(shù)調(diào)用的下一條指令處*/ bad_get_user: /*異常處理代碼*/ xorl%edx,%edx/*清零edx*/ movl$-EFAULT,%eax/*返回結(jié)果-EFAULT寫入到eax寄存器*/ ret/*返回調(diào)用該函數(shù)的位置,結(jié)束函數(shù)的執(zhí)行*/
寄存器eax包含要讀取的第一個字節(jié)的地址。前6條指令本質(zhì)上執(zhí)行與access_ok()宏相同的檢查:確保要讀取的2個字節(jié)地址不會超過4G,也小于當(dāng)前進程addr_limit.seg字段的限制。這個字段在thread_info結(jié)構(gòu)體中的偏移量是24,這就是為什么cmpl指令的第一個操作數(shù)尋址加24的原因。
如果地址合法,則執(zhí)行movzwl指令,將要讀取的數(shù)據(jù)最低2個字節(jié)存儲到edx寄存器,而edx寄存器的高位設(shè)置為0,然后返回0(eax寄存器清零)。如果地址不合法,則清零edx,返回-EFAULT錯誤碼。
put_user(x,ptr)宏與get_user(x,ptr)類似,區(qū)別在于它是寫入數(shù)據(jù)。依賴x的大小,它選擇調(diào)用__put_user_asm( )宏(寫入1、2、4字節(jié)),還是調(diào)用__put_user_u64()(寫入8字節(jié))。如果成功,這兩個宏都返回0(寫入eax寄存器),否則返回-EFAULT。
內(nèi)核態(tài)下還有幾個其它函數(shù)和宏可以訪問用戶進程地址空間,如表10-1所示。注意,它們都有一個以下劃線(__)為前綴的變體。沒有下劃線的函數(shù)和宏會額外檢查想要訪問的線性地址塊是否合法,而有下劃線的則會繞過檢查。尤其是當(dāng)內(nèi)核需要重復(fù)訪問進程地址空間中同一片區(qū)域時,只在開始檢查一遍地址更有效率。
表10-1 可以訪問進程地址空間的函數(shù)和宏
函數(shù) | 行為 |
---|---|
get_user __get_user |
從用戶空間讀取一個整型值(1、2、4字節(jié)) |
put_user __put_user |
寫一個整型值到用戶空間(1、2、4字節(jié)) |
copy_from_user __copy_from_user |
從用戶空間拷貝一塊任意大小的數(shù)據(jù) |
copy_to_user __copy_to_user |
拷貝一塊任意大小的數(shù)據(jù)到用戶空間 |
strncpy_from_user __strncpy_from_user |
從用戶空間拷貝null結(jié)尾的字符串 |
strlen_user strnlen_user |
返回用戶空間中的字符串長度(以NULL結(jié)尾) |
clear_user __clear_user |
清空用戶空間的一段內(nèi)存區(qū)域(填0) |
3 動態(tài)地址檢查:修復(fù)代碼
如前所示,access_ok()對系統(tǒng)調(diào)用的線性地址參數(shù)進行一個粗略檢查。這可以保證用戶進程不會偽造內(nèi)核地址空間,但是,該線性地址仍然可能會不屬于進程地址空間。這種情況下,Page Fault異常將會發(fā)生。
在描述內(nèi)核如何檢測這種類型錯誤之前,先讓我們確定內(nèi)核態(tài)下可能發(fā)生Page Fault異常的4種情況。Page Fault異常處理程序必須區(qū)分這些情況,因為要采取的操作是完全不同的。
內(nèi)核訪問的用戶態(tài)內(nèi)存幀不存在,或是一個只讀頁,這種情況下,頁錯誤異常處理程序必須分配并初始化一個新內(nèi)存幀。(參考第9章的按需分頁和寫時復(fù)制章節(jié))
內(nèi)核訪問自己的內(nèi)存頁,但是相應(yīng)的頁表項還沒有初始化(參考第9章的處理非連續(xù)內(nèi)存區(qū)域訪問)。這種情況下,內(nèi)核必須在當(dāng)前進程的頁表中正確設(shè)置這些項。
某些內(nèi)核函數(shù)有bug,會造成異常發(fā)生;或者,異常是由瞬時硬件錯誤導(dǎo)致的。當(dāng)這種情況發(fā)生時,異常處理程序必須執(zhí)行內(nèi)核oops(參見第9章的在地址空間內(nèi)處理錯誤地址一節(jié))。
本章將要引入的情況:系統(tǒng)調(diào)用服務(wù)例程試圖讀寫不屬于進程地址空間的地址。
Page Fault異常處理程序可以通過確定錯誤的線性地址是否包含在進程所屬的內(nèi)存區(qū)域中,就可以輕松識別出第一種情況。通過檢測相應(yīng)的主內(nèi)核頁表項是否包含映射該地址的非空項即可?,F(xiàn)在,讓我們看看如何識別余下的兩種情況。
4 異常表
確定Page Fault異常源的關(guān)鍵在于,內(nèi)核可訪問進程地址空間的可用調(diào)用非常少。前面我們已經(jīng)描述了,僅有一組函數(shù)和宏可以用來訪問用戶進程地址空間。因此,如果異常是由無效參數(shù)引起的,則導(dǎo)致異常的指令必須包含在其中的一個函數(shù)或宏展開的代碼中。所以說,處理用戶空間的指令數(shù)量相當(dāng)少。
因此,將訪問進程地址空間的每個內(nèi)核指令的地址放入到異常表中并不難。如果我們做到這一點,其它工作就很容易了。當(dāng)Page Fault異常發(fā)生在內(nèi)核態(tài)時,do_page_fault()異常處理程序會檢查異常表:如果包含觸發(fā)異常的指令,則錯誤就是由不合適的系統(tǒng)調(diào)用參數(shù)引起的;否則,可能就是由更嚴重的錯誤導(dǎo)致的。
Linux定義了幾個異常表。主異常表是在構(gòu)建內(nèi)核鏡像時由C編譯器自動產(chǎn)生的。存儲在內(nèi)核代碼段的__ex_table段中,起始地址分別是__start___ex_table和__stop___ex_table,符號是由C編譯器產(chǎn)生的。
Linux 5.18.18中的鏈接文件定義(文件位置:include/asm-generic/vmlinux.lds.h):
/* *Exceptiontable */ #defineEXCEPTION_TABLE(align) .=ALIGN(align); __ex_table:AT(ADDR(__ex_table)-LOAD_OFFSET){ __start___ex_table=.; KEEP(*(__ex_table)) __stop___ex_table=.; }
更重要的是,內(nèi)核中動態(tài)加載的模塊包含了自己獨立的異常表。這個異常表是在構(gòu)建模塊鏡像時由C編譯器自動產(chǎn)生的,當(dāng)模塊被插入到正在運行的內(nèi)核中時,它會被加載到內(nèi)存中。
異常表的每一項,都是一個類型為exception_table_entry的數(shù)據(jù)結(jié)構(gòu),包含兩個域:
insn
訪問進程地址空間指令的線性地址。
fixup
執(zhí)行修正的匯編代碼的地址。insn指令觸發(fā)Page Fault異常時,調(diào)用這些匯編代碼。
修復(fù)代碼由一些匯編指令組成,用于解決由異常觸發(fā)的問題。正如我們將在本節(jié)后面看到的,修復(fù)代碼通常由一系列指令組成,這些指令強制服務(wù)例程向用戶態(tài)進程返回錯誤碼。這些指令通常定義在訪問進程地址空間的同一個宏或函數(shù)中,由C編譯器放置在稱為.fixup的內(nèi)核代碼段的獨立部分中。
search_exception_tables()函數(shù)用于在所有異常表中搜索指定的地址:如果該地址包含在表中,則該函數(shù)返回指向相應(yīng)exception_table_entry結(jié)構(gòu)的指針;否則,返回NULL。因此,Page Fault處理程序do_page_fault()執(zhí)行以下語句:
if((fixup=search_exception_tables(regs->eip))){ regs->eip=fixup->fixup; return1; }
正常情況下,regs->eip指向異常發(fā)生時內(nèi)核態(tài)棧上保存的eip寄存器值。如果寄存器中的值(發(fā)生異常的指令地址)在異常表中,do_page_fault()則用search_exception_tables()返回的表項中的地址替換寄存器中的值。然后,Page Fault處理程序終止,被中斷的程序繼續(xù)執(zhí)行修復(fù)代碼。
5 產(chǎn)生異常表和修復(fù)代碼
GNU Assembler的.section指令允許編程者指定可執(zhí)行文件的哪個section包含后面跟隨的代碼。正如我們將在第20章看到的,一個可執(zhí)行文件可以包含一個代碼segment,繼而,它又可以被分成幾個代碼section。因此,下面的代碼是將一個表項添加到異常表中;"a"屬性標(biāo)識該代碼section必須和內(nèi)核鏡像的其余部分一起加載到內(nèi)存中:
.section__ex_table,"a" .longfaulty_instruction_address,fixup_code_address .previous
.previous指令用于回到之前的位置,也就是離開__ex_table代碼段的定義,繼續(xù)處理之前的代碼。
讓我們再次考慮前面提到的__get_user_1()、__get_user_2()和__get_user_4()函數(shù)。真正訪問進程地址空間的指令是那些標(biāo)記為1、2和3處的匯編指令:
__get_user_1: [...] 1: movzbl (%eax), %edx [...] __get_user_2: [...] 2: movzwl -1(%eax), %edx [...] __get_user_4: [...] 3: movl -3(%eax), %edx [...] bad_get_user: xorl %edx, %edx movl $-EFAULT, %eax ret .section __ex_table,"a" .long 1b, bad_get_user .long 2b, bad_get_user .long 3b, bad_get_user .previous
Linux 5.18.18中的主要邏輯還是這樣,但是進行了代碼封裝。
每個異常表項由2個標(biāo)簽組成。第1個是帶有b后綴的數(shù)字標(biāo)簽,表示標(biāo)簽是backward,表示標(biāo)簽處的代碼在最近的前面。修復(fù)代碼對于這3個函數(shù)是通用的,標(biāo)記為bad_get_user。如果標(biāo)簽1、2或3處的匯編指令產(chǎn)生Page Fault異常,則執(zhí)行修復(fù)代碼。此處,僅僅是向發(fā)起系統(tǒng)調(diào)用的進程返回一個-EFAULT錯誤碼。
查看作用于用戶地址空間的其它內(nèi)核函數(shù)使用的修復(fù)代碼技術(shù)。例如,strlen_user(string)宏。該宏返回系統(tǒng)調(diào)用傳遞的一個以null結(jié)尾的字符串長度,如果發(fā)生錯誤,返回0。該宏實際產(chǎn)生以下匯編代碼:
movl $0, %eax ; 將eax設(shè)置為0, 作為計數(shù)器的初始值 movl $0x7fffffff, %ecx ; 將ecx設(shè)置為0x7fffffff, 即2^31-1, 即最大的有符號整數(shù)值 movl %ecx, %ebx ; 將ecx值拷貝到ebx中 movl string, %edi ; 將字符串string的地址賦值給edi寄存器 0: repne; scasb ; 執(zhí)行重復(fù)動作,將edi指向的內(nèi)存地址開始和累加器eax的值比較, ; 直到匹配到eax或ecx達到零。這個操作的目的是找到字符串中的 ; NULL字節(jié),也就是字符串的結(jié)尾 subl %ecx, %ebx ; 用計數(shù)器ecx的值減去計數(shù)器ebx的值,得到字符串長度 movl %ebx, %eax ; 將計數(shù)器ebx的值(也就是字符串的長度)復(fù)制到累加器eax中 1: .section .fixup,"ax" ; 定義了一個為.fixup的代碼段,并指定屬性為`ax` 2: xorl %eax, %eax ; 異或操作,寄存器清零 jmp 1b ; 跳轉(zhuǎn)到標(biāo)簽為1的代碼處 .previous .section __ex_table,"a" ; 定義了一個異常表, __ex_table, 屬性為a .long 0b, 2b ; 將標(biāo)簽0和標(biāo)簽2處的修復(fù)代碼地址存放到異常表項中 .previous
說明:
repne是重復(fù)執(zhí)行指令
scas是用來搜索字符,后綴b表示按字節(jié)搜索
寄存器ecx和ebx被初始化為0x7fffffff,表示用戶態(tài)地址空間中字符串允許的最大長度。repne;scasb匯編指令迭代掃描edi指向的字符串,并尋找eax寄存器中的0值(也就是字符串?結(jié)束符)。因為scasb每次迭代都會減小ecx的值,所以eax最終存儲了字符串總字節(jié)數(shù)(也就是字符串長度)。
該宏的修復(fù)代碼被插入到.fixup段中。ax屬性表示該代碼段被加載到內(nèi)存中且包含可執(zhí)行代碼。如果標(biāo)簽0的指令產(chǎn)生Page Fault異常,則執(zhí)行修復(fù)代碼;修復(fù)代碼也僅僅是返回了錯誤值0,而沒有返回字符串長度,然后就跳轉(zhuǎn)到了標(biāo)簽1處,標(biāo)簽1后面對應(yīng)的是宏后面的代碼。
第二個.section指令是將repne;scasb指令的地址和fixup代碼地址加入到異常表__ex_table所在的代碼段中。
6 架構(gòu)調(diào)用約定
EABI和OABI
ABI是應(yīng)用程序二進制接口,每個OS都會為運行在該OS的應(yīng)用程序提供ABI。ABI包含了應(yīng)用程序在這個OS下運行時必須遵守的編程約定。對于ARM架構(gòu)而言,它定義了函數(shù)調(diào)用約定、系統(tǒng)調(diào)用形式以及目標(biāo)文件格式等。
在ARM架構(gòu)中,存在兩種不同的ABI形式,OABI和EABI,OABI中的O是old的意思,表示舊有的ABI,而EABI是基于OABI上的改進,或者說它更適合目前大多數(shù)的硬件,OABI和EABI的區(qū)別主要在于浮點的處理和系統(tǒng)調(diào)用。浮點的區(qū)別不做過多討論,對于系統(tǒng)調(diào)用而言,OABI和EABI最大的區(qū)別在于,OABI 的系統(tǒng)調(diào)用指令需要傳遞參數(shù)來指定系統(tǒng)調(diào)用號,而EABI中將系統(tǒng)調(diào)用號保存在r7中。
架構(gòu)特定要求
每種結(jié)構(gòu)ABI對于如何將系統(tǒng)調(diào)用參數(shù)傳遞到內(nèi)核都有自己的要求。對于具有g(shù)libc封裝的系統(tǒng)調(diào)用(例如,大多數(shù)系統(tǒng)調(diào)用),glibc以適合架構(gòu)的方式處理將參數(shù)復(fù)制到正確寄存器。然而,當(dāng)使用syscall()進行系統(tǒng)調(diào)用時,調(diào)用者可能需要處理依賴于體系結(jié)構(gòu)的細節(jié);此要求在某些32位架構(gòu)上最常遇到。
例如,對于ARM架構(gòu)EABI,64位值(例如,long long)必須與偶數(shù)寄存器對對齊。因此,使用syscall()而不是glibc提供的封裝函數(shù),readahead(2)系統(tǒng)調(diào)用將在ARM架構(gòu)上以小端模式調(diào)用EABI,如下所示:
syscall(SYS_readahead,fd,0, (unsignedint)(offset&0xFFFFFFFF), (unsignedint)(offset>>32), count);
因為offset是64位,所以,fd占用了r0,填充0到r1,然后調(diào)用者需要手動將offset切割,以便將其存放到r2/r3這一對寄存器中。還需要注意大小端格式(依賴于平臺使用的C ABI約定)。
架構(gòu)調(diào)用約定
每種架構(gòu)都有調(diào)用和向內(nèi)核傳遞參數(shù)的方式。細節(jié)可以參考下面的兩個表。
第一張表列出了轉(zhuǎn)換到內(nèi)核態(tài)的調(diào)用指令(這可能不是最好或最快的方式,參考vdso機制),表示系統(tǒng)調(diào)用號的寄存器,表示返回系統(tǒng)調(diào)用結(jié)果的寄存器,以及表示發(fā)送錯誤信號的寄存器。
Arch/ABI | 指令 | 調(diào)用號 | 返回值 | 返回值 | 錯誤 | 注釋 |
---|---|---|---|---|---|---|
alpha | callsys | v0 | v0 | a4 | a3 | 1, 6 |
arc | trap0 | r8 | r0 | - | - | |
arm/OABI | swi NR | - | r0 | - | - | 2 |
arm/EABI | swi 0x0 | r7 | r0 | r1 | - | |
arm64 | svc #0 | w8 | x0 | x1 | - | |
blackfin | excpt 0x0 | P0 | R0 | - | - | |
i386 | int $0x80 | eax | eax | edx | - | |
ia64 | break 0x100000 | r15 | r8 | r9 | r10 | 1, 6 |
loongarch | syscall 0 | a7 | a0 | - | - | |
m68k | trap #0 | d0 | d0 | - | - | |
microblaze | brki r14,8 | r12 | r3 | - | - | |
mips | syscall | v0 | v0 | v1 | a3 | 1, 6 |
nios2 | trap | r2 | r2 | - | r7 | |
parisc | ble 0x100(%sr2, %r0) | r20 | r28 | - | - | |
powerpc | sc | r0 | r3 | - | r0 | 1 |
powerpc64 | sc | r0 | r3 | - | cr0.SO | 1 |
riscv | ecall | a7 | a0 | a1 | - | |
s390 | svc 0 | r1 | r2 | r3 | - | 3 |
s390x | svc 0 | r1 | r2 | r3 | - | 3 |
superh | trapa #31 | r3 | r0 | r1 | - | 4, 6 |
sparc/32 | t 0x10 | g1 | o0 | o1 | psr/csr | 1, 6 |
sparc/64 | t 0x6d | g1 | o0 | o1 | psr/csr | 1, 6 |
tile | swint1 | R10 | R00 | - | R01 | 1 |
x86-64 | syscall | rax | rax | rdx | - | 5 |
x32 | syscall | rax | rax | rdx | - | 5 |
xtensa | syscall | a2 | a2 | - | - |
注意:
有些架構(gòu)中,可能會選擇一個寄存器當(dāng)作布爾值使用(0表示無錯誤,-1表示錯誤),通過這種方式告知系統(tǒng)調(diào)用失敗。真正的錯誤值仍然存儲在返回寄存器中。在sparc架構(gòu)上,處理器狀態(tài)寄存器psr中的進位標(biāo)志位csr被用作一個完整寄存器使用。在powerpc64架構(gòu)中,條件寄存器cr0中字段0中的加法溢出標(biāo)志位(SO)會被當(dāng)做錯誤寄存器使用。
NR是系統(tǒng)調(diào)用號
對于s390和s390x,如果系統(tǒng)調(diào)用號小于256,可能會直接通過svc NR傳遞。
對于SuperH架構(gòu),因為歷史原因支持額外的陷阱號,但是trapa #31是推薦使用的。
x32和x86-64共享系統(tǒng)調(diào)用表,但是有細微差別。
某些架構(gòu)(如Alpha、IA-64、MIPS、SuperH、sparc/32、sparc/64)使用了額外的寄存器(返回值第二列),用其從pipe(2)系統(tǒng)調(diào)用中返回第二個返回值;Alpha還在系統(tǒng)調(diào)用getxpid(2)、getxuid(2)、getxgid(2)中使用這種技術(shù)。其它架構(gòu)沒有使用第二個返回寄存器,即使在System V ABI定義了相關(guān)寄存器。
第二個表:傳遞系統(tǒng)調(diào)用參數(shù)的寄存器約定
Arch/ABI | arg1 | arg2 | arg3 | arg4 | arg5 | arg6 | arg7 | 注釋 |
---|---|---|---|---|---|---|---|---|
alpha | a0 | a1 | a2 | a3 | a4 | a5 | - | |
arc | r0 | r1 | r2 | r3 | r4 | r5 | - | |
arm/OABI | r0 | r1 | r2 | r3 | r4 | r5 | r6 | |
arm/EABI | r0 | r1 | r2 | r3 | r4 | r5 | r6 | |
arm64 | x0 | x1 | x2 | x3 | x4 | x5 | - | |
blackfin | R0 | R1 | R2 | R3 | R4 | R5 | - | |
i386 | ebx | ecx | edx | esi | edi | ebp | - | |
ia64 | out0 | out1 | out2 | out3 | out4 | out5 | - | |
loongarch | a0 | a1 | a2 | a3 | a4 | a5 | a6 | |
m68k | d1 | d2 | d3 | d4 | d5 | a0 | - | |
microblaze | r5 | r6 | r7 | r8 | r9 | r10 | - | |
mips/o32 | a0 | a1 | a2 | a3 | - | - | - | 1 |
mips/n32,64 | a0 | a1 | a2 | a3 | a4 | a5 | - | |
nios2 | r4 | r5 | r6 | r7 | r8 | r9 | - | |
parisc | r26 | r25 | r24 | r23 | r22 | r21 | - | |
powerpc | r3 | r4 | r5 | r6 | r7 | r8 | r9 | |
powerpc64 | r3 | r4 | r5 | r6 | r7 | r8 | - | |
riscv | a0 | a1 | a2 | a3 | a4 | a5 | - | |
s390 | r2 | r3 | r4 | r5 | r6 | r7 | - | |
s390x | r2 | r3 | r4 | r5 | r6 | r7 | - | |
superh | r4 | r5 | r6 | r7 | r0 | r1 | r2 | |
sparc/32 | o0 | o1 | o2 | o3 | o4 | o5 | - | |
sparc/64 | o0 | o1 | o2 | o3 | o4 | o5 | - | |
tile | R00 | R01 | R02 | R03 | R04 | R05 | - | |
x86-64 | rdi | rsi | rdx | r10 | r8 | r9 | - | |
x32 | rdi | rsi | rdx | r10 | r8 | r9 | - | |
xtensa | a6 | a3 | a4 | a5 | a8 | a9 | - |
注釋:mips/o32在用戶棧上傳遞5~8參數(shù)
審核編輯:湯梓紅
-
內(nèi)核
+關(guān)注
關(guān)注
3文章
1360瀏覽量
40185 -
Linux
+關(guān)注
關(guān)注
87文章
11207瀏覽量
208717 -
參數(shù)
+關(guān)注
關(guān)注
11文章
1754瀏覽量
32043 -
函數(shù)
+關(guān)注
關(guān)注
3文章
4277瀏覽量
62323 -
系統(tǒng)調(diào)用
+關(guān)注
關(guān)注
0文章
28瀏覽量
8317
原文標(biāo)題:linux內(nèi)核-系統(tǒng)調(diào)用之參數(shù)傳遞
文章出處:【微信號:嵌入式ARM和Linux,微信公眾號:嵌入式ARM和Linux】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論