0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

linux內(nèi)核系統(tǒng)調(diào)用之參數(shù)傳遞

jf_0tjVfeJz ? 來源:嵌入式ARM和Linux ? 2023-12-20 09:32 ? 次閱讀

與普通函數(shù)一樣,系統(tǒng)調(diào)用通常需要一些輸入/輸出參數(shù),這些參數(shù)可能包括實際值(即數(shù)字)、用戶模式進(jìn)程地址空間中的變量地址,甚至包括指向用戶模式函數(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)核怎么知道你想干什么呢?比如,我們在寫多進(jìn)程應(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)準(zhǔn),不能存儲在32位寄存器中的大參數(shù)必須通過引用傳遞。一個典型的例子是settimeofday()系統(tǒng)調(diào)用,它必須讀取64位結(jié)構(gòu)。

對于需要6個以上參數(shù)的系統(tǒng)調(diào)用,使用單個寄存器指向進(jìn)程地址空間中包含參數(shù)值的內(nèi)存區(qū)域。當(dāng)然,程序員不必關(guān)心其中的細(xì)節(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ù)例程進(jìn)入棧時,它會找到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ù)需要知道這些寄存器的值,以便將它們拷貝到子進(jìn)程的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是之前打開的文件描述符嗎,進(jìn)程是否允許對其進(jìn)行寫操作。如果有條件不滿足,則返回負(fù)值,錯誤碼-EBADF。

但是,有一類型的檢查對于所有系統(tǒng)調(diào)用是通用的。每當(dāng)參數(shù)指定地址時,內(nèi)核必須檢查是否在進(jìn)程的地址空間中。檢查有兩種方法:

驗證線性地址是否屬于進(jìn)程地址空間,如果是,包含該地址的內(nèi)存是否有正確的訪問權(quán)限。

僅驗證該線性地址小于PAGE_OFFSET(也就是沒有落在為內(nèi)核保留的間隔地址范圍內(nèi))。

早期的Linux執(zhí)行第一種檢查,但是這相當(dāng)耗時,因為必須為系統(tǒng)調(diào)用中的每個地址參數(shù)執(zhí)行檢查;更重要的是,這通常是無用的,因為錯誤程序并不總是常見。

因此,從2.2版本開始,Linux采用了第2種方案。該方法非常高效,因為不用對進(jìn)程所使用的內(nèi)存區(qū)域描述符進(jìn)行掃描。很明顯,這是粗放式的檢查:驗證線性地址小于PAGE_OFFSET,是驗證其正確性的必要條件但不是充分條件。但是,使用這方法并沒有風(fēng)險,因為后面會造成其它錯誤。

該方法將真正的檢查推遲到了最后時刻,也就是說,物理分頁單元將線性地址轉(zhuǎn)換成物理地址的時候。我們將在后面的“動態(tài)地址檢查:修復(fù)代碼”中,闡述頁錯誤異常處理程序如何檢測到那些用戶態(tài)傳遞給內(nèi)核的錯誤地址(參數(shù))。

此刻,可能有人會想為什么執(zhí)行這種粗檢查?因為這對于保護(hù)進(jìn)程地址空間和內(nèi)核地址空間的非法訪問是至關(guān)重要的。第2章中我們得知,物理內(nèi)存從線性地址PAGE_OFFSET開始映射。這意味著內(nèi)核服務(wù)例程能夠訪問內(nèi)存中的所有內(nèi)存頁。因此,如果不做粗檢查,用戶進(jìn)程可能會傳遞屬于內(nèi)核地址空間的地址作為參數(shù),然后,就能夠訪問這些內(nèi)存而不會造成頁錯誤異常。

對系統(tǒng)調(diào)用的地址參數(shù)進(jìn)行檢查可以使用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字段中的值。對于普通進(jìn)程,該字段為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 訪問進(jìn)程地址空間

