摘要:一般情況下程序計數(shù)器中的值總是按照程序指令順序更新,只有在執(zhí)行跳轉(zhuǎn)指令和函數(shù)調(diào)用指令時才會打破執(zhí)行的順序。不同的體系都提供了特定的函數(shù)調(diào)用指令來實現(xiàn)函數(shù)調(diào)用的功能。位體系下的函數(shù)調(diào)用規(guī)則函數(shù)的調(diào)用函數(shù)調(diào)
古器合尺度,法物應(yīng)矩規(guī)。--蘇洵
一、什么是函數(shù)
可執(zhí)行程序是為了實現(xiàn)某個功能而由不同機器指令按特定規(guī)則進行組合排列的集合。無論高級還是低級程序語言,無論是面向?qū)ο筮€是面向過程的語言最終的代碼都會轉(zhuǎn)化為一條條機器指令的形式被執(zhí)行。為了管理上的方便和對代碼的復(fù)用,往往需要將某一段實現(xiàn)特定功能的指令集合進行抽離和處理從而形成了函數(shù)的概念,函數(shù)也可以稱之為子程序或者子例程。出現(xiàn)函數(shù)的概念后可執(zhí)行程序的機器指令集合將不再是單一的一塊代碼,而是由多個函數(shù)組成的分塊代碼,這樣可執(zhí)行程序就變成了由函數(shù)之間相互調(diào)用這種方式來構(gòu)建和組織了。
一個函數(shù)由函數(shù)簽名、參數(shù)、返回、實現(xiàn)四部分組成。函數(shù)的前三者定義了明確的邊界信息,也稱之為函數(shù)接口描述。函數(shù)接口描述的意義在于調(diào)用者不再需要了解被調(diào)用者函數(shù)的實現(xiàn)細節(jié),而只需要按被調(diào)用者的定義的接口進行交互即可。如何去定義一個函數(shù),如何去實現(xiàn)一個函數(shù),如何去調(diào)用一個函數(shù),如何將參數(shù)傳遞給被調(diào)用的函數(shù),如何使用被調(diào)用者函數(shù)的返回這些都需要有統(tǒng)一的標準規(guī)范來進行界定,這個規(guī)則有兩個層面的標準:在高級語言層面的規(guī)則稱之為API規(guī)則;而在機器指令層面上則由于不同的操作系統(tǒng)以及不同的CPU體系結(jié)構(gòu)下提供的指令集和構(gòu)造程序的方式不同而不同,所以在系統(tǒng)層面的規(guī)則稱之為ABI規(guī)則。本文的重點是詳細介紹函數(shù)調(diào)用、函數(shù)參數(shù)傳遞、函數(shù)返回值這3個方面的ABI規(guī)則,通過對這些規(guī)則的詳細介紹相信您對什么是函數(shù)就會有更加深入的了解。需要注意的是這里的ABI規(guī)則是指基于OC語言實現(xiàn)的程序的ABI規(guī)則,這些規(guī)則并不適用于通過Swift實現(xiàn)的程序以及不適用于Linux等其他操作系統(tǒng)的ABI規(guī)則。
由于內(nèi)容過多因此我將分為兩篇文章來做具體介紹,前一篇文章介紹函數(shù)接口相關(guān)的內(nèi)容,后一篇文章介紹函數(shù)實現(xiàn)相關(guān)的內(nèi)容。
二、函數(shù)調(diào)用
CPU中的程序計數(shù)器(IP/PC)中總是保存著下一條將要執(zhí)行的指令的內(nèi)存地址,這樣每執(zhí)行一條指令就會更新程序計數(shù)器中的值,從而可以繼續(xù)執(zhí)行下一條指令。系統(tǒng)就是這樣通過不停的變化程序計數(shù)器中的值來實現(xiàn)程序指令的執(zhí)行的。一般情況下程序計數(shù)器中的值總是按照程序指令順序更新,只有在執(zhí)行跳轉(zhuǎn)指令和函數(shù)調(diào)用指令時才會打破執(zhí)行的順序。
函數(shù)調(diào)用的本質(zhì)就是將函數(shù)在內(nèi)存中的首地址賦值給程序計數(shù)器(IP/PC),這樣下一條執(zhí)行的指令就變?yōu)榱撕瘮?shù)首地址處的指令,從而實現(xiàn)函數(shù)的調(diào)用。除了要更新程序計數(shù)器的值外還需要保存調(diào)用現(xiàn)場,以便當函數(shù)調(diào)用返回后繼續(xù)執(zhí)行函數(shù)調(diào)用的下一條指令,所以這里所謂的保存調(diào)用現(xiàn)場就是將函數(shù)調(diào)用的下一條指令的地址保存起來。不同的CPU體系都提供了特定的函數(shù)調(diào)用指令來實現(xiàn)函數(shù)調(diào)用的功能。比如x86系統(tǒng)提供一條稱之為call的指令來實現(xiàn)函數(shù)調(diào)用,call指令除了會更新程序計數(shù)器的值外還會把函數(shù)調(diào)用的下一條指令壓入到棧中進行保存;arm系統(tǒng)則提供b系列的指令來實現(xiàn)函數(shù)調(diào)用,b系列指令除了會更新程序計數(shù)器的值外還會把函數(shù)調(diào)用的下一條指令保存到LR寄存器中。
函數(shù)返回的本質(zhì)就是將前面說到的保存的調(diào)用現(xiàn)場地址賦值給程序計數(shù)器,這樣下一條執(zhí)行的指令就變?yōu)榱苏{(diào)用者調(diào)用被調(diào)函數(shù)的下一條指令了。不同的CPU體系也都提供了特定的函數(shù)返回指令來實現(xiàn)函數(shù)返回的功能(arm32位系統(tǒng)除外)。比如x86系統(tǒng)提供一條稱之為ret的指令來實現(xiàn)函數(shù)返回,此指令會將棧頂保存的地址賦值給程序計數(shù)器然后執(zhí)行出棧操作;arm64位系統(tǒng)也提供一條ret指令來實現(xiàn)函數(shù)的返回,此指令則會把當前的LR寄存器的值賦值給程序計數(shù)器。
對于x86系統(tǒng)來說因為執(zhí)行函數(shù)調(diào)用前會將調(diào)用者的下一條指令壓入棧中,而被調(diào)用者函數(shù)內(nèi)部因為有本地棧幀(stack frame)的定義又會將棧頂下移,所以在被調(diào)用者函數(shù)執(zhí)行ret指令返回之前需要確保當前堆棧寄存器SP所指向的棧頂?shù)刂芬捅徽{(diào)用函數(shù)執(zhí)行前的棧頂?shù)刂繁3忠恢拢蝗划攔et指令執(zhí)行時取出的調(diào)用者的下一條指令的值將是錯誤的,從而會產(chǎn)生崩潰異常。
對于arm系統(tǒng)來說因為LR寄存器只有一個,因此如果被調(diào)用函數(shù)內(nèi)部也調(diào)用其他函數(shù)時也會更新LR寄存器的值,一旦LR寄存器被更新后將無法恢復(fù)正確的調(diào)用現(xiàn)場,所以一般情況下被調(diào)用函數(shù)的前幾條指令做的事情就是將LR寄存器的值保存到棧內(nèi)存中,而被調(diào)用函數(shù)的最后幾條指令所的事情就是將棧內(nèi)存中保存的內(nèi)容恢復(fù)到LR寄存器。
有一種特殊的函數(shù)調(diào)用場景就是當函數(shù)調(diào)用發(fā)生在調(diào)用者函數(shù)的最后一條指令時,則不需要進行調(diào)用現(xiàn)場的保護處理,同時也會將函數(shù)調(diào)用指令改為跳轉(zhuǎn)指令,原因是因為調(diào)用者的最后一條指令再無下一條有效的指令,而仍然采用調(diào)用指令的話則保存的調(diào)用現(xiàn)場則是個無效的地址,這樣當函數(shù)返回時將跳轉(zhuǎn)到這個無效的地址從而產(chǎn)生執(zhí)行異常!
為了更好的描述函數(shù)的調(diào)用規(guī)則,假設(shè)A函數(shù)內(nèi)部調(diào)用了B函數(shù)和C函數(shù),下面定義了各函數(shù)的地址,以及函數(shù)調(diào)用處的地址,以及函數(shù)調(diào)用的偽代碼塊:
//這里的XX,YY,ZZ代表的是函數(shù)指令在內(nèi)存中的地址。 A XX1: XX2: 調(diào)用B函數(shù)地址YY1 XX3: XX4: XXn: 跳轉(zhuǎn)到C函數(shù)ZZ1 B YY1: YY2: YY3: YYn: 返回 C ZZ1: ZZ2: ZZ3: ZZn: 返回
1. x86_64體系下的函數(shù)調(diào)用規(guī)則
1.1 函數(shù)的調(diào)用
函數(shù)調(diào)用的指令是call 指令。在匯編語言中call 指令后面的操作數(shù)是調(diào)用的目標函數(shù)的絕對地址,而實際的機器指令中的操作數(shù)則是一個相對地址值,這個地址值是目標函數(shù)地址距離當前指令地址的相對偏移值。無論是x86系統(tǒng)還是arm系統(tǒng)如果指令中的操作數(shù)部分的值是內(nèi)存地址的話,一般都是相對當前指令的偏移地址而不是絕對地址。下面就是函數(shù)調(diào)用指令以及其內(nèi)部實現(xiàn)的等價操作。
call YY1 <==> RIP = YY1, RSP = RSP-8, *RSP = XX3
也就是說執(zhí)行一條函數(shù)調(diào)用指令等價于將指令中的地址賦值給IP寄存器,同時把函數(shù)的返回地址壓入棧寄存器中去。
1.2 函數(shù)的跳轉(zhuǎn)
函數(shù)跳轉(zhuǎn)的指令是jmp指令。在匯編語言中jmp 指令后面的操作數(shù)是調(diào)用的目標函數(shù)的絕對地址,而實際的機器指令中的操作數(shù)則是一個相對地址值,這個地址值是目標函數(shù)地址距離當前指令地址的相對偏移值,下面就是函數(shù)跳轉(zhuǎn)指令以及其內(nèi)部實現(xiàn)的等價操作。
jmp ZZ1 <==> RIP = ZZ1
也就是說執(zhí)行一條跳轉(zhuǎn)指令等價于將指令中的地址賦值給IP寄存器。
1.3 函數(shù)的返回
函數(shù)返回的指令是ret指令。ret指令后面一般不跟操作數(shù),下面就是函數(shù)返回指令以及其內(nèi)部實現(xiàn)的等價操作。
ret <==> RIP = *RSP, RSP = RSP + 8
也就是說執(zhí)行一條ret指令等價于將當前棧寄存器中的值賦值給IP寄存器,同時棧寄存器執(zhí)行POP操作。
2. arm32位體系下的函數(shù)調(diào)用規(guī)則
2.1 函數(shù)的調(diào)用
函數(shù)的調(diào)用指令為bl/blx。 這兩條指令的操作數(shù)可以是相對地址偏移也可以是寄存器。bl/blx的區(qū)別就是bl函數(shù)調(diào)用不會切換指令集,而blx調(diào)用則會從thumb指令集切換到arm指令集或者相反切換。arm32系統(tǒng)中存在著兩套指令集即thumb指令集和arm指令集,其中的arm指令集中的所有的指令的長度都是32位而thumb指令集則存在著32位和16位兩種長度的指令集。兩種指令集是以函數(shù)為單位進行使用的,也就是說一個函數(shù)中的所有指令要么都是arm指令要么就都是thumb指令。正是因為如此如果調(diào)用者函數(shù)和被調(diào)用者函數(shù)之間用的是不同的指令集則需要通過blx來執(zhí)行函數(shù)調(diào)用,而如果二者所用的指令集相同則需要通過bl指令來執(zhí)行調(diào)用。下面就是函數(shù)調(diào)用指令以及其內(nèi)部實現(xiàn)的等價操作。
bl/blx YY1 <==> PC = YY1, LR = XX3
也就是說執(zhí)行一條函數(shù)調(diào)用指令等價于將指令中的地址賦值給PC寄存器,同時把函數(shù)的返回地址賦值給LR寄存器中去。
2.2 函數(shù)的跳轉(zhuǎn)
函數(shù)的跳轉(zhuǎn)指令是b/bx, 這兩條指令的操作數(shù)可以是相對地址偏移也可以是寄存器,b/bx的區(qū)別就是b函數(shù)調(diào)用不會切換指令集。下面就是函數(shù)跳轉(zhuǎn)指令以及其內(nèi)部實現(xiàn)的等價操作。
b/bx ZZ1 <==> PC = ZZ1
也就是說跳轉(zhuǎn)指令等價于將指令中的地址賦值給PC寄存器。
2.3 函數(shù)的返回
arm32位系統(tǒng)沒有專門的函數(shù)返回ret指令,因為arm32位系統(tǒng)可以直接修改PC寄存器的值,所以函數(shù)返回可以直接給PC指令賦值,也可以通過調(diào)用b/bx LR 來實現(xiàn)函數(shù)的返回處理。
b/bx LR //或者 mov PC, XXX
arm32位系統(tǒng)可以直接修改PC寄存器的值,因此函數(shù)返回時可以直接設(shè)置PC寄存器的值為函數(shù)的返回地址,也可以執(zhí)行b/bx跳轉(zhuǎn)指令并指定目標地址為LR寄存器中的值。
3.arm64位體系下的函數(shù)調(diào)用規(guī)則
3.1 函數(shù)的調(diào)用
函數(shù)調(diào)用的指令是bl/blr 其中bl指令的操作數(shù)是距離當前位置相對距離的偏移地址,blr指令的操作數(shù)則是寄存器,表明調(diào)用寄存器所指定的地址。因為bl指令中的操作數(shù)部分是函數(shù)的相對偏移地址,又因為arm64位系統(tǒng)的一條指令占用4個字節(jié),根據(jù)指令的定義bl指令所能跳轉(zhuǎn)的范圍是距離當前位置±32MB的范圍,所以如果要跳轉(zhuǎn)到更遠的地址則需要借助blr指令。 下面就是函數(shù)調(diào)用指令以及其內(nèi)部實現(xiàn)的等價操作。
//如果YY1地址離調(diào)用指令的距離是在±32MB內(nèi)則使用bl指令即可。 bl YY1 <==> PC = YY1, LR = XX3 //如果YY1地址離調(diào)用指令的距離超過±32MB則使用blr指令執(zhí)行間接調(diào)用。 ldr x16, YY1 blr x16
也就是說執(zhí)行一條函數(shù)調(diào)用指令等價于將指令中的地址賦值給PC寄存器,同時把函數(shù)的返回地址賦值給LR寄存器中去。
3.2函數(shù)的跳轉(zhuǎn)
函數(shù)跳轉(zhuǎn)的指令是b/br, 其中b指令的操作數(shù)是距離當前位置相對距離的偏移地址,br指令的操作數(shù)則是寄存器,表明跳轉(zhuǎn)到寄存器所指定的地址中去。下面就是函數(shù)跳轉(zhuǎn)指令以及其內(nèi)部實現(xiàn)的等價操作。
b ZZ1 <==> PC = ZZ1
也就是說跳轉(zhuǎn)指令等價于將指令中的地址賦值給PC寄存器。
3.3 函數(shù)的返回
函數(shù)返回的指令是 ret, 下面就是函數(shù)返回指令以及其內(nèi)部實現(xiàn)的等價操作。
ret <==> PC = LR
也就是說執(zhí)行一條ret指令等價于將LR寄存器中的值賦值給PC寄存器。
三、函數(shù)參數(shù)傳遞
某些函數(shù)定義中有參數(shù)需要傳遞,需要由調(diào)用者函數(shù)將參數(shù)傳遞給被調(diào)用者函數(shù),因此在調(diào)用這類函數(shù)時,需要在執(zhí)行函數(shù)調(diào)用指令之前,進行函數(shù)參數(shù)的傳遞。函數(shù)的參數(shù)個數(shù)可以為0個,也可以為某個固定的數(shù)量,也可以為任意數(shù)量(可變參數(shù))。 函數(shù)的每個參數(shù)類型可以是整型數(shù)據(jù)類型,也可以是浮點數(shù)據(jù)類型,也可以是指針,也可以是結(jié)構(gòu)體。因此在函數(shù)傳遞的規(guī)則上需要明確指出調(diào)用者應(yīng)該如何將參數(shù)進行保存處理,而被調(diào)用者又是從什么地方來獲取這些外部傳遞進來的參數(shù)值。不同體系下的系統(tǒng)會根據(jù)參數(shù)定義的個數(shù)和類型來制定不同的規(guī)則。一般情況下各系統(tǒng)都會約定一些特定的寄存器來進行參數(shù)傳遞交換,或者使用棧內(nèi)存來進行參數(shù)傳遞交換。
1. x86_64體系下的參數(shù)傳遞規(guī)則
1.1 常規(guī)類型參數(shù)
這里面的常規(guī)類型參數(shù)是指除浮點和結(jié)構(gòu)體類型以外的參數(shù)類型,下面就是常規(guī)參數(shù)傳遞的規(guī)則:
R1: 如果函數(shù)沒有參數(shù)則除了進行執(zhí)行函數(shù)調(diào)用外不做任何處理,如果函數(shù)有參數(shù)則在執(zhí)行函數(shù)調(diào)用指令之前需要按下面的規(guī)則設(shè)置參數(shù)值。
R2: 如果函數(shù)的參數(shù)個數(shù)<=6,則參數(shù)傳遞時將按照從左往右的定義的順序依次保存到RDI, RSI, RDX, RCX, R8, R9這6個寄存器中。
R3: 如果參數(shù)的個數(shù)>6, 那么超過6個的參數(shù),將會按從右往左的順序依次壓入到棧中。(因為棧是從高地址往低地址遞減的,所以從棧頂往上來算的話后面的參數(shù)依然是從左到右的順序)
R4: 如果每個參數(shù)的類型的尺寸<8個字節(jié)的情況下,則前6個參數(shù)會分別保存在上述寄存器的對應(yīng)的32位或者16位或者8位版本的寄存器中。
下面是幾個函數(shù)的定義以及在執(zhí)行這個函數(shù)調(diào)用和參數(shù)傳遞的實現(xiàn)規(guī)則(下面代碼塊中上面部分描述的函數(shù)接口,下面部分是函數(shù)調(diào)用ABI規(guī)則):
//函數(shù)的簽名 void foo1(long, long); void foo2(long, long, long, long, long, long); void foo3(long, long, long, long, long, long, long, int, short); //高級語言的函數(shù)調(diào)用以及對應(yīng)的機器指令偽代碼實現(xiàn) foo1(a,b) <==> RDI = a, RSI = b, call foo1 foo2(a,b,c,d,e,f) <==> RDI = a, RSI = b, RDX = c, RCX = d, R8 = e, R9 = f, call foo2 foo3(a,b,c,d,e,f,g,h,i) <== > RDI = a, RSI = b, RDX = c, RCX = d, R8 = e, R9 = f, RSP -= 2, *RSP = i, RSP-=4, *RSP = h, RSP-=8, *RSP = g, call foo3
1.2 浮點類型參數(shù)
如果函數(shù)參數(shù)中有浮點數(shù)(無論是單精度還是雙精度)類型。則參數(shù)保存的地方則不是通用寄存器,而是特定的浮點數(shù)寄存器。下面就是傳遞的規(guī)則:
R5: 如果浮點數(shù)參數(shù)的個數(shù)<=8,那么參數(shù)傳遞將按從左往右的定義順序依次保存到 XMM0 - XMM7這8個寄存器中。
R6: 如果浮點數(shù)參數(shù)個數(shù)>8,那么超過數(shù)量部分的參數(shù),將會按從右往左的順序依次壓入到棧中。
R7: 如果函數(shù)參數(shù)中既有浮點也有常規(guī)參數(shù)那么保存到寄存器中的順序和規(guī)則不會相互影響。
R8: 如果參數(shù)類型是擴展浮點類型(long double),擴展浮點類型的長度是16個字節(jié), 那么所有的long double類型的參數(shù)都將直接壓入到棧(注意這個棧不是浮點寄存器棧)中而不存放到浮點寄存器中。
下面是幾個函數(shù)的例子:
//函數(shù)簽名
void foo4(double, double);
void foo5(double, float, double, double, double, double, double, double, float, double);
void foo6(long, double, long, double, long, long, double);
void foo7(double, long double, long);
//高級語言的函數(shù)調(diào)用以及對應(yīng)的機器指令偽代碼實現(xiàn)
foo4(a,b) <==> XMM0 = a, XMM1 = b, call foo4
foo5(a,b,c,d,e,f,g,h,i,j) <==> XMM0 = a, XMM1 = b, XMM2 = c, XMM3 = d, XMM4 = e, XMM5 = f, XMM6 = g, XMM7 = h, RSP-=8, *RSP = j, RSP-=4 *RSP = i, call foo5
foo6(a,b,c,d,e,f,g) <==> RDI = a, XMM0 = b, RSI = c, XMM1 = d, RDX = e, RCX = f, XMM2 = g, call foo6
foo7(a,b,c) <==> XMM0=a, RSP-=16, *RSP = b的低8字節(jié), *(RSP+8) = b的高8字節(jié), RDI = c, call foo7
1.3 結(jié)構(gòu)體參數(shù)
針對結(jié)構(gòu)體類型的參數(shù),需要考慮結(jié)構(gòu)體中的成員的數(shù)據(jù)類型以及結(jié)構(gòu)體的尺寸兩個因素。這里的結(jié)構(gòu)體的尺寸分為:小于等于8字節(jié)、小于等于16字節(jié)、大于16字節(jié)三種。而結(jié)構(gòu)體成員類型組成則分為:全部都是常規(guī)數(shù)據(jù)類型、全部都是浮點數(shù)據(jù)類型(不包括long double)、以及混合類型三種。這樣一共分為9種組合情況,下面表格描述結(jié)構(gòu)體參數(shù)的的傳遞規(guī)則:
R9:
類型/尺寸 | <=8 | <=16 | >16 |
---|---|---|---|
全部都是常規(guī)數(shù)據(jù)類型 | 6個通用寄存器中的某一個 | 6個通用寄存器中的某連續(xù)兩個 | 壓入棧內(nèi)存中 |
全部都是浮點數(shù)據(jù)類型 | 8個浮點寄存器中的某一個 | 8個浮點寄存器中的某連續(xù)兩個 | 壓入棧內(nèi)存中 |
混合類型 | 優(yōu)先考慮通用寄存器,再考慮浮點寄存器,以及成員排列的順序 | 參考左邊 | 壓入棧內(nèi)存中 |
R10: 小于等于16個字節(jié)的結(jié)構(gòu)體保存到寄存器中的規(guī)則并不是按每個數(shù)據(jù)成員來分別保存到寄存器,而是按結(jié)構(gòu)體中的內(nèi)存布局邊界順序以8字節(jié)為分割單位來保存到寄存器中的。
R11: 如果參數(shù)中混合有結(jié)構(gòu)體、常規(guī)參數(shù)、浮點參數(shù)則按照前10個規(guī)則分別保存?zhèn)鬟f的參數(shù)
下面就是幾個結(jié)構(gòu)體在當做參數(shù)時的示例代碼:
//長度<=8個字節(jié)的結(jié)構(gòu)體
struct S1
{
char a;
char b;
int c;
};
//長度<=16的混合結(jié)構(gòu)體
struct S2
{
float a;
float b;
double c;
};
//長度<=16的混合結(jié)構(gòu)體
struct S3
{
int a;
int b;
double c;
};
//長度>16個字節(jié)的結(jié)構(gòu)體
struct S4
{
long a;
long b;
double c;
}
//函數(shù)簽名
void foo8(struct S1);
void foo9(struct S2);
void foo10(struct S3);
void foo11(struct S4);
//高級語言的函數(shù)調(diào)用以及對應(yīng)的機器指令偽代碼實現(xiàn)
struct S1 s1;
struct S2 s2;
struct S3 s3;
struct S4 s4;
foo8(s1) <==> RDI = s1.a | (s1.b <<8) | (s1.c << 32), call foo8
foo9(s2) <==> XMM0 = s2.a | (s2.b << 32), XMM1 = s2.c, call foo9
foo10(s3) <==> RDI = s3.a | (s3.b << 32), XMM0 = s3.c, call foo10
foo11(s4) <==> RSP -= 24, *RSP = s4.a, *(RSP+8) = s4.b, *(RSP+16)=s4.c, call foo11
針對結(jié)構(gòu)體類型的參數(shù)建議是傳指針而不是傳結(jié)構(gòu)體值本身。
1.4 可變參數(shù)
可變參數(shù)函數(shù)因為其參數(shù)的類型和參數(shù)的數(shù)量不固定,所以系統(tǒng)在編譯時會根據(jù)函數(shù)調(diào)用時傳遞的參數(shù)的值類型而進行不同的處理,因此規(guī)則如下:
R12: 函數(shù)調(diào)用時會根據(jù)傳遞的參數(shù)的數(shù)量和類型從左到右依次存放在對應(yīng)的6個常規(guī)參數(shù)傳遞的寄存器或者XMM0-XMM7中,如果數(shù)量超過規(guī)定則剩余的參數(shù)依次壓入棧內(nèi)存中。
R13:對于可變參數(shù)函數(shù)的調(diào)用會使用AL寄存器,其規(guī)則為:如果傳遞的可變參數(shù)中沒有浮點數(shù)類型則AL寄存器被設(shè)置為0,如果可變參數(shù)中出現(xiàn)了浮點數(shù)類型則AL寄存器會被設(shè)置為1。之所以用AL寄存器來標志的原因是可變參數(shù)內(nèi)部實現(xiàn)因為不知道外部會傳遞什么類型的參數(shù)以及參數(shù)的個數(shù),所以內(nèi)部實現(xiàn)中會將所有作為參數(shù)傳遞的常規(guī)寄存器和作為參數(shù)傳遞的浮點數(shù)寄存器都會保存到一個數(shù)組中去,以方便進行處理。因此這里借助這個AL寄存器來判斷是否有浮點就可以在一定程度上減少將數(shù)組的長度。
下面是可變參數(shù)的調(diào)用示例:
//函數(shù)簽名 void foo12(int a, ...); //高級語言的函數(shù)調(diào)用以及對應(yīng)的機器指令偽代碼實現(xiàn) foo12(10,20,30.0, 40) <==> RDI = 10, RSI = 20, XMM0 = 30.0, RDX = 40,AL=1, call foo12 foo12(10,20,30,40) <==> RDI = 10, RSI = 20, RDX = 30, RCX = 40,AL=0, call foo7
一個有意思的例子: 當調(diào)用printf函數(shù)傳遞的參數(shù)如下:
printf("%f,%d,%d", 10, 20.0, 30.0); //輸出的結(jié)果將是: 20.0,10, );
原因就是參數(shù)傳遞的規(guī)則和格式字符串不匹配導致的,通過上面對可變參數(shù)的傳遞規(guī)則,你能解釋為什么嗎?
2. arm32位體系下的參數(shù)傳遞規(guī)則
整個arm32位體系下的參數(shù)傳遞和參數(shù)返回都不會用到浮點寄存器。對于大于4字節(jié)的基本類型則會拆分為兩部分依次保存到連續(xù)的兩個寄存器中。
2.1 常規(guī)參數(shù)
R1: 對于32位的常規(guī)參數(shù),如果數(shù)量<=4則分別保存到 R0 - R3中, 如果數(shù)量>4則剩余的參數(shù)從右往左分別壓入棧內(nèi)存中。
R2: 如果參數(shù)中有64位的參數(shù)比如long long 類型,則參數(shù)會占用2個寄存器,其中低32位部分保存在前一個寄存器,高32位部分保存在后一個寄存器。
R3: 如果前面3個參數(shù)是32位的參數(shù),而第四個參數(shù)是64位的參數(shù),那么前面三個參數(shù)分別放入R0,R1,R2中,而第四個參數(shù)的低32位部分則放入R3中,高32位部分則壓入到棧內(nèi)存中。
2.2 浮點參數(shù)
R4: 浮點參數(shù)和常規(guī)參數(shù)一樣使用R0到R3寄存器,對于單精度浮點則使用一個寄存器,而雙精度浮點則使用兩個寄存器。超出部分則壓入棧內(nèi)存中。
2.3 結(jié)構(gòu)體參數(shù)
R5: arm32位系統(tǒng)的結(jié)構(gòu)體不區(qū)分成員數(shù)據(jù)類型,只區(qū)分結(jié)構(gòu)體尺寸,系統(tǒng)根據(jù)結(jié)構(gòu)體的內(nèi)存布局以4個字節(jié)為分割單位保存到寄存器或者棧內(nèi)存中。
R6: 結(jié)構(gòu)體尺寸<=4則會將參數(shù)保存到一個寄存器中,如果尺寸<=8則保存到連續(xù)的兩個寄存器中, 如果尺寸<=12則保存到3個連續(xù)的寄存器中, 如果尺寸<=16則保存到4個連續(xù)的寄存器中。如果尺寸>16則保存到棧內(nèi)存中去。
R7: 如果前3個參數(shù)都是32位的參數(shù),而第4個參數(shù)為尺寸>4的結(jié)構(gòu)體,那么第4個參數(shù)的低4個字節(jié)的部分會保存到R3中,其他部分保存到棧內(nèi)存中。
2.4 可變參數(shù)
R8: 可變參數(shù)傳遞根據(jù)參數(shù)的個數(shù)從左到右依次保存到R0-R3四個寄存器中,超過的部分從右往左依次保存到棧內(nèi)存中。 下面的實例代碼:
//函數(shù)簽名 void foo1(int a, ...); //高級語言的函數(shù)調(diào)用以及對應(yīng)的機器指令偽代碼實現(xiàn)。 foo1(10,20,30,40,50) <==> R0 = 10, R1 = 20, R2 = 30, R3 =40, SP -=4, *SP = 50, bl foo1
3.arm64位體系下的參數(shù)傳遞規(guī)則
3.1 常規(guī)參數(shù)
這里面的常規(guī)參數(shù)是指參數(shù)的類型是非浮點和非結(jié)構(gòu)體類型的參數(shù),下面就是常規(guī)參數(shù)傳遞的規(guī)則:
R1: 如果函數(shù)沒有參數(shù)則除了進行執(zhí)行函數(shù)調(diào)用外不做任何處理,如果函數(shù)有參數(shù)則在執(zhí)行函數(shù)調(diào)用指令之前需要按下面的規(guī)則設(shè)置參數(shù)值。
R2: 如果函數(shù)的參數(shù)個數(shù)<=8個, 參數(shù)傳遞將按照從左往右的定義的順序依次保存到X0 - X7 這8個寄存器中。
R3: 如果參數(shù)的個數(shù)>8個,那么超過數(shù)量部分的參數(shù),將會按從右往左的順序依次壓入到棧中。
R4: 如果參數(shù)的類型是小于8個字節(jié)的情況下,則前8個參數(shù)會分別保存在對應(yīng)的32位或者16位或者8位寄存器中。
下面是幾個函數(shù)的例子:
//函數(shù)簽名 void foo1(long, long); void foo2(long, long, long, long, long, long, long, long); void foo3(long, long, long, long, long, long, long, long, long, int, short); //高級語言的函數(shù)調(diào)用以及對應(yīng)的機器指令偽代碼實現(xiàn)。 foo1(a,b) <==> X0 = a, X1 = b, bl foo1 foo2(a,b,c,d,e,f,g,h) <==>X0 = a, X1 = b, X2 = c, X3 = d, X4 = e, X5 = f, X6=g, X7 =h, bl foo2 foo3(a,b,c,d,e,f,g,h,i,j,k) <==>X0 = a, X1 = b, X2 = c, X3 = d, X4 = e, X5 = f, X6=g, X7=h, *SP -=2, *SP=k, SP-=4, *SP = j, SP-= 8, *SP = i, bl foo3
3.2 浮點參數(shù)
如果函數(shù)參數(shù)中有浮點數(shù)(無論是單精度還是雙精度)。則參數(shù)保存的地方則不是通用寄存器,而是特定的浮點數(shù)寄存器。系統(tǒng)提供32個128位的浮點寄存器Q0-Q31(V0-V31),其中的低64位則被稱為D0-D31,其中的低32位則被稱為S0-S31,其中的低16位則被稱為H0-H31,其中的低8位則被稱之為B0-B31。 也就是說單精度浮點保存到S開頭的寄存器, 雙精度浮點保存到D開頭的寄存器。 arm系統(tǒng)中 long double 的長度都是8字節(jié),因此可被當做雙精度浮點。
下面就是傳遞的規(guī)則:
R5: 如果浮點數(shù)參數(shù)的個數(shù)<=8個,那么參數(shù)傳遞將按從左往右的順序依次保存到 D0-D7或者S0-S7 這8個寄存器中。
R6: 如果浮點數(shù)參數(shù)個數(shù)>8個時,那么超過數(shù)量部分的參數(shù),將會按從右往左的順序依次壓入到棧中。
R7: 如果函數(shù)參數(shù)中既有浮點也有常規(guī)參數(shù)那么保存到寄存器中的順序和規(guī)則不會相互影響。
下面是幾個函數(shù)的例子:
//函數(shù)簽名
void foo4(double, double);
void foo5(double, float, float, double, double, double, double, double, double, double);
void foo6(long, double, long, double, long, long, double);
//高級語言的函數(shù)調(diào)用以及對應(yīng)的機器指令偽代碼實現(xiàn)。
foo4(double a, double b) <==> D0 = a, D1 = b, bl foo4
foo5(double a, float b, float c, double d, double e, double f, double g, double h, double i, double j) <==> D0 = a, S1 = b, S2 = c, D3 = d, D4 = e, D5 = f, D6 = g, D7 = h, *SP -=8, *SP = j, *SP -=8, *SP = i, bl foo5
foo6(long a, double b, long c, double d, long e, long f, double g) <==> X0 = a, D0 = b, X1 = c, D1 = d, X2 = e, X3 = f, D2 = g, bl foo6
3.3 結(jié)構(gòu)體參數(shù)
針對結(jié)構(gòu)體類型的參數(shù),需要考慮結(jié)構(gòu)體的尺寸以及數(shù)據(jù)類型和數(shù)量。這里的結(jié)構(gòu)體的尺寸分別是考慮小于等于8字節(jié),小于等于16字節(jié),大于16字節(jié)。而結(jié)構(gòu)體成員類型則分為:全部都是非浮點數(shù)據(jù)成員、全部都是浮點數(shù)成員(這里會區(qū)分單精度和雙精度)、以及混合類型的成員(如果結(jié)構(gòu)體中有單精度和雙精度都算混合)。下面是針對結(jié)構(gòu)體參數(shù)的規(guī)則:
R8: 如果數(shù)據(jù)成員全部都是非浮點數(shù)據(jù)成員則 如果尺寸<=8則會將值保存到X0-X8中的某一個寄存器中, 如果尺寸<=16則會將值保存到X0-X8中的某兩個連續(xù)的寄存器中,如果尺寸>16則結(jié)構(gòu)體將不再按值傳遞而是以指針的形式進行傳遞并保存到X0-X8中的某一個寄存器中。
R9: 如果數(shù)據(jù)成員全部都是單精度浮點成員則如果成員數(shù)量<=4則會將數(shù)據(jù)成員保存到S0-S7中的某4個連續(xù)的浮點寄存器中,如果數(shù)量>4則結(jié)構(gòu)體將不再按值傳遞而是以指針的形式進行傳遞并保存到X0-X8中的某一個寄存器中。
R10: 如果數(shù)據(jù)成員全部都是雙精度浮點成員則如果成員數(shù)量<=4則會將數(shù)據(jù)成員保存到D0-D7中的某4個連續(xù)的浮點寄存器中,如果數(shù)量>4則結(jié)構(gòu)體將不再按值傳遞而是以指針的形式進行傳遞并保存到X0-X8中的某一個寄存器中。
R11: 如果數(shù)據(jù)成員是混合類型的則如果尺寸<=8則保存到X0-X8中的某一個寄存器中,如果尺寸<=16則保存到X0-X8中的某兩個連續(xù)的寄存器中, 如果尺寸>16則結(jié)構(gòu)體將不再按值傳遞而是以指針的形式進行傳遞并保存到X0-X8中的某一個寄存器中。
R12: 因為結(jié)構(gòu)體參數(shù)的寄存器規(guī)則會影響到上述非結(jié)構(gòu)體參數(shù)的傳遞規(guī)則,因此一定程度上可以將結(jié)構(gòu)體當做多個參數(shù)傳遞來看待。
下面是演示的代碼:
//長度<=8個字節(jié)的結(jié)構(gòu)體
struct S1
{
char a;
char b;
int c;
};
//長度<=16的單精度浮點結(jié)構(gòu)體
struct S2
{
float a;
float b;
float c;
};
//長度<=16的混合結(jié)構(gòu)體
struct S3
{
int a;
int b;
double c;
};
//長度>16個字節(jié)的結(jié)構(gòu)體
struct S4
{
long a;
long b;
double c;
}
//函數(shù)簽名
void foo8(struct S1);
void foo9(struct S2);
void foo10(struct S3);
void foo11(struct S4);
//高級語言的函數(shù)調(diào)用以及對應(yīng)的機器指令偽代碼實現(xiàn)
struct S1 s1;
struct S2 s2;
struct S3 s3;
struct S4 s4;
foo8(s1) <==> X0= s1.a | (s1.b <<8) | (s1.c << 32), bl foo8
foo9(s2) <==> S0 = s2.a, S1 = s2.b, S3 = s2.c bl foo9
foo10(s3) <==> X0 = s3.a | (s3.b << 32), X1 = s3.c, bl foo10
foo11(s4) <==> X0 = &s4, bl foo11
3.4 可變參數(shù)
可變參數(shù)函數(shù)因為其參數(shù)的類型和參數(shù)的數(shù)量不固定,所以系統(tǒng)在編譯時會根據(jù)函數(shù)調(diào)用時傳遞的參數(shù)的值類型而進行不同的處理,因此規(guī)則如下:
R13: 函數(shù)調(diào)用時會根據(jù)傳遞的參數(shù)的數(shù)量和類型來決定,其中明確類型的部分按照上面介紹的規(guī)則進行傳遞,而可變部分則從右往左依次壓入到堆棧中。
下面是示例代碼:
//函數(shù)簽名 void foo7(int a, ...); //高級語言的函數(shù)調(diào)用以及對應(yīng)的機器指令偽代碼實現(xiàn) foo7(10, 20, 30.0, 40) <==> X0 = 10, SP-=8, *SP = 40, SP-=8, *SP = 30.0, SP-=8, *SP = 20, bl foo7
一個有意思的例子: 當執(zhí)行printf函數(shù)而傳遞參數(shù)如下:
printf("%f,%d,%d", 10, 20.0, 30.0); //那么輸出的結(jié)果將是: );
因為arm系統(tǒng)對可變參數(shù)的傳遞和x86系統(tǒng)對可變參數(shù)的處理不一致,就會出現(xiàn)真機和模擬器的結(jié)果不一致的問題。甚至在參數(shù)傳遞規(guī)則上arm32位和arm64位系統(tǒng)都有差異。上面的參數(shù)傳遞和描述不匹配的情況下你可以說出為什么輸出的結(jié)果不確定嗎?
四、函數(shù)返回值
函數(shù)調(diào)用除了有參數(shù)傳遞外,還有參數(shù)返回。參數(shù)的傳遞是調(diào)用者向被調(diào)函數(shù)方向的傳遞,而函數(shù)的返回則是被調(diào)用函數(shù)向調(diào)用函數(shù)方向的傳遞,因此調(diào)用者和被調(diào)用者之間應(yīng)該形成統(tǒng)一的規(guī)則。被調(diào)用函數(shù)內(nèi)對返回值的處理應(yīng)該在被調(diào)用函數(shù)返回指令執(zhí)行前。而調(diào)用函數(shù)則應(yīng)該在函數(shù)調(diào)用指令的下一條指令中盡可能早的對返回的結(jié)果進行處理。函數(shù)的返回類型有無、非浮點數(shù)、浮點數(shù)、結(jié)構(gòu)體四種類型,因此針對不同的返回類型系統(tǒng)有不同的處理規(guī)則。
1. x86_64體系下的函數(shù)返回值規(guī)則
1.1 常規(guī)類型返回
R1: 如果函數(shù)有返回值則總是將返回值保存到RAX寄存器中。
1.2 浮點類型返回
R2: 返回的浮點數(shù)類型保存到XMM0寄存器中。
R3: 返回的(擴展雙精度)long double 類型則保存到浮點寄存器棧頂中。FPU計算單元中提供了8個獨立的128位的寄存器STMM0-STMM7,這8個寄存器以堆棧形式組織在一起,統(tǒng)稱為浮點寄存器棧。系統(tǒng)同時也提供了專門的指令來對浮點寄存器棧進行入棧和出棧處理, 編寫浮點指令時這些寄存器也寫作st(x),這里的x是浮點寄存器的索引。需要明確的是XMM系列的寄存器和STMM系列的寄存器是完全不同的兩套寄存器。
1.3 結(jié)構(gòu)體類型返回
針對結(jié)構(gòu)體類型的返回,需要考慮結(jié)構(gòu)體的尺寸以及成員的數(shù)據(jù)類型。這里的結(jié)構(gòu)體的尺寸分為:小于等于8字節(jié),小于等于16字節(jié),大于16字節(jié)。而結(jié)構(gòu)體成員類型則分為:全部都是非浮點數(shù)據(jù)成員、全部都是浮點數(shù)據(jù)成員(不包括 long double)、以及混合類型的成員。這樣一共分為9種情況,下面表格描述針對結(jié)構(gòu)體返回的規(guī)則:
R4
類型/尺寸 | <=8 | <=16 | >16 |
---|---|---|---|
全部非浮點數(shù)據(jù)成員 | RAX | RAX,RDX | 返回的結(jié)構(gòu)體將保存到RDI寄存器所指向的內(nèi)存地址中。也就是RDI寄存器是一個結(jié)構(gòu)體地址指針,這樣函數(shù)參數(shù)中的第一個參數(shù)將由保存到RDI,變?yōu)楸4娴絉SI寄存器了。 |
全部為浮點數(shù)據(jù)成員 | XMM0 | XMM0,XMM1 | 同上 |
混合類型 | 優(yōu)先存放到RAX,或者XMM0,然后再存放到RDX或者XMM1中。一個特殊情況就是如果成員中有l(wèi)ong double類型,則總是按>16字節(jié)的規(guī)則來處理返回值 | 同左 | 同上 |
下面是一個展示的代碼:
//長度<=8個字節(jié)的結(jié)構(gòu)體 struct S1 { char a; char b; int c; }; //長度<=16的混合結(jié)構(gòu)體 struct S2 { int a; int b; double c; }; //長度>16個字節(jié)的結(jié)構(gòu)體 struct S3 { long a; long b; double c; } //函數(shù)簽名 struct S1 foo1(); struct S2 foo2(); struct S3 foo3(int ); //高級語言的函數(shù)調(diào)用以及對應(yīng)的機器指令偽代碼實現(xiàn) struct S1 s1 = foo1() <==> 函數(shù)調(diào)用時:call foo1, 函數(shù)返回時 s1 = RAX struct S2 s2 = foo2() <==> 函數(shù)調(diào)用時:call foo2, 函數(shù)返回時s2.a&s2.b = RAX, s2.c = XMM0 struct S3 s3 = foo3(a) <==> 函數(shù)調(diào)用時: RDI = &s3, RSI = a, call foo3
2. arm32位體系下的函數(shù)返回值規(guī)則
2.1 常規(guī)類型返回
R1: 函數(shù)的返回值的尺寸<=4字節(jié)則保存到R0寄存器,如果返回值的尺寸<=8字節(jié)(比如 long long類型)則保存到R0,R1寄存器其中低32位保存到R0,高32位保存到R1
2.2 浮點類型返回
R2: 單精度浮點數(shù)保存到R0寄存器,雙精度浮點數(shù)保存在R0,R1中其中R0保存低32位,R1保存高32位。 long double 類型的返回同雙精度浮點返回一致。
2.3 結(jié)構(gòu)體類型返回
R3: 不管任何類型的結(jié)構(gòu)體,總是將結(jié)構(gòu)體返回到R0寄存器所指向的內(nèi)存中, 因此R0寄存器中保存的是一個指針,這樣函數(shù)的第一個參數(shù)將保存到R1寄存器并依次往后推,也就是說如果函數(shù)返回的是一個結(jié)構(gòu)體那么系統(tǒng)就會將返回的值當做第一個參數(shù),而將真實的第一個參數(shù)當做第二個參數(shù)。
下面的代碼說明了這種情況:
struct XXX { //任意內(nèi)容 }; //函數(shù)返回結(jié)構(gòu)體 struct XXX foo(int a) { //... } 實際在編譯時會轉(zhuǎn)化為函數(shù) void foo(struct XXX *pret, int a) { }
也就是在arm32位的系統(tǒng)中凡是有結(jié)構(gòu)體作為返回的函數(shù),其實都會將結(jié)構(gòu)體指針作為函數(shù)調(diào)用的第一個參數(shù)保存到R0中,而將源代碼中的第一個參數(shù)保存到R1中。
3.arm64位體系下的函數(shù)返回值規(guī)則
2.1 常規(guī)類型返回
R1: 函數(shù)的返回參數(shù)保存到X0寄存器上
2.2 浮點類型返回
R2: 單精度浮點返回保存到S0,雙精度浮點返回保存到D0
2.3 結(jié)構(gòu)體類型返回
針對結(jié)構(gòu)體類型的參數(shù),需要考慮結(jié)構(gòu)體中的成員的數(shù)據(jù)類型以及整體結(jié)構(gòu)體的尺寸。這里的結(jié)構(gòu)體的尺寸分別是考慮小于等于8字節(jié),小于等于16字節(jié),大于16字節(jié)。而結(jié)構(gòu)體成員類型則分為:全部都是非浮點數(shù)據(jù)成員、全部都是浮點數(shù)成員(這里會區(qū)分單精度和雙精度)、以及混合類型的成員(如果結(jié)構(gòu)體中有單精度和雙精度都算混合)。這樣一共分為9種情,下面就是針對結(jié)構(gòu)體類型返回的規(guī)則:
R3:針對非浮點數(shù)據(jù)成員的結(jié)構(gòu)體來說如果結(jié)構(gòu)體的尺寸<=8,那么結(jié)構(gòu)體的值會保存到X0, 如果尺寸<=16,那么保存到X0,X1中,如果尺寸>16則結(jié)構(gòu)體返回會保存到X8寄存器所指向的內(nèi)存中,也就是X8寄存器比較特殊,專門用來保存返回的結(jié)構(gòu)體的指針。
R4: 如果結(jié)構(gòu)體的成員都是單精度并且數(shù)量<=4 則返回結(jié)構(gòu)體的每個成員分別保存到S0,S1,S2, S3四個寄存中,如果結(jié)構(gòu)體成員數(shù)量超過4個則結(jié)構(gòu)體返回會保存到X8寄存器所指向的內(nèi)存中。
R5: 如果結(jié)構(gòu)體的成員都是雙精度并且數(shù)量<=4 則返回結(jié)構(gòu)體的每個成員分別保存到D0,D1,D2,D3四個寄存器中,如果結(jié)構(gòu)體成員數(shù)量超過4個則結(jié)構(gòu)體返回會保存到X8寄存器所指向的內(nèi)存中。
R6: 如果結(jié)構(gòu)體是混合型數(shù)據(jù)成員,并且結(jié)構(gòu)體的尺寸<=8字節(jié),那么結(jié)構(gòu)體的值保存到X0, 如果尺寸<=16字節(jié)則保存到X0,X1中,如果尺寸>16則結(jié)構(gòu)體返回會保存到X8寄存器所指向的內(nèi)存中。
下面演示幾個結(jié)構(gòu)體定義以及返回結(jié)構(gòu)體的函數(shù):
//長度為16字節(jié)的結(jié)構(gòu)體
struct S1
{
char a;
char b;
double c;
};
//長度超過16字節(jié)的混合成員結(jié)構(gòu)體
struct S2
{
int a;
int b;
int c;
double d;
};
//長度小于等于8字節(jié)的結(jié)構(gòu)體
struct S3
{
int a;
int b;
};
CGRect foo1()
{
//高級語言實現(xiàn)的返回
return CGRectMake(10,20,30,40);
//機器指令的函數(shù)返回的偽代碼如下:
/*
D0 = 10
D1 = 20
D2 = 30
D3 = 40
ret
*/
}
struct S1 foo2()
{
//高級語言實現(xiàn)的返回
return (struct S1){10, 20, 30};
//機器指令的函數(shù)返回的偽代碼如下:
/*
X0 = 10 | 20 << 8
X1 = 30
ret
*/
}
struct S2 foo3()
{
//高級語言實現(xiàn)的返回
return (struct S2){10, 20, 30, 40};
//機器指令的函數(shù)返回的偽代碼如下:
/*
struct S2 *p = X8 //X8中保存返回的結(jié)構(gòu)體內(nèi)存地址
p->a = 10
p->b = 20
p->c = 30
p->d = 40
ret
*/
}
struct S3 foo4()
{
//高級語言實現(xiàn)的返回
return (struct S3){20, 30};
//機器指令的函數(shù)返回的偽代碼如下:
/*
X0 = 20 | 30 << 32
ret
*/
}
從上面的代碼可以看出來在x86_64/arm32兩種體系結(jié)構(gòu)下如果返回的類型是結(jié)構(gòu)體并且滿足特定要求時,系統(tǒng)會將結(jié)構(gòu)體指針當做函數(shù)的第一個參數(shù),而將源代碼中的第一個參數(shù)傳遞的寄存器往后移動,而在arm64位系統(tǒng)中則x8寄存器專門負責處理返回值為特殊結(jié)構(gòu)體的情況。
六、談?wù)刼bjc_msgSend系列函數(shù)
所有的OC方法最終都會通過objc_msgSend系列函數(shù)進行調(diào)用。這個函數(shù)系列有如下函數(shù):
objc_msgSend(void /* id self, SEL op, ... */ ) objc_msgSend_stret(void /* id self, SEL op, ... */ ) objc_msgSend_fpret(void /* id self, SEL op, ... */ ) objc_msgSend_fp2ret(void /* id self, SEL op, ... */ )
這一系列的函數(shù)的差別主要是針對返回類型的不同而使用不同的消息發(fā)送函數(shù)。
從上述的函數(shù)返回值規(guī)則可以看對于long double 類型的函數(shù)返回在x86_64位系統(tǒng)的處理方式比較特殊,其返回的值將保存在特定的浮點堆棧寄存器中,所以objc_msgSend_fpret函數(shù)只用在x86_64位系統(tǒng)中返回類型為long double的OC方法的消息分發(fā)中,其他體系結(jié)構(gòu)都不會用到這個函數(shù)。同樣因為C99中引入了復(fù)數(shù)類型 _Complex 關(guān)鍵字,所以針對這種類型的 long double 返回會使用objc_msgSend_fp2ret函數(shù)。
從上述的函數(shù)的返回值規(guī)則還可以看出對于結(jié)構(gòu)體返回,如果結(jié)構(gòu)體尺寸大于一定的閾值后,x86_64位系統(tǒng)和arm32位系統(tǒng)都會將返回的結(jié)構(gòu)體轉(zhuǎn)化為第一個參數(shù)來進行傳遞,這樣就會使得真實的參數(shù)傳遞的寄存器往后順延,而arm64則直接只用x8寄存器來保存大于閾值的結(jié)構(gòu)體指針且并不會影響到參數(shù)的傳遞順序。因此除了arm64位系統(tǒng)外其他體系結(jié)構(gòu)系統(tǒng)中針對那些返回結(jié)構(gòu)體大于一定閾值的OC方法將使用objc_msgSend_stret函數(shù)進行消息分發(fā)。
上述的函數(shù)返回規(guī)則對
針對函數(shù)的調(diào)用、參數(shù)傳遞、函數(shù)的返回值的介紹規(guī)則就是這些了,當然這些規(guī)則除了對普通函數(shù)適用外對OC類方法也是同樣適用的。至于一個函數(shù)內(nèi)部應(yīng)該怎樣實現(xiàn),其實也是有一定的規(guī)則的。通過這些規(guī)則你可以了解到函數(shù)是如何跟棧內(nèi)存結(jié)合在一起的,以及函數(shù)調(diào)用棧是如何被構(gòu)造出來的,你還可以了解為什么一些函數(shù)調(diào)用不會出現(xiàn)在調(diào)用棧中等等相關(guān)的知識,以及可變參數(shù)函數(shù)內(nèi)部是如何實現(xiàn)的等等這部分的詳細介紹將會在: 深入iOS系統(tǒng)底層之函數(shù)(二):實現(xiàn) 進行深入的探討。
七、參考
blog.csdn.net/q_l_s/artic…
developer.apple.com/library/arc…
armv8,armv7, x86_64位系統(tǒng)CPU手冊
blog.sina.com.cn/s/blog_8619…
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/7070.html
摘要:序一直想寫一些關(guān)于系統(tǒng)底層方面的知識點,并且醞釀了很久,后來也跟其他人交流,你為何不出一個系列呢不必要一次性把所有的東西都寫完后才發(fā)表,我聽說后覺得非常的有道理,雖然自己的水平也很一般,但是想想自己還是有一些積累的。序 一直想寫一些關(guān)于系統(tǒng)底層方面的知識點,并且醞釀了很久,后來也跟其他人交流,你為何不出一個系列呢? 不必要一次性把所有的東西都寫完后才發(fā)表,我聽說后覺得非常的有道理,雖然自己的...
閱讀 713·2023-04-25 19:43
閱讀 3910·2021-11-30 14:52
閱讀 3784·2021-11-30 14:52
閱讀 3852·2021-11-29 11:00
閱讀 3783·2021-11-29 11:00
閱讀 3869·2021-11-29 11:00
閱讀 3558·2021-11-29 11:00
閱讀 6105·2021-11-29 11:00