深刻iOS系統底層之函數調用

古器合尺度,法物應矩規。--蘇洵html

1、什麼是函數

可執行程序是爲了實現某個功能而由不一樣機器指令按特定規則進行組合排列的集合。不管高級仍是低級程序語言,不管是面向對象仍是面向過程的語言最終的代碼都會轉化爲一條條機器指令的形式被執行。爲了管理上的方便和對代碼的複用,每每須要將某一段實現特定功能的指令集合進行抽離和處理從而造成了函數的概念,函數也能夠稱之爲子程序或者子例程。出現函數的概念後可執行程序的機器指令集合將再也不是單一的一塊代碼,而是由多個函數組成的分塊代碼,這樣可執行程序就變成了由函數之間相互調用這種方式來構建和組織了。git

一個函數由函數簽名、參數、返回、實現四部分組成。函數的前三者定義了明確的邊界信息,也稱之爲函數接口描述。函數接口描述的意義在於調用者再也不須要了解被調用者函數的實現細節,而只須要按被調用者的定義的接口進行交互便可。如何去定義一個函數,如何去實現一個函數,如何去調用一個函數,如何將參數傳遞給被調用的函數,如何使用被調用者函數的返回這些都須要有統一的標準規範來進行界定,這個規則有兩個層面的標準:在高級語言層面的規則稱之爲API規則;而在機器指令層面上則因爲不一樣的操做系統以及不一樣的CPU體系結構下提供的指令集和構造程序的方式不一樣而不一樣,因此在系統層面的規則稱之爲ABI規則。本文的重點是詳細介紹函數調用、函數參數傳遞、函數返回值這3個方面的ABI規則,經過對這些規則的詳細介紹相信您對什麼是函數就會有更加深刻的瞭解。須要注意的是這裏的ABI規則是指基於OC語言實現的程序的ABI規則,這些規則並不適用於經過Swift實現的程序以及不適用於Linux等其餘操做系統的ABI規則。github

因爲內容過多所以我將分爲兩篇文章來作具體介紹,前一篇文章介紹函數接口相關的內容,後一篇文章介紹函數實現相關的內容。數組

2、函數調用

CPU中的程序計數器(IP/PC)中老是保存着下一條將要執行的指令的內存地址,這樣每執行一條指令就會更新程序計數器中的值,從而能夠繼續執行下一條指令。系統就是這樣經過不停的變化程序計數器中的值來實現程序指令的執行的。通常狀況下程序計數器中的值老是按照程序指令順序更新,只有在執行跳轉指令和函數調用指令時纔會打破執行的順序。bash

函數調用的本質就是將函數在內存中的首地址賦值給程序計數器(IP/PC),這樣下一條執行的指令就變爲了函數首地址處的指令,從而實現函數的調用。除了要更新程序計數器的值外還須要保存調用現場,以便當函數調用返回後繼續執行函數調用的下一條指令,因此這裏所謂的保存調用現場就是將函數調用的下一條指令的地址保存起來。不一樣的CPU體系都提供了特定的函數調用指令來實現函數調用的功能。好比x86系統提供一條稱之爲call的指令來實現函數調用,call指令除了會更新程序計數器的值外還會把函數調用的下一條指令壓入到棧中進行保存;arm系統則提供b系列的指令來實現函數調用,b系列指令除了會更新程序計數器的值外還會把函數調用的下一條指令保存到LR寄存器中。app

函數返回的本質就是將前面說到的保存的調用現場地址賦值給程序計數器,這樣下一條執行的指令就變爲了調用者調用被調函數的下一條指令了。不一樣的CPU體系也都提供了特定的函數返回指令來實現函數返回的功能(arm32位系統除外)。好比x86系統提供一條稱之爲ret的指令來實現函數返回,此指令會將棧頂保存的地址賦值給程序計數器而後執行出棧操做;arm64位系統也提供一條ret指令來實現函數的返回,此指令則會把當前的LR寄存器的值賦值給程序計數器。函數

對於x86系統來講由於執行函數調用前會將調用者的下一條指令壓入棧中,而被調用者函數內部由於有本地棧幀(stack frame)的定義又會將棧頂下移,因此在被調用者函數執行ret指令返回以前須要確保當前堆棧寄存器SP所指向的棧頂地址要和被調用函數執行前的棧頂地址保持一致,否則當ret指令執行時取出的調用者的下一條指令的值將是錯誤的,從而會產生崩潰異常。佈局

對於arm系統來講由於LR寄存器只有一個,所以若是被調用函數內部也調用其餘函數時也會更新LR寄存器的值,一旦LR寄存器被更新後將沒法恢復正確的調用現場,因此通常狀況下被調用函數的前幾條指令作的事情就是將LR寄存器的值保存到棧內存中,而被調用函數的最後幾條指令所的事情就是將棧內存中保存的內容恢復到LR寄存器。post