系統(tǒng)服務(wù)例程經(jīng)常需要讀寫進(jìn)程地址空間中的數(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/*如果進(jìn)位標(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值進(jìn)行比較*/
/*用于檢查線性地址是否小于addr_limit.seg*/
jaebad_get_user/*如果線性地址≥addr_limit.seg,則跳轉(zhuǎn)到bad_get_user*/
2:movzwl-1(%eax),%edx/*eax中的地址減1,去除一個16位無符號數(shù)進(jìn)行零擴(kuò)展到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)前進(jìn)程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ù)和宏可以訪問用戶進(jìn)程地址空間,如表10-1所示。注意,它們都有一個以下劃線(__)為前綴的變體。沒有下劃線的函數(shù)和宏會額外檢查想要訪問的線性地址塊是否合法,而有下劃線的則會繞過檢查。尤其是當(dāng)內(nèi)核需要重復(fù)訪問進(jìn)程地址空間中同一片區(qū)域時,只在開始檢查一遍地址更有效率。

表10-1 可以訪問進(jìn)程地址空間的函數(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ù)進(jìn)行一個粗略檢查。這可以保證用戶進(jìn)程不會偽造內(nèi)核地址空間,但是,該線性地址仍然可能會不屬于進(jìn)程地址空間。這種情況下,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)前進(jìn)程的頁表中正確設(shè)置這些項。

某些內(nèi)核函數(shù)有bug,會造成異常發(fā)生;或者,異常是由瞬時硬件錯誤導(dǎo)致的。當(dāng)這種情況發(fā)生時,異常處理程序必須執(zhí)行內(nèi)核oops(參見第9章的在地址空間內(nèi)處理錯誤地址一節(jié))。

本章將要引入的情況:系統(tǒng)調(diào)用服務(wù)例程試圖讀寫不屬于進(jìn)程地址空間的地址。

Page Fault異常處理程序可以通過確定錯誤的線性地址是否包含在進(jìn)程所屬的內(nèi)存區(qū)域中,就可以輕松識別出第一種情況。通過檢測相應(yīng)的主內(nèi)核頁表項是否包含映射該地址的非空項即可?,F(xiàn)在,讓我們看看如何識別余下的兩種情況。

4 異常表

確定Page Fault異常源的關(guān)鍵在于,內(nèi)核可訪問進(jìn)程地址空間的可用調(diào)用非常少。前面我們已經(jīng)描述了,僅有一組函數(shù)和宏可以用來訪問用戶進(jìn)程地址空間。因此,如果異常是由無效參數(shù)引起的,則導(dǎo)致異常的指令必須包含在其中的一個函數(shù)或宏展開的代碼中。所以說,處理用戶空間的指令數(shù)量相當(dāng)少。

因此,將訪問進(jìn)程地址空間的每個內(nèi)核指令的地址放入到異常表中并不難。如果我們做到這一點,其它工作就很容易了。當(dāng)Page Fault異常發(fā)生在內(nèi)核態(tài)時,do_page_fault()異常處理程序會檢查異常表:如果包含觸發(fā)異常的指令,則錯誤就是由不合適的系統(tǒng)調(diào)用參數(shù)引起的;否則,可能就是由更嚴(yán)重的錯誤導(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

訪問進(jìn)程地址空間指令的線性地址。

fixup

執(zhí)行修正的匯編代碼的地址。insn指令觸發(fā)Page Fault異常時,調(diào)用這些匯編代碼。

修復(fù)代碼由一些匯編指令組成,用于解決由異常觸發(fā)的問題。正如我們將在本節(jié)后面看到的,修復(fù)代碼通常由一系列指令組成,這些指令強制服務(wù)例程向用戶態(tài)進(jìn)程返回錯誤碼。這些指令通常定義在訪問進(jìn)程地址空間的同一個宏或函數(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ù)。真正訪問進(jìn)程地址空間的指令是那些標(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中的主要邏輯還是這樣,但是進(jìn)行了代碼封裝。

每個異常表項由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)用的進(jìn)程返回一個-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達(dá)到零。這個操作的目的是找到字符串中的
                            ; 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)用程序二進(jìn)制接口,每個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上的改進(jìn),或者說它更適合目前大多數(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()進(jìn)行系統(tǒng)調(diào)用時,調(diào)用者可能需要處理依賴于體系結(jié)構(gòu)的細(xì)節(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ù)的方式。細(xì)節(jié)可以參考下面的兩個表。

第一張表列出了轉(zhuǎn)換到內(nèi)核態(tài)的調(diào)用指令(這可能不是最好或最快的方式,參考vdso機(jī)制),表示系統(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中的進(jìn)位標(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)用表,但是有細(xì)微差別。

某些架構(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)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • 內(nèi)核
    +關(guān)注

    關(guān)注

    3

    文章

    1346

    瀏覽量

    40152
  • Linux
    +關(guān)注

    關(guān)注

    87

    文章

    11161

    瀏覽量

    208460
  • 參數(shù)
    +關(guān)注

    關(guān)注

    11

    文章

    1728

    瀏覽量

    31980
  • 函數(shù)
    +關(guān)注

    關(guān)注

    3

    文章

    4256

    瀏覽量

    62223
  • 系統(tǒng)調(diào)用

    關(guān)注

    0

    文章

    27

    瀏覽量

    8315

原文標(biāo)題:linux內(nèi)核-系統(tǒng)調(diào)用之參數(shù)傳遞

文章出處:【微信號:嵌入式ARM和Linux,微信公眾號:嵌入式ARM和Linux】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

收藏 人收藏

    評論

    相關(guān)推薦

    Linux內(nèi)核系統(tǒng)調(diào)用詳解

    Linux內(nèi)核中設(shè)置了一組用于實現(xiàn)各種系統(tǒng)功能的子程序,稱為系統(tǒng)調(diào)用。用戶可以通過系統(tǒng)
    發(fā)表于 08-23 10:37 ?737次閱讀
    <b class='flag-5'>Linux</b><b class='flag-5'>內(nèi)核</b>中<b class='flag-5'>系統(tǒng)</b><b class='flag-5'>調(diào)用</b>詳解

    Linux內(nèi)核中信號的傳遞過程

    前面我們已經(jīng)介紹了內(nèi)核注意到信號的到來,調(diào)用相關(guān)函數(shù)更新進(jìn)程描述符以便進(jìn)程接收處理信號。但是,如果目標(biāo)進(jìn)程此時沒有運行,內(nèi)核則推遲傳遞信號。現(xiàn)在,我們看看
    的頭像 發(fā)表于 01-17 09:51 ?959次閱讀
    <b class='flag-5'>Linux</b><b class='flag-5'>內(nèi)核</b>中信號的<b class='flag-5'>傳遞</b>過程

    Linux內(nèi)核系統(tǒng)調(diào)用

    Linux內(nèi)核系統(tǒng)調(diào)用1. 應(yīng)用程序通過API而不是直接調(diào)用系統(tǒng)
    發(fā)表于 02-21 10:49

    ARM linux系統(tǒng)調(diào)用的實現(xiàn)原理

    大家都知道linux的應(yīng)用程序要想訪問內(nèi)核必須使用系統(tǒng)調(diào)用從而實現(xiàn)從usr模式轉(zhuǎn)到svc模式。下面咱們看看它的實現(xiàn)過程。
    發(fā)表于 05-30 11:24 ?2228次閱讀

    Linux內(nèi)核系統(tǒng)調(diào)用擴(kuò)展研究

    系統(tǒng)凋用是操作系統(tǒng)內(nèi)核提供給用戶使用內(nèi)核服務(wù)的接口。LinuX操作系統(tǒng)由于其自由開放性,用戶可在
    發(fā)表于 07-25 16:09 ?40次下載
    <b class='flag-5'>Linux</b><b class='flag-5'>內(nèi)核</b><b class='flag-5'>系統(tǒng)</b><b class='flag-5'>調(diào)用</b>擴(kuò)展研究

    編譯Linux2.6內(nèi)核并添加一個系統(tǒng)調(diào)用

    本文以實例來詳細(xì)描述了從準(zhǔn)備一直到使用新內(nèi)核Linux2.6 內(nèi)核編譯過程,然后介紹了添加系統(tǒng)調(diào)用的實現(xiàn)步驟,最后給實驗結(jié)果。
    發(fā)表于 12-01 15:54 ?46次下載

    基于AT91RM9200 處理器系統(tǒng)中BootLoader與內(nèi)核參數(shù)傳遞

    本文著重介BootLoader與內(nèi)核之間 參數(shù)傳遞 這一基本功能。本文的硬件平臺是基于AT91RM9200 處理器系統(tǒng),軟件平臺是Linux
    發(fā)表于 03-28 09:04 ?1910次閱讀
    基于AT91RM9200 處理器<b class='flag-5'>系統(tǒng)</b>中BootLoader與<b class='flag-5'>內(nèi)核</b>的<b class='flag-5'>參數(shù)</b><b class='flag-5'>傳遞</b>

    BootLoader與Linux內(nèi)核參數(shù)傳遞

    在嵌入式系統(tǒng)中,BootLoader 是用來初始化硬件,加載內(nèi)核傳遞參數(shù)。因為嵌入式系統(tǒng)的硬件環(huán)境各不相同,所以嵌入式
    發(fā)表于 04-02 14:31 ?338次閱讀

    Linux系統(tǒng)調(diào)用的技巧

    ()展開,則用戶程序必須包含該文 件。當(dāng)進(jìn)程執(zhí)行到用戶程序的系統(tǒng)調(diào)用命令時,實際上執(zhí)  行了由宏命令_syscallN()展開的函數(shù)。系統(tǒng)調(diào)用參數(shù)
    發(fā)表于 04-02 14:36 ?371次閱讀

    linux內(nèi)核參數(shù)設(shè)置_linux內(nèi)核的功能有哪些

    本文主要闡述了linux內(nèi)核參數(shù)設(shè)置及linux內(nèi)核的功能。
    發(fā)表于 09-17 14:40 ?1344次閱讀
    <b class='flag-5'>linux</b><b class='flag-5'>內(nèi)核</b><b class='flag-5'>參數(shù)</b>設(shè)置_<b class='flag-5'>linux</b><b class='flag-5'>內(nèi)核</b>的功能有哪些

    BootLoader與Linux內(nèi)核參數(shù)傳遞詳細(xì)資料說明

    不同的體系結(jié)構(gòu),如 ARM, Powerpc,X86,MIPS等。本文著重介紹 Bootloader與內(nèi)核之間參數(shù)傳遞這一基本功能。本文的硬件平臺是基于AT91RM9200處理器系統(tǒng),
    發(fā)表于 03-16 10:39 ?13次下載
    BootLoader與<b class='flag-5'>Linux</b><b class='flag-5'>內(nèi)核</b>的<b class='flag-5'>參數(shù)</b><b class='flag-5'>傳遞</b>詳細(xì)資料說明

    如何區(qū)分xenomai、linux系統(tǒng)調(diào)用/服務(wù)

    對于同一個POSIX接口應(yīng)用程序,可能既需要xenomai內(nèi)核提供服務(wù)(xenomai 系統(tǒng)調(diào)用),又需要調(diào)用linux
    的頭像 發(fā)表于 05-10 10:28 ?1963次閱讀

    Linux內(nèi)核系統(tǒng)調(diào)用概述及實現(xiàn)原理

    本文介紹了系統(tǒng)調(diào)用的一些實現(xiàn)細(xì)節(jié)。首先分析了系統(tǒng)調(diào)用的意義,它們與庫函數(shù)和應(yīng)用程序接口(API)有怎樣的關(guān)系。然后,我們考察了Linux
    的頭像 發(fā)表于 05-14 14:11 ?2139次閱讀
    <b class='flag-5'>Linux</b><b class='flag-5'>內(nèi)核</b><b class='flag-5'>系統(tǒng)</b><b class='flag-5'>調(diào)用</b>概述及實現(xiàn)原理

    Linux內(nèi)核模塊參數(shù)傳遞與sysfs文件系統(tǒng)

    Linux應(yīng)用開發(fā)中,為使應(yīng)用程序更加靈活地執(zhí)行用戶的預(yù)期功能,我們有時候會通過命令行傳遞一些參數(shù)到main函數(shù)中,使得代碼邏輯可以依據(jù)參數(shù)執(zhí)行不同的任務(wù)。同樣,
    發(fā)表于 06-07 16:23 ?1982次閱讀

    Linux系統(tǒng)調(diào)用的具體實現(xiàn)原理

    文我將基于 ARM 體系結(jié)構(gòu)角度,從 Linux 應(yīng)用層例子到內(nèi)核系統(tǒng)調(diào)用函數(shù)的整個過程來梳理一遍,講清楚linux
    的頭像 發(fā)表于 09-05 17:16 ?1030次閱讀
    <b class='flag-5'>Linux</b><b class='flag-5'>系統(tǒng)</b><b class='flag-5'>調(diào)用</b>的具體實現(xiàn)原理