從彙編看函數

一、簡介

CPU
中央處理器,內部主要包括寄存器、運算器、控制器。

  • 寄存器:存儲數據
  • 運算器:處理數據
  • 控制器:控制硬件IO口的高低電平。

常用寄存器

  • pc:程序計數器,確定指令位置
  • sp:在任意時刻都會保存棧頂的地址,調用函數就會開闢棧空間(通過操作sp寄存器來開闢棧空間)
  • fp:也稱爲x29寄存器屬於通用寄存器,在某些時刻利用它來保存棧底的地址

x30寄存器

  • x30寄存器存放當前調用函數的返回地址
  • ret指令執行時,會找到x30寄存器保存的地址值,繼續向下執行

常用指令

  • str:讀取寄存器值,存入內存中
  • ldr:讀內存中的值,存入到寄存器
  • stp:入棧指令 stp x0, x1, [sp] 存入兩個值
  • ldp:出棧指令 ldp x0, x1, [sp] 取出兩個值
  • bl:將下一條指令的地址放入lr(x30)寄存器,跳轉到標號處執行指令
  • ret:默認使用lr(x30)寄存器的值,通過底層指令提示CPU此處作爲下條指令的地址
  • orrorr{條件}{S} 目的寄存器,操作數1,操作數2,把結果放置到目的寄存器

函數參數和返回值
ARM64下,函數存放在x0~x7(w0~w7)8個寄存器中,超過8個參數,就會入棧。函數返回值是放在x0寄存器中的。通用寄存器32個。

二、彙編函數嵌套

1、demo1-彙編函數嵌套

.text
.global _A, B
_A:
    mov x0, #0xaaaa
    bl _B
    mov x0,#0xaaaa
    ret
_B:
    mov x0, #0xbbbb
    ret

執行順序:

demo`A:
    0x102c0a0c4 <+0>:  mov    x0, #0xaaaa
    0x102c0a0c8 <+4>:  bl     0x102c0a0d4               ; B	①
    0x102c0a0cc <+8>:  mov    x0, #0xaaaa->  0x102c0a0d0 <+12>: ret    

繼續執行,①和②會來回執行,①->②->①->…。
原因:在A函數中調用了B函數,這裏x30的值將被置爲B函數的結束地址,繼續執行到retret會讀取了x30的地址(B函數的結束地址)①處,繼續往下執行到②,因此就①->②->①->…。

下面看系統是如何處理嵌套函數的調用的:

2、demo2-c函數嵌套

int A(void);
void b() {
    return;
}
void c() {
    b();
}
int main(int argc, char * argv[]) {
    c();
//    A();
}

斷點單步執行打印如下:

demo`c:
    0x102f9a318 <+0>:  stp    x29, x30, [sp, #-0x10]!
    0x102f9a31c <+4>:  mov    x29, sp
    0x102f9a320 <+8>:  bl     0x102f9a314               ; b at main.m:14:5
    0x102f9a324 <+12>: ldp    x29, x30, [sp], #0x10
->  0x102f9a328 <+16>: ret    
  • stp:寫入,向x29、x30寫入到棧空間
  • stp x29, x30, [sp, #-0x10]!:等價於sp = sp-0x10(16字節)並賦值所在地址,拉伸棧空間,拉伸棧空間的大小爲16字節的倍數
  • 執行c函數
  • ldp x29, x30, [sp], #0x10:將用sp所在地址值給x29、x30賦值,sp+0x10釋放空間,保持棧平衡

在每一步打印x30的值:

asm1.png

從上面的運行結果可以看出,x30寄存器在調起內嵌函數前,存儲x30地址到 [sp, #-0x10]的地址中,內嵌函數調用完成後,重新設置當前x30 = spsp存儲了當前函數的地址),執行到retret讀取到的地址即當前函數的結束地址,繼續執行則跳出該函數。

3、demo3-完善demo1
在函數內調用函數,保存當前函數A結束地址x30sp-0x10(16個字節)位置,函數B結束後重新設置x30的值爲sp(函數A的結束地址),這樣就完成嵌套函數調用。

.text
.global _A, B
_A:
    mov x0, #0xaaaa
    str x30,[sp, #-0x10]!
    bl _B
    mov x0,#0xaaaa
    ldr x30, [sp], #0x10
    ret
_B:
    mov x0, #0xbbbb
    ret

如下:

asm2.png

三、函數

上面瞭解了彙編函數嵌套的處理方法,下面看一下在彙編層對參數是怎麼處理的。

int sum(int a, int b) {
    return a+b;
}

int main(int argc, char * argv[]) {
    Int res = sum(5,7);
}

斷點查看主函數彙編代碼:

main.png

  • sub sp, sp, #0x30sp-0x30申請48個字節的棧空間(sp指向可用棧空間的棧頂),sub減指令
  • x29、x30保存棧底棧頂,做爲嵌套函數的中間變量
  • 上面可以看到變量值#0x5、#0x7,存入到w0、w1寄存器中

進入sum函數內查看,彙編指令:

sum.png

  • sum函數內拉伸棧空間
  • str指令將w0、w1寄存器中的值入棧,再取出,有說法是爲防止寄存器被使用,存儲值發生變化,使用前讀取棧區的值就不會出現被串改的問題,優化後的指令是直接走add sp, sp, #0x10的。但是既然在連續執行的指令中都有被串改的可能,那麼在取值後,add前也是有可能被串改的,所以感覺以上說法並不能解釋這一多餘操作,除非後面指令中,有使用該參數值,存儲到棧是有必要的
  • sp, sp, #0x10:數據處理完成回收棧空間
  • ret:有參數函數返回值是x0寄存器的值不是x30寄存器的值,w0x0寄存器的低32位,因此x0=w0ret=w0=0x12=12

編譯器優化:

fast.png

優化後的彙編指令:

fast2.png

  • 優化掉了參數的存儲,取值,直接將寄存器值相加
  • 沒有拉伸棧空間

掉了兩根頭髮!!!

多參數demo

int sum(int a,int b,int c,int d,int e,int f,int g,int h,int i,int j,int k,int l) {
    return a+b+b+c+d+e+f+g+h+i+j+k+l;
}

int main(int argc, char * argv[]) {
    int res = sum(5,7);
}

main函數彙編指令如下:

more.png

初始化寄存器的值,這裏使用w0~w8、x9,這裏w0=x0,w9=x9,不用糾結爲什麼沒有都使用wxwx的低32位,同屬於一個寄存器,在系統級別怎麼用都行。過!

進入函數內部:

add.png

拉伸棧空間,存寄存器值,取值,相加,指令太多,每一條指令耗時1/主頻,複合指令耗時2/主頻,這麼多指令,太燒了。

局部變量

demo1-函數多參數

int funcC() {
    int a = 1;
    int b = 2;
    int c = 3;
    return a+b+c;
}

int main(int argc, char * argv[]) {
    int res = funcC();
}

函數彙編指令如下:

func.png

  • 開闢棧空間0x10
  • 將值存入到w8寄存器中(任意w)
  • 將寄存器值入棧,出棧,計算

再看一段代碼:

int funcC() {
    return 1+2+3;
}

int main(int argc, char * argv[]) {
    int res = funcC();
}

彙編指令:

less.png

這裏就執行了一條指令,其實內部有做add相關指令,這裏做了優化,但相比上面聲明的局部變量,這裏沒有開闢棧空間,省去了很多指令,每一條指令耗時1/主頻,複合指令耗時2/主頻,每條指令都要放電一次,耗電,局部變量悠着點用,當然真正開發中編譯器是會優化掉這些多餘代碼。

……
……

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章