有一種特殊的函數調用場景就是當函數調用發生在調用者函數的最後一條指令時,則不須要進行調用現場的保護處理,同時也會將函數調用指令改成跳轉指令,緣由是由於調用者的最後一條指令再無下一條有效的指令,而仍然採用調用指令的話則保存的調用現場則是個無效的地址,這樣當函數返回時將跳轉到這個無效的地址從而產生執行異常!ui

爲了更好的描述函數的調用規則,假設A函數內部調用了B函數和C函數,下面定義了各函數的地址,以及函數調用處的地址,以及函數調用的僞代碼塊:

//這裏的XX,YY,ZZ表明的是函數指令在內存中的地址。
A  XX1:   
    XX2:   調用B函數地址YY1
    XX3:
    XX4:
    XXn:  跳轉到C函數ZZ1
    
B  YY1:
    YY2:
    YY3:
    YYn:  返回
    
C  ZZ1:
    ZZ2:
    ZZ3:
    ZZn:  返回
複製代碼

1. x86_64體系下的函數調用規則

1.1 函數的調用

函數調用的指令是call 指令。在彙編語言中call 指令後面的操做數是調用的目標函數的絕對地址,而實際的機器指令中的操做數則是一個相對地址值,這個地址值是目標函數地址距離當前指令地址的相對偏移值。不管是x86系統仍是arm系統若是指令中的操做數部分的值是內存地址的話,通常都是相對當前指令的偏移地址而不是絕對地址。下面就是函數調用指令以及其內部實現的等價操做。

call YY1   <==>   RIP = YY1,   RSP = RSP-8,  *RSP = XX3
複製代碼

也就是說執行一條函數調用指令等價於將指令中的地址賦值給IP寄存器,同時把函數的返回地址壓入棧寄存器中去。

1.2 函數的跳轉

函數跳轉的指令是jmp指令。在彙編語言中jmp 指令後面的操做數是調用的目標函數的絕對地址,而實際的機器指令中的操做數則是一個相對地址值,這個地址值是目標函數地址距離當前指令地址的相對偏移值,下面就是函數跳轉指令以及其內部實現的等價操做。

jmp ZZ1  <==>  RIP = ZZ1
複製代碼

也就是說執行一條跳轉指令等價於將指令中的地址賦值給IP寄存器。

1.3 函數的返回

函數返回的指令是ret指令。ret指令後面通常不跟操做數,下面就是函數返回指令以及其內部實現的等價操做。

ret   <==>   RIP = *RSP,   RSP = RSP + 8
複製代碼

也就是說執行一條ret指令等價於將當前棧寄存器中的值賦值給IP寄存器,同時棧寄存器執行POP操做。

2. arm32位體系下的函數調用規則

2.1 函數的調用

函數的調用指令爲bl/blx。 這兩條指令的操做數能夠是相對地址偏移也能夠是寄存器。bl/blx的區別就是bl函數調用不會切換指令集,而blx調用則會從thumb指令集切換到arm指令集或者相反切換。arm32系統中存在着兩套指令集即thumb指令集和arm指令集,其中的arm指令集中的全部的指令的長度都是32位而thumb指令集則存在着32位和16位兩種長度的指令集。兩種指令集是以函數爲單位進行使用的,也就是說一個函數中的全部指令要麼都是arm指令要麼就都是thumb指令。正是由於如此若是調用者函數和被調用者函數之間用的是不一樣的指令集則須要經過blx來執行函數調用,而若是兩者所用的指令集相同則須要經過bl指令來執行調用。下面就是函數調用指令以及其內部實現的等價操做。

bl/blx  YY1  <==>  PC = YY1,  LR = XX3
複製代碼

也就是說執行一條函數調用指令等價於將指令中的地址賦值給PC寄存器,同時把函數的返回地址賦值給LR寄存器中去。

2.2 函數的跳轉

函數的跳轉指令是b/bx, 這兩條指令的操做數能夠是相對地址偏移也能夠是寄存器,b/bx的區別就是b函數調用不會切換指令集。下面就是函數跳轉指令以及其內部實現的等價操做。

b/bx ZZ1   <==>  PC = ZZ1
複製代碼

也就是說跳轉指令等價於將指令中的地址賦值給PC寄存器。

2.3 函數的返回

arm32位系統沒有專門的函數返回ret指令,由於arm32位系統能夠直接修改PC寄存器的值,因此函數返回能夠直接給PC指令賦值,也能夠經過調用b/bx LR 來實現函數的返回處理。

