本文主要是講解函數的參數
、返回值
、局部變量
在匯編中是如何存儲,以及CPSR
標志寄存器
函數的參數和返回值
-
arm64下,函數的
參數
是存放在x0-x7(w0-w7)
這8個寄存器里面的,如果超過8個參數,就會入棧如果自定義函數時,
參數最好不要超過6個
(因為有兩個隱藏參數self,_cmd
)如果函數需要多個參數,可以傳入數組、結構體、指針等類型
-
函數的
返回值
放在x0寄存器
中- 如果返回值
大于8個
字節,就會利用內存傳遞
- 如果返回值
查看系統的參數匯編
下面通過系統中對函數的匯編來查看系統對參數、返回值是如何操作的
int sum(int a, int b){
return a + b;
}
- (void)viewDidLoad{
[super viewDidLoad];
sum(10, 20);
}
-
查看匯編,在跳轉到sum函數之前,已經將參數存入了w0、w1
系統的參數匯編-01 -
在sum函數中,讀取w0、w1,放入w8、w9。然后將相加后的結果放入w0(即返回值在w0寄存器)
系統的參數匯編-02
自己優化實現suma
運行發現,其結果與sum函數是一致的,結果都是30
<!--asm.s-->
.text
.global _suma
_suma:
add x0, x0, x1
ret
<!--調用-->
int suma;
- (void)viewDidLoad{
[super viewDidLoad];
suma(10, 20);
}
編譯器優化
來看以下代碼的匯編
int test(int a, int b, int c, int d, int e, int f, int g, int h, int i){
return a + b + c + d + e + f + g + h + i;
}
- (void)viewDidLoad {
[super viewDidLoad];
test(1, 2, 3, 4, 5, 6, 7, 8, 9);
}
-
在test函數斷住,查看匯編
未優化分析-01
以下是viewDidLoad
棧空間的存入分析過程
未優化分析-02
下圖是對匯編代碼中入棧過程的一個圖示
未優化分析-03 -
以下是
test
函數的匯編分析
未優化分析-04
編譯器優化
-
debug模式改成release模式,此時再來查看匯編代碼,發現沒有test函數,被優化掉了
優化分析-01 如果非要執行test,可以這樣寫
- (void)viewDidLoad {
[super viewDidLoad];
printf("%d", test(1, 2, 3, 4, 5, 6, 7, 8, 9));
}
匯編代碼如下,發現優化后的test函數在匯編中,其本質是一個數
,也就是test函數的返回值
。(相當于將printf("%d", test(1, 2, 3, 4, 5, 6, 7, 8, 9));
直接優化成了printf("%d", 45);
)
通過匯編實現函數
- 定義函數聲明及調用
int funcA(int a, int b);
- (void)viewDidLoad {
[super viewDidLoad];
int a = funcA(10, 20);
printf("%d", a);
}
- 匯編實現
funcA
.text
.global _funcA, _sum
_funcA:
sub sp, sp, #0x10
stp x29, x30, [sp] //保護lr
bl _sum
ldp x29, x30, [sp]
add sp, sp, #0x10
ret
_sum:
add x0, x0, x1
ret
<!--簡寫-->
_funcA:
stp x29, x30, [sp, #-0x10]!
bl _sum
ldp x29, x30, [sp], #0x10
ret
_sum:
add x0, x0, x1
ret
<!--巧合:還可以將bl替換成b-->
//b就是簡單跳轉,在逆向中用于繞過某些代碼(例如安全監測)
_funcA:
b _sum
_sum:
add x0, x0, x1
ret
運行結果如下所示
說明:
- 關于b指令:只是跳轉,不改變lr寄存器
- 拉伸棧空間和參數個數有沒有關系?:有關系,當參數越多時,如果寄存器放不下,就需要用到內存。就會將棧空間放大,影響棧空間的不僅僅是參數個數,還有局部變量
函數的返回值
如果返回值是一個結構體類型,一個寄存器放不下,這時是什么情況?
有以下代碼,運行查看其匯編
struct str {
int a;
int b;
int c;
int d;
int f;
int g;
};
struct Str getStr(int a, int b, int c, int d, int f, int g){
struct Str str1;
str1.a = a;
str1.b = b;
str1.c = c;
str1.d = d;
str1.f = f;
str1.g = g;
return str1;
}
- (void)viewDidLoad {
[super viewDidLoad];
struct Str str2 = getStr(1, 2, 3, 4, 5, 6);
}
- 斷點運行,以下是
viewDidLoad
函數的匯編
返回值調試-01 -
以下是getStr函數的匯編代碼
返回值調試-02
結論:如果返回值大于x0的8個字節
,也會使用棧空間
來存儲
練習
1、如果函數的參數/返回是9個呢?
struct Str{
int a;
int b;
int c;
int d;
int e;
int f;
int g;
int h;
int i;
};
struct Str getStr(int a, int b, int c, int d, int e, int f, int g, int h, int i){
struct Str str1;
str1.a = a;
str1.b = b;
str1.c = c;
str1.d = d;
str1.e = e;
str1.f = f;
str1.g = g;
str1.h = h;
str1.i = i;
return str1;
}
- (void)viewDidLoad {
[super viewDidLoad];
struct Str str2 = getStr(1, 2, 3, 4, 5, 6, 7, 8, 9);
}
-
查看
viewDidLoad
函數匯編
返回值練習-01 -
查看
getStr
函數匯編
返回值練習-02
結論:發現前8
個參數是存儲到寄存器
,第9
個參數是存儲到棧空間
2、如果結構體參數是10個呢?
struct Str{
int a;
int b;
int c;
int d;
int e;
int f;
int g;
int h;
int i;
int j;
};
struct Str getStr(int a, int b, int c, int d, int e, int f, int g, int h, int i, int j){
struct Str str1;
str1.a = a;
str1.b = b;
str1.c = c;
str1.d = d;
str1.e = e;
str1.f = f;
str1.g = g;
str1.h = h;
str1.i = i;
str1.j = j;
return str1;
}
- (void)viewDidLoad {
[super viewDidLoad];
struct Str str2 = getStr(1, 2, 3, 4, 5, 6, 7, 8, 9);
-
查看
viewDidLoad
函數匯編
返回值練習-03 -
查看
getStr
函數匯編
返回值練習-04- 其中
w0-w7
都是參數 -
x8
用于返回值參照 -
w9
用作臨時變量
- 其中
結論:前8個參數存儲到寄存器,后兩個參數存儲到棧空間
函數的局部變量
- 函數的
局部變量
放在棧
里面
分析下面代碼的匯編
int funcB(int a, int b){
int c = 6;
return a + b + c;
}
- (void)viewDidLoad {
[super viewDidLoad];
funcB(10, 20);
}
-
查看
viewDidLoad
的匯編
局部變量-01 -
查看
funcB
的匯編
局部變量-02
總結:
-
局部變量
存儲在棧
空間 - 參數的傳遞是用的寄存器,然后將寄存器的值寫入棧
如果函數有嵌套調用的情況呢?
int funcB(int a, int b){
int c = 6;
int d = funcSum(a, b, c);
return d;
}
int funcSum(int a, int b, int c){
int d = a + b + c;
printf("%d", d);
return d;
}
- (void)viewDidLoad {
[super viewDidLoad];
funcB(10, 20);
}
-
從viewDidLoad到funcB沒有任何變化
函數嵌套-01 -
來看funcB的匯編
函數嵌套-02- 參數入棧,其實本質也是對參數的一個保護
- 所以使用的參數是從棧中獲取,而不是直接通過寄存器獲取
- stur 操作4個字節時使用
標記/狀態寄存器
標記/狀態寄存器:主要用于控制程序的執行流程
引子
分析下面函數的匯編
void func(){
int a = 1;
int b = 2;
if (a == b){
printf("a == b");
}else{
printf("error");
}
}
- (void)viewDidLoad {
[super viewDidLoad];
func();
}
- 查看
func
的匯編
引子-01 - 手動更改
cpsr
的值(從1000 -> 0100)
引子-02
輸入c執行,此時的輸出是a == b
引子-03
所以,從這里可以說明,高四位(31-28)是有特殊含義的
CPSR
CPU內部的寄存器中,有一種特殊的寄存器(對于不同的處理器,個數和數據結構都可能不同)。這種寄存器在ARM中,被稱為
狀態寄存器
,即CPSR(current program status register)寄存器
CPSR和其他寄存器不一樣,其他寄存器是用來存放數據的,都是整個寄存器具有一個含義,而CPSR寄存器是按位起作用的,即它的
每一位都有專門的含義,用于記錄特定的信息
注意:
CPSR
寄存器是32位
的CPSR的
低8位(包括I、F、T和M[4:0])
稱為控制位
,程序無法修改
,除非CPU運行于特權模式下,程序才能修改控制位中間27-8是保留位,主要作用是為了升級,即更新擴展
-
N、Z、C、V
均為條件碼標志位
,它們的內容可被算術或邏輯運算的結果所改變
,并且可以決定某條指令是否被執行,意義重大!
CPSR圖示
N(Negative)標志
- CPSR的
第31位是N
,符號標志位
,它記錄相關指令執行后,其結果是否為負如果
為負
,則N=1
如果是
非負數
,則N=0
- 注意:在ARM64的指令集中,有的指令的執行時影響狀態寄存器的。例如
adds/sub/or
等,他們大都是運算指令(用于進行邏輯/算數運算)- adds 執行add運算,且會改變標志位
案例調試
查看以下代碼的匯編
void func(){
asm(
"mov w0, #0xffffffff\n"
"adds w0, w0, #0x0\n"
);
}
- (void)viewDidLoad {
[super viewDidLoad];
func();
}
-
執行
mov w0, #0xffffffff
:w0賦值-1
N調試-01
查看此時的cpsr,高4位是6(即0110)
N調試-02 -
執行
adds w0, w0, #0x0
:因為w0位負數,加上0x0后,仍為負數,所以N標志為1
N調試-03
從cpsr的值中可以看出,6變成了8,即0110 -> 1000
,N
位從0變成了1
,符合我們的預期
Z(zero)標志
- CPSR的第30位是Z,0標志位,它記錄相關指令執行后,其結果是否為0
如果
結果為0
,則Z=1
(此時能確定前面兩位為01,即N非負,為0,z為1)如果結果
不為0
,則Z=0
對于Z的值,可以這樣看,Z標記相關指令的計算結果是否為0,如果為0,則Z要記錄下”是0“
這樣的肯定信息。在計算機中1表示邏輯真,表示肯定,所以當結果為0時Z=1
(表示”結果為0“)。如果結果不為0,
則Z要記錄下”不是0“
這樣的否定信息。在計算機中0表示邏輯假,表示否定,所以當結果不為0時Z=0,表示”結果不為0“
案例調試
目的:驗證z為1時,N必為0
void func(){
asm(
"mov w0, #0x0\n"
"adds w0, w0, #0x0\n"
);
}
-
查看此時的
CPSR
Z調試-01 執行
mov w0, #0x0
和adds w0, w0, #0x0
,發現N和Z仍然是0和1修改:將adds中的0x0更改為0x1
void func(){
asm(
"mov w0, #0x0\n"
"adds w0, w0, #0x1\n"
);
}
查看CPSR,從圖中可以看出,由于為結果為非負數
,所以N為0
,同時也不為0,則Z為0
C(Carry)標志
CPSR的第
29
位是C
,進位標志位
,一般情況下,進行無符號數的運算
-
加法
運算:當運算結果產生了進位時
(無符號溢出),則C=1
,否則C=0
- 例如 1111 1111 + 1 --> 1 0000 0000,此時的1就保存在C標志位
-
減法運算(包括CMP)
:當運算時產生了借位時
(無符號數溢出),C=0
,否則C=1
C圖示- 例如 0000 0001 - 0000 0010 --> 1111 1111,
對于位數為N的無符號數來說,其對應的二進制信息的最高位,即第N-1位,就是它的最高有效位,而假想存在的第N位,就是相對于最高有效位的更高位,如下所示
進位
當兩個數相加時,有可能產生從最高有效位向更高位的進位,例如兩個32位數據0xaaaaaaaa + 0xaaaaaaaa
,將產生進位,由于這個進位值在32位中無法保存,就說這個進位值丟失了。其實CPU在運算時,并不丟棄這個進位制,而是記錄在一個特殊的寄存器的某一位上,ARM下就用C位來記錄這個進位值,例如下面的指令
mov w0,#0xaaaaaaaa;0xa 的二進制是 1010
adds w0,w0,w0; 執行后 相當于 1010 << 1 進位1(無符號溢出) 所以C標記 為 1
adds w0,w0,w0; 執行后 相當于 0101 << 1 進位0(無符號沒溢出) 所以C標記 為 0
adds w0,w0,w0; 重復上面操作
adds w0,w0,w0
-
首先將
CPSR
變成0x00000000
,然后查看執行mov w0,#0xaaaaaaaa
后的CPSR
進位調試-01 -
執行第一次
adds w0,w0,w0
:進位1,C為1
進位調試-02 -
執行第二次
adds w0,w0,w0
:無進位,C由1變成0
進位調試-03 -
執行第三次
adds w0,w0,w0
:有進位,0變成1
進位調試-04 -
執行第四次
adds w0,w0,w0
,無進位,1變成0
進位調試-05
借位
當兩個數據做減法時,有可能向更高位借位,例如,兩個32位數據0x00000000 - 0x000000ff
,將產生借位,借位后,相當于計算0x100000000 - 0x000000ff
,得到0xffffff01
這個值,由于借了一位,所以C位用來標記借位。C=0,例如下面的指令
mov w0,#0x0
subs w0,w0,#0xff
subs w0,w0,#0xff
subs w0,w0,#0xff
-
將CPSR修改為
0x00000000
,執行mov w0,#0x0
借位調試-01 -
執行第一次
subs w0,w0,#0xff
:產生了借位,所以C=0
借位調試-02 -
執行第二次
subs w0,w0,#0xff
:無借位,所以C=1
借位調試-03 -
執行第三次
subs w0,w0,#0xff
:無借位,所以C=1
借位調試-04
總結
-
函數參數
arm64中,參數是放在
x0-x7
的8個寄存器中如果是浮點數,就會用浮點數寄存器
如果
超過8個
參數就會用棧傳遞
-
函數返回值
一般函數的返回值使用
x0寄存器
保存如果返回值
大于了8個字節
(x0寄存器大小是8個字節),就會利用內存傳遞
返回值
-
函數局部變量
-
局部變量
存儲在棧
空間
-
函數的嵌套調用:會將
x29、x30
寄存器入棧保護-
狀態(標志)寄存器 - CPSR
arm64中
cpsr
寄存器(32位
)為狀態寄存器-
最高4位(28、29、30、31)為標志位
-
N
標志(負標記位)執行結果為
負數N=1
執行結果
非負數N=0
-
Z
標志(0標記位)結果
為0則Z=1
結果
非0則Z=0
-
C
標志(無符號溢出)加法:
進位 C=1,否則C=0
減法:
借位 C=0,否則C=1
-
V
標志(有符號溢出)正數+正數=
負數,則V=1
正數+負數=
正數,則V=0
-