b/bx LR
//或者
mov PC, XXX
複製代碼

arm32位系統能夠直接修改PC寄存器的值,所以函數返回時能夠直接設置PC寄存器的值爲函數的返回地址,也能夠執行b/bx跳轉指令並指定目標地址爲LR寄存器中的值。

3.arm64位體系下的函數調用規則

3.1 函數的調用

函數調用的指令是bl/blr 其中bl指令的操做數是距離當前位置相對距離的偏移地址,blr指令的操做數則是寄存器,代表調用寄存器所指定的地址。由於bl指令中的操做數部分是函數的相對偏移地址,又由於arm64位系統的一條指令佔用4個字節,根據指令的定義bl指令所能跳轉的範圍是距離當前位置±32MB的範圍,因此若是要跳轉到更遠的地址則須要藉助blr指令。 下面就是函數調用指令以及其內部實現的等價操做。

//若是YY1地址離調用指令的距離是在±32MB內則使用bl指令便可。
 bl YY1 <==>  PC = YY1, LR = XX3

//若是YY1地址離調用指令的距離超過±32MB則使用blr指令執行間接調用。
ldr  x16,  YY1
blr  x16
複製代碼

也就是說執行一條函數調用指令等價於將指令中的地址賦值給PC寄存器,同時把函數的返回地址賦值給LR寄存器中去。

3.2函數的跳轉

函數跳轉的指令是b/br, 其中b指令的操做數是距離當前位置相對距離的偏移地址,br指令的操做數則是寄存器,代表跳轉到寄存器所指定的地址中去。下面就是函數跳轉指令以及其內部實現的等價操做。

b ZZ1   <==>  PC = ZZ1
複製代碼

也就是說跳轉指令等價於將指令中的地址賦值給PC寄存器。

3.3 函數的返回

函數返回的指令是 ret, 下面就是函數返回指令以及其內部實現的等價操做。

ret  <==>   PC = LR
複製代碼

也就是說執行一條ret指令等價於將LR寄存器中的值賦值給PC寄存器。

3、函數參數傳遞

某些函數定義中有參數須要傳遞,須要由調用者函數將參數傳遞給被調用者函數,所以在調用這類函數時,須要在執行函數調用指令以前,進行函數參數的傳遞。函數的參數個數能夠爲0個,也能夠爲某個固定的數量,也能夠爲任意數量(可變參數)。 函數的每一個參數類型能夠是整型數據類型,也能夠是浮點數據類型,也能夠是指針,也能夠是結構體。所以在函數傳遞的規則上須要明確指出調用者應該如何將參數進行保存處理,而被調用者又是從什麼地方來獲取這些外部傳遞進來的參數值。不一樣體系下的系統會根據參數定義的個數和類型來制定不一樣的規則。通常狀況下各系統都會約定一些特定的寄存器來進行參數傳遞交換,或者使用棧內存來進行參數傳遞交換。

1. x86_64體系下的參數傳遞規則

1.1 常規類型參數

這裏面的常規類型參數是指除浮點和結構體類型之外的參數類型,下面就是常規參數傳遞的規則:

  • R1: 若是函數沒有參數則除了進行執行函數調用外不作任何處理,若是函數有參數則在執行函數調用指令以前須要按下面的規則設置參數值。

  • R2: 若是函數的參數個數<=6,則參數傳遞時將按照從左往右的定義的順序依次保存到RDI, RSI, RDX, RCX, R8, R9這6個寄存器中。

  • R3: 若是參數的個數>6, 那麼超過6個的參數,將會按從右往左的順序依次壓入到棧中。(由於棧是從高地址往低地址遞減的,因此從棧頂往上來算的話後面的參數依然是從左到右的順序)

  • R4: 若是每一個參數的類型的尺寸<8個字節的狀況下,則前6個參數會分別保存在上述寄存器的對應的32位或者16位或者8位版本的寄存器中。

下面是幾個函數的定義以及在執行這個函數調用和參數傳遞的實現規則(下面代碼塊中上面部分描述的函數接口,下面部分是函數調用ABI規則):

//函數的簽名
void foo1(long, long);
void foo2(long, long, long, long, long, long);
void foo3(long, long, long, long, long, long, long, int, short);

//高級語言的函數調用以及對應的機器指令僞代碼實現
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 浮點類型參數

若是函數參數中有浮點數(不管是單精度仍是雙精度)類型。則參數保存的地方則不是通用寄存器,而是特定的浮點數寄存器。下面就是傳遞的規則:

  • R5: 若是浮點數參數的個數<=8,那麼參數傳遞將按從左往右的定義順序依次保存到 XMM0 - XMM7這8個寄存器中。

  • R6: 若是浮點數參數個數>8,那麼超過數量部分的參數,將會按從右往左的順序依次壓入到棧中。

  • R7: 若是函數參數中既有浮點也有常規參數那麼保存到寄存器中的順序和規則不會相互影響。

  • R8: 若是參數類型是擴展浮點類型(long double),擴展浮點類型的長度是16個字節, 那麼全部的long double類型的參數都將直接壓入到棧(注意這個棧不是浮點寄存器棧)中而不存放到浮點寄存器中。

下面是幾個函數的例子:

//函數簽名
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);

//高級語言的函數調用以及對應的機器指令僞代碼實現
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字節, *(RSP+8) = b的高8字節, RDI = c,  call foo7

複製代碼

1.3 結構體參數

針對結構體類型的參數,須要考慮結構體中的成員的數據類型以及結構體的尺寸兩個因素。這裏的結構體的尺寸分爲:小於等於8字節、小於等於16字節、大於16字節三種。而結構體成員類型組成則分爲:所有都是常規數據類型、所有都是浮點數據類型(不包括long double)、以及混合類型三種。這樣一共分爲9種組合狀況,下面表格描述結構體參數的的傳遞規則:

  • R9:
類型/尺寸 <=8 <=16 >16
所有都是常規數據類型 6個通用寄存器中的某一個 6個通用寄存器中的某連續兩個 壓入棧內存中
所有都是浮點數據類型 8個浮點寄存器中的某一個 8個浮點寄存器中的某連續兩個 壓入棧內存中
混合類型 優先考慮通用寄存器,再考慮浮點寄存器,以及成員排列的順序 參考左邊 壓入棧內存中
  • R10: 小於等於16個字節的結構體保存到寄存器中的規則並非按每一個數據成員來分別保存到寄存器,而是按結構體中的內存佈局邊界順序以8字節爲分割單位來保存到寄存器中的。

  • R11: 若是參數中混合有結構體、常規參數、浮點參數則按照前10個規則分別保存傳遞的參數

下面就是幾個結構體在當作參數時的示例代碼:

//長度<=8個字節的結構體
struct S1
{
    char a;
    char b;
    int c;
};

//長度<=16的混合結構體
struct S2
{
   float a;
   float b;
   double c;
};

//長度<=16的混合結構體
struct S3
{
  int a;
  int b;
  double c;
};

//長度>16個字節的結構體
struct S4
{
   long a;
   long b;
   double c;
}

 //函數簽名
 void foo8(struct S1);
 void foo9(struct S2);
 void foo10(struct S3);
 void foo11(struct S4);


//高級語言的函數調用以及對應的機器指令僞代碼實現
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

複製代碼

針對結構體類型的參數建議是傳指針而不是傳結構體值自己。

1.4 可變參數

可變參數函數由於其參數的類型和參數的數量不固定,因此係統在編譯時會根據函數調用時傳遞的參數的值類型而進行不一樣的處理,所以規則以下:

  • R12: 函數調用時會根據傳遞的參數的數量和類型從左到右依次存放在對應的6個常規參數傳遞的寄存器或者XMM0-XMM7中,若是數量超過規定則剩餘的參數依次壓入棧內存中。

  • R13:對於可變參數函數的調用會使用AL寄存器,其規則爲:若是傳遞的可變參數中沒有浮點數類型則AL寄存器被設置爲0,若是可變參數中出現了浮點數類型則AL寄存器會被設置爲1。之因此用AL寄存器來標誌的緣由是可變參數內部實現由於不知道外部會傳遞什麼類型的參數以及參數的個數,因此內部實現中會將全部做爲參數傳遞的常規寄存器和做爲參數傳遞的浮點數寄存器都會保存到一個數組中去,以方便進行處理。所以這裏藉助這個AL寄存器來判斷是否有浮點就能夠在必定程度上減小將數組的長度。

    下面是可變參數的調用示例:

//函數簽名
void foo12(int a, ...);

//高級語言的函數調用以及對應的機器指令僞代碼實現
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
複製代碼

一個有意思的例子: 當調用printf函數傳遞的參數以下:

printf("%f,%d,%d", 10, 20.0, 30.0);       //輸出的結果將是: 20.0,10, ???  
複製代碼

緣由就是參數傳遞的規則和格式字符串不匹配致使的,經過上面對可變參數的傳遞規則,你能解釋爲何嗎?

2. arm32位體系下的參數傳遞規則

整個arm32位體系下的參數傳遞和參數返回都不會用到浮點寄存器。對於大於4字節的基本類型則會拆分爲兩部分依次保存到連續的兩個寄存器中。

2.1 常規參數

  • R1: 對於32位的常規參數,若是數量<=4則分別保存到 R0 - R3中, 若是數量>4則剩餘的參數從右往左分別壓入棧內存中。

  • R2: 若是參數中有64位的參數好比long long 類型,則參數會佔用2個寄存器,其中低32位部分保存在前一個寄存器,高32位部分保存在後一個寄存器。

  • R3: 若是前面3個參數是32位的參數,而第四個參數是64位的參數,那麼前面三個參數分別放入R0,R1,R2中,而第四個參數的低32位部分則放入R3中,高32位部分則壓入到棧內存中。

2.2 浮點參數

  • R4: 浮點參數和常規參數同樣使用R0到R3寄存器,對於單精度浮點則使用一個寄存器,而雙精度浮點則使用兩個寄存器。超出部分則壓入棧內存中。

2.3 結構體參數

  • R5: arm32位系統的結構體不區分紅員數據類型,只區分結構體尺寸,系統根據結構體的內存佈局以4個字節爲分割單位保存到寄存器或者棧內存中。

  • R6: 結構體尺寸<=4則會將參數保存到一個寄存器中,若是尺寸<=8則保存到連續的兩個寄存器中, 若是尺寸<=12則保存到3個連續的寄存器中, 若是尺寸<=16則保存到4個連續的寄存器中。若是尺寸>16則保存到棧內存中去。

  • R7: 若是前3個參數都是32位的參數,而第4個參數爲尺寸>4的結構體,那麼第4個參數的低4個字節的部分會保存到R3中,其餘部分保存到棧內存中。

2.4 可變參數

  • R8: 可變參數傳遞根據參數的個數從左到右依次保存到R0-R3四個寄存器中,超過的部分從右往左依次保存到棧內存中。 下面的實例代碼:
//函數簽名
void foo1(int a, ...);

//高級語言的函數調用以及對應的機器指令僞代碼實現。
foo1(10,20,30,40,50)  <==> R0 = 10,  R1 = 20, R2 = 30, R3 =40,  SP -=4,  *SP = 50,  bl foo1

複製代碼

3.arm64位體系下的參數傳遞規則

3.1 常規參數

這裏面的常規參數是指參數的類型是非浮點和非結構體類型的參數,下面就是常規參數傳遞的規則:

  • R1: 若是函數沒有參數則除了進行執行函數調用外不作任何處理,若是函數有參數則在執行函數調用指令以前須要按下面的規則設置參數值。

  • R2: 若是函數的參數個數<=8個, 參數傳遞將按照從左往右的定義的順序依次保存到X0 - X7 這8個寄存器中。

  • R3: 若是參數的個數>8個,那麼超過數量部分的參數,將會按從右往左的順序依次壓入到棧中。

  • R4: 若是參數的類型是小於8個字節的狀況下,則前8個參數會分別保存在對應的32位或者16位或者8位寄存器中。

下面是幾個函數的例子:

//函數簽名
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);


//高級語言的函數調用以及對應的機器指令僞代碼實現。
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 浮點參數

若是函數參數中有浮點數(不管是單精度仍是雙精度)。則參數保存的地方則不是通用寄存器,而是特定的浮點數寄存器。系統提供32個128位的浮點寄存器Q0-Q31(V0-V31),其中的低64位則被稱爲D0-D31,其中的低32位則被稱爲S0-S31,其中的低16位則被稱爲H0-H31,其中的低8位則被稱之爲B0-B31。 也就是說單精度浮點保存到S開頭的寄存器, 雙精度浮點保存到D開頭的寄存器。 arm系統中 long double 的長度都是8字節,所以可被當作雙精度浮點。

下面就是傳遞的規則:

  • R5: 若是浮點數參數的個數<=8個,那麼參數傳遞將按從左往右的順序依次保存到 D0-D7或者S0-S7 這8個寄存器中。

  • R6: 若是浮點數參數個數>8個時,那麼超過數量部分的參數,將會按從右往左的順序依次壓入到棧中。

  • R7: 若是函數參數中既有浮點也有常規參數那麼保存到寄存器中的順序和規則不會相互影響。

下面是幾個函數的例子:

//函數簽名
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);

//高級語言的函數調用以及對應的機器指令僞代碼實現。
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 結構體參數

針對結構體類型的參數,須要考慮結構體的尺寸以及數據類型和數量。這裏的結構體的尺寸分別是考慮小於等於8字節,小於等於16字節,大於16字節。而結構體成員類型則分爲:所有都是非浮點數據成員、所有都是浮點數成員(這裏會區分單精度和雙精度)、以及混合類型的成員(若是結構體中有單精度和雙精度都算混合)。下面是針對結構體參數的規則:

  • R8: 若是數據成員所有都是非浮點數據成員則 若是尺寸<=8則會將值保存到X0-X8中的某一個寄存器中, 若是尺寸<=16則會將值保存到X0-X8中的某兩個連續的寄存器中,若是尺寸>16則結構體將再也不按值傳遞而是以指針的形式進行傳遞並保存到X0-X8中的某一個寄存器中。

  • R9: 若是數據成員所有都是單精度浮點成員則若是成員數量<=4則會將數據成員保存到S0-S7中的某4個連續的浮點寄存器中,若是數量>4則結構體將再也不按值傳遞而是以指針的形式進行傳遞並保存到X0-X8中的某一個寄存器中。

  • R10: 若是數據成員所有都是雙精度浮點成員則若是成員數量<=4則會將數據成員保存到D0-D7中的某4個連續的浮點寄存器中,若是數量>4則結構體將再也不按值傳遞而是以指針的形式進行傳遞並保存到X0-X8中的某一個寄存器中。

  • R11: 若是數據成員是混合類型的則若是尺寸<=8則保存到X0-X8中的某一個寄存器中,若是尺寸<=16則保存到X0-X8中的某兩個連續的寄存器中, 若是尺寸>16則結構體將再也不按值傳遞而是以指針的形式進行傳遞並保存到X0-X8中的某一個寄存器中。

  • R12: 由於結構體參數的寄存器規則會影響到上述非結構體參數的傳遞規則,所以必定程度上能夠將結構體當作多個參數傳遞來看待。

下面是演示的代碼:

//長度<=8個字節的結構體
struct S1
{
    char a;
    char b;
    int c;
};

//長度<=16的單精度浮點結構體
struct S2
{
   float a;
   float b;
   float c;
};

//長度<=16的混合結構體
struct S3
{
  int a;
  int b;
  double c;
};

//長度>16個字節的結構體
struct S4
{
   long a;
   long b;
   double c;
}

 //函數簽名
 void foo8(struct S1);
 void foo9(struct S2);
 void foo10(struct S3);
 void foo11(struct S4);


//高級語言的函數調用以及對應的機器指令僞代碼實現
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 可變參數

可變參數函數由於其參數的類型和參數的數量不固定,因此係統在編譯時會根據函數調用時傳遞的參數的值類型而進行不一樣的處理,所以規則以下:

  • R13: 函數調用時會根據傳遞的參數的數量和類型來決定,其中明確類型的部分按照上面介紹的規則進行傳遞,而可變部分則從右往左依次壓入到堆棧中。

下面是示例代碼:

//函數簽名
void foo7(int a, ...);

//高級語言的函數調用以及對應的機器指令僞代碼實現
foo7(10, 20, 30.0, 40)  <==> X0 = 10,   SP-=8,  *SP = 40,  SP-=8,  *SP = 30.0,  SP-=8,  *SP = 20, bl foo7

複製代碼

一個有意思的例子: 當執行printf函數而傳遞參數以下:

printf("%f,%d,%d", 10, 20.0, 30.0);     //那麼輸出的結果將是: ?,?,?     
複製代碼

由於arm系統對可變參數的傳遞和x86系統對可變參數的處理不一致,就會出現真機和模擬器的結果不一致的問題。甚至在參數傳遞規則上arm32位和arm64位系統都有差別。上面的參數傳遞和描述不匹配的狀況下你能夠說出爲何輸出的結果不肯定嗎?

4、函數返回值

函數調用除了有參數傳遞外,還有參數返回。參數的傳遞是調用者向被調函數方向的傳遞,而函數的返回則是被調用函數向調用函數方向的傳遞,所以調用者和被調用者之間應該造成統一的規則。被調用函數內對返回值的處理應該在被調用函數返回指令執行前。而調用函數則應該在函數調用指令的下一條指令中儘量早的對返回的結果進行處理。函數的返回類型有無、非浮點數、浮點數、結構體四種類型,所以針對不一樣的返回類型系統有不一樣的處理規則。

1. x86_64體系下的函數返回值規則

1.1 常規類型返回

  • R1: 若是函數有返回值則老是將返回值保存到RAX寄存器中。

1.2 浮點類型返回

  • R2: 返回的浮點數類型保存到XMM0寄存器中。

  • R3: 返回的(擴展雙精度)long double 類型則保存到浮點寄存器棧頂中。FPU計算單元中提供了8個獨立的128位的寄存器STMM0-STMM7,這8個寄存器以堆棧形式組織在一塊兒,統稱爲浮點寄存器棧。系統同時也提供了專門的指令來對浮點寄存器棧進行入棧和出棧處理, 編寫浮點指令時這些寄存器也寫做st(x),這裏的x是浮點寄存器的索引。須要明確的是XMM系列的寄存器和STMM系列的寄存器是徹底不一樣的兩套寄存器。

1.3 結構體類型返回

針對結構體類型的返回,須要考慮結構體的尺寸以及成員的數據類型。這裏的結構體的尺寸分爲:小於等於8字節,小於等於16字節,大於16字節。而結構體成員類型則分爲:所有都是非浮點數據成員、所有都是浮點數據成員(不包括 long double)、以及混合類型的成員。這樣一共分爲9種狀況,下面表格描述針對結構體返回的規則:

  • R4
類型/尺寸 <=8 <=16 >16
所有非浮點數據成員 RAX RAX,RDX 返回的結構體將保存到RDI寄存器所指向的內存地址中。也就是RDI寄存器是一個結構體地址指針,這樣函數參數中的第一個參數將由保存到RDI,變爲保存到RSI寄存器了。
所有爲浮點數據成員 XMM0 XMM0,XMM1 同上
混合類型 優先存放到RAX,或者XMM0,而後再存放到RDX或者XMM1中。一個特殊狀況就是若是成員中有long double類型,則老是按>16字節的規則來處理返回值 同左 同上

下面是一個展現的代碼:

//長度<=8個字節的結構體
struct S1
{
    char a;
    char b;
    int c;
};

//長度<=16的混合結構體
struct S2
{
  int a;
  int b;
  double c;
};

//長度>16個字節的結構體
struct S3
{
   long a;
   long b;
   double c;
}

 //函數簽名
 struct S1 foo1();
 struct S2 foo2();
 struct S3 foo3(int );

//高級語言的函數調用以及對應的機器指令僞代碼實現
struct S1 s1 = foo1()  <==>  函數調用時:call foo1,  函數返回時 s1 = RAX
struct S2 s2 = foo2() <==> 函數調用時:call foo2, 函數返回時s2.a&s2.b = RAX, s2.c = XMM0
struct S3 s3 = foo3(a)  <==> 函數調用時: RDI = &s3, RSI = a, call foo3


複製代碼

2. arm32位體系下的函數返回值規則

2.1 常規類型返回

  • R1: 函數的返回值的尺寸<=4字節則保存到R0寄存器,若是返回值的尺寸<=8字節(好比 long long類型)則保存到R0,R1寄存器其中低32位保存到R0,高32位保存到R1

2.2 浮點類型返回

  • R2: 單精度浮點數保存到R0寄存器,雙精度浮點數保存在R0,R1中其中R0保存低32位,R1保存高32位。 long double 類型的返回同雙精度浮點返回一致。

2.3 結構體類型返回

  • R3: 無論任何類型的結構體,老是將結構體返回到R0寄存器所指向的內存中, 所以R0寄存器中保存的是一個指針,這樣函數的第一個參數將保存到R1寄存器並依次日後推,也就是說若是函數返回的是一個結構體那麼系統就會將返回的值當作第一個參數,而將真實的第一個參數當作第二個參數。

下面的代碼說明了這種狀況:

struct XXX
{
  //任意內容
};

//函數返回結構體
struct XXX foo(int a)
{
   //...
}

實際在編譯時會轉化爲函數
void foo(struct XXX *pret, int a)
{
}
複製代碼

也就是在arm32位的系統中凡有結構體做爲返回的函數,其實都會將結構體指針做爲函數調用的第一個參數保存到R0中,而將源代碼中的第一個參數保存到R1中。

3.arm64位體系下的函數返回值規則

2.1 常規類型返回

  • R1: 函數的返回參數保存到X0寄存器上

2.2 浮點類型返回

  • R2: 單精度浮點返回保存到S0,雙精度浮點返回保存到D0

2.3 結構體類型返回

針對結構體類型的參數,須要考慮結構體中的成員的數據類型以及總體結構體的尺寸。這裏的結構體的尺寸分別是考慮小於等於8字節,小於等於16字節,大於16字節。而結構體成員類型則分爲:所有都是非浮點數據成員、所有都是浮點數成員(這裏會區分單精度和雙精度)、以及混合類型的成員(若是結構體中有單精度和雙精度都算混合)。這樣一共分爲9種情,下面就是針對結構體類型返回的規則:

  • R3:針對非浮點數據成員的結構體來講若是結構體的尺寸<=8,那麼結構體的值會保存到X0, 若是尺寸<=16,那麼保存到X0,X1中,若是尺寸>16則結構體返回會保存到X8寄存器所指向的內存中,也就是X8寄存器比較特殊,專門用來保存返回的結構體的指針。

  • R4: 若是結構體的成員都是單精度而且數量<=4 則返回結構體的每一個成員分別保存到S0,S1,S2, S3四個寄存中,若是結構體成員數量超過4個則結構體返回會保存到X8寄存器所指向的內存中。

  • R5: 若是結構體的成員都是雙精度而且數量<=4 則返回結構體的每一個成員分別保存到D0,D1,D2,D3四個寄存器中,若是結構體成員數量超過4個則結構體返回會保存到X8寄存器所指向的內存中。

  • R6: 若是結構體是混合型數據成員,而且結構體的尺寸<=8字節,那麼結構體的值保存到X0, 若是尺寸<=16字節則保存到X0,X1中,若是尺寸>16則結構體返回會保存到X8寄存器所指向的內存中。

下面演示幾個結構體定義以及返回結構體的函數:

//長度爲16字節的結構體
struct S1
{
   char a;
   char b;
   double c;
};

 //長度超過16字節的混合成員結構體
struct S2
{
   int a;
   int b;
   int c;
   double d;
};

//長度小於等於8字節的結構體
struct S3
{
   int a;
   int b;
};


CGRect  foo1()
{
      //高級語言實現的返回
      return CGRectMake(10,20,30,40);
     //機器指令的函數返回的僞代碼以下:
    /* 
      D0 = 10
      D1 = 20
      D2 = 30
      D3 = 40
     ret
     */
}

struct S1 foo2()
{
    //高級語言實現的返回
    return (struct S1){10, 20, 30};
   //機器指令的函數返回的僞代碼以下:
    /* 
      X0 = 10 |  20 << 8
      X1 = 30
     ret
     */

}

struct S2 foo3()
{
   //高級語言實現的返回
   return (struct S2){10, 20, 30, 40};
  //機器指令的函數返回的僞代碼以下:
  /*
     struct S2 *p = X8     //X8中保存返回的結構體內存地址
     p->a = 10
     p->b = 20
     p->c = 30
     p->d = 40
     ret
  */
  
}

struct S3 foo4()
{
   //高級語言實現的返回
   return (struct S3){20, 30};
  //機器指令的函數返回的僞代碼以下:
  /*
        X0 = 20 | 30 << 32
        ret
  */
}

複製代碼

從上面的代碼能夠看出來在x86_64/arm32兩種體系結構下若是返回的類型是結構體而且知足特定要求時,系統會將結構體指針當作函數的第一個參數,而將源代碼中的第一個參數傳遞的寄存器日後移動,而在arm64位系統中則x8寄存器專門負責處理返回值爲特殊結構體的狀況。

6、談談objc_msgSend系列函數

全部的OC方法最終都會經過objc_msgSend系列函數進行調用。這個函數系列有以下函數:

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, ... */ )
複製代碼

這一系列的函數的差異主要是針對返回類型的不一樣而使用不一樣的消息發送函數。

從上述的函數返回值規則能夠看對於long double 類型的函數返回在x86_64位系統的處理方式比較特殊,其返回的值將保存在特定的浮點堆棧寄存器中,因此objc_msgSend_fpret函數只用在x86_64位系統中返回類型爲long double的OC方法的消息分發中,其餘體系結構都不會用到這個函數。一樣由於C99中引入了複數類型 _Complex 關鍵字,因此針對這種類型的 long double 返回會使用objc_msgSend_fp2ret函數。

從上述的函數的返回值規則還能夠看出對於結構體返回,若是結構體尺寸大於必定的閾值後,x86_64位系統和arm32位系統都會將返回的結構體轉化爲第一個參數來進行傳遞,這樣就會使得真實的參數傳遞的寄存器日後順延,而arm64則直接只用x8寄存器來保存大於閾值的結構體指針且並不會影響到參數的傳遞順序。所以除了arm64位系統外其餘體系結構系統中針對那些返回結構體大於必定閾值的OC方法將使用objc_msgSend_stret函數進行消息分發。

上述的函數返回規則對<objc/message.h> 中的其餘函數也是一樣適用的。


針對函數的調用、參數傳遞、函數的返回值的介紹規則就是這些了,固然這些規則除了對普通函數適用外對OC類方法也是一樣適用的。至於一個函數內部應該怎樣實現,其實也是有必定的規則的。經過這些規則你能夠了解到函數是如何跟棧內存結合在一塊兒的,以及函數調用棧是如何被構造出來的,你還能夠了解爲何一些函數調用不會出如今調用棧中等等相關的知識,以及可變參數函數內部是如何實現的等等這部分的詳細介紹將會在: 深刻iOS系統底層之函數(二):實現 進行深刻的探討。

7、參考

👉【返回目錄


歡迎你們訪問個人github地址

相關文章
相關標籤/搜索